Skip to main content

starry_kernel/mm/aspace/
objects.rs

1//! Typed page ownership and reverse-mapping records.
2
3use alloc::{sync::Arc, vec::Vec};
4use core::{
5    any::Any,
6    fmt,
7    hash::{Hash, Hasher},
8    sync::atomic::{AtomicBool, AtomicU8, Ordering},
9};
10
11use ax_memory_addr::{MemoryAddr, PAGE_SIZE_4K, PhysAddr, VirtAddr, VirtAddrRange};
12use ax_runtime::hal::paging::HugeSplitDeposit;
13
14use super::{AddressSpaceId, MappingId, PageOrder, RssKind};
15use crate::sync::IrqMutex;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct PageId(u64);
19
20impl PageId {
21    pub fn allocate() -> Self {
22        static NEXT_ID: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(1);
23        Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
24    }
25
26    pub const fn new(value: u64) -> Self {
27        Self(value)
28    }
29
30    pub const fn get(self) -> u64 {
31        self.0
32    }
33}
34
35/// Shared allocation owner behind one or more bounded frame leases.
36///
37/// A huge allocation may be split into base-page PageObjects without asking
38/// the buddy allocator to retag the block: each sublease keeps this allocation
39/// alive, and only the last sublease releases the original range.
40struct FrameAllocation {
41    base: PhysAddr,
42    bytes: usize,
43    /// `None` is used for a frame borrowed from platform firmware or a test
44    /// fixture.  Allocated frames carry their allocator alignment and are
45    /// released exactly once by this token's destructor.
46    release_align: Option<usize>,
47    /// The default release path is the user/data-frame allocator.  Page-table
48    /// detached tokens use a different allocator domain, so the lease carries
49    /// an explicit function pointer rather than making callers smuggle a raw
50    /// physical address through the ownership API.
51    release: fn(PhysAddr, usize),
52    /// External allocations (DMA/device/cache pages) retain their provider in
53    /// the same capability that names the physical range.  The MM layer never
54    /// has to keep a parallel owner beside a bare address list.
55    _anchor: Option<Arc<dyn Any + Send + Sync>>,
56}
57
58impl Drop for FrameAllocation {
59    fn drop(&mut self) {
60        if let Some(align) = self.release_align.take() {
61            (self.release)(self.base, align);
62        }
63    }
64}
65
66/// A bounded capability for a physical frame range owned by a PageObject.
67/// Allocation/deallocation policy remains in the architecture allocator; the
68/// MM layer never treats a bare `PhysAddr` as ownership.
69#[derive(Clone)]
70pub struct FrameLease {
71    paddr: PhysAddr,
72    bytes: usize,
73    allocation: Arc<FrameAllocation>,
74}
75
76impl fmt::Debug for FrameLease {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        formatter
79            .debug_struct("FrameLease")
80            .field("paddr", &self.paddr)
81            .field("bytes", &self.bytes)
82            .field("owned", &self.allocation.release_align.is_some())
83            .field("anchored", &self.allocation._anchor.is_some())
84            .finish()
85    }
86}
87
88fn release_data_frame(paddr: PhysAddr, align: usize) {
89    super::backend::dealloc_frame(paddr, align);
90}
91
92impl FrameLease {
93    pub fn new(paddr: PhysAddr) -> Self {
94        Self {
95            paddr,
96            bytes: PAGE_SIZE_4K,
97            allocation: Arc::new(FrameAllocation {
98                base: paddr,
99                bytes: PAGE_SIZE_4K,
100                release_align: None,
101                release: release_data_frame,
102                _anchor: None,
103            }),
104        }
105    }
106
107    /// Creates a non-releasing lease whose provider remains alive for as long
108    /// as any PageObject or sublease can name this physical range.
109    pub fn borrowed(
110        paddr: PhysAddr,
111        bytes: usize,
112        anchor: Option<Arc<dyn Any + Send + Sync>>,
113    ) -> Option<Self> {
114        if bytes == 0 {
115            return None;
116        }
117        Some(Self {
118            paddr,
119            bytes,
120            allocation: Arc::new(FrameAllocation {
121                base: paddr,
122                bytes,
123                release_align: None,
124                release: release_data_frame,
125                _anchor: anchor,
126            }),
127        })
128    }
129
130    /// Creates an owning lease for a frame allocated by the Starry MM backend.
131    /// The alignment is the same order/size passed to `alloc_frame`.
132    ///
133    /// Ownership cannot be fabricated from an arbitrary physical address:
134    /// ```compile_fail
135    /// fn fabricate_owner() -> starry_kernel::FrameLease {
136    ///     starry_kernel::FrameLease::owned(ax_memory_addr::PhysAddr::from(0x1000usize), 4096)
137    /// }
138    /// ```
139    ///
140    /// # Safety
141    ///
142    /// `paddr` must be the unique, still-owned result of the Starry MM frame
143    /// allocator for exactly `align` bytes/alignment. The caller transfers
144    /// that ownership and must neither free it nor construct another owner.
145    pub unsafe fn owned(paddr: PhysAddr, align: usize) -> Self {
146        Self {
147            paddr,
148            bytes: align,
149            allocation: Arc::new(FrameAllocation {
150                base: paddr,
151                bytes: align,
152                release_align: Some(align),
153                release: release_data_frame,
154                _anchor: None,
155            }),
156        }
157    }
158
159    /// Creates an owning lease whose release function belongs to another
160    /// allocator domain (for example page-table frames).  The function is a
161    /// static capability, not a closure, so dropping a lease remains IRQ-safe
162    /// and cannot allocate.
163    pub fn owned_with_releaser(
164        paddr: PhysAddr,
165        align: usize,
166        release: fn(PhysAddr, usize),
167    ) -> Self {
168        Self {
169            paddr,
170            bytes: align,
171            allocation: Arc::new(FrameAllocation {
172                base: paddr,
173                bytes: align,
174                release_align: Some(align),
175                release,
176                _anchor: None,
177            }),
178        }
179    }
180
181    pub const fn paddr(&self) -> PhysAddr {
182        self.paddr
183    }
184
185    pub const fn size(&self) -> usize {
186        self.bytes
187    }
188
189    /// Derives a bounded lease for a subrange of this allocation. The returned
190    /// lease shares only the final-release owner; its physical identity and
191    /// bounds are independent and can back a separate PageObject/rmap set.
192    pub fn sublease(&self, offset: usize, bytes: usize) -> Option<Self> {
193        if bytes == 0 {
194            return None;
195        }
196        let end = offset.checked_add(bytes)?;
197        if end > self.bytes {
198            return None;
199        }
200        let paddr = self.paddr.as_usize().checked_add(offset)?;
201        let allocation_offset = paddr.checked_sub(self.allocation.base.as_usize())?;
202        if allocation_offset.checked_add(bytes)? > self.allocation.bytes {
203            return None;
204        }
205        Some(Self {
206            paddr: PhysAddr::from_usize(paddr),
207            bytes,
208            allocation: self.allocation.clone(),
209        })
210    }
211}
212
213#[repr(u8)]
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum PageState {
216    Reserved  = 0,
217    Present   = 1,
218    /// Anonymous page carrying the Linux `MADV_FREE` lazy-reclaim mark.
219    LazyFree  = 2,
220    Evicting  = 3,
221    Writeback = 4,
222    Retired   = 5,
223}
224
225impl PageState {
226    fn from_u8(value: u8) -> Self {
227        match value {
228            0 => Self::Reserved,
229            1 => Self::Present,
230            2 => Self::LazyFree,
231            3 => Self::Evicting,
232            4 => Self::Writeback,
233            _ => Self::Retired,
234        }
235    }
236}
237
238/// Reverse mappings are explicit records rather than an implicit scan of all
239/// VMAs.  A slot remains in this set until its PTE invalidation is acknowledged.
240#[derive(Debug, Default)]
241pub struct RmapSet {
242    entries: IrqMutex<RmapEntries>,
243}
244
245#[derive(Debug, Default)]
246struct RmapEntries {
247    keys: Vec<MappingSlotKey>,
248    /// Capacity owned by prepared transactions, not yet installed mappings.
249    reserved: usize,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
253pub struct MappingSlotKey {
254    pub space_id: AddressSpaceId,
255    pub va: VirtAddr,
256}
257
258/// An exclusive claim on rmap capacity prepared outside IRQ-saving locks.
259///
260/// Preparation installs any larger backing store before returning this token.
261/// Other publishers reserve their own space, so committing this claim cannot
262/// lose its capacity to a concurrent fork. Displaced storage remains owned by
263/// the token until all caller-held PTE and graph guards have been released.
264pub(crate) struct MappingGraphReservation<'a> {
265    owner: &'a RmapSet,
266    additional: usize,
267    displaced: Vec<MappingSlotKey>,
268}
269
270impl Drop for MappingGraphReservation<'_> {
271    fn drop(&mut self) {
272        if self.additional != 0 {
273            let mut entries = self.owner.entries.lock();
274            entries.reserved -= self.additional;
275        }
276        // Free backing storage only after the cancellation guard has gone away.
277        drop(core::mem::take(&mut self.displaced));
278    }
279}
280
281impl Hash for MappingSlotKey {
282    fn hash<H: Hasher>(&self, state: &mut H) {
283        self.space_id.hash(state);
284        self.va.as_usize().hash(state);
285    }
286}
287
288impl RmapSet {
289    fn prepare_replace(
290        &self,
291        additional: usize,
292    ) -> Result<MappingGraphReservation<'_>, MappingGraphError> {
293        let mut replacement = Vec::new();
294        loop {
295            let mut entries = self.entries.lock();
296            let reserved = entries
297                .reserved
298                .checked_add(additional)
299                .ok_or(MappingGraphError::ResourceExhausted)?;
300            let required = entries
301                .keys
302                .len()
303                .checked_add(reserved)
304                .ok_or(MappingGraphError::ResourceExhausted)?;
305            if entries.keys.capacity() < required {
306                if replacement.capacity() < required {
307                    drop(entries);
308                    replacement
309                        .try_reserve_exact(required)
310                        .map_err(|_| MappingGraphError::ResourceExhausted)?;
311                    continue;
312                }
313                replacement.extend_from_slice(&entries.keys);
314                core::mem::swap(&mut entries.keys, &mut replacement);
315            }
316            entries.reserved = reserved;
317            return Ok(MappingGraphReservation {
318                owner: self,
319                additional,
320                displaced: replacement,
321            });
322        }
323    }
324
325    /// Commits a capacity claim without allocating or freeing backing storage.
326    /// Validation precedes both key changes and consumption of the claim.
327    fn replace_reserved(
328        &self,
329        old: &[MappingSlotKey],
330        new: &[MappingSlotKey],
331        reservation: &mut MappingGraphReservation<'_>,
332    ) -> Result<(), MappingGraphError> {
333        if !core::ptr::eq(reservation.owner, self) {
334            return Err(MappingGraphError::SlotIdentityMismatch);
335        }
336        if new.len().saturating_sub(old.len()) > reservation.additional {
337            return Err(MappingGraphError::SlotStateConflict);
338        }
339        let mut entries = self.entries.lock();
340        for (index, key) in old.iter().enumerate() {
341            if old[..index].contains(key) || !entries.keys.contains(key) {
342                return Err(MappingGraphError::MissingOldSlot);
343            }
344        }
345        for (index, key) in new.iter().enumerate() {
346            if new[..index].contains(key)
347                || (entries.keys.contains(key) && !old.contains(key))
348            {
349                return Err(MappingGraphError::DuplicateNewSlot);
350            }
351        }
352        for key in old {
353            let index = entries
354                .keys
355                .iter()
356                .position(|entry| entry == key)
357                .ok_or(MappingGraphError::MissingOldSlot)?;
358            entries.keys.swap_remove(index);
359        }
360        entries.keys.extend_from_slice(new);
361        entries.reserved -= reservation.additional;
362        reservation.additional = 0;
363        Ok(())
364    }
365
366    pub fn try_snapshot(&self) -> Result<Vec<MappingSlotKey>, MappingGraphError> {
367        let mut snapshot = Vec::new();
368        loop {
369            let required = self.entries.lock().keys.len();
370            if snapshot.capacity() < required {
371                snapshot
372                    .try_reserve_exact(required.saturating_sub(snapshot.len()))
373                    .map_err(|_| MappingGraphError::ResourceExhausted)?;
374            }
375            let entries = self.entries.lock();
376            if entries.keys.len() > snapshot.capacity() {
377                drop(entries);
378                continue;
379            }
380            snapshot.clear();
381            snapshot.extend_from_slice(&entries.keys);
382            return Ok(snapshot);
383        }
384    }
385
386    #[cfg(test)]
387    pub fn snapshot(&self) -> Vec<MappingSlotKey> {
388        self.try_snapshot()
389            .expect("test reverse-mapping snapshot allocation must succeed")
390    }
391
392    fn all_mappings_belong_to(&self, mm_id: AddressSpaceId, expected: u32) -> bool {
393        let entries = self.entries.lock();
394        usize::try_from(expected).is_ok_and(|expected| entries.keys.len() == expected)
395            && entries.keys.iter().all(|entry| entry.space_id == mm_id)
396    }
397
398    pub fn is_empty(&self) -> bool {
399        self.entries.lock().keys.is_empty()
400    }
401}
402
403pub struct PageObject {
404    pub id: PageId,
405    frame: FrameLease,
406    state: AtomicU8,
407    /// Origin of the resident contents.  Mapping-specific policy (for example
408    /// whether a cached file page is reported as File or Shmem) still belongs
409    /// to MappingSlot, but private COW replacement changes the underlying
410    /// object itself from File to Anon.
411    resident_kind: AtomicU8,
412    /// Number of installed mapping slots referring to this page.  The frame
413    /// lease is released only after the last slot is detached and the registry
414    /// drops its own reference.
415    mapping_refs: core::sync::atomic::AtomicU32,
416    writeback_generation: core::sync::atomic::AtomicU64,
417    /// Set only after every TLB obligation for the most recently detached
418    /// reverse mapping has completed.  An evicting page cannot resume its
419    /// cache eviction while this flag is false.
420    eviction_tlb_ready: AtomicBool,
421    /// Serializes the page-state check with mapping-ref and rmap publication.
422    /// This is the Rust ownership boundary corresponding to Linux's page/rmap
423    /// locking layer; it is deliberately independent from VMA publication and
424    /// PTE stripe locks.
425    mapping_graph: IrqMutex<()>,
426    pub rmap: RmapSet,
427}
428
429impl PageObject {
430    pub fn new(id: PageId, frame: FrameLease) -> Arc<Self> {
431        Self::new_with_resident_kind(id, frame, None)
432    }
433
434    pub fn new_with_resident_kind(
435        id: PageId,
436        frame: FrameLease,
437        resident_kind: Option<RssKind>,
438    ) -> Arc<Self> {
439        Arc::new(Self {
440            id,
441            frame,
442            state: AtomicU8::new(PageState::Reserved as u8),
443            resident_kind: AtomicU8::new(RssKind::slot_value(resident_kind)),
444            mapping_refs: core::sync::atomic::AtomicU32::new(0),
445            writeback_generation: core::sync::atomic::AtomicU64::new(0),
446            eviction_tlb_ready: AtomicBool::new(false),
447            mapping_graph: IrqMutex::new(()),
448            rmap: RmapSet::default(),
449        })
450    }
451
452    pub fn new_present(id: PageId, frame: FrameLease) -> Arc<Self> {
453        Self::new_present_with_resident_kind(id, frame, None)
454    }
455
456    pub fn new_present_with_resident_kind(
457        id: PageId,
458        frame: FrameLease,
459        resident_kind: Option<RssKind>,
460    ) -> Arc<Self> {
461        let page = Self::new_with_resident_kind(id, frame, resident_kind);
462        // A freshly allocated frame is reserved until its first PTE is ready.
463        // This transition cannot fail for a new object; retaining the explicit
464        // check keeps the invariant visible to callers and tests.
465        let _ = page.transition(PageState::Reserved, PageState::Present);
466        page
467    }
468
469    pub fn state(&self) -> PageState {
470        PageState::from_u8(self.state.load(Ordering::Acquire))
471    }
472
473    pub fn frame(&self) -> &FrameLease {
474        &self.frame
475    }
476
477    /// Publishes CPU-written RAM contents before granting instruction fetch.
478    /// The mapping transaction retains the frame or its external provider pin
479    /// throughout maintenance; a borrowed FrameLease alone is not ownership.
480    pub(super) fn prepare_executable_mapping(
481        &self,
482        paddr: PhysAddr,
483        size: usize,
484        flags: ax_runtime::hal::paging::MappingFlags,
485    ) {
486        use ax_runtime::hal::{mem::phys_to_virt, paging::MappingFlags};
487
488        if !flags.contains(MappingFlags::EXECUTE)
489            || flags.intersects(MappingFlags::DEVICE | MappingFlags::UNCACHED)
490        {
491            return;
492        }
493        let offset = paddr
494            .as_usize()
495            .checked_sub(self.frame.paddr().as_usize())
496            .expect("executable leaf must belong to its retained page");
497        assert!(
498            offset <= self.frame.size() && size <= self.frame.size() - offset,
499            "executable leaf exceeds its retained page"
500        );
501        let range = ax_cpu::cache::CacheRange::new(phys_to_virt(paddr), size)
502            .expect("retained RAM alias must not wrap");
503        // SAFETY: the fault/populate/protect transaction retains this bounded
504        // RAM range through its owning frame, pending file-cache pin, or live
505        // MappingSlot eviction exclusion. It has not published the new
506        // executable PTE yet. Cleaning preserves other CPU data; unlike DMA
507        // invalidation it cannot discard dirty bytes.
508        unsafe { ax_cpu::cache::clean_dcache_range_to_pou(range) };
509        // arm64 broadcasts invalidation; remote user exception return provides
510        // context synchronization. RISC-V also executes fence.i on user return.
511        ax_cpu::cache::flush_icache_all();
512    }
513
514    pub fn resident_kind(&self) -> Option<RssKind> {
515        RssKind::from_slot_value(self.resident_kind.load(Ordering::Acquire))
516    }
517
518    pub(crate) fn set_resident_kind(&self, kind: Option<RssKind>) {
519        self.resident_kind
520            .store(RssKind::slot_value(kind), Ordering::Release);
521    }
522
523    pub fn mapping_refs(&self) -> u32 {
524        self.mapping_refs.load(Ordering::Acquire)
525    }
526
527    /// Returns whether all installed mappings are owned by one address space.
528    ///
529    /// This mirrors Linux's large-anonymous-folio `mm_id` reuse test: splitting
530    /// one PMD into 512 PTEs raises the mapcount, but does not by itself make
531    /// the folio shared.  A write may reuse the subpage when every rmap still
532    /// belongs to this MM; fork introduces another MM identity and therefore
533    /// forces a base-page COW copy.
534    pub(crate) fn exclusively_mapped_by(&self, mm_id: AddressSpaceId) -> bool {
535        let _graph = self.mapping_graph.lock();
536        if !matches!(self.state(), PageState::Present | PageState::LazyFree) {
537            return false;
538        }
539        let mappings = self.mapping_refs.load(Ordering::Acquire);
540        mappings != 0 && self.rmap.all_mappings_belong_to(mm_id, mappings)
541    }
542
543    fn publish_slot_graph(&self, key: MappingSlotKey) -> bool {
544        let Ok(mut reservation) = self.rmap.prepare_replace(1) else {
545            return false;
546        };
547        let published = {
548            let _graph = self.mapping_graph.lock();
549            if !matches!(self.state(), PageState::Present | PageState::LazyFree) {
550                false
551            } else {
552                let current = self.mapping_refs.load(Ordering::Acquire);
553                let Some(next) = current.checked_add(1) else {
554                    return false;
555                };
556                match self.rmap.replace_reserved(&[], &[key], &mut reservation) {
557                    Err(_) => false,
558                    Ok(()) if !matches!(self.state(), PageState::Present | PageState::LazyFree) => {
559                        let _ = self.rmap.replace_reserved(&[key], &[], &mut reservation);
560                        false
561                    }
562                    Ok(()) => {
563                        self.mapping_refs.store(next, Ordering::Release);
564                        true
565                    }
566                }
567            }
568        };
569        // Preparation may leave the old vector allocation in the token.
570        // Release it only after the IRQ-saving graph guard has gone away.
571        drop(reservation);
572        published
573    }
574
575    fn detach_slot_graph(&self, key: MappingSlotKey) -> bool {
576        let Ok(mut reservation) = self.rmap.prepare_replace(0) else {
577            return false;
578        };
579        let (detached, became_exclusive) = {
580            let _graph = self.mapping_graph.lock();
581            let current = self.mapping_refs.load(Ordering::Acquire);
582            let Some(next) = current.checked_sub(1) else {
583                return false;
584            };
585            if self
586                .rmap
587                .replace_reserved(&[key], &[], &mut reservation)
588                .is_err()
589            {
590                (false, false)
591            } else {
592                self.mapping_refs.store(next, Ordering::Release);
593                (true, current > 1 && next == 1)
594            }
595        };
596        drop(reservation);
597        if became_exclusive && self.state() == PageState::LazyFree {
598            // A fork-shared lazy-free page was ineligible during the original
599            // MADV_FREE pass. Its last foreign mapping disappearing is a new
600            // reclaimability edge, analogous to Linux putting a newly eligible
601            // lazy-free folio back on reclaimable LRU state.
602            super::lifecycle::request_lazy_free_reclaim();
603        }
604        detached
605    }
606
607    pub(crate) fn prepare_mapping_graph_replace(
608        &self,
609        old: &[MappingSlotKey],
610        new: &[MappingSlotKey],
611    ) -> Result<MappingGraphReservation<'_>, MappingGraphError> {
612        self.rmap
613            .prepare_replace(new.len().saturating_sub(old.len()))
614    }
615
616    pub(crate) fn replace_mapping_graph_reserved(
617        &self,
618        old: &[MappingSlotKey],
619        new: &[MappingSlotKey],
620        reservation: &mut MappingGraphReservation<'_>,
621    ) -> Result<(), MappingGraphError> {
622        let became_exclusive = {
623            let _graph = self.mapping_graph.lock();
624            if !matches!(self.state(), PageState::Present | PageState::LazyFree) {
625                return Err(MappingGraphError::PageNotPresent);
626            }
627            let current = self.mapping_refs.load(Ordering::Acquire);
628            let next = if new.len() >= old.len() {
629                let additional = u32::try_from(new.len() - old.len())
630                    .map_err(|_| MappingGraphError::RefOverflow)?;
631                current
632                    .checked_add(additional)
633                    .ok_or(MappingGraphError::RefOverflow)?
634            } else {
635                let removed = u32::try_from(old.len() - new.len())
636                    .map_err(|_| MappingGraphError::RefUnderflow)?;
637                current
638                    .checked_sub(removed)
639                    .ok_or(MappingGraphError::RefUnderflow)?
640            };
641            self.rmap.replace_reserved(old, new, reservation)?;
642            self.mapping_refs.store(next, Ordering::Release);
643            current > 1 && next == 1
644        };
645        if became_exclusive && self.state() == PageState::LazyFree {
646            super::lifecycle::request_lazy_free_reclaim();
647        }
648        Ok(())
649    }
650
651    /// Atomically replaces a set of reverse mappings and adjusts the installed
652    /// PTE reference count by the same cardinality delta.  All fallible rmap
653    /// reservation and validation completes before either fact is changed.
654    pub(crate) fn replace_mapping_graph(
655        &self,
656        old: &[MappingSlotKey],
657        new: &[MappingSlotKey],
658    ) -> Result<(), MappingGraphError> {
659        let mut reservation = self.prepare_mapping_graph_replace(old, new)?;
660        let result = self.replace_mapping_graph_reserved(old, new, &mut reservation);
661        drop(reservation);
662        result
663    }
664
665    pub(crate) fn transition(&self, expected: PageState, next: PageState) -> bool {
666        let valid = matches!(
667            (expected, next),
668            (PageState::Reserved, PageState::Present)
669                | (PageState::Present, PageState::LazyFree)
670                | (PageState::LazyFree, PageState::Present)
671                | (PageState::LazyFree, PageState::Evicting)
672                | (PageState::LazyFree, PageState::Retired)
673                | (PageState::Present, PageState::Evicting)
674                | (PageState::Present, PageState::Writeback)
675                | (PageState::Evicting, PageState::Present)
676                | (PageState::Evicting, PageState::Retired)
677                | (PageState::Writeback, PageState::Present)
678                | (PageState::Writeback, PageState::Retired)
679        );
680        valid
681            && self
682                .state
683                .compare_exchange(
684                    expected as u8,
685                    next as u8,
686                    Ordering::AcqRel,
687                    Ordering::Acquire,
688                )
689                .is_ok()
690    }
691
692    pub(crate) fn mark_lazy_free(&self) -> bool {
693        self.transition(PageState::Present, PageState::LazyFree)
694    }
695
696    pub(crate) fn clear_lazy_free(&self) -> bool {
697        self.transition(PageState::LazyFree, PageState::Present)
698    }
699
700    /// Pins a resident page for rmap-driven eviction.  The lease must either
701    /// be cancelled or completed; dropping it intentionally leaves the page
702    /// in `Evicting`, so a failed caller cannot accidentally make a page
703    /// reclaimable while a stale PTE still exists.
704    pub(crate) fn eviction_lease(self: &Arc<Self>) -> Result<EvictionLease, EvictionError> {
705        let _graph = self.mapping_graph.lock();
706        if !self.transition(PageState::Present, PageState::Evicting) {
707            return Err(EvictionError::NotPresent);
708        }
709        self.eviction_tlb_ready.store(false, Ordering::Release);
710        Ok(EvictionLease { page: self.clone() })
711    }
712
713    /// Marks the current eviction safe to resume after its detached PTEs have
714    /// been acknowledged by every target CPU.
715    pub(crate) fn complete_eviction_tlb(&self) {
716        if self.state() == PageState::Evicting {
717            self.eviction_tlb_ready.store(true, Ordering::Release);
718        }
719    }
720
721    /// Reacquires ownership of an eviction that previously stopped at a TLB
722    /// quarantine boundary.  The readiness bit is consumed so at most one
723    /// reclaimer can continue that state transition.
724    pub(crate) fn resume_eviction_lease(self: &Arc<Self>) -> Result<EvictionLease, EvictionError> {
725        if self.state() != PageState::Evicting
726            || self
727                .eviction_tlb_ready
728                .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
729                .is_err()
730        {
731            return Err(EvictionError::Busy);
732        }
733        Ok(EvictionLease { page: self.clone() })
734    }
735
736    /// Begins one writeback protection generation.  While this lease exists a
737    /// new mapping slot cannot be published and eviction cannot retire the
738    /// frame; the caller must protect every rmap entry before completing it.
739    pub(crate) fn writeback_lease(self: &Arc<Self>) -> Result<WritebackLease, WritebackError> {
740        let _graph = self.mapping_graph.lock();
741        if !self.transition(PageState::Present, PageState::Writeback) {
742            return Err(WritebackError::Busy);
743        }
744        let generation = match self.writeback_generation.try_update(
745            Ordering::AcqRel,
746            Ordering::Acquire,
747            |current| current.checked_add(1),
748        ) {
749            Ok(previous) => previous + 1,
750            Err(_) => {
751                let _ = self.transition(PageState::Writeback, PageState::Present);
752                return Err(WritebackError::GenerationExhausted);
753            }
754        };
755        Ok(WritebackLease {
756            page: self.clone(),
757            generation,
758        })
759    }
760}
761
762#[derive(Debug, Clone, Copy, PartialEq, Eq)]
763pub enum MappingGraphError {
764    PageNotPresent,
765    MissingOldSlot,
766    DuplicateNewSlot,
767    ResourceExhausted,
768    RefOverflow,
769    RefUnderflow,
770    SlotStateConflict,
771    SlotIdentityMismatch,
772    RollbackFailed,
773}
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq)]
776pub enum EvictionError {
777    NotPresent,
778    Busy,
779}
780
781#[derive(Debug, Clone, Copy, PartialEq, Eq)]
782pub enum WritebackError {
783    Busy,
784    GenerationExhausted,
785}
786
787/// Ownership token held while a page's reverse mappings are being revoked.
788pub struct EvictionLease {
789    page: Arc<PageObject>,
790}
791
792impl EvictionLease {
793    pub fn page(&self) -> &Arc<PageObject> {
794        &self.page
795    }
796
797    pub(crate) fn cancel(self) -> bool {
798        self.page.eviction_tlb_ready.store(false, Ordering::Release);
799        self.page
800            .transition(PageState::Evicting, PageState::Present)
801    }
802
803    pub(crate) fn retire(self) -> Result<(), (Self, EvictionError)> {
804        if !self.page.rmap.is_empty() || self.page.mapping_refs() != 0 {
805            return Err((self, EvictionError::Busy));
806        }
807        if self
808            .page
809            .transition(PageState::Evicting, PageState::Retired)
810        {
811            Ok(())
812        } else {
813            Err((self, EvictionError::Busy))
814        }
815    }
816}
817
818/// Pins a PageObject while one dirty-generation snapshot is protected.
819pub struct WritebackLease {
820    page: Arc<PageObject>,
821    generation: u64,
822}
823
824impl WritebackLease {
825    pub const fn generation(&self) -> u64 {
826        self.generation
827    }
828
829    pub(crate) fn cancel(self) -> bool {
830        self.page
831            .transition(PageState::Writeback, PageState::Present)
832    }
833
834    pub(crate) fn complete(self) -> Result<u64, (Self, WritebackError)> {
835        if self
836            .page
837            .transition(PageState::Writeback, PageState::Present)
838        {
839            Ok(self.generation)
840        } else {
841            Err((self, WritebackError::Busy))
842        }
843    }
844}
845
846#[repr(u8)]
847#[derive(Debug, Clone, Copy, PartialEq, Eq)]
848pub enum SlotState {
849    Reserved = 0,
850    Present  = 1,
851    Detached = 2,
852}
853
854impl SlotState {
855    fn from_u8(value: u8) -> Self {
856        match value {
857            0 => Self::Reserved,
858            1 => Self::Present,
859            _ => Self::Detached,
860        }
861    }
862}
863
864/// Ownership record for one installed PTE.
865pub struct MappingSlot {
866    pub mapping: MappingId,
867    pub mm_id: AddressSpaceId,
868    pub va: VirtAddr,
869    pub page_order: PageOrder,
870    pub page: Arc<PageObject>,
871    /// Byte offset of this materialized leaf inside `page.frame()`.
872    ///
873    /// A PMD split keeps one large PageObject, matching Linux's large-folio
874    /// ownership, while each base PTE names a different physical subrange.
875    /// Recording that subrange here lets later mutation/rmap code validate
876    /// exact ownership without rediscovering a PageObject from a raw PFN.
877    frame_offset: usize,
878    /// Linux resident category owned by this installed mapping.  It belongs
879    /// to the slot rather than the VMA or PageObject because a file-private
880    /// page can change from File to Anon at one VA while other mappings of the
881    /// same logical object keep their own classification.
882    resident_kind: AtomicU8,
883    /// Preallocated child table bound to this slot's huge leaf.  It is never
884    /// visible to hardware until a typed split consumes it.  Dropping a whole
885    /// huge mapping therefore releases the unpublished deposit without a TLB
886    /// obligation, matching Linux's deposited-PTE-page ownership.
887    huge_split_deposit: IrqMutex<Option<HugeSplitDeposit>>,
888    state: AtomicU8,
889}
890
891impl MappingSlot {
892    #[cfg(test)]
893    pub(crate) fn new(
894        mapping: MappingId,
895        mm_id: AddressSpaceId,
896        va: VirtAddr,
897        page_order: PageOrder,
898        page: Arc<PageObject>,
899        resident_kind: Option<RssKind>,
900    ) -> Self {
901        Self {
902            mapping,
903            mm_id,
904            va,
905            page_order,
906            page,
907            frame_offset: 0,
908            resident_kind: AtomicU8::new(RssKind::slot_value(resident_kind)),
909            huge_split_deposit: IrqMutex::new(None),
910            state: AtomicU8::new(SlotState::Reserved as u8),
911        }
912    }
913
914    pub(crate) fn new_with_frame_offset(
915        mapping: MappingId,
916        mm_id: AddressSpaceId,
917        va: VirtAddr,
918        page_order: PageOrder,
919        page: Arc<PageObject>,
920        frame_offset: usize,
921        resident_kind: Option<RssKind>,
922    ) -> Option<Self> {
923        let bytes = PAGE_SIZE_4K.checked_shl(page_order.get().into())?;
924        let end = frame_offset.checked_add(bytes)?;
925        if !frame_offset.is_multiple_of(PAGE_SIZE_4K) || end > page.frame().size() {
926            return None;
927        }
928        Some(Self {
929            mapping,
930            mm_id,
931            va,
932            page_order,
933            page,
934            frame_offset,
935            resident_kind: AtomicU8::new(RssKind::slot_value(resident_kind)),
936            huge_split_deposit: IrqMutex::new(None),
937            state: AtomicU8::new(SlotState::Reserved as u8),
938        })
939    }
940
941    pub(crate) const fn frame_offset(&self) -> usize {
942        self.frame_offset
943    }
944
945    pub(crate) fn mapped_paddr(&self) -> Option<PhysAddr> {
946        self.page.frame().paddr().checked_add(self.frame_offset)
947    }
948
949    /// Attaches the deposit before the slot is published into the address
950    /// space.  A second deposit would mean two page-table-frame owners for one
951    /// huge leaf and is rejected by returning ownership to the caller.
952    pub(crate) fn attach_huge_split_deposit(
953        self,
954        deposit: HugeSplitDeposit,
955    ) -> Result<Self, HugeSplitDeposit> {
956        {
957            let mut owner = self.huge_split_deposit.lock();
958            if owner.is_some() {
959                return Err(deposit);
960            }
961            *owner = Some(deposit);
962        }
963        Ok(self)
964    }
965
966    pub(crate) fn has_huge_split_deposit(&self) -> bool {
967        self.huge_split_deposit.lock().is_some()
968    }
969
970    pub(crate) fn take_huge_split_deposit(&self) -> Option<HugeSplitDeposit> {
971        self.huge_split_deposit.lock().take()
972    }
973
974    pub(crate) fn restore_huge_split_deposit(
975        &self,
976        deposit: HugeSplitDeposit,
977    ) -> Result<(), HugeSplitDeposit> {
978        let mut owner = self.huge_split_deposit.lock();
979        if owner.is_some() {
980            return Err(deposit);
981        }
982        *owner = Some(deposit);
983        Ok(())
984    }
985
986    pub(crate) fn overlaps(&self, range: VirtAddrRange) -> bool {
987        let Some(bytes) = PAGE_SIZE_4K.checked_shl(self.page_order.get().into()) else {
988            return false;
989        };
990        let Some(end) = self.va.checked_add(bytes) else {
991            return false;
992        };
993        self.va < range.end && range.start < end
994    }
995
996    pub fn state(&self) -> SlotState {
997        SlotState::from_u8(self.state.load(Ordering::Acquire))
998    }
999
1000    pub fn resident_kind(&self) -> Option<RssKind> {
1001        RssKind::from_slot_value(self.resident_kind.load(Ordering::Acquire))
1002    }
1003
1004    /// Reclassifies one published resident fact (for example File -> Anon on
1005    /// a private COW write).  The owning address-space mutation gate provides
1006    /// serialization; release/acquire keeps lock-free statistics snapshots
1007    /// from observing a torn category.
1008    pub(crate) fn set_resident_kind(&self, kind: Option<RssKind>) {
1009        self.resident_kind
1010            .store(RssKind::slot_value(kind), Ordering::Release);
1011    }
1012
1013    pub(crate) fn publish(&self) -> bool {
1014        if self
1015            .state
1016            .compare_exchange(
1017                SlotState::Reserved as u8,
1018                SlotState::Present as u8,
1019                Ordering::AcqRel,
1020                Ordering::Acquire,
1021            )
1022            .is_err()
1023        {
1024            return false;
1025        }
1026        if self.page.publish_slot_graph(MappingSlotKey {
1027            space_id: self.mm_id,
1028            va: self.va,
1029        }) {
1030            true
1031        } else {
1032            let _ = self.state.compare_exchange(
1033                SlotState::Present as u8,
1034                SlotState::Reserved as u8,
1035                Ordering::AcqRel,
1036                Ordering::Acquire,
1037            );
1038            false
1039        }
1040    }
1041
1042    pub(crate) fn detach(&self) -> bool {
1043        self.detach_slot()
1044    }
1045
1046    /// Restores a detached slot and its mapping reference together. Mutation
1047    /// rollback uses the same Arc identity captured in the preimage, so rmap,
1048    /// refcount and slot state cannot be reconstructed as unrelated
1049    /// best-effort operations.
1050    pub(crate) fn restore(&self) -> bool {
1051        if self
1052            .state
1053            .compare_exchange(
1054                SlotState::Detached as u8,
1055                SlotState::Present as u8,
1056                Ordering::AcqRel,
1057                Ordering::Acquire,
1058            )
1059            .is_err()
1060        {
1061            return false;
1062        }
1063        if self.page.publish_slot_graph(MappingSlotKey {
1064            space_id: self.mm_id,
1065            va: self.va,
1066        }) {
1067            true
1068        } else {
1069            let _ = self.state.compare_exchange(
1070                SlotState::Present as u8,
1071                SlotState::Detached as u8,
1072                Ordering::AcqRel,
1073                Ordering::Acquire,
1074            );
1075            false
1076        }
1077    }
1078
1079    fn detach_slot(&self) -> bool {
1080        if self
1081            .state
1082            .compare_exchange(
1083                SlotState::Present as u8,
1084                SlotState::Detached as u8,
1085                Ordering::AcqRel,
1086                Ordering::Acquire,
1087            )
1088            .is_err()
1089        {
1090            return false;
1091        }
1092        if !self.page.detach_slot_graph(MappingSlotKey {
1093            space_id: self.mm_id,
1094            va: self.va,
1095        }) {
1096            let _ = self.state.compare_exchange(
1097                SlotState::Detached as u8,
1098                SlotState::Present as u8,
1099                Ordering::AcqRel,
1100                Ordering::Acquire,
1101            );
1102            return false;
1103        }
1104        true
1105    }
1106
1107    pub(crate) fn publish_after_graph_replace(&self) -> bool {
1108        self.state
1109            .compare_exchange(
1110                SlotState::Reserved as u8,
1111                SlotState::Present as u8,
1112                Ordering::AcqRel,
1113                Ordering::Acquire,
1114            )
1115            .is_ok()
1116    }
1117
1118    pub(crate) fn detach_after_graph_replace(&self) -> bool {
1119        self.state
1120            .compare_exchange(
1121                SlotState::Present as u8,
1122                SlotState::Detached as u8,
1123                Ordering::AcqRel,
1124                Ordering::Acquire,
1125            )
1126            .is_ok()
1127    }
1128
1129    /// Relocates one installed mapping record without changing the page's
1130    /// mapping-reference cardinality.
1131    ///
1132    /// Linux `move_ptes()` moves a PTE while retaining the same folio/rmap
1133    /// ownership.  This is the equivalent software-ownership transition: the
1134    /// reverse-mapping key is replaced under the PageObject graph lock, then
1135    /// the replacement slot becomes Present and the old slot becomes Detached.
1136    /// A failed state transition restores the old graph before returning.
1137    pub(crate) fn relocate_to(&self, replacement: &Self) -> Result<(), MappingGraphError> {
1138        if self.state() != SlotState::Present || replacement.state() != SlotState::Reserved {
1139            return Err(MappingGraphError::SlotStateConflict);
1140        }
1141        if self.mm_id != replacement.mm_id
1142            || self.page_order != replacement.page_order
1143            || !Arc::ptr_eq(&self.page, &replacement.page)
1144        {
1145            return Err(MappingGraphError::SlotIdentityMismatch);
1146        }
1147
1148        let old_key = MappingSlotKey {
1149            space_id: self.mm_id,
1150            va: self.va,
1151        };
1152        let new_key = MappingSlotKey {
1153            space_id: replacement.mm_id,
1154            va: replacement.va,
1155        };
1156        if old_key == new_key {
1157            return Err(MappingGraphError::DuplicateNewSlot);
1158        }
1159
1160        self.page.replace_mapping_graph(&[old_key], &[new_key])?;
1161        if !replacement.publish_after_graph_replace() {
1162            if self
1163                .page
1164                .replace_mapping_graph(&[new_key], &[old_key])
1165                .is_err()
1166            {
1167                return Err(MappingGraphError::RollbackFailed);
1168            }
1169            return Err(MappingGraphError::SlotStateConflict);
1170        }
1171        if self.detach_after_graph_replace() {
1172            return Ok(());
1173        }
1174
1175        let replacement_reserved = replacement.reserve_after_graph_replace();
1176        let graph_restored = replacement_reserved
1177            && self
1178                .page
1179                .replace_mapping_graph(&[new_key], &[old_key])
1180                .is_ok();
1181        if !replacement_reserved || !graph_restored {
1182            return Err(MappingGraphError::RollbackFailed);
1183        }
1184        Err(MappingGraphError::SlotStateConflict)
1185    }
1186
1187    pub(crate) fn restore_after_graph_replace(&self) -> bool {
1188        self.state
1189            .compare_exchange(
1190                SlotState::Detached as u8,
1191                SlotState::Present as u8,
1192                Ordering::AcqRel,
1193                Ordering::Acquire,
1194            )
1195            .is_ok()
1196    }
1197
1198    pub(crate) fn reserve_after_graph_replace(&self) -> bool {
1199        self.state
1200            .compare_exchange(
1201                SlotState::Present as u8,
1202                SlotState::Reserved as u8,
1203                Ordering::AcqRel,
1204                Ordering::Acquire,
1205            )
1206            .is_ok()
1207    }
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212    #[cfg(not(axtest))]
1213    use core::sync::atomic::AtomicUsize;
1214
1215    use super::*;
1216
1217    #[cfg(not(axtest))]
1218    static RELEASES: AtomicUsize = AtomicUsize::new(0);
1219
1220    #[cfg(not(axtest))]
1221    fn record_release(_paddr: PhysAddr, bytes: usize) {
1222        assert_eq!(bytes, PAGE_SIZE_4K * 4);
1223        RELEASES.fetch_add(1, Ordering::Relaxed);
1224    }
1225
1226    #[test]
1227    fn shared_page_tracks_slots_until_detach() {
1228        let page = PageObject::new(
1229            PageId::new(1),
1230            FrameLease::new(PhysAddr::from_usize(0x1000)),
1231        );
1232        assert!(page.transition(PageState::Reserved, PageState::Present));
1233        let id = AddressSpaceId::allocate();
1234        let slot_a = MappingSlot::new(
1235            MappingId::new(1),
1236            id,
1237            VirtAddr::from_usize(0x4000),
1238            PageOrder::BASE,
1239            page.clone(),
1240            Some(RssKind::Anon),
1241        );
1242        let slot_b = MappingSlot::new(
1243            MappingId::new(1),
1244            id,
1245            VirtAddr::from_usize(0x5000),
1246            PageOrder::BASE,
1247            page.clone(),
1248            Some(RssKind::Anon),
1249        );
1250        assert!(slot_a.publish());
1251        assert!(slot_b.publish());
1252        assert_eq!(page.rmap.snapshot().len(), 2);
1253        assert!(slot_a.detach());
1254        assert_eq!(page.rmap.snapshot().len(), 1);
1255        assert!(slot_b.detach());
1256        assert!(page.rmap.is_empty());
1257    }
1258
1259    #[cfg_attr(axtest, axtest::axtest)]
1260    #[cfg_attr(not(axtest), test)]
1261    fn concurrent_mapping_publication_preserves_reserved_capacity() {
1262        let page = PageObject::new_present(
1263            PageId::new(7),
1264            FrameLease::new(PhysAddr::from_usize(0x1000)),
1265        );
1266        let first = MappingSlotKey {
1267            space_id: AddressSpaceId::allocate(),
1268            va: VirtAddr::from_usize(0x4000),
1269        };
1270        let second = MappingSlotKey {
1271            space_id: AddressSpaceId::allocate(),
1272            va: first.va,
1273        };
1274        // Both MMs prepare against the same page before either publishes.
1275        // Completing the second fork must not consume the first fork's space.
1276        let mut first_reservation = page.prepare_mapping_graph_replace(&[], &[first]).unwrap();
1277        page.replace_mapping_graph(&[], &[second]).unwrap();
1278        page.replace_mapping_graph_reserved(&[], &[first], &mut first_reservation)
1279            .unwrap();
1280        drop(first_reservation);
1281        assert_eq!(page.mapping_refs(), 2);
1282        let mappings = page.rmap.snapshot();
1283        assert_eq!(mappings.len(), 2);
1284        assert!(mappings.contains(&first));
1285        assert!(mappings.contains(&second));
1286        let cancelled = page.prepare_mapping_graph_replace(&[], &[first]).unwrap();
1287        drop(cancelled);
1288        assert_eq!(page.rmap.entries.lock().reserved, 0);
1289        assert_eq!(page.mapping_refs(), 2);
1290        page.replace_mapping_graph(&[first, second], &[]).unwrap();
1291        assert_eq!(page.mapping_refs(), 0);
1292        assert!(page.rmap.is_empty());
1293    }
1294
1295    #[cfg_attr(axtest, axtest::axtest)]
1296    #[cfg_attr(not(axtest), test)]
1297    fn lazy_free_page_requeues_when_a_shared_mapping_becomes_exclusive() {
1298        let page = PageObject::new_present(
1299            PageId::new(5),
1300            FrameLease::new(PhysAddr::from_usize(0xa000)),
1301        );
1302        let mapping = MappingId::new(5);
1303        let parent = MappingSlot::new(
1304            mapping,
1305            AddressSpaceId::allocate(),
1306            VirtAddr::from_usize(0xa000),
1307            PageOrder::BASE,
1308            page.clone(),
1309            Some(RssKind::Anon),
1310        );
1311        let child = MappingSlot::new(
1312            mapping,
1313            AddressSpaceId::allocate(),
1314            VirtAddr::from_usize(0xb000),
1315            PageOrder::BASE,
1316            page.clone(),
1317            Some(RssKind::Anon),
1318        );
1319        assert!(parent.publish());
1320        assert!(child.publish());
1321        assert!(page.mark_lazy_free());
1322        assert_eq!(page.mapping_refs(), 2);
1323
1324        let requests = super::super::lifecycle::lazy_free_reclaim_request_count_for_test();
1325        assert!(child.detach());
1326        assert_eq!(page.mapping_refs(), 1);
1327        assert!(
1328            super::super::lifecycle::lazy_free_reclaim_request_count_for_test() > requests,
1329            "the 2 -> 1 mapping transition must publish a new reclaim edge"
1330        );
1331        assert!(parent.detach());
1332    }
1333
1334    #[cfg_attr(axtest, axtest::axtest)]
1335    #[cfg_attr(not(axtest), test)]
1336    fn lazy_free_page_requeues_after_a_batch_graph_replacement() {
1337        let page = PageObject::new_present(
1338            PageId::new(6),
1339            FrameLease::new(PhysAddr::from_usize(0xc000)),
1340        );
1341        let first = MappingSlotKey {
1342            space_id: AddressSpaceId::allocate(),
1343            va: VirtAddr::from_usize(0xc000),
1344        };
1345        let second = MappingSlotKey {
1346            space_id: AddressSpaceId::allocate(),
1347            va: VirtAddr::from_usize(0xd000),
1348        };
1349        let replacement = MappingSlotKey {
1350            space_id: first.space_id,
1351            va: VirtAddr::from_usize(0xe000),
1352        };
1353        page.replace_mapping_graph(&[], &[first, second]).unwrap();
1354        assert!(page.mark_lazy_free());
1355        assert_eq!(page.mapping_refs(), 2);
1356
1357        let requests = super::super::lifecycle::lazy_free_reclaim_request_count_for_test();
1358        let mut reservation = page
1359            .prepare_mapping_graph_replace(&[first, second], &[replacement])
1360            .unwrap();
1361        page.replace_mapping_graph_reserved(&[first, second], &[replacement], &mut reservation)
1362            .unwrap();
1363        drop(reservation);
1364
1365        assert_eq!(page.mapping_refs(), 1);
1366        assert!(
1367            super::super::lifecycle::lazy_free_reclaim_request_count_for_test() > requests,
1368            "a batch graph replacement ending at one mapping must publish a reclaim edge"
1369        );
1370        page.replace_mapping_graph(&[replacement], &[]).unwrap();
1371    }
1372
1373    #[test]
1374    fn reserved_page_cannot_publish_a_mapping_slot() {
1375        let page = PageObject::new(
1376            PageId::new(2),
1377            FrameLease::new(PhysAddr::from_usize(0x2000)),
1378        );
1379        let slot = MappingSlot::new(
1380            MappingId::new(2),
1381            AddressSpaceId::allocate(),
1382            VirtAddr::from_usize(0x6000),
1383            PageOrder::BASE,
1384            page,
1385            None,
1386        );
1387        assert!(!slot.publish());
1388        assert_eq!(slot.state(), SlotState::Reserved);
1389    }
1390
1391    #[cfg_attr(axtest, axtest::axtest)]
1392    #[cfg_attr(not(axtest), test)]
1393    fn relocation_replaces_rmap_key_without_changing_mapping_refs() {
1394        let page = PageObject::new_present(
1395            PageId::new(4),
1396            FrameLease::new(PhysAddr::from_usize(0x4000)),
1397        );
1398        let mm_id = AddressSpaceId::allocate();
1399        let old_va = VirtAddr::from_usize(0x8000);
1400        let new_va = VirtAddr::from_usize(0x9000);
1401        let old = MappingSlot::new(
1402            MappingId::new(4),
1403            mm_id,
1404            old_va,
1405            PageOrder::BASE,
1406            page.clone(),
1407            Some(RssKind::Anon),
1408        );
1409        let replacement = MappingSlot::new(
1410            MappingId::new(4),
1411            mm_id,
1412            new_va,
1413            PageOrder::BASE,
1414            page.clone(),
1415            Some(RssKind::Anon),
1416        );
1417        assert!(old.publish());
1418        assert_eq!(page.mapping_refs(), 1);
1419
1420        old.relocate_to(&replacement).unwrap();
1421
1422        assert_eq!(old.state(), SlotState::Detached);
1423        assert_eq!(replacement.state(), SlotState::Present);
1424        assert_eq!(page.mapping_refs(), 1);
1425        assert_eq!(
1426            page.rmap.snapshot(),
1427            alloc::vec![MappingSlotKey {
1428                space_id: mm_id,
1429                va: new_va,
1430            }]
1431        );
1432        assert!(replacement.detach());
1433        assert_eq!(page.mapping_refs(), 0);
1434    }
1435
1436    #[test]
1437    fn eviction_cannot_resume_before_tlb_retirement() {
1438        let page = PageObject::new_present(
1439            PageId::new(3),
1440            FrameLease::new(PhysAddr::from_usize(0x3000)),
1441        );
1442        let lease = page.eviction_lease().unwrap();
1443
1444        // A published eviction deliberately drops its lease while the remote
1445        // receipt is outstanding.  Neither a second reclaimer nor the cache
1446        // may turn the page back into a reusable Present page at this point.
1447        drop(lease);
1448        assert_eq!(page.state(), PageState::Evicting);
1449        assert!(matches!(
1450            page.resume_eviction_lease(),
1451            Err(EvictionError::Busy)
1452        ));
1453
1454        page.complete_eviction_tlb();
1455        let resumed = page.resume_eviction_lease().unwrap();
1456        assert!(resumed.cancel());
1457        assert_eq!(page.state(), PageState::Present);
1458    }
1459
1460    #[test]
1461    fn split_frame_leases_release_the_allocation_once_after_the_last_subpage() {
1462        RELEASES.store(0, Ordering::Relaxed);
1463        let owner = FrameLease::owned_with_releaser(
1464            PhysAddr::from_usize(0x20_0000),
1465            PAGE_SIZE_4K * 4,
1466            record_release,
1467        );
1468        let first = owner.sublease(0, PAGE_SIZE_4K).unwrap();
1469        let last = owner.sublease(PAGE_SIZE_4K * 3, PAGE_SIZE_4K).unwrap();
1470        assert_eq!(first.paddr(), PhysAddr::from_usize(0x20_0000));
1471        assert_eq!(last.paddr(), PhysAddr::from_usize(0x20_3000));
1472        assert!(owner.sublease(PAGE_SIZE_4K * 4, PAGE_SIZE_4K).is_none());
1473
1474        drop(owner);
1475        drop(first);
1476        assert_eq!(RELEASES.load(Ordering::Relaxed), 0);
1477        drop(last);
1478        assert_eq!(RELEASES.load(Ordering::Relaxed), 1);
1479    }
1480}