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