Skip to main content

cpu_local/register/
mod.rs

1//! Architecture register primitives and shared validation.
2
3use core::{pin::Pin, ptr::NonNull, sync::atomic::Ordering};
4
5#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
6use crate::preempt::PreemptionState;
7use crate::{
8    ContextSwitchError, CpuAreaRef, CpuIndex, CpuLocalError, CpuPin, ExecutionContextHeader,
9    preempt::PreemptionSnapshot,
10};
11
12#[cfg(all(not(feature = "host-test"), target_arch = "aarch64"))]
13mod aarch64;
14#[cfg(feature = "host-test")]
15mod host;
16#[cfg(all(not(feature = "host-test"), target_arch = "loongarch64"))]
17mod loongarch64;
18#[cfg(all(
19    not(feature = "host-test"),
20    any(target_arch = "riscv32", target_arch = "riscv64")
21))]
22mod riscv;
23#[cfg(all(not(feature = "host-test"), target_arch = "x86_64"))]
24mod x86_64;
25
26#[cfg(all(not(feature = "host-test"), target_arch = "aarch64"))]
27use aarch64 as imp;
28#[cfg(feature = "host-test")]
29use host as imp;
30#[cfg(all(not(feature = "host-test"), target_arch = "loongarch64"))]
31use loongarch64 as imp;
32#[cfg(all(
33    not(feature = "host-test"),
34    any(target_arch = "riscv32", target_arch = "riscv64")
35))]
36use riscv as imp;
37#[cfg(all(not(feature = "host-test"), target_arch = "x86_64"))]
38use x86_64 as imp;
39
40#[cfg(all(
41    not(feature = "host-test"),
42    not(any(
43        target_arch = "x86_64",
44        target_arch = "aarch64",
45        target_arch = "riscv32",
46        target_arch = "riscv64",
47        target_arch = "loongarch64"
48    ))
49))]
50compile_error!("cpu-local supports x86_64, AArch64, RISC-V, and LoongArch64 only");
51
52#[derive(Clone, Copy, Debug)]
53pub(super) struct ArchitectureCurrentModel {
54    pub(super) linux_current: CurrentContextSource,
55    pub(super) unikernel_tls: CurrentContextSource,
56}
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub(super) enum CurrentContextSource {
60    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
61    ArchitectureRegister,
62    #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
63    RuntimeAnchor,
64}
65
66impl ArchitectureCurrentModel {
67    const fn current_context_source(self, tls_enabled: bool) -> CurrentContextSource {
68        if tls_enabled {
69            self.unikernel_tls
70        } else {
71            self.linux_current
72        }
73    }
74}
75
76/// Architecture register boundary for current-state observations.
77///
78/// The execution-context implementation is the portable default. Backends may
79/// override it only when their current preemption owner has a cheaper native
80/// representation, while preserving the same advisory snapshot semantics.
81pub(super) trait ArchitectureRegisterBackend {
82    #[inline(always)]
83    fn current_cpu_index() -> Result<CpuIndex, CpuLocalError> {
84        default_current_cpu_index()
85    }
86
87    #[inline(always)]
88    fn current_preemption_snapshot() -> Result<PreemptionSnapshot, CpuLocalError> {
89        default_current_preemption_snapshot()
90    }
91}
92
93fn default_current_cpu_index() -> Result<CpuIndex, CpuLocalError> {
94    Ok(current_area()?.cpu_index())
95}
96
97/// Installs the final area of an offline CPU.
98///
99/// # Safety
100///
101/// The area must remain mapped until shutdown. The CPU must be offline with
102/// traps disabled, and no previous area may be installed on this physical CPU.
103#[doc(hidden)]
104pub unsafe fn install_cpu_area(area: CpuAreaRef) -> Result<(), CpuLocalError> {
105    imp::validate_environment()?;
106    let boot_context = area.prefix().boot_context().header();
107    let boot_pointer = boot_context as *const ExecutionContextHeader as usize;
108    // SAFETY: the caller owns the offline register installation boundary.
109    unsafe { imp::install_cpu_base(area.base(), boot_pointer) };
110    if unsafe { imp::read_cpu_base()? } != area.base() {
111        fatal_register_invariant();
112    }
113    Ok(())
114}
115
116pub(crate) fn current_area() -> Result<CpuAreaRef, CpuLocalError> {
117    let area_base = unsafe { imp::read_cpu_base()? };
118    if area_base == 0 {
119        return Err(CpuLocalError::AreaNotInstalled);
120    }
121    // SAFETY: only install_cpu_area writes the architecture-owned base after
122    // validating it, and its contract keeps that area mapped until shutdown.
123    Ok(unsafe { CpuAreaRef::from_installed_base(area_base) })
124}
125
126/// Reads the logical CPU index selected by the architecture register.
127///
128/// # Safety
129///
130/// Callers must retain the migration exclusion that makes the selected CPU
131/// area stable for the complete observation.
132#[inline(always)]
133pub unsafe fn current_cpu_index() -> Result<CpuIndex, CpuLocalError> {
134    imp::Backend::current_cpu_index()
135}
136
137/// Reads the architecture CPU-area base without validating current context.
138///
139/// # Safety
140///
141/// The caller must prevent migration and context switches while using the
142/// selected CPU. The installed area must remain mapped until shutdown.
143#[inline(always)]
144pub(crate) unsafe fn current_cpu_area_base() -> Result<usize, CpuLocalError> {
145    let area_base = unsafe { imp::read_cpu_base()? };
146    if area_base == 0 {
147        return Err(CpuLocalError::AreaNotInstalled);
148    }
149    if !area_base.is_multiple_of(core::mem::align_of::<crate::CpuAreaPrefix>()) {
150        return Err(CpuLocalError::InvalidAreaBase { base: area_base });
151    }
152    Ok(area_base)
153}
154
155#[inline(always)]
156pub(crate) fn current_preemption_snapshot() -> Result<PreemptionSnapshot, CpuLocalError> {
157    imp::Backend::current_preemption_snapshot()
158}
159
160#[inline(always)]
161fn default_current_preemption_snapshot() -> Result<PreemptionSnapshot, CpuLocalError> {
162    let current = unsafe { current_context_unpinned()? };
163    // SAFETY: the architecture current source identifies this executing
164    // context. An interrupt may migrate it before the dereference, but this
165    // instruction stream can resume only as the same live context, whose
166    // preemption word migrates with it.
167    Ok(unsafe { current.as_ref() }.preemption_state().snapshot())
168}
169
170/// Commits the current-context source before the architecture switch tail.
171///
172/// # Safety
173///
174/// The caller must own the final IRQ-disabled context-switch boundary. `value`
175/// must identify the prepared pinned header and remain alive while current.
176pub(crate) unsafe fn commit_current_context(_area: CpuAreaRef, _value: usize) {
177    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
178        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
179        CurrentContextSource::RuntimeAnchor => _area
180            .runtime_anchor()
181            .current_context_slot()
182            .store(_value, Ordering::Release),
183        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
184        CurrentContextSource::ArchitectureRegister => {
185            core::sync::atomic::compiler_fence(Ordering::Release);
186            #[cfg(feature = "host-test")]
187            unsafe {
188                imp::write_current_context(_value)
189            };
190        }
191    }
192}
193
194/// Returns the pinned header selected by this image's sole current source.
195///
196/// Context installation and switch preparation validate the header's CPU
197/// binding before publication. Like Linux `current`, this hot lookup trusts
198/// that published invariant while the caller's pin prevents migration.
199pub fn current_context(pin: &CpuPin<'_>) -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
200    let area = pin.area();
201    let raw = match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
202        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
203        CurrentContextSource::ArchitectureRegister => unsafe {
204            imp::read_current_context(area.base())
205        },
206        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
207        CurrentContextSource::RuntimeAnchor => area.runtime_anchor().current_context_raw(),
208    };
209    validated_context_pointer(raw)
210}
211
212/// Reads the current header before a caller can construct its migration guard.
213///
214/// # Safety
215///
216/// The caller must keep the owning execution context alive and must not
217/// dereference the result after a context switch.
218#[doc(hidden)]
219pub unsafe fn current_context_unpinned() -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
220    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
221        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
222        CurrentContextSource::ArchitectureRegister => {
223            // The architecture current source does not require a sampled CPU
224            // area. Reading one first would race migration because this
225            // function is itself used to construct the preemption guard.
226            let register = unsafe { imp::read_current_context(0) };
227            validated_context_pointer(register)
228        }
229        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
230        CurrentContextSource::RuntimeAnchor => {
231            #[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
232            {
233                // GS selects the CPU-owned current slot, but the value names
234                // the pinned execution context. An interrupt may migrate this
235                // instruction stream after the read; when it resumes, the
236                // same context and immutable publication are still live.
237                validated_context_pointer(unsafe { imp::read_current_context(0) })
238            }
239            #[cfg(not(all(target_arch = "x86_64", not(feature = "host-test"))))]
240            loop {
241                // Other anchor-backed architectures require the sampled area
242                // to remain stable until their current slot has been read.
243                let area = current_area()?;
244                let register = unsafe { imp::read_current_context(area.base()) };
245                if unsafe { imp::read_cpu_base()? } != area.base() {
246                    continue;
247                }
248                return validated_context_pointer(register);
249            }
250        }
251    }
252}
253
254/// Reports whether `context` is the current CPU area's permanent boot context.
255///
256/// Runtime layers use this area-identity check before they publish their first
257/// owned execution context. Hot readers that already own a live context header
258/// can inspect [`ExecutionContextHeader::is_permanent_boot_context`] directly.
259#[doc(hidden)]
260pub fn is_permanent_boot_context(
261    context: NonNull<ExecutionContextHeader>,
262) -> Result<bool, CpuLocalError> {
263    let area = current_area()?;
264    Ok(context == NonNull::from(area.prefix().boot_context().header()))
265}
266
267fn validated_context_pointer(raw: usize) -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
268    if raw == 0 || !raw.is_multiple_of(core::mem::align_of::<ExecutionContextHeader>()) {
269        return Err(CpuLocalError::CurrentContextMismatch);
270    }
271    NonNull::new(raw as *mut ExecutionContextHeader).ok_or(CpuLocalError::CurrentContextMismatch)
272}
273
274#[cfg(feature = "host-test")]
275pub(crate) mod host_test {
276    /// Number of modeled architecture-register operations since the last reset.
277    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
278    pub struct RegisterReadCounts {
279        /// Reads of the architecture CPU-area base.
280        pub cpu_base: usize,
281        /// Reads of the selected architecture current-context source.
282        pub current_context: usize,
283        /// Stable observations of an execution context's CPU binding.
284        pub binding_observations: usize,
285        /// Complete reconstructions and identity checks of an initialized area.
286        pub initialized_area_validations: usize,
287    }
288
289    /// Resets the current host thread's modeled register-operation counters.
290    pub fn reset_register_read_counts() {
291        super::imp::reset_register_read_counts();
292    }
293
294    /// Returns the current host thread's modeled register-operation counters.
295    pub fn register_read_counts() -> RegisterReadCounts {
296        super::imp::register_read_counts()
297    }
298
299    pub(crate) fn record_initialized_area_validation() {
300        super::imp::record_initialized_area_validation();
301    }
302
303    pub(crate) fn record_binding_observation() {
304        super::imp::record_binding_observation();
305    }
306}
307
308#[cfg(all(test, feature = "host-test"))]
309mod tests {
310    use core::mem::MaybeUninit;
311
312    use super::*;
313    use crate::{CpuAreaPrefix, CpuIndex};
314
315    fn modeled_area(cpu_index: usize) -> CpuAreaRef {
316        let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
317        let base = storage.as_mut_ptr() as usize;
318        storage.write(
319            CpuAreaPrefix::initialize(CpuIndex::try_from(cpu_index).unwrap(), base).unwrap(),
320        );
321        // SAFETY: the initialized fixture is leaked for the process lifetime.
322        unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap()
323    }
324
325    #[test]
326    fn independent_current_register_ignores_kernel_tls_feature() {
327        let independent = ArchitectureCurrentModel {
328            linux_current: CurrentContextSource::ArchitectureRegister,
329            unikernel_tls: CurrentContextSource::ArchitectureRegister,
330        };
331        assert_eq!(
332            independent.current_context_source(false),
333            CurrentContextSource::ArchitectureRegister,
334        );
335        assert_eq!(
336            independent.current_context_source(true),
337            CurrentContextSource::ArchitectureRegister,
338        );
339    }
340
341    #[test]
342    fn aliased_current_register_follows_kernel_tls_feature() {
343        let aliased = ArchitectureCurrentModel {
344            linux_current: CurrentContextSource::ArchitectureRegister,
345            unikernel_tls: CurrentContextSource::RuntimeAnchor,
346        };
347        assert_eq!(
348            aliased.current_context_source(false),
349            CurrentContextSource::ArchitectureRegister,
350        );
351        assert_eq!(
352            aliased.current_context_source(true),
353            CurrentContextSource::RuntimeAnchor,
354        );
355    }
356
357    #[test]
358    fn current_context_unpinned_survives_migration_during_bootstrap_read() {
359        let first = modeled_area(0);
360        let second = modeled_area(1);
361        let first_boot = first.prefix().boot_context().header();
362
363        // SAFETY: this host thread serially owns both leaked CPU fixtures.
364        unsafe { imp::install_cpu_base(first.base(), first_boot as *const _ as usize) };
365        imp::migrate_on_next_current_read(second.base());
366
367        assert_eq!(
368            // SAFETY: both boot headers have process-lifetime storage.
369            unsafe { current_context_unpinned() },
370            if cfg!(feature = "tls") {
371                Ok(NonNull::from(second.prefix().boot_context().header()))
372            } else {
373                Ok(NonNull::from(first_boot))
374            },
375        );
376    }
377
378    #[test]
379    fn current_context_unpinned_rejects_an_uninstalled_host_area() {
380        let rejected = std::thread::spawn(move || {
381            // SAFETY: the fresh host thread has no installed CPU area, so no
382            // execution-context pointer can be returned.
383            unsafe { current_context_unpinned() }.is_err()
384        })
385        .join()
386        .expect("host current-context probe panicked");
387
388        assert!(rejected);
389    }
390
391    #[test]
392    fn permanent_boot_context_is_classified_by_area_identity() {
393        let area = modeled_area(0);
394        let boot = area.prefix().boot_context().header();
395        let runtime_context = Box::pin(ExecutionContextHeader::new());
396
397        // SAFETY: this host thread serially owns the leaked CPU fixture.
398        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
399
400        assert!(boot.is_permanent_boot_context());
401        assert!(!runtime_context.is_permanent_boot_context());
402        assert_eq!(is_permanent_boot_context(NonNull::from(boot)), Ok(true));
403        assert_eq!(
404            is_permanent_boot_context(runtime_context.as_ref().as_non_null()),
405            Ok(false)
406        );
407    }
408
409    #[test]
410    fn installed_current_area_reuses_install_time_identity_validation() {
411        let area = modeled_area(0);
412        let boot = area.prefix().boot_context().header();
413
414        // SAFETY: this host thread serially owns the leaked CPU fixture.
415        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
416        host_test::reset_register_read_counts();
417
418        assert_eq!(current_area(), Ok(area));
419        assert_eq!(
420            host_test::register_read_counts(),
421            host_test::RegisterReadCounts {
422                cpu_base: 1,
423                current_context: 0,
424                binding_observations: 0,
425                initialized_area_validations: 0,
426            },
427            "a live installed base must not repeat shutdown-lifetime identity validation",
428        );
429    }
430
431    #[test]
432    fn pin_construction_trusts_published_area_and_context_identity() {
433        let area = modeled_area(0);
434        let boot = area.prefix().boot_context().header();
435
436        // SAFETY: this host thread serially owns the leaked CPU fixture.
437        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
438        host_test::reset_register_read_counts();
439
440        // SAFETY: the host fixture cannot migrate or switch during the call.
441        unsafe { crate::with_cpu_pin(|_| ()) }.unwrap();
442
443        assert_eq!(
444            host_test::register_read_counts().initialized_area_validations,
445            0,
446            "pin construction must reuse the area identity validated before installation",
447        );
448        assert_eq!(
449            host_test::register_read_counts().binding_observations,
450            0,
451            "pin construction must trust the current binding published by the switch boundary",
452        );
453        assert_eq!(
454            host_test::register_read_counts().current_context,
455            0,
456            "pin construction must not re-read the current context after publication",
457        );
458    }
459
460    #[test]
461    fn backend_default_observes_the_current_execution_context() {
462        let area = modeled_area(0);
463        let boot = area.prefix().boot_context().header();
464
465        // SAFETY: this host thread serially owns the leaked CPU fixture.
466        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
467
468        let snapshot = current_preemption_snapshot()
469            .expect("host backend default should observe its boot context");
470        assert_eq!(snapshot.depth(), 1);
471        assert!(!snapshot.is_pending());
472    }
473
474    #[test]
475    #[cfg(not(feature = "tls"))]
476    fn architecture_current_is_authoritative_when_anchor_is_stale() {
477        let area = modeled_area(0);
478        let boot = area.prefix().boot_context().header();
479        let next = Box::pin(ExecutionContextHeader::new());
480
481        // SAFETY: this host thread serially owns the modeled CPU and both
482        // process-lifetime context headers.
483        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
484        unsafe {
485            crate::with_cpu_pin(|pin| {
486                let next_epoch = next.as_ref().bind_cpu(area).unwrap();
487                imp::set_architecture_current(next.as_ref().as_non_null().as_ptr() as usize);
488
489                assert_eq!(current_context(pin), Ok(next.as_ref().as_non_null()));
490
491                imp::set_architecture_current(0);
492                next.as_ref().unbind_cpu(next_epoch).unwrap();
493            })
494        }
495        .unwrap();
496    }
497}
498
499/// Binds and publishes the first execution context on an offline CPU.
500///
501/// # Safety
502///
503/// The CPU must remain offline and trap-free. `header` must stay pinned and
504/// alive until its owner replaces it through a prepared switch.
505#[doc(hidden)]
506pub unsafe fn install_bootstrap_context(
507    pin: &CpuPin<'_>,
508    header: Pin<&ExecutionContextHeader>,
509) -> Result<(), ContextSwitchError> {
510    let epoch = unsafe { header.bind_cpu(pin.area()) }?;
511    let pointer = header.as_non_null().as_ptr() as usize;
512    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
513        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
514        CurrentContextSource::RuntimeAnchor => unsafe {
515            commit_current_context(pin.area(), pointer)
516        },
517        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
518        CurrentContextSource::ArchitectureRegister => unsafe {
519            imp::write_current_context(pointer)
520        },
521    }
522    if current_context(pin) != Ok(header.as_non_null()) || !header.is_bound_to(pin.area()) {
523        // The register is already committed, so continuing would make all
524        // later Rust execution unsound. Rollback is intentionally impossible.
525        let _ = epoch;
526        fatal_register_invariant();
527    }
528    Ok(())
529}
530
531/// Reads execution-context-owned kernel TLS under an explicit CPU pin.
532#[cfg(feature = "tls")]
533pub fn kernel_tls(_pin: &CpuPin<'_>) -> usize {
534    unsafe { imp::read_kernel_tls() }
535}
536
537/// Installs execution-context-owned kernel TLS at an offline bootstrap boundary.
538///
539/// # Safety
540///
541/// The caller must own the offline CPU or IRQ-disabled final context switch, and
542/// `value` must remain a valid TLS base for the installed execution context.
543#[cfg(feature = "tls")]
544#[doc(hidden)]
545pub unsafe fn install_kernel_tls(_pin: &CpuPin<'_>, value: usize) {
546    unsafe { imp::write_kernel_tls(value) };
547}
548
549#[cold]
550#[inline(never)]
551pub(crate) fn fatal_register_invariant() -> ! {
552    panic!("CPU-local register commit did not retain the validated state")
553}
554
555#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
556#[inline(always)]
557pub(crate) unsafe fn enter_x86_preemption() {
558    unsafe { imp::enter_preemption() };
559}
560
561#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
562#[inline(always)]
563pub(crate) unsafe fn current_x86_preemption_state() -> &'static PreemptionState {
564    // SAFETY: the caller has already incremented the GS-selected preemption
565    // word and retains that exclusion through the returned reference.
566    unsafe { imp::current_preemption_state() }
567}
568
569/// Compares one CPU-owned preemption transition without cross-CPU locking.
570///
571/// # Safety
572///
573/// `state` must be the live owner retained by a positive preemption depth on
574/// the current CPU. No remote CPU may access the word during this operation.
575#[cfg(all(test, target_arch = "x86_64", not(feature = "host-test")))]
576#[inline(always)]
577pub(crate) unsafe fn compare_exchange_x86_preemption_state(
578    state: &PreemptionState,
579    current: u32,
580    next: u32,
581) -> bool {
582    // SAFETY: the caller supplies the CPU-local ownership contract forwarded
583    // by this architecture boundary.
584    unsafe { imp::compare_exchange_preemption_state(state, current, next) }
585}
586
587#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
588#[inline(always)]
589pub(crate) unsafe fn read_current_x86_preemption_state_raw() -> u32 {
590    // SAFETY: forwarded by the caller's live preemption token.
591    unsafe { imp::read_preemption_state() }
592}
593
594#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
595#[inline(always)]
596pub(crate) unsafe fn compare_exchange_current_x86_preemption_state(
597    current: u32,
598    next: u32,
599) -> bool {
600    // SAFETY: forwarded by the caller's positive local preemption depth.
601    unsafe { imp::compare_exchange_current_preemption_state(current, next) }
602}
603
604#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
605#[inline(always)]
606pub(crate) unsafe fn decrement_current_x86_preemption_state() {
607    // SAFETY: forwarded by the caller's nested local preemption depth.
608    unsafe { imp::decrement_current_preemption_state() }
609}