Skip to main content

starry_kernel/mm/aspace/
mutation.rs

1//! Transaction states shared by all address-space mutations.
2
3use alloc::{sync::Arc, vec::Vec};
4use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
5
6use ax_memory_addr::{VirtAddr, VirtAddrRange};
7use heapless::Vec as InlineVec;
8
9use super::{AddressSpaceId, VmEpoch, objects::FrameLease};
10use crate::sync::{IrqMutex, try_push_irq_vec, try_reserve_irq_vec};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MutationState {
14    Prepared,
15    Applied,
16    PublishedPendingTlb,
17    Published,
18    Retired,
19    Aborted,
20    NeedsRepair,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PublishEvent {
25    MappingPublished {
26        space_id: AddressSpaceId,
27        epoch: VmEpoch,
28    },
29    MappingRetired {
30        space_id: AddressSpaceId,
31        epoch: VmEpoch,
32    },
33}
34
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct VmaDelta {
37    pub inserted: u32,
38    pub removed: u32,
39    pub split: u32,
40    pub merged: u32,
41}
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub struct PteDelta {
45    pub mapped: u32,
46    pub unmapped: u32,
47    pub protected: u32,
48}
49
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct MappingDelta {
52    pub attached: u32,
53    pub detached: u32,
54}
55
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
57pub struct ResidentDelta {
58    pub anon: i64,
59    pub file: i64,
60    pub shmem: i64,
61}
62
63impl ResidentDelta {
64    pub const fn total(self) -> i64 {
65        self.anon
66            .saturating_add(self.file)
67            .saturating_add(self.shmem)
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum EvictionResult {
73    Retired,
74    Busy,
75    Unsupported,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct TlbRange {
80    pub start: VirtAddr,
81    pub size: usize,
82}
83
84impl TlbRange {
85    pub fn new(start: VirtAddr, size: usize) -> Option<Self> {
86        VirtAddrRange::try_from_start_size(start, size).map(|_| Self { start, size })
87    }
88
89    fn overlaps(self, other: Self) -> bool {
90        let Some(left) = VirtAddrRange::try_from_start_size(self.start, self.size) else {
91            return true;
92        };
93        let Some(right) = VirtAddrRange::try_from_start_size(other.start, other.size) else {
94            return true;
95        };
96        left.overlaps(right)
97    }
98}
99
100/// A shootdown obligation.  It is deliberately independent of the platform
101/// IPI implementation so a timeout cannot be mistaken for a completed flush.
102#[derive(Debug, Clone)]
103pub struct TlbRequest {
104    pub space_id: AddressSpaceId,
105    pub epoch: VmEpoch,
106    pub targets: usize,
107    pub ranges: InlineVec<TlbRange, MAX_INLINE_TLB_RANGES>,
108    acknowledged: usize,
109    ranges_collapsed_to_full_flush: bool,
110}
111
112// This is a batching threshold, not an ABI limit.  More disjoint ranges are
113// conservatively collapsed into one full-address-space flush, just as Linux
114// mmu_gather falls back to flushing a batch when its inline storage is full.
115const MAX_INLINE_TLB_RANGES: usize = 8;
116
117// A receipt emits exactly one publication and one retirement event.  Keeping
118// those records inline makes cloning a published receipt allocation-free.
119const MAX_PUBLISH_EVENTS: usize = 2;
120
121#[derive(Debug)]
122struct QuarantinedFrame {
123    frame: FrameLease,
124    request: TlbRequest,
125}
126
127/// Holds detached frames until every CPU named by a shootdown acknowledges it.
128///
129/// The queue is intentionally frame-only: VMA metadata and page-cache locks
130/// are never held while an IPI is sent or retried.  A timeout leaves entries in
131/// this queue, making unsafe reuse impossible.
132#[derive(Debug, Default)]
133pub struct TlbQuarantine {
134    entries: IrqMutex<Vec<QuarantinedFrame>>,
135}
136
137impl TlbQuarantine {
138    /// Adds a detached frame to the quarantine.
139    ///
140    /// The frame is returned inside [`QuarantineFailure`] when the queue
141    /// cannot reserve storage.  Dropping a frame on this path would release
142    /// physical memory while an old translation may still be live, so there
143    /// is deliberately no infallible or fire-and-forget variant of this API.
144    pub fn defer(&self, frame: FrameLease, request: TlbRequest) -> Result<(), QuarantineFailure> {
145        self.try_defer(frame, request)
146    }
147
148    pub fn try_defer(
149        &self,
150        frame: FrameLease,
151        request: TlbRequest,
152    ) -> Result<(), QuarantineFailure> {
153        if request.is_complete() {
154            // A local/full-flush request has no remote observer.  Do not put
155            // an already-safe frame in a queue that can only be drained by an
156            // acknowledgement callback.
157            return Ok(());
158        }
159        try_push_irq_vec(&self.entries, QuarantinedFrame { frame, request }).map_err(|entry| {
160            QuarantineFailure {
161                frame: entry.frame,
162                reason: QuarantineError::ResourceExhausted,
163            }
164        })
165    }
166
167    /// Fallible insertion that returns ownership of the frame on allocation
168    /// failure.  Callers performing teardown can then quarantine it in a
169    /// higher-level repair queue instead of accidentally dropping a frame
170    /// while a stale translation may still exist.
171    pub fn try_defer_recoverable(
172        &self,
173        frame: FrameLease,
174        request: TlbRequest,
175    ) -> Result<(), QuarantineFailure> {
176        self.try_defer(frame, request)
177    }
178
179    pub fn pending(&self) -> usize {
180        self.entries.lock().len()
181    }
182
183    pub fn requests(&self) -> Result<Vec<TlbRequest>, QuarantineError> {
184        let mut requests = Vec::new();
185        loop {
186            let required = self.entries.lock().len();
187            requests
188                .try_reserve_exact(required)
189                .map_err(|_| QuarantineError::ResourceExhausted)?;
190            let entries = self.entries.lock();
191            if entries.len() > requests.capacity() {
192                continue;
193            }
194            requests.extend(entries.iter().map(|entry| entry.request.clone()));
195            return Ok(requests);
196        }
197    }
198
199    pub fn contains_request(&self, space_id: AddressSpaceId, epoch: VmEpoch) -> bool {
200        self.entries
201            .lock()
202            .iter()
203            .any(|entry| entry.request.space_id == space_id && entry.request.epoch == epoch)
204    }
205
206    /// Removes entries whose obligations were already satisfied by a local
207    /// flush.  Remote acknowledgements are still handled by
208    /// [`Self::acknowledge`].
209    pub fn reap_ready(&self) -> Result<Vec<FrameLease>, QuarantineError> {
210        let mut released = Vec::new();
211        loop {
212            if released.len() == released.capacity() {
213                if !self
214                    .entries
215                    .lock()
216                    .iter()
217                    .any(|entry| entry.request.is_complete())
218                {
219                    return Ok(released);
220                }
221                released
222                    .try_reserve(1)
223                    .map_err(|_| QuarantineError::ResourceExhausted)?;
224            }
225            let entry = {
226                let mut entries = self.entries.lock();
227                entries
228                    .iter()
229                    .position(|entry| entry.request.is_complete())
230                    .map(|index| entries.swap_remove(index))
231            };
232            let Some(entry) = entry else {
233                return Ok(released);
234            };
235            // Both the request and the frame owner leave the IRQ-save queue
236            // before either can be dropped.  `push` cannot grow after the
237            // reservation above.
238            let QuarantinedFrame { frame, request } = entry;
239            released.push(frame);
240            drop(request);
241        }
242    }
243
244    /// Records one acknowledgement and returns frames that became reclaimable.
245    pub fn acknowledge(
246        &self,
247        space_id: AddressSpaceId,
248        epoch: VmEpoch,
249        cpu: usize,
250    ) -> Result<Vec<FrameLease>, QuarantineError> {
251        {
252            let mut entries = self.entries.lock();
253            for entry in entries.iter_mut() {
254                if entry.request.space_id == space_id && entry.request.epoch == epoch {
255                    let _ = entry.request.acknowledge(cpu);
256                }
257            }
258        }
259        self.reap_ready()
260    }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum QuarantineError {
265    ResourceExhausted,
266}
267
268/// Failure to record a TLB obligation while retaining ownership of the frame
269/// that must not be released yet.
270#[derive(Debug)]
271pub struct QuarantineFailure {
272    pub frame: FrameLease,
273    pub reason: QuarantineError,
274}
275
276impl TlbRequest {
277    pub fn new(space_id: AddressSpaceId, epoch: VmEpoch, targets: usize) -> Self {
278        Self {
279            space_id,
280            epoch,
281            targets,
282            ranges: InlineVec::new(),
283            acknowledged: 0,
284            ranges_collapsed_to_full_flush: false,
285        }
286    }
287
288    pub fn with_range(mut self, range: TlbRange) -> Self {
289        self.add_range(range);
290        self
291    }
292
293    /// Fallible variant used by prepare paths that must not turn metadata
294    /// pressure into a panic.
295    pub fn try_with_range(mut self, range: TlbRange) -> Result<Self, MutationError> {
296        self.add_range(range);
297        Ok(self)
298    }
299
300    fn add_range(&mut self, range: TlbRange) {
301        if self.ranges_collapsed_to_full_flush {
302            return;
303        }
304        if self.ranges.push(range).is_err() {
305            self.ranges.clear();
306            self.ranges_collapsed_to_full_flush = true;
307        }
308    }
309
310    pub const fn targets(&self) -> usize {
311        self.targets
312    }
313
314    pub const fn acknowledged_mask(&self) -> usize {
315        self.acknowledged
316    }
317
318    pub fn acknowledge(&mut self, cpu: usize) -> bool {
319        if cpu >= usize::BITS as usize {
320            return false;
321        }
322        let bit = 1usize << cpu;
323        if self.acknowledged & bit != 0 || self.targets & bit == 0 {
324            return false;
325        }
326        self.acknowledged |= bit;
327        true
328    }
329
330    pub fn pending(&self) -> usize {
331        self.targets & !self.acknowledged
332    }
333
334    pub fn is_complete(&self) -> bool {
335        self.pending() == 0
336    }
337
338    fn overlaps(&self, other: &Self) -> bool {
339        if self.space_id != other.space_id {
340            return false;
341        }
342        // An empty range list is the full-address-space fallback. This also
343        // covers an overfull inline range batch after it has been collapsed.
344        if self.ranges.is_empty() || other.ranges.is_empty() {
345            return true;
346        }
347        self.ranges
348            .iter()
349            .any(|left| other.ranges.iter().any(|right| left.overlaps(*right)))
350    }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum MutationError {
355    WrongState,
356    EpochConflict,
357    ApplyFailed,
358    TlbPending,
359    PendingTlbOverlap,
360    ResourceExhausted,
361    NeedsRepair,
362    EpochExhausted,
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366enum MutationPrecondition {
367    None,
368    NoPendingTlbOverlap,
369}
370
371/// Serialization point for all VMA/PTE publication.
372///
373/// `AddrSpace` is still protected by its sleepable outer mutex, but keeping the
374/// epoch and the typestate transition together prevents a future mutation
375/// caller from publishing a receipt against a stale root. The gate never
376/// allocates under its commit lock or performs page-table or file operations.
377pub struct MutationGate {
378    epoch: AtomicU64,
379    health: core::sync::atomic::AtomicU8,
380    #[cfg(test)]
381    fail_next_commit_before_publish: core::sync::atomic::AtomicBool,
382    #[cfg(test)]
383    last_retired_receipt: IrqMutex<Option<MutationReceipt>>,
384    /// Serializes the short epoch-CAS/publication section.  File I/O and page
385    /// table work happen before entering this gate; the lock therefore cannot
386    /// introduce a sleep-under-gate path while making the commit decision
387    /// linearizable.
388    commit_lock: IrqMutex<()>,
389    /// Published receipts whose remote TLB obligations are still outstanding.
390    /// Keeping the receipt here is important: returning `TlbPending` must not
391    /// drop the only record that ties detached frames to the shootdown.
392    pending: IrqMutex<Vec<MutationReceipt>>,
393}
394
395impl Default for MutationGate {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401impl MutationGate {
402    pub const fn new() -> Self {
403        Self {
404            epoch: AtomicU64::new(0),
405            health: core::sync::atomic::AtomicU8::new(0),
406            #[cfg(test)]
407            fail_next_commit_before_publish: core::sync::atomic::AtomicBool::new(false),
408            #[cfg(test)]
409            last_retired_receipt: IrqMutex::new(None),
410            commit_lock: IrqMutex::new(()),
411            pending: IrqMutex::new(Vec::new()),
412        }
413    }
414
415    pub fn current_epoch(&self) -> VmEpoch {
416        VmEpoch::new(self.epoch.load(Ordering::Acquire))
417    }
418
419    pub fn needs_repair(&self) -> bool {
420        self.health.load(Ordering::Acquire) != 0
421    }
422
423    pub fn mark_needs_repair(&self) {
424        self.health.store(1, Ordering::Release);
425    }
426
427    pub fn clear_repair(&self) {
428        self.health.store(0, Ordering::Release);
429    }
430
431    #[cfg(test)]
432    pub(crate) fn fail_next_commit_before_publish(&self) {
433        self.fail_next_commit_before_publish
434            .store(true, Ordering::Release);
435    }
436
437    #[cfg(test)]
438    pub(crate) fn last_retired_receipt(&self) -> Option<MutationReceipt> {
439        self.last_retired_receipt.lock().clone()
440    }
441
442    pub fn begin(&self, space_id: AddressSpaceId, targets: usize) -> PreparedMutation {
443        PreparedMutation::new(space_id, self.current_epoch(), targets)
444    }
445
446    /// Begins publication into a software-empty range.
447    ///
448    /// The resulting zero-target request may use the fresh-PTE fast path only
449    /// while no older unacknowledged shootdown overlaps its range. Commit
450    /// checks that precondition under the same serialization gate that
451    /// publishes the new epoch.
452    pub fn begin_fresh_mapping(&self, space_id: AddressSpaceId) -> PreparedMutation {
453        let mut mutation = PreparedMutation::new(space_id, self.current_epoch(), 0);
454        mutation.precondition = MutationPrecondition::NoPendingTlbOverlap;
455        mutation
456    }
457
458    /// Checks a prepared mutation before it changes the materialized PTE view.
459    ///
460    /// `AddrSpace` serializes the following apply phase with its outer mutex,
461    /// so an existing conflict can only disappear before commit. Commit still
462    /// repeats this check under the publication gate to make the invariant
463    /// independent of that caller-side locking discipline.
464    pub(super) fn validate_publish_preconditions(
465        &self,
466        mutation: &PreparedMutation,
467    ) -> Result<(), MutationError> {
468        let commit_guard = self.commit_lock.lock();
469        let pending_overlap = self.pending_overlap_request(mutation).is_some();
470        drop(commit_guard);
471        if pending_overlap {
472            Err(MutationError::PendingTlbOverlap)
473        } else {
474            Ok(())
475        }
476    }
477
478    /// Copies one blocking request into inline storage. The fault owner can
479    /// cancel its unpublished candidate, leave all MM locks, and service this
480    /// older obligation before retrying; no heap snapshot is required.
481    pub(super) fn pending_overlap_request(
482        &self,
483        mutation: &PreparedMutation,
484    ) -> Option<TlbRequest> {
485        if mutation.precondition != MutationPrecondition::NoPendingTlbOverlap {
486            return None;
487        }
488        self.pending
489            .lock()
490            .iter()
491            .find(|pending| {
492                pending
493                    .tlb_obligation
494                    .overlaps(&mutation.receipt.tlb_obligation)
495            })
496            .map(|pending| pending.tlb_obligation.clone())
497    }
498
499    /// Begins a mutation whose final shootdown targets are frozen at commit.
500    ///
501    /// Scheduler activation can race the prepare/apply phase. Keeping the
502    /// shared active-CPU source until the short publication step lets commit
503    /// include a CPU that installed this root after prepare, while the
504    /// resulting [`MutationReceipt`] still contains an immutable mask.
505    pub fn begin_with_active_targets(
506        &self,
507        space_id: AddressSpaceId,
508        active_targets: Arc<AtomicUsize>,
509    ) -> PreparedMutation {
510        PreparedMutation::new_with_active_targets(space_id, self.current_epoch(), active_targets)
511    }
512
513    /// Publishes a fully applied transaction and advances the epoch.
514    ///
515    /// A pending TLB obligation is returned as an error; the caller must retain
516    /// the receipt and complete the acknowledgements before reclaiming old
517    /// mappings.  `AddrSpace` uses a zero-target request for its current local
518    /// page-table path and therefore takes this fast path synchronously.
519    pub fn commit(&self, mut mutation: PreparedMutation) -> Result<MutationReceipt, MutationError> {
520        #[cfg(test)]
521        if self
522            .fail_next_commit_before_publish
523            .swap(false, Ordering::AcqRel)
524        {
525            return Err(MutationError::ResourceExhausted);
526        }
527        if self.needs_repair() {
528            return Err(MutationError::NeedsRepair);
529        }
530        mutation.freeze_active_targets();
531        let needs_pending_slot = !mutation.receipt.tlb_obligation.is_complete();
532        // Capacity preparation must not run while the IRQ-saving commit gate
533        // is held.  Recheck after taking the gate because another commit may
534        // consume the observed spare slot; acknowledgements can only remove
535        // entries, so once this loop succeeds the slot remains ours until
536        // publication finishes.
537        let _commit_guard = loop {
538            if needs_pending_slot {
539                try_reserve_irq_vec(&self.pending, 1)
540                    .map_err(|_| MutationError::ResourceExhausted)?;
541            }
542            let guard = self.commit_lock.lock();
543            let has_capacity = !needs_pending_slot || {
544                let pending = self.pending.lock();
545                pending.len() < pending.capacity()
546            };
547            if has_capacity {
548                break guard;
549            }
550            drop(guard);
551        };
552        let pending_overlap = self.pending_overlap_request(&mutation).is_some();
553        if pending_overlap {
554            // Drop the IRQ-saving publication gate before dropping the
555            // prepared transaction. The latter may gain owned resources in
556            // future prepare phases and must never destroy them under a gate.
557            drop(_commit_guard);
558            return Err(MutationError::PendingTlbOverlap);
559        }
560        let base_epoch = self.current_epoch();
561        let new_epoch = base_epoch
562            .checked_next()
563            .ok_or(MutationError::EpochExhausted)?;
564        // Epoch exhaustion must be discovered while the receipt is still in
565        // Prepared.  Once `apply` has consumed it, returning a normal error
566        // would falsely claim that an Applied transaction was aborted even
567        // though no inverse/preimage was available here.
568        let applied = mutation.apply(base_epoch)?;
569        if self
570            .epoch
571            .compare_exchange(
572                base_epoch.get(),
573                new_epoch.get(),
574                Ordering::AcqRel,
575                Ordering::Acquire,
576            )
577            .is_err()
578        {
579            let _ = applied.abort();
580            return Err(MutationError::EpochConflict);
581        }
582        let pending = applied.publish(new_epoch);
583        if pending.receipt().tlb_obligation.is_complete() {
584            let published = pending.finish()?;
585            let receipt = published.retire();
586            #[cfg(test)]
587            {
588                *self.last_retired_receipt.lock() = Some(receipt.clone());
589            }
590            return Ok(receipt);
591        }
592
593        self.pending.lock().push(pending.into_receipt());
594        Err(MutationError::TlbPending)
595    }
596
597    /// Number of receipts waiting for remote TLB acknowledgement.
598    pub fn pending_count(&self) -> usize {
599        self.pending.lock().len()
600    }
601
602    pub fn pending_requests(&self) -> Result<Vec<TlbRequest>, MutationError> {
603        let mut requests = Vec::new();
604        loop {
605            let required = self.pending.lock().len();
606            requests
607                .try_reserve_exact(required)
608                .map_err(|_| MutationError::ResourceExhausted)?;
609            let pending = self.pending.lock();
610            if pending.len() > requests.capacity() {
611                continue;
612            }
613            requests.extend(pending.iter().map(|receipt| receipt.tlb_obligation.clone()));
614            return Ok(requests);
615        }
616    }
617
618    /// Returns one immutable shootdown request without exposing the pending
619    /// receipt or its mutation state.  The address-space owner uses this to
620    /// hand the obligation to the architecture TLB service after publication.
621    pub fn pending_request(&self, space_id: AddressSpaceId, epoch: VmEpoch) -> Option<TlbRequest> {
622        self.pending
623            .lock()
624            .iter()
625            .find(|receipt| {
626                receipt.tlb_obligation.space_id == space_id && receipt.new_epoch == epoch
627            })
628            .map(|receipt| receipt.tlb_obligation.clone())
629    }
630
631    /// Acknowledge one CPU for a published receipt.  The receipt remains in
632    /// quarantine until the last requested CPU confirms the invalidation.
633    pub fn acknowledge(
634        &self,
635        space_id: AddressSpaceId,
636        epoch: VmEpoch,
637        cpu: usize,
638    ) -> Result<Option<MutationReceipt>, MutationError> {
639        let mut pending = self.pending.lock();
640        let Some(index) = pending.iter().position(|receipt| {
641            receipt.tlb_obligation.space_id == space_id && receipt.new_epoch == epoch
642        }) else {
643            return Err(MutationError::WrongState);
644        };
645        let receipt = &mut pending[index];
646        if !receipt.tlb_obligation.acknowledge(cpu) {
647            return Err(MutationError::TlbPending);
648        }
649        if !receipt.tlb_obligation.is_complete() {
650            return Ok(None);
651        }
652        let mut receipt = pending.swap_remove(index);
653        receipt.state = MutationState::Retired;
654        receipt
655            .events
656            .push(PublishEvent::MappingRetired { space_id, epoch })
657            .expect("a receipt emits one retirement event");
658        #[cfg(test)]
659        {
660            *self.last_retired_receipt.lock() = Some(receipt.clone());
661        }
662        Ok(Some(receipt))
663    }
664}
665
666#[derive(Debug, Clone)]
667pub struct MutationReceipt {
668    pub base_epoch: VmEpoch,
669    pub new_epoch: VmEpoch,
670    pub vma_delta: VmaDelta,
671    pub pte_delta: PteDelta,
672    pub mapping_delta: MappingDelta,
673    pub resident_delta: ResidentDelta,
674    pub tlb_obligation: TlbRequest,
675    events: InlineVec<PublishEvent, MAX_PUBLISH_EVENTS>,
676    state: MutationState,
677}
678
679impl MutationReceipt {
680    pub fn state(&self) -> MutationState {
681        self.state
682    }
683
684    pub fn events(&self) -> &[PublishEvent] {
685        &self.events
686    }
687
688    pub fn space_id(&self) -> AddressSpaceId {
689        self.tlb_obligation.space_id
690    }
691
692    pub fn tlb_pending(&self) -> usize {
693        self.tlb_obligation.pending()
694    }
695}
696
697/// Pre-publication transaction.  Dropping this value is an abort and never
698/// changes the published address-space epoch.
699pub struct PreparedMutation {
700    receipt: MutationReceipt,
701    active_targets: Option<Arc<AtomicUsize>>,
702    precondition: MutationPrecondition,
703}
704
705impl PreparedMutation {
706    pub fn new(space_id: AddressSpaceId, base_epoch: VmEpoch, targets: usize) -> Self {
707        Self {
708            receipt: MutationReceipt {
709                base_epoch,
710                new_epoch: base_epoch,
711                vma_delta: VmaDelta::default(),
712                pte_delta: PteDelta::default(),
713                mapping_delta: MappingDelta::default(),
714                resident_delta: ResidentDelta::default(),
715                tlb_obligation: TlbRequest::new(space_id, base_epoch, targets),
716                events: InlineVec::new(),
717                state: MutationState::Prepared,
718            },
719            active_targets: None,
720            precondition: MutationPrecondition::None,
721        }
722    }
723
724    fn new_with_active_targets(
725        space_id: AddressSpaceId,
726        base_epoch: VmEpoch,
727        active_targets: Arc<AtomicUsize>,
728    ) -> Self {
729        let targets = active_targets.load(Ordering::Acquire);
730        let mut mutation = Self::new(space_id, base_epoch, targets);
731        mutation.active_targets = Some(active_targets);
732        mutation
733    }
734
735    /// Converts the live scheduler mask into the immutable publication mask.
736    ///
737    /// PTE apply has completed before `MutationGate::commit` calls this. A CPU
738    /// that was active during apply and remains active is therefore included.
739    /// A CPU removed from the mask has already installed another root and
740    /// completed the full-flush fallback before releasing its non-cloneable
741    /// activation lease. Keeping the prepare-time bit would therefore invent
742    /// an obligation for a CPU that can no longer retain the preimage. A CPU
743    /// that activates after this load installs the already-updated root through
744    /// the same root-switch flush path.
745    fn freeze_active_targets(&mut self) {
746        if let Some(active_targets) = self.active_targets.take() {
747            self.receipt.tlb_obligation.targets = active_targets.load(Ordering::Acquire);
748        }
749    }
750
751    pub fn receipt(&self) -> &MutationReceipt {
752        &self.receipt
753    }
754
755    /// Records the metadata deltas reserved during `prepare`.  These setters
756    /// do not publish anything; they only make the eventual receipt auditable.
757    pub fn set_vma_delta(&mut self, delta: VmaDelta) {
758        self.receipt.vma_delta = delta;
759    }
760
761    pub fn set_pte_delta(&mut self, delta: PteDelta) {
762        self.receipt.pte_delta = delta;
763    }
764
765    pub fn set_mapping_delta(&mut self, delta: MappingDelta) {
766        self.receipt.mapping_delta = delta;
767    }
768
769    pub fn set_resident_delta(&mut self, delta: ResidentDelta) {
770        self.receipt.resident_delta = delta;
771    }
772
773    pub fn add_tlb_range(&mut self, range: TlbRange) {
774        self.receipt.tlb_obligation.add_range(range);
775    }
776
777    pub fn try_reserve_tlb_ranges(&mut self, additional: usize) -> Result<(), MutationError> {
778        let request = &mut self.receipt.tlb_obligation;
779        if !request.ranges_collapsed_to_full_flush
780            && request.ranges.len().saturating_add(additional) > MAX_INLINE_TLB_RANGES
781        {
782            request.ranges.clear();
783            request.ranges_collapsed_to_full_flush = true;
784        }
785        Ok(())
786    }
787
788    pub fn try_add_tlb_range(&mut self, range: TlbRange) -> Result<(), MutationError> {
789        self.receipt.tlb_obligation.add_range(range);
790        Ok(())
791    }
792
793    pub fn apply(mut self, current_epoch: VmEpoch) -> Result<AppliedMutation, MutationError> {
794        if self.receipt.state != MutationState::Prepared {
795            return Err(MutationError::WrongState);
796        }
797        if current_epoch != self.receipt.base_epoch {
798            self.receipt.state = MutationState::Aborted;
799            return Err(MutationError::EpochConflict);
800        }
801        self.receipt.state = MutationState::Applied;
802        Ok(AppliedMutation {
803            receipt: self.receipt,
804        })
805    }
806}
807
808pub struct AppliedMutation {
809    receipt: MutationReceipt,
810}
811
812impl AppliedMutation {
813    pub fn publish(mut self, new_epoch: VmEpoch) -> PublishedPendingTlb {
814        self.receipt.new_epoch = new_epoch;
815        self.receipt.tlb_obligation.epoch = new_epoch;
816        self.receipt
817            .events
818            .push(PublishEvent::MappingPublished {
819                space_id: self.receipt.tlb_obligation.space_id,
820                epoch: new_epoch,
821            })
822            .expect("a receipt emits one publication event");
823        self.receipt.state = MutationState::PublishedPendingTlb;
824        PublishedPendingTlb {
825            receipt: self.receipt,
826        }
827    }
828
829    pub fn abort(mut self) -> Result<(), MutationError> {
830        self.receipt.state = MutationState::Aborted;
831        Ok(())
832    }
833}
834
835pub struct PublishedPendingTlb {
836    receipt: MutationReceipt,
837}
838
839impl PublishedPendingTlb {
840    pub fn receipt(&self) -> &MutationReceipt {
841        &self.receipt
842    }
843
844    pub fn acknowledge(mut self, cpu: usize) -> Result<Self, MutationError> {
845        if !self.receipt.tlb_obligation.acknowledge(cpu) {
846            return Err(MutationError::TlbPending);
847        }
848        Ok(self)
849    }
850
851    pub fn finish(mut self) -> Result<PublishedMutation, MutationError> {
852        if !self.receipt.tlb_obligation.is_complete() {
853            return Err(MutationError::TlbPending);
854        }
855        self.receipt.state = MutationState::Published;
856        Ok(PublishedMutation {
857            receipt: self.receipt,
858        })
859    }
860
861    pub(crate) fn into_receipt(self) -> MutationReceipt {
862        self.receipt
863    }
864}
865
866pub struct PublishedMutation {
867    receipt: MutationReceipt,
868}
869
870impl PublishedMutation {
871    pub fn receipt(&self) -> &MutationReceipt {
872        &self.receipt
873    }
874
875    pub fn retire(mut self) -> MutationReceipt {
876        self.receipt.state = MutationState::Retired;
877        self.receipt
878            .events
879            .push(PublishEvent::MappingRetired {
880                space_id: self.receipt.tlb_obligation.space_id,
881                epoch: self.receipt.new_epoch,
882            })
883            .expect("a receipt emits one retirement event");
884        self.receipt
885    }
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    #[cfg_attr(axtest, axtest::axtest)]
893    #[cfg_attr(not(axtest), test)]
894    fn publish_cannot_retire_before_all_tlb_acks() {
895        let id = AddressSpaceId::allocate();
896        let prepared = PreparedMutation::new(id, VmEpoch::new(4), 0b11);
897        let applied = prepared.apply(VmEpoch::new(4)).unwrap();
898        let pending = applied.publish(VmEpoch::new(5));
899        assert!(!pending.receipt().tlb_obligation.is_complete());
900        let pending = pending.acknowledge(0).unwrap();
901        assert!(!pending.receipt().tlb_obligation.is_complete());
902        let pending = pending.acknowledge(1).unwrap();
903        assert!(pending.receipt().tlb_obligation.is_complete());
904        let published = pending.finish().unwrap();
905        assert_eq!(published.receipt().state(), MutationState::Published);
906        assert_eq!(published.retire().state(), MutationState::Retired);
907    }
908
909    #[cfg_attr(axtest, axtest::axtest)]
910    #[cfg_attr(not(axtest), test)]
911    fn stale_epoch_aborts_without_publication() {
912        let prepared = PreparedMutation::new(AddressSpaceId::allocate(), VmEpoch::new(7), 0);
913        assert!(matches!(
914            prepared.apply(VmEpoch::new(8)),
915            Err(MutationError::EpochConflict)
916        ));
917    }
918
919    #[cfg_attr(axtest, axtest::axtest)]
920    #[cfg_attr(not(axtest), test)]
921    fn quarantine_releases_frame_only_after_acknowledgement() {
922        let space_id = AddressSpaceId::allocate();
923        let epoch = VmEpoch::new(3);
924        let request = TlbRequest::new(space_id, epoch, 0b11);
925        let quarantine = TlbQuarantine::default();
926        quarantine
927            .defer(
928                FrameLease::new(ax_memory_addr::PhysAddr::from_usize(0x2000)),
929                request,
930            )
931            .expect("quarantine insertion must retain frame ownership");
932        assert_eq!(quarantine.pending(), 1);
933        assert!(
934            quarantine
935                .acknowledge(space_id, epoch, 0)
936                .unwrap()
937                .is_empty()
938        );
939        let released = quarantine.acknowledge(space_id, epoch, 1).unwrap();
940        assert_eq!(released.len(), 1);
941        assert_eq!(
942            released[0].paddr(),
943            ax_memory_addr::PhysAddr::from_usize(0x2000)
944        );
945        assert_eq!(quarantine.pending(), 0);
946    }
947
948    #[cfg_attr(axtest, axtest::axtest)]
949    #[cfg_attr(not(axtest), test)]
950    fn quarantine_does_not_retain_local_flushes() {
951        let quarantine = TlbQuarantine::default();
952        quarantine
953            .defer(
954                FrameLease::new(ax_memory_addr::PhysAddr::from_usize(0x3000)),
955                TlbRequest::new(AddressSpaceId::allocate(), VmEpoch::new(1), 0),
956            )
957            .expect("completed local flush needs no queue allocation");
958        assert_eq!(quarantine.pending(), 0);
959        assert!(quarantine.requests().unwrap().is_empty());
960    }
961
962    #[cfg_attr(axtest, axtest::axtest)]
963    #[cfg_attr(not(axtest), test)]
964    fn gate_retains_pending_receipt_until_last_ack() {
965        let gate = MutationGate::new();
966        let id = AddressSpaceId::allocate();
967        let mutation = gate.begin(id, 0b11);
968        assert_eq!(
969            gate.commit(mutation).unwrap_err(),
970            MutationError::TlbPending
971        );
972        assert_eq!(gate.pending_count(), 1);
973        assert!(gate.acknowledge(id, VmEpoch::new(1), 0).unwrap().is_none());
974        let receipt = gate
975            .acknowledge(id, VmEpoch::new(1), 1)
976            .unwrap()
977            .expect("last acknowledgement retires receipt");
978        assert_eq!(receipt.state(), MutationState::Retired);
979        assert_eq!(gate.pending_count(), 0);
980    }
981
982    #[cfg_attr(axtest, axtest::axtest)]
983    #[cfg_attr(not(axtest), test)]
984    fn commit_freezes_cpus_activated_after_prepare() {
985        let gate = MutationGate::new();
986        let id = AddressSpaceId::allocate();
987        let active_targets = Arc::new(AtomicUsize::new(0b0001));
988        let mutation = gate.begin_with_active_targets(id, active_targets.clone());
989
990        // CPU 2 installs this address-space root while the mutation is still
991        // applying. The old prepare-time snapshot omitted it.
992        active_targets.fetch_or(0b0100, Ordering::Release);
993
994        assert_eq!(
995            gate.commit(mutation).unwrap_err(),
996            MutationError::TlbPending
997        );
998        let request = gate
999            .pending_request(id, VmEpoch::new(1))
1000            .expect("published mutation retains its shootdown request");
1001        assert_eq!(request.targets(), 0b0101);
1002    }
1003
1004    #[cfg_attr(axtest, axtest::axtest)]
1005    #[cfg_attr(not(axtest), test)]
1006    fn oversized_tlb_range_batch_falls_back_to_full_flush() {
1007        let gate = MutationGate::new();
1008        let id = AddressSpaceId::allocate();
1009        let mut mutation = gate.begin(id, 1);
1010        for index in 0..=MAX_INLINE_TLB_RANGES {
1011            mutation.add_tlb_range(
1012                TlbRange::new(VirtAddr::from_usize(index * 0x1000), 0x1000).unwrap(),
1013            );
1014        }
1015
1016        assert_eq!(
1017            gate.commit(mutation).unwrap_err(),
1018            MutationError::TlbPending
1019        );
1020        let request = gate
1021            .pending_request(id, VmEpoch::new(1))
1022            .expect("published mutation retains its shootdown request");
1023        assert!(
1024            request.ranges.is_empty(),
1025            "an overfull inline batch must conservatively request a full flush"
1026        );
1027    }
1028
1029    #[cfg_attr(axtest, axtest::axtest)]
1030    #[cfg_attr(not(axtest), test)]
1031    fn prepared_failure_does_not_publish_or_advance_epoch() {
1032        let gate = MutationGate::new();
1033        let id = AddressSpaceId::allocate();
1034        gate.fail_next_commit_before_publish();
1035
1036        assert_eq!(
1037            gate.commit(gate.begin(id, 0)).unwrap_err(),
1038            MutationError::ResourceExhausted
1039        );
1040        assert_eq!(gate.current_epoch(), VmEpoch::new(0));
1041        assert_eq!(gate.pending_count(), 0);
1042        assert!(gate.last_retired_receipt().is_none());
1043
1044        let receipt = gate
1045            .commit(gate.begin(id, 0))
1046            .expect("a later prepared mutation remains publishable");
1047        assert_eq!(receipt.state(), MutationState::Retired);
1048        assert_eq!(gate.current_epoch(), VmEpoch::new(1));
1049        let recorded = gate
1050            .last_retired_receipt()
1051            .expect("retired receipt remains observable to the test hook");
1052        assert_eq!(recorded.state(), receipt.state());
1053        assert_eq!(recorded.new_epoch, receipt.new_epoch);
1054    }
1055
1056    #[cfg_attr(axtest, axtest::axtest)]
1057    #[cfg_attr(not(axtest), test)]
1058    fn quarantine_retry_ignores_unrelated_acknowledgements() {
1059        let space_id = AddressSpaceId::allocate();
1060        let epoch = VmEpoch::new(9);
1061        let quarantine = TlbQuarantine::default();
1062        quarantine
1063            .defer(
1064                FrameLease::new(ax_memory_addr::PhysAddr::from_usize(0x4000)),
1065                TlbRequest::new(space_id, epoch, 1usize << 3),
1066            )
1067            .expect("quarantine insertion must retain frame ownership");
1068
1069        assert!(
1070            quarantine
1071                .acknowledge(space_id, epoch, 2)
1072                .unwrap()
1073                .is_empty()
1074        );
1075        assert_eq!(quarantine.pending(), 1);
1076        assert!(
1077            quarantine
1078                .acknowledge(space_id, VmEpoch::new(10), 3)
1079                .unwrap()
1080                .is_empty()
1081        );
1082        assert_eq!(quarantine.pending(), 1);
1083
1084        let released = quarantine.acknowledge(space_id, epoch, 3).unwrap();
1085        assert_eq!(released.len(), 1);
1086        assert_eq!(
1087            released[0].paddr(),
1088            ax_memory_addr::PhysAddr::from_usize(0x4000)
1089        );
1090        assert_eq!(quarantine.pending(), 0);
1091    }
1092}