Skip to main content

starry_kernel/mm/aspace/
lifecycle.rs

1//! Typed ownership and activation protocol for Starry user address spaces.
2//!
3//! An address space has three independent kinds of users: process owners,
4//! short-lived kernel pins, and CPUs that may still have its translations
5//! installed.  Keeping those counters in one object makes it impossible for a
6//! process reference to be mistaken for an MMU activation reference.
7
8use alloc::{
9    borrow::ToOwned,
10    collections::BTreeMap,
11    sync::{Arc, Weak},
12    vec::Vec,
13};
14use core::{
15    ops::{
16        Bound::{Excluded, Unbounded},
17        Deref,
18    },
19    sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering},
20};
21
22use ax_memory_addr::{PhysAddr, VirtAddr};
23use ax_runtime::hal::trap::PageFaultFlags;
24
25use super::{AddrSpace, FaultResult, PageFaultApplyOutcome, TransparentHugePageMode};
26use crate::sync::{IrqMutex, Mutex};
27
28mod work_queue;
29use work_queue::{MmWorkLink, MmWorkQueue};
30
31/// Monotonic identity independent from a page-table root physical address.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct AddressSpaceId(u64);
34
35impl AddressSpaceId {
36    pub(crate) fn allocate() -> Self {
37        static NEXT_ID: AtomicU64 = AtomicU64::new(1);
38        Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
39    }
40
41    /// Returns the stable numeric identity used by TLB requests and tracing.
42    pub const fn get(self) -> u64 {
43        self.0
44    }
45}
46
47/// Software generation of VMA/PTE publication.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
49pub struct VmEpoch(u64);
50
51impl VmEpoch {
52    pub const fn new(value: u64) -> Self {
53        Self(value)
54    }
55
56    pub const fn get(self) -> u64 {
57        self.0
58    }
59
60    pub const fn next(self) -> Self {
61        Self(self.0.saturating_add(1))
62    }
63
64    pub const fn checked_next(self) -> Option<Self> {
65        match self.0.checked_add(1) {
66            Some(value) => Some(Self(value)),
67            None => None,
68        }
69    }
70}
71
72/// Hardware TLB tag plus software generation.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct AddressSpaceTag {
75    pub hardware_tag: u16,
76    pub generation: u64,
77    pub mode: TagMode,
78}
79
80/// Architecture-neutral active-CPU bitset.  Platform code can replace this
81/// alias with a wider mask without changing the ownership API.
82pub type CpuMask = usize;
83
84impl AddressSpaceTag {
85    pub const fn tagged(hardware_tag: u16, generation: u64) -> Self {
86        Self {
87            hardware_tag,
88            generation,
89            mode: TagMode::Tagged,
90        }
91    }
92
93    pub const fn full_flush(generation: u64) -> Self {
94        Self {
95            hardware_tag: 0,
96            generation,
97            mode: TagMode::FullFlush,
98        }
99    }
100
101    pub const fn is_tagged(self) -> bool {
102        matches!(self.mode, TagMode::Tagged)
103    }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum TagMode {
108    Tagged,
109    FullFlush,
110}
111
112/// Result of allocating a hardware address-space tag.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct TagAllocation {
115    pub tag: AddressSpaceTag,
116    /// A generation rollover means the numeric tag may have an older owner.
117    /// Architecture backends must invalidate the incoming tag before making
118    /// its root reachable; an eager all-CPU flush is an allowed optimization.
119    pub rollover: bool,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum TagAllocationError {
124    GenerationExhausted,
125}
126
127/// Small, architecture-neutral tag allocator.
128///
129/// `capacity` is the number of hardware tag values available, including the
130/// reserved zero value.  Values `1..capacity` are handed out in a generation;
131/// zero is never used for a tagged context.  Passing a capacity of zero or one
132/// selects the conservative full-flush mode used when an architecture has no
133/// usable ASID/PCID facility.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct AddressSpaceTagAllocator {
136    capacity: u32,
137    next: u32,
138    generation: u64,
139    mode: TagMode,
140}
141
142impl AddressSpaceTagAllocator {
143    pub const fn new(capacity: u32) -> Self {
144        let capacity = if capacity > (1 << 16) {
145            1 << 16
146        } else {
147            capacity
148        };
149        if capacity <= 1 {
150            Self {
151                capacity,
152                next: 0,
153                generation: 0,
154                mode: TagMode::FullFlush,
155            }
156        } else {
157            Self {
158                capacity,
159                next: 1,
160                generation: 0,
161                mode: TagMode::Tagged,
162            }
163        }
164    }
165
166    pub const fn mode(self) -> TagMode {
167        self.mode
168    }
169
170    pub const fn capacity(self) -> u32 {
171        self.capacity
172    }
173
174    pub const fn generation(self) -> u64 {
175        self.generation
176    }
177
178    pub fn allocate(&mut self) -> Result<TagAllocation, TagAllocationError> {
179        if self.mode == TagMode::FullFlush {
180            return Ok(TagAllocation {
181                tag: AddressSpaceTag::full_flush(self.generation),
182                rollover: false,
183            });
184        }
185        let mut rollover = false;
186        if self.next >= self.capacity {
187            self.rollover()?;
188            rollover = true;
189        }
190        let tag = u16::try_from(self.next).map_err(|_| TagAllocationError::GenerationExhausted)?;
191        self.next = self
192            .next
193            .checked_add(1)
194            .ok_or(TagAllocationError::GenerationExhausted)?;
195        Ok(TagAllocation {
196            tag: AddressSpaceTag::tagged(tag, self.generation),
197            rollover,
198        })
199    }
200
201    /// Forces a new generation after an architecture-wide invalidation.
202    pub fn rollover(&mut self) -> Result<u64, TagAllocationError> {
203        if self.mode == TagMode::FullFlush {
204            return Ok(self.generation);
205        }
206        self.generation = self
207            .generation
208            .checked_add(1)
209            .ok_or(TagAllocationError::GenerationExhausted)?;
210        self.next = 1;
211        Ok(self.generation)
212    }
213}
214
215// Platform capability probing is deliberately kept at the architecture
216// boundary. Every CPU contributes its capability before becoming TLB-ready;
217// the first MM freezes their minimum. Returning one usable value selects tag
218// zero and a full flush for every installation.
219// Architecture code invalidates an incoming nonzero tag before installing it,
220// so a generation rollover cannot expose an inactive stale translation.
221static TAG_ALLOCATOR: IrqMutex<Option<AddressSpaceTagAllocator>> = IrqMutex::new(None);
222
223fn allocate_default_tag(epoch: u64) -> AddressSpaceTag {
224    let mut allocator_slot = TAG_ALLOCATOR.lock();
225    let allocator = allocator_slot.get_or_insert_with(|| {
226        AddressSpaceTagAllocator::new(ax_runtime::hal::cache::freeze_address_space_tag_capacity())
227    });
228    let mode = allocator.mode();
229    let capacity = allocator.capacity();
230    let generation = allocator.generation();
231    let allocation = allocator.allocate();
232    match (mode, capacity, allocation) {
233        (TagMode::Tagged, _, Ok(allocation)) => allocation.tag,
234        (_, _, Ok(allocation)) => AddressSpaceTag::full_flush(epoch.max(allocation.tag.generation)),
235        (_, 0, _) | (_, 1, _) | (_, _, Err(TagAllocationError::GenerationExhausted)) => {
236            AddressSpaceTag::full_flush(epoch.max(generation))
237        }
238    }
239}
240
241impl Default for AddressSpaceTag {
242    fn default() -> Self {
243        Self {
244            hardware_tag: 0,
245            generation: 0,
246            mode: TagMode::FullFlush,
247        }
248    }
249}
250
251/// Root and identity installed by the scheduler on one CPU.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub struct InstalledPageTableRoot {
254    space_id: AddressSpaceId,
255    root: PhysAddr,
256    tag: AddressSpaceTag,
257    epoch: VmEpoch,
258}
259
260impl InstalledPageTableRoot {
261    /// Returns the stable software address-space identity.
262    pub const fn space_id(self) -> AddressSpaceId {
263        self.space_id
264    }
265
266    pub(crate) const fn root(self) -> PhysAddr {
267        self.root
268    }
269
270    /// Returns the hardware tag and its software generation.
271    pub const fn tag(self) -> AddressSpaceTag {
272        self.tag
273    }
274
275    /// Returns the VMA/PTE publication epoch represented by this root.
276    pub const fn epoch(self) -> VmEpoch {
277        self.epoch
278    }
279}
280
281/// Scheduler-visible per-CPU state.  `active_cpus` means that translations for
282/// this address space may still exist in those CPUs' TLBs; it is deliberately
283/// not an affinity mask.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct AddressSpaceCpuState {
286    pub mm_id: AddressSpaceId,
287    pub active_cpus: CpuMask,
288    pub installed_epoch: VmEpoch,
289    pub tag: AddressSpaceTag,
290}
291
292impl AddressSpaceCpuState {
293    pub fn is_active(&self, cpu: usize) -> bool {
294        cpu < usize::BITS as usize && self.active_cpus & (1usize << cpu) != 0
295    }
296}
297
298/// Name used by scheduler code that treats the root, identity, tag and epoch
299/// as one installed context.
300pub type InstalledAddressSpace = InstalledPageTableRoot;
301
302#[repr(u8)]
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum MmState {
305    Live        = 0,
306    Retiring    = 1,
307    Retired     = 2,
308    Reclaiming  = 3,
309    Freed       = 4,
310    NeedsRepair = 5,
311}
312
313impl MmState {
314    fn from_u8(value: u8) -> Self {
315        match value {
316            0 => Self::Live,
317            1 => Self::Retiring,
318            2 => Self::Retired,
319            3 => Self::Reclaiming,
320            4 => Self::Freed,
321            _ => Self::NeedsRepair,
322        }
323    }
324}
325
326struct MmInner {
327    aspace: Arc<Mutex<AddrSpace>>,
328    id: AddressSpaceId,
329    root: AtomicUsize,
330    epoch: Arc<AtomicU64>,
331    tag: AddressSpaceTag,
332    transparent_huge_page_mode: AtomicU8,
333    install_seq: AtomicU64,
334    /// Linearizes ownership-count changes with `Retiring -> Retired` and
335    /// `RetirePermit` creation. The gate is IRQ-safe and never covers page
336    /// table work, allocation, I/O, callbacks, or reclaim.
337    lifecycle_gate: IrqMutex<()>,
338    state: AtomicU8,
339    user_refs: AtomicUsize,
340    kernel_pins: AtomicUsize,
341    active_count: AtomicUsize,
342    active_mask: Arc<AtomicUsize>,
343    runtime_cpu_state: Arc<ax_runtime::thread::AddressSpaceCpuState>,
344    /// Per-CPU counts are needed while an outgoing and incoming task briefly
345    /// overlap during a context switch. A bit alone cannot represent that
346    /// state without leaving stale active bits behind.
347    active_per_cpu: [AtomicUsize; usize::BITS as usize],
348    retire_queued: AtomicBool,
349    /// Allocated with the MM, like Linux's mm_struct::async_put_work. Token
350    /// destruction never needs to allocate a separate deferred-work node.
351    work_link: IrqMutex<MmWorkLink>,
352}
353
354#[derive(Clone, Copy)]
355enum ActivationMode {
356    Exclusive,
357    SchedulerHandoff,
358}
359
360enum ActivationAuthority<'a> {
361    /// A process owner may only start new user execution while the MM is live.
362    UserOwner(&'a AtomicBool),
363    /// An established kernel pin may finish an already-started continuation
364    /// after the last user owner has published `Retiring`.
365    PinnedContinuation,
366}
367
368impl ActivationAuthority<'_> {
369    fn permits(&self, state: MmState) -> bool {
370        match self {
371            Self::UserOwner(owner) => {
372                owner.load(Ordering::Acquire) && matches!(state, MmState::Live)
373            }
374            Self::PinnedContinuation => matches!(state, MmState::Live | MmState::Retiring),
375        }
376    }
377}
378
379impl MmInner {
380    fn state(&self) -> MmState {
381        MmState::from_u8(self.state.load(Ordering::Acquire))
382    }
383
384    fn transparent_huge_page_mode(&self) -> TransparentHugePageMode {
385        TransparentHugePageMode::from_storage(
386            self.transparent_huge_page_mode.load(Ordering::Acquire),
387        )
388    }
389
390    fn is_quiescent_locked(&self) -> bool {
391        self.user_refs.load(Ordering::Relaxed) == 0
392            && self.kernel_pins.load(Ordering::Relaxed) == 0
393            && self.active_count.load(Ordering::Relaxed) == 0
394            && self.active_mask.load(Ordering::Relaxed) == 0
395    }
396
397    fn maybe_retire_locked(&self) {
398        if !self.is_quiescent_locked() {
399            return;
400        }
401        let _ = self.state.compare_exchange(
402            MmState::Retiring as u8,
403            MmState::Retired as u8,
404            Ordering::AcqRel,
405            Ordering::Acquire,
406        );
407    }
408
409    fn take_retire_permit(inner: &Arc<Self>) -> Option<RetirePermit> {
410        let _gate = inner.lifecycle_gate.lock();
411        inner.maybe_retire_locked();
412        if inner.state() != MmState::Retired
413            || !inner.is_quiescent_locked()
414            || inner
415                .retire_queued
416                .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
417                .is_err()
418        {
419            return None;
420        }
421        Some(RetirePermit(Some(inner.clone())))
422    }
423
424    fn try_pin(inner: &Arc<Self>) -> Result<MmPin, PinError> {
425        let _gate = inner.lifecycle_gate.lock();
426        if inner.state() != MmState::Live {
427            return Err(PinError::Retired);
428        }
429        let pins = inner.kernel_pins.load(Ordering::Relaxed);
430        let Some(next_pins) = pins.checked_add(1) else {
431            return Err(PinError::Overflow);
432        };
433        inner.kernel_pins.store(next_pins, Ordering::Release);
434        Ok(MmPin(inner.clone()))
435    }
436
437    fn installed(&self) -> InstalledAddressSpace {
438        loop {
439            let sequence = self.install_seq.load(Ordering::Acquire);
440            if sequence & 1 != 0 {
441                core::hint::spin_loop();
442                continue;
443            }
444            let root = self.root.load(Ordering::Acquire);
445            let epoch = self.epoch.load(Ordering::Acquire);
446            if self.install_seq.load(Ordering::Acquire) == sequence {
447                let mut tag = self.tag;
448                if tag.mode == TagMode::FullFlush {
449                    // Full-flush mode has no reusable hardware context; use
450                    // the publication epoch as the software generation so a
451                    // stale install can still be diagnosed by its identity.
452                    tag.generation = epoch;
453                }
454                return InstalledAddressSpace {
455                    space_id: self.id,
456                    root: PhysAddr::from_usize(root),
457                    tag,
458                    epoch: VmEpoch::new(epoch),
459                };
460            }
461        }
462    }
463
464    fn acquire_activation(
465        inner: &Arc<Self>,
466        cpu: usize,
467        mode: ActivationMode,
468        authority: ActivationAuthority<'_>,
469    ) -> Result<ActivationLease, ActivationError> {
470        if cpu >= ax_runtime::hal::cpu_num().min(usize::BITS as usize) {
471            return Err(ActivationError::InvalidCpu);
472        }
473        let _gate = inner.lifecycle_gate.lock();
474        if !authority.permits(inner.state()) {
475            return Err(ActivationError::Retired);
476        }
477        let bit = 1usize << cpu;
478        let cpu_refs = &inner.active_per_cpu[cpu];
479        if matches!(mode, ActivationMode::Exclusive) && cpu_refs.load(Ordering::Relaxed) != 0 {
480            return Err(ActivationError::AlreadyActive);
481        }
482        let previous_cpu = cpu_refs.load(Ordering::Relaxed);
483        let Some(next_cpu) = previous_cpu.checked_add(1) else {
484            return Err(ActivationError::Overflow);
485        };
486        let previous_total = inner.active_count.load(Ordering::Relaxed);
487        let Some(next_total) = previous_total.checked_add(1) else {
488            return Err(ActivationError::Overflow);
489        };
490        if previous_cpu == 0 {
491            inner.active_mask.fetch_or(bit, Ordering::Release);
492        }
493        cpu_refs.store(next_cpu, Ordering::Release);
494        inner.active_count.store(next_total, Ordering::Release);
495        Ok(ActivationLease {
496            inner: inner.clone(),
497            cpu,
498            installed: inner.installed(),
499            released: false,
500        })
501    }
502}
503
504/// Weak identity index used only to resolve an explicit reverse-map entry.
505/// File-cache callbacks never retain an address-space pointer of their own;
506/// they name an MM by `AddressSpaceId` and obtain a typed kernel pin here.
507///
508/// This is task-context metadata, not an IRQ, PTE, rmap, or allocator-pressure
509/// lock.  A sleepable mutex therefore permits the `BTreeMap` to allocate a
510/// node while inserting, without turning every fork/exec/exit into a complete
511/// path-copy of all live MMs.  The allocator's pressure hook never enters an
512/// MM endpoint, so allocation cannot recurse into this mutex.  Lookups clone
513/// only one `Weak`, and removed values are destroyed after releasing the lock.
514type MmRegistry = BTreeMap<AddressSpaceId, Weak<MmInner>>;
515static MM_REGISTRY: Mutex<MmRegistry> = Mutex::new(BTreeMap::new());
516static LAZY_FREE_SCAN_CURSOR: AtomicU64 = AtomicU64::new(0);
517
518fn next_registry_id_after<V>(
519    registry: &BTreeMap<AddressSpaceId, V>,
520    after: AddressSpaceId,
521) -> Option<AddressSpaceId> {
522    registry
523        .range((Excluded(after), Unbounded))
524        .next()
525        .or_else(|| registry.first_key_value())
526        .map(|(&id, _)| id)
527}
528
529/// Selects and retains at most one registry entry while holding the identity
530/// lock. The caller must still acquire an `MmPin` before entering the address
531/// space. The returned Arc is released after all MM work, never from the
532/// registry critical section.
533fn next_registered_mm_after(
534    after: AddressSpaceId,
535) -> Option<(AddressSpaceId, Option<Arc<MmInner>>)> {
536    let registry = MM_REGISTRY.lock();
537    let id = next_registry_id_after(&registry, after)?;
538    Some((id, registry.get(&id).and_then(Weak::upgrade)))
539}
540
541/// Visits a bounded, round-robin snapshot of registry identities.
542///
543/// Selection and reclamation are deliberately injected separately: the
544/// registry lock only retains one item at a time, while the caller performs
545/// all address-space work after that lock has been released.  Persisting the
546/// last selected identity prevents a small page quota from repeatedly
547/// favoring the lowest address-space ID.
548fn reclaim_registered_items<T>(
549    limit: usize,
550    visit_limit: usize,
551    cursor: &AtomicU64,
552    mut next: impl FnMut(AddressSpaceId) -> Option<(AddressSpaceId, T)>,
553    mut reclaim: impl FnMut(T, usize) -> usize,
554) -> usize {
555    if limit == 0 {
556        return 0;
557    }
558    let mut reclaimed = 0;
559    let mut visited = 0;
560    let mut first_visited = None;
561    while visited < visit_limit && reclaimed < limit {
562        let after = AddressSpaceId(cursor.load(Ordering::Acquire));
563        let Some((id, item)) = next(after) else {
564            break;
565        };
566        if first_visited == Some(id) {
567            break;
568        }
569        if first_visited.is_none() {
570            first_visited = Some(id);
571        }
572        cursor.store(id.get(), Ordering::Release);
573        visited += 1;
574        let remaining = limit - reclaimed;
575        let pages = reclaim(item, remaining);
576        debug_assert!(pages <= remaining);
577        reclaimed += pages.min(remaining);
578    }
579    reclaimed
580}
581
582fn registered_mm(id: AddressSpaceId) -> Option<Weak<MmInner>> {
583    MM_REGISTRY.lock().get(&id).cloned()
584}
585
586fn register_mm(inner: &Arc<MmInner>) -> Result<(), MmCreateError> {
587    let mut registry = MM_REGISTRY.lock();
588    if registry.contains_key(&inner.id) {
589        return Err(MmCreateError::DuplicateIdentity);
590    }
591    let displaced = registry.insert(inner.id, Arc::downgrade(inner));
592    drop(registry);
593    debug_assert!(displaced.is_none());
594    drop(displaced);
595    Ok(())
596}
597
598fn unregister_mm(inner: &Arc<MmInner>) {
599    let removed = remove_registered_mm(inner.id);
600    debug_assert!(removed, "address-space identity was not registered");
601}
602
603fn remove_registered_mm(id: AddressSpaceId) -> bool {
604    let removed = MM_REGISTRY.lock().remove(&id);
605    let found = removed.is_some();
606    drop(removed);
607    found
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
611pub(crate) enum RmapMmLookupError {
612    Gone,
613    Busy,
614}
615
616/// Returns whether userspace still owns the address space named by `id`.
617///
618/// This is a lifecycle observation, not a pin: callers may use it to decide
619/// whether logical VMA state remains Linux-visible, but must acquire an
620/// [`MmPin`] before touching VMA or PTE contents.  In particular, a retiring
621/// MM is already absent from memfd writable-mapping checks even though its
622/// frames remain quarantined until CPU activations and kernel pins drain.
623pub(crate) fn is_address_space_live(id: AddressSpaceId) -> bool {
624    match registered_mm(id) {
625        // Fork and exec build a complete MM before publishing its first
626        // MmHandle.  Treat that preparation window as live so a concurrent
627        // F_SEAL_WRITE cannot slip between VMA publication and lifecycle
628        // registration.
629        None => true,
630        Some(weak) => weak
631            .upgrade()
632            .is_some_and(|inner| inner.state() == MmState::Live),
633    }
634}
635
636/// Resolves one rmap identity into a short-lived kernel pin.  A retiring MM is
637/// deliberately reported as Busy: an eviction must not assume that a missing
638/// new pin means its stale PTE has already disappeared.
639pub(crate) fn pin_mm_for_rmap(id: AddressSpaceId) -> Result<MmPin, RmapMmLookupError> {
640    let weak = registered_mm(id);
641    let Some(weak) = weak else {
642        return Err(RmapMmLookupError::Gone);
643    };
644    let Some(inner) = weak.upgrade() else {
645        remove_registered_mm(id);
646        return Err(RmapMmLookupError::Gone);
647    };
648    match inner.state() {
649        MmState::Live => MmInner::try_pin(&inner).map_err(|_| RmapMmLookupError::Busy),
650        MmState::Freed => Err(RmapMmLookupError::Gone),
651        MmState::Retiring | MmState::Retired | MmState::Reclaiming | MmState::NeedsRepair => {
652            Err(RmapMmLookupError::Busy)
653        }
654    }
655}
656
657/// Explicit process ownership of an address space.
658pub struct MmHandle {
659    inner: Arc<MmInner>,
660    /// Whether this particular handle still represents one user ownership
661    /// reference.  A process may retire its owner before `ProcessData` itself
662    /// is dropped (for example while becoming a zombie); keeping a non-owner
663    /// view lets diagnostics finish without resurrecting the MM.
664    owner: AtomicBool,
665}
666
667impl MmHandle {
668    /// Creates the first user owner for an address space.
669    pub(crate) fn from_arc(aspace: Arc<Mutex<AddrSpace>>) -> Result<Self, MmCreateError> {
670        let epoch = aspace.lock().vm_epoch().get();
671        Self::from_arc_with_tag(aspace, allocate_default_tag(epoch))
672    }
673
674    /// Creates the first owner with an explicitly selected architecture tag.
675    /// The default [`Self::from_arc`] path selects tagged or full-flush mode
676    /// from the architecture capability probe used by the shared allocator.
677    pub(crate) fn from_arc_with_tag(
678        aspace: Arc<Mutex<AddrSpace>>,
679        tag: AddressSpaceTag,
680    ) -> Result<Self, MmCreateError> {
681        let (id, root, epoch, epoch_source, active_mask) = {
682            let guard = aspace.lock();
683            (
684                guard.address_space_id(),
685                guard.materialized_root().as_usize(),
686                guard.vm_epoch().get(),
687                guard.published_epoch_source(),
688                guard.tlb_targets(),
689            )
690        };
691        let tag = if tag.mode == TagMode::FullFlush {
692            AddressSpaceTag::full_flush(epoch)
693        } else {
694            tag
695        };
696        let handle = Self {
697            inner: Arc::new(MmInner {
698                aspace,
699                id,
700                root: AtomicUsize::new(root),
701                epoch: epoch_source,
702                tag,
703                transparent_huge_page_mode: AtomicU8::new(TransparentHugePageMode::Enabled as u8),
704                install_seq: AtomicU64::new(0),
705                lifecycle_gate: IrqMutex::new(()),
706                state: AtomicU8::new(MmState::Live as u8),
707                user_refs: AtomicUsize::new(1),
708                kernel_pins: AtomicUsize::new(0),
709                active_count: AtomicUsize::new(0),
710                runtime_cpu_state: Arc::new(
711                    ax_runtime::thread::AddressSpaceCpuState::with_mm_active_mask(
712                        PhysAddr::from_usize(root),
713                        active_mask.clone(),
714                    ),
715                ),
716                active_mask,
717                active_per_cpu: core::array::from_fn(|_| AtomicUsize::new(0)),
718                retire_queued: AtomicBool::new(false),
719                work_link: IrqMutex::new(MmWorkLink::default()),
720            }),
721            owner: AtomicBool::new(true),
722        };
723        register_mm(&handle.inner)?;
724        Ok(handle)
725    }
726
727    pub fn id(&self) -> AddressSpaceId {
728        self.inner.id
729    }
730
731    pub fn state(&self) -> MmState {
732        self.inner.state()
733    }
734
735    pub fn user_refs(&self) -> usize {
736        self.inner.user_refs.load(Ordering::Acquire)
737    }
738
739    pub fn kernel_pins(&self) -> usize {
740        self.inner.kernel_pins.load(Ordering::Acquire)
741    }
742
743    pub fn active_cpus(&self) -> usize {
744        self.inner.active_count.load(Ordering::Acquire)
745    }
746
747    pub fn active_cpu_mask(&self) -> usize {
748        self.inner.active_mask.load(Ordering::Acquire)
749    }
750
751    /// Returns an observation of the single scheduler-owned CPU footprint.
752    ///
753    /// This value is deliberately read-only: callers cannot publish a second
754    /// active mask beside the `ActivationLease` counters in `MmInner`.
755    pub fn cpu_state(&self) -> AddressSpaceCpuState {
756        let installed = self.installed();
757        AddressSpaceCpuState {
758            mm_id: installed.space_id,
759            active_cpus: self.active_cpu_mask(),
760            installed_epoch: installed.epoch,
761            tag: installed.tag,
762        }
763    }
764
765    pub fn installed(&self) -> InstalledAddressSpace {
766        self.inner.installed()
767    }
768
769    /// Returns the process policy attached to this MM identity.
770    pub fn transparent_huge_page_mode(&self) -> TransparentHugePageMode {
771        self.inner.transparent_huge_page_mode()
772    }
773
774    /// Changes the process-wide THP policy while excluding concurrent faults.
775    pub fn set_transparent_huge_page_mode(&self, mode: TransparentHugePageMode) {
776        let _aspace = self.inner.aspace.lock();
777        self.inner
778            .transparent_huge_page_mode
779            .store(mode as u8, Ordering::Release);
780    }
781
782    /// Refreshes the software view after a page-table root replacement.
783    pub fn refresh_installation(&self) {
784        let guard = self.inner.aspace.lock();
785        // A switch can read the descriptor in IRQ context. Exclude that reader
786        // while the sequence is odd, including on a preemptible RT kernel.
787        let _gate = self.inner.lifecycle_gate.lock();
788        self.inner.install_seq.fetch_add(1, Ordering::AcqRel);
789        self.inner
790            .root
791            .store(guard.materialized_root().as_usize(), Ordering::Release);
792        self.inner
793            .epoch
794            .store(guard.vm_epoch().get(), Ordering::Release);
795        self.inner.install_seq.fetch_add(1, Ordering::Release);
796    }
797
798    /// Explicitly duplicates a process owner (`fork`, `CLONE_VM`, or `vfork`).
799    pub fn clone_user_ref(&self) -> Result<Self, CloneUserRefError> {
800        let _gate = self.inner.lifecycle_gate.lock();
801        if !self.owner.load(Ordering::Relaxed) || self.inner.state() != MmState::Live {
802            return Err(CloneUserRefError::Retired);
803        }
804        let refs = self.inner.user_refs.load(Ordering::Relaxed);
805        let Some(next_refs) = refs.checked_add(1) else {
806            return Err(CloneUserRefError::Overflow);
807        };
808        self.inner.user_refs.store(next_refs, Ordering::Release);
809        Ok(Self {
810            inner: self.inner.clone(),
811            owner: AtomicBool::new(true),
812        })
813    }
814
815    pub fn pin(&self) -> Result<MmPin, PinError> {
816        MmInner::try_pin(&self.inner)
817    }
818
819    pub fn activation(&self, cpu: usize) -> Result<ActivationLease, ActivationError> {
820        MmInner::acquire_activation(
821            &self.inner,
822            cpu,
823            ActivationMode::Exclusive,
824            ActivationAuthority::UserOwner(&self.owner),
825        )
826    }
827
828    /// Transitions the last user owner to `Retiring` without reclaiming data.
829    pub fn retire_if_quiescent(&self) -> Option<RetirePermit> {
830        MmInner::take_retire_permit(&self.inner)
831    }
832
833    /// Releases this handle's process ownership while retaining a non-owning
834    /// view of the address space.  This is the operation used by process exit;
835    /// unlike cloning and dropping a temporary handle, it cannot keep the
836    /// owner count artificially non-zero.
837    pub fn release_user_ref(&self) -> Option<RetirePermit> {
838        {
839            let _gate = self.inner.lifecycle_gate.lock();
840            if self.owner.swap(false, Ordering::Relaxed) {
841                let previous = self.inner.user_refs.load(Ordering::Relaxed);
842                debug_assert!(previous > 0, "MmHandle user reference underflow");
843                self.inner.user_refs.store(previous - 1, Ordering::Release);
844                if previous == 1 {
845                    let _ = self.inner.state.compare_exchange(
846                        MmState::Live as u8,
847                        MmState::Retiring as u8,
848                        Ordering::AcqRel,
849                        Ordering::Acquire,
850                    );
851                }
852                self.inner.maybe_retire_locked();
853            }
854        }
855        self.retire_if_quiescent()
856    }
857}
858
859impl Drop for MmHandle {
860    fn drop(&mut self) {
861        {
862            let _gate = self.inner.lifecycle_gate.lock();
863            if !self.owner.swap(false, Ordering::Relaxed) {
864                return;
865            }
866            let previous = self.inner.user_refs.load(Ordering::Relaxed);
867            debug_assert!(previous > 0, "MmHandle user reference underflow");
868            self.inner.user_refs.store(previous - 1, Ordering::Release);
869            if previous == 1 {
870                let _ = self.inner.state.compare_exchange(
871                    MmState::Live as u8,
872                    MmState::Retiring as u8,
873                    Ordering::AcqRel,
874                    Ordering::Acquire,
875                );
876            }
877            self.inner.maybe_retire_locked();
878        }
879        queue_if_retired(&self.inner);
880    }
881}
882
883/// Short-lived kernel ownership.  It is safe to drop in IRQ context because
884/// only counters and preallocated queue links are touched; actual page-table
885/// destruction belongs to the sleepable reclaimer.
886pub struct MmPin(Arc<MmInner>);
887
888impl MmPin {
889    pub fn id(&self) -> AddressSpaceId {
890        self.0.id
891    }
892
893    /// Returns the process policy attached to the pinned MM identity.
894    pub fn transparent_huge_page_mode(&self) -> TransparentHugePageMode {
895        self.0.transparent_huge_page_mode()
896    }
897
898    /// Changes the process-wide THP policy while excluding concurrent faults.
899    pub fn set_transparent_huge_page_mode(&self, mode: TransparentHugePageMode) {
900        let _aspace = self.0.aspace.lock();
901        self.0
902            .transparent_huge_page_mode
903            .store(mode as u8, Ordering::Release);
904    }
905
906    /// Resolves a fault under the MM's process-wide THP policy.
907    pub fn handle_page_fault_result(
908        &self,
909        vaddr: VirtAddr,
910        access_flags: PageFaultFlags,
911    ) -> FaultResult {
912        let plan = {
913            let aspace = self.0.aspace.lock();
914            let mode = self.0.transparent_huge_page_mode();
915            match aspace.plan_page_fault(vaddr, access_flags, mode) {
916                Ok(plan) => plan,
917                Err(result) => return result,
918            }
919        };
920        // Allocation, file I/O and page-cache reservation happen with no
921        // address-space metadata lock held. The apply phase below rechecks the
922        // exact VMA epoch and PTE preimage before publishing anything.
923        let prepared = match AddrSpace::prepare_page_fault(plan) {
924            Ok(prepared) => prepared,
925            Err(result) => return result,
926        };
927        let mut attempt = prepared.into_apply_attempt();
928        let outcome = {
929            let mut aspace = self.0.aspace.lock();
930            aspace.apply_prepared_page_fault(&mut attempt)
931        };
932        let result = match outcome {
933            PageFaultApplyOutcome::Complete(result) => result,
934            PageFaultApplyOutcome::Cancel(result) => {
935                if attempt.cancel().is_ok() {
936                    result
937                } else {
938                    FaultResult::Retry
939                }
940            }
941            PageFaultApplyOutcome::NeedsRepair(result) => {
942                attempt.release_to_repair_state();
943                result
944            }
945            PageFaultApplyOutcome::CancelPendingTlb { request, targets } => {
946                // Cancellation releases candidate frames and page-table
947                // deposits outside the MM lock. Servicing the old receipt is
948                // also lock-external; merely returning Retry would strand it
949                // and make every subsequent refault hit the same blocker.
950                if attempt.cancel().is_ok()
951                    && AddrSpace::flush_tlb_requests(core::slice::from_ref(&request), &targets)
952                        .is_ok()
953                {
954                    let aspace = self.0.aspace.lock();
955                    let _ = aspace.acknowledge_tlb_requests(core::slice::from_ref(&request));
956                }
957                FaultResult::Retry
958            }
959            PageFaultApplyOutcome::PendingTlb { request, targets } => {
960                if AddrSpace::flush_tlb_requests(core::slice::from_ref(&request), &targets).is_err()
961                {
962                    FaultResult::Retry
963                } else {
964                    let aspace = self.0.aspace.lock();
965                    if aspace
966                        .acknowledge_tlb_requests(core::slice::from_ref(&request))
967                        .is_ok()
968                    {
969                        FaultResult::Handled
970                    } else {
971                        FaultResult::Retry
972                    }
973                }
974            }
975        };
976        if matches!(result, FaultResult::Handled) {
977            ax_cpu::mmu::update_mmu_cache(vaddr);
978        }
979        result
980    }
981
982    /// Resolves a fault for a kernel faultable user-copy scope.
983    pub fn handle_page_fault(&self, vaddr: VirtAddr, access_flags: PageFaultFlags) -> bool {
984        loop {
985            match self.handle_page_fault_result(vaddr, access_flags) {
986                FaultResult::Handled => return true,
987                // The failed transaction has released its metadata guard and
988                // cancelled its candidate owners. Reacquire a fresh VMA/PTE
989                // snapshot after allowing the competing publisher to progress;
990                // a transient conflict must not take the copy's EFAULT fixup.
991                FaultResult::Retry => crate::task::yield_now(),
992                _ => return false,
993            }
994        }
995    }
996
997    /// Acquires scheduler activation for an already-pinned kernel
998    /// continuation. The pin is the typed proof that `Retiring` still has a
999    /// live executor and therefore cannot advance to `Retired`.
1000    pub(crate) fn activation_for_switch(
1001        &self,
1002        cpu: usize,
1003    ) -> Result<ActivationLease, ActivationError> {
1004        MmInner::acquire_activation(
1005            &self.0,
1006            cpu,
1007            ActivationMode::SchedulerHandoff,
1008            ActivationAuthority::PinnedContinuation,
1009        )
1010    }
1011}
1012
1013impl Deref for MmPin {
1014    type Target = Mutex<AddrSpace>;
1015
1016    fn deref(&self) -> &Self::Target {
1017        &self.0.aspace
1018    }
1019}
1020
1021impl Drop for MmPin {
1022    fn drop(&mut self) {
1023        {
1024            let _gate = self.0.lifecycle_gate.lock();
1025            let previous = self.0.kernel_pins.load(Ordering::Relaxed);
1026            debug_assert!(previous > 0, "MmPin reference underflow");
1027            self.0.kernel_pins.store(previous - 1, Ordering::Release);
1028            self.0.maybe_retire_locked();
1029        }
1030        queue_if_retired(&self.0);
1031    }
1032}
1033
1034/// A per-CPU activation.  The scheduler owns the value and must consume/drop
1035/// it only after installing another root or the kernel root.
1036pub struct ActivationLease {
1037    inner: Arc<MmInner>,
1038    cpu: usize,
1039    installed: InstalledPageTableRoot,
1040    released: bool,
1041}
1042
1043impl ActivationLease {
1044    pub fn installed(&self) -> InstalledPageTableRoot {
1045        self.installed
1046    }
1047
1048    pub const fn cpu(&self) -> usize {
1049        self.cpu
1050    }
1051
1052    /// Transfers a pre-acquired lease into the runtime's inline switch storage.
1053    pub(crate) fn into_scheduler_activation(
1054        mut self,
1055    ) -> ax_runtime::thread::SchedulerAddressSpaceActivation {
1056        let activation = ax_runtime::thread::SchedulerAddressSpaceActivation::new(
1057            task_address_space(self.installed),
1058            self.cpu,
1059            self.inner.clone(),
1060        );
1061        self.released = true;
1062        activation
1063    }
1064
1065    /// Consumes the lease after the architecture has installed a different
1066    /// address-space root on this CPU.
1067    ///
1068    /// A plain `Drop` deliberately does not clear the active bit: losing a
1069    /// lease before the root write must leak retirement progress rather than
1070    /// permit a stale hardware root to be reclaimed.
1071    pub(crate) fn release_after_root_switch(mut self) {
1072        release_activation_accounting(&self.inner, self.cpu);
1073        self.released = true;
1074    }
1075
1076    /// Consumes the lease after an offline path has installed the kernel root
1077    /// and completed its local full TLB flush.
1078    pub fn release_after_kernel_switch(self) {
1079        self.release_after_root_switch();
1080    }
1081}
1082
1083impl Drop for ActivationLease {
1084    fn drop(&mut self) {
1085        if !self.released {
1086            abandon_activation(self.inner.clone(), self.cpu);
1087        }
1088    }
1089}
1090
1091fn abandon_activation(inner: Arc<MmInner>, cpu: usize) {
1092    {
1093        let _gate = inner.lifecycle_gate.lock();
1094        inner
1095            .state
1096            .store(MmState::NeedsRepair as u8, Ordering::Release);
1097    }
1098    warn!(
1099        "address-space activation for mm {} cpu {} dropped before root-switch proof",
1100        inner.id.get(),
1101        cpu
1102    );
1103    enqueue_repair_candidate(inner);
1104}
1105
1106fn release_activation_accounting(inner: &Arc<MmInner>, cpu: usize) {
1107    {
1108        let _gate = inner.lifecycle_gate.lock();
1109        let previous = inner.active_count.load(Ordering::Relaxed);
1110        debug_assert!(previous > 0, "ActivationLease reference underflow");
1111        inner.active_count.store(previous - 1, Ordering::Release);
1112        if cpu < usize::BITS as usize {
1113            let cpu_refs = &inner.active_per_cpu[cpu];
1114            let previous_cpu = cpu_refs.load(Ordering::Relaxed);
1115            debug_assert!(previous_cpu > 0, "per-CPU activation reference underflow");
1116            cpu_refs.store(previous_cpu - 1, Ordering::Release);
1117            if previous_cpu == 1 {
1118                inner
1119                    .active_mask
1120                    .fetch_and(!(1usize << cpu), Ordering::Release);
1121            }
1122        }
1123        inner.maybe_retire_locked();
1124    }
1125    queue_if_retired(inner);
1126}
1127
1128impl ax_runtime::thread::SchedulerAddressSpaceOwner for MmInner {
1129    fn release_after_root_switch(
1130        self: Arc<Self>,
1131        proof: ax_runtime::thread::AddressSpaceSwitchProof,
1132    ) {
1133        release_activation_accounting(&self, proof.cpu());
1134    }
1135
1136    fn cancel_before_install(self: Arc<Self>, cpu: usize) {
1137        release_activation_accounting(&self, cpu);
1138    }
1139
1140    fn abandon(self: Arc<Self>, cpu: usize) {
1141        abandon_activation(self, cpu);
1142    }
1143}
1144
1145fn task_address_space(installed: InstalledAddressSpace) -> ax_hal::context::InstalledAddressSpace {
1146    ax_hal::context::InstalledAddressSpace::user(
1147        installed.space_id().get(),
1148        installed.root(),
1149        installed.tag().hardware_tag,
1150        installed.tag().generation,
1151        installed.epoch().get(),
1152        match installed.tag().mode {
1153            TagMode::Tagged => ax_hal::context::InstalledAddressSpaceMode::Tagged,
1154            TagMode::FullFlush => ax_hal::context::InstalledAddressSpaceMode::FullFlush,
1155        },
1156    )
1157    .expect("typed Starry address-space installation must remain valid")
1158}
1159
1160/// Task ownership can end before the CPU releases its lazy MM. The independent
1161/// inner Arc anchors callback storage until the runtime reaps its owning token.
1162struct RuntimeMmOwner {
1163    _inner: Arc<MmInner>,
1164    pin: IrqMutex<Option<MmPin>>,
1165}
1166
1167// SAFETY: the pin permits activations of Live/Retiring MM state. Each activation
1168// publishes the shared TLB target bit under the lifecycle gate. `inner` remains
1169// owned by the runtime token until all CPU leases have drained, so the IRQ-off
1170// release callbacks cannot drop the final page-table or MM-shell allocation.
1171unsafe impl ax_runtime::thread::UserAddressSpaceOwner for RuntimeMmOwner {
1172    fn prepare_activation(
1173        &self,
1174        cpu: usize,
1175    ) -> Result<
1176        ax_runtime::thread::SchedulerAddressSpaceActivation,
1177        ax_runtime::task::thread::TaskError,
1178    > {
1179        self.pin
1180            .lock()
1181            .as_ref()
1182            .ok_or(ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)?
1183            .activation_for_switch(cpu)
1184            .map(ActivationLease::into_scheduler_activation)
1185            .map_err(|_| ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)
1186    }
1187
1188    fn detach_from_task(&self) {
1189        let pin = self.pin.lock().take();
1190        drop(pin);
1191    }
1192}
1193
1194impl MmHandle {
1195    /// Prepares task-scoped MM ownership before publishing a runnable task.
1196    pub(crate) fn scheduler_address_space(
1197        &self,
1198    ) -> Result<ax_runtime::thread::TaskAddressSpace, ax_runtime::task::thread::TaskError> {
1199        let pin = self
1200            .pin()
1201            .map_err(|_| ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)?;
1202        ax_runtime::thread::TaskAddressSpace::new_managed(
1203            self.installed().root(),
1204            self.inner.runtime_cpu_state.clone(),
1205            RuntimeMmOwner {
1206                _inner: self.inner.clone(),
1207                pin: IrqMutex::new(Some(pin)),
1208            },
1209        )
1210    }
1211}
1212
1213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1214pub enum ActivationError {
1215    Retired,
1216    AlreadyActive,
1217    InvalidCpu,
1218    Overflow,
1219}
1220
1221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1222pub enum CloneUserRefError {
1223    Retired,
1224    Overflow,
1225}
1226
1227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1228pub enum PinError {
1229    Retired,
1230    Overflow,
1231}
1232
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum MmCreateError {
1235    DuplicateIdentity,
1236}
1237
1238/// Permission to perform potentially sleeping destruction after quiescence.
1239pub struct RetirePermit(Option<Arc<MmInner>>);
1240
1241/// Retired MMs are queued as inert permits; a sleepable reaper must call
1242/// [`reap_retired`] from process context.  No page-table or backend cleanup is
1243/// performed by `Drop` of a handle/pin/activation token.
1244static RETIRE_QUEUE: IrqMutex<MmWorkQueue> = IrqMutex::new(MmWorkQueue::new());
1245static REPAIR_QUEUE: IrqMutex<MmWorkQueue> = IrqMutex::new(MmWorkQueue::new());
1246static RECLAIMER_STARTED: AtomicBool = AtomicBool::new(false);
1247static REPAIR_RETRY_REQUESTED: AtomicBool = AtomicBool::new(false);
1248
1249struct CoalescedReclaimRequest {
1250    pending: AtomicBool,
1251}
1252
1253impl CoalescedReclaimRequest {
1254    const fn new() -> Self {
1255        Self {
1256            pending: AtomicBool::new(false),
1257        }
1258    }
1259
1260    fn request(&self) {
1261        self.pending.store(true, Ordering::Release);
1262    }
1263
1264    fn run_one_batch(&self, limit: usize, reclaim: impl FnOnce(usize) -> usize) -> usize {
1265        if limit == 0 || !self.pending.swap(false, Ordering::AcqRel) {
1266            return 0;
1267        }
1268        let reclaimed = reclaim(limit);
1269        if reclaimed >= limit {
1270            // Hitting the batch limit means another eligible page may remain.
1271            // Keep the edge set for the next bounded worker pass.
1272            self.request();
1273        }
1274        reclaimed
1275    }
1276}
1277
1278static LAZY_FREE_RECLAIM: CoalescedReclaimRequest = CoalescedReclaimRequest::new();
1279#[cfg(test)]
1280static LAZY_FREE_RECLAIM_REQUESTS: AtomicUsize = AtomicUsize::new(0);
1281
1282/// Coalesces `MADV_FREE` publications into one sleepable worker pass.
1283///
1284/// Linux makes lazy-free pages reclaimable on its LRU and only scans them from
1285/// a reclaim invocation; it does not poll every live `mm` at a fixed interval.
1286/// Starry's current reclaim engine has no anonymous LRU yet, so this bit is the
1287/// allocation-free publication edge between the VMA transaction and worker.
1288pub(super) fn request_lazy_free_reclaim() {
1289    #[cfg(test)]
1290    LAZY_FREE_RECLAIM_REQUESTS.fetch_add(1, Ordering::Relaxed);
1291    LAZY_FREE_RECLAIM.request();
1292}
1293
1294#[cfg(test)]
1295pub(super) fn lazy_free_reclaim_request_count_for_test() -> usize {
1296    LAZY_FREE_RECLAIM_REQUESTS.load(Ordering::Relaxed)
1297}
1298
1299fn queue_if_retired(inner: &Arc<MmInner>) {
1300    if let Some(permit) = MmInner::take_retire_permit(inner) {
1301        enqueue_retire(permit);
1302    }
1303}
1304
1305/// Queues a permit returned by an explicit owner release, without allocation.
1306pub fn enqueue_retire(permit: RetirePermit) {
1307    drop(permit);
1308}
1309
1310fn enqueue_mm_work(queue: &IrqMutex<MmWorkQueue>, inner: Arc<MmInner>) {
1311    let duplicate = queue.lock().push(inner);
1312    // The existing queue entry owns the MM on this path; release the extra
1313    // reference only after dropping the IRQ-safe queue guard.
1314    drop(duplicate);
1315}
1316
1317fn enqueue_repair_candidate(inner: Arc<MmInner>) {
1318    enqueue_mm_work(&REPAIR_QUEUE, inner);
1319}
1320
1321/// Reclaims up to `limit` retired address spaces.  This function is deliberately
1322/// explicit so callers can schedule it on a sleepable kernel worker; it never
1323/// holds the queue lock while taking the address-space mutex.
1324pub fn reap_retired(limit: usize) -> (usize, usize) {
1325    if limit == 0 {
1326        return (0, 0);
1327    }
1328    let count = {
1329        let queue = RETIRE_QUEUE.lock();
1330        limit.min(queue.len())
1331    };
1332    let mut reclaimed = 0;
1333    let mut failed = 0;
1334    for _ in 0..count {
1335        let Some(inner) = RETIRE_QUEUE.lock().pop() else {
1336            break;
1337        };
1338        if inner.state() == MmState::Freed {
1339            // The producer may still be dropping its handle after enqueue.
1340            // Atomically take the final owner here, rather than letting its
1341            // eventual IRQ-context Arc drop destroy the root and metadata.
1342            release_mm_shell(inner);
1343            continue;
1344        }
1345        match RetirePermit(Some(inner)).reclaim() {
1346            Ok(()) => reclaimed += 1,
1347            Err(_) => failed += 1,
1348        }
1349    }
1350    (reclaimed, failed)
1351}
1352
1353/// Reclaims anonymous `MADV_FREE` pages from live address spaces without
1354/// retaining the global identity lock across an address-space mutex.
1355pub fn reclaim_live_lazy_free_pages(limit: usize) -> usize {
1356    if limit == 0 {
1357        return 0;
1358    }
1359    let visit_limit = {
1360        let registry = MM_REGISTRY.lock();
1361        registry.len()
1362    };
1363    reclaim_registered_items(
1364        limit,
1365        visit_limit,
1366        &LAZY_FREE_SCAN_CURSOR,
1367        next_registered_mm_after,
1368        |inner, remaining| {
1369            let Some(inner) = inner else {
1370                return 0;
1371            };
1372            let Ok(pin) = MmInner::try_pin(&inner) else {
1373                return 0;
1374            };
1375            match pin.lock().reclaim_lazy_free_pages(remaining) {
1376                Ok(pages) => pages,
1377                Err(error) => {
1378                    warn!(
1379                        "lazy-free reclaim for address space {} entered repair: {error}",
1380                        pin.id().get()
1381                    );
1382                    0
1383                }
1384            }
1385        },
1386    )
1387}
1388
1389/// Returns address spaces whose last reclaim attempt entered `NeedsRepair`.
1390/// The caller may repair the backend and invoke [`RepairPermit::retry`] for each
1391/// returned permit; no cleanup is attempted implicitly.
1392pub struct RepairPermit(Arc<MmInner>);
1393
1394pub fn take_repair_candidates(limit: usize) -> Vec<RepairPermit> {
1395    let count = {
1396        let queue = REPAIR_QUEUE.lock();
1397        limit.min(queue.len())
1398    };
1399    let mut candidates: Vec<RepairPermit> = Vec::new();
1400    if candidates.try_reserve_exact(count).is_err() {
1401        return candidates;
1402    }
1403    for _ in 0..count {
1404        let Some(inner) = REPAIR_QUEUE.lock().pop() else {
1405            break;
1406        };
1407        candidates.push(RepairPermit(inner));
1408    }
1409    candidates
1410}
1411
1412/// Requests one explicit repair retry pass on the sleepable reclaimer.
1413///
1414/// Merely starting the worker never retries `NeedsRepair` address spaces.  A
1415/// filesystem/TLB repair coordinator calls this after it has established that
1416/// the failed precondition is fixed; coalescing requests in one bit keeps the
1417/// hot path allocation-free while preserving the decision boundary.
1418pub fn request_repair_retry() {
1419    REPAIR_RETRY_REQUESTED.store(true, Ordering::Release);
1420}
1421
1422impl RepairPermit {
1423    pub fn retry(self) -> Result<(), ReclaimError> {
1424        {
1425            let _gate = self.0.lifecycle_gate.lock();
1426            if !self.0.is_quiescent_locked()
1427                || self
1428                    .0
1429                    .state
1430                    .compare_exchange(
1431                        MmState::NeedsRepair as u8,
1432                        MmState::Retired as u8,
1433                        Ordering::AcqRel,
1434                        Ordering::Acquire,
1435                    )
1436                    .is_err()
1437            {
1438                return Err(ReclaimError::NotRetired);
1439            }
1440            self.0.retire_queued.store(false, Ordering::Release);
1441        }
1442        let permit = MmInner::take_retire_permit(&self.0).ok_or(ReclaimError::NotRetired)?;
1443        enqueue_retire(permit);
1444        Ok(())
1445    }
1446}
1447
1448/// Starts the one sleepable reclaimer used by the live Starry kernel.  Handles,
1449/// pins and CPU leases only enqueue inert permits, so all potentially blocking
1450/// backend/page-table destruction happens here in process context.
1451pub fn spawn_reclaimer_task() {
1452    if RECLAIMER_STARTED
1453        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1454        .is_err()
1455    {
1456        return;
1457    }
1458    ax_std::thread::Builder::new()
1459        .name("starry-mm-reclaimer".to_owned())
1460        .spawn(|| {
1461            loop {
1462                let _ = ax_mm::retry_kernel_virtual_quarantines(16);
1463                let _ = LAZY_FREE_RECLAIM.run_one_batch(16, reclaim_live_lazy_free_pages);
1464                let _ = reap_retired(16);
1465                if REPAIR_RETRY_REQUESTED.swap(false, Ordering::AcqRel) {
1466                    // A repair coordinator explicitly requested this pass after
1467                    // proving the failed precondition is fixed.  Without that
1468                    // request the queue remains untouched and `NeedsRepair` is
1469                    // never silently treated as success.
1470                    for permit in take_repair_candidates(16) {
1471                        let _ = permit.retry();
1472                    }
1473                }
1474                ax_std::thread::sleep(core::time::Duration::from_millis(10));
1475            }
1476        })
1477        .expect("MM reclaimer thread must start before userspace");
1478}
1479
1480impl RetirePermit {
1481    fn into_inner(mut self) -> Arc<MmInner> {
1482        self.0.take().expect("retire permit is consumed once")
1483    }
1484
1485    pub fn reclaim(self) -> Result<(), ReclaimError> {
1486        let inner = self.into_inner();
1487        let result = Self::reclaim_inner(&inner);
1488        if result.is_ok() {
1489            release_mm_shell(inner);
1490        } else {
1491            enqueue_repair_candidate(inner);
1492        }
1493        result
1494    }
1495
1496    fn reclaim_inner(inner: &Arc<MmInner>) -> Result<(), ReclaimError> {
1497        {
1498            let _gate = inner.lifecycle_gate.lock();
1499            if !inner.is_quiescent_locked()
1500                || inner
1501                    .state
1502                    .compare_exchange(
1503                        MmState::Retired as u8,
1504                        MmState::Reclaiming as u8,
1505                        Ordering::AcqRel,
1506                        Ordering::Acquire,
1507                    )
1508                    .is_err()
1509            {
1510                return Err(ReclaimError::NotRetired);
1511            }
1512        }
1513        let result = inner.aspace.lock().try_reclaim_contents();
1514        match result {
1515            Ok(()) => {
1516                {
1517                    let _gate = inner.lifecycle_gate.lock();
1518                    inner.state.store(MmState::Freed as u8, Ordering::Release);
1519                }
1520                unregister_mm(inner);
1521                Ok(())
1522            }
1523            Err(_) => {
1524                {
1525                    let _gate = inner.lifecycle_gate.lock();
1526                    inner
1527                        .state
1528                        .store(MmState::NeedsRepair as u8, Ordering::Release);
1529                    inner.retire_queued.store(false, Ordering::Release);
1530                }
1531                Err(ReclaimError::Backend)
1532            }
1533        }
1534    }
1535
1536    /// Requests an explicit retry for a failed, quiescent MM.
1537    pub fn retry(self) -> Result<(), ReclaimError> {
1538        RepairPermit(self.into_inner()).retry()
1539    }
1540}
1541
1542/// Called only from sleepable reclamation. Keep an extra owner queued until
1543/// every producer has completed its token destructor; strong-count sampling
1544/// alone would race the final drop and a registry Weak upgrade.
1545fn release_mm_shell(inner: Arc<MmInner>) {
1546    match Arc::try_unwrap(inner) {
1547        Ok(inner) => drop(inner),
1548        Err(inner) => enqueue_mm_work(&RETIRE_QUEUE, inner),
1549    }
1550}
1551
1552impl Drop for RetirePermit {
1553    fn drop(&mut self) {
1554        if let Some(inner) = self.0.take() {
1555            enqueue_mm_work(&RETIRE_QUEUE, inner);
1556        }
1557    }
1558}
1559
1560impl Drop for RepairPermit {
1561    fn drop(&mut self) {
1562        if self.0.state() == MmState::NeedsRepair {
1563            enqueue_repair_candidate(self.0.clone());
1564        } else {
1565            enqueue_mm_work(&RETIRE_QUEUE, self.0.clone());
1566        }
1567    }
1568}
1569
1570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1571pub enum ReclaimError {
1572    NotRetired,
1573    Backend,
1574}
1575
1576#[cfg(all(test, axtest))]
1577mod tests {
1578    use super::*;
1579    use crate::mm::MappingOperation;
1580
1581    #[axtest::axtest]
1582    fn failed_direct_reclaim_preserves_repair_ownership() {
1583        let aspace = Arc::new(Mutex::new(
1584            AddrSpace::new_empty(VirtAddr::from(0x7400_0000), 0x1000).unwrap(),
1585        ));
1586        let handle = MmHandle::from_arc(aspace).unwrap();
1587        let weak = Arc::downgrade(&handle.inner);
1588        handle
1589            .inner
1590            .aspace
1591            .lock()
1592            .mutation_gate
1593            .fail_next_commit_before_publish();
1594        let permit = handle.release_user_ref().unwrap();
1595        drop(handle);
1596        assert_eq!(permit.reclaim(), Err(ReclaimError::Backend));
1597        let inner = weak
1598            .upgrade()
1599            .expect("failed reclaim must retain its MM for repair");
1600        assert_eq!(inner.state(), MmState::NeedsRepair);
1601        // Taking and abandoning a repair token must not silently discard it.
1602        drop(take_repair_candidates(usize::MAX));
1603        drop(inner);
1604        assert!(weak.upgrade().is_some());
1605        for candidate in take_repair_candidates(usize::MAX) {
1606            if candidate.0.id == weak.upgrade().unwrap().id {
1607                candidate.retry().unwrap();
1608            }
1609        }
1610        let _ = reap_retired(usize::MAX);
1611        let _ = reap_retired(usize::MAX);
1612        assert!(weak.upgrade().is_none());
1613    }
1614
1615    #[axtest::axtest]
1616    fn mm_pin_services_blocking_discard_before_refault_retry() {
1617        use ax_runtime::hal::paging::MappingFlags;
1618
1619        use super::super::{MutationError, TlbRange};
1620
1621        let start = VirtAddr::from(0x7500_0000);
1622        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1623        let handle = MmHandle::from_arc(aspace).unwrap();
1624        let pin = handle.pin().unwrap();
1625        {
1626            let mut aspace = pin.lock();
1627            aspace
1628                .map(
1629                    start,
1630                    0x1000,
1631                    MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER,
1632                    true,
1633                    MappingOperation::new_alloc(start, 0x1000, "[pin-refault]"),
1634                )
1635                .unwrap();
1636            aspace.discard_range(start, 0x1000).unwrap();
1637            let mut discard = aspace.mutation_gate.begin(aspace.id, 1);
1638            discard.add_tlb_range(TlbRange::new(start, 0x1000).unwrap());
1639            assert_eq!(
1640                aspace.mutation_gate.commit(discard).unwrap_err(),
1641                MutationError::TlbPending
1642            );
1643        }
1644        let access = PageFaultFlags::READ | PageFaultFlags::USER;
1645        assert!(matches!(
1646            pin.handle_page_fault_result(start, access),
1647            FaultResult::Retry
1648        ));
1649        assert_eq!(pin.lock().pending_tlb_obligations(), 0);
1650        assert!(matches!(
1651            pin.handle_page_fault_result(start, access),
1652            FaultResult::Handled
1653        ));
1654        drop(pin);
1655        let permit = handle.release_user_ref().unwrap();
1656        drop(handle);
1657        permit.reclaim().unwrap();
1658    }
1659
1660    #[axtest::axtest]
1661    fn faultable_user_copy_retries_pending_tlb_before_reporting_success() {
1662        use ax_runtime::hal::paging::MappingFlags;
1663
1664        use super::super::{MutationError, TlbRange};
1665
1666        let start = VirtAddr::from(0x7600_0000);
1667        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1668        let handle = MmHandle::from_arc(aspace).unwrap();
1669        let pin = handle.pin().unwrap();
1670        {
1671            let mut aspace = pin.lock();
1672            aspace
1673                .map(
1674                    start,
1675                    0x1000,
1676                    MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER,
1677                    true,
1678                    MappingOperation::new_alloc(start, 0x1000, "[copy-refault]"),
1679                )
1680                .unwrap();
1681            aspace.discard_range(start, 0x1000).unwrap();
1682            let mut discard = aspace.mutation_gate.begin(aspace.id, 1);
1683            discard.add_tlb_range(TlbRange::new(start, 0x1000).unwrap());
1684            assert_eq!(
1685                aspace.mutation_gate.commit(discard).unwrap_err(),
1686                MutationError::TlbPending
1687            );
1688        }
1689        // A kernel copy faults without USER, unlike a userspace instruction.
1690        // The old discard receipt forces the first attempt to return Retry.
1691        assert!(pin.handle_page_fault(start, PageFaultFlags::WRITE));
1692        assert_eq!(pin.lock().pending_tlb_obligations(), 0);
1693        assert!(
1694            pin.lock()
1695                .pt
1696                .query(start)
1697                .is_ok_and(|(_, flags, _)| flags.contains(MappingFlags::WRITE))
1698        );
1699        assert!(!pin.handle_page_fault(start, PageFaultFlags::EXECUTE));
1700        assert!(!pin.handle_page_fault(start + 0x1000, PageFaultFlags::WRITE));
1701        let exhausted = AddrSpace::classify_fault_error(false, crate::StarryError::NoMemory);
1702        pin.lock().mutation_gate.mark_needs_repair();
1703        let quarantined = pin.handle_page_fault_result(start, PageFaultFlags::WRITE);
1704        // This test did not damage any PTE; release its synthetic quarantine.
1705        pin.lock().mutation_gate.clear_repair();
1706        drop(pin);
1707        let permit = handle.release_user_ref().unwrap();
1708        drop(handle);
1709        permit.reclaim().unwrap();
1710        assert!(
1711            exhausted != FaultResult::Retry && matches!(quarantined, FaultResult::Sigbus(_)),
1712            "allocation failure and repair quarantine are terminal: {exhausted:?}, {quarantined:?}"
1713        );
1714        assert_eq!(exhausted, FaultResult::NoMemory);
1715        assert_eq!(
1716            quarantined,
1717            FaultResult::Sigbus(super::super::BusCode::ObjErr)
1718        );
1719    }
1720
1721    #[axtest::axtest]
1722    fn abandoned_activation_retains_the_unproved_hardware_root() {
1723        let aspace = Arc::new(Mutex::new(
1724            AddrSpace::new_empty(VirtAddr::from(0x7300_0000), 0x1000).unwrap(),
1725        ));
1726        let handle = MmHandle::from_arc(aspace).unwrap();
1727        let weak = Arc::downgrade(&handle.inner);
1728        // No hardware root is installed by this synthetic activation. The
1729        // missing proof still must retain the same ownership as a real CPU.
1730        let activation = handle.activation(0).unwrap();
1731        drop(handle);
1732        drop(activation);
1733        let retained = weak.upgrade();
1734        assert!(
1735            retained.is_some(),
1736            "an unproved active root must stay owned by repair quarantine"
1737        );
1738        let inner = retained.unwrap();
1739        assert_eq!(inner.state(), MmState::NeedsRepair);
1740        assert_eq!(inner.active_count.load(Ordering::Acquire), 1);
1741        // Only the test can supply this proof: no CPU ever installed the MM.
1742        {
1743            let _gate = inner.lifecycle_gate.lock();
1744            inner.active_count.store(0, Ordering::Release);
1745            inner.active_mask.store(0, Ordering::Release);
1746            inner.active_per_cpu[0].store(0, Ordering::Release);
1747        }
1748        for candidate in take_repair_candidates(usize::MAX) {
1749            if Arc::ptr_eq(&candidate.0, &inner) {
1750                candidate.retry().unwrap();
1751            } else {
1752                drop(candidate);
1753            }
1754        }
1755        drop(inner);
1756        let _ = reap_retired(usize::MAX);
1757        let _ = reap_retired(usize::MAX);
1758        assert!(weak.upgrade().is_none());
1759    }
1760
1761    #[axtest::axtest]
1762    fn lazy_free_reclaim_only_scans_after_a_publication_edge() {
1763        let request = CoalescedReclaimRequest::new();
1764        let mut scans = 0usize;
1765
1766        assert_eq!(
1767            request.run_one_batch(16, |_| {
1768                scans += 1;
1769                0
1770            }),
1771            0
1772        );
1773        assert_eq!(scans, 0, "an idle worker must not scan live MMs");
1774
1775        request.request();
1776        request.request();
1777        assert_eq!(
1778            request.run_one_batch(16, |limit| {
1779                scans += 1;
1780                limit
1781            }),
1782            16
1783        );
1784        assert_eq!(scans, 1, "coalesced publications need one scan");
1785
1786        assert_eq!(
1787            request.run_one_batch(16, |_| {
1788                scans += 1;
1789                3
1790            }),
1791            3,
1792            "a full batch must schedule one bounded continuation"
1793        );
1794        assert_eq!(scans, 2);
1795        assert_eq!(request.run_one_batch(16, |_| unreachable!()), 0);
1796    }
1797
1798    #[axtest::axtest]
1799    fn lazy_free_batches_advance_the_production_registry_cursor() {
1800        let mut registry = BTreeMap::new();
1801        registry.insert(AddressSpaceId(2), ());
1802        registry.insert(AddressSpaceId(5), ());
1803        registry.insert(AddressSpaceId(9), ());
1804        let cursor = AtomicU64::new(0);
1805        let mut visits = Vec::new();
1806
1807        for _ in 0..4 {
1808            assert_eq!(
1809                reclaim_registered_items(
1810                    1,
1811                    registry.len(),
1812                    &cursor,
1813                    |after| {
1814                        let id = next_registry_id_after(&registry, after)?;
1815                        Some((id, id))
1816                    },
1817                    |id, remaining| {
1818                        assert_eq!(remaining, 1);
1819                        visits.push(id);
1820                        1
1821                    },
1822                ),
1823                1
1824            );
1825        }
1826
1827        assert_eq!(
1828            visits,
1829            [
1830                AddressSpaceId(2),
1831                AddressSpaceId(5),
1832                AddressSpaceId(9),
1833                AddressSpaceId(2),
1834            ]
1835        );
1836    }
1837
1838    #[axtest::axtest]
1839    fn tag_allocator_uses_explicit_full_flush_fallback() {
1840        for capacity in [0, 1] {
1841            let mut allocator = AddressSpaceTagAllocator::new(capacity);
1842            let allocation = allocator.allocate().unwrap();
1843            assert_eq!(allocator.mode(), TagMode::FullFlush);
1844            assert_eq!(allocation.tag, AddressSpaceTag::full_flush(0));
1845            assert!(!allocation.rollover);
1846        }
1847    }
1848
1849    #[axtest::axtest]
1850    fn tag_allocator_represents_the_complete_sixteen_bit_space() {
1851        let mut allocator = AddressSpaceTagAllocator::new(1 << 16);
1852        allocator.next = u32::from(u16::MAX);
1853
1854        let last = allocator.allocate().unwrap();
1855        assert_eq!(last.tag, AddressSpaceTag::tagged(u16::MAX, 0));
1856        assert!(!last.rollover);
1857
1858        let reused = allocator.allocate().unwrap();
1859        assert_eq!(reused.tag, AddressSpaceTag::tagged(1, 1));
1860        assert!(reused.rollover);
1861    }
1862
1863    #[axtest::axtest]
1864    fn tag_allocator_rollover_restarts_at_one_in_a_new_generation() {
1865        let mut allocator = AddressSpaceTagAllocator::new(4);
1866        for expected in 1..4 {
1867            let allocation = allocator.allocate().unwrap();
1868            assert_eq!(allocation.tag, AddressSpaceTag::tagged(expected, 0));
1869            assert!(!allocation.rollover);
1870        }
1871
1872        let allocation = allocator.allocate().unwrap();
1873        assert_eq!(allocation.tag, AddressSpaceTag::tagged(1, 1));
1874        assert!(allocation.rollover);
1875    }
1876
1877    #[axtest::axtest]
1878    fn tag_allocator_never_wraps_an_exhausted_generation() {
1879        let mut allocator = AddressSpaceTagAllocator::new(4);
1880        allocator.next = allocator.capacity;
1881        allocator.generation = u64::MAX;
1882
1883        assert_eq!(
1884            allocator.allocate(),
1885            Err(TagAllocationError::GenerationExhausted)
1886        );
1887        assert_eq!(allocator.generation(), u64::MAX);
1888        assert_eq!(allocator.next, allocator.capacity);
1889    }
1890
1891    #[axtest::axtest]
1892    fn activation_and_mutation_share_active_cpu_mask() {
1893        let aspace = Arc::new(Mutex::new(
1894            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
1895        ));
1896        let mutation_targets = aspace.lock().tlb_targets();
1897        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
1898
1899        let epoch_before = handle.installed().epoch;
1900        {
1901            let mut guard = aspace.lock();
1902            let start = guard.base();
1903            guard
1904                .map(
1905                    start,
1906                    ax_memory_addr::PAGE_SIZE_4K,
1907                    ax_runtime::hal::paging::MappingFlags::READ
1908                        | ax_runtime::hal::paging::MappingFlags::USER,
1909                    false,
1910                    MappingOperation::new_alloc(
1911                        start,
1912                        ax_memory_addr::PAGE_SIZE_4K,
1913                        "[epoch-source-test]",
1914                    ),
1915                )
1916                .unwrap();
1917        }
1918        assert_eq!(handle.installed().epoch, epoch_before.next());
1919
1920        assert!(Arc::ptr_eq(&mutation_targets, &handle.inner.active_mask));
1921        drop(aspace);
1922
1923        let activation = handle.activation(2).unwrap();
1924        assert_eq!(mutation_targets.load(Ordering::Acquire), 1usize << 2);
1925
1926        activation.release_after_kernel_switch();
1927        assert_eq!(mutation_targets.load(Ordering::Acquire), 0);
1928
1929        let permit = handle
1930            .release_user_ref()
1931            .expect("an inactive ownerless address space must become reclaimable");
1932        permit.reclaim().unwrap();
1933    }
1934
1935    #[axtest::axtest]
1936    fn mm_pin_fault_prepares_outside_and_publishes_through_the_live_mm() {
1937        let start = ax_memory_addr::VirtAddr::from_usize(0x4000);
1938        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1939        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
1940        {
1941            let mut guard = aspace.lock();
1942            guard
1943                .map(
1944                    start,
1945                    ax_memory_addr::PAGE_SIZE_4K,
1946                    ax_runtime::hal::paging::MappingFlags::READ
1947                        | ax_runtime::hal::paging::MappingFlags::WRITE
1948                        | ax_runtime::hal::paging::MappingFlags::USER,
1949                    false,
1950                    MappingOperation::new_alloc(
1951                        start,
1952                        ax_memory_addr::PAGE_SIZE_4K,
1953                        "[mm-pin-fault-test]",
1954                    ),
1955                )
1956                .unwrap();
1957        }
1958
1959        let pin = handle.pin().unwrap();
1960        let activation = handle.activation(2).unwrap();
1961        assert_eq!(
1962            pin.handle_page_fault_result(start, PageFaultFlags::READ | PageFaultFlags::USER,),
1963            FaultResult::Handled
1964        );
1965        {
1966            let guard = aspace.lock();
1967            assert_eq!(guard.resident_page_counts().anon, 1);
1968            assert!(guard.pending_tlb_requests().unwrap().is_empty());
1969            assert_eq!(
1970                guard
1971                    .mutation_gate
1972                    .last_retired_receipt()
1973                    .unwrap()
1974                    .tlb_obligation
1975                    .targets(),
1976                0,
1977                "a previously-none PTE must not shoot down another CPU"
1978            );
1979        }
1980
1981        activation.release_after_kernel_switch();
1982        drop(pin);
1983        drop(aspace);
1984        let permit = handle
1985            .release_user_ref()
1986            .expect("the quiescent MM must become reclaimable after the fault");
1987        permit.reclaim().unwrap();
1988    }
1989
1990    #[axtest::axtest]
1991    fn completed_root_switch_is_not_frozen_into_a_new_tlb_obligation() {
1992        let aspace = Arc::new(Mutex::new(
1993            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
1994        ));
1995        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
1996        let activation = handle.activation(2).unwrap();
1997
1998        let mutation = aspace.lock().prepare_mutation();
1999        activation.release_after_kernel_switch();
2000
2001        let receipt = aspace
2002            .lock()
2003            .mutation_gate
2004            .commit(mutation)
2005            .expect("a CPU that completed its root switch must not remain a TLB target");
2006        assert_eq!(receipt.tlb_obligation.targets(), 0);
2007        assert_eq!(handle.active_cpu_mask(), 0);
2008
2009        let permit = handle
2010            .release_user_ref()
2011            .expect("an inactive ownerless address space must become reclaimable");
2012        permit.reclaim().unwrap();
2013    }
2014
2015    #[axtest::axtest]
2016    fn last_user_owner_cannot_reclaim_an_active_address_space() {
2017        let aspace = Arc::new(Mutex::new(
2018            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2019        ));
2020        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
2021        let activation = handle.activation(2).unwrap();
2022        let root = handle.installed().root;
2023
2024        assert!(is_address_space_live(handle.id()));
2025        assert!(handle.release_user_ref().is_none());
2026        assert_eq!(handle.state(), MmState::Retiring);
2027        assert!(!is_address_space_live(handle.id()));
2028        assert_eq!(handle.installed().root, root);
2029        assert_eq!(handle.cpu_state().active_cpus, 1usize << 2);
2030        assert_ne!(aspace.lock().materialized_root().as_usize(), 0);
2031
2032        activation.release_after_kernel_switch();
2033        assert_eq!(handle.state(), MmState::Retired);
2034
2035        let (reclaimed, failed) = reap_retired(usize::MAX);
2036        assert!(reclaimed >= 1);
2037        assert_eq!(failed, 0);
2038        assert_eq!(handle.state(), MmState::Freed);
2039    }
2040
2041    #[axtest::axtest]
2042    fn pinned_exit_continuation_can_run_while_the_mm_is_retiring() {
2043        let aspace = Arc::new(Mutex::new(
2044            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2045        ));
2046        let handle = MmHandle::from_arc(aspace).unwrap();
2047        let pin = handle.pin().unwrap();
2048
2049        assert!(handle.release_user_ref().is_none());
2050        assert_eq!(handle.state(), MmState::Retiring);
2051
2052        let activation = pin
2053            .activation_for_switch(2)
2054            .expect("a pinned exit continuation must remain schedulable while retiring");
2055        assert_eq!(handle.active_cpu_mask(), 1usize << 2);
2056        activation.release_after_kernel_switch();
2057
2058        drop(pin);
2059        assert_eq!(handle.state(), MmState::Retired);
2060        let (reclaimed, failed) = reap_retired(usize::MAX);
2061        assert!(reclaimed >= 1);
2062        assert_eq!(failed, 0);
2063        assert_eq!(handle.state(), MmState::Freed);
2064    }
2065
2066    #[axtest::axtest]
2067    fn retire_permit_revalidates_quiescence_under_the_lifecycle_gate() {
2068        let aspace = Arc::new(Mutex::new(
2069            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2070        ));
2071        let handle = MmHandle::from_arc(aspace).unwrap();
2072        let pin = handle.pin().unwrap();
2073
2074        assert!(handle.release_user_ref().is_none());
2075        assert_eq!(handle.state(), MmState::Retiring);
2076
2077        // Model the old snapshot/CAS window after it published `Retired` from
2078        // stale zero counters. Permit creation must independently revalidate
2079        // quiescence under the same gate instead of trusting state alone.
2080        {
2081            let _gate = handle.inner.lifecycle_gate.lock();
2082            handle
2083                .inner
2084                .state
2085                .store(MmState::Retired as u8, Ordering::Release);
2086        }
2087        assert!(MmInner::take_retire_permit(&handle.inner).is_none());
2088        assert!(!handle.inner.retire_queued.load(Ordering::Acquire));
2089
2090        {
2091            let _gate = handle.inner.lifecycle_gate.lock();
2092            handle
2093                .inner
2094                .state
2095                .store(MmState::Retiring as u8, Ordering::Release);
2096        }
2097        drop(pin);
2098        assert_eq!(handle.state(), MmState::Retired);
2099        let (reclaimed, failed) = reap_retired(usize::MAX);
2100        assert!(reclaimed >= 1);
2101        assert_eq!(failed, 0);
2102        assert_eq!(handle.state(), MmState::Freed);
2103    }
2104}