Skip to main content

starry_kernel/mm/aspace/
mod.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4    vec::Vec,
5};
6use core::{fmt, sync::atomic::AtomicUsize};
7
8use ax_fs_ng::file::CachedPagePin;
9use ax_memory_addr::{
10    MemoryAddr, PAGE_SIZE_4K, PageIter4K, PhysAddr, VirtAddr, VirtAddrRange, is_aligned_4k,
11};
12#[cfg(not(any(target_arch = "aarch64", target_arch = "loongarch64")))]
13use ax_mm::RootEntryShare;
14use ax_runtime::hal::{
15    mem::phys_to_virt,
16    paging::{
17        InstalledHugeSplit, MappingFlags, PageTable, PageTableEntry, PageTableMapDeposit,
18        PageTableMapPlan, PageTableMovePlan, PagingAllocator, PagingError,
19    },
20    trap::PageFaultFlags,
21};
22
23use crate::{
24    StarryError, StarryResult,
25    config::USER_HEAP_BASE,
26    mm::{ProcessVmStat, ProcessVmStatSnapshot, UserVirtualAddressLayout},
27    sync::{IrqMutex, LockdepMutexExt, Mutex, try_reserve_irq_vec},
28};
29
30#[cfg(test)]
31fn complete_page_fault_with(
32    handled: bool,
33    vaddr: VirtAddr,
34    update_mmu_cache: impl FnOnce(VirtAddr),
35) -> bool {
36    if handled {
37        update_mmu_cache(vaddr);
38    }
39    handled
40}
41
42mod accounting;
43mod backend;
44pub(crate) mod domain;
45pub mod lifecycle;
46pub(crate) mod mutation;
47pub(crate) mod objects;
48pub(crate) mod reclaim;
49pub(crate) mod vma;
50
51pub(crate) use self::domain::PageTableDomain;
52// These are intentionally exported as the new ownership/transaction surface;
53// they are consumed by the migration work as call sites leave the legacy
54// `MappingOperation` facade.
55pub use self::mutation::{
56    AppliedMutation, EvictionResult, MappingDelta, MutationError, MutationGate, MutationReceipt,
57    MutationState, PreparedMutation, PteDelta, PublishEvent, PublishedMutation,
58    PublishedPendingTlb, QuarantineError, QuarantineFailure, ResidentDelta, TlbQuarantine,
59    TlbRange, TlbRequest, VmaDelta,
60};
61use self::{
62    accounting::ResidentWatermark,
63    backend::{
64        FaultFallback, FaultMaterialization, FaultPteSnapshot, PopulateRequest, PreparedPteOwner,
65        ProviderPublication, PteMaterialization, PteOwnerTransition,
66    },
67};
68pub use self::{
69    backend::*,
70    lifecycle::*,
71    objects::{
72        EvictionError, EvictionLease, FrameLease, MappingGraphError, MappingSlot, MappingSlotKey,
73        PageId, PageObject, PageState, RmapSet, SlotState, WritebackError, WritebackLease,
74    },
75    reclaim::*,
76    vma::*,
77};
78
79#[cfg(all(test, axtest))]
80static MAPPING_GRAPH_SNAPSHOT_CALLS: AtomicUsize = AtomicUsize::new(0);
81
82#[derive(Clone, Copy, PartialEq, Eq)]
83enum MovedPageDestination {
84    /// The destination PTE was empty and now names the source PageObject.
85    SourceOwner,
86    /// An eager target backend had already materialized the destination PTE.
87    TargetOwner { slot_va: VirtAddr },
88}
89
90#[derive(Clone, Copy)]
91struct MovedPage {
92    src_va: VirtAddr,
93    dst_va: VirtAddr,
94    paddr: PhysAddr,
95    page_size: usize,
96    destination: MovedPageDestination,
97}
98
99enum PreparedMovedSlot {
100    Relocate {
101        source_key: MappingSlotKey,
102        target_key: MappingSlotKey,
103        source: Arc<MappingSlot>,
104        replacement: Arc<MappingSlot>,
105    },
106    DetachSource {
107        source_key: MappingSlotKey,
108        target_key: MappingSlotKey,
109        source: Arc<MappingSlot>,
110    },
111}
112const CLONED_ADDR_SPACE_LOCK_SUBCLASS: u32 = 1;
113
114#[derive(Clone, Copy)]
115struct ForkParentPteProtection {
116    va: VirtAddr,
117    paddr: PhysAddr,
118    page_size: usize,
119    original_flags: MappingFlags,
120    protected_flags: MappingFlags,
121}
122
123struct PreparedForkParentMutation {
124    mutation: PreparedMutation,
125    ptes: Vec<ForkParentPteProtection>,
126    ranges: Vec<VirtAddrRange>,
127}
128
129#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
130pub(crate) struct ResidentPageCounts {
131    pub anon: u64,
132    pub file: u64,
133    pub shmem: u64,
134}
135
136impl ResidentPageCounts {
137    pub const fn total(self) -> u64 {
138        self.anon
139            .saturating_add(self.file)
140            .saturating_add(self.shmem)
141    }
142
143    fn checked_delta_to(self, after: Self) -> StarryResult<ResidentDelta> {
144        fn delta(before: u64, after: u64) -> StarryResult<i64> {
145            let value = i128::from(after) - i128::from(before);
146            i64::try_from(value).map_err(|_| StarryError::BadState)
147        }
148
149        Ok(ResidentDelta {
150            anon: delta(self.anon, after.anon)?,
151            file: delta(self.file, after.file)?,
152            shmem: delta(self.shmem, after.shmem)?,
153        })
154    }
155
156    fn checked_apply(&mut self, delta: ResidentDelta) -> StarryResult {
157        fn apply(current: u64, delta: i64) -> StarryResult<u64> {
158            if delta >= 0 {
159                current
160                    .checked_add(u64::try_from(delta).map_err(|_| StarryError::BadState)?)
161                    .ok_or(StarryError::BadState)
162            } else {
163                current
164                    .checked_sub(delta.unsigned_abs())
165                    .ok_or(StarryError::BadState)
166            }
167        }
168
169        self.anon = apply(self.anon, delta.anon)?;
170        self.file = apply(self.file, delta.file)?;
171        self.shmem = apply(self.shmem, delta.shmem)?;
172        Ok(())
173    }
174
175    fn checked_add_pages(&mut self, kind: Option<RssKind>, pages: u64) -> StarryResult {
176        let bucket = match kind {
177            Some(RssKind::Anon) => &mut self.anon,
178            Some(RssKind::File) => &mut self.file,
179            Some(RssKind::Shmem) => &mut self.shmem,
180            None => return Ok(()),
181        };
182        *bucket = bucket.checked_add(pages).ok_or(StarryError::BadState)?;
183        Ok(())
184    }
185
186    fn checked_negated_delta(self) -> StarryResult<ResidentDelta> {
187        Ok(ResidentDelta {
188            anon: -i64::try_from(self.anon).map_err(|_| StarryError::BadState)?,
189            file: -i64::try_from(self.file).map_err(|_| StarryError::BadState)?,
190            shmem: -i64::try_from(self.shmem).map_err(|_| StarryError::BadState)?,
191        })
192    }
193
194    fn checked_positive_delta(self) -> StarryResult<ResidentDelta> {
195        Ok(ResidentDelta {
196            anon: i64::try_from(self.anon).map_err(|_| StarryError::BadState)?,
197            file: i64::try_from(self.file).map_err(|_| StarryError::BadState)?,
198            shmem: i64::try_from(self.shmem).map_err(|_| StarryError::BadState)?,
199        })
200    }
201}
202
203impl ResidentDelta {
204    fn for_pages(kind: Option<RssKind>, pages: i64) -> Self {
205        match kind {
206            Some(RssKind::Anon) => Self {
207                anon: pages,
208                ..Self::default()
209            },
210            Some(RssKind::File) => Self {
211                file: pages,
212                ..Self::default()
213            },
214            Some(RssKind::Shmem) => Self {
215                shmem: pages,
216                ..Self::default()
217            },
218            None => Self::default(),
219        }
220    }
221
222    fn checked_add_assign(&mut self, other: Self) -> StarryResult {
223        self.anon = self
224            .anon
225            .checked_add(other.anon)
226            .ok_or(StarryError::BadState)?;
227        self.file = self
228            .file
229            .checked_add(other.file)
230            .ok_or(StarryError::BadState)?;
231        self.shmem = self
232            .shmem
233            .checked_add(other.shmem)
234            .ok_or(StarryError::BadState)?;
235        Ok(())
236    }
237}
238
239#[derive(Debug, Clone, Copy, Default)]
240struct PteOwnerPublication {
241    satisfied_pages: usize,
242    mapping_delta: MappingDelta,
243    resident_delta: ResidentDelta,
244}
245
246struct PreparedSlotPublication {
247    key: MappingSlotKey,
248    previous: Option<Arc<MappingSlot>>,
249    replacement: Option<Arc<MappingSlot>>,
250    resident_kind: Option<RssKind>,
251    provider_publication: ProviderPublication,
252    mapping_delta: MappingDelta,
253    resident_delta: ResidentDelta,
254}
255
256#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
257struct MappingSlotFingerprint {
258    key: MappingSlotKey,
259    mapping: MappingId,
260    page: PageId,
261    page_order: PageOrder,
262}
263
264/// Pre-apply facts used to derive one receipt from the published mapping
265/// graph.  This snapshot owns no page or frame reference; transaction
266/// preimages retain the actual objects needed for rollback.
267struct MappingGraphSnapshot {
268    slots: Vec<MappingSlotFingerprint>,
269    resident: ResidentPageCounts,
270}
271
272impl MappingGraphSnapshot {
273    fn delta_to(&self, after: &Self) -> StarryResult<(MappingDelta, ResidentDelta)> {
274        let mut before_index = 0usize;
275        let mut after_index = 0usize;
276        let mut attached = 0usize;
277        let mut detached = 0usize;
278        while before_index < self.slots.len() && after_index < after.slots.len() {
279            match self.slots[before_index].cmp(&after.slots[after_index]) {
280                core::cmp::Ordering::Less => {
281                    detached = detached.checked_add(1).ok_or(StarryError::BadState)?;
282                    before_index += 1;
283                }
284                core::cmp::Ordering::Equal => {
285                    before_index += 1;
286                    after_index += 1;
287                }
288                core::cmp::Ordering::Greater => {
289                    attached = attached.checked_add(1).ok_or(StarryError::BadState)?;
290                    after_index += 1;
291                }
292            }
293        }
294        detached = detached
295            .checked_add(self.slots.len() - before_index)
296            .ok_or(StarryError::BadState)?;
297        attached = attached
298            .checked_add(after.slots.len() - after_index)
299            .ok_or(StarryError::BadState)?;
300        Ok((
301            MappingDelta {
302                attached: u32::try_from(attached).map_err(|_| StarryError::BadState)?,
303                detached: u32::try_from(detached).map_err(|_| StarryError::BadState)?,
304            },
305            self.resident.checked_delta_to(after.resident)?,
306        ))
307    }
308}
309
310/// Permission views carried by one mapping operation. Keeping current,
311/// user-visible and maximum rights together prevents a caller from
312/// accidentally treating a lowered permission as the immutable VM envelope.
313#[derive(Debug, Clone, Copy)]
314pub struct MappingPermissions {
315    pub current: MappingFlags,
316    pub reported: MappingFlags,
317    pub maximum: MappingFlags,
318}
319
320#[derive(Debug, Clone, Copy)]
321pub(crate) struct MappingPublication {
322    replace: bool,
323    huge_page_advice: HugePageAdvice,
324    lock_mode: VmaLockMode,
325    advice_policy: VmaAdvicePolicy,
326    memlock_limit: Option<MemlockLimit>,
327}
328
329/// Per-syscall view of Linux `RLIMIT_MEMLOCK` and `CAP_IPC_LOCK`.
330///
331/// The limit is only an authorization input. Charged pages are always derived
332/// from the immutable VMA root, so rollback, split, merge and unmap cannot
333/// leave a second counter out of sync.
334#[derive(Debug, Clone, Copy)]
335pub(crate) struct MemlockLimit {
336    page_limit: u64,
337    bypass_limit: bool,
338    may_lock: bool,
339    exceeded_error: MemlockLimitError,
340}
341
342#[derive(Debug, Clone, Copy)]
343enum MemlockLimitError {
344    NoMemory,
345    WouldBlock,
346}
347
348impl MemlockLimit {
349    pub(crate) const fn for_mlock(byte_limit: u64, bypass_limit: bool) -> Self {
350        Self {
351            page_limit: byte_limit / PAGE_SIZE_4K as u64,
352            bypass_limit,
353            may_lock: byte_limit != 0 || bypass_limit,
354            exceeded_error: MemlockLimitError::NoMemory,
355        }
356    }
357
358    pub(crate) const fn for_mapping(byte_limit: u64, bypass_limit: bool) -> Self {
359        Self {
360            page_limit: byte_limit / PAGE_SIZE_4K as u64,
361            bypass_limit,
362            may_lock: byte_limit != 0 || bypass_limit,
363            exceeded_error: MemlockLimitError::WouldBlock,
364        }
365    }
366
367    pub(crate) const fn can_lock(self) -> bool {
368        self.may_lock
369    }
370
371    fn validate(self, locked_pages: u64) -> StarryResult {
372        if self.bypass_limit || locked_pages <= self.page_limit {
373            return Ok(());
374        }
375        Err(match self.exceeded_error {
376            MemlockLimitError::NoMemory => StarryError::NoMemory,
377            MemlockLimitError::WouldBlock => StarryError::WouldBlock,
378        })
379    }
380}
381
382impl MappingPublication {
383    const fn new(replace: bool) -> Self {
384        Self {
385            replace,
386            huge_page_advice: HugePageAdvice::Default,
387            lock_mode: VmaLockMode::Unlocked,
388            advice_policy: VmaAdvicePolicy::DEFAULT,
389            memlock_limit: None,
390        }
391    }
392
393    pub(crate) const fn mmap(
394        replace: bool,
395        lock_mode: VmaLockMode,
396        memlock_limit: Option<MemlockLimit>,
397    ) -> Self {
398        Self {
399            replace,
400            huge_page_advice: HugePageAdvice::Default,
401            lock_mode,
402            advice_policy: VmaAdvicePolicy::DEFAULT,
403            memlock_limit,
404        }
405    }
406
407    const fn mremap(
408        replace: bool,
409        huge_page_advice: HugePageAdvice,
410        lock_mode: VmaLockMode,
411        advice_policy: VmaAdvicePolicy,
412        memlock_limit: Option<MemlockLimit>,
413    ) -> Self {
414        Self {
415            replace,
416            huge_page_advice,
417            lock_mode,
418            advice_policy,
419            memlock_limit,
420        }
421    }
422}
423
424/// Publication result of one address-space mutation.
425///
426/// A pending TLB acknowledgement is a published mutation: metadata and the
427/// matching scalar state must remain visible while the receipt owns the
428/// detached resources.  Keeping it distinct from an unpublished error avoids
429/// reconstructing publication state by comparing epochs at syscall call sites.
430pub(crate) enum AddressSpaceMutationOutcome {
431    Complete,
432    PublishedPendingTlb(StarryError),
433}
434
435impl AddressSpaceMutationOutcome {
436    fn into_result(self) -> StarryResult {
437        match self {
438            Self::Complete => Ok(()),
439            Self::PublishedPendingTlb(error) => Err(error),
440        }
441    }
442}
443
444/// Linux keeps `start_brk` and `brk` in `mm_struct`, under `mmap_lock`.
445/// Keeping the equivalent values inside `AddrSpace` gives `CLONE_VM` one
446/// shared fact and makes a forked MM receive a snapshot with its VMA tree.
447#[derive(Debug, Clone, Copy)]
448struct HeapState {
449    start: usize,
450    current: usize,
451}
452
453impl HeapState {
454    const fn new(start: usize) -> Self {
455        Self {
456            start,
457            current: start,
458        }
459    }
460}
461
462/// Linux `mm->start_data`/`mm->end_data` metadata for the main executable.
463///
464/// The ELF loader publishes this once while the MM is still unreachable.  It
465/// is then copied with fork and read by both `brk` and procfs, preventing the
466/// resource-limit calculation and its observable metadata from diverging.
467#[derive(Debug, Clone, Copy, Default)]
468struct ExecutableDataLayout {
469    start: usize,
470    end: usize,
471}
472
473impl ExecutableDataLayout {
474    fn try_new(start: usize, end: usize) -> StarryResult<Self> {
475        if start == 0 || end < start {
476            return Err(StarryError::MalformedExecutable);
477        }
478        Ok(Self { start, end })
479    }
480
481    fn size(self) -> Option<usize> {
482        self.end.checked_sub(self.start)
483    }
484}
485
486fn checked_page_align_up(value: usize) -> StarryResult<usize> {
487    value
488        .checked_add(PAGE_SIZE_4K - 1)
489        .map(|rounded| rounded & !(PAGE_SIZE_4K - 1))
490        .ok_or(StarryError::InvalidInput)
491}
492
493struct ResidentLeafPreimage {
494    va: VirtAddr,
495    paddr: PhysAddr,
496    page_size: usize,
497    flags: MappingFlags,
498    backend: MappingOperation,
499    page: Arc<PageObject>,
500    slot: Arc<MappingSlot>,
501}
502
503#[derive(Clone, Copy)]
504struct OccupiedPteLeaf {
505    range: VirtAddrRange,
506    paddr: PhysAddr,
507    flags: MappingFlags,
508}
509
510struct MappingPreimage {
511    vma_root: Arc<VmaMap>,
512    vm_stat: ProcessVmStatSnapshot,
513    leaves: Vec<ResidentLeafPreimage>,
514}
515
516struct AppliedHugeSplit {
517    installed: InstalledHugeSplit,
518    old_slot: Arc<MappingSlot>,
519    child_keys: Vec<MappingSlotKey>,
520    child_slots: Vec<Arc<MappingSlot>>,
521    previous_mapping_slots: BTreeMap<MappingSlotKey, Arc<MappingSlot>>,
522}
523
524#[derive(Clone, Copy)]
525struct ProtectionLeafPreimage {
526    va: VirtAddr,
527    paddr: PhysAddr,
528    page_size: usize,
529    flags: MappingFlags,
530}
531
532/// Ownership retained after a PTE is detached and before its TLB receipt is
533/// acknowledged.  Each vector has a distinct ownership role: backend clones
534/// retain shared/device anchors, page objects retain anonymous frames, and
535/// cache pins retain file frames whose PageObject lease is intentionally
536/// non-owning.
537#[derive(Default)]
538struct RetiredMappingOwners {
539    backends: Vec<MappingOperation>,
540    pages: Vec<Arc<PageObject>>,
541    cache_pins: Vec<CachedPagePin>,
542    /// File pages whose eviction stopped after PTE publication but before
543    /// every target CPU acknowledged the receipt.  Releasing this owner marks
544    /// the existing EvictionLease resumable; it must never make the page
545    /// Present again.
546    deferred_evictions: Vec<Arc<PageObject>>,
547}
548
549impl RetiredMappingOwners {
550    fn is_empty(&self) -> bool {
551        self.backends.is_empty()
552            && self.pages.is_empty()
553            && self.cache_pins.is_empty()
554            && self.deferred_evictions.is_empty()
555    }
556}
557
558struct RetiredMappingBatch {
559    epoch: VmEpoch,
560    owners: RetiredMappingOwners,
561}
562
563#[derive(Debug)]
564enum CommitMutationError {
565    /// The epoch/VMA root was not published.  A caller that retained an exact
566    /// preimage may restore it and return the original error.
567    Unpublished(StarryError),
568    /// Publication already advanced the epoch and retained the receipt.  The
569    /// visible mapping must not be rolled back while a remote CPU may still
570    /// hold the preimage translation.
571    PublishedPendingTlb(StarryError),
572}
573
574#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575enum MutationPublication {
576    Complete,
577    PendingTlb,
578}
579
580struct PageFaultPlan {
581    base_epoch: VmEpoch,
582    space_id: AddressSpaceId,
583    vaddr: VirtAddr,
584    range: VirtAddrRange,
585    vma_flags: MappingFlags,
586    access_flags: MappingFlags,
587    operation: MappingOperation,
588    request: PopulateRequest,
589    preimage: FaultPteSnapshot,
590    map_plans: Option<PageFaultMapPlans>,
591}
592
593struct PageFaultMapPlans {
594    preferred: PageTableMapPlan,
595    fallback: Option<PageTableMapPlan>,
596}
597
598fn prepare_mapping_publication_mutation(
599    gate: &MutationGate,
600    space_id: AddressSpaceId,
601    active_targets: &Arc<AtomicUsize>,
602    start: VirtAddr,
603    size: usize,
604    replaces_existing: bool,
605) -> PreparedMutation {
606    // A non-replacing mmap publishes into a range whose current VMA and PTE
607    // preimage are empty. It can use Linux's fresh-PTE fast path only if no
608    // older shootdown for that VA is still pending. MAP_FIXED-style
609    // replacement retains the live target source until commit so a CPU
610    // activated during apply is still included in the shootdown receipt.
611    let mut mutation = if replaces_existing {
612        gate.begin_with_active_targets(space_id, active_targets.clone())
613    } else {
614        gate.begin_fresh_mapping(space_id)
615    };
616    if let Some(range) = TlbRange::new(start, size) {
617        mutation.add_tlb_range(range);
618    }
619    mutation
620}
621
622struct PreparedPageFault {
623    plan: PageFaultPlan,
624    materialization: FaultMaterialization,
625    map_deposit: Option<PageTableMapDeposit>,
626}
627
628impl PreparedPageFault {
629    fn into_apply_attempt(self) -> PageFaultApplyAttempt {
630        PageFaultApplyAttempt {
631            prepared: Some(self),
632            orphaned_map_deposit: None,
633        }
634    }
635
636    fn cancel(self) -> StarryResult {
637        self.plan
638            .operation
639            .cancel_prepared_fault_publication(self.materialization)
640    }
641}
642
643/// Retains ownership of a prepared fault while the address-space lock is held.
644///
645/// Cancellation leaves the token populated so the caller can release backend
646/// reservations after dropping the address-space lock. Successful publication
647/// consumes it before returning, keeping the apply result small without adding
648/// a heap allocation to every page fault.
649struct PageFaultApplyAttempt {
650    prepared: Option<PreparedPageFault>,
651    /// An internally duplicated deposit is retained here so even a corrupted
652    /// state never releases page-table frames below the address-space mutex.
653    orphaned_map_deposit: Option<PageTableMapDeposit>,
654}
655
656impl PageFaultApplyAttempt {
657    fn prepared(&self) -> &PreparedPageFault {
658        self.prepared
659            .as_ref()
660            .expect("page-fault apply attempt must retain its prepared token")
661    }
662
663    fn take_prepared(&mut self) -> PreparedPageFault {
664        self.prepared
665            .take()
666            .expect("page-fault apply attempt must consume its prepared token once")
667    }
668
669    fn take_map_deposit(&mut self) -> Option<PageTableMapDeposit> {
670        self.prepared.as_mut()?.map_deposit.take()
671    }
672
673    fn restore_map_deposit(&mut self, deposit: PageTableMapDeposit) {
674        if let Some(prepared) = self.prepared.as_mut()
675            && prepared.map_deposit.is_none()
676        {
677            prepared.map_deposit = Some(deposit);
678            return;
679        }
680        // This slot is reachable only if the internal prepared-token invariant
681        // was already violated. Retain the extra owner for lock-free release
682        // instead of dropping either page-table path here.
683        debug_assert!(self.orphaned_map_deposit.is_none());
684        self.orphaned_map_deposit = Some(deposit);
685    }
686
687    fn cancel(self) -> StarryResult {
688        let Self {
689            prepared,
690            orphaned_map_deposit,
691        } = self;
692        drop(orphaned_map_deposit);
693        prepared
694            .expect("cancelled page-fault apply attempt must retain its prepared token")
695            .cancel()
696    }
697
698    /// Releases only the caller-side token after an indeterminate apply.
699    ///
700    /// A Pending backend publication remains the explicit quarantine owner:
701    /// CowPageIndex retains the allocated PageObject and FilePageDomain retains
702    /// both the PageObject and CachedPagePin. The map deposit, if any, is still
703    /// unreachable and is deliberately dropped here after the address-space
704    /// mutex and every PTE stripe have been released.
705    fn release_to_repair_state(self) {
706        debug_assert!(self.prepared.is_some());
707        drop(self);
708    }
709}
710
711enum PageFaultApplyOutcome {
712    Complete(FaultResult),
713    Cancel(FaultResult),
714    NeedsRepair(FaultResult),
715    /// No PTE or owner was published. Cancel the prepared candidate before
716    /// servicing this older obligation, then plan the fault again.
717    CancelPendingTlb {
718        request: TlbRequest,
719        targets: Arc<AtomicUsize>,
720    },
721    PendingTlb {
722        request: TlbRequest,
723        targets: Arc<AtomicUsize>,
724    },
725}
726
727/// Result of revoking one file-cache reverse mapping.  A pending TLB result is
728/// successful publication, not a rollback-safe error.  `NeedsRepair` likewise
729/// keeps the page in Evicting so it cannot be reused from an unproved state.
730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub(crate) enum EvictMappingOutcome {
732    Complete,
733    PublishedPendingTlb,
734    NeedsRepair,
735}
736
737/// The virtual memory address space.
738pub struct AddrSpace {
739    id: AddressSpaceId,
740    /// Immutable ABI and hardware address limits captured at MM creation.
741    layout: UserVirtualAddressLayout,
742    /// The sole VMA metadata and executable-operation publication owner.
743    /// Readers receive metadata-only snapshots; mutations path-copy a complete
744    /// successor before any PTE apply phase begins.
745    vma_root: Arc<VmaMap>,
746    /// Heap boundaries owned by this MM and protected by the same lock as its
747    /// VMA/PTE mutation.  This is the Rust equivalent of Linux `mm->start_brk`
748    /// and `mm->brk`, not a process-side mirror.
749    heap: HeapState,
750    executable_data: ExecutableDataLayout,
751    /// The main image remains write-protected until this MM is retired.
752    executable_file: Option<Arc<ax_fs_ng::file::ExecutableFile>>,
753    pt: PageTable,
754    /// Fixed-order PTE/structure lock domains.  The page-table root remains a
755    /// materialized view; ownership is carried by VMA/page records.
756    pte_domain: PageTableDomain,
757    /// Monotonic publication epoch for VMA/PTE mutations.
758    mutation_gate: MutationGate,
759    /// Lock-free scheduler view of the last published mutation epoch.  It is
760    /// shared with `MmInner`; every commit updates it before returning so an
761    /// activation cannot install a stale generation.
762    published_epoch: Arc<core::sync::atomic::AtomicU64>,
763    /// All VmX counters for this address space.  Maintained automatically by
764    /// `map`, `unmap`, `clear`, and `try_clone`; never touch from outside mm/.
765    pub vm_stat: ProcessVmStat,
766    /// Current Linux RSS buckets. This value changes only when one published
767    /// mutation receipt is committed; MappingSlot remains the corresponding
768    /// installed-page ownership fact and rollback never touches these counters.
769    resident_pages: ResidentPageCounts,
770    resident_watermark: ResidentWatermark,
771    /// Frames detached by a mutation remain here until every TLB obligation
772    /// acknowledges the corresponding `(space_id, epoch)` request.
773    tlb_quarantine: TlbQuarantine,
774    /// Shared with the typed MM lifecycle object.  It is a materialized
775    /// active-CPU mask used only to form shootdown targets; ownership remains
776    /// in `MmInner`, and no lock is taken on the scheduler hot path.
777    tlb_targets: Arc<AtomicUsize>,
778    /// Authoritative per-address-space resident mapping records.  Legacy
779    /// backends still perform the hardware write, then this index publishes
780    /// the corresponding `MappingSlot`/rmap record in the same mutation path.
781    mapping_slots: BTreeMap<MappingSlotKey, Arc<MappingSlot>>,
782    /// Mapping owners detached by published mutations.  A batch is removed
783    /// only after the matching epoch's active-CPU shootdown completes.  An
784    /// uncommitted mutation that entered NeedsRepair deliberately leaves its
785    /// batch here, preventing teardown from fabricating a successful retire.
786    retired_mapping_batches: IrqMutex<Vec<RetiredMappingBatch>>,
787}
788
789impl AddrSpace {
790    /// Returns the address space base.
791    pub const fn base(&self) -> VirtAddr {
792        self.layout.range().start
793    }
794
795    /// Returns the address space end.
796    pub const fn end(&self) -> VirtAddr {
797        self.layout.task_size()
798    }
799
800    /// Returns the address space size.
801    pub fn size(&self) -> usize {
802        self.layout.range().size()
803    }
804
805    /// Returns the initial-stack ceiling captured by this MM.
806    pub const fn stack_top(&self) -> VirtAddr {
807        self.layout.stack_top()
808    }
809
810    /// Returns the immutable heap start recorded for `/proc/[pid]/stat` and
811    /// resource-limit calculations.
812    pub(crate) const fn heap_start(&self) -> usize {
813        self.heap.start
814    }
815
816    /// Returns the current program break while the address-space lock is held.
817    pub(crate) const fn heap_break(&self) -> usize {
818        self.heap.current
819    }
820
821    /// Transfers the loader's exclusion lease to the address-space lifetime.
822    pub(crate) fn set_executable_file(&mut self, file: ax_fs_ng::file::ExecutableFile) {
823        self.executable_file = Some(Arc::new(file));
824    }
825
826    /// Ends file exclusion at last-user exit, separately from physical reclaim.
827    /// Returned leases must be destroyed after releasing the MM metadata lock.
828    fn take_file_accesses(
829        &mut self,
830    ) -> (
831        Option<Arc<ax_fs_ng::file::ExecutableFile>>,
832        Vec<Arc<ax_fs_ng::file::WriteAccess>>,
833    ) {
834        let mut writers = Vec::new();
835        for entry in self.vma_root.iter_entries() {
836            if let Some(access) = entry.operation().take_write_access() {
837                writers.push(access);
838            }
839        }
840        (self.executable_file.take(), writers)
841    }
842
843    /// Publishes the main executable's Linux `start_data`/`end_data` pair.
844    /// This is only called for an unpublished loader-owned address space.
845    pub(crate) fn set_executable_data_layout(&mut self, start: usize, end: usize) -> StarryResult {
846        let layout = ExecutableDataLayout::try_new(start, end)?;
847        if start < self.base().as_usize() || end > self.end().as_usize() {
848            return Err(StarryError::MalformedExecutable);
849        }
850        self.executable_data = layout;
851        Ok(())
852    }
853
854    pub(crate) const fn executable_data_bounds(&self) -> (usize, usize) {
855        (self.executable_data.start, self.executable_data.end)
856    }
857
858    pub(crate) fn executable_data_size(&self) -> Option<usize> {
859        self.executable_data.size()
860    }
861
862    /// Applies one Linux-style `brk` VMA change and publishes the scalar break
863    /// under the same address-space lock.
864    ///
865    /// Unpublished mapping failures leave both values unchanged.  A mutation
866    /// whose VMA/PTE epoch is already published also publishes `brk` before
867    /// returning [`AddressSpaceMutationOutcome::PublishedPendingTlb`]; its
868    /// retained receipt prevents reuse until shootdown acknowledgement.
869    pub(crate) fn resize_heap_break(
870        &mut self,
871        requested: usize,
872        initial_mapping_end: usize,
873    ) -> StarryResult<AddressSpaceMutationOutcome> {
874        let old_break = self.heap.current;
875        let old_aligned = checked_page_align_up(old_break)?;
876        let new_aligned = checked_page_align_up(requested)?;
877
878        let outcome = if new_aligned > old_aligned {
879            let map_start = initial_mapping_end.max(old_aligned);
880            let map_size = new_aligned.saturating_sub(map_start);
881            if map_size == 0 {
882                AddressSpaceMutationOutcome::Complete
883            } else {
884                let start = VirtAddr::from(map_start);
885                let flags = MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER;
886                self.map_with_permissions_mode_classified(
887                    start,
888                    map_size,
889                    MappingPermissions {
890                        current: flags,
891                        reported: flags,
892                        maximum: flags,
893                    },
894                    false,
895                    MappingOperation::new_alloc(start, PAGE_SIZE_4K, "[heap]"),
896                    MappingPublication::new(false),
897                )?
898            }
899        } else if new_aligned < old_aligned {
900            let unmap_start = initial_mapping_end.max(new_aligned);
901            let unmap_size = old_aligned.saturating_sub(unmap_start);
902            if unmap_size == 0 {
903                AddressSpaceMutationOutcome::Complete
904            } else {
905                self.unmap_classified(VirtAddr::from(unmap_start), unmap_size)?
906            }
907        } else {
908            AddressSpaceMutationOutcome::Complete
909        };
910
911        // Linux preserves the exact unaligned user request while VMA changes
912        // use page-aligned boundaries.
913        self.heap.current = requested;
914        Ok(outcome)
915    }
916
917    /// Translates one materialized user address without exposing the page
918    /// table implementation to callers.
919    pub(crate) fn translate(&self, vaddr: VirtAddr) -> StarryResult<PhysAddr> {
920        self.pt
921            .query(vaddr)
922            .map(|(paddr, ..)| paddr)
923            .map_err(Into::into)
924    }
925
926    /// Returns the size of the resident leaf containing `vaddr`.
927    ///
928    /// Absence means the VMA is lazy or has been reclaimed; callers must use
929    /// the immutable VMA snapshot to distinguish that from an invalid VA.
930    pub(crate) fn resident_span(&self, vaddr: VirtAddr) -> Option<usize> {
931        self.pt.query(vaddr).ok().map(|(_, _, size)| size)
932    }
933
934    /// Returns resident bytes from an address to its owning leaf's end,
935    /// including permissionless leaves. This is a residency snapshot, not an
936    /// access capability; callers must validate VMA coverage separately.
937    pub(crate) fn resident_bytes_from(&self, vaddr: VirtAddr) -> Option<usize> {
938        let key = MappingSlotKey {
939            space_id: self.id,
940            va: vaddr,
941        };
942        let (_, slot) = self.mapping_slots.range(..=key).next_back()?;
943        if slot.state() != SlotState::Present {
944            return None;
945        }
946        let bytes = PAGE_SIZE_4K.checked_shl(slot.page_order.get().into())?;
947        let end = slot.va.checked_add(bytes)?;
948        (vaddr >= slot.va && vaddr < end).then(|| end.as_usize() - vaddr.as_usize())
949    }
950
951    /// Iterates only MappingSlots that can overlap `range`.
952    ///
953    /// A huge leaf may begin before `range.start`, so the ordered walk includes
954    /// the immediate predecessor before visiting keys whose start lies inside
955    /// the range. No earlier slot can overlap because published slots within
956    /// one address space never overlap each other.
957    fn mapping_slots_overlapping(
958        &self,
959        range: VirtAddrRange,
960    ) -> impl Iterator<Item = (&MappingSlotKey, &Arc<MappingSlot>)> {
961        let start = MappingSlotKey {
962            space_id: self.id,
963            va: range.start,
964        };
965        let end = MappingSlotKey {
966            space_id: self.id,
967            va: range.end,
968        };
969        let predecessor = self
970            .mapping_slots
971            .range(..start)
972            .next_back()
973            .filter(move |(key, slot)| key.space_id == self.id && slot.overlaps(range));
974        let inside = self
975            .mapping_slots
976            .range(start..end)
977            .filter(move |(key, slot)| key.space_id == self.id && slot.overlaps(range));
978        predecessor.into_iter().chain(inside)
979    }
980
981    fn mapping_slot_summary(
982        &self,
983        range: VirtAddrRange,
984    ) -> StarryResult<(usize, usize, ResidentPageCounts)> {
985        let mut slots = 0usize;
986        let mut materialized_pages = 0usize;
987        let mut resident = ResidentPageCounts::default();
988        for (_, slot) in self.mapping_slots_overlapping(range) {
989            if slot.state() != SlotState::Present {
990                return Err(StarryError::BadState);
991            }
992            let pages = 1usize
993                .checked_shl(slot.page_order.get().into())
994                .ok_or(StarryError::BadState)?;
995            slots = slots.checked_add(1).ok_or(StarryError::BadState)?;
996            materialized_pages = materialized_pages
997                .checked_add(pages)
998                .ok_or(StarryError::BadState)?;
999            resident.checked_add_pages(
1000                slot.resident_kind(),
1001                u64::try_from(pages).map_err(|_| StarryError::BadState)?,
1002            )?;
1003        }
1004        Ok((slots, materialized_pages, resident))
1005    }
1006
1007    fn resident_counts_from_all_slots(&self) -> StarryResult<ResidentPageCounts> {
1008        let mut resident = ResidentPageCounts::default();
1009        for slot in self.mapping_slots.values() {
1010            if slot.state() != SlotState::Present {
1011                return Err(StarryError::BadState);
1012            }
1013            let pages = 1u64
1014                .checked_shl(slot.page_order.get().into())
1015                .ok_or(StarryError::BadState)?;
1016            resident.checked_add_pages(slot.resident_kind(), pages)?;
1017        }
1018        Ok(resident)
1019    }
1020
1021    /// Returns occupied page-table leaves overlapping any requested range.
1022    ///
1023    /// This includes retained non-present leaves: protection may remove
1024    /// hardware access without releasing the PTE's frame ownership. Walking
1025    /// allocated page-table frames keeps sparse transactions proportional to
1026    /// materialized state instead of the virtual address span.
1027    fn occupied_pte_leaves_overlapping(
1028        &self,
1029        ranges: &[VirtAddrRange],
1030    ) -> StarryResult<Vec<OccupiedPteLeaf>> {
1031        let mut leaves = Vec::new();
1032        for range in ranges {
1033            for entry in self.pt.walk_occupied_range(range.start, range.end) {
1034                let leaf_start = entry.vaddr;
1035                let page_size = self
1036                    .pt
1037                    .mapping_size_for_level(entry.level)
1038                    .ok_or(StarryError::BadState)?;
1039                let leaf_end = leaf_start
1040                    .checked_add(page_size)
1041                    .ok_or(StarryError::BadState)?;
1042                if leaf_start >= range.end || leaf_end <= range.start {
1043                    continue;
1044                }
1045                if leaf_start < range.start || leaf_end > range.end {
1046                    return Err(StarryError::OperationNotSupported);
1047                }
1048                let is_directory_level = entry.level > 1;
1049                leaves.try_reserve(1).map_err(|_| StarryError::NoMemory)?;
1050                leaves.push(OccupiedPteLeaf {
1051                    range: VirtAddrRange::new(leaf_start, leaf_end),
1052                    paddr: entry.pte.paddr(is_directory_level),
1053                    flags: entry.pte.config(is_directory_level),
1054                });
1055            }
1056        }
1057        Ok(leaves)
1058    }
1059
1060    /// Retains the published ownership records matching the occupied PTE
1061    /// leaves in the requested ranges. Both directions are verified so neither
1062    /// a PTE without a MappingSlot nor a Present slot without a PTE can enter a
1063    /// transaction preimage.
1064    fn materialized_slots_overlapping(
1065        &self,
1066        ranges: &[VirtAddrRange],
1067    ) -> StarryResult<Vec<(MappingSlotKey, Arc<MappingSlot>, OccupiedPteLeaf)>> {
1068        let leaves = self.occupied_pte_leaves_overlapping(ranges)?;
1069        let mut slots = Vec::new();
1070        slots
1071            .try_reserve(leaves.len())
1072            .map_err(|_| StarryError::NoMemory)?;
1073        for leaf in leaves {
1074            let key = MappingSlotKey {
1075                space_id: self.id,
1076                va: leaf.range.start,
1077            };
1078            let slot = self
1079                .mapping_slots
1080                .get(&key)
1081                .cloned()
1082                .ok_or(StarryError::BadState)?;
1083            let expected_size = PAGE_SIZE_4K
1084                .checked_shl(slot.page_order.get().into())
1085                .ok_or(StarryError::BadState)?;
1086            if slot.state() != SlotState::Present
1087                || slot.mm_id != self.id
1088                || slot.va != key.va
1089                || expected_size != leaf.range.size()
1090                || slot.mapped_paddr() != Some(leaf.paddr)
1091            {
1092                return Err(StarryError::BadState);
1093            }
1094            slots.push((key, slot, leaf));
1095        }
1096        let overlapping_slots = ranges
1097            .iter()
1098            .map(|range| self.mapping_slots_overlapping(*range).count())
1099            .sum::<usize>();
1100        if overlapping_slots != slots.len() {
1101            return Err(StarryError::BadState);
1102        }
1103        Ok(slots)
1104    }
1105
1106    /// Rejects an operation that would carve only part of a materialized
1107    /// huge-page leaf.  Until the typed THP split receipt is wired through all
1108    /// four architectures, moving or replacing such a leaf must fail before
1109    /// either source or destination metadata changes.
1110    pub(crate) fn validate_materialized_leaf_boundaries(
1111        &self,
1112        start: VirtAddr,
1113        size: usize,
1114    ) -> StarryResult {
1115        self.validate_region(start, size)?;
1116        let range =
1117            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
1118        for (key, slot, leaf) in self.materialized_slots_overlapping(&[range])? {
1119            let page_size = leaf.range.size();
1120            let leaf_end = slot
1121                .va
1122                .checked_add(page_size)
1123                .ok_or(StarryError::BadState)?;
1124            if slot.va < range.start || leaf_end > range.end {
1125                return Err(StarryError::OperationNotSupported);
1126            }
1127            if key.va != slot.va
1128                || slot.mm_id != self.id
1129                || slot.state() != SlotState::Present
1130                || slot.mapped_paddr() != Some(leaf.paddr)
1131            {
1132                return Err(StarryError::BadState);
1133            }
1134        }
1135        Ok(())
1136    }
1137
1138    /// Splits only huge leaves crossed by a mutation boundary.  Allocation of
1139    /// child MappingSlots and the replacement BTreeMap happens before the PTE
1140    /// stripe is acquired; apply itself consumes the leaf's deposited table and
1141    /// publishes one rmap/refcount cardinality change.
1142    fn apply_partial_huge_splits(
1143        &mut self,
1144        range: VirtAddrRange,
1145    ) -> StarryResult<Vec<AppliedHugeSplit>> {
1146        let mut candidates = Vec::new();
1147        candidates
1148            .try_reserve(2)
1149            .map_err(|_| StarryError::NoMemory)?;
1150        for (key, slot) in self.mapping_slots_overlapping(range) {
1151            if slot.page_order == PageOrder::BASE || !slot.overlaps(range) {
1152                continue;
1153            }
1154            let slot_size = PAGE_SIZE_4K
1155                .checked_shl(slot.page_order.get().into())
1156                .ok_or(StarryError::BadState)?;
1157            let slot_end = slot
1158                .va
1159                .checked_add(slot_size)
1160                .ok_or(StarryError::BadState)?;
1161            if range.start <= slot.va && slot_end <= range.end {
1162                continue;
1163            }
1164            candidates
1165                .try_reserve(1)
1166                .map_err(|_| StarryError::NoMemory)?;
1167            candidates.push((*key, slot.clone()));
1168        }
1169
1170        let mut applied = Vec::new();
1171        applied
1172            .try_reserve(candidates.len())
1173            .map_err(|_| StarryError::NoMemory)?;
1174        for (key, slot) in candidates {
1175            match self.apply_one_partial_huge_split(key, slot) {
1176                Ok(split) => applied.push(split),
1177                Err(error) => {
1178                    if !self.rollback_applied_huge_splits(applied) {
1179                        self.mutation_gate.mark_needs_repair();
1180                        return Err(StarryError::BadState);
1181                    }
1182                    return Err(error);
1183                }
1184            }
1185        }
1186        Ok(applied)
1187    }
1188
1189    /// Applies boundary splits for several already-validated, disjoint
1190    /// mutation ranges.  Earlier ranges are rolled back if a later range
1191    /// cannot consume its deposit, so callers either receive every split
1192    /// receipt or observe the original huge-leaf graph.
1193    fn apply_partial_huge_splits_for_ranges(
1194        &mut self,
1195        ranges: &[VirtAddrRange],
1196    ) -> StarryResult<Vec<AppliedHugeSplit>> {
1197        let capacity = ranges.len().checked_mul(2).ok_or(StarryError::NoMemory)?;
1198        let mut applied = Vec::new();
1199        applied
1200            .try_reserve_exact(capacity)
1201            .map_err(|_| StarryError::NoMemory)?;
1202        for range in ranges {
1203            match self.apply_partial_huge_splits(*range) {
1204                Ok(mut splits) => applied.append(&mut splits),
1205                Err(error) => {
1206                    if !self.rollback_applied_huge_splits(applied) {
1207                        self.mutation_gate.mark_needs_repair();
1208                        return Err(StarryError::BadState);
1209                    }
1210                    return Err(error);
1211                }
1212            }
1213        }
1214        Ok(applied)
1215    }
1216
1217    fn apply_one_partial_huge_split(
1218        &mut self,
1219        old_key: MappingSlotKey,
1220        old_slot: Arc<MappingSlot>,
1221    ) -> StarryResult<AppliedHugeSplit> {
1222        // Starry's first THP implementation is order-9.  A larger block needs
1223        // another deposited level and must not be represented as 4 KiB slots.
1224        if old_slot.page_order != PageOrder::new(9)
1225            || old_slot.state() != SlotState::Present
1226            || !self
1227                .mapping_slots
1228                .get(&old_key)
1229                .is_some_and(|slot| Arc::ptr_eq(slot, &old_slot))
1230        {
1231            return Err(StarryError::OperationNotSupported);
1232        }
1233        let block_size = PAGE_SIZE_4K
1234            .checked_shl(old_slot.page_order.get().into())
1235            .ok_or(StarryError::BadState)?;
1236        let block_range = VirtAddrRange::try_from_start_size(old_slot.va, block_size)
1237            .ok_or(StarryError::BadState)?;
1238        let (mapped_paddr, _, mapped_size) = self.pt.query(old_slot.va)?;
1239        if old_slot.mapped_paddr() != Some(mapped_paddr)
1240            || mapped_size != block_size
1241            || old_slot.page.frame().size() < block_size
1242        {
1243            return Err(StarryError::BadState);
1244        }
1245
1246        let child_count = block_size / PAGE_SIZE_4K;
1247        let mut child_keys = Vec::new();
1248        let mut child_slots = Vec::new();
1249        child_keys
1250            .try_reserve_exact(child_count)
1251            .map_err(|_| StarryError::NoMemory)?;
1252        child_slots
1253            .try_reserve_exact(child_count)
1254            .map_err(|_| StarryError::NoMemory)?;
1255        for index in 0..child_count {
1256            let offset = index
1257                .checked_mul(PAGE_SIZE_4K)
1258                .ok_or(StarryError::BadState)?;
1259            let va = old_slot
1260                .va
1261                .checked_add(offset)
1262                .ok_or(StarryError::BadState)?;
1263            let key = MappingSlotKey {
1264                space_id: self.id,
1265                va,
1266            };
1267            let child = MappingSlot::new_with_frame_offset(
1268                old_slot.mapping,
1269                self.id,
1270                va,
1271                PageOrder::BASE,
1272                old_slot.page.clone(),
1273                old_slot
1274                    .frame_offset()
1275                    .checked_add(offset)
1276                    .ok_or(StarryError::BadState)?,
1277                old_slot.resident_kind(),
1278            )
1279            .ok_or(StarryError::BadState)?;
1280            child_keys.push(key);
1281            child_slots.push(Arc::new(child));
1282        }
1283
1284        // Build the published slot root before touching the materialized page
1285        // table. BTreeMap has no fallible reserve API on this toolchain.
1286        let mut next_mapping_slots = self.mapping_slots.clone();
1287        let removed = next_mapping_slots
1288            .remove(&old_key)
1289            .ok_or(StarryError::BadState)?;
1290        if !Arc::ptr_eq(&removed, &old_slot) {
1291            return Err(StarryError::BadState);
1292        }
1293        for (key, slot) in child_keys.iter().copied().zip(child_slots.iter().cloned()) {
1294            if next_mapping_slots.insert(key, slot).is_some() {
1295                return Err(StarryError::BadState);
1296            }
1297        }
1298
1299        // THP split grows one rmap entry into 512. Reserve any replacement
1300        // backing store and claim capacity before taking the PTE stripe; apply
1301        // below only changes keys under the IRQ-saving graph lock.
1302        let old_keys = [old_key];
1303        let mut graph_reservation = old_slot
1304            .page
1305            .prepare_mapping_graph_replace(&old_keys, &child_keys)
1306            .map_err(|error| match error {
1307                MappingGraphError::ResourceExhausted | MappingGraphError::RefOverflow => {
1308                    StarryError::NoMemory
1309                }
1310                _ => StarryError::BadState,
1311            })?;
1312
1313        let deposit = old_slot
1314            .take_huge_split_deposit()
1315            .ok_or(StarryError::BadState)?;
1316        let mutation_gate = &self.mutation_gate;
1317        let pte_domain = &self.pte_domain;
1318        let pt = &mut self.pt;
1319        let stripe = pte_domain.lock_range(block_range);
1320        let installed = match pt.try_split_huge_page_with(deposit) {
1321            Ok(installed) => installed,
1322            Err(failure) => {
1323                let (error, deposit) = failure.into_parts();
1324                match old_slot.restore_huge_split_deposit(deposit) {
1325                    Ok(()) => {
1326                        drop(stripe);
1327                        drop(graph_reservation);
1328                        return Err(error.into());
1329                    }
1330                    Err(orphaned) => {
1331                        // The detached table is unreachable, but releasing its
1332                        // frame while IRQs are disabled would nest the global
1333                        // allocator below the PTE stripe.
1334                        drop(stripe);
1335                        drop(graph_reservation);
1336                        drop(orphaned);
1337                        mutation_gate.mark_needs_repair();
1338                        return Err(StarryError::BadState);
1339                    }
1340                }
1341            }
1342        };
1343
1344        if let Err(graph_error) = old_slot.page.replace_mapping_graph_reserved(
1345            &old_keys,
1346            &child_keys,
1347            &mut graph_reservation,
1348        ) {
1349            let restored = pt.restore_huge_split(installed);
1350            match restored {
1351                Ok(deposit) => match old_slot.restore_huge_split_deposit(deposit) {
1352                    Ok(()) => {
1353                        drop(stripe);
1354                        drop(graph_reservation);
1355                        return Err(match graph_error {
1356                            MappingGraphError::ResourceExhausted
1357                            | MappingGraphError::RefOverflow => StarryError::NoMemory,
1358                            _ => StarryError::BadState,
1359                        });
1360                    }
1361                    Err(orphaned) => {
1362                        drop(stripe);
1363                        drop(graph_reservation);
1364                        drop(orphaned);
1365                    }
1366                },
1367                Err(_) => {
1368                    drop(stripe);
1369                    drop(graph_reservation);
1370                }
1371            }
1372            mutation_gate.mark_needs_repair();
1373            return Err(StarryError::BadState);
1374        }
1375
1376        let mut published_children = 0usize;
1377        for child in &child_slots {
1378            if !child.publish_after_graph_replace() {
1379                break;
1380            }
1381            published_children += 1;
1382        }
1383        let old_detached =
1384            published_children == child_slots.len() && old_slot.detach_after_graph_replace();
1385        if !old_detached {
1386            for child in child_slots[..published_children].iter().rev() {
1387                let _ = child.reserve_after_graph_replace();
1388            }
1389            let graph_restored = old_slot
1390                .page
1391                .replace_mapping_graph_reserved(&child_keys, &old_keys, &mut graph_reservation)
1392                .is_ok();
1393            let restored = pt.restore_huge_split(installed);
1394            let (deposit_restored, orphaned) = match restored {
1395                Ok(deposit) => match old_slot.restore_huge_split_deposit(deposit) {
1396                    Ok(()) => (true, None),
1397                    Err(orphaned) => (false, Some(orphaned)),
1398                },
1399                Err(_) => (false, None),
1400            };
1401            drop(stripe);
1402            drop(graph_reservation);
1403            drop(orphaned);
1404            if graph_restored && deposit_restored {
1405                return Err(StarryError::BadState);
1406            }
1407            mutation_gate.mark_needs_repair();
1408            return Err(StarryError::BadState);
1409        }
1410
1411        drop(stripe);
1412        drop(graph_reservation);
1413        let previous_mapping_slots =
1414            core::mem::replace(&mut self.mapping_slots, next_mapping_slots);
1415        Ok(AppliedHugeSplit {
1416            installed,
1417            old_slot,
1418            child_keys,
1419            child_slots,
1420            previous_mapping_slots,
1421        })
1422    }
1423
1424    fn rollback_applied_huge_splits(&mut self, mut splits: Vec<AppliedHugeSplit>) -> bool {
1425        while let Some(split) = splits.pop() {
1426            let block_range = VirtAddrRange::try_from_start_size(
1427                split.installed.block_vaddr(),
1428                split.installed.block_size(),
1429            );
1430            let Some(block_range) = block_range else {
1431                return false;
1432            };
1433            let old_key = MappingSlotKey {
1434                space_id: self.id,
1435                va: split.old_slot.va,
1436            };
1437            let old_keys = [old_key];
1438            let mut graph_reservation = match split
1439                .old_slot
1440                .page
1441                .prepare_mapping_graph_replace(&split.child_keys, &old_keys)
1442            {
1443                Ok(reservation) => reservation,
1444                Err(_) => return false,
1445            };
1446            let pte_domain = &self.pte_domain;
1447            let pt = &mut self.pt;
1448            let stripe = pte_domain.lock_range(block_range);
1449            let Ok(deposit) = pt.restore_huge_split(split.installed) else {
1450                drop(stripe);
1451                drop(graph_reservation);
1452                return false;
1453            };
1454            if split
1455                .old_slot
1456                .page
1457                .replace_mapping_graph_reserved(
1458                    &split.child_keys,
1459                    &old_keys,
1460                    &mut graph_reservation,
1461                )
1462                .is_err()
1463            {
1464                let orphaned = split.old_slot.restore_huge_split_deposit(deposit).err();
1465                drop(stripe);
1466                drop(graph_reservation);
1467                drop(orphaned);
1468                return false;
1469            }
1470            let slots_restored = !split
1471                .child_slots
1472                .iter()
1473                .any(|slot| !slot.detach_after_graph_replace())
1474                && split.old_slot.restore_after_graph_replace();
1475            let orphaned = split.old_slot.restore_huge_split_deposit(deposit).err();
1476            let deposit_restored = orphaned.is_none();
1477            drop(stripe);
1478            drop(graph_reservation);
1479            drop(orphaned);
1480            if !slots_restored || !deposit_restored {
1481                return false;
1482            }
1483            self.mapping_slots = split.previous_mapping_slots;
1484        }
1485        true
1486    }
1487
1488    fn capture_protection_leaf_preimage(
1489        &self,
1490        range: VirtAddrRange,
1491    ) -> StarryResult<Vec<ProtectionLeafPreimage>> {
1492        let occupied = self.occupied_pte_leaves_overlapping(&[range])?;
1493        let mut leaves = Vec::new();
1494        leaves
1495            .try_reserve(occupied.len())
1496            .map_err(|_| StarryError::NoMemory)?;
1497        for leaf in occupied {
1498            leaves.push(ProtectionLeafPreimage {
1499                va: leaf.range.start,
1500                paddr: leaf.paddr,
1501                page_size: leaf.range.size(),
1502                flags: leaf.flags,
1503            });
1504        }
1505        Ok(leaves)
1506    }
1507
1508    fn restore_protection_leaf_preimage(&mut self, leaves: &[ProtectionLeafPreimage]) -> bool {
1509        for leaf in leaves.iter().rev() {
1510            let Ok((paddr, _, page_size)) = self.pt.query(leaf.va) else {
1511                return false;
1512            };
1513            if paddr != leaf.paddr || page_size != leaf.page_size {
1514                return false;
1515            }
1516            if self.pt.protect_page(leaf.va, leaf.flags) != Ok(leaf.page_size) {
1517                return false;
1518            }
1519        }
1520        true
1521    }
1522
1523    fn abort_unpublished_protection(
1524        &mut self,
1525        vma_root: Arc<VmaMap>,
1526        vm_stat: ProcessVmStatSnapshot,
1527        leaves: &[ProtectionLeafPreimage],
1528        splits: Vec<AppliedHugeSplit>,
1529        original_error: StarryError,
1530    ) -> StarryResult {
1531        let ptes_restored = self.restore_protection_leaf_preimage(leaves);
1532        self.vma_root = vma_root;
1533        self.vm_stat.restore(vm_stat);
1534        let splits_restored = self.rollback_applied_huge_splits(splits);
1535        if ptes_restored && splits_restored {
1536            self.mutation_gate.clear_repair();
1537            Err(original_error)
1538        } else {
1539            self.mutation_gate.mark_needs_repair();
1540            Err(StarryError::BadState)
1541        }
1542    }
1543
1544    /// Installs the kernel root entries required by architectures that share a
1545    /// single hardware root between kernel and userspace.
1546    ///
1547    /// This intent-specific operation deliberately does not expose a mutable
1548    /// page-table reference to callers.
1549    ///
1550    /// # Safety
1551    ///
1552    /// `source` and every shared intermediate node must outlive this address
1553    /// space, and the capability's range must not overlap its user-owned range.
1554    #[cfg(not(any(target_arch = "aarch64", target_arch = "loongarch64")))]
1555    pub(crate) unsafe fn share_kernel_root_entries_from(
1556        &mut self,
1557        source: RootEntryShare<'_>,
1558    ) -> Result<(), PagingError> {
1559        // SAFETY: the caller provides the lifetime and non-overlap proof stated
1560        // by this method's contract.
1561        unsafe { source.install_into(&mut self.pt) }
1562    }
1563
1564    /// Returns the materialized hardware root for lifecycle publication.
1565    const fn materialized_root(&self) -> PhysAddr {
1566        self.pt.root_paddr()
1567    }
1568
1569    /// Checks if the address space contains the given address range.
1570    pub fn contains_range(&self, start: VirtAddr, size: usize) -> bool {
1571        let Some(range) = VirtAddrRange::try_from_start_size(start, size) else {
1572            return false;
1573        };
1574        range.start >= self.base() && range.end <= self.end()
1575    }
1576
1577    /// Creates a new empty address space.
1578    pub fn new_empty(base: VirtAddr, size: usize) -> StarryResult<Self> {
1579        Self::new_with_layout(UserVirtualAddressLayout::from_range(base, size)?)
1580    }
1581
1582    /// Creates a user MM from one already validated immutable layout.
1583    pub(crate) fn new_user(layout: UserVirtualAddressLayout) -> StarryResult<Self> {
1584        Self::new_with_layout(layout)
1585    }
1586
1587    fn new_with_layout(layout: UserVirtualAddressLayout) -> StarryResult<Self> {
1588        Ok(Self {
1589            id: AddressSpaceId::allocate(),
1590            layout,
1591            vma_root: Arc::new(VmaMap::default()),
1592            heap: HeapState::new(USER_HEAP_BASE),
1593            executable_data: ExecutableDataLayout::default(),
1594            executable_file: None,
1595            pt: PageTable::new(PagingAllocator).map_err(|_| StarryError::NoMemory)?,
1596            pte_domain: PageTableDomain::new(),
1597            mutation_gate: MutationGate::new(),
1598            published_epoch: Arc::new(core::sync::atomic::AtomicU64::new(0)),
1599            vm_stat: ProcessVmStat::new(),
1600            resident_pages: ResidentPageCounts::default(),
1601            resident_watermark: ResidentWatermark::new(),
1602            tlb_quarantine: TlbQuarantine::default(),
1603            tlb_targets: Arc::new(AtomicUsize::new(0)),
1604            mapping_slots: BTreeMap::new(),
1605            retired_mapping_batches: IrqMutex::new(Vec::new()),
1606        })
1607    }
1608
1609    /// Returns the stable identity used by scheduler activation and TLB
1610    /// obligations. It is independent of the page-table root address.
1611    pub const fn address_space_id(&self) -> AddressSpaceId {
1612        self.id
1613    }
1614
1615    /// Returns the current VMA/PTE publication epoch.
1616    pub fn vm_epoch(&self) -> VmEpoch {
1617        self.mutation_gate.current_epoch()
1618    }
1619
1620    pub(crate) fn tlb_targets(&self) -> Arc<AtomicUsize> {
1621        self.tlb_targets.clone()
1622    }
1623
1624    pub(crate) fn published_epoch_source(&self) -> Arc<core::sync::atomic::AtomicU64> {
1625        self.published_epoch.clone()
1626    }
1627
1628    fn publish_mutation_classified(
1629        &mut self,
1630        mutation: PreparedMutation,
1631    ) -> Result<MutationPublication, CommitMutationError> {
1632        let mut next_resident = self.resident_pages;
1633        next_resident
1634            .checked_apply(mutation.receipt().resident_delta)
1635            .map_err(CommitMutationError::Unpublished)?;
1636        match self.mutation_gate.commit(mutation) {
1637            Ok(receipt) => {
1638                self.resident_pages = next_resident;
1639                self.published_epoch.store(
1640                    receipt.new_epoch.get(),
1641                    core::sync::atomic::Ordering::Release,
1642                );
1643                self.resident_watermark
1644                    .observe_resident_total(self.resident_pages.total());
1645                Ok(MutationPublication::Complete)
1646            }
1647            Err(MutationError::TlbPending) => {
1648                self.resident_pages = next_resident;
1649                self.published_epoch.store(
1650                    self.mutation_gate.current_epoch().get(),
1651                    core::sync::atomic::Ordering::Release,
1652                );
1653                // The epoch and mapping graph are already published even
1654                // though old translations and owners remain quarantined.
1655                // Linux likewise records RSS high-water before reclaiming the
1656                // old page-table view; a TLB timeout must not hide the new RSS.
1657                self.resident_watermark
1658                    .observe_resident_total(self.resident_pages.total());
1659                Ok(MutationPublication::PendingTlb)
1660            }
1661            Err(error) => Err(CommitMutationError::Unpublished(
1662                Self::map_unpublished_mutation_error(error),
1663            )),
1664        }
1665    }
1666
1667    fn map_unpublished_mutation_error(error: MutationError) -> StarryError {
1668        match error {
1669            MutationError::ResourceExhausted => StarryError::NoMemory,
1670            MutationError::PendingTlbOverlap => StarryError::ResourceBusy,
1671            MutationError::NeedsRepair
1672            | MutationError::EpochExhausted
1673            | MutationError::EpochConflict
1674            | MutationError::WrongState
1675            | MutationError::ApplyFailed
1676            | MutationError::TlbPending => StarryError::BadState,
1677        }
1678    }
1679
1680    fn commit_mutation_classified(
1681        &mut self,
1682        mutation: PreparedMutation,
1683    ) -> Result<(), CommitMutationError> {
1684        match self.publish_mutation_classified(mutation)? {
1685            MutationPublication::Complete => Ok(()),
1686            MutationPublication::PendingTlb => {
1687                // Compatibility callers still use the synchronous service.
1688                // Faults use `publish_mutation_classified` directly, drop the
1689                // address-space mutex, and complete this platform operation
1690                // outside every VMA/PTE/rmap lock.
1691                self.service_pending_tlb()
1692                    .map(|_| ())
1693                    .map_err(CommitMutationError::PublishedPendingTlb)
1694            }
1695        }
1696    }
1697
1698    fn commit_mutation(&mut self, mutation: PreparedMutation) -> StarryResult {
1699        match self.commit_mutation_classified(mutation) {
1700            Ok(()) => Ok(()),
1701            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
1702            Err(CommitMutationError::Unpublished(error)) => {
1703                // Most compatibility callers have already applied a
1704                // backend/PTE delta and do not retain an inverse.  They must
1705                // remain quarantined.  Transactional callers such as fault
1706                // use `commit_mutation_classified` directly and restore their
1707                // preimage before returning.
1708                self.mutation_gate.mark_needs_repair();
1709                Err(error)
1710            }
1711        }
1712    }
1713
1714    /// Completes outstanding address-space-tagged TLB obligations.
1715    ///
1716    /// The request snapshot and active-CPU source can outlive the address-space
1717    /// mutex. Platform shootdown therefore runs without a VMA, PTE, rmap, or
1718    /// page-cache lock, after which the short acknowledgement phase retires the
1719    /// matching receipts and quarantined owners.
1720    pub fn service_pending_tlb(&self) -> StarryResult<usize> {
1721        let requests = self
1722            .mutation_gate
1723            .pending_requests()
1724            .map_err(|_| StarryError::NoMemory)?;
1725        Self::flush_tlb_requests(&requests, &self.tlb_targets)?;
1726        self.acknowledge_tlb_requests(&requests)
1727    }
1728
1729    fn flush_tlb_requests(requests: &[TlbRequest], tlb_targets: &AtomicUsize) -> StarryResult {
1730        for request in requests {
1731            let pending_targets = request.pending();
1732            // `tlb_targets` is the full-flush fallback's equivalent of
1733            // Linux's loaded-mm footprint. A bit can only be cleared after
1734            // another root is installed and the local TLB is flushed.
1735            let live_targets =
1736                pending_targets & tlb_targets.load(core::sync::atomic::Ordering::Acquire);
1737            if request.ranges.is_empty() && live_targets != 0 {
1738                ax_runtime::hal::cache::flush_tlb_all_on_cpus(live_targets)
1739                    .map_err(Self::map_tlb_shootdown_error)?;
1740            } else if live_targets != 0 {
1741                for range in &request.ranges {
1742                    ax_runtime::hal::cache::flush_tlb_range_on_cpus(
1743                        live_targets,
1744                        range.start,
1745                        range.size,
1746                    )
1747                    .map_err(Self::map_tlb_shootdown_error)?;
1748                }
1749            }
1750        }
1751        Ok(())
1752    }
1753
1754    fn map_tlb_shootdown_error(error: ax_runtime::hal::cache::TlbShootdownError) -> StarryError {
1755        match error {
1756            ax_runtime::hal::cache::TlbShootdownError::Timeout => StarryError::TimedOut,
1757            ax_runtime::hal::cache::TlbShootdownError::Unsupported
1758            | ax_runtime::hal::cache::TlbShootdownError::CpuOffline => StarryError::Unsupported,
1759            ax_runtime::hal::cache::TlbShootdownError::GenerationExhausted => {
1760                StarryError::Errno(syscalls::Errno::EOVERFLOW)
1761            }
1762            ax_runtime::hal::cache::TlbShootdownError::Platform => StarryError::Io,
1763        }
1764    }
1765
1766    fn acknowledge_tlb_requests(&self, requests: &[TlbRequest]) -> StarryResult<usize> {
1767        let mut completed = 0;
1768        for request in requests {
1769            let pending_targets = request.pending();
1770            // Every target now has either a shootdown proof or a root-switch
1771            // proof. A later activation installs the published epoch and
1772            // performs its required local tag/full flush before use.
1773            for cpu in 0..usize::BITS as usize {
1774                if pending_targets & (1usize << cpu) == 0 {
1775                    continue;
1776                }
1777                match self.mutation_gate.acknowledge(self.id, request.epoch, cpu) {
1778                    Ok(_) | Err(MutationError::WrongState) | Err(MutationError::TlbPending) => {}
1779                    Err(_) => return Err(StarryError::BadState),
1780                }
1781                self.tlb_quarantine
1782                    .acknowledge(self.id, request.epoch, cpu)
1783                    .map_err(|_| StarryError::NoMemory)?;
1784            }
1785            if self
1786                .mutation_gate
1787                .pending_request(self.id, request.epoch)
1788                .is_none()
1789            {
1790                self.release_retired_mapping_owners(request.epoch);
1791            }
1792            completed += 1;
1793        }
1794        Ok(completed)
1795    }
1796
1797    fn prepare_mutation(&self) -> PreparedMutation {
1798        self.mutation_gate
1799            .begin_with_active_targets(self.id, self.tlb_targets.clone())
1800    }
1801
1802    /// Begins a metadata-only publication that cannot leave stale hardware
1803    /// translations.  Unlike a PTE mutation, it deliberately captures no
1804    /// active CPU targets and therefore cannot manufacture a full-flush
1805    /// obligation for a VMA advice-bit change.
1806    fn prepare_metadata_mutation(&self) -> PreparedMutation {
1807        self.mutation_gate.begin(self.id, 0)
1808    }
1809
1810    fn prepare_mutation_range(&self, start: VirtAddr, size: usize) -> PreparedMutation {
1811        let mut mutation = self.prepare_mutation();
1812        if let Some(range) = TlbRange::new(start, size) {
1813            mutation.add_tlb_range(range);
1814        }
1815        mutation
1816    }
1817
1818    /// Begins a fresh-PTE publication that cannot invalidate an older present
1819    /// translation. Linux installs a previously-none fault PTE under the PTL
1820    /// and returns through `update_mmu_cache()` without a remote shootdown;
1821    /// only permission changes and page replacement require an active-CPU TLB
1822    /// obligation. The range remains in the receipt for auditability.
1823    fn prepare_fresh_pte_mutation_range(&self, start: VirtAddr, size: usize) -> PreparedMutation {
1824        prepare_mapping_publication_mutation(
1825            &self.mutation_gate,
1826            self.id,
1827            &self.tlb_targets,
1828            start,
1829            size,
1830            false,
1831        )
1832    }
1833
1834    /// Reserves and captures every owner that may become unreachable when a
1835    /// materialized mapping in `range` is detached.  This runs before the PTE
1836    /// mutation, so allocation or cache-identity failure leaves the published
1837    /// state untouched.
1838    /// Records one VMA's backend, and the file-cache pins its shared lease
1839    /// keeps alive, into the retired-mapping owners.
1840    fn collect_retired_mapping_owner(
1841        &self,
1842        entry: &Arc<VmaEntry>,
1843        range: VirtAddrRange,
1844        owners: &mut RetiredMappingOwners,
1845    ) -> StarryResult {
1846        owners
1847            .backends
1848            .try_reserve(1)
1849            .map_err(|_| StarryError::NoMemory)?;
1850        let backend = entry.operation_clone();
1851
1852        if backend.shared_file_lease().is_some() {
1853            let start = entry.start().max(range.start).align_down_4k();
1854            let end = entry.end().min(range.end);
1855            let file_range = VirtAddrRange::new(start, end);
1856            let count = self.mapping_slots_overlapping(file_range).count();
1857            owners
1858                .cache_pins
1859                .try_reserve(count)
1860                .map_err(|_| StarryError::NoMemory)?;
1861            for (_, slot) in self.mapping_slots_overlapping(file_range) {
1862                if slot.state() != SlotState::Present
1863                    || slot.mapping != backend.mapping_id()
1864                    || slot.page_order != PageOrder::BASE
1865                {
1866                    return Err(StarryError::BadState);
1867                }
1868                let paddr = slot.mapped_paddr().ok_or(StarryError::BadState)?;
1869                let (installed, _, page_size) = self.pt.query(slot.va)?;
1870                if installed != paddr || page_size != PAGE_SIZE_4K {
1871                    return Err(StarryError::BadState);
1872                }
1873                let pin = backend
1874                    .pin_file_cache_owner_for_mapping(slot.va, paddr)?
1875                    .ok_or(StarryError::BadState)?;
1876                owners.cache_pins.push(pin);
1877            }
1878        }
1879        owners.backends.push(backend);
1880        Ok(())
1881    }
1882
1883    fn prepare_retired_mapping_owners(
1884        &self,
1885        range: VirtAddrRange,
1886    ) -> StarryResult<RetiredMappingOwners> {
1887        try_reserve_irq_vec(&self.retired_mapping_batches, 1).map_err(|_| StarryError::NoMemory)?;
1888
1889        let mut owners = RetiredMappingOwners::default();
1890        // Only the VMAs the range covers matter here. Breaking out of a
1891        // full-map iteration saved nothing: `iter_entries` builds the entire
1892        // vector before the loop reads its first entry.
1893        let mut failure = None;
1894        self.vma_root.for_each_overlapping_entry(range, |entry| {
1895            match self.collect_retired_mapping_owner(entry, range, &mut owners) {
1896                Ok(()) => true,
1897                Err(err) => {
1898                    failure = Some(err);
1899                    false
1900                }
1901            }
1902        });
1903        if let Some(err) = failure {
1904            return Err(err);
1905        }
1906
1907        let matching_slots = self.mapping_slots_overlapping(range).count();
1908        owners
1909            .pages
1910            .try_reserve(matching_slots)
1911            .map_err(|_| StarryError::NoMemory)?;
1912        for (_, slot) in self.mapping_slots_overlapping(range) {
1913            if slot.state() != SlotState::Present {
1914                return Err(StarryError::BadState);
1915            }
1916            owners.pages.push(slot.page.clone());
1917        }
1918        Ok(owners)
1919    }
1920
1921    /// Reserves the post-publication owner used by rmap-driven file eviction.
1922    /// Both allocations happen before the PTE is detached, so an allocation
1923    /// failure is an ordinary retry with no visible state change.
1924    fn prepare_deferred_eviction_owner(
1925        &self,
1926        page: &Arc<PageObject>,
1927    ) -> StarryResult<RetiredMappingOwners> {
1928        try_reserve_irq_vec(&self.retired_mapping_batches, 1).map_err(|_| StarryError::NoMemory)?;
1929        let mut owners = RetiredMappingOwners::default();
1930        owners
1931            .deferred_evictions
1932            .try_reserve(1)
1933            .map_err(|_| StarryError::NoMemory)?;
1934        owners.deferred_evictions.push(page.clone());
1935        Ok(owners)
1936    }
1937
1938    fn park_retired_mapping_owners(&self, epoch: VmEpoch, owners: RetiredMappingOwners) {
1939        if owners.is_empty() {
1940            return;
1941        }
1942        // Capacity was reserved by `prepare_retired_mapping_owners` before
1943        // any PTE changed, so this publication cannot allocate or fail.
1944        self.retired_mapping_batches
1945            .lock()
1946            .push(RetiredMappingBatch { epoch, owners });
1947    }
1948
1949    fn release_retired_mapping_owners(&self, epoch: VmEpoch) {
1950        loop {
1951            let batch = {
1952                let mut batches = self.retired_mapping_batches.lock();
1953                batches
1954                    .iter()
1955                    .position(|batch| batch.epoch == epoch)
1956                    .map(|index| batches.swap_remove(index))
1957            };
1958            let Some(batch) = batch else {
1959                break;
1960            };
1961            debug_assert!(!batch.owners.is_empty());
1962            for page in &batch.owners.deferred_evictions {
1963                page.complete_eviction_tlb();
1964            }
1965            // Cache pins may take the page-cache lock in Drop.  Release them
1966            // only after the IRQ-safe batch lock is gone.
1967            drop(batch);
1968        }
1969    }
1970
1971    fn pending_retired_mapping_batches(&self) -> usize {
1972        self.retired_mapping_batches.lock().len()
1973    }
1974
1975    /// Number of mutations whose remote TLB obligations are not complete.
1976    pub fn pending_tlb_obligations(&self) -> usize {
1977        self.mutation_gate.pending_count()
1978    }
1979
1980    /// Snapshot of outstanding shootdown requests.  The caller may submit
1981    /// these to the platform IPI layer without retaining an address-space
1982    /// metadata lock.
1983    pub fn pending_tlb_requests(&self) -> StarryResult<Vec<TlbRequest>> {
1984        self.mutation_gate
1985            .pending_requests()
1986            .map_err(|_| StarryError::NoMemory)
1987    }
1988
1989    /// Records one remote acknowledgement and returns frames that became safe
1990    /// to reclaim from the quarantine.
1991    pub fn acknowledge_tlb(
1992        &self,
1993        space_id: AddressSpaceId,
1994        epoch: VmEpoch,
1995        cpu: usize,
1996    ) -> StarryResult<Vec<FrameLease>> {
1997        match self.mutation_gate.acknowledge(space_id, epoch, cpu) {
1998            Ok(_) | Err(MutationError::TlbPending) => {}
1999            Err(MutationError::WrongState)
2000                if self.tlb_quarantine.contains_request(space_id, epoch) =>
2001            {
2002                // The last gate acknowledgement may have removed the receipt
2003                // just before this caller drained the frame quarantine.  The
2004                // frame-level obligation is still authoritative, so accept
2005                // this idempotent late acknowledgement.
2006            }
2007            Err(_) => return Err(StarryError::ResourceBusy),
2008        }
2009        let released = self
2010            .tlb_quarantine
2011            .acknowledge(space_id, epoch, cpu)
2012            .map_err(|_| StarryError::NoMemory)?;
2013        if self
2014            .mutation_gate
2015            .pending_request(space_id, epoch)
2016            .is_none()
2017        {
2018            self.release_retired_mapping_owners(epoch);
2019        }
2020        Ok(released)
2021    }
2022
2023    pub fn quarantine_frame(
2024        &self,
2025        frame: FrameLease,
2026        request: TlbRequest,
2027    ) -> Result<(), QuarantineFailure> {
2028        self.tlb_quarantine.try_defer(frame, request)
2029    }
2030
2031    fn validate_region(&self, start: VirtAddr, size: usize) -> StarryResult {
2032        if self.mutation_gate.needs_repair() {
2033            return Err(StarryError::BadState);
2034        }
2035        if size == 0 || !self.contains_range(start, size) {
2036            return Err(StarryError::NoMemory);
2037        }
2038        if !start.is_aligned_4k() || !is_aligned_4k(size) {
2039            return Err(StarryError::InvalidInput);
2040        }
2041        Ok(())
2042    }
2043
2044    /// Retains the exact materialized leaves and their frame owners before a
2045    /// mapping replacement starts.  The immutable VMA root is deliberately
2046    /// not changed here; it remains the publication preimage until commit.
2047    fn capture_mapping_preimage(&self, range: VirtAddrRange) -> StarryResult<MappingPreimage> {
2048        self.capture_mapping_preimage_ranges(&[range])
2049    }
2050
2051    /// Retains one coherent metadata and resident-leaf preimage for several
2052    /// disjoint ranges.  `mremap` uses this before publishing its destination
2053    /// so a later PTE or metadata failure can restore both the source and a
2054    /// replaced fixed target, rather than merely deleting the new target.
2055    fn capture_mapping_preimage_ranges(
2056        &self,
2057        ranges: &[VirtAddrRange],
2058    ) -> StarryResult<MappingPreimage> {
2059        for (index, range) in ranges.iter().enumerate() {
2060            self.validate_region(range.start, range.size())?;
2061            if ranges[..index]
2062                .iter()
2063                .any(|previous| previous.overlaps(*range))
2064            {
2065                return Err(StarryError::InvalidInput);
2066            }
2067        }
2068        let resident_slots = self.materialized_slots_overlapping(ranges)?;
2069        let mut leaves = Vec::new();
2070        leaves
2071            .try_reserve(resident_slots.len())
2072            .map_err(|_| StarryError::NoMemory)?;
2073        for (key, slot, occupied_leaf) in resident_slots {
2074            let page_size = occupied_leaf.range.size();
2075            let end = slot
2076                .va
2077                .checked_add(page_size)
2078                .ok_or(StarryError::BadState)?;
2079            let range = ranges
2080                .iter()
2081                .find(|range| slot.overlaps(**range))
2082                .ok_or(StarryError::BadState)?;
2083            // A partial huge-leaf replacement needs the THP split receipt.
2084            // Reject it before mutation until that typed split path is active;
2085            // restoring only part of a block descriptor would be unsound.
2086            if slot.va < range.start || end > range.end {
2087                return Err(StarryError::OperationNotSupported);
2088            }
2089            let paddr = occupied_leaf.paddr;
2090            let backend = self
2091                .vma_root
2092                .lookup_entry(slot.va)
2093                .map(|entry| entry.operation_clone())
2094                .ok_or(StarryError::BadState)?;
2095            let page = slot.page.clone();
2096            let frame_start = page.frame().paddr().as_usize();
2097            let frame_end = frame_start
2098                .checked_add(page.frame().size())
2099                .ok_or(StarryError::BadState)?;
2100            let leaf_start = paddr.as_usize();
2101            let leaf_end = leaf_start
2102                .checked_add(page_size)
2103                .ok_or(StarryError::BadState)?;
2104            if key.va != slot.va
2105                || slot.state() != SlotState::Present
2106                || slot.mm_id != self.id
2107                || slot.mapping != backend.mapping_id()
2108                || slot.mapped_paddr() != Some(paddr)
2109                || leaf_start < frame_start
2110                || leaf_end > frame_end
2111            {
2112                return Err(StarryError::BadState);
2113            }
2114            leaves.push(ResidentLeafPreimage {
2115                va: slot.va,
2116                paddr,
2117                page_size,
2118                flags: occupied_leaf.flags,
2119                backend,
2120                page,
2121                slot,
2122            });
2123        }
2124        Ok(MappingPreimage {
2125            vma_root: self.vma_root.clone(),
2126            vm_stat: self.vm_stat.snapshot(),
2127            leaves,
2128        })
2129    }
2130
2131    /// Reverts a not-yet-published mapping using the retained materialized
2132    /// preimage.  Returning an error means the caller must enter NeedsRepair;
2133    /// it must never report the original syscall error as if state were old.
2134    fn restore_mapping_preimage(
2135        &mut self,
2136        range: VirtAddrRange,
2137        preimage: MappingPreimage,
2138    ) -> StarryResult {
2139        self.restore_mapping_preimage_ranges(&[range], preimage)
2140    }
2141
2142    /// Aborts an unpublished mapping mutation by restoring its exact software
2143    /// and materialized-page-table preimage.  `MemorySet` deliberately reports
2144    /// `NeedsRepair` after a partial PTE apply because it cannot prove its own
2145    /// metadata-only rollback repaired the page table.  At this layer we own
2146    /// the retained frame/backend references and can make that proof; only a
2147    /// complete restore is allowed to clear the repair latch and return the
2148    /// original syscall error.
2149    fn abort_unpublished_mapping_mutation(
2150        &mut self,
2151        range: VirtAddrRange,
2152        preimage: MappingPreimage,
2153        original_error: StarryError,
2154    ) -> StarryResult {
2155        self.abort_unpublished_parked_mapping_mutation(range, preimage, None, original_error)
2156    }
2157
2158    /// Restores a file-eviction delta that failed before epoch publication.
2159    /// The caller may cancel its EvictionLease only when this returns `Err`:
2160    /// `NeedsRepair` means restoration could not be proved and deliberately
2161    /// leaves the page pinned in Evicting.
2162    fn abort_file_eviction_mutation(
2163        &mut self,
2164        range: VirtAddrRange,
2165        preimage: MappingPreimage,
2166        parked_epoch: Option<VmEpoch>,
2167        original_error: StarryError,
2168    ) -> Result<EvictMappingOutcome, StarryError> {
2169        match self.restore_mapping_preimage(range, preimage) {
2170            Ok(()) => {
2171                if let Some(epoch) = parked_epoch {
2172                    self.release_retired_mapping_owners(epoch);
2173                }
2174                self.mutation_gate.clear_repair();
2175                Err(original_error)
2176            }
2177            Err(_) => {
2178                self.mutation_gate.mark_needs_repair();
2179                Ok(EvictMappingOutcome::NeedsRepair)
2180            }
2181        }
2182    }
2183
2184    /// Variant of [`Self::abort_unpublished_mapping_mutation`] for an unmap
2185    /// or replacement whose detached owners were already parked for the next
2186    /// epoch.  The batch is released only after the old PTE/rmap graph has
2187    /// been proved restored; an indeterminate restore deliberately keeps the
2188    /// extra owners alive for a repair worker.
2189    fn abort_unpublished_parked_mapping_mutation(
2190        &mut self,
2191        range: VirtAddrRange,
2192        preimage: MappingPreimage,
2193        parked_epoch: Option<VmEpoch>,
2194        original_error: StarryError,
2195    ) -> StarryResult {
2196        self.abort_unpublished_parked_mapping_mutation_ranges(
2197            &[range],
2198            preimage,
2199            parked_epoch,
2200            original_error,
2201        )
2202    }
2203
2204    fn abort_unpublished_parked_mapping_mutation_ranges(
2205        &mut self,
2206        ranges: &[VirtAddrRange],
2207        preimage: MappingPreimage,
2208        parked_epoch: Option<VmEpoch>,
2209        original_error: StarryError,
2210    ) -> StarryResult {
2211        match self.restore_mapping_preimage_ranges(ranges, preimage) {
2212            Ok(()) => {
2213                if let Some(epoch) = parked_epoch {
2214                    self.release_retired_mapping_owners(epoch);
2215                }
2216                self.mutation_gate.clear_repair();
2217                Err(original_error)
2218            }
2219            Err(_) => {
2220                self.mutation_gate.mark_needs_repair();
2221                Err(StarryError::BadState)
2222            }
2223        }
2224    }
2225
2226    /// Aborts an unpublished mapping mutation that first materialized one or
2227    /// two THP boundary splits.  The exact leaf/slot preimage is restored
2228    /// before collapsing the deposited child table back into the original
2229    /// huge descriptor; only then may parked owners be released.
2230    fn abort_unpublished_split_mapping_mutation(
2231        &mut self,
2232        range: VirtAddrRange,
2233        preimage: MappingPreimage,
2234        parked_epoch: Option<VmEpoch>,
2235        splits: Vec<AppliedHugeSplit>,
2236        original_error: StarryError,
2237    ) -> StarryResult {
2238        self.abort_unpublished_split_mapping_mutation_ranges(
2239            &[range],
2240            preimage,
2241            parked_epoch,
2242            splits,
2243            original_error,
2244        )
2245    }
2246
2247    /// Multi-range form used by mremap.  Resident source/target preimages are
2248    /// restored before deposited child tables are collapsed, matching the
2249    /// inverse of prepare/apply ordering.
2250    fn abort_unpublished_split_mapping_mutation_ranges(
2251        &mut self,
2252        ranges: &[VirtAddrRange],
2253        preimage: MappingPreimage,
2254        parked_epoch: Option<VmEpoch>,
2255        splits: Vec<AppliedHugeSplit>,
2256        original_error: StarryError,
2257    ) -> StarryResult {
2258        if self
2259            .restore_mapping_preimage_ranges(ranges, preimage)
2260            .is_ok()
2261            && self.rollback_applied_huge_splits(splits)
2262        {
2263            if let Some(epoch) = parked_epoch {
2264                self.release_retired_mapping_owners(epoch);
2265            }
2266            self.mutation_gate.clear_repair();
2267            Err(original_error)
2268        } else {
2269            self.mutation_gate.mark_needs_repair();
2270            Err(StarryError::BadState)
2271        }
2272    }
2273
2274    /// Reverts a prepared boundary split when no mapping leaf has otherwise
2275    /// changed.  This is used for allocation/capture failures between split
2276    /// apply and the first backend unmap.
2277    fn abort_unpublished_huge_splits(
2278        &mut self,
2279        splits: Vec<AppliedHugeSplit>,
2280        original_error: StarryError,
2281    ) -> StarryResult {
2282        if self.rollback_applied_huge_splits(splits) {
2283            self.mutation_gate.clear_repair();
2284            Err(original_error)
2285        } else {
2286            self.mutation_gate.mark_needs_repair();
2287            Err(StarryError::BadState)
2288        }
2289    }
2290
2291    fn restore_mapping_preimage_ranges(
2292        &mut self,
2293        ranges: &[VirtAddrRange],
2294        preimage: MappingPreimage,
2295    ) -> StarryResult {
2296        let mut current_memfds = Vec::new();
2297        for range in ranges {
2298            current_memfds.extend(crate::syscall::memfd_collect_metas_touching_mprotect_range(
2299                self,
2300                range.start,
2301                range.size(),
2302            ));
2303        }
2304        let MappingPreimage {
2305            vma_root,
2306            vm_stat,
2307            leaves,
2308        } = preimage;
2309        if self.detach_current_materialized_ranges(ranges).is_err() {
2310            self.mutation_gate.mark_needs_repair();
2311            return Err(StarryError::BadState);
2312        }
2313        for range in ranges {
2314            self.detach_mapping_slots(*range)?;
2315        }
2316        self.vma_root = vma_root.clone();
2317        for leaf in leaves {
2318            if leaf
2319                .backend
2320                .restore_resident_preimage(
2321                    ResidentLeafRestore {
2322                        va: leaf.va,
2323                        paddr: leaf.paddr,
2324                        page_size: leaf.page_size,
2325                        flags: leaf.flags,
2326                        page: Some(&leaf.page),
2327                    },
2328                    &mut self.pt,
2329                )
2330                .is_err()
2331            {
2332                self.mutation_gate.mark_needs_repair();
2333                return Err(StarryError::BadState);
2334            }
2335            let key = MappingSlotKey {
2336                space_id: self.id,
2337                va: leaf.va,
2338            };
2339            if self.mapping_slots.contains_key(&key)
2340                || !leaf.slot.restore()
2341                || self.mapping_slots.insert(key, leaf.slot).is_some()
2342            {
2343                self.mutation_gate.mark_needs_repair();
2344                return Err(StarryError::BadState);
2345            }
2346            leaf.page.set_resident_kind(
2347                self.mapping_slots
2348                    .get(&key)
2349                    .and_then(|slot| slot.resident_kind()),
2350            );
2351        }
2352        self.vma_root = vma_root;
2353        self.vm_stat.restore(vm_stat);
2354        crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(self, &current_memfds);
2355        for range in ranges {
2356            let restored_memfds = crate::syscall::memfd_collect_metas_touching_mprotect_range(
2357                self,
2358                range.start,
2359                range.size(),
2360            );
2361            crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(
2362                self,
2363                &restored_memfds,
2364            );
2365        }
2366        Ok(())
2367    }
2368
2369    /// Finds a free area that can accommodate the given size.
2370    ///
2371    /// The search starts from the given hint address, and the area should be
2372    /// within the given limit range.
2373    ///
2374    /// Returns the start address of the free area. Returns None if no such area
2375    /// is found.
2376    pub fn find_free_area(
2377        &self,
2378        hint: VirtAddr,
2379        size: usize,
2380        limit: VirtAddrRange,
2381        align: usize,
2382    ) -> Option<VirtAddr> {
2383        self.vma_root.find_free_area(hint, size, limit, align)
2384    }
2385
2386    /// Returns immutable VMA metadata that can cross a lock or I/O boundary.
2387    pub fn find_area_snapshot(&self, vaddr: VirtAddr) -> Option<Arc<VmaSnapshot>> {
2388        self.vma_root.lookup(vaddr)
2389    }
2390
2391    /// Publishes an immutable VMA index snapshot for procfs and fault readers.
2392    pub fn vma_map_snapshot(&self) -> Arc<VmaMap> {
2393        self.vma_root.clone()
2394    }
2395
2396    /// Returns owned VMA snapshots intersecting a checked range.
2397    pub fn vma_snapshots_in_range(
2398        &self,
2399        start: VirtAddr,
2400        size: usize,
2401    ) -> StarryResult<Vec<Arc<VmaSnapshot>>> {
2402        let range =
2403            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
2404        if range.is_empty() {
2405            return Ok(Vec::new());
2406        }
2407        Ok(self.vma_root.lookup_range(range))
2408    }
2409
2410    pub(crate) fn vma_inspection_records(&self) -> StarryResult<Vec<VmaInspectionRecord>> {
2411        let mut records = Vec::new();
2412        records
2413            .try_reserve(self.vma_root.len())
2414            .map_err(|_| StarryError::NoMemory)?;
2415        for entry in self.vma_root.iter_entries() {
2416            records.push(entry.inspection_record()?);
2417        }
2418        Ok(records)
2419    }
2420
2421    pub(crate) fn max_mapped_end(&self) -> Option<VirtAddr> {
2422        self.vma_root.iter().last().map(|vma| vma.range.end)
2423    }
2424
2425    pub(crate) fn next_advice_fragment(
2426        &self,
2427        cursor: VirtAddr,
2428        end: VirtAddr,
2429    ) -> Option<VmaAdviceFragment> {
2430        self.vma_root
2431            .iter_entries()
2432            .find(|entry| entry.end() > cursor && entry.start() < end)
2433            .and_then(|entry| entry.advice_fragment(cursor, end))
2434    }
2435
2436    pub(crate) fn validate_mprotect_mapping_capabilities(
2437        &self,
2438        start: VirtAddr,
2439        size: usize,
2440        flags: MappingFlags,
2441    ) -> StarryResult<Vec<SharedFileMappingLease>> {
2442        let range =
2443            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
2444        let operations = self.mapping_operation_fragments(range, true)?;
2445        let mut files = Vec::new();
2446        files
2447            .try_reserve(operations.len())
2448            .map_err(|_| StarryError::NoMemory)?;
2449        for (_, operation) in operations {
2450            operation.check_mprotect_flags(flags)?;
2451            if let Some(file) = operation.shared_file_lease() {
2452                files.push(file);
2453            }
2454        }
2455        Ok(files)
2456    }
2457
2458    pub(crate) fn shared_futex_identity(&self, address: VirtAddr) -> Option<SharedFutexIdentity> {
2459        self.vma_root
2460            .lookup_entry(address)
2461            .and_then(|entry| entry.operation().shared_futex_identity(address))
2462    }
2463
2464    pub(crate) fn mincore_probe(&self, address: VirtAddr) -> Option<VmaResidencyProbe> {
2465        self.vma_root
2466            .lookup_entry(address)
2467            .map(|entry| entry.residency_probe())
2468    }
2469
2470    pub(crate) fn mremap_source(&self, address: VirtAddr) -> Option<VmaMremapSource> {
2471        self.vma_root
2472            .lookup_entry(address)
2473            .map(|entry| entry.mremap_source())
2474    }
2475
2476    pub(crate) fn shared_file_vma_at(&self, address: VirtAddr) -> Option<SharedFileVmaRecord> {
2477        self.vma_root
2478            .lookup_entry(address)
2479            .and_then(|entry| entry.shared_file_record())
2480    }
2481
2482    pub(crate) fn shared_file_vmas(&self) -> Vec<SharedFileVmaRecord> {
2483        self.vma_root
2484            .iter_entries()
2485            .filter_map(|entry| entry.shared_file_record())
2486            .collect()
2487    }
2488
2489    #[allow(clippy::too_many_arguments)]
2490    pub(crate) fn mremap_move_from_source(
2491        &mut self,
2492        source: &VmaMremapSource,
2493        src: VirtAddr,
2494        src_size: usize,
2495        target: VirtAddr,
2496        target_size: usize,
2497        huge_page_advice: HugePageAdvice,
2498        dontunmap: bool,
2499        source_offset: usize,
2500        replace_target: bool,
2501        memlock_limit: Option<MemlockLimit>,
2502    ) -> StarryResult {
2503        let operation = source.relocated_operation(target, source_offset, target_size)?;
2504        self.mremap_move_transaction(
2505            src,
2506            src_size,
2507            target,
2508            target_size,
2509            MappingPermissions {
2510                current: source.rights(),
2511                reported: source.reported_rights(),
2512                maximum: source.max_rights(),
2513            },
2514            operation,
2515            huge_page_advice,
2516            source.lock_mode(),
2517            source.advice_policy(),
2518            dontunmap,
2519            replace_target,
2520            memlock_limit,
2521        )
2522    }
2523
2524    pub(crate) fn duplicate_shared_mremap_source(
2525        &mut self,
2526        source: &VmaMremapSource,
2527        target: VirtAddr,
2528        target_size: usize,
2529        source_offset: usize,
2530        replace_target: bool,
2531        memlock_limit: Option<MemlockLimit>,
2532    ) -> StarryResult {
2533        let object = source.shared_object().ok_or(StarryError::InvalidInput)?;
2534        let backend_start = target
2535            .as_usize()
2536            .checked_sub(source_offset)
2537            .map(VirtAddr::from_usize)
2538            .ok_or(StarryError::InvalidInput)?;
2539        self.map_mremap_duplicate(
2540            target,
2541            target_size,
2542            MappingPermissions {
2543                current: source.rights(),
2544                reported: source.reported_rights(),
2545                maximum: source.rights(),
2546            },
2547            MappingOperation::new_shared(backend_start, object),
2548            MappingPublication::mremap(
2549                replace_target,
2550                source.huge_page_advice(),
2551                source.lock_mode(),
2552                source.advice_policy(),
2553                memlock_limit,
2554            ),
2555        )
2556    }
2557
2558    fn validate_memlock_successor(
2559        &self,
2560        successor: &VmaMap,
2561        memlock_limit: Option<MemlockLimit>,
2562    ) -> StarryResult {
2563        let previous_locked = self.vma_root.locked_pages().ok_or(StarryError::BadState)?;
2564        let successor_locked = successor.locked_pages().ok_or(StarryError::BadState)?;
2565        if successor_locked <= previous_locked {
2566            return Ok(());
2567        }
2568        memlock_limit
2569            .ok_or(StarryError::BadState)?
2570            .validate(successor_locked)
2571    }
2572
2573    fn publish_vma_metadata_successor(
2574        &mut self,
2575        previous_root: Arc<VmaMap>,
2576        successor: VmaMap,
2577        operation: &'static str,
2578    ) -> StarryResult {
2579        let before_vmas = previous_root.len();
2580        let after_vmas = successor.len();
2581        let mut mutation = self.prepare_metadata_mutation();
2582        mutation.set_vma_delta(VmaDelta {
2583            split: u32::try_from(after_vmas.saturating_sub(before_vmas)).unwrap_or(u32::MAX),
2584            merged: u32::try_from(before_vmas.saturating_sub(after_vmas)).unwrap_or(u32::MAX),
2585            ..VmaDelta::default()
2586        });
2587        self.vma_root = Arc::new(successor);
2588        match self.commit_mutation_classified(mutation) {
2589            Ok(()) => Ok(()),
2590            Err(CommitMutationError::Unpublished(error)) => {
2591                self.vma_root = previous_root;
2592                Err(error)
2593            }
2594            Err(CommitMutationError::PublishedPendingTlb(error)) => {
2595                // Metadata-only receipts have an empty target mask. Reaching
2596                // this branch is an invariant failure, not a recoverable TLB
2597                // timeout that a syscall may report as partially committed.
2598                self.mutation_gate.mark_needs_repair();
2599                warn!(
2600                    "metadata-only {operation} unexpectedly required TLB acknowledgement: {error}"
2601                );
2602                Err(StarryError::BadState)
2603            }
2604        }
2605    }
2606
2607    /// Publishes Linux-compatible transparent-huge-page advice for a mapped
2608    /// VMA range without changing any materialized PTE.
2609    ///
2610    /// The immutable root is the sole owner of this per-VMA policy.  A failed
2611    /// metadata commit restores the previous root; because no translation is
2612    /// changed, the receipt carries no TLB target or retirement obligation.
2613    pub fn advise_huge_pages(
2614        &mut self,
2615        start: VirtAddr,
2616        size: usize,
2617        advice: HugePageAdvice,
2618    ) -> StarryResult {
2619        self.validate_region(start, size)?;
2620        let range =
2621            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
2622        let previous_root = self.vma_root.clone();
2623        let affected = previous_root.lookup_range(range);
2624        if affected.is_empty() || !previous_root.contains_range(start, size) {
2625            return Err(StarryError::NoMemory);
2626        }
2627        if affected.iter().all(|vma| vma.huge_page_advice == advice) {
2628            return Ok(());
2629        }
2630
2631        let successor = previous_root
2632            .with_huge_page_advice(range, advice)
2633            .ok_or(StarryError::NoMemory)?;
2634        self.publish_vma_metadata_successor(previous_root, successor, "VMA THP advice")
2635    }
2636
2637    /// Publishes one Linux access, fork-inheritance or dump policy update.
2638    /// The immutable VMA root is both the current state and rollback preimage.
2639    pub(crate) fn advise_vma_policy(
2640        &mut self,
2641        start: VirtAddr,
2642        size: usize,
2643        update: VmaAdviceUpdate,
2644    ) -> StarryResult {
2645        self.validate_region(start, size)?;
2646        let range =
2647            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
2648        let previous_root = self.vma_root.clone();
2649        let affected = previous_root.lookup_range(range);
2650        if affected.is_empty() || !previous_root.contains_range(start, size) {
2651            return Err(StarryError::NoMemory);
2652        }
2653        if affected
2654            .iter()
2655            .all(|vma| vma.advice_policy.apply(update) == vma.advice_policy)
2656        {
2657            return Ok(());
2658        }
2659        let successor = previous_root
2660            .with_advice_update(range, update)
2661            .ok_or(StarryError::NoMemory)?;
2662        self.publish_vma_metadata_successor(previous_root, successor, "VMA madvise policy")
2663    }
2664
2665    /// Publishes Linux `VM_LOCKED`/`VM_LOCKONFAULT` policy for a fully mapped
2666    /// range. The policy is part of the immutable VMA root, so split, merge,
2667    /// `mprotect`, `mremap`, `msync`, and proc readers observe one coherent
2668    /// fact. Page population is deliberately a separate operation: Linux
2669    /// publishes the lock policy before `__mm_populate`, and a later populate
2670    /// failure does not undo the VMA flags.
2671    fn set_vma_lock_mode(
2672        &mut self,
2673        start: VirtAddr,
2674        size: usize,
2675        lock_mode: VmaLockMode,
2676        memlock_limit: Option<MemlockLimit>,
2677    ) -> StarryResult {
2678        self.validate_region(start, size)?;
2679        let range =
2680            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
2681        let previous_root = self.vma_root.clone();
2682        let affected = previous_root.lookup_range(range);
2683        if affected.is_empty() || !previous_root.contains_range(start, size) {
2684            return Err(StarryError::NoMemory);
2685        }
2686        if affected.iter().all(|vma| vma.lock_mode == lock_mode) {
2687            return Ok(());
2688        }
2689
2690        let successor = previous_root
2691            .with_lock_mode(range, lock_mode)
2692            .ok_or(StarryError::NoMemory)?;
2693        self.validate_memlock_successor(&successor, memlock_limit)?;
2694        self.publish_vma_metadata_successor(previous_root, successor, "VMA lock update")
2695    }
2696
2697    pub(crate) fn lock_vma_range(
2698        &mut self,
2699        start: VirtAddr,
2700        size: usize,
2701        lock_mode: VmaLockMode,
2702        memlock_limit: MemlockLimit,
2703    ) -> StarryResult {
2704        if !lock_mode.is_locked() {
2705            return Err(StarryError::InvalidInput);
2706        }
2707        self.set_vma_lock_mode(start, size, lock_mode, Some(memlock_limit))
2708    }
2709
2710    pub(crate) fn unlock_vma_range(&mut self, start: VirtAddr, size: usize) -> StarryResult {
2711        self.set_vma_lock_mode(start, size, VmaLockMode::Unlocked, None)
2712    }
2713
2714    /// Publishes the exact PageObject owners returned by one backend PTE
2715    /// operation.
2716    ///
2717    /// Linux fault paths carry a referenced folio into the PTL critical
2718    /// section, recheck the PTE, establish the new rmap and only then publish
2719    /// or replace the PTE. Starry's backend currently applies the PTE before
2720    /// returning to this outer mutation gate, so this method performs the
2721    /// corresponding identity recheck and publishes the MappingSlot/rmap
2722    /// before the mutation receipt can become visible. The PTE's raw physical
2723    /// address is validation data only; it is never used to discover or create
2724    /// a PageObject.
2725    fn publish_prepared_pte_owners(
2726        &mut self,
2727        operation: &MappingOperation,
2728        range: VirtAddrRange,
2729        materialization: &PteMaterialization,
2730    ) -> StarryResult<PteOwnerPublication> {
2731        let owners = materialization.owners();
2732        let mut publications = Vec::new();
2733        publications
2734            .try_reserve(owners.len())
2735            .map_err(|_| StarryError::NoMemory)?;
2736        let mut seen = Vec::new();
2737        seen.try_reserve(owners.len())
2738            .map_err(|_| StarryError::NoMemory)?;
2739        let mut mapping_delta = MappingDelta::default();
2740        let mut resident_delta = ResidentDelta::default();
2741
2742        // Bulk populate may carry multiple leaves. Prepare every fallible slot
2743        // and rmap owner before publishing the first one so rollback retains a
2744        // complete inverse operation.
2745        for owner in owners {
2746            let publication = self.prepare_slot_publication(operation, range, owner)?;
2747            if seen.contains(&publication.key) {
2748                return Err(StarryError::BadState);
2749            }
2750            seen.push(publication.key);
2751            mapping_delta.attached = mapping_delta
2752                .attached
2753                .checked_add(publication.mapping_delta.attached)
2754                .ok_or(StarryError::BadState)?;
2755            mapping_delta.detached = mapping_delta
2756                .detached
2757                .checked_add(publication.mapping_delta.detached)
2758                .ok_or(StarryError::BadState)?;
2759            resident_delta.checked_add_assign(publication.resident_delta)?;
2760            publications.push(publication);
2761        }
2762
2763        for publication in publications {
2764            self.apply_slot_publication(operation, publication)?;
2765        }
2766        Ok(PteOwnerPublication {
2767            satisfied_pages: materialization.satisfied_pages(),
2768            mapping_delta,
2769            resident_delta,
2770        })
2771    }
2772
2773    fn publish_prepared_fault_owner(
2774        &mut self,
2775        operation: &MappingOperation,
2776        range: VirtAddrRange,
2777        materialization: &FaultMaterialization,
2778    ) -> StarryResult<PteOwnerPublication> {
2779        let Some(owner) = materialization.owner() else {
2780            return Ok(PteOwnerPublication {
2781                satisfied_pages: materialization.satisfied_pages(),
2782                ..PteOwnerPublication::default()
2783            });
2784        };
2785        // A hardware fault carries one owner inline, so neither preparation
2786        // nor publication needs a temporary Vec.
2787        let publication = self.prepare_slot_publication(operation, range, owner)?;
2788        let mapping_delta = publication.mapping_delta;
2789        let resident_delta = publication.resident_delta;
2790        self.apply_slot_publication(operation, publication)?;
2791        Ok(PteOwnerPublication {
2792            satisfied_pages: materialization.satisfied_pages(),
2793            mapping_delta,
2794            resident_delta,
2795        })
2796    }
2797
2798    fn prepare_slot_publication(
2799        &mut self,
2800        operation: &MappingOperation,
2801        range: VirtAddrRange,
2802        owner: &PreparedPteOwner,
2803    ) -> StarryResult<PreparedSlotPublication> {
2804        let va = owner.va;
2805        let paddr = owner.paddr;
2806        let page_size = owner.page_size;
2807        let page = &owner.page;
2808        let resident_kind = owner.resident_kind;
2809        let transition = owner.transition;
2810        let provider_publication = owner.provider_publication;
2811        if page_size < PAGE_SIZE_4K || !page_size.is_power_of_two() || !va.is_aligned(page_size) {
2812            return Err(StarryError::BadState);
2813        }
2814        let leaf_range =
2815            VirtAddrRange::try_from_start_size(va, page_size).ok_or(StarryError::BadState)?;
2816        if !range.contains_range(leaf_range) {
2817            return Err(StarryError::BadState);
2818        }
2819        let frame_start = page.frame().paddr().as_usize();
2820        let frame_end = frame_start
2821            .checked_add(page.frame().size())
2822            .ok_or(StarryError::BadState)?;
2823        let leaf_start = paddr.as_usize();
2824        let leaf_end = leaf_start
2825            .checked_add(page_size)
2826            .ok_or(StarryError::BadState)?;
2827        if leaf_start < frame_start || leaf_end > frame_end {
2828            return Err(StarryError::BadState);
2829        }
2830        match self.pt.query(va) {
2831            Ok((installed, _, installed_size))
2832                if installed == paddr && installed_size == page_size => {}
2833            Ok(_) | Err(_) => return Err(StarryError::BadState),
2834        }
2835        if !matches!(page.state(), PageState::Present | PageState::LazyFree) {
2836            return Err(StarryError::BadState);
2837        }
2838
2839        let key = MappingSlotKey {
2840            space_id: self.id,
2841            va,
2842        };
2843        let previous = self.mapping_slots.get(&key).cloned();
2844        let order = page_size
2845            .trailing_zeros()
2846            .checked_sub(PAGE_SIZE_4K.trailing_zeros())
2847            .and_then(|order| u8::try_from(order).ok())
2848            .map(PageOrder::new)
2849            .ok_or(StarryError::BadState)?;
2850        let same_owner = previous.as_ref().is_some_and(|slot| {
2851            slot.state() == SlotState::Present
2852                && slot.mapping == operation.mapping_id()
2853                && slot.page_order == order
2854                && slot.mapped_paddr() == Some(paddr)
2855                && Arc::ptr_eq(&slot.page, page)
2856                && (order == PageOrder::BASE || slot.has_huge_split_deposit())
2857        });
2858        match transition {
2859            PteOwnerTransition::Updated if !same_owner => return Err(StarryError::BadState),
2860            PteOwnerTransition::Replaced if previous.is_none() || same_owner => {
2861                return Err(StarryError::BadState);
2862            }
2863            PteOwnerTransition::Installed
2864            | PteOwnerTransition::Replaced
2865            | PteOwnerTransition::Updated => {}
2866        }
2867
2868        let replacement = if same_owner {
2869            None
2870        } else {
2871            let split_deposit = if order == PageOrder::BASE {
2872                None
2873            } else {
2874                Some(self.pt.prepare_huge_split(va)?)
2875            };
2876            let frame_offset = paddr
2877                .as_usize()
2878                .checked_sub(page.frame().paddr().as_usize())
2879                .ok_or(StarryError::BadState)?;
2880            let slot = MappingSlot::new_with_frame_offset(
2881                operation.mapping_id(),
2882                self.id,
2883                va,
2884                order,
2885                page.clone(),
2886                frame_offset,
2887                resident_kind,
2888            )
2889            .ok_or(StarryError::BadState)?;
2890            let slot = match split_deposit {
2891                Some(deposit) => slot
2892                    .attach_huge_split_deposit(deposit)
2893                    .map_err(|_| StarryError::BadState)?,
2894                None => slot,
2895            };
2896            Some(Arc::new(slot))
2897        };
2898        let mut mapping_delta = MappingDelta::default();
2899        let mut resident_delta = ResidentDelta::default();
2900        if !same_owner {
2901            mapping_delta.attached = 1;
2902            mapping_delta.detached = u32::from(previous.is_some());
2903        }
2904        if let Some(previous) = &previous {
2905            let pages = 1i64
2906                .checked_shl(previous.page_order.get().into())
2907                .ok_or(StarryError::BadState)?;
2908            resident_delta
2909                .checked_add_assign(ResidentDelta::for_pages(previous.resident_kind(), -pages))?;
2910        }
2911        let pages = 1i64
2912            .checked_shl(order.get().into())
2913            .ok_or(StarryError::BadState)?;
2914        resident_delta.checked_add_assign(ResidentDelta::for_pages(resident_kind, pages))?;
2915        Ok(PreparedSlotPublication {
2916            key,
2917            previous,
2918            replacement,
2919            resident_kind,
2920            provider_publication,
2921            mapping_delta,
2922            resident_delta,
2923        })
2924    }
2925
2926    fn apply_slot_publication(
2927        &mut self,
2928        operation: &MappingOperation,
2929        publication: PreparedSlotPublication,
2930    ) -> StarryResult {
2931        let PreparedSlotPublication {
2932            key,
2933            previous,
2934            replacement,
2935            resident_kind,
2936            provider_publication,
2937            mapping_delta: _,
2938            resident_delta: _,
2939        } = publication;
2940        let Some(replacement) = replacement else {
2941            let current = self.mapping_slots.get(&key).ok_or(StarryError::BadState)?;
2942            if previous
2943                .as_ref()
2944                .is_none_or(|previous| !Arc::ptr_eq(previous, current))
2945            {
2946                return Err(StarryError::BadState);
2947            }
2948            current.set_resident_kind(resident_kind);
2949            current.page.set_resident_kind(resident_kind);
2950            if provider_publication == ProviderPublication::Pending {
2951                operation.finish_page_publication(key.va, &current.page)?;
2952            }
2953            return Ok(());
2954        };
2955
2956        if let Some(previous) = &previous {
2957            let Some(current) = self.mapping_slots.remove(&key) else {
2958                return Err(StarryError::BadState);
2959            };
2960            if !Arc::ptr_eq(previous, &current) || !current.detach() {
2961                self.mapping_slots.insert(key, current);
2962                return Err(StarryError::BadState);
2963            }
2964        } else if self.mapping_slots.contains_key(&key) {
2965            return Err(StarryError::BadState);
2966        }
2967
2968        if !replacement.publish() {
2969            if let Some(previous) = previous
2970                && (!previous.restore() || self.mapping_slots.insert(key, previous).is_some())
2971            {
2972                self.mutation_gate.mark_needs_repair();
2973            }
2974            return Err(StarryError::BadState);
2975        }
2976        if provider_publication == ProviderPublication::Pending
2977            && let Err(error) = operation.finish_page_publication(key.va, &replacement.page)
2978        {
2979            let replacement_detached = replacement.detach();
2980            let previous_restored = previous.is_none_or(|previous| {
2981                previous.restore() && self.mapping_slots.insert(key, previous).is_none()
2982            });
2983            if !replacement_detached || !previous_restored {
2984                self.mutation_gate.mark_needs_repair();
2985                return Err(StarryError::BadState);
2986            }
2987            return Err(error);
2988        }
2989        if self.mapping_slots.insert(key, replacement).is_some() {
2990            self.mutation_gate.mark_needs_repair();
2991            return Err(StarryError::BadState);
2992        }
2993        Ok(())
2994    }
2995
2996    fn detach_mapping_slots(&mut self, range: VirtAddrRange) -> StarryResult {
2997        let keys: Vec<_> = self
2998            .mapping_slots_overlapping(range)
2999            .map(|(key, _)| *key)
3000            .collect();
3001        for key in keys {
3002            if let Some(slot) = self.mapping_slots.remove(&key)
3003                && !slot.detach()
3004            {
3005                self.mapping_slots.insert(key, slot);
3006                return Err(StarryError::BadState);
3007            }
3008        }
3009        Ok(())
3010    }
3011
3012    /// Revokes one file-cache reverse mapping.  The PageObject and MappingSlot
3013    /// identities are checked before the PTE is touched, then the remote TLB is
3014    /// acknowledged before the backend's mapping reference is released.
3015    pub(crate) fn evict_file_mapping_slot(
3016        &mut self,
3017        key: MappingSlotKey,
3018        page: &Arc<PageObject>,
3019    ) -> Result<EvictMappingOutcome, StarryError> {
3020        if key.space_id != self.id {
3021            return Err(StarryError::BadState);
3022        }
3023        let slot = self
3024            .mapping_slots
3025            .get(&key)
3026            .cloned()
3027            .ok_or(StarryError::BadState)?;
3028        if !Arc::ptr_eq(&slot.page, page) {
3029            return Err(StarryError::BadState);
3030        }
3031
3032        let range = VirtAddrRange::from_start_size(key.va, PAGE_SIZE_4K);
3033        let preimage = self.capture_mapping_preimage(range)?;
3034        let retired_owner = self.prepare_deferred_eviction_owner(page)?;
3035        let mut mutation = self.prepare_mutation_range(key.va, PAGE_SIZE_4K);
3036        let retire_epoch = mutation
3037            .receipt()
3038            .base_epoch
3039            .checked_next()
3040            .ok_or(StarryError::BadState)?;
3041        mutation.set_pte_delta(PteDelta {
3042            unmapped: 1,
3043            ..PteDelta::default()
3044        });
3045        mutation.set_mapping_delta(MappingDelta {
3046            detached: 1,
3047            ..MappingDelta::default()
3048        });
3049        mutation.set_resident_delta(ResidentDelta::for_pages(slot.resident_kind(), -1));
3050
3051        let unmap_plan = self.pt.plan_unmap_page(key.va)?;
3052        if slot.mapped_paddr() != Some(unmap_plan.paddr()) || unmap_plan.page_size() != PAGE_SIZE_4K
3053        {
3054            return Err(StarryError::BadState);
3055        }
3056        let apply_result = (|| -> StarryResult {
3057            let unmapped = {
3058                let pte_domain = &self.pte_domain;
3059                let pt = &mut self.pt;
3060                let range = VirtAddrRange::from_start_size(key.va, PAGE_SIZE_4K);
3061                let _structure = pte_domain.lock_structure();
3062                let _stripe = pte_domain.lock_range(range);
3063                pt.try_unmap_page_with(unmap_plan)?
3064            };
3065            if slot.mapped_paddr() != Some(unmapped.0) || unmapped.2 != PAGE_SIZE_4K {
3066                return Err(StarryError::BadState);
3067            }
3068            // `page` and the caller's EvictionLease keep the frame owned until
3069            // this receipt is acknowledged.  No independent all-CPU flush is
3070            // allowed here: the receipt is the sole retirement obligation.
3071            let removed = self
3072                .mapping_slots
3073                .remove(&key)
3074                .ok_or(StarryError::BadState)?;
3075            if !Arc::ptr_eq(&removed, &slot) || !removed.detach() {
3076                return Err(StarryError::BadState);
3077            }
3078            Ok(())
3079        })();
3080        if let Err(error) = apply_result {
3081            return self.abort_file_eviction_mutation(range, preimage, None, error);
3082        }
3083
3084        self.park_retired_mapping_owners(retire_epoch, retired_owner);
3085        match self.commit_mutation_classified(mutation) {
3086            Ok(()) => {
3087                self.release_retired_mapping_owners(retire_epoch);
3088                Ok(EvictMappingOutcome::Complete)
3089            }
3090            Err(CommitMutationError::PublishedPendingTlb(_)) => {
3091                Ok(EvictMappingOutcome::PublishedPendingTlb)
3092            }
3093            Err(CommitMutationError::Unpublished(error)) => {
3094                self.abort_file_eviction_mutation(range, preimage, Some(retire_epoch), error)
3095            }
3096        }
3097    }
3098
3099    /// Write-protects one dirty file-cache reverse mapping for a writeback
3100    /// generation.  No file/cache lock is held by this operation.
3101    pub(crate) fn protect_file_mapping_slot(
3102        &mut self,
3103        key: MappingSlotKey,
3104        page: &Arc<PageObject>,
3105    ) -> StarryResult {
3106        if key.space_id != self.id {
3107            return Err(StarryError::BadState);
3108        }
3109        let slot = self.mapping_slots.get(&key).ok_or(StarryError::BadState)?;
3110        if !Arc::ptr_eq(&slot.page, page) {
3111            return Err(StarryError::BadState);
3112        }
3113
3114        let (paddr, flags, page_size) = self.pt.query(key.va)?;
3115        if slot.mapped_paddr() != Some(paddr) || page_size != PAGE_SIZE_4K {
3116            return Err(StarryError::BadState);
3117        }
3118        if !flags.contains(MappingFlags::WRITE) {
3119            return Ok(());
3120        }
3121        let mut mutation = self.prepare_mutation_range(key.va, PAGE_SIZE_4K);
3122        mutation.set_pte_delta(PteDelta {
3123            protected: 1,
3124            ..PteDelta::default()
3125        });
3126        {
3127            let pt = &mut self.pt;
3128            let _stripe = self
3129                .pte_domain
3130                .lock_range(VirtAddrRange::from_start_size(key.va, PAGE_SIZE_4K));
3131            // Disjoint field borrows retain stripe exclusion without aliasing
3132            // the mutable page-table owner through a raw pointer.
3133            pt.remap_page(key.va, paddr, flags - MappingFlags::WRITE)?;
3134        }
3135        // `commit_mutation` publishes the PTE delta and synchronously services
3136        // its tagged TLB request.  The WritebackLease is completed only after
3137        // this method returns, so dirty-page snapshotting cannot race ahead of
3138        // the acknowledgement.
3139        self.commit_mutation(mutation)
3140    }
3141
3142    pub fn resident_mapping_slots(&self) -> Vec<Arc<MappingSlot>> {
3143        self.mapping_slots.values().cloned().collect()
3144    }
3145
3146    fn capture_mapping_graph_snapshot(
3147        &self,
3148        ranges: &[VirtAddrRange],
3149    ) -> StarryResult<MappingGraphSnapshot> {
3150        #[cfg(all(test, axtest))]
3151        MAPPING_GRAPH_SNAPSHOT_CALLS.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
3152        let slot_count = ranges
3153            .iter()
3154            .map(|range| self.mapping_slots_overlapping(*range).count())
3155            .sum();
3156        let mut slots = Vec::new();
3157        slots
3158            .try_reserve(slot_count)
3159            .map_err(|_| StarryError::NoMemory)?;
3160        let mut seen = BTreeSet::new();
3161        let mut resident = ResidentPageCounts::default();
3162        for range in ranges {
3163            for (key, slot) in self.mapping_slots_overlapping(*range) {
3164                if slot.state() != SlotState::Present || !seen.insert(*key) {
3165                    continue;
3166                }
3167                slots.push(MappingSlotFingerprint {
3168                    key: *key,
3169                    mapping: slot.mapping,
3170                    page: slot.page.id,
3171                    page_order: slot.page_order,
3172                });
3173                let pages = 1u64
3174                    .checked_shl(u32::from(slot.page_order.get()))
3175                    .ok_or(StarryError::BadState)?;
3176                match slot.resident_kind() {
3177                    Some(RssKind::Anon) => {
3178                        resident.anon = resident
3179                            .anon
3180                            .checked_add(pages)
3181                            .ok_or(StarryError::BadState)?;
3182                    }
3183                    Some(RssKind::File) => {
3184                        resident.file = resident
3185                            .file
3186                            .checked_add(pages)
3187                            .ok_or(StarryError::BadState)?;
3188                    }
3189                    Some(RssKind::Shmem) => {
3190                        resident.shmem = resident
3191                            .shmem
3192                            .checked_add(pages)
3193                            .ok_or(StarryError::BadState)?;
3194                    }
3195                    None => {}
3196                }
3197            }
3198        }
3199        slots.sort_unstable();
3200        Ok(MappingGraphSnapshot { slots, resident })
3201    }
3202
3203    fn set_mapping_graph_receipt_delta(
3204        &self,
3205        mutation: &mut PreparedMutation,
3206        before: &MappingGraphSnapshot,
3207        ranges: &[VirtAddrRange],
3208    ) -> StarryResult {
3209        let after = self.capture_mapping_graph_snapshot(ranges)?;
3210        let (mapping_delta, resident_delta) = before.delta_to(&after)?;
3211        mutation.set_mapping_delta(mapping_delta);
3212        mutation.set_resident_delta(resident_delta);
3213        Ok(())
3214    }
3215
3216    /// Returns the current resident set published by mutation receipts.
3217    pub(crate) fn resident_page_counts(&self) -> ResidentPageCounts {
3218        self.resident_pages
3219    }
3220
3221    #[cfg(all(test, axtest))]
3222    fn reset_mapping_graph_snapshot_calls_for_test(&self) {
3223        MAPPING_GRAPH_SNAPSHOT_CALLS.store(0, core::sync::atomic::Ordering::Relaxed);
3224    }
3225
3226    #[cfg(all(test, axtest))]
3227    fn mapping_graph_snapshot_calls_for_test(&self) -> usize {
3228        MAPPING_GRAPH_SNAPSHOT_CALLS.load(core::sync::atomic::Ordering::Relaxed)
3229    }
3230
3231    pub(crate) fn resident_hiwater_pages(&self) -> u64 {
3232        self.resident_watermark.hiwater_pages()
3233    }
3234
3235    /// Collects executable operations for every VMA intersection in virtual
3236    /// order.  The returned values are owned, so no persistent-tree node is
3237    /// borrowed while a backend touches page tables or enters file I/O.
3238    fn mapping_operation_fragments(
3239        &self,
3240        range: VirtAddrRange,
3241        require_full_coverage: bool,
3242    ) -> StarryResult<Vec<(VirtAddrRange, MappingOperation)>> {
3243        let mut fragments = Vec::new();
3244        fragments
3245            .try_reserve(self.vma_root.len())
3246            .map_err(|_| StarryError::NoMemory)?;
3247        let mut covered = range.start;
3248        for entry in self.vma_root.iter_entries() {
3249            if entry.start() >= range.end {
3250                break;
3251            }
3252            if entry.end() <= range.start {
3253                continue;
3254            }
3255            let fragment =
3256                VirtAddrRange::new(entry.start().max(range.start), entry.end().min(range.end));
3257            if require_full_coverage && fragment.start > covered {
3258                return Err(StarryError::NoMemory);
3259            }
3260            covered = covered.max(fragment.end);
3261            fragments.push((fragment, entry.operation_clone()));
3262        }
3263        if require_full_coverage && covered < range.end {
3264            return Err(StarryError::NoMemory);
3265        }
3266        Ok(fragments)
3267    }
3268
3269    /// Detaches only leaves that are actually materialized for `operation`.
3270    /// This is used to clean a backend that reported failure after installing
3271    /// a prefix; unlike a whole-range unmap it is valid when the remaining
3272    /// addresses never acquired PTEs.
3273    fn detach_materialized_operation(
3274        &mut self,
3275        range: VirtAddrRange,
3276        operation: &MappingOperation,
3277    ) -> bool {
3278        let Ok(occupied) = self.occupied_pte_leaves_overlapping(&[range]) else {
3279            return false;
3280        };
3281        let leaves: Vec<_> = occupied.into_iter().map(|leaf| leaf.range).collect();
3282        if leaves
3283            .iter()
3284            .any(|leaf| !operation.validate_unmap_range(*leaf, &self.pt))
3285        {
3286            return false;
3287        }
3288        leaves
3289            .into_iter()
3290            .all(|leaf| operation.unmap_range(leaf, &mut self.pt).is_ok())
3291    }
3292
3293    /// Detaches every occupied leaf covered by the currently unpublished VMA
3294    /// root. The page-table walk visits allocated tables only, so rollback of
3295    /// a sparse multi-gigabyte mapping does not scan every virtual base page.
3296    fn detach_current_materialized_ranges(&mut self, ranges: &[VirtAddrRange]) -> StarryResult {
3297        let leaves = self.occupied_pte_leaves_overlapping(ranges)?;
3298        let mut operations = Vec::new();
3299        operations
3300            .try_reserve(leaves.len())
3301            .map_err(|_| StarryError::NoMemory)?;
3302        for leaf in leaves {
3303            let operation = self
3304                .vma_root
3305                .lookup_entry(leaf.range.start)
3306                .map(|entry| entry.operation_clone())
3307                .ok_or(StarryError::BadState)?;
3308            operations.push((leaf.range, operation));
3309        }
3310        if operations
3311            .iter()
3312            .any(|(leaf, operation)| !operation.validate_unmap_range(*leaf, &self.pt))
3313        {
3314            return Err(StarryError::BadState);
3315        }
3316        for (leaf, operation) in operations {
3317            operation.unmap_range(leaf, &mut self.pt)?;
3318        }
3319        Ok(())
3320    }
3321
3322    #[allow(clippy::too_many_arguments)]
3323    fn prepare_mapping_successor(
3324        &self,
3325        range: VirtAddrRange,
3326        permissions: MappingPermissions,
3327        operation: &MappingOperation,
3328        huge_page_advice: HugePageAdvice,
3329        lock_mode: VmaLockMode,
3330        advice_policy: VmaAdvicePolicy,
3331        replace: bool,
3332    ) -> StarryResult<VmaMap> {
3333        let entry = self
3334            .vma_root
3335            .prepare_mapping_entry(
3336                range,
3337                permissions.current,
3338                permissions.reported,
3339                permissions.maximum,
3340                huge_page_advice,
3341                lock_mode,
3342                advice_policy,
3343                operation.clone(),
3344            )
3345            .ok_or(StarryError::BadState)?;
3346        let successor = self.vma_root.with_mapping_entry(entry, replace).ok_or(
3347            if self.vma_root.overlaps(range) && !replace {
3348                StarryError::AlreadyExists
3349            } else {
3350                StarryError::BadState
3351            },
3352        )?;
3353        Ok(successor)
3354    }
3355
3356    /// Applies only the materialized PTE half of a prepared mapping. The
3357    /// caller owns the successor root and publishes it only after this phase
3358    /// has completed.
3359    fn apply_mapping_pages_unpublished(
3360        &mut self,
3361        range: VirtAddrRange,
3362        permissions: MappingPermissions,
3363        operation: &MappingOperation,
3364        replace: bool,
3365    ) -> StarryResult<PteMaterialization> {
3366        let replaced = if replace {
3367            self.mapping_operation_fragments(range, false)?
3368        } else {
3369            Vec::new()
3370        };
3371        if replaced
3372            .iter()
3373            .any(|(fragment, old)| !old.validate_unmap_range(*fragment, &self.pt))
3374            || (!replace && !operation.validate_map_range(range, &self.pt))
3375        {
3376            return Err(StarryError::BadState);
3377        }
3378        for (fragment, old) in &replaced {
3379            old.unmap_range(*fragment, &mut self.pt)?;
3380        }
3381        match operation.map_range(range, permissions.current, &mut self.pt) {
3382            Ok(materialization) => Ok(materialization),
3383            Err(error) => {
3384                if !self.detach_materialized_operation(range, operation) {
3385                    self.mutation_gate.mark_needs_repair();
3386                    return Err(StarryError::BadState);
3387                }
3388                Err(error)
3389            }
3390        }
3391    }
3392
3393    /// Applies a fresh mapping or a `MAP_FIXED` replacement while keeping the
3394    /// immutable successor unpublished until every backend step succeeds.
3395    #[allow(clippy::too_many_arguments)]
3396    fn apply_mapping_unpublished(
3397        &mut self,
3398        range: VirtAddrRange,
3399        permissions: MappingPermissions,
3400        operation: &MappingOperation,
3401        huge_page_advice: HugePageAdvice,
3402        lock_mode: VmaLockMode,
3403        advice_policy: VmaAdvicePolicy,
3404        memlock_limit: Option<MemlockLimit>,
3405        replace: bool,
3406    ) -> StarryResult<PteMaterialization> {
3407        let successor = self.prepare_mapping_successor(
3408            range,
3409            permissions,
3410            operation,
3411            huge_page_advice,
3412            lock_mode,
3413            advice_policy,
3414            replace,
3415        )?;
3416        self.validate_memlock_successor(&successor, memlock_limit)?;
3417        let materialization =
3418            self.apply_mapping_pages_unpublished(range, permissions, operation, replace)?;
3419        self.vma_root = Arc::new(successor);
3420        Ok(materialization)
3421    }
3422
3423    fn apply_unmap_pages_unpublished(&mut self, range: VirtAddrRange) -> StarryResult {
3424        let operations = self.mapping_operation_fragments(range, false)?;
3425        if operations
3426            .iter()
3427            .any(|(fragment, operation)| !operation.validate_unmap_range(*fragment, &self.pt))
3428        {
3429            return Err(StarryError::BadState);
3430        }
3431        for (fragment, operation) in operations {
3432            operation.unmap_range(fragment, &mut self.pt)?;
3433        }
3434        Ok(())
3435    }
3436
3437    /// Applies VMA/PTE removal from one precomputed persistent-tree successor.
3438    /// Holes are accepted, matching Linux `munmap`; every intersecting backend
3439    /// is validated before the first PTE is detached.
3440    fn apply_unmap_unpublished(&mut self, range: VirtAddrRange) -> StarryResult {
3441        let successor = self
3442            .vma_root
3443            .without_range(range)
3444            .ok_or(StarryError::BadState)?;
3445        self.apply_unmap_pages_unpublished(range)?;
3446        self.vma_root = Arc::new(successor);
3447        Ok(())
3448    }
3449
3450    /// Applies a complete mprotect carve from one immutable successor.
3451    fn apply_protection_unpublished(
3452        &mut self,
3453        range: VirtAddrRange,
3454        flags: MappingFlags,
3455        reported_flags: MappingFlags,
3456    ) -> StarryResult {
3457        let successor = self
3458            .vma_root
3459            .with_permissions(range, flags, reported_flags)
3460            .ok_or(StarryError::NoMemory)?;
3461        let operations = self.mapping_operation_fragments(range, true)?;
3462        if operations
3463            .iter()
3464            .any(|(fragment, operation)| !operation.validate_protect_range(*fragment, &self.pt))
3465        {
3466            return Err(StarryError::BadState);
3467        }
3468        for (fragment, operation) in operations {
3469            operation.protect_range(fragment, flags, &mut self.pt)?;
3470        }
3471        self.vma_root = Arc::new(successor);
3472        Ok(())
3473    }
3474
3475    fn apply_extend_unpublished(
3476        &mut self,
3477        address: VirtAddr,
3478        additional_size: usize,
3479        memlock_limit: Option<MemlockLimit>,
3480    ) -> StarryResult<(VirtAddrRange, MappingOperation, PteMaterialization)> {
3481        let entry = self
3482            .vma_root
3483            .lookup_entry(address)
3484            .ok_or(StarryError::InvalidInput)?;
3485        let suffix = VirtAddrRange::try_from_start_size(entry.end(), additional_size)
3486            .ok_or(StarryError::InvalidInput)?;
3487        let operation = entry.operation_clone();
3488        let flags = entry.rights();
3489        let successor = self
3490            .vma_root
3491            .with_extended_right(address, additional_size)
3492            .ok_or(StarryError::AlreadyExists)?;
3493        self.validate_memlock_successor(&successor, memlock_limit)?;
3494        if !operation.validate_map_range(suffix, &self.pt) {
3495            return Err(StarryError::BadState);
3496        }
3497        let materialization = match operation.map_range(suffix, flags, &mut self.pt) {
3498            Ok(materialization) => materialization,
3499            Err(error) => {
3500                if !self.detach_materialized_operation(suffix, &operation) {
3501                    self.mutation_gate.mark_needs_repair();
3502                    return Err(StarryError::BadState);
3503                }
3504                return Err(error);
3505            }
3506        };
3507        self.vma_root = Arc::new(successor);
3508        Ok((suffix, operation, materialization))
3509    }
3510
3511    /// Add a new linear mapping.
3512    ///
3513    /// See [`MappingOperation`] for more details about the mapping backends.
3514    ///
3515    /// The `flags` parameter indicates the mapping permissions and attributes.
3516    ///
3517    /// Returns an error if the address range is out of the address space or not
3518    /// aligned.
3519    pub fn map_linear(
3520        &mut self,
3521        start_vaddr: VirtAddr,
3522        start_paddr: PhysAddr,
3523        size: usize,
3524        flags: MappingFlags,
3525    ) -> StarryResult {
3526        self.validate_region(start_vaddr, size)?;
3527        let range = VirtAddrRange::from_start_size(start_vaddr, size);
3528        let preimage = self.capture_mapping_preimage(range)?;
3529        let graph_preimage = self.capture_mapping_graph_snapshot(&[range])?;
3530        let mut mutation = self.prepare_mutation_range(start_vaddr, size);
3531
3532        if !start_paddr.is_aligned_4k() {
3533            return Err(StarryError::InvalidInput);
3534        }
3535        if start_paddr.checked_add(size).is_none() {
3536            return Err(StarryError::InvalidInput);
3537        }
3538
3539        let operation = MappingOperation::new_linear(start_vaddr, start_paddr, false);
3540        let materialization = match self.apply_mapping_unpublished(
3541            range,
3542            MappingPermissions {
3543                current: flags,
3544                reported: flags,
3545                maximum: flags,
3546            },
3547            &operation,
3548            HugePageAdvice::Default,
3549            VmaLockMode::Unlocked,
3550            VmaAdvicePolicy::default(),
3551            None,
3552            false,
3553        ) {
3554            Ok(materialization) => materialization,
3555            Err(error) => {
3556                return self.abort_unpublished_mapping_mutation(range, preimage, error);
3557            }
3558        };
3559        mutation.set_vma_delta(VmaDelta {
3560            inserted: 1,
3561            ..VmaDelta::default()
3562        });
3563        mutation.set_pte_delta(PteDelta {
3564            mapped: u32::try_from(size / PAGE_SIZE_4K).unwrap_or(u32::MAX),
3565            ..PteDelta::default()
3566        });
3567        self.vm_stat.on_map((size / PAGE_SIZE_4K) as u64);
3568        if let Err(error) = self.publish_prepared_pte_owners(&operation, range, &materialization) {
3569            return self.abort_unpublished_mapping_mutation(range, preimage, error);
3570        }
3571        if let Err(error) =
3572            self.set_mapping_graph_receipt_delta(&mut mutation, &graph_preimage, &[range])
3573        {
3574            return self.abort_unpublished_mapping_mutation(range, preimage, error);
3575        }
3576        match self.commit_mutation_classified(mutation) {
3577            Ok(()) => Ok(()),
3578            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
3579            Err(CommitMutationError::Unpublished(error)) => {
3580                self.abort_unpublished_mapping_mutation(range, preimage, error)
3581            }
3582        }
3583    }
3584
3585    pub fn map(
3586        &mut self,
3587        start: VirtAddr,
3588        size: usize,
3589        flags: MappingFlags,
3590        populate: bool,
3591        backend: MappingOperation,
3592    ) -> StarryResult {
3593        self.map_with_reported_flags(start, size, flags, flags, populate, backend)
3594    }
3595
3596    /// Applies a mapping and preserves the distinction between an unpublished
3597    /// failure and a published mutation whose TLB obligation is still
3598    /// pending.  Syscalls that also update an external ownership index (for
3599    /// example SysV SHM) must use this outcome-aware entry point so they can
3600    /// finish that bookkeeping even when the hardware shootdown cannot be
3601    /// acknowledged synchronously.
3602    pub(crate) fn map_outcome(
3603        &mut self,
3604        start: VirtAddr,
3605        size: usize,
3606        flags: MappingFlags,
3607        populate: bool,
3608        backend: MappingOperation,
3609    ) -> StarryResult<AddressSpaceMutationOutcome> {
3610        self.map_with_permissions_mode_classified(
3611            start,
3612            size,
3613            MappingPermissions {
3614                current: flags,
3615                reported: flags,
3616                maximum: flags,
3617            },
3618            populate,
3619            backend,
3620            MappingPublication::new(false),
3621        )
3622    }
3623
3624    pub fn map_with_reported_flags(
3625        &mut self,
3626        start: VirtAddr,
3627        size: usize,
3628        flags: MappingFlags,
3629        reported_flags: MappingFlags,
3630        populate: bool,
3631        backend: MappingOperation,
3632    ) -> StarryResult {
3633        self.map_with_permissions(
3634            start,
3635            size,
3636            MappingPermissions {
3637                current: flags,
3638                reported: reported_flags,
3639                maximum: flags,
3640            },
3641            populate,
3642            backend,
3643        )
3644    }
3645
3646    /// Maps a region and, when `replace` is true, atomically replaces any
3647    /// overlapping VMAs (the `MAP_FIXED` operation).  The replacement bit is
3648    /// deliberately explicit instead of making callers unmap first: an
3649    /// allocation, permission, or backend failure must leave the old mapping
3650    /// untouched.  [`MemorySet::map`] owns the preimage/rollback of the
3651    /// overlapping page-table fragments.
3652    pub fn map_with_permissions_replace(
3653        &mut self,
3654        start: VirtAddr,
3655        size: usize,
3656        permissions: MappingPermissions,
3657        populate: bool,
3658        backend: MappingOperation,
3659        replace: bool,
3660    ) -> StarryResult {
3661        self.map_with_permissions_mode(
3662            start,
3663            size,
3664            permissions,
3665            populate,
3666            backend,
3667            MappingPublication::new(replace),
3668        )
3669    }
3670
3671    /// Maps a region with one prepared metadata publication policy. Keeping
3672    /// replacement, huge-page advice, and VMA lock policy in one value avoids
3673    /// independently publishing fields that describe the same mapping.
3674    pub(crate) fn map_with_permissions_publication(
3675        &mut self,
3676        start: VirtAddr,
3677        size: usize,
3678        permissions: MappingPermissions,
3679        populate: bool,
3680        backend: MappingOperation,
3681        publication: MappingPublication,
3682    ) -> StarryResult {
3683        self.map_with_permissions_mode(start, size, permissions, populate, backend, publication)
3684    }
3685
3686    /// Installs the duplicate created by Linux's `mremap(old_size == 0)`
3687    /// special case while preserving the source VMA's THP advice in the same
3688    /// mapping receipt.
3689    pub(crate) fn map_mremap_duplicate(
3690        &mut self,
3691        start: VirtAddr,
3692        size: usize,
3693        permissions: MappingPermissions,
3694        backend: MappingOperation,
3695        publication: MappingPublication,
3696    ) -> StarryResult {
3697        self.map_with_permissions_mode(start, size, permissions, false, backend, publication)
3698    }
3699
3700    /// Maps a region while retaining the maximum permission envelope allowed
3701    /// by the original file/VM policy.  `mprotect` may lower permissions and
3702    /// later restore them only inside this envelope; the current PTE flags
3703    /// alone are not sufficient to express that Linux invariant.
3704    pub fn map_with_permissions(
3705        &mut self,
3706        start: VirtAddr,
3707        size: usize,
3708        permissions: MappingPermissions,
3709        populate: bool,
3710        backend: MappingOperation,
3711    ) -> StarryResult {
3712        self.map_with_permissions_mode(
3713            start,
3714            size,
3715            permissions,
3716            populate,
3717            backend,
3718            MappingPublication::new(false),
3719        )
3720    }
3721
3722    fn map_with_permissions_mode(
3723        &mut self,
3724        start: VirtAddr,
3725        size: usize,
3726        permissions: MappingPermissions,
3727        populate: bool,
3728        backend: MappingOperation,
3729        publication: MappingPublication,
3730    ) -> StarryResult {
3731        self.map_with_permissions_mode_classified(
3732            start,
3733            size,
3734            permissions,
3735            populate,
3736            backend,
3737            publication,
3738        )?
3739        .into_result()
3740    }
3741
3742    fn map_with_permissions_mode_classified(
3743        &mut self,
3744        start: VirtAddr,
3745        size: usize,
3746        permissions: MappingPermissions,
3747        populate: bool,
3748        backend: MappingOperation,
3749        publication: MappingPublication,
3750    ) -> StarryResult<AddressSpaceMutationOutcome> {
3751        let MappingPublication {
3752            replace,
3753            huge_page_advice,
3754            lock_mode,
3755            advice_policy,
3756            memlock_limit,
3757        } = publication;
3758        self.validate_region(start, size)?;
3759        if !permissions.maximum.contains(permissions.current) {
3760            return Err(StarryError::PermissionDenied);
3761        }
3762        let range =
3763            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
3764        let mut mutation = prepare_mapping_publication_mutation(
3765            &self.mutation_gate,
3766            self.id,
3767            &self.tlb_targets,
3768            start,
3769            size,
3770            replace,
3771        );
3772        self.mutation_gate
3773            .validate_publish_preconditions(&mutation)
3774            .map_err(Self::map_unpublished_mutation_error)?;
3775        // Count the old VSS before the persistent root performs replacement. This is
3776        // metadata only and therefore cannot make a failed map visible.
3777        let removed_pages = if replace {
3778            self.vma_root
3779                .iter_entries()
3780                .filter(|entry| entry.start() < range.end && entry.end() > range.start)
3781                .try_fold(0u64, |pages, entry| {
3782                    let lo = entry.start().max(range.start);
3783                    let hi = entry.end().min(range.end);
3784                    let fragment = hi.checked_sub_addr(lo).ok_or(StarryError::InvalidInput)?;
3785                    pages
3786                        .checked_add((fragment / PAGE_SIZE_4K) as u64)
3787                        .ok_or(StarryError::InvalidInput)
3788                })?
3789        } else {
3790            0
3791        };
3792        let mapping_preimage = self.capture_mapping_preimage(range)?;
3793        let graph_preimage = self.capture_mapping_graph_snapshot(&[range])?;
3794        let retire_epoch = mutation
3795            .receipt()
3796            .base_epoch
3797            .checked_next()
3798            .ok_or(StarryError::BadState)?;
3799        let retired_owners = replace
3800            .then(|| self.prepare_retired_mapping_owners(range))
3801            .transpose()?;
3802
3803        // Keep the identities of shared memfd VMAs so their writable-count
3804        // side band can be recomputed only after the replacement commits.  A
3805        // event before the VMA/PTE apply would make an allocation failure
3806        // externally visible despite the old mapping being restored.
3807        let touched_memfds = if replace {
3808            crate::syscall::memfd_collect_metas_touching_mprotect_range(self, start, size)
3809        } else {
3810            Vec::new()
3811        };
3812
3813        let map_materialization = match self.apply_mapping_unpublished(
3814            range,
3815            permissions,
3816            &backend,
3817            huge_page_advice,
3818            lock_mode,
3819            advice_policy,
3820            memlock_limit,
3821            replace,
3822        ) {
3823            Ok(materialization) => materialization,
3824            Err(error) => {
3825                return self
3826                    .abort_unpublished_mapping_mutation(range, mapping_preimage, error)
3827                    .map(|()| AddressSpaceMutationOutcome::Complete);
3828            }
3829        };
3830        if let Some(owners) = retired_owners {
3831            self.park_retired_mapping_owners(retire_epoch, owners);
3832        }
3833        if removed_pages != 0
3834            && let Err(error) = self.detach_mapping_slots(range)
3835        {
3836            return self
3837                .abort_unpublished_parked_mapping_mutation(
3838                    range,
3839                    mapping_preimage,
3840                    replace.then_some(retire_epoch),
3841                    error,
3842                )
3843                .map(|()| AddressSpaceMutationOutcome::Complete);
3844        }
3845        if let Err(error) = self.publish_prepared_pte_owners(&backend, range, &map_materialization)
3846        {
3847            return self
3848                .abort_unpublished_parked_mapping_mutation(
3849                    range,
3850                    mapping_preimage,
3851                    replace.then_some(retire_epoch),
3852                    error,
3853                )
3854                .map(|()| AddressSpaceMutationOutcome::Complete);
3855        }
3856        if populate
3857            && let Err(populate_error) = self.apply_populate_area(start, size, permissions.current)
3858        {
3859            return self
3860                .abort_unpublished_parked_mapping_mutation(
3861                    range,
3862                    mapping_preimage,
3863                    replace.then_some(retire_epoch),
3864                    populate_error,
3865                )
3866                .map(|()| AddressSpaceMutationOutcome::Complete);
3867        }
3868        mutation.set_vma_delta(VmaDelta {
3869            inserted: 1,
3870            removed: u32::try_from(removed_pages).unwrap_or(u32::MAX),
3871            ..VmaDelta::default()
3872        });
3873        self.vm_stat.on_map((size / PAGE_SIZE_4K) as u64);
3874        if removed_pages != 0 {
3875            self.vm_stat.on_unmap(removed_pages);
3876        }
3877        if let Err(error) =
3878            self.set_mapping_graph_receipt_delta(&mut mutation, &graph_preimage, &[range])
3879        {
3880            return self
3881                .abort_unpublished_parked_mapping_mutation(
3882                    range,
3883                    mapping_preimage,
3884                    replace.then_some(retire_epoch),
3885                    error,
3886                )
3887                .map(|()| AddressSpaceMutationOutcome::Complete);
3888        }
3889        match self.commit_mutation_classified(mutation) {
3890            Ok(()) => {
3891                if replace {
3892                    self.release_retired_mapping_owners(retire_epoch);
3893                    crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(
3894                        self,
3895                        &touched_memfds,
3896                    );
3897                } else {
3898                    crate::syscall::memfd_on_after_map(self, start);
3899                }
3900                Ok(AddressSpaceMutationOutcome::Complete)
3901            }
3902            Err(CommitMutationError::PublishedPendingTlb(error)) => {
3903                if replace {
3904                    crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(
3905                        self,
3906                        &touched_memfds,
3907                    );
3908                } else {
3909                    crate::syscall::memfd_on_after_map(self, start);
3910                }
3911                Ok(AddressSpaceMutationOutcome::PublishedPendingTlb(error))
3912            }
3913            Err(CommitMutationError::Unpublished(error)) => self
3914                .abort_unpublished_parked_mapping_mutation(
3915                    range,
3916                    mapping_preimage,
3917                    replace.then_some(retire_epoch),
3918                    error,
3919                )
3920                .map(|()| AddressSpaceMutationOutcome::Complete),
3921        }
3922    }
3923
3924    /// Applies backend population and its typed MappingSlot/rmap owners without
3925    /// publishing an epoch or external event. The caller owns the retained
3926    /// mapping preimage until the surrounding mutation commits.
3927    fn apply_populate_area(
3928        &mut self,
3929        mut start: VirtAddr,
3930        size: usize,
3931        access_flags: MappingFlags,
3932    ) -> StarryResult<usize> {
3933        self.validate_region(start, size)?;
3934        let end = start.checked_add(size).ok_or(StarryError::InvalidInput)?;
3935        let mut populated = 0usize;
3936
3937        loop {
3938            let area_end = {
3939                let Some(entry) = self.vma_root.lookup_entry(start) else {
3940                    break;
3941                };
3942                let entry_end = entry.end();
3943                let range = VirtAddrRange::new(start, entry_end.min(end));
3944                let flags = entry.rights();
3945                let backend = entry.operation_clone();
3946                let request = PopulateRequest::area(range, backend.page_size())?;
3947                let materialization =
3948                    backend.populate(self.id, request, flags, access_flags, &mut self.pt)?;
3949                let publication =
3950                    self.publish_prepared_pte_owners(&backend, range, &materialization)?;
3951                populated = populated
3952                    .checked_add(publication.satisfied_pages)
3953                    .ok_or(StarryError::NoMemory)?;
3954                entry_end
3955            };
3956            start = area_end;
3957            assert!(start.is_aligned_4k());
3958            if start >= end {
3959                break;
3960            }
3961        }
3962
3963        if start < end {
3964            // If the area is not fully mapped, we return ENOMEM.
3965            return Err(StarryError::NoMemory);
3966        }
3967
3968        Ok(populated)
3969    }
3970
3971    /// Returns whether every materialized leaf in `range` already permits the
3972    /// requested user access.
3973    ///
3974    /// This is a software page-table check under the address-space mutex, so it
3975    /// is the architecture-independent fallback for CPUs without a cheap user
3976    /// translation probe.  It deliberately requires `USER` even though
3977    /// `UserAccessIntent` only carries read/write intent.  A present supervisor
3978    /// mapping must never make a user-copy preparation succeed.
3979    fn materialized_range_satisfies_access(
3980        &self,
3981        range: VirtAddrRange,
3982        access_flags: MappingFlags,
3983    ) -> bool {
3984        let required = access_flags | MappingFlags::USER;
3985        let mut cursor = range.start;
3986        while cursor < range.end {
3987            let Ok((_, flags, leaf_size)) = self.pt.query(cursor) else {
3988                return false;
3989            };
3990            if leaf_size < PAGE_SIZE_4K || !leaf_size.is_power_of_two() || !flags.contains(required)
3991            {
3992                return false;
3993            }
3994            let Some(leaf_end) = cursor.align_down(leaf_size).checked_add(leaf_size) else {
3995                return false;
3996            };
3997            if leaf_end <= cursor {
3998                return false;
3999            }
4000            cursor = leaf_end.min(range.end);
4001        }
4002        true
4003    }
4004
4005    /// Populates an already-published area with physical frames.
4006    pub fn populate_area(
4007        &mut self,
4008        start: VirtAddr,
4009        size: usize,
4010        access_flags: MappingFlags,
4011    ) -> StarryResult {
4012        let range =
4013            VirtAddrRange::try_from_start_size(start, size).ok_or(StarryError::InvalidInput)?;
4014        self.validate_region(start, size)?;
4015        if self.can_access_range(start, size, access_flags)
4016            && self.materialized_range_satisfies_access(range, access_flags)
4017        {
4018            return Ok(());
4019        }
4020        let preimage = self.capture_mapping_preimage(range)?;
4021        let graph_preimage = self.capture_mapping_graph_snapshot(&[range])?;
4022        let mut mutation = self.prepare_mutation_range(start, size);
4023        let retire_epoch = mutation
4024            .receipt()
4025            .base_epoch
4026            .checked_next()
4027            .ok_or(StarryError::BadState)?;
4028        let retired_owners = (!graph_preimage.slots.is_empty())
4029            .then(|| self.prepare_retired_mapping_owners(range))
4030            .transpose()?;
4031        let populated = match self.apply_populate_area(start, size, access_flags) {
4032            Ok(populated) => populated,
4033            Err(populate_error) => {
4034                if self.restore_mapping_preimage(range, preimage).is_err() {
4035                    return Err(StarryError::BadState);
4036                }
4037                return Err(populate_error);
4038            }
4039        };
4040
4041        mutation.set_pte_delta(PteDelta {
4042            mapped: u32::try_from(populated).unwrap_or(u32::MAX),
4043            ..PteDelta::default()
4044        });
4045        if let Err(error) =
4046            self.set_mapping_graph_receipt_delta(&mut mutation, &graph_preimage, &[range])
4047        {
4048            return self.abort_unpublished_mapping_mutation(range, preimage, error);
4049        }
4050        if let Some(owners) = retired_owners {
4051            self.park_retired_mapping_owners(retire_epoch, owners);
4052        }
4053        match self.commit_mutation_classified(mutation) {
4054            Ok(()) => {
4055                self.release_retired_mapping_owners(retire_epoch);
4056                Ok(())
4057            }
4058            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
4059            Err(CommitMutationError::Unpublished(error)) => self
4060                .abort_unpublished_parked_mapping_mutation(
4061                    range,
4062                    preimage,
4063                    Some(retire_epoch),
4064                    error,
4065                ),
4066        }
4067    }
4068
4069    /// Discards the physical pages backing `[start, start+size)` while keeping
4070    /// the VMA metadata intact (Linux `MADV_DONTNEED` semantics).
4071    pub fn discard_range(&mut self, start: VirtAddr, size: usize) -> StarryResult {
4072        self.validate_region(start, size)?;
4073        let retired_range = VirtAddrRange::from_start_size(start, size);
4074        let end = start.checked_add(size).ok_or(StarryError::InvalidInput)?;
4075
4076        let mut frags: alloc::vec::Vec<(VirtAddrRange, MappingOperation)> = alloc::vec::Vec::new();
4077        frags
4078            .try_reserve(self.vma_root.len())
4079            .map_err(|_| StarryError::NoMemory)?;
4080        let mut covered = start;
4081        for entry in self.vma_root.iter_entries() {
4082            if entry.start() >= end {
4083                break;
4084            }
4085            if entry.end() <= start {
4086                continue;
4087            }
4088            let frag_start = entry.start().max(start);
4089            let frag_end = entry.end().min(end);
4090            if frag_start > covered {
4091                return Err(StarryError::NoMemory);
4092            }
4093            let backend = entry.operation_clone();
4094            // Device/linear mappings cannot reconstruct a discarded PTE.
4095            // External huge-page providers reject a partial-page carve before
4096            // any split or PTE mutation is published.
4097            backend.validate_discard_fragment(VirtAddrRange::new(frag_start, frag_end))?;
4098            frags.push((VirtAddrRange::new(frag_start, frag_end), backend));
4099            covered = frag_end;
4100        }
4101        if covered < end {
4102            return Err(StarryError::NoMemory);
4103        }
4104
4105        let mut mutation = self.prepare_mutation_range(start, size);
4106        mutation
4107            .try_reserve_tlb_ranges(2)
4108            .map_err(|_| StarryError::NoMemory)?;
4109        let retire_epoch = mutation
4110            .receipt()
4111            .base_epoch
4112            .checked_next()
4113            .ok_or(StarryError::BadState)?;
4114        let splits = self.apply_partial_huge_splits(retired_range)?;
4115        for index in 0..splits.len() {
4116            let split = &splits[index];
4117            let Some(tlb_range) =
4118                TlbRange::new(split.installed.block_vaddr(), split.installed.block_size())
4119            else {
4120                return self.abort_unpublished_huge_splits(splits, StarryError::BadState);
4121            };
4122            mutation.add_tlb_range(tlb_range);
4123        }
4124        if frags
4125            .iter()
4126            .any(|(range, backend)| !backend.validate_unmap_range(*range, &self.pt))
4127        {
4128            return self.abort_unpublished_huge_splits(splits, StarryError::OperationNotSupported);
4129        }
4130
4131        let preimage = match self.capture_mapping_preimage(retired_range) {
4132            Ok(preimage) => preimage,
4133            Err(error) => return self.abort_unpublished_huge_splits(splits, error),
4134        };
4135        let retired_owners = match self.prepare_retired_mapping_owners(retired_range) {
4136            Ok(owners) => owners,
4137            Err(error) => {
4138                return self.abort_unpublished_split_mapping_mutation(
4139                    retired_range,
4140                    preimage,
4141                    None,
4142                    splits,
4143                    error,
4144                );
4145            }
4146        };
4147        let Ok((detached_slots, retired_pages, retired_resident)) =
4148            self.mapping_slot_summary(retired_range)
4149        else {
4150            return self.abort_unpublished_split_mapping_mutation(
4151                retired_range,
4152                preimage,
4153                None,
4154                splits,
4155                StarryError::BadState,
4156            );
4157        };
4158        let split_slots = splits.iter().try_fold(0usize, |slots, split| {
4159            slots.checked_add(split.child_slots.len().saturating_sub(1))
4160        });
4161        let Some(split_slots) = split_slots else {
4162            return self.abort_unpublished_split_mapping_mutation(
4163                retired_range,
4164                preimage,
4165                None,
4166                splits,
4167                StarryError::BadState,
4168            );
4169        };
4170
4171        let deferred_tlb = DeferredTlbRetireGuard::enter();
4172        for (range, backend) in frags {
4173            if let Err(error) = backend.unmap_range(range, &mut self.pt) {
4174                drop(deferred_tlb);
4175                if let Err(flush_error) = crate::mm::flush_tlb_range_sync(start, size) {
4176                    warn!("discard repair could not invalidate {start:?}+{size:#x}: {flush_error}");
4177                }
4178                return self.abort_unpublished_split_mapping_mutation(
4179                    retired_range,
4180                    preimage,
4181                    None,
4182                    splits,
4183                    error,
4184                );
4185            }
4186        }
4187        drop(deferred_tlb);
4188        if let Err(error) = self.detach_mapping_slots(retired_range) {
4189            return self.abort_unpublished_split_mapping_mutation(
4190                retired_range,
4191                preimage,
4192                None,
4193                splits,
4194                error,
4195            );
4196        }
4197        self.park_retired_mapping_owners(retire_epoch, retired_owners);
4198        mutation.set_pte_delta(PteDelta {
4199            unmapped: u32::try_from(retired_pages).unwrap_or(u32::MAX),
4200            ..PteDelta::default()
4201        });
4202        mutation.set_mapping_delta(MappingDelta {
4203            attached: u32::try_from(split_slots).unwrap_or(u32::MAX),
4204            detached: u32::try_from(detached_slots).unwrap_or(u32::MAX),
4205        });
4206        mutation.set_resident_delta(retired_resident.checked_negated_delta()?);
4207
4208        match self.commit_mutation_classified(mutation) {
4209            Ok(()) => {
4210                self.release_retired_mapping_owners(retire_epoch);
4211                Ok(())
4212            }
4213            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
4214            Err(CommitMutationError::Unpublished(error)) => self
4215                .abort_unpublished_split_mapping_mutation(
4216                    retired_range,
4217                    preimage,
4218                    Some(retire_epoch),
4219                    splits,
4220                    error,
4221                ),
4222        }
4223    }
4224
4225    /// Marks exclusive private-anonymous PageObjects as lazily free.
4226    ///
4227    /// The PageObject is the sole mark owner.  Writable leaves are protected
4228    /// in the same receipt so a later store faults and changes `LazyFree` back
4229    /// to `Present` before write permission is republished.  COW-shared pages
4230    /// are intentionally skipped: a mapping-local hint must not mark another
4231    /// process's shared PageObject reclaimable.
4232    pub fn mark_lazy_free(&mut self, start: VirtAddr, size: usize) -> StarryResult {
4233        self.validate_region(start, size)?;
4234        let end = start.checked_add(size).ok_or(StarryError::InvalidInput)?;
4235        let mut covered = start;
4236        for entry in self.vma_root.iter_entries() {
4237            if entry.start() >= end {
4238                break;
4239            }
4240            if entry.end() <= start {
4241                continue;
4242            }
4243            let fragment_start = entry.start().max(start);
4244            if fragment_start > covered {
4245                return Err(StarryError::NoMemory);
4246            }
4247            if !entry.operation().is_private_anonymous() {
4248                return Err(StarryError::InvalidInput);
4249            }
4250            covered = entry.end().min(end);
4251        }
4252        if covered < end {
4253            return Err(StarryError::NoMemory);
4254        }
4255
4256        let range = VirtAddrRange::from_start_size(start, size);
4257        let mut candidates = Vec::new();
4258        candidates
4259            .try_reserve(self.mapping_slots_overlapping(range).count())
4260            .map_err(|_| StarryError::NoMemory)?;
4261        for (_, slot) in self.mapping_slots_overlapping(range) {
4262            if slot.page_order != PageOrder::BASE || slot.page.mapping_refs() != 1 {
4263                continue;
4264            }
4265            match slot.page.state() {
4266                PageState::LazyFree => continue,
4267                PageState::Present => {}
4268                PageState::Reserved
4269                | PageState::Evicting
4270                | PageState::Writeback
4271                | PageState::Retired => return Err(StarryError::ResourceBusy),
4272            }
4273            let (paddr, flags, page_size) = self.pt.query(slot.va)?;
4274            if slot.mapped_paddr() != Some(paddr) || page_size != PAGE_SIZE_4K {
4275                return Err(StarryError::BadState);
4276            }
4277            candidates.push((slot.va, paddr, flags, slot.page.clone()));
4278        }
4279        if candidates.is_empty() {
4280            return Ok(());
4281        }
4282
4283        let mut mutation = self.prepare_mutation_range(start, size);
4284        let pt = &mut self.pt;
4285        let stripes = self.pte_domain.lock_range(range);
4286        let mut protected = 0usize;
4287        for &(va, paddr, flags, _) in &candidates {
4288            if !flags.contains(MappingFlags::WRITE) {
4289                continue;
4290            }
4291            // The ordered stripe cursor borrows only the lock domain; the
4292            // page table remains exclusively borrowed throughout apply.
4293            if pt
4294                .remap_page(va, paddr, flags - MappingFlags::WRITE)
4295                .is_err()
4296            {
4297                for &(old_va, old_paddr, old_flags, _) in candidates.iter().rev() {
4298                    if old_flags.contains(MappingFlags::WRITE) {
4299                        // Covered by the same stripe cursor.
4300                        let _ = pt.remap_page(old_va, old_paddr, old_flags);
4301                    }
4302                }
4303                return Err(StarryError::BadState);
4304            }
4305            protected += 1;
4306        }
4307        drop(stripes);
4308
4309        for (marked, (_, _, _, page)) in candidates.iter().enumerate() {
4310            if !page.mark_lazy_free() {
4311                for (_, _, _, marked_page) in candidates[..marked].iter().rev() {
4312                    let _ = marked_page.clear_lazy_free();
4313                }
4314                let pt = &mut self.pt;
4315                let _stripes = self.pte_domain.lock_range(range);
4316                for &(va, paddr, flags, _) in &candidates {
4317                    if flags.contains(MappingFlags::WRITE) {
4318                        // Covered by the ordered stripe cursor.
4319                        let _ = pt.remap_page(va, paddr, flags);
4320                    }
4321                }
4322                return Err(StarryError::ResourceBusy);
4323            }
4324        }
4325
4326        mutation.set_pte_delta(PteDelta {
4327            protected: u32::try_from(protected).unwrap_or(u32::MAX),
4328            ..PteDelta::default()
4329        });
4330        match self.commit_mutation_classified(mutation) {
4331            Ok(()) => {
4332                lifecycle::request_lazy_free_reclaim();
4333                Ok(())
4334            }
4335            Err(CommitMutationError::PublishedPendingTlb(error)) => {
4336                // The LazyFree state is already visible even though detached
4337                // owners remain quarantined for the outstanding shootdown.
4338                lifecycle::request_lazy_free_reclaim();
4339                Err(error)
4340            }
4341            Err(CommitMutationError::Unpublished(error)) => {
4342                for (_, _, _, page) in candidates.iter().rev() {
4343                    if !page.clear_lazy_free() {
4344                        self.mutation_gate.mark_needs_repair();
4345                        return Err(StarryError::BadState);
4346                    }
4347                }
4348                let pt = &mut self.pt;
4349                let _stripes = self.pte_domain.lock_range(range);
4350                for &(va, paddr, flags, _) in &candidates {
4351                    if flags.contains(MappingFlags::WRITE)
4352                        && pt.remap_page(va, paddr, flags).is_err()
4353                    {
4354                        self.mutation_gate.mark_needs_repair();
4355                        return Err(StarryError::BadState);
4356                    }
4357                }
4358                Err(error)
4359            }
4360        }
4361    }
4362
4363    /// Reclaims up to `limit` exclusive anonymous pages previously marked by
4364    /// `MADV_FREE`.  Each detached PTE uses the ordinary discard receipt, so
4365    /// the PageObject cannot reach `Retired` until the active-CPU TLB request
4366    /// has completed and its reverse mapping is gone.
4367    pub(crate) fn reclaim_lazy_free_pages(&mut self, limit: usize) -> StarryResult<usize> {
4368        if limit == 0 {
4369            return Ok(0);
4370        }
4371        let mut reclaimed = 0;
4372        let mut cursor = None;
4373        while reclaimed < limit {
4374            let eligible = |(key, slot): (&MappingSlotKey, &Arc<MappingSlot>)| {
4375                (slot.page_order == PageOrder::BASE
4376                    && slot.page.state() == PageState::LazyFree
4377                    && slot.page.mapping_refs() == 1)
4378                    .then(|| (*key, slot.page.clone()))
4379            };
4380            let candidate = if let Some(after) = cursor {
4381                self.mapping_slots
4382                    .range((
4383                        core::ops::Bound::Excluded(after),
4384                        core::ops::Bound::Unbounded,
4385                    ))
4386                    .find_map(eligible)
4387            } else {
4388                self.mapping_slots.iter().find_map(eligible)
4389            };
4390            let Some((key, page)) = candidate else {
4391                break;
4392            };
4393            cursor = Some(key);
4394            let Some(entry) = self.vma_root.lookup_entry(key.va) else {
4395                return Err(StarryError::BadState);
4396            };
4397            if !entry.operation().is_private_anonymous() {
4398                return Err(StarryError::BadState);
4399            }
4400            let still_reclaimable = self.mapping_slots.get(&key).is_some_and(|slot| {
4401                Arc::ptr_eq(&slot.page, &page)
4402                    && page.state() == PageState::LazyFree
4403                    && page.mapping_refs() == 1
4404            });
4405            if !still_reclaimable {
4406                continue;
4407            }
4408            self.discard_range(key.va, PAGE_SIZE_4K)?;
4409            if !page.rmap.is_empty()
4410                || page.mapping_refs() != 0
4411                || !page.transition(PageState::LazyFree, PageState::Retired)
4412            {
4413                self.mutation_gate.mark_needs_repair();
4414                return Err(StarryError::BadState);
4415            }
4416            reclaimed += 1;
4417        }
4418        Ok(reclaimed)
4419    }
4420
4421    /// Removes mappings within the specified virtual address range.
4422    ///
4423    /// Returns an error if the address range is out of the address space or not
4424    /// aligned.
4425    pub fn unmap(&mut self, start: VirtAddr, size: usize) -> StarryResult {
4426        self.unmap_classified(start, size)?.into_result()
4427    }
4428
4429    /// Outcome-aware counterpart to [`Self::unmap`].  The returned pending
4430    /// error means that the VMA/PTE/rmap removal is already published; callers
4431    /// must not restore the old mapping or skip their corresponding ownership
4432    /// index update.
4433    pub(crate) fn unmap_outcome(
4434        &mut self,
4435        start: VirtAddr,
4436        size: usize,
4437    ) -> StarryResult<AddressSpaceMutationOutcome> {
4438        self.unmap_classified(start, size)
4439    }
4440
4441    fn unmap_classified(
4442        &mut self,
4443        start: VirtAddr,
4444        size: usize,
4445    ) -> StarryResult<AddressSpaceMutationOutcome> {
4446        self.validate_region(start, size)?;
4447        let range = VirtAddrRange::from_start_size(start, size);
4448        let before_vmas = self.vma_root.len();
4449        // Memfd's writable-map counter is a side-band fact derived from the
4450        // VMA tree.  Prepare its delta while the old metadata is still
4451        // visible, but publish it only after the VMA/PTE transaction below.
4452        let memfd_deltas = crate::syscall::memfd_prepare_aspace_unmap_deltas(self, start, size);
4453        let mut mutation = self.prepare_mutation_range(start, size);
4454        let retire_epoch = mutation
4455            .receipt()
4456            .base_epoch
4457            .checked_next()
4458            .ok_or(StarryError::BadState)?;
4459        mutation
4460            .try_reserve_tlb_ranges(2)
4461            .map_err(|_| StarryError::NoMemory)?;
4462        let splits = self.apply_partial_huge_splits(range)?;
4463        for index in 0..splits.len() {
4464            let split = &splits[index];
4465            let Some(tlb_range) =
4466                TlbRange::new(split.installed.block_vaddr(), split.installed.block_size())
4467            else {
4468                return self
4469                    .abort_unpublished_huge_splits(splits, StarryError::BadState)
4470                    .map(|()| AddressSpaceMutationOutcome::Complete);
4471            };
4472            mutation.add_tlb_range(tlb_range);
4473        }
4474        let preimage = match self.capture_mapping_preimage(range) {
4475            Ok(preimage) => preimage,
4476            Err(error) => {
4477                return self
4478                    .abort_unpublished_huge_splits(splits, error)
4479                    .map(|()| AddressSpaceMutationOutcome::Complete);
4480            }
4481        };
4482        let retired_owners = match self.prepare_retired_mapping_owners(range) {
4483            Ok(owners) => owners,
4484            Err(error) => {
4485                return self
4486                    .abort_unpublished_huge_splits(splits, error)
4487                    .map(|()| AddressSpaceMutationOutcome::Complete);
4488            }
4489        };
4490        let Ok((detached_slots, detached_resident_pages, detached_resident)) =
4491            self.mapping_slot_summary(range)
4492        else {
4493            return self
4494                .abort_unpublished_huge_splits(splits, StarryError::BadState)
4495                .map(|()| AddressSpaceMutationOutcome::Complete);
4496        };
4497        let split_slots = splits.iter().try_fold(0usize, |slots, split| {
4498            slots.checked_add(split.child_slots.len().saturating_sub(1))
4499        });
4500        let Some(split_slots) = split_slots else {
4501            return self
4502                .abort_unpublished_huge_splits(splits, StarryError::BadState)
4503                .map(|()| AddressSpaceMutationOutcome::Complete);
4504        };
4505
4506        // Compute the actual mapped bytes being removed (unmap is already O(n)).
4507        let end = start.checked_add(size).ok_or(StarryError::InvalidInput)?;
4508        let removed_pages: u64 = self
4509            .vma_root
4510            .iter_entries()
4511            .filter(|entry| entry.start() < end && entry.end() > start)
4512            .map(|entry| {
4513                let lo = entry.start().max(start);
4514                let hi = entry.end().min(end);
4515                ((hi - lo) / PAGE_SIZE_4K) as u64
4516            })
4517            .sum();
4518
4519        let deferred_tlb = DeferredTlbRetireGuard::enter();
4520        if let Err(error) = self.apply_unmap_unpublished(range) {
4521            drop(deferred_tlb);
4522            if let Err(flush_error) = crate::mm::flush_tlb_range_sync(start, size) {
4523                warn!("unmap repair could not invalidate {start:?}+{size:#x}: {flush_error}");
4524            }
4525            return self
4526                .abort_unpublished_split_mapping_mutation(range, preimage, None, splits, error)
4527                .map(|()| AddressSpaceMutationOutcome::Complete);
4528        }
4529        drop(deferred_tlb);
4530        if let Err(error) = self.detach_mapping_slots(range) {
4531            return self
4532                .abort_unpublished_split_mapping_mutation(range, preimage, None, splits, error)
4533                .map(|()| AddressSpaceMutationOutcome::Complete);
4534        }
4535        self.park_retired_mapping_owners(retire_epoch, retired_owners);
4536        mutation.set_pte_delta(PteDelta {
4537            unmapped: u32::try_from(detached_resident_pages).unwrap_or(u32::MAX),
4538            ..PteDelta::default()
4539        });
4540        mutation.set_mapping_delta(MappingDelta {
4541            attached: u32::try_from(split_slots).unwrap_or(u32::MAX),
4542            detached: u32::try_from(detached_slots).unwrap_or(u32::MAX),
4543        });
4544        mutation.set_resident_delta(detached_resident.checked_negated_delta()?);
4545        self.vm_stat.on_unmap(removed_pages);
4546        let after_vmas = self.vma_root.len();
4547        mutation.set_vma_delta(VmaDelta {
4548            removed: u32::try_from(before_vmas.saturating_sub(after_vmas)).unwrap_or(u32::MAX),
4549            split: u32::try_from(after_vmas.saturating_sub(before_vmas)).unwrap_or(u32::MAX),
4550            ..VmaDelta::default()
4551        });
4552        match self.commit_mutation_classified(mutation) {
4553            Ok(()) => {
4554                self.release_retired_mapping_owners(retire_epoch);
4555                crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
4556                Ok(AddressSpaceMutationOutcome::Complete)
4557            }
4558            Err(CommitMutationError::PublishedPendingTlb(error)) => {
4559                crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
4560                Ok(AddressSpaceMutationOutcome::PublishedPendingTlb(error))
4561            }
4562            Err(CommitMutationError::Unpublished(error)) => self
4563                .abort_unpublished_split_mapping_mutation(
4564                    range,
4565                    preimage,
4566                    Some(retire_epoch),
4567                    splits,
4568                    error,
4569                )
4570                .map(|()| AddressSpaceMutationOutcome::Complete),
4571        }
4572    }
4573
4574    fn prepare_moved_slots(
4575        &mut self,
4576        moved_pages: &[MovedPage],
4577        target_mapping: MappingId,
4578    ) -> StarryResult<Vec<PreparedMovedSlot>> {
4579        let mut prepared = Vec::new();
4580        prepared
4581            .try_reserve(moved_pages.len())
4582            .map_err(|_| StarryError::NoMemory)?;
4583
4584        for moved in moved_pages {
4585            let source_key = MappingSlotKey {
4586                space_id: self.id,
4587                va: moved.src_va,
4588            };
4589            let target_slot_va = match moved.destination {
4590                MovedPageDestination::SourceOwner => moved.dst_va,
4591                MovedPageDestination::TargetOwner { slot_va } => slot_va,
4592            };
4593            let target_key = MappingSlotKey {
4594                space_id: self.id,
4595                va: target_slot_va,
4596            };
4597            if matches!(moved.destination, MovedPageDestination::SourceOwner)
4598                && prepared.iter().any(|entry| {
4599                    matches!(
4600                        entry,
4601                        PreparedMovedSlot::Relocate {
4602                            target_key: existing,
4603                            ..
4604                        } if *existing == target_key
4605                    )
4606                })
4607            {
4608                return Err(StarryError::BadState);
4609            }
4610            let source = self
4611                .mapping_slots
4612                .get(&source_key)
4613                .cloned()
4614                .ok_or(StarryError::BadState)?;
4615            let page_order = moved
4616                .page_size
4617                .trailing_zeros()
4618                .checked_sub(PAGE_SIZE_4K.trailing_zeros())
4619                .and_then(|order| u8::try_from(order).ok())
4620                .map(PageOrder::new)
4621                .ok_or(StarryError::BadState)?;
4622            let frame_start = source.page.frame().paddr().as_usize();
4623            let frame_end = frame_start
4624                .checked_add(source.page.frame().size())
4625                .ok_or(StarryError::BadState)?;
4626            let leaf_start = moved.paddr.as_usize();
4627            let leaf_end = leaf_start
4628                .checked_add(moved.page_size)
4629                .ok_or(StarryError::BadState)?;
4630            if source.state() != SlotState::Present
4631                || source.mm_id != self.id
4632                || source.va != moved.src_va
4633                || source.page_order != page_order
4634                || source.mapped_paddr() != Some(moved.paddr)
4635                || leaf_start < frame_start
4636                || leaf_end > frame_end
4637                || (page_order != PageOrder::BASE && !source.has_huge_split_deposit())
4638            {
4639                return Err(StarryError::BadState);
4640            }
4641
4642            match moved.destination {
4643                MovedPageDestination::SourceOwner => {
4644                    let replacement = MappingSlot::new_with_frame_offset(
4645                        target_mapping,
4646                        self.id,
4647                        moved.dst_va,
4648                        page_order,
4649                        source.page.clone(),
4650                        source.frame_offset(),
4651                        source.resident_kind(),
4652                    )
4653                    .ok_or(StarryError::BadState)?;
4654                    let replacement = if page_order == PageOrder::BASE {
4655                        replacement
4656                    } else {
4657                        replacement
4658                            .attach_huge_split_deposit(self.pt.prepare_huge_split(moved.dst_va)?)
4659                            .map_err(|_| StarryError::BadState)?
4660                    };
4661                    prepared.push(PreparedMovedSlot::Relocate {
4662                        source_key,
4663                        target_key,
4664                        source,
4665                        replacement: Arc::new(replacement),
4666                    });
4667                }
4668                MovedPageDestination::TargetOwner { .. } => {
4669                    prepared.push(PreparedMovedSlot::DetachSource {
4670                        source_key,
4671                        target_key,
4672                        source,
4673                    });
4674                }
4675            }
4676        }
4677        Ok(prepared)
4678    }
4679
4680    fn publish_moved_slots(&mut self, prepared: Vec<PreparedMovedSlot>) -> StarryResult {
4681        for entry in prepared {
4682            match entry {
4683                PreparedMovedSlot::Relocate {
4684                    source_key,
4685                    target_key,
4686                    source,
4687                    replacement,
4688                } => {
4689                    if self.mapping_slots.contains_key(&target_key) {
4690                        return Err(StarryError::BadState);
4691                    }
4692                    let removed = self
4693                        .mapping_slots
4694                        .remove(&source_key)
4695                        .ok_or(StarryError::BadState)?;
4696                    if !Arc::ptr_eq(&removed, &source) {
4697                        self.mapping_slots.insert(source_key, removed);
4698                        return Err(StarryError::BadState);
4699                    }
4700                    if let Err(error) = source.relocate_to(&replacement) {
4701                        let restored = source.state() == SlotState::Present
4702                            && self
4703                                .mapping_slots
4704                                .insert(source_key, source.clone())
4705                                .is_none();
4706                        if !restored || error == MappingGraphError::RollbackFailed {
4707                            self.mutation_gate.mark_needs_repair();
4708                        }
4709                        return Err(match error {
4710                            MappingGraphError::ResourceExhausted => StarryError::NoMemory,
4711                            _ => StarryError::BadState,
4712                        });
4713                    }
4714                    if self.mapping_slots.insert(target_key, replacement).is_some() {
4715                        // `&mut self` and the pre-insertion check make this
4716                        // unreachable unless the map itself was already corrupt.
4717                        self.mutation_gate.mark_needs_repair();
4718                        return Err(StarryError::BadState);
4719                    }
4720                }
4721                PreparedMovedSlot::DetachSource {
4722                    source_key,
4723                    target_key,
4724                    source,
4725                } => {
4726                    let target = self
4727                        .mapping_slots
4728                        .get(&target_key)
4729                        .ok_or(StarryError::BadState)?;
4730                    if target.state() != SlotState::Present {
4731                        return Err(StarryError::BadState);
4732                    }
4733                    let removed = self
4734                        .mapping_slots
4735                        .remove(&source_key)
4736                        .ok_or(StarryError::BadState)?;
4737                    if !Arc::ptr_eq(&removed, &source) || !removed.detach() {
4738                        self.mapping_slots.insert(source_key, removed);
4739                        return Err(StarryError::BadState);
4740                    }
4741                }
4742            }
4743        }
4744        Ok(())
4745    }
4746
4747    /// Applies only the materialized PTE portion of a relocation. Metadata,
4748    /// MappingSlot publication and the epoch receipt belong to the outer
4749    /// transaction. Pages already materialized at `dst` (shared backends) are
4750    /// kept, while an empty destination receives the exact source PTE owner.
4751    fn apply_move_pages(
4752        &mut self,
4753        src: VirtAddr,
4754        dst: VirtAddr,
4755        size: usize,
4756    ) -> StarryResult<Vec<MovedPage>> {
4757        let move_range =
4758            VirtAddrRange::try_from_start_size(src, size).ok_or(StarryError::InvalidInput)?;
4759        let dst_range =
4760            VirtAddrRange::try_from_start_size(dst, size).ok_or(StarryError::InvalidInput)?;
4761        if move_range.overlaps(dst_range) {
4762            // The low-level mover walks source leaves while writing the
4763            // destination.  Overlapping intervals would make a later source
4764            // lookup observe an already moved leaf (and can duplicate or
4765            // destroy data), so callers must first perform the explicit
4766            // overlap-aware mremap preparation.
4767            return Err(StarryError::InvalidInput);
4768        }
4769        self.validate_materialized_leaf_boundaries(src, size)?;
4770        let source_slots = self.materialized_slots_overlapping(&[move_range])?;
4771        let mut mapped_pages = alloc::vec::Vec::new();
4772        mapped_pages
4773            .try_reserve(source_slots.len())
4774            .map_err(|_| StarryError::NoMemory)?;
4775        for (key, slot, occupied_leaf) in source_slots {
4776            let offset = key.va.checked_sub_addr(src).ok_or(StarryError::BadState)?;
4777            let dst_va = dst.checked_add(offset).ok_or(StarryError::InvalidInput)?;
4778            let paddr = occupied_leaf.paddr;
4779            let flags = occupied_leaf.flags;
4780            let page_size = occupied_leaf.range.size();
4781            let expected_size = PAGE_SIZE_4K
4782                .checked_shl(slot.page_order.get().into())
4783                .ok_or(StarryError::BadState)?;
4784            if slot.state() != SlotState::Present
4785                || slot.va != key.va
4786                || slot.mm_id != self.id
4787                || slot.mapped_paddr() != Some(paddr)
4788                || page_size != expected_size
4789            {
4790                return Err(StarryError::BadState);
4791            }
4792            if !key.va.is_aligned(page_size) || !dst_va.is_aligned(page_size) {
4793                return Err(StarryError::OperationNotSupported);
4794            }
4795            mapped_pages.push((key.va, dst_va, paddr, flags, page_size));
4796        }
4797
4798        let mut moved_pages = alloc::vec::Vec::new();
4799        moved_pages
4800            .try_reserve(mapped_pages.len())
4801            .map_err(|_| StarryError::NoMemory)?;
4802        let mut move_plans = Vec::<PageTableMovePlan>::new();
4803        move_plans
4804            .try_reserve(mapped_pages.len())
4805            .map_err(|_| StarryError::NoMemory)?;
4806
4807        // Linux allocates destination PTE/PMD directories before taking the
4808        // leaf PTL.  Publish the same kind of empty structural deposit here:
4809        // allocation and loser destruction happen outside every IRQ-saving
4810        // page-table lock, and no materialized mapping is visible yet.
4811        for &(_, dst_va, _, _, page_size) in &mapped_pages {
4812            match self.pt.query_occupied(dst_va) {
4813                Ok(_) => {}
4814                Err(PagingError::NotMapped) => {
4815                    let plan = self.pt.plan_map_page(dst_va, page_size)?;
4816                    if let Some(deposit) = plan.prepare_path()? {
4817                        let apply_result = {
4818                            let _structure = self.pte_domain.lock_structure();
4819                            self.pt.try_install_map_path(deposit)
4820                        };
4821                        if let Err(failure) = apply_result {
4822                            let (error, deposit) = failure.into_parts();
4823                            // The deposit owns page-table frames.  It is
4824                            // intentionally destroyed after the IRQ-saving
4825                            // structure guard above has gone out of scope.
4826                            drop(deposit);
4827                            return Err(error.into());
4828                        }
4829                    }
4830                }
4831                Err(error) => return Err(error.into()),
4832            }
4833        }
4834
4835        for &(src_va, dst_va, paddr, flags, page_size) in &mapped_pages {
4836            let plan = self.pt.plan_move_page(src_va, dst_va)?;
4837            if plan.source_vaddr() != src_va
4838                || plan.destination_vaddr() != dst_va
4839                || plan.paddr() != paddr
4840                || plan.config() != flags
4841                || plan.page_size() != page_size
4842            {
4843                return Err(StarryError::BadState);
4844            }
4845            let destination = if plan.destination_is_occupied() {
4846                let target_size = plan.destination_page_size();
4847                if target_size < PAGE_SIZE_4K || !target_size.is_power_of_two() {
4848                    return Err(StarryError::BadState);
4849                }
4850                MovedPageDestination::TargetOwner {
4851                    slot_va: dst_va.align_down(target_size),
4852                }
4853            } else {
4854                MovedPageDestination::SourceOwner
4855            };
4856            moved_pages.push(MovedPage {
4857                src_va,
4858                dst_va,
4859                paddr,
4860                page_size,
4861                destination,
4862            });
4863            move_plans.push(plan);
4864        }
4865
4866        // Every fallible allocation and all path publication completed before
4867        // these IRQ-saving guards.  The generic batch first revalidates every
4868        // source/destination preimage and only then performs infallible leaf
4869        // stores, so rollback never allocates or waits for an IPI under PTL.
4870        let pte_domain = &self.pte_domain;
4871        let cursor = &mut self.pt;
4872        let apply_result = {
4873            let _structure = pte_domain.lock_structure();
4874            let pte_stripes = pte_domain.lock_ranges(&[move_range, dst_range]);
4875            debug_assert!(!pte_stripes.stripe_indices().is_empty());
4876            cursor.try_move_pages_with(&move_plans)
4877        };
4878        let applied = apply_result?;
4879        if applied != moved_pages.len() {
4880            self.mutation_gate.mark_needs_repair();
4881            return Err(StarryError::BadState);
4882        }
4883        Ok(moved_pages)
4884    }
4885
4886    /// Relocates one complete logical mapping under a single epoch receipt.
4887    /// The target VMA, source metadata, PTEs, rmap slots, RSS and memfd side
4888    /// bands are prepared from one preimage and become visible together.
4889    #[allow(clippy::too_many_arguments)]
4890    pub(crate) fn mremap_move_transaction(
4891        &mut self,
4892        src: VirtAddr,
4893        src_size: usize,
4894        target: VirtAddr,
4895        target_size: usize,
4896        permissions: MappingPermissions,
4897        target_backend: MappingOperation,
4898        huge_page_advice: HugePageAdvice,
4899        lock_mode: VmaLockMode,
4900        advice_policy: VmaAdvicePolicy,
4901        dontunmap: bool,
4902        replace_target: bool,
4903        memlock_limit: Option<MemlockLimit>,
4904    ) -> StarryResult {
4905        self.validate_region(src, src_size)?;
4906        self.validate_region(target, target_size)?;
4907        if !permissions.maximum.contains(permissions.current) {
4908            return Err(StarryError::PermissionDenied);
4909        }
4910        let source_range =
4911            VirtAddrRange::try_from_start_size(src, src_size).ok_or(StarryError::InvalidInput)?;
4912        let target_range = VirtAddrRange::try_from_start_size(target, target_size)
4913            .ok_or(StarryError::InvalidInput)?;
4914        if source_range.overlaps(target_range) {
4915            return Err(StarryError::InvalidInput);
4916        }
4917        let move_size = src_size.min(target_size);
4918        let moved_source_range =
4919            VirtAddrRange::try_from_start_size(src, move_size).ok_or(StarryError::InvalidInput)?;
4920
4921        let source_page_size = self
4922            .vma_root
4923            .lookup_entry(src)
4924            .map(|entry| entry.operation().page_size())
4925            .ok_or(StarryError::BadAddress)?;
4926        let source_replacement =
4927            dontunmap.then(|| MappingOperation::new_alloc(src, source_page_size, ""));
4928        let rollback_ranges = [source_range, target_range];
4929        let tail_range = if src_size > move_size {
4930            Some(
4931                VirtAddrRange::try_from_start_size(
4932                    src.checked_add(move_size)
4933                        .ok_or(StarryError::InvalidInput)?,
4934                    src_size - move_size,
4935                )
4936                .ok_or(StarryError::InvalidInput)?,
4937            )
4938        } else {
4939            None
4940        };
4941
4942        let target_removed_pages = if replace_target {
4943            self.vma_root
4944                .iter_entries()
4945                .filter(|entry| {
4946                    entry.start() < target_range.end && entry.end() > target_range.start
4947                })
4948                .try_fold(0u64, |pages, entry| {
4949                    let lo = entry.start().max(target_range.start);
4950                    let hi = entry.end().min(target_range.end);
4951                    let bytes = hi.checked_sub_addr(lo).ok_or(StarryError::BadState)?;
4952                    pages
4953                        .checked_add((bytes / PAGE_SIZE_4K) as u64)
4954                        .ok_or(StarryError::InvalidInput)
4955                })?
4956        } else {
4957            0
4958        };
4959        // Allocate and validate the complete target/source VMA successor
4960        // before any huge split or PTE mutation. This is the metadata prepare
4961        // phase of the mremap receipt.
4962        let target_successor = self.prepare_mapping_successor(
4963            target_range,
4964            permissions,
4965            &target_backend,
4966            huge_page_advice,
4967            lock_mode,
4968            advice_policy,
4969            replace_target,
4970        )?;
4971        let mut final_successor = target_successor.clone();
4972        if dontunmap {
4973            final_successor = final_successor
4974                .without_range(moved_source_range)
4975                .ok_or(StarryError::BadState)?;
4976            let replacement = source_replacement.as_ref().ok_or(StarryError::BadState)?;
4977            let replacement_entry = final_successor
4978                .prepare_mapping_entry(
4979                    moved_source_range,
4980                    permissions.current,
4981                    permissions.reported,
4982                    permissions.maximum,
4983                    huge_page_advice,
4984                    // Linux keeps the copied target locked but always clears
4985                    // VM_LOCKED and VM_LOCKONFAULT on the source VMA after a
4986                    // successful MREMAP_DONTUNMAP move.
4987                    VmaLockMode::Unlocked,
4988                    advice_policy,
4989                    replacement.clone(),
4990                )
4991                .ok_or(StarryError::BadState)?;
4992            final_successor = final_successor
4993                .with_mapping_entry(replacement_entry, false)
4994                .ok_or(StarryError::BadState)?;
4995        } else {
4996            if let Some(tail) = tail_range {
4997                final_successor = final_successor
4998                    .without_range(tail)
4999                    .ok_or(StarryError::BadState)?;
5000            }
5001            final_successor = final_successor
5002                .without_range(moved_source_range)
5003                .ok_or(StarryError::BadState)?;
5004        }
5005        self.validate_memlock_successor(&final_successor, memlock_limit)?;
5006        let before_vmas = self.vma_root.len();
5007        let graph_preimage = self.capture_mapping_graph_snapshot(&rollback_ranges)?;
5008
5009        let mut mutation = self.prepare_mutation_range(src, src_size);
5010        mutation
5011            .try_reserve_tlb_ranges(5)
5012            .map_err(|error| match error {
5013                MutationError::ResourceExhausted => StarryError::NoMemory,
5014                _ => StarryError::BadState,
5015            })?;
5016        mutation
5017            .add_tlb_range(TlbRange::new(target, target_size).ok_or(StarryError::InvalidInput)?);
5018        let retire_epoch = mutation
5019            .receipt()
5020            .base_epoch
5021            .checked_next()
5022            .ok_or(StarryError::BadState)?;
5023
5024        // Linux moves a complete PMD leaf directly, but splits a PMD when
5025        // either the moved source extent or the replacement target cuts only
5026        // part of it.  Consume every deposited table before capturing the
5027        // rollback preimage, and bind the full PMD invalidations to this same
5028        // mremap receipt.
5029        let split_ranges = [moved_source_range, target_range];
5030        let splits = self.apply_partial_huge_splits_for_ranges(&split_ranges)?;
5031        for index in 0..splits.len() {
5032            let split = &splits[index];
5033            let Some(tlb_range) =
5034                TlbRange::new(split.installed.block_vaddr(), split.installed.block_size())
5035            else {
5036                return self.abort_unpublished_huge_splits(splits, StarryError::BadState);
5037            };
5038            mutation.add_tlb_range(tlb_range);
5039        }
5040
5041        let preimage = match self.capture_mapping_preimage_ranges(&rollback_ranges) {
5042            Ok(preimage) => preimage,
5043            Err(error) => return self.abort_unpublished_huge_splits(splits, error),
5044        };
5045        let target_owners = match replace_target
5046            .then(|| self.prepare_retired_mapping_owners(target_range))
5047            .transpose()
5048        {
5049            Ok(owners) => owners,
5050            Err(error) => {
5051                return self.abort_unpublished_split_mapping_mutation_ranges(
5052                    &rollback_ranges,
5053                    preimage,
5054                    None,
5055                    splits,
5056                    error,
5057                );
5058            }
5059        };
5060        let tail_owners = match tail_range
5061            .map(|range| self.prepare_retired_mapping_owners(range))
5062            .transpose()
5063        {
5064            Ok(owners) => owners,
5065            Err(error) => {
5066                return self.abort_unpublished_split_mapping_mutation_ranges(
5067                    &rollback_ranges,
5068                    preimage,
5069                    None,
5070                    splits,
5071                    error,
5072                );
5073            }
5074        };
5075
5076        let mut memfd_deltas = crate::syscall::memfd_prepare_aspace_replace_deltas(
5077            self,
5078            target,
5079            target_size,
5080            permissions.current,
5081            &target_backend,
5082        );
5083        if dontunmap {
5084            if let Some(replacement) = source_replacement.as_ref() {
5085                memfd_deltas.extend(crate::syscall::memfd_prepare_aspace_replace_deltas(
5086                    self,
5087                    src,
5088                    move_size,
5089                    permissions.current,
5090                    replacement,
5091                ));
5092            }
5093        } else {
5094            memfd_deltas.extend(crate::syscall::memfd_prepare_aspace_unmap_deltas(
5095                self, src, src_size,
5096            ));
5097        }
5098
5099        let apply_result = (|| -> StarryResult<usize> {
5100            let target_materialization = self.apply_mapping_pages_unpublished(
5101                target_range,
5102                permissions,
5103                &target_backend,
5104                replace_target,
5105            )?;
5106            self.vma_root = Arc::new(target_successor.clone());
5107
5108            let moved_pages = self.apply_move_pages(src, target, move_size)?;
5109            let prepared_moved_slots =
5110                self.prepare_moved_slots(&moved_pages, target_backend.mapping_id())?;
5111            if !dontunmap && let Some(tail) = tail_range {
5112                self.apply_unmap_pages_unpublished(tail)?;
5113            }
5114
5115            if replace_target {
5116                self.detach_mapping_slots(target_range)?;
5117            }
5118            self.publish_prepared_pte_owners(
5119                &target_backend,
5120                target_range,
5121                &target_materialization,
5122            )?;
5123            self.publish_moved_slots(prepared_moved_slots)?;
5124            if !dontunmap && let Some(tail) = tail_range {
5125                self.detach_mapping_slots(tail)?;
5126            }
5127            if self
5128                .mapping_slots_overlapping(moved_source_range)
5129                .next()
5130                .is_some()
5131            {
5132                return Err(StarryError::BadState);
5133            }
5134            self.vma_root = Arc::new(final_successor.clone());
5135
5136            self.vm_stat.on_map((target_size / PAGE_SIZE_4K) as u64);
5137            if target_removed_pages != 0 {
5138                self.vm_stat.on_unmap(target_removed_pages);
5139            }
5140            if !dontunmap {
5141                self.vm_stat.on_unmap((src_size / PAGE_SIZE_4K) as u64);
5142            }
5143            Ok(moved_pages.len())
5144        })();
5145
5146        let moved_leaves = match apply_result {
5147            Ok(moved) => moved,
5148            Err(error) => {
5149                return self.abort_unpublished_split_mapping_mutation_ranges(
5150                    &rollback_ranges,
5151                    preimage,
5152                    None,
5153                    splits,
5154                    error,
5155                );
5156            }
5157        };
5158        if let Some(owners) = target_owners {
5159            self.park_retired_mapping_owners(retire_epoch, owners);
5160        }
5161        if let Some(owners) = tail_owners {
5162            self.park_retired_mapping_owners(retire_epoch, owners);
5163        }
5164
5165        let after_vmas = self.vma_root.len();
5166        mutation.set_vma_delta(VmaDelta {
5167            inserted: u32::try_from(after_vmas.saturating_sub(before_vmas)).unwrap_or(u32::MAX),
5168            removed: u32::try_from(before_vmas.saturating_sub(after_vmas)).unwrap_or(u32::MAX),
5169            ..VmaDelta::default()
5170        });
5171        mutation.set_pte_delta(PteDelta {
5172            mapped: u32::try_from(moved_leaves).unwrap_or(u32::MAX),
5173            unmapped: u32::try_from(moved_leaves).unwrap_or(u32::MAX),
5174            ..PteDelta::default()
5175        });
5176        if let Err(error) =
5177            self.set_mapping_graph_receipt_delta(&mut mutation, &graph_preimage, &rollback_ranges)
5178        {
5179            return self.abort_unpublished_split_mapping_mutation_ranges(
5180                &rollback_ranges,
5181                preimage,
5182                Some(retire_epoch),
5183                splits,
5184                error,
5185            );
5186        }
5187
5188        match self.commit_mutation_classified(mutation) {
5189            Ok(()) => {
5190                self.release_retired_mapping_owners(retire_epoch);
5191                crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
5192                Ok(())
5193            }
5194            Err(CommitMutationError::PublishedPendingTlb(error)) => {
5195                crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
5196                Err(error)
5197            }
5198            Err(CommitMutationError::Unpublished(error)) => self
5199                .abort_unpublished_split_mapping_mutation_ranges(
5200                    &rollback_ranges,
5201                    preimage,
5202                    Some(retire_epoch),
5203                    splits,
5204                    error,
5205                ),
5206        }
5207    }
5208
5209    /// Grows the mapping containing `addr` by `additional_size` at its end.
5210    pub fn extend_area(&mut self, addr: VirtAddr, additional_size: usize) -> StarryResult {
5211        self.extend_area_with_memlock(addr, additional_size, None)
5212    }
5213
5214    pub(crate) fn extend_area_with_memlock(
5215        &mut self,
5216        addr: VirtAddr,
5217        additional_size: usize,
5218        memlock_limit: Option<MemlockLimit>,
5219    ) -> StarryResult {
5220        if additional_size == 0 {
5221            return Ok(());
5222        }
5223        let entry = self
5224            .vma_root
5225            .lookup_entry(addr)
5226            .ok_or(StarryError::InvalidInput)?;
5227        if !additional_size.is_multiple_of(PAGE_SIZE_4K) {
5228            return Err(StarryError::InvalidInput);
5229        }
5230        let old_end = entry.end();
5231        let grown = VirtAddrRange::try_from_start_size(old_end, additional_size)
5232            .ok_or(StarryError::InvalidInput)?;
5233        let preimage = self.capture_mapping_preimage(grown)?;
5234        let graph_preimage = self.capture_mapping_graph_snapshot(&[grown])?;
5235        let mut mutation = self.prepare_mutation_range(old_end, additional_size);
5236        if entry
5237            .end()
5238            .checked_add(additional_size)
5239            .is_none_or(|new_end| new_end > self.end())
5240        {
5241            return Err(StarryError::NoMemory);
5242        }
5243        let (materialized_range, operation, materialization) =
5244            match self.apply_extend_unpublished(addr, additional_size, memlock_limit) {
5245                Ok(applied) => applied,
5246                Err(error) => {
5247                    return self.abort_unpublished_mapping_mutation(grown, preimage, error);
5248                }
5249            };
5250        if materialized_range != grown {
5251            return self.abort_unpublished_mapping_mutation(grown, preimage, StarryError::BadState);
5252        }
5253        self.vm_stat.on_map((additional_size / PAGE_SIZE_4K) as u64);
5254        if let Err(error) = self.publish_prepared_pte_owners(&operation, grown, &materialization) {
5255            return self.abort_unpublished_mapping_mutation(grown, preimage, error);
5256        }
5257        if let Err(error) =
5258            self.set_mapping_graph_receipt_delta(&mut mutation, &graph_preimage, &[grown])
5259        {
5260            return self.abort_unpublished_mapping_mutation(grown, preimage, error);
5261        }
5262        match self.commit_mutation_classified(mutation) {
5263            Ok(()) => Ok(()),
5264            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
5265            Err(CommitMutationError::Unpublished(error)) => {
5266                self.abort_unpublished_mapping_mutation(grown, preimage, error)
5267            }
5268        }
5269    }
5270
5271    /// To process data in this area with the given function.
5272    ///
5273    /// Now it supports reading and writing data in the given interval.
5274    fn process_area_data<F>(&self, start: VirtAddr, size: usize, mut f: F) -> StarryResult
5275    where
5276        F: FnMut(VirtAddr, usize, usize),
5277    {
5278        if size == 0 {
5279            return Ok(());
5280        }
5281        if !self.contains_range(start, size) {
5282            return Err(StarryError::InvalidInput);
5283        }
5284        let end = start.checked_add(size).ok_or(StarryError::InvalidInput)?;
5285        // Aligning with the low-level helper can wrap at `usize::MAX`; use a
5286        // checked addition so user-copy never turns an invalid end into a
5287        // low address.
5288        let end_align_up = end
5289            .as_usize()
5290            .checked_add(PAGE_SIZE_4K - 1)
5291            .map(|value| VirtAddr::from_usize(value & !(PAGE_SIZE_4K - 1)))
5292            .ok_or(StarryError::InvalidInput)?;
5293        let page_start = start.align_down_4k();
5294        let pages = PageIter4K::new(page_start, end_align_up).ok_or(StarryError::InvalidInput)?;
5295        let mut copied = 0usize;
5296        for vaddr in pages {
5297            let (paddr, ..) = self.pt.query(vaddr).map_err(|_| StarryError::BadAddress)?;
5298            let page_offset = if vaddr == page_start {
5299                start.align_offset_4k()
5300            } else {
5301                0
5302            };
5303            let copy_size = (PAGE_SIZE_4K - page_offset).min(size - copied);
5304            if copy_size == 0 {
5305                break;
5306            }
5307            let paddr = paddr
5308                .checked_add(page_offset)
5309                .ok_or(StarryError::BadAddress)?;
5310            f(phys_to_virt(paddr), copied, copy_size);
5311            copied = copied
5312                .checked_add(copy_size)
5313                .ok_or(StarryError::InvalidInput)?;
5314        }
5315        (copied == size)
5316            .then_some(())
5317            .ok_or(StarryError::BadAddress)
5318    }
5319
5320    pub fn read(&self, start: VirtAddr, buf: &mut [u8]) -> StarryResult {
5321        self.process_area_data(start, buf.len(), |src, offset, read_size| unsafe {
5322            core::ptr::copy_nonoverlapping(src.as_ptr(), buf.as_mut_ptr().add(offset), read_size);
5323        })
5324    }
5325
5326    /// To write data to the address space.
5327    ///
5328    /// # Arguments
5329    ///
5330    /// * `start_vaddr` - The start virtual address to write.
5331    /// * `buf` - The buffer to write to the address space.
5332    pub fn write(&self, start: VirtAddr, buf: &[u8]) -> StarryResult {
5333        self.process_area_data(start, buf.len(), |dst, offset, write_size| unsafe {
5334            core::ptr::copy_nonoverlapping(buf.as_ptr().add(offset), dst.as_mut_ptr(), write_size);
5335        })
5336    }
5337
5338    /// Synchronizes instruction fetch after modifying executable memory through this address space.
5339    pub fn sync_modified_text(&self, start: VirtAddr, size: usize) -> StarryResult {
5340        if size == 0 {
5341            return Ok(());
5342        }
5343
5344        self.process_area_data(start, size, |dst, _offset, sync_size| {
5345            let range = ax_cpu::cache::CacheRange::new(dst, sync_size)
5346                .expect("mapped text chunk must not wrap");
5347            // SAFETY: process_area_data retains the translated backing mapping
5348            // for this chunk until the callback completes its cache maintenance.
5349            unsafe { ax_cpu::cache::clean_dcache_range_to_pou(range) };
5350        })?;
5351        ax_cpu::cache::flush_icache_all();
5352        Ok(())
5353    }
5354
5355    /// Updates mapping within the specified virtual address range.
5356    ///
5357    /// Returns an error if the address range is out of the address space or not
5358    /// aligned.
5359    pub fn protect(&mut self, start: VirtAddr, size: usize, flags: MappingFlags) -> StarryResult {
5360        self.protect_with_reported_flags(start, size, flags, flags)
5361    }
5362
5363    pub fn protect_with_reported_flags(
5364        &mut self,
5365        start: VirtAddr,
5366        size: usize,
5367        flags: MappingFlags,
5368        reported_flags: MappingFlags,
5369    ) -> StarryResult {
5370        self.validate_region(start, size)?;
5371        let range = VirtAddrRange::from_start_size(start, size);
5372
5373        // Validate against the immutable permission envelope before touching
5374        // any PTE or VMA fragment.  This is intentionally done here (rather
5375        // than in one backend) because a range can span several fragments and
5376        // the envelope belongs to the VMA record itself.
5377        start.checked_add(size).ok_or(StarryError::InvalidInput)?;
5378        // The bounds check reads only the VMAs the range covers. Walking the
5379        // whole map and breaking out of it saved nothing: `iter_entries` builds
5380        // the entire vector before the loop gets to look at the first entry.
5381        let mut denied = false;
5382        self.vma_root.for_each_overlapping_entry(range, |entry| {
5383            if entry.max_rights().contains(flags) {
5384                return true;
5385            }
5386            denied = true;
5387            false
5388        });
5389        if denied {
5390            return Err(StarryError::PermissionDenied);
5391        }
5392
5393        let vma_preimage = self.vma_root.clone();
5394        let vm_stat_preimage = self.vm_stat.snapshot();
5395        let before_vmas = self.vma_root.len();
5396        let mut mutation = self.prepare_mutation_range(start, size);
5397        mutation
5398            .try_reserve_tlb_ranges(2)
5399            .map_err(|_| StarryError::NoMemory)?;
5400        let splits = self.apply_partial_huge_splits(range)?;
5401        for split in &splits {
5402            let Some(tlb_range) =
5403                TlbRange::new(split.installed.block_vaddr(), split.installed.block_size())
5404            else {
5405                return self.abort_unpublished_protection(
5406                    vma_preimage,
5407                    vm_stat_preimage,
5408                    &[],
5409                    splits,
5410                    StarryError::BadState,
5411                );
5412            };
5413            mutation.add_tlb_range(tlb_range);
5414        }
5415        let protection_preimage = match self.capture_protection_leaf_preimage(range) {
5416            Ok(preimage) => preimage,
5417            Err(error) => {
5418                return self.abort_unpublished_protection(
5419                    vma_preimage,
5420                    vm_stat_preimage,
5421                    &[],
5422                    splits,
5423                    error,
5424                );
5425            }
5426        };
5427        let protected_leaves = protection_preimage.len();
5428        mutation.set_pte_delta(PteDelta {
5429            protected: u32::try_from(protected_leaves).unwrap_or(u32::MAX),
5430            ..PteDelta::default()
5431        });
5432        let split_slots = splits
5433            .iter()
5434            .map(|split| split.child_slots.len().saturating_sub(1))
5435            .sum::<usize>();
5436        mutation.set_mapping_delta(MappingDelta {
5437            attached: u32::try_from(split_slots).unwrap_or(u32::MAX),
5438            ..MappingDelta::default()
5439        });
5440        let touched_memfds =
5441            crate::syscall::memfd_collect_metas_touching_mprotect_range(self, start, size);
5442        if flags.contains(MappingFlags::EXECUTE) {
5443            for leaf in &protection_preimage {
5444                let key = MappingSlotKey {
5445                    space_id: self.id,
5446                    va: leaf.va,
5447                };
5448                let slot = self
5449                    .mapping_slots
5450                    .get(&key)
5451                    .expect("published executable leaf must retain its mapping owner");
5452                slot.page.prepare_executable_mapping(
5453                    leaf.paddr,
5454                    leaf.page_size,
5455                    flags | (leaf.flags & (MappingFlags::DEVICE | MappingFlags::UNCACHED)),
5456                );
5457            }
5458        }
5459        if let Err(error) = self.apply_protection_unpublished(range, flags, reported_flags) {
5460            return self.abort_unpublished_protection(
5461                vma_preimage,
5462                vm_stat_preimage,
5463                &protection_preimage,
5464                splits,
5465                error,
5466            );
5467        }
5468        let after_vmas = self.vma_root.len();
5469        mutation.set_vma_delta(VmaDelta {
5470            split: u32::try_from(after_vmas.saturating_sub(before_vmas)).unwrap_or(u32::MAX),
5471            merged: u32::try_from(before_vmas.saturating_sub(after_vmas)).unwrap_or(u32::MAX),
5472            ..VmaDelta::default()
5473        });
5474
5475        match self.commit_mutation_classified(mutation) {
5476            Ok(()) => {
5477                crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(
5478                    self,
5479                    &touched_memfds,
5480                );
5481                Ok(())
5482            }
5483            Err(CommitMutationError::PublishedPendingTlb(error)) => {
5484                // Publication is already externally visible; side-band
5485                // accounting follows it even while old frame ownership stays
5486                // quarantined behind the outstanding TLB receipt.
5487                crate::syscall::memfd_resync_shared_writable_counts_after_mprotect(
5488                    self,
5489                    &touched_memfds,
5490                );
5491                Err(error)
5492            }
5493            Err(CommitMutationError::Unpublished(error)) => self.abort_unpublished_protection(
5494                vma_preimage,
5495                vm_stat_preimage,
5496                &protection_preimage,
5497                splits,
5498                error,
5499            ),
5500        }
5501    }
5502
5503    fn ensure_quiescent_for_content_clear(&self) -> StarryResult {
5504        if self.tlb_targets.load(core::sync::atomic::Ordering::Acquire) != 0
5505            || self.mutation_gate.pending_count() != 0
5506            || self.pending_retired_mapping_batches() != 0
5507        {
5508            return Err(StarryError::ResourceBusy);
5509        }
5510        Ok(())
5511    }
5512
5513    /// Removes every user mapping after the caller has proved that this page
5514    /// table cannot be installed on a CPU.  This is the shared apply step for
5515    /// unpublished-image abort and retired-MM reclaim; it deliberately does
5516    /// not publish an epoch or side-band event by itself.
5517    fn clear_quiescent_contents(&mut self) -> StarryResult {
5518        self.ensure_quiescent_for_content_clear()?;
5519        let range = self.layout.range();
5520        let operations = self.mapping_operation_fragments(range, false)?;
5521        if operations
5522            .iter()
5523            .any(|(fragment, operation)| !operation.validate_unmap_range(*fragment, &self.pt))
5524        {
5525            return Err(StarryError::BadState);
5526        }
5527
5528        let deferred_tlb = DeferredTlbRetireGuard::enter();
5529        // A retired MM has no users, pins, activations, pending receipts or
5530        // page-table walkers.  An unpublished loader image is likewise held by
5531        // one `&mut AddrSpace`.  Linux uses the same isolation proof to run
5532        // `free_pgtables()` without a PTL after VMAs have been detached.  Do
5533        // not acquire the IRQ-saving structure lock here: backend validation,
5534        // occupied-leaf vectors, page-table frame release and Arc destruction
5535        // are all allowed to allocate or enter the allocator's reclaim path.
5536        let clear_result = operations
5537            .into_iter()
5538            .try_for_each(|(fragment, operation)| operation.unmap_range(fragment, &mut self.pt));
5539        drop(deferred_tlb);
5540        if let Err(error) = clear_result {
5541            if let Err(flush_error) = crate::mm::flush_tlb_range_sync(range.start, range.size()) {
5542                warn!(
5543                    "quiescent address-space clear could not invalidate {:?}+{:#x}: {flush_error}",
5544                    range.start,
5545                    range.size()
5546                );
5547            }
5548            self.mutation_gate.mark_needs_repair();
5549            return Err(error);
5550        }
5551        let slots = core::mem::take(&mut self.mapping_slots);
5552        for slot in slots.into_values() {
5553            slot.detach();
5554        }
5555        self.resident_watermark.reset();
5556        self.vm_stat.on_clear();
5557        self.vma_root = Arc::new(VmaMap::default());
5558        self.executable_file = None;
5559        // Once every materialized and software owner is empty, a prior repair
5560        // bit belonging solely to this unpublished/retired image is resolved.
5561        self.mutation_gate.clear_repair();
5562        Ok(())
5563    }
5564
5565    /// Aborts an address-space image that has never been registered in
5566    /// [`MmHandle`] or installed by the scheduler.
5567    ///
5568    /// Linux drops `bprm->mm` through `mmput()` before `begin_new_exec`; it does
5569    /// not publish an externally visible VMA mutation for a failed ELF or
5570    /// interpreter attempt.  Starry keeps the allocated root so the loader may
5571    /// reuse its borrowed kernel entries, but the discard has the same
5572    /// unpublished semantics: no [`MutationReceipt`] and no epoch advance.
5573    pub(crate) fn reset_uninstalled_for_loader(&mut self) -> StarryResult {
5574        self.ensure_quiescent_for_content_clear()?;
5575        let range = self.layout.range();
5576        let memfd_deltas =
5577            crate::syscall::memfd_prepare_aspace_unmap_deltas(self, range.start, range.size());
5578        self.clear_quiescent_contents()?;
5579        self.resident_pages = ResidentPageCounts::default();
5580        self.heap = HeapState::new(USER_HEAP_BASE);
5581        self.executable_data = ExecutableDataLayout::default();
5582        crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
5583        Ok(())
5584    }
5585
5586    /// Clears a retired, formerly published MM and records that teardown in
5587    /// the ordinary mutation protocol before page-table frames are detached.
5588    fn clear_retired_contents(&mut self) -> StarryResult {
5589        self.ensure_quiescent_for_content_clear()?;
5590        let base_epoch = self.vm_epoch();
5591        base_epoch.checked_next().ok_or(StarryError::BadState)?;
5592        let range = self.layout.range();
5593        let removed_vmas = self.vma_root.len();
5594        let detached_slots = self.mapping_slots.len();
5595        let materialized_pages = self
5596            .mapping_slots
5597            .values()
5598            .try_fold(0usize, |pages, slot| {
5599                pages.checked_add(1usize.checked_shl(slot.page_order.get().into())?)
5600            })
5601            .ok_or(StarryError::BadState)?;
5602        let memfd_deltas =
5603            crate::syscall::memfd_prepare_aspace_unmap_deltas(self, range.start, range.size());
5604        let mut mutation = self.prepare_mutation_range(range.start, range.size());
5605        mutation.set_vma_delta(VmaDelta {
5606            removed: u32::try_from(removed_vmas).unwrap_or(u32::MAX),
5607            ..VmaDelta::default()
5608        });
5609        mutation.set_pte_delta(PteDelta {
5610            unmapped: u32::try_from(materialized_pages).unwrap_or(u32::MAX),
5611            ..PteDelta::default()
5612        });
5613        mutation.set_mapping_delta(MappingDelta {
5614            detached: u32::try_from(detached_slots).unwrap_or(u32::MAX),
5615            ..MappingDelta::default()
5616        });
5617        mutation.set_resident_delta(self.resident_pages.checked_negated_delta()?);
5618        self.clear_quiescent_contents()?;
5619        let result = self.commit_mutation(mutation);
5620        if self.vm_epoch() != base_epoch {
5621            crate::syscall::memfd_apply_shared_writable_deltas(&memfd_deltas);
5622        }
5623        result
5624    }
5625
5626    /// Reclaims all mappings after lifecycle quiescence.
5627    pub(crate) fn try_reclaim_contents(&mut self) -> StarryResult {
5628        // A retired permit is only valid after every CPU has switched away
5629        // and every earlier shootdown receipt has been acknowledged.  Keep
5630        // this check in the owning address-space object as a second line of
5631        // defence: callers must not be able to clear a root merely because an
5632        // `Arc` happened to be the last strong reference.
5633        if self.tlb_targets.load(core::sync::atomic::Ordering::Acquire) != 0
5634            || self.mutation_gate.pending_count() != 0
5635            || self.pending_retired_mapping_batches() != 0
5636        {
5637            return Err(StarryError::ResourceBusy);
5638        }
5639        self.clear_retired_contents()?;
5640        let epoch = self.vm_epoch();
5641
5642        // Detach page-table frames from the materialized tree before allocator
5643        // release. Lifecycle quiescence is the zero-target form of Linux's
5644        // mmu-gather contract: no CPU can still walk this root, so the typed
5645        // allocator capability may be consumed immediately. Published
5646        // mutations with remote observers take the ordinary TLB quarantine
5647        // path before an MM can reach Retired.
5648        let targets = self.tlb_targets.load(core::sync::atomic::Ordering::Acquire);
5649        let request = TlbRequest::new(self.id, epoch, targets);
5650        debug_assert!(request.is_complete());
5651        // SAFETY: lifecycle only calls this after all user/kernel references
5652        // and scheduler activations are quiescent.  `PageTable::detach` leaves
5653        // the owning table inert and transfers each frame to a token. The
5654        // completed zero-target request proves that consuming each token in
5655        // the callback cannot race an architectural page-table walk.
5656        unsafe {
5657            self.pt.detach(|token| token.reclaim());
5658        }
5659        Ok(())
5660    }
5661
5662    /// Checks whether an access to the specified memory region is valid.
5663    ///
5664    /// Returns `true` if the memory region given by `range` is all mapped and
5665    /// has proper permission flags (i.e. containing `access_flags`).
5666    pub fn can_access_range(
5667        &self,
5668        start: VirtAddr,
5669        size: usize,
5670        access_flags: MappingFlags,
5671    ) -> bool {
5672        let Some(range) = VirtAddrRange::try_from_start_size(start, size) else {
5673            return false;
5674        };
5675        if range.is_empty() {
5676            return false;
5677        }
5678        // This sits under every user-pointer check, so it must not allocate or
5679        // touch a reference count per VMA: an ordinary `clock_gettime` writing
5680        // one `timespec` was flattening the whole VMA tree.
5681        let mut cursor = range.start;
5682        let mut permitted = false;
5683        self.vma_root.for_each_overlapping(range, |vma| {
5684            if vma.range.end <= cursor {
5685                return true;
5686            }
5687            if vma.range.start > cursor || !vma.rights.contains(access_flags) {
5688                return false;
5689            }
5690            cursor = vma.range.end.min(range.end);
5691            if cursor >= range.end {
5692                permitted = true;
5693                return false;
5694            }
5695            true
5696        });
5697        permitted
5698    }
5699
5700    /// Chooses the materialized leaf size for one fault without changing the
5701    /// MappingGroup's long-term THP policy.
5702    ///
5703    /// A missing PTE inside a split PMD must be faulted as one base page.  It
5704    /// is safe to retry the policy-sized mapping only when the complete policy
5705    /// unit belongs to this VMA and no sibling PTE is still installed.  This
5706    /// is the same distinction Linux makes between a none PMD eligible for a
5707    /// new THP and a deposited PTE table containing a split folio.
5708    fn fault_transaction_page_size(
5709        &self,
5710        vaddr: VirtAddr,
5711        vma_range: VirtAddrRange,
5712        policy_size: usize,
5713    ) -> StarryResult<usize> {
5714        match self.pt.query(vaddr) {
5715            Ok((_, _, leaf_size)) => return Ok(leaf_size),
5716            Err(PagingError::NotMapped) => {}
5717            Err(error) => return Err(error.into()),
5718        }
5719        if policy_size == PAGE_SIZE_4K {
5720            return Ok(PAGE_SIZE_4K);
5721        }
5722        if policy_size < PAGE_SIZE_4K
5723            || !policy_size.is_power_of_two()
5724            || !policy_size.is_multiple_of(PAGE_SIZE_4K)
5725        {
5726            return Err(StarryError::BadState);
5727        }
5728
5729        let policy_start = vaddr.align_down(policy_size);
5730        let policy_range = VirtAddrRange::try_from_start_size(policy_start, policy_size)
5731            .ok_or(StarryError::BadState)?;
5732        if !vma_range.contains_range(policy_range) {
5733            return Ok(PAGE_SIZE_4K);
5734        }
5735
5736        // Any occupied sibling proves that this policy unit already owns a
5737        // base-page table. Replacing it with a huge leaf would overwrite live
5738        // or retained mappings and their rmap ownership. Follow allocated
5739        // page-table paths instead of issuing 512 base-page queries.
5740        if self
5741            .pt
5742            .walk_occupied_range(policy_range.start, policy_range.end)
5743            .next()
5744            .is_some()
5745        {
5746            return Ok(PAGE_SIZE_4K);
5747        }
5748        Ok(policy_size)
5749    }
5750
5751    fn plan_page_fault(
5752        &self,
5753        vaddr: VirtAddr,
5754        access_flags: PageFaultFlags,
5755        thp_mode: TransparentHugePageMode,
5756    ) -> Result<PageFaultPlan, FaultResult> {
5757        if self.mutation_gate.needs_repair() {
5758            // An indeterminate publication retains its owners in quarantine.
5759            // Neither a present PTE nor another allocation proves it usable.
5760            return Err(FaultResult::Sigbus(BusCode::ObjErr));
5761        }
5762        if !self.layout.range().contains(vaddr) {
5763            return Err(FaultResult::Unmapped);
5764        }
5765        let access_flags = MappingFlags::from(access_flags);
5766        let Some(entry) = self.vma_root.lookup_entry(vaddr) else {
5767            return Err(FaultResult::Unmapped);
5768        };
5769        let vma = entry.snapshot().clone();
5770        let flags = vma.rights;
5771        if !flags.contains(access_flags) {
5772            return Err(FaultResult::PermissionDenied);
5773        }
5774        let backend = entry.operation_clone();
5775        let Some(policy_size) = vma
5776            .group
5777            .page_policy
5778            .fault_leaf_size(vma.huge_page_advice, thp_mode)
5779        else {
5780            return Err(FaultResult::Unmapped);
5781        };
5782        let page_size = match self.fault_transaction_page_size(vaddr, vma.range, policy_size) {
5783            Ok(page_size) => page_size,
5784            Err(error) => {
5785                warn!("could not classify page-fault leaf for {vaddr:?}: {error}");
5786                return Err(FaultResult::Retry);
5787            }
5788        };
5789        let page_start = vaddr.align_down(page_size);
5790        let Some(range) = VirtAddrRange::try_from_start_size(page_start, page_size) else {
5791            return Err(FaultResult::Unmapped);
5792        };
5793        let fault_fallback = if vma.group.page_policy.permits_fault_fallback() {
5794            FaultFallback::BasePage
5795        } else {
5796            FaultFallback::Forbidden
5797        };
5798        let request = match PopulateRequest::fault(range, page_size, vaddr, fault_fallback) {
5799            Ok(request) => request,
5800            Err(_) => return Err(FaultResult::Unmapped),
5801        };
5802        let preimage = match FaultPteSnapshot::capture(&self.pt, page_start) {
5803            Ok(preimage) => preimage,
5804            Err(error) => {
5805                warn!("could not capture page-fault PTE at {page_start:?}: {error}");
5806                return Err(FaultResult::Retry);
5807            }
5808        };
5809        let map_plans = if preimage == FaultPteSnapshot::NotMapped {
5810            let preferred = match self.pt.plan_map_page(page_start, page_size) {
5811                Ok(plan) => plan,
5812                Err(error) => {
5813                    warn!("could not plan page-table path for {page_start:?}: {error}");
5814                    return Err(FaultResult::Retry);
5815                }
5816            };
5817            let fallback = if page_size > PAGE_SIZE_4K && fault_fallback == FaultFallback::BasePage
5818            {
5819                let fallback_start = vaddr.align_down_4k();
5820                match self.pt.plan_map_page(fallback_start, PAGE_SIZE_4K) {
5821                    Ok(plan) => Some(plan),
5822                    Err(error) => {
5823                        warn!(
5824                            "could not plan fallback page-table path for {fallback_start:?}: \
5825                             {error}"
5826                        );
5827                        return Err(FaultResult::Retry);
5828                    }
5829                }
5830            } else {
5831                None
5832            };
5833            Some(PageFaultMapPlans {
5834                preferred,
5835                fallback,
5836            })
5837        } else {
5838            None
5839        };
5840        Ok(PageFaultPlan {
5841            base_epoch: self.vm_epoch(),
5842            space_id: self.id,
5843            vaddr,
5844            range,
5845            vma_flags: flags,
5846            access_flags,
5847            operation: backend,
5848            request,
5849            preimage,
5850            map_plans,
5851        })
5852    }
5853
5854    fn classify_fault_error(file_backed: bool, error: StarryError) -> FaultResult {
5855        if matches!(
5856            error,
5857            StarryError::NoMemory
5858                | StarryError::Paging(PagingError::NoMemory)
5859                | StarryError::Vfs(axfs_ng_vfs::VfsError::NoMemory)
5860        ) {
5861            return FaultResult::NoMemory;
5862        }
5863        if matches!(error, StarryError::ResourceBusy) {
5864            return FaultResult::Retry;
5865        }
5866        if !file_backed {
5867            return FaultResult::Unmapped;
5868        }
5869        match error {
5870            StarryError::ResourceBusy | StarryError::Vfs(axfs_ng_vfs::VfsError::ResourceBusy) => {
5871                FaultResult::Retry
5872            }
5873            StarryError::BadAddress => FaultResult::Sigbus(BusCode::AdrErr),
5874            StarryError::Io | StarryError::Vfs(_) => FaultResult::Sigbus(BusCode::ObjErr),
5875            _ => FaultResult::Unmapped,
5876        }
5877    }
5878
5879    fn prepare_fault_materialization(
5880        plan: &PageFaultPlan,
5881        request: PopulateRequest,
5882    ) -> Result<FaultMaterialization, FaultResult> {
5883        match plan.operation.prepare_fault(
5884            plan.space_id,
5885            request,
5886            plan.vma_flags,
5887            plan.access_flags,
5888            plan.preimage,
5889        ) {
5890            Ok(materialization) => Ok(materialization),
5891            Err(error) => {
5892                warn!(
5893                    "failed to prepare page fault for {:?} ({:?}): {error}",
5894                    plan.vaddr, plan.vma_flags
5895                );
5896                Err(Self::classify_fault_error(
5897                    plan.operation.is_file_backed(),
5898                    error,
5899                ))
5900            }
5901        }
5902    }
5903
5904    fn cancel_fault_materialization(
5905        plan: &PageFaultPlan,
5906        materialization: FaultMaterialization,
5907    ) -> Result<(), FaultResult> {
5908        plan.operation
5909            .cancel_prepared_fault_publication(materialization)
5910            .map_err(|error| {
5911                warn!(
5912                    "failed to cancel prepared page fault for {:?}: {error}",
5913                    plan.vaddr
5914                );
5915                FaultResult::Retry
5916            })
5917    }
5918
5919    fn prepare_page_fault(mut plan: PageFaultPlan) -> Result<PreparedPageFault, FaultResult> {
5920        let mut materialization = Self::prepare_fault_materialization(&plan, plan.request)?;
5921        let installed_owner = materialization.owner().and_then(|owner| {
5922            (owner.transition == PteOwnerTransition::Installed).then_some((
5923                owner.va,
5924                owner.paddr,
5925                owner.page_size,
5926            ))
5927        });
5928        let map_deposit = if let Some((owner_va, owner_paddr, owner_page_size)) = installed_owner {
5929            let Some(plans) = plan.map_plans.take() else {
5930                Self::cancel_fault_materialization(&plan, materialization)?;
5931                return Err(FaultResult::Retry);
5932            };
5933            let PageFaultMapPlans {
5934                preferred,
5935                mut fallback,
5936            } = plans;
5937            let preferred_selected =
5938                preferred.vaddr() == owner_va && preferred.page_size() == owner_page_size;
5939            let fallback_selected = fallback.as_ref().is_some_and(|fallback| {
5940                fallback.vaddr() == owner_va && fallback.page_size() == owner_page_size
5941            });
5942            if !preferred_selected && !fallback_selected {
5943                Self::cancel_fault_materialization(&plan, materialization)?;
5944                return Err(FaultResult::Retry);
5945            }
5946            let Some(flags) = materialization.pte_flags() else {
5947                Self::cancel_fault_materialization(&plan, materialization)?;
5948                return Err(FaultResult::Retry);
5949            };
5950
5951            if fallback_selected {
5952                let Some(fallback_request) = plan.request.into_base_page_fallback() else {
5953                    Self::cancel_fault_materialization(&plan, materialization)?;
5954                    return Err(FaultResult::Retry);
5955                };
5956                plan.request = fallback_request;
5957                plan.range = fallback_request.range();
5958                let Some(fallback) = fallback.take() else {
5959                    Self::cancel_fault_materialization(&plan, materialization)?;
5960                    return Err(FaultResult::Retry);
5961                };
5962                match fallback.prepare(owner_paddr, flags) {
5963                    Ok(deposit) => Some(deposit),
5964                    Err(error) => {
5965                        warn!(
5966                            "could not prepare fallback page-table path for {owner_va:?}: {error}"
5967                        );
5968                        Self::cancel_fault_materialization(&plan, materialization)?;
5969                        return Err(Self::classify_fault_error(false, error.into()));
5970                    }
5971                }
5972            } else {
5973                match preferred.prepare(owner_paddr, flags) {
5974                    Ok(deposit) => Some(deposit),
5975                    Err(PagingError::NoMemory) if fallback.is_some() => {
5976                        // Releasing the huge PageObject first can make enough
5977                        // memory available for the base page plus its deeper
5978                        // table path. This matches Linux's preallocate, recheck,
5979                        // and retry boundary without allocating under the PTL.
5980                        Self::cancel_fault_materialization(&plan, materialization)?;
5981                        let Some(fallback_request) = plan.request.into_base_page_fallback() else {
5982                            return Err(FaultResult::Retry);
5983                        };
5984                        plan.request = fallback_request;
5985                        plan.range = fallback_request.range();
5986                        materialization =
5987                            Self::prepare_fault_materialization(&plan, fallback_request)?;
5988                        let Some(owner) = materialization.owner() else {
5989                            Self::cancel_fault_materialization(&plan, materialization)?;
5990                            return Err(FaultResult::Retry);
5991                        };
5992                        let Some(fallback) = fallback.take() else {
5993                            Self::cancel_fault_materialization(&plan, materialization)?;
5994                            return Err(FaultResult::Retry);
5995                        };
5996                        if owner.transition != PteOwnerTransition::Installed
5997                            || owner.va != fallback.vaddr()
5998                            || owner.page_size != fallback.page_size()
5999                        {
6000                            Self::cancel_fault_materialization(&plan, materialization)?;
6001                            return Err(FaultResult::Retry);
6002                        }
6003                        let Some(flags) = materialization.pte_flags() else {
6004                            Self::cancel_fault_materialization(&plan, materialization)?;
6005                            return Err(FaultResult::Retry);
6006                        };
6007                        match fallback.prepare(owner.paddr, flags) {
6008                            Ok(deposit) => Some(deposit),
6009                            Err(error) => {
6010                                warn!(
6011                                    "could not prepare base-page table path for {:?}: {error}",
6012                                    owner.va
6013                                );
6014                                Self::cancel_fault_materialization(&plan, materialization)?;
6015                                return Err(Self::classify_fault_error(false, error.into()));
6016                            }
6017                        }
6018                    }
6019                    Err(error) => {
6020                        warn!("could not prepare page-table path for {owner_va:?}: {error}");
6021                        Self::cancel_fault_materialization(&plan, materialization)?;
6022                        return Err(Self::classify_fault_error(false, error.into()));
6023                    }
6024                }
6025            }
6026        } else {
6027            None
6028        };
6029        Ok(PreparedPageFault {
6030            plan,
6031            materialization,
6032            map_deposit,
6033        })
6034    }
6035
6036    fn page_fault_plan_is_current(&self, plan: &PageFaultPlan) -> bool {
6037        if self.vm_epoch() != plan.base_epoch || !plan.preimage.matches(plan.range.start, &self.pt)
6038        {
6039            return false;
6040        }
6041        self.vma_root.lookup_entry(plan.vaddr).is_some_and(|entry| {
6042            entry.snapshot().rights == plan.vma_flags
6043                && entry.snapshot().range.contains_range(plan.range)
6044                && entry.operation().mapping_id() == plan.operation.mapping_id()
6045        })
6046    }
6047
6048    fn apply_prepared_page_fault(
6049        &mut self,
6050        attempt: &mut PageFaultApplyAttempt,
6051    ) -> PageFaultApplyOutcome {
6052        if !self.page_fault_plan_is_current(&attempt.prepared().plan) {
6053            return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6054        }
6055
6056        let (pages, file_backed, vaddr, vma_flags, range, access_flags, fault_preimage) = {
6057            let prepared = attempt.prepared();
6058            (
6059                prepared.materialization.satisfied_pages(),
6060                prepared.plan.operation.is_file_backed(),
6061                prepared.plan.vaddr,
6062                prepared.plan.vma_flags,
6063                prepared.plan.range,
6064                prepared.plan.access_flags,
6065                prepared.plan.preimage,
6066            )
6067        };
6068        if pages == 0 {
6069            let result = if file_backed {
6070                FaultResult::Sigbus(BusCode::AdrErr)
6071            } else {
6072                warn!("no pages prepared for {vaddr:?} ({vma_flags:?})");
6073                FaultResult::Unmapped
6074            };
6075            return PageFaultApplyOutcome::Cancel(result);
6076        }
6077        if attempt.prepared().materialization.owner().is_none() {
6078            return PageFaultApplyOutcome::Cancel(FaultResult::Handled);
6079        }
6080        let (owner_va, owner_paddr, owner_page_size, owner_transition, desired_flags) = {
6081            let prepared = attempt.prepared();
6082            let owner = prepared
6083                .materialization
6084                .owner()
6085                .expect("checked fault owner must remain present");
6086            let Some(desired_flags) = prepared.materialization.pte_flags() else {
6087                return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6088            };
6089            (
6090                owner.va,
6091                owner.paddr,
6092                owner.page_size,
6093                owner.transition,
6094                desired_flags,
6095            )
6096        };
6097        let mapping_preimage = match self.capture_mapping_preimage(range) {
6098            Ok(preimage) => preimage,
6099            Err(error) => {
6100                warn!("could not retain page-fault preimage for {vaddr:?}: {error}");
6101                return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6102            }
6103        };
6104        let replaces_owner = attempt
6105            .prepared()
6106            .materialization
6107            .owner()
6108            .is_some_and(|owner| owner.transition == PteOwnerTransition::Replaced);
6109        let retired_owners = if replaces_owner {
6110            match self.prepare_retired_mapping_owners(range) {
6111                Ok(owners) => Some(owners),
6112                Err(error) => {
6113                    warn!("could not reserve page-fault retire owners for {vaddr:?}: {error}");
6114                    return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6115                }
6116            }
6117        } else {
6118            None
6119        };
6120        let lazy_free_page = access_flags
6121            .contains(MappingFlags::WRITE)
6122            .then(|| {
6123                self.mapping_slots
6124                    .get(&MappingSlotKey {
6125                        space_id: self.id,
6126                        va: owner_va,
6127                    })
6128                    .filter(|slot| slot.page.state() == PageState::LazyFree)
6129                    .map(|slot| slot.page.clone())
6130            })
6131            .flatten();
6132        let fresh_install = matches!(fault_preimage, FaultPteSnapshot::NotMapped)
6133            && attempt
6134                .prepared()
6135                .materialization
6136                .owner()
6137                .is_some_and(|owner| owner.transition == PteOwnerTransition::Installed);
6138        let mut mutation = if fresh_install {
6139            self.prepare_fresh_pte_mutation_range(range.start, range.size())
6140        } else {
6141            self.prepare_mutation_range(range.start, range.size())
6142        };
6143        // A software-empty PTE can still have an older cached translation
6144        // after a failed discard shootdown. Check before touching the PTE or
6145        // publishing its new owner; commit repeats the non-cancellable check.
6146        // The outer MM mutex excludes new publishers until this apply ends.
6147        if let Some(request) = self.mutation_gate.pending_overlap_request(&mutation) {
6148            return PageFaultApplyOutcome::CancelPendingTlb {
6149                request,
6150                targets: self.tlb_targets(),
6151            };
6152        }
6153        let Some(retire_epoch) = mutation.receipt().base_epoch.checked_next() else {
6154            return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6155        };
6156
6157        let mut map_deposit = if owner_transition == PteOwnerTransition::Installed {
6158            match attempt.take_map_deposit() {
6159                Some(deposit) => Some(deposit),
6160                None => return PageFaultApplyOutcome::Cancel(FaultResult::Retry),
6161            }
6162        } else {
6163            None
6164        };
6165        let apply_result = {
6166            let _structure = (owner_transition == PteOwnerTransition::Installed)
6167                .then(|| self.pte_domain.lock_structure());
6168            let _stripe = self.pte_domain.lock_range(range);
6169            let pt = &mut self.pt;
6170            let preimage_matches = fault_preimage.matches(range.start, pt);
6171            let result = if !preimage_matches {
6172                Err(PagingError::stale_map_deposit(owner_va))
6173            } else {
6174                // Linux set_ptes synchronizes executable folios before making
6175                // the PTE visible, including read faults on executable VMAs.
6176                let owner = attempt
6177                    .prepared()
6178                    .materialization
6179                    .owner()
6180                    .expect("prepared fault retains its page until PTE publication");
6181                owner
6182                    .page
6183                    .prepare_executable_mapping(owner_paddr, owner_page_size, desired_flags);
6184                match owner_transition {
6185                    PteOwnerTransition::Installed => {
6186                        let deposit = map_deposit
6187                            .take()
6188                            .expect("fresh page fault must retain its map deposit");
6189                        match pt.try_map_page_with(deposit) {
6190                            Ok(()) => Ok(owner_page_size),
6191                            Err(failure) => {
6192                                let (error, deposit) = failure.into_parts();
6193                                map_deposit = Some(deposit);
6194                                Err(error)
6195                            }
6196                        }
6197                    }
6198                    PteOwnerTransition::Replaced | PteOwnerTransition::Updated => {
6199                        pt.remap_page(owner_va, owner_paddr, desired_flags)
6200                    }
6201                }
6202            };
6203            result.and_then(|installed_size| {
6204                (installed_size == owner_page_size)
6205                    .then_some(installed_size)
6206                    .ok_or(PagingError::NotMapped)
6207            })
6208        };
6209        if let Some(deposit) = map_deposit.take() {
6210            attempt.restore_map_deposit(deposit);
6211        }
6212        if let Err(error) = apply_result {
6213            warn!("could not apply prepared page fault for {vaddr:?}: {error}");
6214            if fault_preimage.matches(range.start, &self.pt) {
6215                return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6216            }
6217            if self
6218                .restore_mapping_preimage(range, mapping_preimage)
6219                .is_ok()
6220            {
6221                return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6222            } else {
6223                self.mutation_gate.mark_needs_repair();
6224            }
6225            return PageFaultApplyOutcome::NeedsRepair(FaultResult::Retry);
6226        }
6227
6228        mutation.set_pte_delta(PteDelta {
6229            mapped: u32::try_from(pages).unwrap_or(u32::MAX),
6230            ..PteDelta::default()
6231        });
6232        let publication = match self.publish_prepared_fault_owner(
6233            &attempt.prepared().plan.operation,
6234            range,
6235            &attempt.prepared().materialization,
6236        ) {
6237            Ok(publication) => publication,
6238            Err(error) => {
6239                warn!("could not publish prepared page owner for {vaddr:?}: {error}");
6240                if self
6241                    .restore_mapping_preimage(range, mapping_preimage)
6242                    .is_err()
6243                {
6244                    self.mutation_gate.mark_needs_repair();
6245                    return PageFaultApplyOutcome::NeedsRepair(FaultResult::Retry);
6246                }
6247                return PageFaultApplyOutcome::Cancel(FaultResult::Retry);
6248            }
6249        };
6250        let PreparedPageFault {
6251            plan: _,
6252            materialization: _,
6253            map_deposit,
6254        } = attempt.take_prepared();
6255        debug_assert!(map_deposit.is_none());
6256        mutation.set_mapping_delta(publication.mapping_delta);
6257        mutation.set_resident_delta(publication.resident_delta);
6258        if let Some(page) = &lazy_free_page
6259            && !page.clear_lazy_free()
6260        {
6261            if self
6262                .restore_mapping_preimage(range, mapping_preimage)
6263                .is_err()
6264            {
6265                self.mutation_gate.mark_needs_repair();
6266            }
6267            return PageFaultApplyOutcome::Complete(FaultResult::Retry);
6268        }
6269        if let Some(owners) = retired_owners {
6270            self.park_retired_mapping_owners(retire_epoch, owners);
6271        }
6272        match self.publish_mutation_classified(mutation) {
6273            Ok(MutationPublication::Complete) => {
6274                self.release_retired_mapping_owners(retire_epoch);
6275                PageFaultApplyOutcome::Complete(FaultResult::Handled)
6276            }
6277            Ok(MutationPublication::PendingTlb) => {
6278                let Some(request) = self.mutation_gate.pending_request(self.id, retire_epoch)
6279                else {
6280                    self.mutation_gate.mark_needs_repair();
6281                    return PageFaultApplyOutcome::Complete(FaultResult::Retry);
6282                };
6283                PageFaultApplyOutcome::PendingTlb {
6284                    request,
6285                    targets: self.tlb_targets(),
6286                }
6287            }
6288            Err(CommitMutationError::Unpublished(error)) => {
6289                warn!("page-fault publication for {vaddr:?} failed before publish: {error}");
6290                if self
6291                    .restore_mapping_preimage(range, mapping_preimage)
6292                    .is_err()
6293                {
6294                    self.mutation_gate.mark_needs_repair();
6295                } else {
6296                    self.release_retired_mapping_owners(retire_epoch);
6297                    if let Some(page) = &lazy_free_page
6298                        && !page.mark_lazy_free()
6299                    {
6300                        self.mutation_gate.mark_needs_repair();
6301                    }
6302                }
6303                PageFaultApplyOutcome::Complete(FaultResult::Retry)
6304            }
6305            Err(CommitMutationError::PublishedPendingTlb(error)) => {
6306                warn!("unexpected synchronous TLB result for page fault {vaddr:?}: {error}");
6307                let Some(request) = self.mutation_gate.pending_request(self.id, retire_epoch)
6308                else {
6309                    self.mutation_gate.mark_needs_repair();
6310                    return PageFaultApplyOutcome::Complete(FaultResult::Retry);
6311                };
6312                PageFaultApplyOutcome::PendingTlb {
6313                    request,
6314                    targets: self.tlb_targets(),
6315                }
6316            }
6317        }
6318    }
6319
6320    /// Test-only synchronous wrapper. Production faults are orchestrated by
6321    /// `MmPin`, which drops the address-space mutex around prepare and TLB IPI.
6322    #[cfg(all(test, axtest))]
6323    fn handle_page_fault_result(
6324        &mut self,
6325        vaddr: VirtAddr,
6326        access_flags: PageFaultFlags,
6327    ) -> FaultResult {
6328        let plan =
6329            match self.plan_page_fault(vaddr, access_flags, TransparentHugePageMode::default()) {
6330                Ok(plan) => plan,
6331                Err(result) => return result,
6332            };
6333        let prepared = match Self::prepare_page_fault(plan) {
6334            Ok(prepared) => prepared,
6335            Err(result) => return result,
6336        };
6337        let mut attempt = prepared.into_apply_attempt();
6338        let result = match self.apply_prepared_page_fault(&mut attempt) {
6339            PageFaultApplyOutcome::Complete(result) => result,
6340            PageFaultApplyOutcome::Cancel(result) => {
6341                if attempt.cancel().is_ok() {
6342                    result
6343                } else {
6344                    FaultResult::Retry
6345                }
6346            }
6347            PageFaultApplyOutcome::NeedsRepair(result) => {
6348                attempt.release_to_repair_state();
6349                result
6350            }
6351            PageFaultApplyOutcome::CancelPendingTlb { request, targets } => {
6352                if attempt.cancel().is_ok()
6353                    && Self::flush_tlb_requests(core::slice::from_ref(&request), &targets).is_ok()
6354                {
6355                    let _ = self.acknowledge_tlb_requests(core::slice::from_ref(&request));
6356                }
6357                FaultResult::Retry
6358            }
6359            PageFaultApplyOutcome::PendingTlb { request, targets } => {
6360                if Self::flush_tlb_requests(core::slice::from_ref(&request), &targets).is_ok()
6361                    && self
6362                        .acknowledge_tlb_requests(core::slice::from_ref(&request))
6363                        .is_ok()
6364                {
6365                    FaultResult::Handled
6366                } else {
6367                    FaultResult::Retry
6368                }
6369            }
6370        };
6371        complete_page_fault_with(
6372            matches!(result, FaultResult::Handled),
6373            vaddr,
6374            ax_cpu::mmu::update_mmu_cache,
6375        );
6376        result
6377    }
6378
6379    /// Captures every resident parent PTE that fork must make read-only.
6380    ///
6381    /// This is a pure prepare phase: all vectors and TLB ranges are reserved
6382    /// before either the parent or child page table is changed. The outer
6383    /// address-space lock keeps the captured leaf identity stable until apply.
6384    fn prepare_fork_parent_mutation(&self) -> StarryResult<Option<PreparedForkParentMutation>> {
6385        let mut mutation = self.prepare_mutation();
6386        let mut ptes = Vec::new();
6387        let mut ranges = Vec::new();
6388
6389        for entry in self.vma_root.iter_entries() {
6390            if entry.snapshot().advice_policy.dont_fork() {
6391                continue;
6392            }
6393            if !entry.operation().requires_fork_write_protect() {
6394                continue;
6395            }
6396            let mut range_changed = false;
6397            for leaf in self.occupied_pte_leaves_overlapping(&[entry.range()])? {
6398                let page_size = leaf.range.size();
6399                if page_size < PAGE_SIZE_4K || !page_size.is_power_of_two() {
6400                    return Err(StarryError::BadState);
6401                }
6402                let protected_flags = leaf.flags - MappingFlags::WRITE;
6403                if protected_flags != leaf.flags {
6404                    ptes.try_reserve(1).map_err(|_| StarryError::NoMemory)?;
6405                    ptes.push(ForkParentPteProtection {
6406                        va: leaf.range.start,
6407                        paddr: leaf.paddr,
6408                        page_size,
6409                        original_flags: leaf.flags,
6410                        protected_flags,
6411                    });
6412                    range_changed = true;
6413                }
6414            }
6415            if range_changed {
6416                ranges.try_reserve(1).map_err(|_| StarryError::NoMemory)?;
6417                ranges.push(entry.range());
6418                mutation
6419                    .try_add_tlb_range(
6420                        TlbRange::new(entry.start(), entry.size())
6421                            .ok_or(StarryError::InvalidInput)?,
6422                    )
6423                    .map_err(|error| match error {
6424                        MutationError::ResourceExhausted => StarryError::NoMemory,
6425                        _ => StarryError::BadState,
6426                    })?;
6427            }
6428        }
6429
6430        if ptes.is_empty() {
6431            return Ok(None);
6432        }
6433        mutation.set_pte_delta(PteDelta {
6434            protected: u32::try_from(ptes.len()).unwrap_or(u32::MAX),
6435            ..PteDelta::default()
6436        });
6437        Ok(Some(PreparedForkParentMutation {
6438            mutation,
6439            ptes,
6440            ranges,
6441        }))
6442    }
6443
6444    fn rollback_fork_parent_ptes(
6445        cursor: &mut PageTable,
6446        applied: &[ForkParentPteProtection],
6447    ) -> bool {
6448        let mut complete = true;
6449        for protection in applied.iter().rev() {
6450            let current_matches =
6451                cursor
6452                    .query(protection.va)
6453                    .is_ok_and(|(paddr, flags, page_size)| {
6454                        paddr == protection.paddr
6455                            && flags == protection.protected_flags
6456                            && page_size == protection.page_size
6457                    });
6458            if !current_matches
6459                || cursor
6460                    .protect_page(protection.va, protection.original_flags)
6461                    .is_err()
6462            {
6463                complete = false;
6464            }
6465        }
6466        complete
6467    }
6468
6469    /// Applies and publishes the parent half of fork after the child is fully
6470    /// prepared but still unreachable by the scheduler.
6471    fn apply_fork_parent_mutation(&mut self, prepared: PreparedForkParentMutation) -> StarryResult {
6472        let PreparedForkParentMutation {
6473            mutation,
6474            ptes,
6475            ranges,
6476        } = prepared;
6477        let pt = &mut self.pt;
6478        let pte_stripes = self.pte_domain.lock_ranges(&ranges);
6479        // The outer `&mut self` excludes other address-space mutations,
6480        // and `pte_stripes` covers every captured parent leaf in ascending
6481        // stripe order.
6482        let cursor = pt;
6483        for (applied, protection) in ptes.iter().enumerate() {
6484            let preimage_matches =
6485                cursor
6486                    .query(protection.va)
6487                    .is_ok_and(|(paddr, flags, page_size)| {
6488                        paddr == protection.paddr
6489                            && flags == protection.original_flags
6490                            && page_size == protection.page_size
6491                    });
6492            if !preimage_matches
6493                || cursor
6494                    .protect_page(protection.va, protection.protected_flags)
6495                    .is_err()
6496            {
6497                if !Self::rollback_fork_parent_ptes(cursor, &ptes[..applied]) {
6498                    self.mutation_gate.mark_needs_repair();
6499                    return Err(StarryError::BadState);
6500                }
6501                return Err(StarryError::BadState);
6502            }
6503        }
6504        drop(pte_stripes);
6505
6506        // Publication freezes the live active-CPU mask and does not return
6507        // success until every CPU that could have cached a writable parent
6508        // translation has acknowledged the shootdown.  A failure before
6509        // publication consumes the retained PTE preimage; a post-publication
6510        // shootdown failure must keep the read-only state visible.
6511        match self.commit_mutation_classified(mutation) {
6512            Ok(()) => Ok(()),
6513            Err(CommitMutationError::PublishedPendingTlb(error)) => Err(error),
6514            Err(CommitMutationError::Unpublished(error)) => {
6515                let pt = &mut self.pt;
6516                let _pte_stripes = self.pte_domain.lock_ranges(&ranges);
6517                // Disjoint field borrows exclude competing address-space
6518                // mutations and the ordered stripe cursor covers every leaf
6519                // whose preimage is restored below.
6520                let restored = Self::rollback_fork_parent_ptes(pt, &ptes);
6521                if restored {
6522                    self.mutation_gate.clear_repair();
6523                    Err(error)
6524                } else {
6525                    self.mutation_gate.mark_needs_repair();
6526                    Err(StarryError::BadState)
6527                }
6528            }
6529        }
6530    }
6531
6532    fn abort_unpublished_clone(child: &mut Self) -> StarryResult {
6533        match child.reset_uninstalled_for_loader() {
6534            Ok(()) => Ok(()),
6535            Err(error)
6536                if child.vma_root.is_empty()
6537                    && child.mapping_slots.is_empty()
6538                    && child.pending_retired_mapping_batches() == 0 =>
6539            {
6540                // A prior child-only bookkeeping failure can leave its gate in
6541                // NeedsRepair, causing the final epoch bump to fail after clear
6542                // already removed every owned mapping. The object is still
6543                // safe to drop because it was never installed or published.
6544                warn!(
6545                    "unpublished fork child cleared all mappings but could not publish cleanup \
6546                     epoch: {error}"
6547                );
6548                Ok(())
6549            }
6550            Err(error) => Err(error),
6551        }
6552    }
6553
6554    /// Attempts to clone the current address space into a new one.
6555    ///
6556    /// This method creates a new empty address space with the same base and
6557    /// size, then iterates over all memory areas in the original address
6558    /// space to copy or share their mappings into the new one.
6559    ///
6560    /// Memfd shared-writable deltas are prepared while the unpublished child
6561    /// is built and applied only after the child's receipt is published.
6562    /// (`CLONE_VM` shares one address space and does not duplicate VMAs here.)
6563    pub fn try_clone(&mut self) -> StarryResult<Arc<Mutex<Self>>> {
6564        // Capture every fallible parent-side allocation and PTE preimage before
6565        // constructing the child. No published parent state changes in this
6566        // phase, so a child preparation failure is a true abort.
6567        let parent_mutation = self.prepare_fork_parent_mutation()?;
6568        let new_aspace = Arc::new(Mutex::new(Self::new_with_layout(self.layout)?));
6569
6570        // The caller holds the source AddrSpace lock while this fresh AddrSpace
6571        // is being populated. The new lock is not published yet, so this is a
6572        // structured source -> cloned-address-space nesting.
6573        let mut guard = new_aspace.lock_nested(CLONED_ADDR_SPACE_LOCK_SUBCLASS);
6574        guard.heap = self.heap;
6575        guard.executable_data = self.executable_data;
6576        guard.executable_file = self.executable_file.clone();
6577        let mut child_memfd_deltas = Vec::new();
6578        let mut child_vss_pages = 0u64;
6579
6580        let child_preparation = (|| -> StarryResult {
6581            let self_modify = &mut self.pt;
6582            for entry in self.vma_root.iter_entries() {
6583                if entry.snapshot().advice_policy.dont_fork() {
6584                    continue;
6585                }
6586                let (new_backend, materialization) = entry.operation().clone_map(
6587                    entry.range(),
6588                    entry.rights(),
6589                    self_modify,
6590                    &mut guard.pt,
6591                )?;
6592                let start = entry.start();
6593                child_memfd_deltas.extend(crate::syscall::memfd_prepare_aspace_replace_deltas(
6594                    &guard,
6595                    start,
6596                    entry.size(),
6597                    entry.rights(),
6598                    &new_backend,
6599                ));
6600
6601                let child_entry = guard
6602                    .vma_root
6603                    .prepare_mapping_entry(
6604                        entry.range(),
6605                        entry.rights(),
6606                        entry.reported_rights(),
6607                        entry.max_rights(),
6608                        entry.snapshot().huge_page_advice,
6609                        VmaLockMode::Unlocked,
6610                        entry.snapshot().advice_policy,
6611                        new_backend.clone(),
6612                    )
6613                    .ok_or(StarryError::BadState)?;
6614                let child_root = guard
6615                    .vma_root
6616                    .with_mapping_entry(child_entry, false)
6617                    .ok_or(StarryError::BadState)?;
6618                guard.vma_root = Arc::new(child_root);
6619                guard.publish_prepared_pte_owners(&new_backend, entry.range(), &materialization)?;
6620                child_vss_pages = child_vss_pages
6621                    .checked_add((entry.size() / PAGE_SIZE_4K) as u64)
6622                    .ok_or(StarryError::BadState)?;
6623            }
6624
6625            // VM_DONTCOPY areas are absent from the child, so derive both
6626            // total_vm and hiwater_vm from the root that was actually built.
6627            guard.vm_stat.seed_clone(child_vss_pages);
6628            Ok(())
6629        })();
6630
6631        if let Err(error) = child_preparation {
6632            if let Err(cleanup_error) = Self::abort_unpublished_clone(&mut guard) {
6633                warn!(
6634                    "fork child preparation failed ({error}); unpublished cleanup also failed \
6635                     ({cleanup_error})"
6636                );
6637                return Err(StarryError::BadState);
6638            }
6639            return Err(error);
6640        }
6641
6642        // Only now may the published parent lose write permission. A failed
6643        // shootdown leaves the parent mutation receipt pending and the child
6644        // unreachable; it is never returned with a stale writable parent TLB.
6645        if let Some(parent_mutation) = parent_mutation
6646            && let Err(error) = self.apply_fork_parent_mutation(parent_mutation)
6647        {
6648            if let Err(cleanup_error) = Self::abort_unpublished_clone(&mut guard) {
6649                warn!(
6650                    "fork parent publication failed ({error}); unpublished child cleanup also \
6651                     failed ({cleanup_error})"
6652                );
6653                return Err(StarryError::BadState);
6654            }
6655            return Err(error);
6656        }
6657
6658        // The child has no CPU activations, but its complete VMA/PTE/RSS/rmap
6659        // view still receives one auditable mutation receipt before the Arc is
6660        // handed to the caller.
6661        if !guard.vma_root.is_empty() {
6662            let mut child_mutation = guard.prepare_mutation();
6663            child_mutation.set_vma_delta(VmaDelta {
6664                inserted: u32::try_from(guard.vma_root.len()).unwrap_or(u32::MAX),
6665                ..VmaDelta::default()
6666            });
6667            child_mutation.set_pte_delta(PteDelta {
6668                mapped: u32::try_from(guard.mapping_slots.len()).unwrap_or(u32::MAX),
6669                ..PteDelta::default()
6670            });
6671            child_mutation.set_mapping_delta(MappingDelta {
6672                attached: u32::try_from(guard.mapping_slots.len()).unwrap_or(u32::MAX),
6673                ..MappingDelta::default()
6674            });
6675            child_mutation.set_resident_delta(
6676                guard
6677                    .resident_counts_from_all_slots()?
6678                    .checked_positive_delta()?,
6679            );
6680            if let Err(error) = guard.commit_mutation(child_mutation) {
6681                if let Err(cleanup_error) = Self::abort_unpublished_clone(&mut guard) {
6682                    warn!(
6683                        "fork child publication failed ({error}); unpublished cleanup also failed \
6684                         ({cleanup_error})"
6685                    );
6686                    return Err(StarryError::BadState);
6687                }
6688                return Err(error);
6689            }
6690        }
6691        crate::syscall::memfd_apply_shared_writable_deltas(&child_memfd_deltas);
6692        drop(guard);
6693
6694        Ok(new_aspace)
6695    }
6696}
6697
6698#[cfg(all(test, not(axtest)))]
6699fn page_fault_completion_updates_only_success_for_test() -> bool {
6700    use core::cell::Cell;
6701
6702    let calls = Cell::new(0);
6703    let observed = Cell::new(VirtAddr::from(0));
6704    let success = complete_page_fault_with(true, VirtAddr::from(0x4567), |vaddr| {
6705        calls.set(calls.get() + 1);
6706        observed.set(vaddr);
6707    });
6708    let rejected = complete_page_fault_with(false, VirtAddr::from(0x89ab), |_| {
6709        calls.set(calls.get() + 1);
6710    });
6711
6712    success && !rejected && calls.get() == 1 && observed.get() == VirtAddr::from(0x4567)
6713}
6714
6715impl fmt::Debug for AddrSpace {
6716    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
6717        f.debug_struct("AddrSpace")
6718            .field("id", &self.id)
6719            .field("layout", &self.layout)
6720            .field("page_table_root", &self.pt.root_paddr())
6721            .field("vma_root", &self.vma_root)
6722            .field("vm_epoch", &self.vm_epoch())
6723            .finish()
6724    }
6725}
6726
6727impl Drop for AddrSpace {
6728    fn drop(&mut self) {
6729        // Destruction is not a recovery context: it cannot report a partial
6730        // page-table/backend failure and may run while an allocator lock is
6731        // held.  Normal owners must pass through `RetirePermit::reclaim`,
6732        // which performs the fallible clear/detach protocol.  A direct drop
6733        // therefore only records a diagnostic; `PageTable`'s own Drop handles
6734        // its frame bookkeeping, while any remaining mapping ownership stays
6735        // visible to the repair/leak detector instead of being half-freed.
6736        let has_retired_batches = self.pending_retired_mapping_batches() != 0;
6737        if !self.vma_root.is_empty() || !self.mapping_slots.is_empty() || has_retired_batches {
6738            warn!(
6739                "address space {} dropped before retire/reclaim; mappings intentionally retained",
6740                self.id.get()
6741            );
6742            // Do not let `PageTable`'s destructor recursively free
6743            // intermediate frames while a stale PTE/VMA or resident slot is
6744            // still observable.  The lifecycle repair path owns any later
6745            // reclamation decision; this destructor has no fallible return
6746            // channel and therefore leaks conservatively.
6747            self.pt.leak();
6748            if has_retired_batches {
6749                let batches = core::mem::take(&mut *self.retired_mapping_batches.lock());
6750                for batch in batches {
6751                    core::mem::forget(batch);
6752                }
6753            }
6754        }
6755    }
6756}
6757
6758#[cfg(test)]
6759mod tests {
6760    use alloc::sync::Arc;
6761    use core::sync::atomic::AtomicUsize;
6762
6763    use ax_memory_addr::{PAGE_SIZE_4K, VirtAddr};
6764
6765    use super::{
6766        AddressSpaceId, MutationError, MutationGate, TlbRange, VmEpoch,
6767        prepare_mapping_publication_mutation,
6768    };
6769
6770    #[cfg(all(test, not(axtest)))]
6771    #[test]
6772    fn page_fault_completion_updates_only_success() {
6773        assert!(super::page_fault_completion_updates_only_success_for_test());
6774    }
6775
6776    #[cfg_attr(axtest, axtest::axtest)]
6777    #[cfg_attr(not(axtest), test)]
6778    fn fresh_mapping_publication_has_no_tlb_targets() {
6779        let gate = MutationGate::new();
6780        let id = AddressSpaceId::allocate();
6781        let targets = Arc::new(AtomicUsize::new(0b1110));
6782        let start = VirtAddr::from(0x20_0000);
6783
6784        let fresh =
6785            prepare_mapping_publication_mutation(&gate, id, &targets, start, PAGE_SIZE_4K, false);
6786        assert_eq!(fresh.receipt().tlb_obligation.targets(), 0);
6787
6788        let replacement =
6789            prepare_mapping_publication_mutation(&gate, id, &targets, start, PAGE_SIZE_4K, true);
6790        assert_eq!(replacement.receipt().tlb_obligation.targets(), 0b1110);
6791    }
6792
6793    #[cfg_attr(axtest, axtest::axtest)]
6794    #[cfg_attr(not(axtest), test)]
6795    fn fresh_mapping_cannot_reuse_range_with_pending_shootdown() {
6796        let gate = MutationGate::new();
6797        let id = AddressSpaceId::allocate();
6798        let targets = Arc::new(AtomicUsize::new(0b1));
6799        let start = VirtAddr::from(0x20_0000);
6800        let mut unmap = gate.begin(id, 0b1);
6801        unmap.add_tlb_range(TlbRange::new(start, PAGE_SIZE_4K).unwrap());
6802        assert_eq!(gate.commit(unmap).unwrap_err(), MutationError::TlbPending);
6803
6804        let nonoverlapping = prepare_mapping_publication_mutation(
6805            &gate,
6806            id,
6807            &targets,
6808            start + PAGE_SIZE_4K * 2,
6809            PAGE_SIZE_4K,
6810            false,
6811        );
6812        gate.validate_publish_preconditions(&nonoverlapping)
6813            .unwrap();
6814        gate.commit(nonoverlapping).unwrap();
6815
6816        let fresh =
6817            prepare_mapping_publication_mutation(&gate, id, &targets, start, PAGE_SIZE_4K, false);
6818        assert_eq!(
6819            gate.validate_publish_preconditions(&fresh),
6820            Err(MutationError::PendingTlbOverlap)
6821        );
6822        assert_eq!(
6823            gate.commit(fresh).unwrap_err(),
6824            MutationError::PendingTlbOverlap
6825        );
6826        assert_eq!(gate.current_epoch(), VmEpoch::new(2));
6827
6828        gate.acknowledge(id, VmEpoch::new(1), 0).unwrap().unwrap();
6829        let retry =
6830            prepare_mapping_publication_mutation(&gate, id, &targets, start, PAGE_SIZE_4K, false);
6831        gate.validate_publish_preconditions(&retry).unwrap();
6832        gate.commit(retry).unwrap();
6833    }
6834
6835    #[cfg_attr(axtest, axtest::axtest)]
6836    #[cfg_attr(not(axtest), test)]
6837    fn pending_full_flush_blocks_every_fresh_mapping_range() {
6838        let gate = MutationGate::new();
6839        let id = AddressSpaceId::allocate();
6840        let targets = Arc::new(AtomicUsize::new(0b1));
6841        assert_eq!(
6842            gate.commit(gate.begin(id, 0b1)).unwrap_err(),
6843            MutationError::TlbPending
6844        );
6845
6846        let fresh = prepare_mapping_publication_mutation(
6847            &gate,
6848            id,
6849            &targets,
6850            VirtAddr::from(0x40_0000),
6851            PAGE_SIZE_4K,
6852            false,
6853        );
6854        assert_eq!(
6855            gate.commit(fresh).unwrap_err(),
6856            MutationError::PendingTlbOverlap
6857        );
6858        assert_eq!(gate.current_epoch(), VmEpoch::new(1));
6859    }
6860
6861    #[cfg(axtest)]
6862    fn refault_waits_for_discard_shootdown(full_flush: bool) {
6863        use ax_runtime::hal::trap::PageFaultFlags;
6864
6865        use super::{AddrSpace, FaultResult, MappingFlags, MappingOperation, PagingError};
6866
6867        let start = VirtAddr::from(0x7200_0000);
6868        let mut aspace = AddrSpace::new_empty(start, PAGE_SIZE_4K).unwrap();
6869        let flags = MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER;
6870        aspace
6871            .map(
6872                start,
6873                PAGE_SIZE_4K,
6874                flags,
6875                true,
6876                MappingOperation::new_alloc(start, PAGE_SIZE_4K, "[discard-refault]"),
6877            )
6878            .unwrap();
6879        aspace.discard_range(start, PAGE_SIZE_4K).unwrap();
6880        // Deterministically retain an unacknowledged discard obligation. The
6881        // page table is real, but no hardware CPU uses this test-only MM.
6882        let mut discard = aspace.mutation_gate.begin(aspace.id, 1);
6883        if !full_flush {
6884            discard.add_tlb_range(TlbRange::new(start, PAGE_SIZE_4K).unwrap());
6885        }
6886        assert_eq!(
6887            aspace.mutation_gate.commit(discard).unwrap_err(),
6888            MutationError::TlbPending
6889        );
6890        let epoch = aspace.vm_epoch();
6891        let plan = aspace
6892            .plan_page_fault(
6893                start,
6894                PageFaultFlags::READ | PageFaultFlags::USER,
6895                Default::default(),
6896            )
6897            .ok()
6898            .unwrap();
6899        let prepared = AddrSpace::prepare_page_fault(plan).ok().unwrap();
6900        let mut attempt = prepared.into_apply_attempt();
6901        let outcome = aspace.apply_prepared_page_fault(&mut attempt);
6902        let unpublished = matches!(aspace.pt.query(start), Err(PagingError::NotMapped))
6903            && aspace.vm_epoch() == epoch
6904            && aspace.mapping_slots.is_empty()
6905            && attempt.prepared.is_some()
6906            && !aspace.mutation_gate.needs_repair();
6907        drop(outcome);
6908        if attempt.prepared.is_some() {
6909            attempt.cancel().unwrap();
6910        }
6911        aspace
6912            .mutation_gate
6913            .acknowledge(aspace.id, epoch, 0)
6914            .unwrap()
6915            .unwrap();
6916        let retry =
6917            aspace.handle_page_fault_result(start, PageFaultFlags::READ | PageFaultFlags::USER);
6918        let recovered = matches!(retry, FaultResult::Handled) && aspace.pt.query(start).is_ok();
6919        aspace.reset_uninstalled_for_loader().unwrap();
6920        assert!(
6921            unpublished,
6922            "refault must leave the PTE, epoch and owner graph untouched until discard is \
6923             acknowledged"
6924        );
6925        assert!(
6926            recovered,
6927            "acknowledged discard must allow refault to make progress"
6928        );
6929    }
6930
6931    #[cfg(axtest)]
6932    #[axtest::axtest]
6933    fn refault_waits_for_pending_discard_range() {
6934        refault_waits_for_discard_shootdown(false);
6935    }
6936
6937    #[cfg(axtest)]
6938    #[axtest::axtest]
6939    fn refault_waits_for_pending_discard_full_flush() {
6940        refault_waits_for_discard_shootdown(true);
6941    }
6942}