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
5use crate::{ContextSwitchError, CpuAreaRef, CpuLocalError, CpuPin, ExecutionContextHeader};
6
7#[cfg(all(not(feature = "host-test"), target_arch = "aarch64"))]
8mod aarch64;
9#[cfg(feature = "host-test")]
10mod host;
11#[cfg(all(not(feature = "host-test"), target_arch = "loongarch64"))]
12mod loongarch64;
13#[cfg(all(
14    not(feature = "host-test"),
15    any(target_arch = "riscv32", target_arch = "riscv64")
16))]
17mod riscv;
18#[cfg(all(not(feature = "host-test"), target_arch = "x86_64"))]
19mod x86_64;
20
21#[cfg(all(not(feature = "host-test"), target_arch = "aarch64"))]
22use aarch64 as imp;
23#[cfg(feature = "host-test")]
24use host as imp;
25#[cfg(all(not(feature = "host-test"), target_arch = "loongarch64"))]
26use loongarch64 as imp;
27#[cfg(all(
28    not(feature = "host-test"),
29    any(target_arch = "riscv32", target_arch = "riscv64")
30))]
31use riscv as imp;
32#[cfg(all(not(feature = "host-test"), target_arch = "x86_64"))]
33use x86_64 as imp;
34
35#[cfg(all(
36    not(feature = "host-test"),
37    not(any(
38        target_arch = "x86_64",
39        target_arch = "aarch64",
40        target_arch = "riscv32",
41        target_arch = "riscv64",
42        target_arch = "loongarch64"
43    ))
44))]
45compile_error!("cpu-local supports x86_64, AArch64, RISC-V, and LoongArch64 only");
46
47#[derive(Clone, Copy, Debug)]
48pub(super) struct ArchitectureCurrentModel {
49    pub(super) linux_current: CurrentContextSource,
50    pub(super) unikernel_tls: CurrentContextSource,
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub(super) enum CurrentContextSource {
55    #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
56    ArchitectureRegister,
57    #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
58    RuntimeAnchor,
59}
60
61impl ArchitectureCurrentModel {
62    const fn current_context_source(self, tls_enabled: bool) -> CurrentContextSource {
63        if tls_enabled {
64            self.unikernel_tls
65        } else {
66            self.linux_current
67        }
68    }
69}
70
71/// Installs the final area of an offline CPU.
72///
73/// # Safety
74///
75/// The area must remain mapped until shutdown. The CPU must be offline with
76/// traps disabled, and no previous area may be installed on this physical CPU.
77#[doc(hidden)]
78pub unsafe fn install_cpu_area(area: CpuAreaRef) -> Result<(), CpuLocalError> {
79    imp::validate_environment()?;
80    let boot_context = area.prefix().boot_context().header();
81    let boot_pointer = boot_context as *const ExecutionContextHeader as usize;
82    // SAFETY: the caller owns the offline register installation boundary.
83    unsafe { imp::install_cpu_base(area.base(), boot_pointer) };
84    if unsafe { imp::read_cpu_base()? } != area.base() {
85        fatal_register_invariant();
86    }
87    Ok(())
88}
89
90pub(crate) fn current_area() -> Result<CpuAreaRef, CpuLocalError> {
91    let area_base = unsafe { imp::read_cpu_base()? };
92    if area_base == 0 {
93        return Err(CpuLocalError::AreaNotInstalled);
94    }
95    // SAFETY: only install_cpu_area writes the architecture-owned base, and
96    // its contract requires a shutdown-lifetime initialized area.
97    unsafe { CpuAreaRef::from_initialized_base(area_base) }
98}
99
100/// Reads the architecture CPU-area base without validating current context.
101///
102/// # Safety
103///
104/// The caller must prevent migration and context switches while using the
105/// selected CPU. The installed area must remain mapped until shutdown.
106#[inline(always)]
107pub(crate) unsafe fn current_cpu_area_base() -> Result<usize, CpuLocalError> {
108    let area_base = unsafe { imp::read_cpu_base()? };
109    if area_base == 0 {
110        return Err(CpuLocalError::AreaNotInstalled);
111    }
112    if !area_base.is_multiple_of(core::mem::align_of::<crate::CpuAreaPrefix>()) {
113        return Err(CpuLocalError::InvalidAreaBase { base: area_base });
114    }
115    Ok(area_base)
116}
117
118/// Commits the current-context source before the architecture switch tail.
119///
120/// # Safety
121///
122/// The caller must own the final IRQ-disabled context-switch boundary. `value`
123/// must identify the prepared pinned header and remain alive while current.
124pub(crate) unsafe fn commit_current_context(_area: CpuAreaRef, _value: usize) {
125    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
126        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
127        CurrentContextSource::RuntimeAnchor => _area
128            .runtime_anchor()
129            .current_context_slot()
130            .store(_value, Ordering::Release),
131        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
132        CurrentContextSource::ArchitectureRegister => {
133            core::sync::atomic::compiler_fence(Ordering::Release);
134            #[cfg(feature = "host-test")]
135            unsafe {
136                imp::write_current_context(_value)
137            };
138        }
139    }
140}
141
142/// Returns the pinned header selected by this image's sole current source.
143pub fn current_context(pin: &CpuPin<'_>) -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
144    let area = pin.area();
145    let raw = match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
146        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
147        CurrentContextSource::ArchitectureRegister => unsafe {
148            imp::read_current_context(area.base())
149        },
150        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
151        CurrentContextSource::RuntimeAnchor => area.runtime_anchor().current_context_raw(),
152    };
153    let pointer = validated_context_pointer(raw)?;
154    // SAFETY: context publication only accepts pinned headers that remain
155    // alive while current, and the caller retains the required CPU pin.
156    let context_area = unsafe { pointer.as_ref() }
157        .cpu_area()
158        .ok_or(CpuLocalError::CurrentContextMismatch)?;
159    if context_area != area {
160        return Err(CpuLocalError::CurrentContextMismatch);
161    }
162    Ok(pointer)
163}
164
165/// Reads the current header before a caller can construct its migration guard.
166///
167/// # Safety
168///
169/// The caller must keep the owning execution context alive and must not
170/// dereference the result after a context switch.
171#[doc(hidden)]
172pub unsafe fn current_context_unpinned() -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
173    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
174        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
175        CurrentContextSource::ArchitectureRegister => {
176            // The architecture current source does not require a sampled CPU
177            // area. Reading one first would race migration because this
178            // function is itself used to construct the preemption guard.
179            let register = unsafe { imp::read_current_context(0) };
180            validated_context_pointer(register)
181        }
182        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
183        CurrentContextSource::RuntimeAnchor => loop {
184            // Architectures whose current source is also the kernel TLS base
185            // keep current in the CPU runtime anchor. Retry if migration
186            // changes the area before the guard can be constructed.
187            let area = current_area()?;
188            let register = unsafe { imp::read_current_context(area.base()) };
189            if unsafe { imp::read_cpu_base()? } != area.base() {
190                continue;
191            }
192            return validated_context_pointer(register);
193        },
194    }
195}
196
197/// Reports whether `context` is the CPU area's permanent boot context.
198///
199/// Runtime layers use this distinction before they publish their first owned
200/// execution context. The check compares identities only; the boot context
201/// does not carry a runtime kind, task cookie, or consumer pointer.
202#[doc(hidden)]
203pub fn is_permanent_boot_context(
204    context: NonNull<ExecutionContextHeader>,
205) -> Result<bool, CpuLocalError> {
206    let area = current_area()?;
207    Ok(context == NonNull::from(area.prefix().boot_context().header()))
208}
209
210fn validated_context_pointer(raw: usize) -> Result<NonNull<ExecutionContextHeader>, CpuLocalError> {
211    if raw == 0 || !raw.is_multiple_of(core::mem::align_of::<ExecutionContextHeader>()) {
212        return Err(CpuLocalError::CurrentContextMismatch);
213    }
214    NonNull::new(raw as *mut ExecutionContextHeader).ok_or(CpuLocalError::CurrentContextMismatch)
215}
216
217#[cfg(feature = "host-test")]
218pub(crate) mod host_test {
219    /// Number of modeled architecture-register operations since the last reset.
220    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
221    pub struct RegisterReadCounts {
222        /// Reads of the architecture CPU-area base.
223        pub cpu_base: usize,
224        /// Reads of the selected architecture current-context source.
225        pub current_context: usize,
226        /// Complete reconstructions and identity checks of an initialized area.
227        pub initialized_area_validations: usize,
228    }
229
230    /// Resets the current host thread's modeled register-operation counters.
231    pub fn reset_register_read_counts() {
232        super::imp::reset_register_read_counts();
233    }
234
235    /// Returns the current host thread's modeled register-operation counters.
236    pub fn register_read_counts() -> RegisterReadCounts {
237        super::imp::register_read_counts()
238    }
239
240    pub(crate) fn record_initialized_area_validation() {
241        super::imp::record_initialized_area_validation();
242    }
243}
244
245#[cfg(all(test, feature = "host-test"))]
246mod tests {
247    use core::mem::MaybeUninit;
248
249    use super::*;
250    use crate::{CpuAreaPrefix, CpuIndex};
251
252    fn modeled_area(cpu_index: usize) -> CpuAreaRef {
253        let storage = Box::leak(Box::new(MaybeUninit::<CpuAreaPrefix>::uninit()));
254        let base = storage.as_mut_ptr() as usize;
255        storage.write(
256            CpuAreaPrefix::initialize(CpuIndex::try_from(cpu_index).unwrap(), base).unwrap(),
257        );
258        // SAFETY: the initialized fixture is leaked for the process lifetime.
259        unsafe { CpuAreaRef::from_initialized_base(base) }.unwrap()
260    }
261
262    #[test]
263    fn independent_current_register_ignores_kernel_tls_feature() {
264        let independent = ArchitectureCurrentModel {
265            linux_current: CurrentContextSource::ArchitectureRegister,
266            unikernel_tls: CurrentContextSource::ArchitectureRegister,
267        };
268        assert_eq!(
269            independent.current_context_source(false),
270            CurrentContextSource::ArchitectureRegister,
271        );
272        assert_eq!(
273            independent.current_context_source(true),
274            CurrentContextSource::ArchitectureRegister,
275        );
276    }
277
278    #[test]
279    fn aliased_current_register_follows_kernel_tls_feature() {
280        let aliased = ArchitectureCurrentModel {
281            linux_current: CurrentContextSource::ArchitectureRegister,
282            unikernel_tls: CurrentContextSource::RuntimeAnchor,
283        };
284        assert_eq!(
285            aliased.current_context_source(false),
286            CurrentContextSource::ArchitectureRegister,
287        );
288        assert_eq!(
289            aliased.current_context_source(true),
290            CurrentContextSource::RuntimeAnchor,
291        );
292    }
293
294    #[test]
295    fn current_context_unpinned_survives_migration_during_bootstrap_read() {
296        let first = modeled_area(0);
297        let second = modeled_area(1);
298        let first_boot = first.prefix().boot_context().header();
299
300        // SAFETY: this host thread serially owns both leaked CPU fixtures.
301        unsafe { imp::install_cpu_base(first.base(), first_boot as *const _ as usize) };
302        imp::migrate_on_next_current_read(second.base());
303
304        assert_eq!(
305            // SAFETY: both boot headers have process-lifetime storage.
306            unsafe { current_context_unpinned() },
307            if cfg!(feature = "tls") {
308                Ok(NonNull::from(second.prefix().boot_context().header()))
309            } else {
310                Ok(NonNull::from(first_boot))
311            },
312        );
313    }
314
315    #[test]
316    fn current_context_unpinned_rejects_an_uninstalled_host_area() {
317        let rejected = std::thread::spawn(move || {
318            // SAFETY: the fresh host thread has no installed CPU area, so no
319            // execution-context pointer can be returned.
320            unsafe { current_context_unpinned() }.is_err()
321        })
322        .join()
323        .expect("host current-context probe panicked");
324
325        assert!(rejected);
326    }
327
328    #[test]
329    fn permanent_boot_context_is_classified_by_area_identity() {
330        let area = modeled_area(0);
331        let boot = area.prefix().boot_context().header();
332        let runtime_context = Box::pin(ExecutionContextHeader::new());
333
334        // SAFETY: this host thread serially owns the leaked CPU fixture.
335        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
336
337        assert_eq!(is_permanent_boot_context(NonNull::from(boot)), Ok(true));
338        assert_eq!(
339            is_permanent_boot_context(runtime_context.as_ref().as_non_null()),
340            Ok(false)
341        );
342    }
343
344    #[test]
345    #[cfg(not(feature = "tls"))]
346    fn architecture_current_is_authoritative_when_anchor_is_stale() {
347        let area = modeled_area(0);
348        let boot = area.prefix().boot_context().header();
349        let next = Box::pin(ExecutionContextHeader::new());
350
351        // SAFETY: this host thread serially owns the modeled CPU and both
352        // process-lifetime context headers.
353        unsafe { imp::install_cpu_base(area.base(), boot as *const _ as usize) };
354        unsafe {
355            crate::with_cpu_pin(|pin| {
356                let next_epoch = next.as_ref().bind_cpu(area).unwrap();
357                imp::set_architecture_current(next.as_ref().as_non_null().as_ptr() as usize);
358
359                assert_eq!(current_context(pin), Ok(next.as_ref().as_non_null()));
360
361                imp::set_architecture_current(0);
362                next.as_ref().unbind_cpu(next_epoch).unwrap();
363            })
364        }
365        .unwrap();
366    }
367}
368
369/// Binds and publishes the first execution context on an offline CPU.
370///
371/// # Safety
372///
373/// The CPU must remain offline and trap-free. `header` must stay pinned and
374/// alive until its owner replaces it through a prepared switch.
375#[doc(hidden)]
376pub unsafe fn install_bootstrap_context(
377    pin: &CpuPin<'_>,
378    header: Pin<&ExecutionContextHeader>,
379) -> Result<(), ContextSwitchError> {
380    let epoch = unsafe { header.bind_cpu(pin.area()) }?;
381    let pointer = header.as_non_null().as_ptr() as usize;
382    match imp::CURRENT_MODEL.current_context_source(cfg!(feature = "tls")) {
383        #[cfg(not(all(target_arch = "aarch64", not(feature = "host-test"))))]
384        CurrentContextSource::RuntimeAnchor => unsafe {
385            commit_current_context(pin.area(), pointer)
386        },
387        #[cfg(any(not(target_arch = "x86_64"), feature = "host-test"))]
388        CurrentContextSource::ArchitectureRegister => unsafe {
389            imp::write_current_context(pointer)
390        },
391    }
392    if current_context(pin) != Ok(header.as_non_null()) {
393        // The register is already committed, so continuing would make all
394        // later Rust execution unsound. Rollback is intentionally impossible.
395        let _ = epoch;
396        fatal_register_invariant();
397    }
398    Ok(())
399}
400
401/// Reads execution-context-owned kernel TLS under an explicit CPU pin.
402#[cfg(feature = "tls")]
403pub fn kernel_tls(_pin: &CpuPin<'_>) -> usize {
404    unsafe { imp::read_kernel_tls() }
405}
406
407/// Installs execution-context-owned kernel TLS at an offline bootstrap boundary.
408///
409/// # Safety
410///
411/// The caller must own the offline CPU or IRQ-disabled final context switch, and
412/// `value` must remain a valid TLS base for the installed execution context.
413#[cfg(feature = "tls")]
414#[doc(hidden)]
415pub unsafe fn install_kernel_tls(_pin: &CpuPin<'_>, value: usize) {
416    unsafe { imp::write_kernel_tls(value) };
417}
418
419#[cold]
420#[inline(never)]
421pub(crate) fn fatal_register_invariant() -> ! {
422    panic!("CPU-local register commit did not retain the validated state")
423}
424
425#[cfg(all(target_arch = "x86_64", not(feature = "host-test")))]
426#[inline(always)]
427pub(crate) unsafe fn enter_x86_preemption() {
428    unsafe { imp::enter_preemption() };
429}