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. The last release may sleep to end
837    /// executable/writer exclusion; callers must not hold the address-space lock.
838    pub fn release_user_ref(&self) -> Option<RetirePermit> {
839        let mut last_user = false;
840        {
841            let _gate = self.inner.lifecycle_gate.lock();
842            if self.owner.swap(false, Ordering::Relaxed) {
843                let previous = self.inner.user_refs.load(Ordering::Relaxed);
844                debug_assert!(previous > 0, "MmHandle user reference underflow");
845                self.inner.user_refs.store(previous - 1, Ordering::Release);
846                if previous == 1 {
847                    last_user = true;
848                    let _ = self.inner.state.compare_exchange(
849                        MmState::Live as u8,
850                        MmState::Retiring as u8,
851                        Ordering::AcqRel,
852                        Ordering::Acquire,
853                    );
854                }
855                self.inner.maybe_retire_locked();
856            }
857        }
858        if last_user {
859            // Linux releases mm->exe_file when the last process owner exits,
860            // independently of deferred page-table/CPU reclamation. No new
861            // user owner can appear after the Retiring transition. Detach this
862            // MM's executable and VMA writer leases under its metadata lock,
863            // then drop them outside both MM locks because destruction may block.
864            let file_accesses = self.inner.aspace.lock().take_file_accesses();
865            drop(file_accesses);
866        }
867        self.retire_if_quiescent()
868    }
869}
870
871impl Drop for MmHandle {
872    fn drop(&mut self) {
873        {
874            let _gate = self.inner.lifecycle_gate.lock();
875            if !self.owner.swap(false, Ordering::Relaxed) {
876                return;
877            }
878            let previous = self.inner.user_refs.load(Ordering::Relaxed);
879            debug_assert!(previous > 0, "MmHandle user reference underflow");
880            self.inner.user_refs.store(previous - 1, Ordering::Release);
881            if previous == 1 {
882                let _ = self.inner.state.compare_exchange(
883                    MmState::Live as u8,
884                    MmState::Retiring as u8,
885                    Ordering::AcqRel,
886                    Ordering::Acquire,
887                );
888            }
889            self.inner.maybe_retire_locked();
890        }
891        queue_if_retired(&self.inner);
892    }
893}
894
895/// Short-lived kernel ownership.  It is safe to drop in IRQ context because
896/// only counters and preallocated queue links are touched; actual page-table
897/// destruction belongs to the sleepable reclaimer.
898pub struct MmPin(Arc<MmInner>);
899
900impl MmPin {
901    pub fn id(&self) -> AddressSpaceId {
902        self.0.id
903    }
904
905    /// Returns the process policy attached to the pinned MM identity.
906    pub fn transparent_huge_page_mode(&self) -> TransparentHugePageMode {
907        self.0.transparent_huge_page_mode()
908    }
909
910    /// Changes the process-wide THP policy while excluding concurrent faults.
911    pub fn set_transparent_huge_page_mode(&self, mode: TransparentHugePageMode) {
912        let _aspace = self.0.aspace.lock();
913        self.0
914            .transparent_huge_page_mode
915            .store(mode as u8, Ordering::Release);
916    }
917
918    /// Resolves a fault under the MM's process-wide THP policy.
919    pub fn handle_page_fault_result(
920        &self,
921        vaddr: VirtAddr,
922        access_flags: PageFaultFlags,
923    ) -> FaultResult {
924        let plan = {
925            let aspace = self.0.aspace.lock();
926            let mode = self.0.transparent_huge_page_mode();
927            match aspace.plan_page_fault(vaddr, access_flags, mode) {
928                Ok(plan) => plan,
929                Err(result) => return result,
930            }
931        };
932        // Allocation, file I/O and page-cache reservation happen with no
933        // address-space metadata lock held. The apply phase below rechecks the
934        // exact VMA epoch and PTE preimage before publishing anything.
935        let prepared = match AddrSpace::prepare_page_fault(plan) {
936            Ok(prepared) => prepared,
937            Err(result) => return result,
938        };
939        let mut attempt = prepared.into_apply_attempt();
940        let outcome = {
941            let mut aspace = self.0.aspace.lock();
942            aspace.apply_prepared_page_fault(&mut attempt)
943        };
944        let result = match outcome {
945            PageFaultApplyOutcome::Complete(result) => result,
946            PageFaultApplyOutcome::Cancel(result) => {
947                if attempt.cancel().is_ok() {
948                    result
949                } else {
950                    FaultResult::Retry
951                }
952            }
953            PageFaultApplyOutcome::NeedsRepair(result) => {
954                attempt.release_to_repair_state();
955                result
956            }
957            PageFaultApplyOutcome::CancelPendingTlb { request, targets } => {
958                // Cancellation releases candidate frames and page-table
959                // deposits outside the MM lock. Servicing the old receipt is
960                // also lock-external; merely returning Retry would strand it
961                // and make every subsequent refault hit the same blocker.
962                if attempt.cancel().is_ok()
963                    && AddrSpace::flush_tlb_requests(core::slice::from_ref(&request), &targets)
964                        .is_ok()
965                {
966                    let aspace = self.0.aspace.lock();
967                    let _ = aspace.acknowledge_tlb_requests(core::slice::from_ref(&request));
968                }
969                FaultResult::Retry
970            }
971            PageFaultApplyOutcome::PendingTlb { request, targets } => {
972                if AddrSpace::flush_tlb_requests(core::slice::from_ref(&request), &targets).is_err()
973                {
974                    FaultResult::Retry
975                } else {
976                    let aspace = self.0.aspace.lock();
977                    if aspace
978                        .acknowledge_tlb_requests(core::slice::from_ref(&request))
979                        .is_ok()
980                    {
981                        FaultResult::Handled
982                    } else {
983                        FaultResult::Retry
984                    }
985                }
986            }
987        };
988        if matches!(result, FaultResult::Handled) {
989            ax_cpu::mmu::update_mmu_cache(vaddr);
990        }
991        result
992    }
993
994    /// Resolves a fault for a kernel faultable user-copy scope.
995    pub fn handle_page_fault(&self, vaddr: VirtAddr, access_flags: PageFaultFlags) -> bool {
996        loop {
997            match self.handle_page_fault_result(vaddr, access_flags) {
998                FaultResult::Handled => return true,
999                // The failed transaction has released its metadata guard and
1000                // cancelled its candidate owners. Reacquire a fresh VMA/PTE
1001                // snapshot after allowing the competing publisher to progress;
1002                // a transient conflict must not take the copy's EFAULT fixup.
1003                FaultResult::Retry => crate::task::yield_now(),
1004                _ => return false,
1005            }
1006        }
1007    }
1008
1009    /// Acquires scheduler activation for an already-pinned kernel
1010    /// continuation. The pin is the typed proof that `Retiring` still has a
1011    /// live executor and therefore cannot advance to `Retired`.
1012    pub(crate) fn activation_for_switch(
1013        &self,
1014        cpu: usize,
1015    ) -> Result<ActivationLease, ActivationError> {
1016        MmInner::acquire_activation(
1017            &self.0,
1018            cpu,
1019            ActivationMode::SchedulerHandoff,
1020            ActivationAuthority::PinnedContinuation,
1021        )
1022    }
1023}
1024
1025impl Deref for MmPin {
1026    type Target = Mutex<AddrSpace>;
1027
1028    fn deref(&self) -> &Self::Target {
1029        &self.0.aspace
1030    }
1031}
1032
1033impl Drop for MmPin {
1034    fn drop(&mut self) {
1035        {
1036            let _gate = self.0.lifecycle_gate.lock();
1037            let previous = self.0.kernel_pins.load(Ordering::Relaxed);
1038            debug_assert!(previous > 0, "MmPin reference underflow");
1039            self.0.kernel_pins.store(previous - 1, Ordering::Release);
1040            self.0.maybe_retire_locked();
1041        }
1042        queue_if_retired(&self.0);
1043    }
1044}
1045
1046/// A per-CPU activation.  The scheduler owns the value and must consume/drop
1047/// it only after installing another root or the kernel root.
1048pub struct ActivationLease {
1049    inner: Arc<MmInner>,
1050    cpu: usize,
1051    installed: InstalledPageTableRoot,
1052    released: bool,
1053}
1054
1055impl ActivationLease {
1056    pub fn installed(&self) -> InstalledPageTableRoot {
1057        self.installed
1058    }
1059
1060    pub const fn cpu(&self) -> usize {
1061        self.cpu
1062    }
1063
1064    /// Transfers a pre-acquired lease into the runtime's inline switch storage.
1065    pub(crate) fn into_scheduler_activation(
1066        mut self,
1067    ) -> ax_runtime::thread::SchedulerAddressSpaceActivation {
1068        let activation = ax_runtime::thread::SchedulerAddressSpaceActivation::new(
1069            task_address_space(self.installed),
1070            self.cpu,
1071            self.inner.clone(),
1072        );
1073        self.released = true;
1074        activation
1075    }
1076
1077    /// Consumes the lease after the architecture has installed a different
1078    /// address-space root on this CPU.
1079    ///
1080    /// A plain `Drop` deliberately does not clear the active bit: losing a
1081    /// lease before the root write must leak retirement progress rather than
1082    /// permit a stale hardware root to be reclaimed.
1083    pub(crate) fn release_after_root_switch(mut self) {
1084        release_activation_accounting(&self.inner, self.cpu);
1085        self.released = true;
1086    }
1087
1088    /// Consumes the lease after an offline path has installed the kernel root
1089    /// and completed its local full TLB flush.
1090    pub fn release_after_kernel_switch(self) {
1091        self.release_after_root_switch();
1092    }
1093}
1094
1095impl Drop for ActivationLease {
1096    fn drop(&mut self) {
1097        if !self.released {
1098            abandon_activation(self.inner.clone(), self.cpu);
1099        }
1100    }
1101}
1102
1103fn abandon_activation(inner: Arc<MmInner>, cpu: usize) {
1104    {
1105        let _gate = inner.lifecycle_gate.lock();
1106        inner
1107            .state
1108            .store(MmState::NeedsRepair as u8, Ordering::Release);
1109    }
1110    warn!(
1111        "address-space activation for mm {} cpu {} dropped before root-switch proof",
1112        inner.id.get(),
1113        cpu
1114    );
1115    enqueue_repair_candidate(inner);
1116}
1117
1118fn release_activation_accounting(inner: &Arc<MmInner>, cpu: usize) {
1119    {
1120        let _gate = inner.lifecycle_gate.lock();
1121        let previous = inner.active_count.load(Ordering::Relaxed);
1122        debug_assert!(previous > 0, "ActivationLease reference underflow");
1123        inner.active_count.store(previous - 1, Ordering::Release);
1124        if cpu < usize::BITS as usize {
1125            let cpu_refs = &inner.active_per_cpu[cpu];
1126            let previous_cpu = cpu_refs.load(Ordering::Relaxed);
1127            debug_assert!(previous_cpu > 0, "per-CPU activation reference underflow");
1128            cpu_refs.store(previous_cpu - 1, Ordering::Release);
1129            if previous_cpu == 1 {
1130                inner
1131                    .active_mask
1132                    .fetch_and(!(1usize << cpu), Ordering::Release);
1133            }
1134        }
1135        inner.maybe_retire_locked();
1136    }
1137    queue_if_retired(inner);
1138}
1139
1140impl ax_runtime::thread::SchedulerAddressSpaceOwner for MmInner {
1141    fn release_after_root_switch(
1142        self: Arc<Self>,
1143        proof: ax_runtime::thread::AddressSpaceSwitchProof,
1144    ) {
1145        release_activation_accounting(&self, proof.cpu());
1146    }
1147
1148    fn cancel_before_install(self: Arc<Self>, cpu: usize) {
1149        release_activation_accounting(&self, cpu);
1150    }
1151
1152    fn abandon(self: Arc<Self>, cpu: usize) {
1153        abandon_activation(self, cpu);
1154    }
1155}
1156
1157fn task_address_space(installed: InstalledAddressSpace) -> ax_hal::context::InstalledAddressSpace {
1158    ax_hal::context::InstalledAddressSpace::user(
1159        installed.space_id().get(),
1160        installed.root(),
1161        installed.tag().hardware_tag,
1162        installed.tag().generation,
1163        installed.epoch().get(),
1164        match installed.tag().mode {
1165            TagMode::Tagged => ax_hal::context::InstalledAddressSpaceMode::Tagged,
1166            TagMode::FullFlush => ax_hal::context::InstalledAddressSpaceMode::FullFlush,
1167        },
1168    )
1169    .expect("typed Starry address-space installation must remain valid")
1170}
1171
1172/// Task ownership can end before the CPU releases its lazy MM. The independent
1173/// inner Arc anchors callback storage until the runtime reaps its owning token.
1174struct RuntimeMmOwner {
1175    _inner: Arc<MmInner>,
1176    pin: IrqMutex<Option<MmPin>>,
1177}
1178
1179// SAFETY: the pin permits activations of Live/Retiring MM state. Each activation
1180// publishes the shared TLB target bit under the lifecycle gate. `inner` remains
1181// owned by the runtime token until all CPU leases have drained, so the IRQ-off
1182// release callbacks cannot drop the final page-table or MM-shell allocation.
1183unsafe impl ax_runtime::thread::UserAddressSpaceOwner for RuntimeMmOwner {
1184    fn prepare_activation(
1185        &self,
1186        cpu: usize,
1187    ) -> Result<
1188        ax_runtime::thread::SchedulerAddressSpaceActivation,
1189        ax_runtime::task::thread::TaskError,
1190    > {
1191        self.pin
1192            .lock()
1193            .as_ref()
1194            .ok_or(ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)?
1195            .activation_for_switch(cpu)
1196            .map(ActivationLease::into_scheduler_activation)
1197            .map_err(|_| ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)
1198    }
1199
1200    fn detach_from_task(&self) {
1201        let pin = self.pin.lock().take();
1202        drop(pin);
1203    }
1204}
1205
1206impl MmHandle {
1207    /// Prepares task-scoped MM ownership before publishing a runnable task.
1208    pub(crate) fn scheduler_address_space(
1209        &self,
1210    ) -> Result<ax_runtime::thread::TaskAddressSpace, ax_runtime::task::thread::TaskError> {
1211        let pin = self
1212            .pin()
1213            .map_err(|_| ax_runtime::task::thread::TaskError::InvalidRuntimeHandle)?;
1214        ax_runtime::thread::TaskAddressSpace::new_managed(
1215            self.installed().root(),
1216            self.inner.runtime_cpu_state.clone(),
1217            RuntimeMmOwner {
1218                _inner: self.inner.clone(),
1219                pin: IrqMutex::new(Some(pin)),
1220            },
1221        )
1222    }
1223}
1224
1225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1226pub enum ActivationError {
1227    Retired,
1228    AlreadyActive,
1229    InvalidCpu,
1230    Overflow,
1231}
1232
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum CloneUserRefError {
1235    Retired,
1236    Overflow,
1237}
1238
1239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1240pub enum PinError {
1241    Retired,
1242    Overflow,
1243}
1244
1245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1246pub enum MmCreateError {
1247    DuplicateIdentity,
1248}
1249
1250/// Permission to perform potentially sleeping destruction after quiescence.
1251pub struct RetirePermit(Option<Arc<MmInner>>);
1252
1253/// Retired MMs are queued as inert permits; a sleepable reaper must call
1254/// [`reap_retired`] from process context.  No page-table or backend cleanup is
1255/// performed by `Drop` of a handle/pin/activation token.
1256static RETIRE_QUEUE: IrqMutex<MmWorkQueue> = IrqMutex::new(MmWorkQueue::new());
1257static REPAIR_QUEUE: IrqMutex<MmWorkQueue> = IrqMutex::new(MmWorkQueue::new());
1258static RECLAIMER_STARTED: AtomicBool = AtomicBool::new(false);
1259static REPAIR_RETRY_REQUESTED: AtomicBool = AtomicBool::new(false);
1260
1261struct CoalescedReclaimRequest {
1262    pending: AtomicBool,
1263}
1264
1265impl CoalescedReclaimRequest {
1266    const fn new() -> Self {
1267        Self {
1268            pending: AtomicBool::new(false),
1269        }
1270    }
1271
1272    fn request(&self) {
1273        self.pending.store(true, Ordering::Release);
1274    }
1275
1276    fn run_one_batch(&self, limit: usize, reclaim: impl FnOnce(usize) -> usize) -> usize {
1277        if limit == 0 || !self.pending.swap(false, Ordering::AcqRel) {
1278            return 0;
1279        }
1280        let reclaimed = reclaim(limit);
1281        if reclaimed >= limit {
1282            // Hitting the batch limit means another eligible page may remain.
1283            // Keep the edge set for the next bounded worker pass.
1284            self.request();
1285        }
1286        reclaimed
1287    }
1288}
1289
1290static LAZY_FREE_RECLAIM: CoalescedReclaimRequest = CoalescedReclaimRequest::new();
1291#[cfg(test)]
1292static LAZY_FREE_RECLAIM_REQUESTS: AtomicUsize = AtomicUsize::new(0);
1293
1294/// Coalesces `MADV_FREE` publications into one sleepable worker pass.
1295///
1296/// Linux makes lazy-free pages reclaimable on its LRU and only scans them from
1297/// a reclaim invocation; it does not poll every live `mm` at a fixed interval.
1298/// Starry's current reclaim engine has no anonymous LRU yet, so this bit is the
1299/// allocation-free publication edge between the VMA transaction and worker.
1300pub(super) fn request_lazy_free_reclaim() {
1301    #[cfg(test)]
1302    LAZY_FREE_RECLAIM_REQUESTS.fetch_add(1, Ordering::Relaxed);
1303    LAZY_FREE_RECLAIM.request();
1304}
1305
1306#[cfg(test)]
1307pub(super) fn lazy_free_reclaim_request_count_for_test() -> usize {
1308    LAZY_FREE_RECLAIM_REQUESTS.load(Ordering::Relaxed)
1309}
1310
1311fn queue_if_retired(inner: &Arc<MmInner>) {
1312    if let Some(permit) = MmInner::take_retire_permit(inner) {
1313        enqueue_retire(permit);
1314    }
1315}
1316
1317/// Queues a permit returned by an explicit owner release, without allocation.
1318pub fn enqueue_retire(permit: RetirePermit) {
1319    drop(permit);
1320}
1321
1322fn enqueue_mm_work(queue: &IrqMutex<MmWorkQueue>, inner: Arc<MmInner>) {
1323    let duplicate = queue.lock().push(inner);
1324    // The existing queue entry owns the MM on this path; release the extra
1325    // reference only after dropping the IRQ-safe queue guard.
1326    drop(duplicate);
1327}
1328
1329fn enqueue_repair_candidate(inner: Arc<MmInner>) {
1330    enqueue_mm_work(&REPAIR_QUEUE, inner);
1331}
1332
1333/// Reclaims up to `limit` retired address spaces.  This function is deliberately
1334/// explicit so callers can schedule it on a sleepable kernel worker; it never
1335/// holds the queue lock while taking the address-space mutex.
1336pub fn reap_retired(limit: usize) -> (usize, usize) {
1337    if limit == 0 {
1338        return (0, 0);
1339    }
1340    let count = {
1341        let queue = RETIRE_QUEUE.lock();
1342        limit.min(queue.len())
1343    };
1344    let mut reclaimed = 0;
1345    let mut failed = 0;
1346    for _ in 0..count {
1347        let Some(inner) = RETIRE_QUEUE.lock().pop() else {
1348            break;
1349        };
1350        if inner.state() == MmState::Freed {
1351            // The producer may still be dropping its handle after enqueue.
1352            // Atomically take the final owner here, rather than letting its
1353            // eventual IRQ-context Arc drop destroy the root and metadata.
1354            release_mm_shell(inner);
1355            continue;
1356        }
1357        match RetirePermit(Some(inner)).reclaim() {
1358            Ok(()) => reclaimed += 1,
1359            Err(_) => failed += 1,
1360        }
1361    }
1362    (reclaimed, failed)
1363}
1364
1365/// Reclaims anonymous `MADV_FREE` pages from live address spaces without
1366/// retaining the global identity lock across an address-space mutex.
1367pub fn reclaim_live_lazy_free_pages(limit: usize) -> usize {
1368    if limit == 0 {
1369        return 0;
1370    }
1371    let visit_limit = {
1372        let registry = MM_REGISTRY.lock();
1373        registry.len()
1374    };
1375    reclaim_registered_items(
1376        limit,
1377        visit_limit,
1378        &LAZY_FREE_SCAN_CURSOR,
1379        next_registered_mm_after,
1380        |inner, remaining| {
1381            let Some(inner) = inner else {
1382                return 0;
1383            };
1384            let Ok(pin) = MmInner::try_pin(&inner) else {
1385                return 0;
1386            };
1387            match pin.lock().reclaim_lazy_free_pages(remaining) {
1388                Ok(pages) => pages,
1389                Err(error) => {
1390                    warn!(
1391                        "lazy-free reclaim for address space {} entered repair: {error}",
1392                        pin.id().get()
1393                    );
1394                    0
1395                }
1396            }
1397        },
1398    )
1399}
1400
1401/// Returns address spaces whose last reclaim attempt entered `NeedsRepair`.
1402/// The caller may repair the backend and invoke [`RepairPermit::retry`] for each
1403/// returned permit; no cleanup is attempted implicitly.
1404pub struct RepairPermit(Arc<MmInner>);
1405
1406pub fn take_repair_candidates(limit: usize) -> Vec<RepairPermit> {
1407    let count = {
1408        let queue = REPAIR_QUEUE.lock();
1409        limit.min(queue.len())
1410    };
1411    let mut candidates: Vec<RepairPermit> = Vec::new();
1412    if candidates.try_reserve_exact(count).is_err() {
1413        return candidates;
1414    }
1415    for _ in 0..count {
1416        let Some(inner) = REPAIR_QUEUE.lock().pop() else {
1417            break;
1418        };
1419        candidates.push(RepairPermit(inner));
1420    }
1421    candidates
1422}
1423
1424/// Requests one explicit repair retry pass on the sleepable reclaimer.
1425///
1426/// Merely starting the worker never retries `NeedsRepair` address spaces.  A
1427/// filesystem/TLB repair coordinator calls this after it has established that
1428/// the failed precondition is fixed; coalescing requests in one bit keeps the
1429/// hot path allocation-free while preserving the decision boundary.
1430pub fn request_repair_retry() {
1431    REPAIR_RETRY_REQUESTED.store(true, Ordering::Release);
1432}
1433
1434impl RepairPermit {
1435    pub fn retry(self) -> Result<(), ReclaimError> {
1436        {
1437            let _gate = self.0.lifecycle_gate.lock();
1438            if !self.0.is_quiescent_locked()
1439                || self
1440                    .0
1441                    .state
1442                    .compare_exchange(
1443                        MmState::NeedsRepair as u8,
1444                        MmState::Retired as u8,
1445                        Ordering::AcqRel,
1446                        Ordering::Acquire,
1447                    )
1448                    .is_err()
1449            {
1450                return Err(ReclaimError::NotRetired);
1451            }
1452            self.0.retire_queued.store(false, Ordering::Release);
1453        }
1454        let permit = MmInner::take_retire_permit(&self.0).ok_or(ReclaimError::NotRetired)?;
1455        enqueue_retire(permit);
1456        Ok(())
1457    }
1458}
1459
1460/// Starts the one sleepable reclaimer used by the live Starry kernel.  Handles,
1461/// pins and CPU leases only enqueue inert permits, so all potentially blocking
1462/// backend/page-table destruction happens here in process context.
1463pub fn spawn_reclaimer_task() {
1464    if RECLAIMER_STARTED
1465        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1466        .is_err()
1467    {
1468        return;
1469    }
1470    ax_std::thread::Builder::new()
1471        .name("starry-mm-reclaimer".to_owned())
1472        .spawn(|| {
1473            loop {
1474                let _ = ax_mm::retry_kernel_virtual_quarantines(16);
1475                let _ = LAZY_FREE_RECLAIM.run_one_batch(16, reclaim_live_lazy_free_pages);
1476                let _ = reap_retired(16);
1477                if REPAIR_RETRY_REQUESTED.swap(false, Ordering::AcqRel) {
1478                    // A repair coordinator explicitly requested this pass after
1479                    // proving the failed precondition is fixed.  Without that
1480                    // request the queue remains untouched and `NeedsRepair` is
1481                    // never silently treated as success.
1482                    for permit in take_repair_candidates(16) {
1483                        let _ = permit.retry();
1484                    }
1485                }
1486                ax_std::thread::sleep(core::time::Duration::from_millis(10));
1487            }
1488        })
1489        .expect("MM reclaimer thread must start before userspace");
1490}
1491
1492impl RetirePermit {
1493    fn into_inner(mut self) -> Arc<MmInner> {
1494        self.0.take().expect("retire permit is consumed once")
1495    }
1496
1497    pub fn reclaim(self) -> Result<(), ReclaimError> {
1498        let inner = self.into_inner();
1499        let result = Self::reclaim_inner(&inner);
1500        if result.is_ok() {
1501            release_mm_shell(inner);
1502        } else {
1503            enqueue_repair_candidate(inner);
1504        }
1505        result
1506    }
1507
1508    fn reclaim_inner(inner: &Arc<MmInner>) -> Result<(), ReclaimError> {
1509        {
1510            let _gate = inner.lifecycle_gate.lock();
1511            if !inner.is_quiescent_locked()
1512                || inner
1513                    .state
1514                    .compare_exchange(
1515                        MmState::Retired as u8,
1516                        MmState::Reclaiming as u8,
1517                        Ordering::AcqRel,
1518                        Ordering::Acquire,
1519                    )
1520                    .is_err()
1521            {
1522                return Err(ReclaimError::NotRetired);
1523            }
1524        }
1525        let result = inner.aspace.lock().try_reclaim_contents();
1526        match result {
1527            Ok(()) => {
1528                {
1529                    let _gate = inner.lifecycle_gate.lock();
1530                    inner.state.store(MmState::Freed as u8, Ordering::Release);
1531                }
1532                unregister_mm(inner);
1533                Ok(())
1534            }
1535            Err(_) => {
1536                {
1537                    let _gate = inner.lifecycle_gate.lock();
1538                    inner
1539                        .state
1540                        .store(MmState::NeedsRepair as u8, Ordering::Release);
1541                    inner.retire_queued.store(false, Ordering::Release);
1542                }
1543                Err(ReclaimError::Backend)
1544            }
1545        }
1546    }
1547
1548    /// Requests an explicit retry for a failed, quiescent MM.
1549    pub fn retry(self) -> Result<(), ReclaimError> {
1550        RepairPermit(self.into_inner()).retry()
1551    }
1552}
1553
1554/// Called only from sleepable reclamation. Keep an extra owner queued until
1555/// every producer has completed its token destructor; strong-count sampling
1556/// alone would race the final drop and a registry Weak upgrade.
1557fn release_mm_shell(inner: Arc<MmInner>) {
1558    match Arc::try_unwrap(inner) {
1559        Ok(inner) => drop(inner),
1560        Err(inner) => enqueue_mm_work(&RETIRE_QUEUE, inner),
1561    }
1562}
1563
1564impl Drop for RetirePermit {
1565    fn drop(&mut self) {
1566        if let Some(inner) = self.0.take() {
1567            enqueue_mm_work(&RETIRE_QUEUE, inner);
1568        }
1569    }
1570}
1571
1572impl Drop for RepairPermit {
1573    fn drop(&mut self) {
1574        if self.0.state() == MmState::NeedsRepair {
1575            enqueue_repair_candidate(self.0.clone());
1576        } else {
1577            enqueue_mm_work(&RETIRE_QUEUE, self.0.clone());
1578        }
1579    }
1580}
1581
1582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1583pub enum ReclaimError {
1584    NotRetired,
1585    Backend,
1586}
1587
1588#[cfg(all(test, axtest))]
1589mod tests {
1590    use super::*;
1591    use crate::mm::MappingOperation;
1592
1593    #[axtest::axtest]
1594    fn failed_direct_reclaim_preserves_repair_ownership() {
1595        let aspace = Arc::new(Mutex::new(
1596            AddrSpace::new_empty(VirtAddr::from(0x7400_0000), 0x1000).unwrap(),
1597        ));
1598        let handle = MmHandle::from_arc(aspace).unwrap();
1599        let weak = Arc::downgrade(&handle.inner);
1600        handle
1601            .inner
1602            .aspace
1603            .lock()
1604            .mutation_gate
1605            .fail_next_commit_before_publish();
1606        let permit = handle.release_user_ref().unwrap();
1607        drop(handle);
1608        assert_eq!(permit.reclaim(), Err(ReclaimError::Backend));
1609        let inner = weak
1610            .upgrade()
1611            .expect("failed reclaim must retain its MM for repair");
1612        assert_eq!(inner.state(), MmState::NeedsRepair);
1613        // Taking and abandoning a repair token must not silently discard it.
1614        drop(take_repair_candidates(usize::MAX));
1615        drop(inner);
1616        assert!(weak.upgrade().is_some());
1617        for candidate in take_repair_candidates(usize::MAX) {
1618            if candidate.0.id == weak.upgrade().unwrap().id {
1619                candidate.retry().unwrap();
1620            }
1621        }
1622        let _ = reap_retired(usize::MAX);
1623        let _ = reap_retired(usize::MAX);
1624        assert!(weak.upgrade().is_none());
1625    }
1626
1627    #[axtest::axtest]
1628    fn mm_pin_services_blocking_discard_before_refault_retry() {
1629        use ax_runtime::hal::paging::MappingFlags;
1630
1631        use super::super::{MutationError, TlbRange};
1632
1633        let start = VirtAddr::from(0x7500_0000);
1634        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1635        let handle = MmHandle::from_arc(aspace).unwrap();
1636        let pin = handle.pin().unwrap();
1637        {
1638            let mut aspace = pin.lock();
1639            aspace
1640                .map(
1641                    start,
1642                    0x1000,
1643                    MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER,
1644                    true,
1645                    MappingOperation::new_alloc(start, 0x1000, "[pin-refault]"),
1646                )
1647                .unwrap();
1648            aspace.discard_range(start, 0x1000).unwrap();
1649            let mut discard = aspace.mutation_gate.begin(aspace.id, 1);
1650            discard.add_tlb_range(TlbRange::new(start, 0x1000).unwrap());
1651            assert_eq!(
1652                aspace.mutation_gate.commit(discard).unwrap_err(),
1653                MutationError::TlbPending
1654            );
1655        }
1656        let access = PageFaultFlags::READ | PageFaultFlags::USER;
1657        assert!(matches!(
1658            pin.handle_page_fault_result(start, access),
1659            FaultResult::Retry
1660        ));
1661        assert_eq!(pin.lock().pending_tlb_obligations(), 0);
1662        assert!(matches!(
1663            pin.handle_page_fault_result(start, access),
1664            FaultResult::Handled
1665        ));
1666        drop(pin);
1667        let permit = handle.release_user_ref().unwrap();
1668        drop(handle);
1669        permit.reclaim().unwrap();
1670    }
1671
1672    #[axtest::axtest]
1673    fn faultable_user_copy_retries_pending_tlb_before_reporting_success() {
1674        use ax_runtime::hal::paging::MappingFlags;
1675
1676        use super::super::{MutationError, TlbRange};
1677
1678        let start = VirtAddr::from(0x7600_0000);
1679        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1680        let handle = MmHandle::from_arc(aspace).unwrap();
1681        let pin = handle.pin().unwrap();
1682        {
1683            let mut aspace = pin.lock();
1684            aspace
1685                .map(
1686                    start,
1687                    0x1000,
1688                    MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER,
1689                    true,
1690                    MappingOperation::new_alloc(start, 0x1000, "[copy-refault]"),
1691                )
1692                .unwrap();
1693            aspace.discard_range(start, 0x1000).unwrap();
1694            let mut discard = aspace.mutation_gate.begin(aspace.id, 1);
1695            discard.add_tlb_range(TlbRange::new(start, 0x1000).unwrap());
1696            assert_eq!(
1697                aspace.mutation_gate.commit(discard).unwrap_err(),
1698                MutationError::TlbPending
1699            );
1700        }
1701        // A kernel copy faults without USER, unlike a userspace instruction.
1702        // The old discard receipt forces the first attempt to return Retry.
1703        assert!(pin.handle_page_fault(start, PageFaultFlags::WRITE));
1704        assert_eq!(pin.lock().pending_tlb_obligations(), 0);
1705        assert!(
1706            pin.lock()
1707                .pt
1708                .query(start)
1709                .is_ok_and(|(_, flags, _)| flags.contains(MappingFlags::WRITE))
1710        );
1711        assert!(!pin.handle_page_fault(start, PageFaultFlags::EXECUTE));
1712        assert!(!pin.handle_page_fault(start + 0x1000, PageFaultFlags::WRITE));
1713        let exhausted = AddrSpace::classify_fault_error(false, crate::StarryError::NoMemory);
1714        pin.lock().mutation_gate.mark_needs_repair();
1715        let quarantined = pin.handle_page_fault_result(start, PageFaultFlags::WRITE);
1716        // This test did not damage any PTE; release its synthetic quarantine.
1717        pin.lock().mutation_gate.clear_repair();
1718        drop(pin);
1719        let permit = handle.release_user_ref().unwrap();
1720        drop(handle);
1721        permit.reclaim().unwrap();
1722        assert!(
1723            exhausted != FaultResult::Retry && matches!(quarantined, FaultResult::Sigbus(_)),
1724            "allocation failure and repair quarantine are terminal: {exhausted:?}, {quarantined:?}"
1725        );
1726        assert_eq!(exhausted, FaultResult::NoMemory);
1727        assert_eq!(
1728            quarantined,
1729            FaultResult::Sigbus(super::super::BusCode::ObjErr)
1730        );
1731    }
1732
1733    #[axtest::axtest]
1734    fn abandoned_activation_retains_the_unproved_hardware_root() {
1735        let aspace = Arc::new(Mutex::new(
1736            AddrSpace::new_empty(VirtAddr::from(0x7300_0000), 0x1000).unwrap(),
1737        ));
1738        let handle = MmHandle::from_arc(aspace).unwrap();
1739        let weak = Arc::downgrade(&handle.inner);
1740        // No hardware root is installed by this synthetic activation. The
1741        // missing proof still must retain the same ownership as a real CPU.
1742        let activation = handle.activation(0).unwrap();
1743        drop(handle);
1744        drop(activation);
1745        let retained = weak.upgrade();
1746        assert!(
1747            retained.is_some(),
1748            "an unproved active root must stay owned by repair quarantine"
1749        );
1750        let inner = retained.unwrap();
1751        assert_eq!(inner.state(), MmState::NeedsRepair);
1752        assert_eq!(inner.active_count.load(Ordering::Acquire), 1);
1753        // Only the test can supply this proof: no CPU ever installed the MM.
1754        {
1755            let _gate = inner.lifecycle_gate.lock();
1756            inner.active_count.store(0, Ordering::Release);
1757            inner.active_mask.store(0, Ordering::Release);
1758            inner.active_per_cpu[0].store(0, Ordering::Release);
1759        }
1760        for candidate in take_repair_candidates(usize::MAX) {
1761            if Arc::ptr_eq(&candidate.0, &inner) {
1762                candidate.retry().unwrap();
1763            } else {
1764                drop(candidate);
1765            }
1766        }
1767        drop(inner);
1768        let _ = reap_retired(usize::MAX);
1769        let _ = reap_retired(usize::MAX);
1770        assert!(weak.upgrade().is_none());
1771    }
1772
1773    #[axtest::axtest]
1774    fn lazy_free_reclaim_only_scans_after_a_publication_edge() {
1775        let request = CoalescedReclaimRequest::new();
1776        let mut scans = 0usize;
1777
1778        assert_eq!(
1779            request.run_one_batch(16, |_| {
1780                scans += 1;
1781                0
1782            }),
1783            0
1784        );
1785        assert_eq!(scans, 0, "an idle worker must not scan live MMs");
1786
1787        request.request();
1788        request.request();
1789        assert_eq!(
1790            request.run_one_batch(16, |limit| {
1791                scans += 1;
1792                limit
1793            }),
1794            16
1795        );
1796        assert_eq!(scans, 1, "coalesced publications need one scan");
1797
1798        assert_eq!(
1799            request.run_one_batch(16, |_| {
1800                scans += 1;
1801                3
1802            }),
1803            3,
1804            "a full batch must schedule one bounded continuation"
1805        );
1806        assert_eq!(scans, 2);
1807        assert_eq!(request.run_one_batch(16, |_| unreachable!()), 0);
1808    }
1809
1810    #[axtest::axtest]
1811    fn lazy_free_batches_advance_the_production_registry_cursor() {
1812        let mut registry = BTreeMap::new();
1813        registry.insert(AddressSpaceId(2), ());
1814        registry.insert(AddressSpaceId(5), ());
1815        registry.insert(AddressSpaceId(9), ());
1816        let cursor = AtomicU64::new(0);
1817        let mut visits = Vec::new();
1818
1819        for _ in 0..4 {
1820            assert_eq!(
1821                reclaim_registered_items(
1822                    1,
1823                    registry.len(),
1824                    &cursor,
1825                    |after| {
1826                        let id = next_registry_id_after(&registry, after)?;
1827                        Some((id, id))
1828                    },
1829                    |id, remaining| {
1830                        assert_eq!(remaining, 1);
1831                        visits.push(id);
1832                        1
1833                    },
1834                ),
1835                1
1836            );
1837        }
1838
1839        assert_eq!(
1840            visits,
1841            [
1842                AddressSpaceId(2),
1843                AddressSpaceId(5),
1844                AddressSpaceId(9),
1845                AddressSpaceId(2),
1846            ]
1847        );
1848    }
1849
1850    #[axtest::axtest]
1851    fn tag_allocator_uses_explicit_full_flush_fallback() {
1852        for capacity in [0, 1] {
1853            let mut allocator = AddressSpaceTagAllocator::new(capacity);
1854            let allocation = allocator.allocate().unwrap();
1855            assert_eq!(allocator.mode(), TagMode::FullFlush);
1856            assert_eq!(allocation.tag, AddressSpaceTag::full_flush(0));
1857            assert!(!allocation.rollover);
1858        }
1859    }
1860
1861    #[axtest::axtest]
1862    fn tag_allocator_represents_the_complete_sixteen_bit_space() {
1863        let mut allocator = AddressSpaceTagAllocator::new(1 << 16);
1864        allocator.next = u32::from(u16::MAX);
1865
1866        let last = allocator.allocate().unwrap();
1867        assert_eq!(last.tag, AddressSpaceTag::tagged(u16::MAX, 0));
1868        assert!(!last.rollover);
1869
1870        let reused = allocator.allocate().unwrap();
1871        assert_eq!(reused.tag, AddressSpaceTag::tagged(1, 1));
1872        assert!(reused.rollover);
1873    }
1874
1875    #[axtest::axtest]
1876    fn tag_allocator_rollover_restarts_at_one_in_a_new_generation() {
1877        let mut allocator = AddressSpaceTagAllocator::new(4);
1878        for expected in 1..4 {
1879            let allocation = allocator.allocate().unwrap();
1880            assert_eq!(allocation.tag, AddressSpaceTag::tagged(expected, 0));
1881            assert!(!allocation.rollover);
1882        }
1883
1884        let allocation = allocator.allocate().unwrap();
1885        assert_eq!(allocation.tag, AddressSpaceTag::tagged(1, 1));
1886        assert!(allocation.rollover);
1887    }
1888
1889    #[axtest::axtest]
1890    fn tag_allocator_never_wraps_an_exhausted_generation() {
1891        let mut allocator = AddressSpaceTagAllocator::new(4);
1892        allocator.next = allocator.capacity;
1893        allocator.generation = u64::MAX;
1894
1895        assert_eq!(
1896            allocator.allocate(),
1897            Err(TagAllocationError::GenerationExhausted)
1898        );
1899        assert_eq!(allocator.generation(), u64::MAX);
1900        assert_eq!(allocator.next, allocator.capacity);
1901    }
1902
1903    #[axtest::axtest]
1904    fn activation_and_mutation_share_active_cpu_mask() {
1905        let aspace = Arc::new(Mutex::new(
1906            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
1907        ));
1908        let mutation_targets = aspace.lock().tlb_targets();
1909        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
1910
1911        let epoch_before = handle.installed().epoch;
1912        {
1913            let mut guard = aspace.lock();
1914            let start = guard.base();
1915            guard
1916                .map(
1917                    start,
1918                    ax_memory_addr::PAGE_SIZE_4K,
1919                    ax_runtime::hal::paging::MappingFlags::READ
1920                        | ax_runtime::hal::paging::MappingFlags::USER,
1921                    false,
1922                    MappingOperation::new_alloc(
1923                        start,
1924                        ax_memory_addr::PAGE_SIZE_4K,
1925                        "[epoch-source-test]",
1926                    ),
1927                )
1928                .unwrap();
1929        }
1930        assert_eq!(handle.installed().epoch, epoch_before.next());
1931
1932        assert!(Arc::ptr_eq(&mutation_targets, &handle.inner.active_mask));
1933        drop(aspace);
1934
1935        let activation = handle.activation(2).unwrap();
1936        assert_eq!(mutation_targets.load(Ordering::Acquire), 1usize << 2);
1937
1938        activation.release_after_kernel_switch();
1939        assert_eq!(mutation_targets.load(Ordering::Acquire), 0);
1940
1941        let permit = handle
1942            .release_user_ref()
1943            .expect("an inactive ownerless address space must become reclaimable");
1944        permit.reclaim().unwrap();
1945    }
1946
1947    #[axtest::axtest]
1948    fn mm_pin_fault_prepares_outside_and_publishes_through_the_live_mm() {
1949        let start = ax_memory_addr::VirtAddr::from_usize(0x4000);
1950        let aspace = Arc::new(Mutex::new(AddrSpace::new_empty(start, 0x1000).unwrap()));
1951        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
1952        {
1953            let mut guard = aspace.lock();
1954            guard
1955                .map(
1956                    start,
1957                    ax_memory_addr::PAGE_SIZE_4K,
1958                    ax_runtime::hal::paging::MappingFlags::READ
1959                        | ax_runtime::hal::paging::MappingFlags::WRITE
1960                        | ax_runtime::hal::paging::MappingFlags::USER,
1961                    false,
1962                    MappingOperation::new_alloc(
1963                        start,
1964                        ax_memory_addr::PAGE_SIZE_4K,
1965                        "[mm-pin-fault-test]",
1966                    ),
1967                )
1968                .unwrap();
1969        }
1970
1971        let pin = handle.pin().unwrap();
1972        let activation = handle.activation(2).unwrap();
1973        assert_eq!(
1974            pin.handle_page_fault_result(start, PageFaultFlags::READ | PageFaultFlags::USER,),
1975            FaultResult::Handled
1976        );
1977        {
1978            let guard = aspace.lock();
1979            assert_eq!(guard.resident_page_counts().anon, 1);
1980            assert!(guard.pending_tlb_requests().unwrap().is_empty());
1981            assert_eq!(
1982                guard
1983                    .mutation_gate
1984                    .last_retired_receipt()
1985                    .unwrap()
1986                    .tlb_obligation
1987                    .targets(),
1988                0,
1989                "a previously-none PTE must not shoot down another CPU"
1990            );
1991        }
1992
1993        activation.release_after_kernel_switch();
1994        drop(pin);
1995        drop(aspace);
1996        let permit = handle
1997            .release_user_ref()
1998            .expect("the quiescent MM must become reclaimable after the fault");
1999        permit.reclaim().unwrap();
2000    }
2001
2002    #[axtest::axtest]
2003    fn completed_root_switch_is_not_frozen_into_a_new_tlb_obligation() {
2004        let aspace = Arc::new(Mutex::new(
2005            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2006        ));
2007        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
2008        let activation = handle.activation(2).unwrap();
2009
2010        let mutation = aspace.lock().prepare_mutation();
2011        activation.release_after_kernel_switch();
2012
2013        let receipt = aspace
2014            .lock()
2015            .mutation_gate
2016            .commit(mutation)
2017            .expect("a CPU that completed its root switch must not remain a TLB target");
2018        assert_eq!(receipt.tlb_obligation.targets(), 0);
2019        assert_eq!(handle.active_cpu_mask(), 0);
2020
2021        let permit = handle
2022            .release_user_ref()
2023            .expect("an inactive ownerless address space must become reclaimable");
2024        permit.reclaim().unwrap();
2025    }
2026
2027    #[axtest::axtest]
2028    fn last_user_owner_cannot_reclaim_an_active_address_space() {
2029        let aspace = Arc::new(Mutex::new(
2030            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2031        ));
2032        let handle = MmHandle::from_arc(aspace.clone()).unwrap();
2033        let activation = handle.activation(2).unwrap();
2034        let root = handle.installed().root;
2035
2036        assert!(is_address_space_live(handle.id()));
2037        assert!(handle.release_user_ref().is_none());
2038        assert_eq!(handle.state(), MmState::Retiring);
2039        assert!(!is_address_space_live(handle.id()));
2040        assert_eq!(handle.installed().root, root);
2041        assert_eq!(handle.cpu_state().active_cpus, 1usize << 2);
2042        assert_ne!(aspace.lock().materialized_root().as_usize(), 0);
2043
2044        activation.release_after_kernel_switch();
2045        assert_eq!(handle.state(), MmState::Retired);
2046
2047        let (reclaimed, failed) = reap_retired(usize::MAX);
2048        assert!(reclaimed >= 1);
2049        assert_eq!(failed, 0);
2050        assert_eq!(handle.state(), MmState::Freed);
2051    }
2052
2053    #[axtest::axtest]
2054    fn pinned_exit_continuation_can_run_while_the_mm_is_retiring() {
2055        let aspace = Arc::new(Mutex::new(
2056            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2057        ));
2058        let handle = MmHandle::from_arc(aspace).unwrap();
2059        let pin = handle.pin().unwrap();
2060
2061        assert!(handle.release_user_ref().is_none());
2062        assert_eq!(handle.state(), MmState::Retiring);
2063
2064        let activation = pin
2065            .activation_for_switch(2)
2066            .expect("a pinned exit continuation must remain schedulable while retiring");
2067        assert_eq!(handle.active_cpu_mask(), 1usize << 2);
2068        activation.release_after_kernel_switch();
2069
2070        drop(pin);
2071        assert_eq!(handle.state(), MmState::Retired);
2072        let (reclaimed, failed) = reap_retired(usize::MAX);
2073        assert!(reclaimed >= 1);
2074        assert_eq!(failed, 0);
2075        assert_eq!(handle.state(), MmState::Freed);
2076    }
2077
2078    #[axtest::axtest]
2079    fn retire_permit_revalidates_quiescence_under_the_lifecycle_gate() {
2080        let aspace = Arc::new(Mutex::new(
2081            AddrSpace::new_empty(ax_memory_addr::VirtAddr::from_usize(0x1000), 0x1000).unwrap(),
2082        ));
2083        let handle = MmHandle::from_arc(aspace).unwrap();
2084        let pin = handle.pin().unwrap();
2085
2086        assert!(handle.release_user_ref().is_none());
2087        assert_eq!(handle.state(), MmState::Retiring);
2088
2089        // Model the old snapshot/CAS window after it published `Retired` from
2090        // stale zero counters. Permit creation must independently revalidate
2091        // quiescence under the same gate instead of trusting state alone.
2092        {
2093            let _gate = handle.inner.lifecycle_gate.lock();
2094            handle
2095                .inner
2096                .state
2097                .store(MmState::Retired as u8, Ordering::Release);
2098        }
2099        assert!(MmInner::take_retire_permit(&handle.inner).is_none());
2100        assert!(!handle.inner.retire_queued.load(Ordering::Acquire));
2101
2102        {
2103            let _gate = handle.inner.lifecycle_gate.lock();
2104            handle
2105                .inner
2106                .state
2107                .store(MmState::Retiring as u8, Ordering::Release);
2108        }
2109        drop(pin);
2110        assert_eq!(handle.state(), MmState::Retired);
2111        let (reclaimed, failed) = reap_retired(usize::MAX);
2112        assert!(reclaimed >= 1);
2113        assert_eq!(failed, 0);
2114        assert_eq!(handle.state(), MmState::Freed);
2115    }
2116}