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