Skip to main content

ax_runtime/thread/
address_space.rs

1//! Runtime-owned address-space tokens and per-CPU active-mm state.
2
3use alloc::{boxed::Box, sync::Arc};
4#[cfg(feature = "uspace")]
5use core::sync::atomic::AtomicBool;
6#[cfg(feature = "qperf-metrics")]
7use core::sync::atomic::AtomicU64;
8use core::{
9    marker::PhantomData,
10    mem::align_of,
11    ptr,
12    sync::atomic::{AtomicU32, AtomicUsize, Ordering},
13};
14
15use ax_hal::percpu::CpuPin;
16use ax_memory_addr::PhysAddr;
17use ax_task::{
18    runtime::{
19        RuntimeStatus,
20        resource::{
21            AddressSpaceDestroyOutcome, AddressSpaceHandle, AddressSpaceMembarrierId,
22            AddressSpaceMembarrierState, AddressSpaceReclaimArmOutcome, AddressSpaceToken,
23            MembarrierRegistration, MembarrierRegistrationPhase,
24        },
25    },
26    thread::TaskError,
27};
28
29use super::mm_activation::UserAddressSpaceOwner;
30#[cfg(feature = "uspace")]
31use super::mm_activation::{AddressSpaceSwitchProof, SchedulerAddressSpaceActivation};
32#[cfg(feature = "uspace")]
33use super::with_current_cpu_pin;
34
35/// OS-owned lifetime anchor retained by a scheduler address-space token.
36trait TaskAddressSpaceOwner: Send + Sync {
37    /// Releases ownership that follows the attached task while retaining any
38    /// storage needed by CPUs that still carry the address space as lazy mm.
39    fn detach_from_task(&self);
40
41    #[cfg(feature = "uspace")]
42    fn prepare_activation(
43        &self,
44        _cpu: usize,
45    ) -> Result<Option<SchedulerAddressSpaceActivation>, RuntimeStatus> {
46        Ok(None)
47    }
48}
49
50struct ManagedTaskAddressSpaceOwner<T>(T);
51
52impl<T: UserAddressSpaceOwner> TaskAddressSpaceOwner for ManagedTaskAddressSpaceOwner<T> {
53    fn detach_from_task(&self) {
54        self.0.detach_from_task();
55    }
56
57    #[cfg(feature = "uspace")]
58    fn prepare_activation(
59        &self,
60        cpu: usize,
61    ) -> Result<Option<SchedulerAddressSpaceActivation>, RuntimeStatus> {
62        self.0
63            .prepare_activation(cpu)
64            .map(Some)
65            .map_err(|_| RuntimeStatus::InvalidHandle)
66    }
67}
68
69struct RetainedTaskAddressSpaceOwner<T>(T);
70
71impl<T: Send + Sync> TaskAddressSpaceOwner for RetainedTaskAddressSpaceOwner<T> {
72    fn detach_from_task(&self) {}
73}
74
75struct DetachableTaskAddressSpaceOwner<T> {
76    owner: T,
77    detached: core::sync::atomic::AtomicBool,
78    detach: fn(&T),
79}
80
81impl<T: Send + Sync> TaskAddressSpaceOwner for DetachableTaskAddressSpaceOwner<T> {
82    fn detach_from_task(&self) {
83        if !self.detached.swap(true, Ordering::AcqRel) {
84            (self.detach)(&self.owner);
85        }
86    }
87}
88
89struct RuntimeAddressSpace {
90    /// Number of runtime tokens currently borrowing this address-space owner.
91    ///
92    /// The CPU footprint itself belongs to `cpu_state`; same-mm switches keep
93    /// the existing lease even when the selected task token changes.
94    active_leases: AtomicUsize,
95    reclaim_waiting: AtomicUsize,
96    cpu_state: Arc<AddressSpaceCpuState>,
97    _owner: Box<dyn TaskAddressSpaceOwner>,
98}
99
100#[cfg(feature = "uspace")]
101impl RuntimeAddressSpace {
102    fn root(&self) -> usize {
103        self.cpu_state.root()
104    }
105}
106
107const _: () = assert!(crate::CPU_CAPACITY <= usize::BITS as usize);
108
109#[cfg(feature = "qperf-metrics")]
110static ACTIVE_MM_SAME_ACTIVATIONS: AtomicU64 = AtomicU64::new(0);
111#[cfg(feature = "qperf-metrics")]
112static ACTIVE_MM_DIFFERENT_ACTIVATIONS: AtomicU64 = AtomicU64::new(0);
113#[cfg(feature = "qperf-metrics")]
114static ACTIVE_MM_KERNEL_LAZY_ACTIVATIONS: AtomicU64 = AtomicU64::new(0);
115#[cfg(feature = "qperf-metrics")]
116static ACTIVE_MM_HARDWARE_ROOT_WRITES: AtomicU64 = AtomicU64::new(0);
117#[cfg(feature = "qperf-metrics")]
118static ACTIVE_MM_LEASE_ACTIVATIONS: AtomicU64 = AtomicU64::new(0);
119#[cfg(feature = "qperf-metrics")]
120static ACTIVE_MM_LEASE_DEACTIVATIONS: AtomicU64 = AtomicU64::new(0);
121#[cfg(feature = "qperf-metrics")]
122static ACTIVE_MM_RECLAIM_READY: AtomicU64 = AtomicU64::new(0);
123#[cfg(feature = "qperf-metrics")]
124static ACTIVE_MM_RECLAIM_DESTROYED: AtomicU64 = AtomicU64::new(0);
125
126#[cfg(feature = "qperf-metrics")]
127#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
128pub(super) struct QperfAddressSpaceMetricsSnapshot {
129    pub(super) same_activations: u64,
130    pub(super) different_activations: u64,
131    pub(super) kernel_lazy_activations: u64,
132    pub(super) hardware_root_writes: u64,
133    pub(super) lease_activations: u64,
134    pub(super) lease_deactivations: u64,
135    pub(super) reclaim_ready: u64,
136    pub(super) reclaim_destroyed: u64,
137}
138
139#[cfg(feature = "qperf-metrics")]
140pub(super) fn qperf_address_space_metrics_snapshot() -> QperfAddressSpaceMetricsSnapshot {
141    QperfAddressSpaceMetricsSnapshot {
142        same_activations: ACTIVE_MM_SAME_ACTIVATIONS.load(Ordering::Relaxed),
143        different_activations: ACTIVE_MM_DIFFERENT_ACTIVATIONS.load(Ordering::Relaxed),
144        kernel_lazy_activations: ACTIVE_MM_KERNEL_LAZY_ACTIVATIONS.load(Ordering::Relaxed),
145        hardware_root_writes: ACTIVE_MM_HARDWARE_ROOT_WRITES.load(Ordering::Relaxed),
146        lease_activations: ACTIVE_MM_LEASE_ACTIVATIONS.load(Ordering::Relaxed),
147        lease_deactivations: ACTIVE_MM_LEASE_DEACTIVATIONS.load(Ordering::Relaxed),
148        reclaim_ready: ACTIVE_MM_RECLAIM_READY.load(Ordering::Relaxed),
149        reclaim_destroyed: ACTIVE_MM_RECLAIM_DESTROYED.load(Ordering::Relaxed),
150    }
151}
152
153/// Shared CPU-footprint state for one hardware page-table root.
154///
155/// Every scheduler token for threads sharing one OS address space must carry
156/// the same tracker. The runtime publishes a CPU bit before installing the
157/// root and clears it only after replacing the hardware root, so page-table
158/// mutation can target every CPU that may retain a translation.
159pub struct AddressSpaceCpuState {
160    root: usize,
161    active_mask: ActiveCpuMask,
162    membarrier_bits: AtomicU32,
163}
164
165enum ActiveCpuMask {
166    Runtime(AtomicUsize),
167    Mm(Arc<AtomicUsize>),
168}
169
170impl AddressSpaceCpuState {
171    /// Creates inactive runtime state permanently bound to one `mm` root.
172    pub fn new(root: PhysAddr) -> Self {
173        Self {
174            root: root.as_usize(),
175            active_mask: ActiveCpuMask::Runtime(AtomicUsize::new(0)),
176            membarrier_bits: AtomicU32::new(0),
177        }
178    }
179
180    /// Shares the MM's authoritative footprint; only its activation leases may
181    /// publish or clear bits. Runtime tokens retain separate reclamation counts.
182    pub fn with_mm_active_mask(root: PhysAddr, active_mask: Arc<AtomicUsize>) -> Self {
183        Self {
184            root: root.as_usize(),
185            active_mask: ActiveCpuMask::Mm(active_mask),
186            membarrier_bits: AtomicU32::new(0),
187        }
188    }
189
190    fn matches_root(&self, root: PhysAddr) -> bool {
191        self.root() == root.as_usize()
192    }
193
194    fn root(&self) -> usize {
195        self.root
196    }
197
198    /// Returns the CPUs that may currently retain translations for this root.
199    pub fn active_mask(&self) -> usize {
200        match &self.active_mask {
201            ActiveCpuMask::Runtime(mask) => mask.load(Ordering::Acquire),
202            ActiveCpuMask::Mm(mask) => mask.load(Ordering::Acquire),
203        }
204    }
205
206    fn membarrier_state(this: &Arc<Self>) -> AddressSpaceMembarrierState {
207        let raw = Arc::as_ptr(this).expose_provenance();
208        // SAFETY: every scheduler token and `AddrSpace` owner retains this Arc,
209        // so its allocation cannot be reused while the identity is rq-visible.
210        let identity = unsafe { AddressSpaceMembarrierId::from_raw(raw) };
211        let bits = this.membarrier_bits.load(Ordering::SeqCst);
212        // SAFETY: `membarrier_bits` is changed only through the typed phase
213        // update below and therefore contains only declared registration bits.
214        unsafe { AddressSpaceMembarrierState::new(identity, bits) }
215    }
216
217    fn update_membarrier_state(
218        this: &Arc<Self>,
219        registration: MembarrierRegistration,
220        phase: MembarrierRegistrationPhase,
221    ) -> AddressSpaceMembarrierState {
222        let bit = match phase {
223            MembarrierRegistrationPhase::Begin => registration.requested_bit(),
224            MembarrierRegistrationPhase::Complete => {
225                assert!(
226                    this.membarrier_bits.load(Ordering::SeqCst) & registration.requested_bit() != 0,
227                    "membarrier registration completed before its requested phase"
228                );
229                registration.ready_bit()
230            }
231        };
232        this.membarrier_bits.fetch_or(bit, Ordering::SeqCst);
233        Self::membarrier_state(this)
234    }
235
236    #[cfg(any(feature = "uspace", test))]
237    fn cpu_bit(cpu_id: usize) -> usize {
238        1usize.checked_shl(cpu_id as u32).unwrap_or_else(|| {
239            panic!("CPU {cpu_id} cannot be represented in an address-space mask")
240        })
241    }
242
243    #[cfg(any(feature = "uspace", test))]
244    fn activate(&self, cpu_id: usize) {
245        if let ActiveCpuMask::Runtime(mask) = &self.active_mask {
246            mask.fetch_or(Self::cpu_bit(cpu_id), Ordering::Release);
247        }
248    }
249
250    #[cfg(any(feature = "uspace", test))]
251    fn deactivate(&self, cpu_id: usize) {
252        if let ActiveCpuMask::Runtime(mask) = &self.active_mask {
253            mask.fetch_and(!Self::cpu_bit(cpu_id), Ordering::Release);
254        }
255    }
256}
257
258/// Move-only runtime token for one user address space.
259pub struct TaskAddressSpace(Option<AddressSpaceToken>);
260
261impl TaskAddressSpace {
262    /// Creates a scheduler token that owns `owner` until address-space reap.
263    pub fn new(root: PhysAddr, owner: impl Send + Sync + 'static) -> Result<Self, TaskError> {
264        Self::new_with_owner(
265            root,
266            Arc::new(AddressSpaceCpuState::new(root)),
267            Box::new(RetainedTaskAddressSpaceOwner(owner)),
268        )
269    }
270
271    /// Creates a scheduler token with task-detach ownership semantics.
272    ///
273    /// `detach` runs once in ordinary task context after the attached thread
274    /// has entered lazy kernel-mm state. It may release task-scoped accounting,
275    /// but `owner` must keep the hardware page-table root valid until its final
276    /// drop after all active-CPU leases have drained.
277    pub fn new_with_task_detach<T: Send + Sync + 'static>(
278        root: PhysAddr,
279        cpu_state: Arc<AddressSpaceCpuState>,
280        owner: T,
281        detach: fn(&T),
282    ) -> Result<Self, TaskError> {
283        Self::new_with_owner(
284            root,
285            cpu_state,
286            Box::new(DetachableTaskAddressSpaceOwner {
287                owner,
288                detached: core::sync::atomic::AtomicBool::new(false),
289                detach,
290            }),
291        )
292    }
293
294    /// Creates a task token backed by an OS-owned MM lifecycle.
295    /// Allocation occurs here in task context, before scheduler publication.
296    pub fn new_managed<T: UserAddressSpaceOwner + 'static>(
297        root: PhysAddr,
298        cpu_state: Arc<AddressSpaceCpuState>,
299        owner: T,
300    ) -> Result<Self, TaskError> {
301        Self::new_with_owner(
302            root,
303            cpu_state,
304            Box::new(ManagedTaskAddressSpaceOwner(owner)),
305        )
306    }
307
308    fn new_with_owner(
309        root: PhysAddr,
310        cpu_state: Arc<AddressSpaceCpuState>,
311        owner: Box<dyn TaskAddressSpaceOwner>,
312    ) -> Result<Self, TaskError> {
313        if root.as_usize() == 0 || !cpu_state.matches_root(root) {
314            return Err(TaskError::InvalidRuntimeHandle);
315        }
316        let address_space = Box::new(RuntimeAddressSpace {
317            active_leases: AtomicUsize::new(0),
318            reclaim_waiting: AtomicUsize::new(0),
319            cpu_state,
320            _owner: owner,
321        });
322        let raw = Box::into_raw(address_space).expose_provenance();
323        // SAFETY: the fresh allocation transfers its unique destruction right
324        // into this move-only token.
325        Ok(Self(Some(unsafe { AddressSpaceToken::from_raw(raw) })))
326    }
327
328    pub(super) fn handle(&self) -> AddressSpaceHandle {
329        self.0
330            .as_ref()
331            .unwrap_or_else(|| unreachable!("address-space token already transferred"))
332            .handle()
333    }
334
335    #[cfg(feature = "uspace")]
336    pub(super) fn token_mut(&mut self) -> &mut AddressSpaceToken {
337        self.0
338            .as_mut()
339            .unwrap_or_else(|| unreachable!("address-space token already transferred"))
340    }
341
342    pub(super) fn take_token(&mut self) -> AddressSpaceToken {
343        self.0
344            .take()
345            .unwrap_or_else(|| unreachable!("address-space token already transferred"))
346    }
347}
348
349fn detach_runtime_address_space_owner(address_space: AddressSpaceHandle) {
350    runtime_address_space(address_space)
351        .unwrap_or_else(|_| panic!("address-space detach received an invalid owning handle"))
352        ._owner
353        .detach_from_task();
354}
355
356#[cfg(any(feature = "uspace", test))]
357fn detach_replaced_address_space_owner(address_space: AddressSpaceHandle) {
358    detach_runtime_address_space_owner(address_space);
359}
360
361impl Drop for TaskAddressSpace {
362    fn drop(&mut self) {
363        let Some(address_space) = self.0.take() else {
364            return;
365        };
366        detach_runtime_address_space_owner(address_space.handle());
367        let outcome = destroy_runtime_address_space(address_space.handle());
368        assert_eq!(
369            outcome,
370            AddressSpaceDestroyOutcome::Released,
371            "unpublished address space retained an active CPU lease"
372        );
373    }
374}
375
376#[ax_percpu::def_percpu]
377static ACTIVE_ADDRESS_SPACE: usize = 0;
378
379#[ax_percpu::def_percpu]
380#[cfg(feature = "uspace")]
381static ACTIVE_MM_ACTIVATION: Option<SchedulerAddressSpaceActivation> = None;
382
383#[cfg(feature = "uspace")]
384fn replace_active_activation(
385    pin: &CpuPin<'_>,
386    next: Option<SchedulerAddressSpaceActivation>,
387) -> Option<SchedulerAddressSpaceActivation> {
388    debug_assert!(!ax_hal::asm::irqs_enabled());
389    // SAFETY: the root-switch transaction holds local IRQ exclusion. This
390    // CPU-only slot has no remote readers, and the mutable borrow cannot escape.
391    unsafe {
392        ax_percpu::with_exclusive_cpu(pin, |exclusive| {
393            ACTIVE_MM_ACTIVATION
394                .with_current_mut(exclusive, |active| core::mem::replace(active, next))
395        })
396    }
397}
398
399#[cfg(feature = "uspace")]
400fn install_mm_identity(installed: ax_hal::context::InstalledAddressSpace) {
401    // SAFETY: the prepared/active lease owns the root, and the caller keeps IRQs
402    // disabled from CPU-footprint publication through active-lease publication.
403    unsafe { ax_hal::asm::install_user_address_space(installed) };
404    #[cfg(feature = "qperf-metrics")]
405    ACTIVE_MM_HARDWARE_ROOT_WRITES.fetch_add(1, Ordering::Relaxed);
406}
407
408/// Last-active-mm notification claimed before the raw context switch.
409///
410/// The incoming switch tail consumes this bit and returns it to ax-task. The
411/// scheduler publishes task work only after releasing the outgoing task's
412/// `on_cpu` claim, matching Linux `finish_task_switch()` ordering.
413#[ax_percpu::def_percpu]
414#[cfg(feature = "uspace")]
415static CONTEXT_SWITCH_RECLAIM_READY: AtomicBool = AtomicBool::new(false);
416
417fn runtime_address_space(
418    address_space: AddressSpaceHandle,
419) -> Result<&'static RuntimeAddressSpace, RuntimeStatus> {
420    let raw = address_space.into_raw();
421    if raw == 0 || !raw.is_multiple_of(align_of::<RuntimeAddressSpace>()) {
422        return Err(RuntimeStatus::InvalidHandle);
423    }
424    let address_space = ptr::with_exposed_provenance::<RuntimeAddressSpace>(raw);
425    // SAFETY: a borrowed handle is reachable only while its owning token or a
426    // per-CPU active lease keeps this allocation live.
427    Ok(unsafe { &*address_space })
428}
429
430#[cfg(feature = "uspace")]
431fn offline_kernel_root() -> usize {
432    if cfg!(any(target_arch = "x86_64", target_arch = "riscv64")) {
433        // SAFETY: CPU offline holds IRQ exclusion, and bring-up published the
434        // immutable root before the CPU became scheduler-visible.
435        unsafe { with_current_cpu_pin(super::bootstrap::offline_kernel_root) }
436    } else {
437        // AArch64 and LoongArch keep kernel mappings in their separate upper
438        // root. Zero leaves no lower/user translation active while offline.
439        0
440    }
441}
442
443#[cfg(feature = "uspace")]
444pub(super) fn current_hardware_root() -> usize {
445    ax_hal::asm::read_user_page_table().as_usize()
446}
447
448#[cfg(feature = "uspace")]
449pub(super) fn validate_current_user_address_space(
450    pin: &CpuPin<'_>,
451    selected: AddressSpaceHandle,
452) -> Result<(), RuntimeStatus> {
453    if selected.is_none() {
454        return Err(RuntimeStatus::InvalidHandle);
455    }
456    let active_raw = ACTIVE_ADDRESS_SPACE.read_current(pin);
457    if active_raw == 0 {
458        return Err(RuntimeStatus::InvalidHandle);
459    }
460    // SAFETY: a non-zero CPU-local publication originates from a live active
461    // lease and remains pinned by the IRQ-off caller.
462    let active = runtime_address_space(unsafe { AddressSpaceHandle::from_raw(active_raw) })?;
463    let selected = runtime_address_space(selected)?;
464    let cpu_bit = AddressSpaceCpuState::cpu_bit(pin.area().cpu_index().as_usize());
465    if !same_logical_address_space(active, selected)
466        || active.active_leases.load(Ordering::Acquire) == 0
467        || active.cpu_state.active_mask() & cpu_bit == 0
468        || current_hardware_root() != active.root()
469    {
470        return Err(RuntimeStatus::InvalidHandle);
471    }
472    Ok(())
473}
474
475#[cfg(any(feature = "uspace", test))]
476#[derive(Clone, Copy, Debug, Eq, PartialEq)]
477enum HardwareAddressSpaceTransition {
478    SameAddressSpace,
479    DifferentAddressSpace,
480}
481
482#[cfg(any(feature = "uspace", test))]
483fn hardware_root_install_required(
484    current_root: usize,
485    next_root: usize,
486    transition: HardwareAddressSpaceTransition,
487) -> bool {
488    current_root != next_root || transition == HardwareAddressSpaceTransition::DifferentAddressSpace
489}
490
491#[cfg(feature = "uspace")]
492fn install_hardware_root(root: usize, transition: HardwareAddressSpaceTransition) {
493    if hardware_root_install_required(current_hardware_root(), root, transition) {
494        let root = ax_memory_addr::PhysAddr::from(root);
495        // SAFETY: callers retain local IRQ exclusion for the complete active-mm
496        // transaction.
497        unsafe { ax_hal::asm::write_user_page_table(root) };
498        #[cfg(feature = "qperf-metrics")]
499        ACTIVE_MM_HARDWARE_ROOT_WRITES.fetch_add(1, Ordering::Relaxed);
500        // Linux reloads CR3 when the logical mm changes even if a reclaimed
501        // page-table frame gives the new mm the same root address. Otherwise
502        // non-PCID x86 can retain translations from the former mm. The other
503        // architecture backends only update their root register and require an
504        // explicit invalidation for the same identity transition.
505        #[cfg(not(target_arch = "x86_64"))]
506        ax_hal::asm::flush_tlb(None);
507    }
508}
509
510#[cfg(feature = "uspace")]
511fn enter_lazy_kernel_address_space() {
512    // Linux's current x86, RISC-V and LoongArch enter_lazy_tlb paths retain the
513    // loaded user root and only change scheduler/ASID bookkeeping. AArch64
514    // installs its reserved lower root so a kernel thread cannot use the
515    // previous task's user mappings.
516    #[cfg(target_arch = "aarch64")]
517    install_hardware_root(0, HardwareAddressSpaceTransition::DifferentAddressSpace);
518}
519
520#[cfg(feature = "uspace")]
521fn commit_user_address_space_activation(
522    cpu_id: usize,
523    previous_raw: usize,
524    previous: Option<&RuntimeAddressSpace>,
525    next_raw: usize,
526    next: &RuntimeAddressSpace,
527    install_root: impl FnOnce(usize, HardwareAddressSpaceTransition),
528    publish_active: impl FnOnce(usize),
529) -> bool {
530    let same_address_space = next_raw == previous_raw
531        || previous.is_some_and(|previous| same_logical_address_space(previous, next));
532    if same_address_space {
533        #[cfg(feature = "qperf-metrics")]
534        ACTIVE_MM_SAME_ACTIVATIONS.fetch_add(1, Ordering::Relaxed);
535        debug_assert_eq!(
536            previous.map(RuntimeAddressSpace::root),
537            Some(next.root()),
538            "one address-space CPU tracker cannot describe different roots"
539        );
540        // Match Linux arm64's `enter_lazy_tlb()`/`switch_mm_irqs_off()` pair:
541        // retain the active-mm lease, but restore the user root if a kernel
542        // thread temporarily installed the reserved lower root. The runtime
543        // backend suppresses the write when the hardware root is already
544        // correct, so user-to-user switches in the same mm remain a no-op.
545        install_root(
546            next.root(),
547            HardwareAddressSpaceTransition::SameAddressSpace,
548        );
549        return false;
550    }
551    #[cfg(feature = "qperf-metrics")]
552    ACTIVE_MM_DIFFERENT_ACTIVATIONS.fetch_add(1, Ordering::Relaxed);
553    next.active_leases.fetch_add(1, Ordering::AcqRel);
554    next.cpu_state.activate(cpu_id);
555    #[cfg(feature = "qperf-metrics")]
556    ACTIVE_MM_LEASE_ACTIVATIONS.fetch_add(1, Ordering::Relaxed);
557    install_root(
558        next.root(),
559        HardwareAddressSpaceTransition::DifferentAddressSpace,
560    );
561    publish_active(next_raw);
562    if let Some(previous) = previous {
563        previous.cpu_state.deactivate(cpu_id);
564        #[cfg(feature = "qperf-metrics")]
565        ACTIVE_MM_LEASE_DEACTIVATIONS.fetch_add(1, Ordering::Relaxed);
566        release_active_cpu(previous)
567    } else {
568        false
569    }
570}
571
572#[cfg(feature = "uspace")]
573fn same_logical_address_space(first: &RuntimeAddressSpace, second: &RuntimeAddressSpace) -> bool {
574    Arc::ptr_eq(&first.cpu_state, &second.cpu_state)
575}
576
577enum PreparedAddressSpaceAction {
578    KernelLazy,
579    #[cfg(all(feature = "uspace", not(target_arch = "aarch64")))]
580    SameUser,
581    #[cfg(feature = "uspace")]
582    User {
583        next_raw: usize,
584        next: &'static RuntimeAddressSpace,
585        activation: Option<SchedulerAddressSpaceActivation>,
586    },
587}
588
589#[derive(Clone, Copy, Debug, Eq, PartialEq)]
590pub(super) enum AddressSpaceTransitionPhase {
591    #[cfg(feature = "uspace")]
592    CurrentTask,
593    ContextSwitch,
594}
595
596/// CPU-bound address-space half of one scheduler switch transaction.
597///
598/// Preparation validates every handle, the scheduler-selected logical `mm`,
599/// the current active-mm lease and the membarrier identity without changing
600/// hardware or ownership. Commit is therefore infallible and may be placed
601/// immediately before the naked architecture switch.
602#[must_use = "a prepared address-space switch must be committed with its context switch"]
603pub(super) struct PreparedAddressSpaceSwitch<'pin, 'cpu> {
604    #[cfg(feature = "uspace")]
605    pin: &'pin CpuPin<'cpu>,
606    phase: AddressSpaceTransitionPhase,
607    #[cfg(feature = "uspace")]
608    previous_raw: usize,
609    #[cfg(feature = "uspace")]
610    previous: Option<&'static RuntimeAddressSpace>,
611    action: PreparedAddressSpaceAction,
612    _not_send_or_sync: PhantomData<(&'pin CpuPin<'cpu>, *mut ())>,
613}
614
615impl PreparedAddressSpaceSwitch<'_, '_> {
616    /// Commits the active-mm transition without running fallible logic.
617    #[inline(always)]
618    pub(super) fn commit(self) {
619        #[cfg(feature = "uspace")]
620        let pin = self.pin;
621        match self.phase {
622            #[cfg(feature = "uspace")]
623            AddressSpaceTransitionPhase::CurrentTask => assert!(
624                !ax_hal::asm::irqs_enabled(),
625                "current-task address-space commit requires local IRQ exclusion"
626            ),
627            AddressSpaceTransitionPhase::ContextSwitch => {}
628        }
629        #[cfg(not(feature = "uspace"))]
630        debug_assert_eq!(
631            self.phase,
632            AddressSpaceTransitionPhase::ContextSwitch,
633            "kernel-only builds prepare address spaces only for scheduler switches"
634        );
635        #[cfg(feature = "uspace")]
636        assert_eq!(
637            ACTIVE_ADDRESS_SPACE.read_current(pin),
638            self.previous_raw,
639            "active address space changed after switch preparation"
640        );
641
642        match self.action {
643            PreparedAddressSpaceAction::KernelLazy => {
644                #[cfg(feature = "uspace")]
645                {
646                    #[cfg(feature = "qperf-metrics")]
647                    ACTIVE_MM_KERNEL_LAZY_ACTIVATIONS.fetch_add(1, Ordering::Relaxed);
648                    enter_lazy_kernel_address_space();
649                }
650            }
651            #[cfg(all(feature = "uspace", not(target_arch = "aarch64")))]
652            PreparedAddressSpaceAction::SameUser => {}
653            #[cfg(feature = "uspace")]
654            PreparedAddressSpaceAction::User {
655                next_raw,
656                next,
657                mut activation,
658            } => {
659                let cpu_id = pin.area().cpu_index().as_usize();
660                let installed = activation
661                    .as_ref()
662                    .map(SchedulerAddressSpaceActivation::installed)
663                    .or_else(|| {
664                        if !self
665                            .previous
666                            .is_some_and(|previous| same_logical_address_space(previous, next))
667                        {
668                            return None;
669                        }
670                        ACTIVE_MM_ACTIVATION.with_current(pin, |active| {
671                            active
672                                .as_ref()
673                                .map(SchedulerAddressSpaceActivation::installed)
674                        })
675                    });
676                let reclaim_ready = commit_user_address_space_activation(
677                    pin.area().cpu_index().as_usize(),
678                    self.previous_raw,
679                    self.previous,
680                    next_raw,
681                    next,
682                    |root, transition| {
683                        if let Some(installed) = installed {
684                            if hardware_root_install_required(
685                                current_hardware_root(),
686                                root,
687                                transition,
688                            ) {
689                                install_mm_identity(installed);
690                            }
691                        } else {
692                            install_hardware_root(root, transition);
693                        }
694                    },
695                    |active| {
696                        if let Some(next) = activation.as_mut() {
697                            next.commit(cpu_id);
698                        }
699                        let previous = replace_active_activation(pin, activation.take());
700                        ACTIVE_ADDRESS_SPACE.write_current(pin, active);
701                        if let Some(previous) = previous {
702                            previous.release(AddressSpaceSwitchProof::new(cpu_id));
703                        }
704                    },
705                );
706                if reclaim_ready {
707                    route_reclaim_notification(
708                        self.phase,
709                        || {
710                            CONTEXT_SWITCH_RECLAIM_READY.with_current(pin, |pending| {
711                                assert!(
712                                    !pending.swap(true, Ordering::AcqRel),
713                                    "context switch retained an unconsumed active-mm reclaim edge"
714                                );
715                            });
716                        },
717                        ax_task::runtime::resource::notify_address_space_reclaim,
718                    );
719                }
720            }
721        }
722    }
723}
724
725/// Validates and prepares the address-space half of a scheduler switch.
726pub(super) fn prepare_runtime_address_space_switch<'pin, 'cpu>(
727    _pin: &'pin CpuPin<'cpu>,
728    previous_selected: AddressSpaceHandle,
729    next_selected: AddressSpaceHandle,
730    same_address_space: bool,
731    phase: AddressSpaceTransitionPhase,
732) -> Result<PreparedAddressSpaceSwitch<'pin, 'cpu>, RuntimeStatus> {
733    #[cfg(feature = "uspace")]
734    let pin = _pin;
735    #[cfg(feature = "uspace")]
736    let cpu_id = pin.area().cpu_index().as_usize();
737    #[cfg(any(not(feature = "uspace"), target_arch = "aarch64"))]
738    let _ = same_address_space;
739
740    #[cfg(feature = "uspace")]
741    {
742        let previous_raw = ACTIVE_ADDRESS_SPACE.read_current(pin);
743        #[cfg(not(target_arch = "aarch64"))]
744        if same_address_space {
745            debug_assert!(!previous_selected.is_none());
746            debug_assert!(!next_selected.is_none());
747            debug_assert_ne!(previous_raw, 0);
748            return Ok(PreparedAddressSpaceSwitch {
749                pin,
750                phase,
751                previous_raw,
752                previous: None,
753                action: PreparedAddressSpaceAction::SameUser,
754                _not_send_or_sync: PhantomData,
755            });
756        }
757        let previous = if previous_raw == 0 {
758            None
759        } else {
760            // SAFETY: a non-zero CPU-local publication is created only from a
761            // live runtime address-space handle and retains its active lease.
762            let previous = unsafe { AddressSpaceHandle::from_raw(previous_raw) };
763            Some(runtime_address_space(previous)?)
764        };
765
766        #[cfg(not(target_arch = "aarch64"))]
767        if let Some((previous, next)) = previous.zip(
768            (!next_selected.is_none())
769                .then(|| runtime_address_space(next_selected))
770                .transpose()?,
771        ) && same_logical_address_space(previous, next)
772        {
773            // Linux's ordinary same-mm path compares the CPU's loaded mm with
774            // next->mm and returns without inspecting the old task token,
775            // active mask, lease count or hardware root. x86, RISC-V and
776            // LoongArch retain the loaded root across lazy kernel threads, so
777            // their user-to-user same-mm switch is likewise a pure no-op.
778            // AArch64 is excluded because its lazy path installs the reserved
779            // lower root and must restore TTBR0 on the following user switch.
780            debug_assert!(!previous_selected.is_none());
781            debug_assert!(
782                runtime_address_space(previous_selected)
783                    .is_ok_and(|selected| same_logical_address_space(previous, selected))
784            );
785            debug_assert_ne!(previous.active_leases.load(Ordering::Acquire), 0);
786            debug_assert!(
787                previous.cpu_state.active_mask() & AddressSpaceCpuState::cpu_bit(cpu_id) != 0
788            );
789            return Ok(PreparedAddressSpaceSwitch {
790                pin,
791                phase,
792                previous_raw,
793                previous: Some(previous),
794                action: PreparedAddressSpaceAction::SameUser,
795                _not_send_or_sync: PhantomData,
796            });
797        }
798
799        if let Some(previous) = previous {
800            let bit = AddressSpaceCpuState::cpu_bit(cpu_id);
801            if previous.active_leases.load(Ordering::Acquire) == 0
802                || previous.cpu_state.active_mask() & bit == 0
803            {
804                return Err(RuntimeStatus::InvalidHandle);
805            }
806        }
807
808        let previous_state = if previous_selected.is_none() {
809            None
810        } else {
811            let selected = runtime_address_space(previous_selected)?;
812            let Some(active) = previous else {
813                return Err(RuntimeStatus::InvalidArgument);
814            };
815            if !same_logical_address_space(active, selected) {
816                return Err(RuntimeStatus::InvalidArgument);
817            }
818            Some(selected)
819        };
820
821        let (next_state, action) = if next_selected.is_none() {
822            (None, PreparedAddressSpaceAction::KernelLazy)
823        } else {
824            let next = runtime_address_space(next_selected)?;
825            (
826                Some(next),
827                PreparedAddressSpaceAction::User {
828                    next_raw: next_selected.into_raw(),
829                    next,
830                    activation: if previous
831                        .is_some_and(|previous| same_logical_address_space(previous, next))
832                    {
833                        None
834                    } else {
835                        next._owner.prepare_activation(cpu_id)?
836                    },
837                },
838            )
839        };
840
841        // The switch barrier is keyed only by the logical mm identity. The
842        // membarrier registration bits are consumed by their own syscall
843        // paths; loading them on every same-mm thread switch is not part of
844        // Linux switch_mm() semantics and adds a SeqCst read to the hot path.
845        let changed_address_space = match (previous_state, next_state) {
846            (Some(previous), Some(next)) => !same_logical_address_space(previous, next),
847            (Some(_), None) | (None, Some(_)) => true,
848            (None, None) => false,
849        };
850        if changed_address_space {
851            // Common four-architecture counterpart of Linux switch_mm() and
852            // mmdrop's ordering after rq->curr publication and before user
853            // execution.
854            core::sync::atomic::fence(Ordering::SeqCst);
855        }
856
857        Ok(PreparedAddressSpaceSwitch {
858            pin,
859            phase,
860            previous_raw,
861            previous,
862            action,
863            _not_send_or_sync: PhantomData,
864        })
865    }
866
867    #[cfg(not(feature = "uspace"))]
868    {
869        if !previous_selected.is_none() || !next_selected.is_none() {
870            return Err(RuntimeStatus::Unsupported);
871        }
872        Ok(PreparedAddressSpaceSwitch {
873            phase,
874            action: PreparedAddressSpaceAction::KernelLazy,
875            _not_send_or_sync: PhantomData,
876        })
877    }
878}
879
880pub(super) fn release_current_active_address_space() {
881    #[cfg(feature = "uspace")]
882    let reclaim_ready = unsafe {
883        with_current_cpu_pin(|pin| {
884            let previous_raw = ACTIVE_ADDRESS_SPACE.read_current(pin);
885            if previous_raw == 0 {
886                return false;
887            }
888            install_hardware_root(
889                offline_kernel_root(),
890                HardwareAddressSpaceTransition::DifferentAddressSpace,
891            );
892            // CPU offline invalidates every local tag before retiring its MM.
893            ax_hal::asm::flush_tlb(None);
894            if let Some(activation) = replace_active_activation(pin, None) {
895                activation.release(AddressSpaceSwitchProof::new(
896                    pin.area().cpu_index().as_usize(),
897                ));
898            }
899            ACTIVE_ADDRESS_SPACE.write_current(pin, 0);
900            let previous = AddressSpaceHandle::from_raw(previous_raw);
901            let previous = runtime_address_space(previous)
902                .unwrap_or_else(|_| panic!("offline CPU retained a stale active address space"));
903            previous
904                .cpu_state
905                .deactivate(pin.area().cpu_index().as_usize());
906            #[cfg(feature = "qperf-metrics")]
907            ACTIVE_MM_LEASE_DEACTIVATIONS.fetch_add(1, Ordering::Relaxed);
908            release_active_cpu(previous)
909        })
910    };
911    #[cfg(feature = "uspace")]
912    if reclaim_ready {
913        ax_task::runtime::resource::notify_address_space_reclaim();
914    }
915}
916
917pub(super) fn destroy_runtime_address_space(
918    address_space: AddressSpaceHandle,
919) -> AddressSpaceDestroyOutcome {
920    let address_space = runtime_address_space(address_space)
921        .unwrap_or_else(|_| panic!("address-space destruction received an invalid owning handle"));
922    if address_space.active_leases.load(Ordering::Acquire) != 0 {
923        return AddressSpaceDestroyOutcome::Active;
924    }
925    let raw = address_space as *const RuntimeAddressSpace as *mut RuntimeAddressSpace;
926    // SAFETY: the caller owns the unique AddressSpaceToken destruction right,
927    // and the zero active count proves no CPU retains a borrowed pointer.
928    drop(unsafe { Box::from_raw(raw) });
929    #[cfg(feature = "qperf-metrics")]
930    ACTIVE_MM_RECLAIM_DESTROYED.fetch_add(1, Ordering::Relaxed);
931    AddressSpaceDestroyOutcome::Released
932}
933
934pub(super) fn arm_runtime_address_space_reclaim(
935    address_space: AddressSpaceHandle,
936) -> AddressSpaceReclaimArmOutcome {
937    let address_space = runtime_address_space(address_space)
938        .unwrap_or_else(|_| panic!("address-space reclaim arm received an invalid owning handle"));
939    address_space.reclaim_waiting.store(1, Ordering::Release);
940    if address_space.active_leases.load(Ordering::Acquire) == 0 {
941        address_space.reclaim_waiting.store(0, Ordering::Release);
942        AddressSpaceReclaimArmOutcome::Ready
943    } else {
944        AddressSpaceReclaimArmOutcome::Armed
945    }
946}
947
948pub(super) fn runtime_address_space_membarrier_state(
949    address_space: AddressSpaceHandle,
950) -> AddressSpaceMembarrierState {
951    let address_space = runtime_address_space(address_space)
952        .unwrap_or_else(|_| panic!("membarrier received an invalid address-space handle"));
953    AddressSpaceCpuState::membarrier_state(&address_space.cpu_state)
954}
955
956pub(super) fn update_runtime_address_space_membarrier_state(
957    address_space: AddressSpaceHandle,
958    registration: MembarrierRegistration,
959    phase: MembarrierRegistrationPhase,
960) -> AddressSpaceMembarrierState {
961    let address_space = runtime_address_space(address_space).unwrap_or_else(|_| {
962        panic!("membarrier registration received an invalid address-space handle")
963    });
964    AddressSpaceCpuState::update_membarrier_state(&address_space.cpu_state, registration, phase)
965}
966
967#[cfg(any(feature = "uspace", test))]
968fn release_active_cpu(address_space: &RuntimeAddressSpace) -> bool {
969    let active = address_space.active_leases.fetch_sub(1, Ordering::AcqRel);
970    assert!(active >= 1, "active address-space lease count underflow");
971    let reclaim_ready = active == 1 && address_space.reclaim_waiting.swap(0, Ordering::AcqRel) != 0;
972    #[cfg(feature = "qperf-metrics")]
973    if reclaim_ready {
974        ACTIVE_MM_RECLAIM_READY.fetch_add(1, Ordering::Relaxed);
975    }
976    reclaim_ready
977}
978
979#[cfg(any(feature = "uspace", test))]
980fn route_reclaim_notification(
981    phase: AddressSpaceTransitionPhase,
982    defer: impl FnOnce(),
983    publish: impl FnOnce(),
984) {
985    #[cfg(not(feature = "uspace"))]
986    let _ = publish;
987    match phase {
988        #[cfg(feature = "uspace")]
989        AddressSpaceTransitionPhase::CurrentTask => publish(),
990        AddressSpaceTransitionPhase::ContextSwitch => defer(),
991    }
992}
993
994pub(super) fn take_context_switch_reclaim_ready() -> bool {
995    #[cfg(feature = "uspace")]
996    {
997        // SAFETY: the incoming runtime switch tail retains the scheduler baton
998        // and therefore cannot migrate while consuming the CPU-local edge.
999        unsafe {
1000            with_current_cpu_pin(|pin| {
1001                CONTEXT_SWITCH_RECLAIM_READY
1002                    .with_current(pin, |pending| pending.swap(false, Ordering::AcqRel))
1003            })
1004        }
1005    }
1006    #[cfg(not(feature = "uspace"))]
1007    {
1008        false
1009    }
1010}
1011
1012#[cfg(feature = "uspace")]
1013fn commit_current_task_address_space_transition<T>(
1014    next_selected: AddressSpaceHandle,
1015    action: impl FnOnce() -> Result<T, TaskError>,
1016) -> Result<T, TaskError> {
1017    let _irq = crate::task::sync::IrqSaveGuard::new();
1018    // SAFETY: the IRQ guard pins the current CPU through preparation, the
1019    // scheduler-token operation, and the infallible active-mm commit.
1020    unsafe {
1021        with_current_cpu_pin(|pin| {
1022            let previous_selected = ax_task::runtime::resource::current_address_space_handle()?;
1023            let prepared = prepare_runtime_address_space_switch(
1024                pin,
1025                previous_selected,
1026                next_selected,
1027                false,
1028                AddressSpaceTransitionPhase::CurrentTask,
1029            )
1030            .map_err(super::runtime_status_error)?;
1031            let result = action()?;
1032            // No fallible operation may follow the ownership transition above.
1033            prepared.commit();
1034            Ok(result)
1035        })
1036    }
1037}
1038
1039/// Replaces the running user task's owning address-space token.
1040pub fn switch_current_address_space(address_space: TaskAddressSpace) -> Result<(), TaskError> {
1041    #[cfg(feature = "uspace")]
1042    {
1043        let mut address_space = address_space;
1044        let next_handle = address_space.handle();
1045        let previous = commit_current_task_address_space_transition(next_handle, || {
1046            ax_task::runtime::resource::replace_current_address_space(address_space.token_mut())
1047        })?;
1048        let transferred = address_space.take_token();
1049        debug_assert!(transferred.is_none());
1050
1051        // Reclaim may allocate or drop an OS ownership anchor. It therefore
1052        // runs only after the exec transaction has restored normal IRQ state.
1053        detach_replaced_address_space_owner(previous.handle());
1054        ax_task::runtime::resource::release_address_space_token(previous)
1055    }
1056    #[cfg(not(feature = "uspace"))]
1057    {
1058        let _ = address_space;
1059        Err(TaskError::RuntimeFailure(RuntimeStatus::Unsupported as u32))
1060    }
1061}
1062
1063/// Detaches the running user task from its address space before exit
1064/// publication, matching Linux `exit_mm` ordering.
1065pub fn detach_current_address_space() -> Result<(), TaskError> {
1066    #[cfg(feature = "uspace")]
1067    {
1068        let previous = commit_current_task_address_space_transition(
1069            AddressSpaceHandle::NONE,
1070            ax_task::runtime::resource::detach_current_address_space,
1071        )?;
1072
1073        // The task-scoped owner may acquire sleepable OS locks. Run it only
1074        // after restoring IRQs, while the runtime wrapper still pins the root
1075        // for any CPU retaining it as a lazy active mm.
1076        detach_runtime_address_space_owner(previous.handle());
1077        ax_task::runtime::resource::release_address_space_token(previous)
1078    }
1079    #[cfg(not(feature = "uspace"))]
1080    {
1081        Err(TaskError::RuntimeFailure(RuntimeStatus::Unsupported as u32))
1082    }
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087    use alloc::sync::Arc;
1088    use core::sync::atomic::{AtomicUsize, Ordering};
1089
1090    use super::*;
1091
1092    struct CountDrop(Arc<AtomicUsize>);
1093
1094    impl Drop for CountDrop {
1095        fn drop(&mut self) {
1096            self.0.fetch_add(1, Ordering::Release);
1097        }
1098    }
1099
1100    #[test]
1101    fn switch_activation_defers_reclaim_notification_until_switch_tail() {
1102        let deferred = AtomicUsize::new(0);
1103        let published = AtomicUsize::new(0);
1104
1105        route_reclaim_notification(
1106            AddressSpaceTransitionPhase::ContextSwitch,
1107            || {
1108                deferred.fetch_add(1, Ordering::Relaxed);
1109            },
1110            || {
1111                published.fetch_add(1, Ordering::Relaxed);
1112            },
1113        );
1114
1115        assert_eq!(deferred.load(Ordering::Relaxed), 1);
1116        assert_eq!(published.load(Ordering::Relaxed), 0);
1117    }
1118
1119    #[test]
1120    fn active_cpu_lease_blocks_owner_destruction_until_release() {
1121        let drops = Arc::new(AtomicUsize::new(0));
1122        let mut token = TaskAddressSpace::new(
1123            ax_memory_addr::PhysAddr::from(0x4000),
1124            CountDrop(Arc::clone(&drops)),
1125        )
1126        .unwrap();
1127        let handle = token.handle();
1128        let runtime = runtime_address_space(handle).unwrap();
1129        runtime.active_leases.fetch_add(1, Ordering::AcqRel);
1130        let owned = token.take_token();
1131
1132        assert_eq!(
1133            destroy_runtime_address_space(handle),
1134            AddressSpaceDestroyOutcome::Active
1135        );
1136        assert_eq!(
1137            arm_runtime_address_space_reclaim(handle),
1138            AddressSpaceReclaimArmOutcome::Armed
1139        );
1140        assert_eq!(drops.load(Ordering::Acquire), 0);
1141
1142        assert!(release_active_cpu(runtime));
1143        assert_eq!(
1144            destroy_runtime_address_space(handle),
1145            AddressSpaceDestroyOutcome::Released
1146        );
1147        assert_eq!(drops.load(Ordering::Acquire), 1);
1148        assert!(!owned.is_none());
1149    }
1150
1151    #[test]
1152    fn task_detach_releases_task_owner_before_lazy_cpu_lease() {
1153        struct CountDetach {
1154            detaches: Arc<AtomicUsize>,
1155            drops: Arc<AtomicUsize>,
1156        }
1157
1158        impl Drop for CountDetach {
1159            fn drop(&mut self) {
1160                self.drops.fetch_add(1, Ordering::Release);
1161            }
1162        }
1163
1164        fn detach(owner: &CountDetach) {
1165            owner.detaches.fetch_add(1, Ordering::Release);
1166        }
1167
1168        let detaches = Arc::new(AtomicUsize::new(0));
1169        let drops = Arc::new(AtomicUsize::new(0));
1170        let mut token = TaskAddressSpace::new_with_task_detach(
1171            ax_memory_addr::PhysAddr::from(0x4000),
1172            Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000))),
1173            CountDetach {
1174                detaches: Arc::clone(&detaches),
1175                drops: Arc::clone(&drops),
1176            },
1177            detach,
1178        )
1179        .unwrap();
1180        let handle = token.handle();
1181        let runtime = runtime_address_space(handle).unwrap();
1182        runtime.active_leases.fetch_add(1, Ordering::AcqRel);
1183        let owned = token.take_token();
1184
1185        detach_runtime_address_space_owner(handle);
1186        detach_runtime_address_space_owner(handle);
1187        assert_eq!(detaches.load(Ordering::Acquire), 1);
1188        assert_eq!(drops.load(Ordering::Acquire), 0);
1189        assert_eq!(
1190            destroy_runtime_address_space(handle),
1191            AddressSpaceDestroyOutcome::Active
1192        );
1193
1194        assert!(!release_active_cpu(runtime));
1195        assert_eq!(
1196            destroy_runtime_address_space(handle),
1197            AddressSpaceDestroyOutcome::Released
1198        );
1199        assert_eq!(drops.load(Ordering::Acquire), 1);
1200        assert!(!owned.is_none());
1201    }
1202
1203    #[test]
1204    fn replaced_task_owner_is_detached_before_runtime_release() {
1205        struct CountDetach(Arc<AtomicUsize>);
1206
1207        fn detach(owner: &CountDetach) {
1208            owner.0.fetch_add(1, Ordering::Release);
1209        }
1210
1211        let detaches = Arc::new(AtomicUsize::new(0));
1212        let mut token = TaskAddressSpace::new_with_task_detach(
1213            PhysAddr::from(0x4000),
1214            Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000))),
1215            CountDetach(Arc::clone(&detaches)),
1216            detach,
1217        )
1218        .unwrap();
1219        let handle = token.handle();
1220        let owned = token.take_token();
1221
1222        detach_replaced_address_space_owner(handle);
1223        assert_eq!(detaches.load(Ordering::Acquire), 1);
1224        assert_eq!(
1225            destroy_runtime_address_space(handle),
1226            AddressSpaceDestroyOutcome::Released
1227        );
1228        assert!(!owned.is_none());
1229    }
1230
1231    #[test]
1232    fn shared_cpu_state_publishes_and_withdraws_cpu_footprints() {
1233        let tracker = AddressSpaceCpuState::new(PhysAddr::from(0x4000));
1234
1235        tracker.activate(1);
1236        tracker.activate(3);
1237        assert_eq!(tracker.active_mask(), (1usize << 1) | (1usize << 3));
1238
1239        tracker.deactivate(1);
1240        assert_eq!(tracker.active_mask(), 1usize << 3);
1241    }
1242
1243    #[test]
1244    fn address_space_cpu_state_rejects_mismatched_root() {
1245        let tracker = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000)));
1246        let token =
1247            TaskAddressSpace::new_with_task_detach(PhysAddr::from(0x8000), tracker, (), |_| {});
1248
1249        assert!(matches!(token, Err(TaskError::InvalidRuntimeHandle)));
1250    }
1251
1252    #[test]
1253    fn shared_mm_tokens_share_membarrier_identity_and_registration() {
1254        let cpu_state = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000)));
1255        let first = TaskAddressSpace::new_with_task_detach(
1256            PhysAddr::from(0x4000),
1257            Arc::clone(&cpu_state),
1258            (),
1259            |_| {},
1260        )
1261        .unwrap();
1262        let second = TaskAddressSpace::new_with_task_detach(
1263            PhysAddr::from(0x4000),
1264            Arc::clone(&cpu_state),
1265            (),
1266            |_| {},
1267        )
1268        .unwrap();
1269
1270        let requested = update_runtime_address_space_membarrier_state(
1271            first.handle(),
1272            MembarrierRegistration::PrivateExpedited,
1273            MembarrierRegistrationPhase::Begin,
1274        );
1275        let observed = runtime_address_space_membarrier_state(second.handle());
1276        assert_eq!(requested, observed);
1277        assert!(observed.requested(MembarrierRegistration::PrivateExpedited));
1278        assert!(!observed.ready(MembarrierRegistration::PrivateExpedited));
1279
1280        let ready = update_runtime_address_space_membarrier_state(
1281            second.handle(),
1282            MembarrierRegistration::PrivateExpedited,
1283            MembarrierRegistrationPhase::Complete,
1284        );
1285        assert_eq!(
1286            runtime_address_space_membarrier_state(first.handle()),
1287            ready
1288        );
1289        assert!(ready.ready(MembarrierRegistration::PrivateExpedited));
1290    }
1291
1292    #[cfg(feature = "uspace")]
1293    #[test]
1294    fn same_mm_activation_retains_the_existing_cpu_lease_without_hardware_work() {
1295        let tracker = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000)));
1296        let previous = TaskAddressSpace::new_with_task_detach(
1297            PhysAddr::from(0x4000),
1298            Arc::clone(&tracker),
1299            (),
1300            |_| {},
1301        )
1302        .unwrap();
1303        let next = TaskAddressSpace::new_with_task_detach(
1304            PhysAddr::from(0x4000),
1305            Arc::clone(&tracker),
1306            (),
1307            |_| {},
1308        )
1309        .unwrap();
1310        let previous_runtime = runtime_address_space(previous.handle()).unwrap();
1311        let next_runtime = runtime_address_space(next.handle()).unwrap();
1312        previous_runtime.active_leases.store(1, Ordering::Release);
1313        tracker.activate(0);
1314        let hardware_root = AtomicUsize::new(0x4000);
1315        let hardware_installs = AtomicUsize::new(0);
1316        let active_publications = AtomicUsize::new(0);
1317
1318        let reclaim_ready = commit_user_address_space_activation(
1319            0,
1320            previous.handle().into_raw(),
1321            Some(previous_runtime),
1322            next.handle().into_raw(),
1323            next_runtime,
1324            |root, _transition| {
1325                if hardware_root.swap(root, Ordering::AcqRel) != root {
1326                    hardware_installs.fetch_add(1, Ordering::Relaxed);
1327                }
1328            },
1329            |_| {
1330                active_publications.fetch_add(1, Ordering::Relaxed);
1331            },
1332        );
1333
1334        let previous_leases = previous_runtime.active_leases.load(Ordering::Acquire);
1335        let next_leases = next_runtime.active_leases.load(Ordering::Acquire);
1336        previous_runtime.active_leases.store(0, Ordering::Release);
1337        next_runtime.active_leases.store(0, Ordering::Release);
1338        tracker.deactivate(0);
1339
1340        assert_eq!(previous_leases, 1);
1341        assert_eq!(next_leases, 0);
1342        assert_eq!(hardware_installs.load(Ordering::Relaxed), 0);
1343        assert_eq!(active_publications.load(Ordering::Relaxed), 0);
1344        assert!(!reclaim_ready);
1345    }
1346
1347    #[cfg(feature = "uspace")]
1348    #[test]
1349    fn lazy_kernel_root_is_restored_before_same_mm_user_execution() {
1350        let tracker = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000)));
1351        let previous = TaskAddressSpace::new_with_task_detach(
1352            PhysAddr::from(0x4000),
1353            Arc::clone(&tracker),
1354            (),
1355            |_| {},
1356        )
1357        .unwrap();
1358        let next = TaskAddressSpace::new_with_task_detach(
1359            PhysAddr::from(0x4000),
1360            Arc::clone(&tracker),
1361            (),
1362            |_| {},
1363        )
1364        .unwrap();
1365        let previous_runtime = runtime_address_space(previous.handle()).unwrap();
1366        let next_runtime = runtime_address_space(next.handle()).unwrap();
1367        previous_runtime.active_leases.store(1, Ordering::Release);
1368        tracker.activate(0);
1369        let hardware_root = AtomicUsize::new(0);
1370        let hardware_installs = AtomicUsize::new(0);
1371        let active_publications = AtomicUsize::new(0);
1372
1373        let reclaim_ready = commit_user_address_space_activation(
1374            0,
1375            previous.handle().into_raw(),
1376            Some(previous_runtime),
1377            next.handle().into_raw(),
1378            next_runtime,
1379            |root, _transition| {
1380                if hardware_root.swap(root, Ordering::AcqRel) != root {
1381                    hardware_installs.fetch_add(1, Ordering::Relaxed);
1382                }
1383            },
1384            |_| {
1385                active_publications.fetch_add(1, Ordering::Relaxed);
1386            },
1387        );
1388
1389        let previous_leases = previous_runtime.active_leases.load(Ordering::Acquire);
1390        let next_leases = next_runtime.active_leases.load(Ordering::Acquire);
1391        previous_runtime.active_leases.store(0, Ordering::Release);
1392        next_runtime.active_leases.store(0, Ordering::Release);
1393        tracker.deactivate(0);
1394
1395        assert_eq!(hardware_root.load(Ordering::Acquire), 0x4000);
1396        assert_eq!(hardware_installs.load(Ordering::Relaxed), 1);
1397        assert_eq!(previous_leases, 1);
1398        assert_eq!(next_leases, 0);
1399        assert_eq!(active_publications.load(Ordering::Relaxed), 0);
1400        assert!(!reclaim_ready);
1401    }
1402
1403    #[cfg(feature = "uspace")]
1404    #[test]
1405    fn same_mm_chain_releases_the_retained_active_handle_on_other_mm_switch() {
1406        let shared = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x4000)));
1407        let other = Arc::new(AddressSpaceCpuState::new(PhysAddr::from(0x8000)));
1408        let first = TaskAddressSpace::new_with_task_detach(
1409            PhysAddr::from(0x4000),
1410            Arc::clone(&shared),
1411            (),
1412            |_| {},
1413        )
1414        .unwrap();
1415        let second = TaskAddressSpace::new_with_task_detach(
1416            PhysAddr::from(0x4000),
1417            Arc::clone(&shared),
1418            (),
1419            |_| {},
1420        )
1421        .unwrap();
1422        let third = TaskAddressSpace::new_with_task_detach(
1423            PhysAddr::from(0x8000),
1424            Arc::clone(&other),
1425            (),
1426            |_| {},
1427        )
1428        .unwrap();
1429        let first_runtime = runtime_address_space(first.handle()).unwrap();
1430        let second_runtime = runtime_address_space(second.handle()).unwrap();
1431        let third_runtime = runtime_address_space(third.handle()).unwrap();
1432        first_runtime.active_leases.store(1, Ordering::Release);
1433        shared.activate(0);
1434        let active = AtomicUsize::new(first.handle().into_raw());
1435
1436        assert!(same_logical_address_space(first_runtime, second_runtime));
1437        assert!(!commit_user_address_space_activation(
1438            0,
1439            active.load(Ordering::Acquire),
1440            Some(first_runtime),
1441            second.handle().into_raw(),
1442            second_runtime,
1443            |_, _| {},
1444            |next| active.store(next, Ordering::Release),
1445        ));
1446        assert_eq!(active.load(Ordering::Acquire), first.handle().into_raw());
1447        assert_eq!(first_runtime.active_leases.load(Ordering::Acquire), 1);
1448        assert_eq!(second_runtime.active_leases.load(Ordering::Acquire), 0);
1449
1450        assert!(!commit_user_address_space_activation(
1451            0,
1452            active.load(Ordering::Acquire),
1453            Some(first_runtime),
1454            third.handle().into_raw(),
1455            third_runtime,
1456            |_, transition| {
1457                assert_eq!(
1458                    transition,
1459                    HardwareAddressSpaceTransition::DifferentAddressSpace
1460                );
1461            },
1462            |next| active.store(next, Ordering::Release),
1463        ));
1464        assert_eq!(active.load(Ordering::Acquire), third.handle().into_raw());
1465        assert_eq!(first_runtime.active_leases.load(Ordering::Acquire), 0);
1466        assert_eq!(second_runtime.active_leases.load(Ordering::Acquire), 0);
1467        assert_eq!(third_runtime.active_leases.load(Ordering::Acquire), 1);
1468        assert_eq!(shared.active_mask(), 0);
1469        assert_eq!(other.active_mask(), 1);
1470
1471        third_runtime.active_leases.store(0, Ordering::Release);
1472        other.deactivate(0);
1473    }
1474
1475    #[test]
1476    fn different_address_space_reusing_same_root_requires_hardware_install() {
1477        assert!(hardware_root_install_required(
1478            0x4000,
1479            0x4000,
1480            HardwareAddressSpaceTransition::DifferentAddressSpace,
1481        ));
1482        assert!(!hardware_root_install_required(
1483            0x4000,
1484            0x4000,
1485            HardwareAddressSpaceTransition::SameAddressSpace,
1486        ));
1487    }
1488}