Skip to main content

ostd/specs/mm/frame/
segment.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Spec/proof companion for [`crate::mm::frame::segment`].
3use vstd::prelude::*;
4use vstd_extra::drop_tracking::*;
5use vstd_extra::ownership::*;
6
7use core::ops::Range;
8
9use crate::mm::frame::{AnyFrameMeta, MetaSlot, Segment};
10use crate::mm::{Paddr, Vaddr, paddr_to_vaddr};
11use crate::specs::arch::kspace::FRAME_METADATA_RANGE;
12use crate::specs::arch::mm::{MAX_PADDR, PAGE_SIZE};
13use crate::specs::mm::frame::mapping::{META_SLOT_SIZE, frame_to_index, meta_addr};
14use crate::specs::mm::frame::meta_region_owners::MetaRegionOwners;
15use crate::specs::mm::virt_mem::MemView;
16
17verus! {
18
19impl<M: AnyFrameMeta + ?Sized> TrackDrop for Segment<M> {
20    /// The tracked state for `ManuallyDrop` purposes is the global
21    /// [`MetaRegionOwners`]. The [`SegmentOwner<M>`] is threaded in
22    /// separately via `verus_spec`, and the *real* per-segment obligation
23    /// (with the segment-range ledger entry) is held on `SegmentOwner`
24    /// itself — not via this `TrackDrop` impl. That keeps
25    /// `ManuallyDrop::new(self, Tracked(regions))` callable in places like
26    /// `Segment::split` / `Segment::into_raw` where the segment is
27    /// "temporarily forgotten" without an actual ledger event.
28    type State = MetaRegionOwners;
29
30    /// Real segment-range key. The token produced by `constructor_spec`
31    /// carries `self.range` as identity. The mint here does NOT insert
32    /// into `obligations` — the real per-segment entry is added by
33    /// [`Segment::from_unused`] and removed by [`Segment::drop`] directly.
34    /// Carrying `Range<Paddr>` on the token still strengthens the
35    /// discipline: a token forged for one segment can't masquerade as
36    /// belonging to another (the `consume_requires`/`drop_requires`
37    /// checks would refuse the mismatched key).
38    type Key = Range<Paddr>;
39
40    open spec fn key(self) -> Self::Key {
41        self.range
42    }
43
44    open spec fn constructor_requires(self, s: Self::State) -> bool {
45        true
46    }
47
48    open spec fn constructor_ensures(
49        self,
50        s0: Self::State,
51        s1: Self::State,
52        obl_key: Self::Key,
53    ) -> bool {
54        &&& s0 =~= s1
55        &&& obl_key == self.range
56    }
57
58    proof fn constructor_spec(self, tracked s: &mut Self::State) -> (tracked obl: DropObligation<
59        Self::Key,
60    >) {
61        DropObligation::tracked_mint(self.range)
62    }
63
64    open spec fn drop_requires(self, s: Self::State) -> bool {
65        s.inv()
66    }
67
68    open spec fn drop_ensures(self, s0: Self::State, s1: Self::State, obl_key: Self::Key) -> bool {
69        true
70    }
71
72    open spec fn consume_requires(self, s: Self::State, obl_key: Self::Key) -> bool {
73        true
74    }
75
76    open spec fn consume_ensures(
77        self,
78        s0: Self::State,
79        s1: Self::State,
80        obl_key: Self::Key,
81    ) -> bool {
82        s0 =~= s1
83    }
84
85    proof fn consume_obligation(
86        self,
87        tracked s: &mut Self::State,
88        tracked obl: DropObligation<Self::Key>,
89    ) {
90        // No-op: the token is ledger-less identity. The real segment-range
91        // ledger lives on `SegmentOwner` and is redeemed by
92        // `Segment::drop` directly, not via this hook.
93    }
94}
95
96/// A [`SegmentOwner<M>`] holds the permission tokens for all frames in the
97/// [`Segment<M>`] for verification purposes.
98///
99/// The `range` field tracks which physical-address range this owner corresponds
100/// to. It is a ghost-only field used by [`Self::inv`] to express the structural
101/// connection between `perms` and the segment's frames.
102/// Number of frames in a page-aligned physical range.
103#[verifier::inline]
104pub open spec fn seg_nframes(range: Range<Paddr>) -> int {
105    (range.end - range.start) / PAGE_SIZE as int
106}
107
108pub tracked struct SegmentOwner<M: AnyFrameMeta + ?Sized> {
109    /// The physical-address range of the segment that this owner corresponds to.
110    ///
111    /// Design B (Arc-style): the owner no longer holds the per-frame slot
112    /// permissions. Each frame's `simple_pptr::PointsTo<MetaSlot>` stays
113    /// canonical in `regions.slots[idx]` and is *borrowed* on drop/next;
114    /// the segment merely contributes one (forgotten) reference per frame.
115    ///
116    /// Per-frame linear-drop: the owner carries *no* obligation token. The
117    /// segment's "must drop" guarantee is enforced entirely by the per-frame
118    /// `regions.frame_obligations` multiset (one count per segment frame, see
119    /// [`SegmentOwner::relate_regions`]) combined with the boundary
120    /// `clean_inv()` check — silently dropping a `SegmentOwner` leaves those
121    /// counts outstanding and breaks `clean_inv()`. Redeem-time tokens are
122    /// fabricated on demand via `DropObligation::tracked_mint`, so no token
123    /// needs to travel with the owner.
124    pub ghost range: Range<Paddr>,
125    pub _marker: core::marker::PhantomData<M>,
126}
127
128impl<M: AnyFrameMeta + ?Sized> Inv for SegmentOwner<M> {
129    /// The invariant of a [`SegmentOwner`]:
130    ///
131    /// - the tracked `range` is page-aligned and within bounds;
132    /// - the number of permissions matches the number of frames in the range;
133    /// - each permission's address corresponds to the meta slot of its frame
134    ///   (so consecutive permissions are spaced by `META_SLOT_SIZE`);
135    /// - each permission is initialized and individually well-formed.
136    /// - the bundled obligation token is keyed to this owner's `range`.
137    open spec fn inv(self) -> bool {
138        &&& self.range.start % PAGE_SIZE == 0
139        &&& self.range.end % PAGE_SIZE == 0
140        &&& self.range.start <= self.range.end <= MAX_PADDR
141    }
142}
143
144impl<M: AnyFrameMeta + ?Sized> SegmentOwner<M> {
145    /// The cross-object relation between a [`SegmentOwner`] and the global
146    /// [`MetaRegionOwners`].
147    ///
148    /// For every frame `i` in the segment, this asserts:
149    /// - the slot owner is present in `regions` and the perm matches it,
150    /// - the slot's `self_addr` is consistent with its index,
151    /// - the slot has a live, non-`UNUSED` reference count,
152    /// - `raw_count == 1` (the segment holds one forgotten reference per frame),
153    /// - the slot's perm is *not* in `regions.slots` (it lives in `self.perms`),
154    /// - distinct frames in the segment map to distinct slot indices.
155    ///
156    /// This is an invariant preserved by every operation that transforms a
157    /// `SegmentOwner` together with `MetaRegionOwners` — analogous to
158    /// [`crate::specs::mm::frame::unique::UniqueFrameOwner::global_inv`] but
159    /// spanning all frames in a segment.
160    pub open spec fn relate_regions(self, regions: MetaRegionOwners) -> bool {
161        &&& forall|i: int|
162            #![trigger frame_to_index((self.range.start + i * PAGE_SIZE) as usize)]
163            0 <= i < seg_nframes(self.range) ==> {
164                let idx = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
165                // Per-frame linear-drop: the segment holds one (forgotten)
166                // reference per frame, recorded as a `frame_obligations` count.
167                // Combined with the boundary `clean_inv()` (which requires the
168                // multiset empty), this gates `Segment::drop`'s per-frame
169                // redeem against a genuine outstanding entry — the per-frame
170                // analogue of the old `obligations.contains(range)` check.
171                &&& regions.frame_obligations.count(idx) >= 1
172                &&& regions.slot_owners.contains_key(idx)
173                &&& regions.slots.contains_key(
174                    idx,
175                )
176                // Borrow-protocol transition: `raw_count` is dormant.
177                &&& regions.slot_owners[idx].self_addr == meta_addr(idx)
178                &&& regions.slot_owners[idx].inner_perms.ref_count.value()
179                    > 0
180                // Segment frames are shared (never `UNIQUE`); the upper
181                // bound also keeps post-`fetch_sub` out of the forbidden
182                // `(REF_COUNT_MAX, REF_COUNT_UNIQUE)` zone.
183                &&& regions.slot_owners[idx].inner_perms.ref_count.value()
184                    <= crate::mm::frame::meta::REF_COUNT_MAX
185                // A segment holds its frames as a unit; they are not
186                // mapped into any page table, so the slot carries no PTE
187                // paths. Needed to discharge `Frame::drop`'s strengthened
188                // precondition (`ref_count == 1 ==> paths_in_pt empty`)
189                // in the per-frame teardown loop.
190                &&& regions.slot_owners[idx].paths_in_pt.is_empty()
191                &&& regions.slot_owners[idx].usage
192                    == crate::specs::mm::frame::meta_owners::PageUsage::Frame
193            }
194        &&& forall|i: int, j: int|
195            #![trigger frame_to_index((self.range.start + i * PAGE_SIZE) as usize),
196                frame_to_index((self.range.start + j * PAGE_SIZE) as usize)]
197            0 <= i < j < seg_nframes(self.range) ==> frame_to_index(
198                (self.range.start + i * PAGE_SIZE) as usize,
199            ) != frame_to_index((self.range.start + j * PAGE_SIZE) as usize)
200    }
201
202    /// Manually instantiates the [`relate_regions`] forall at a specific index.
203    /// Use this to extract per-frame facts (slot_owner present, slot perm in
204    /// `regions.slots`, raw_count == 1, ref_count > 0, etc.) without fighting
205    /// trigger inference.
206    pub proof fn relate_regions_at(self, regions: MetaRegionOwners, i: int)
207        requires
208            self.relate_regions(regions),
209            0 <= i < seg_nframes(self.range),
210        ensures
211            ({
212                let idx = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
213                &&& regions.frame_obligations.count(idx) >= 1
214                &&& regions.slot_owners.contains_key(idx)
215                &&& regions.slots.contains_key(
216                    idx,
217                )
218                // Borrow-protocol transition: `raw_count` is dormant.
219                &&& regions.slot_owners[idx].self_addr == meta_addr(idx)
220                &&& regions.slot_owners[idx].inner_perms.ref_count.value() > 0
221                &&& regions.slot_owners[idx].inner_perms.ref_count.value()
222                    <= crate::mm::frame::meta::REF_COUNT_MAX
223                &&& regions.slot_owners[idx].paths_in_pt.is_empty()
224                &&& regions.slot_owners[idx].usage
225                    == crate::specs::mm::frame::meta_owners::PageUsage::Frame
226            }),
227    {
228        // Trigger the forall at index `i`.
229        let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
230    }
231
232    /// Manually instantiates the [`relate_regions`] distinctness forall at a
233    /// specific index pair: distinct in-range frames map to distinct slot
234    /// indices. Reusable lever for `from_unused`/`split`/`slice` proofs.
235    pub proof fn relate_regions_distinct(self, regions: MetaRegionOwners, i: int, j: int)
236        requires
237            self.relate_regions(regions),
238            0 <= i < j < seg_nframes(self.range),
239        ensures
240            frame_to_index((self.range.start + i * PAGE_SIZE) as usize) != frame_to_index(
241                (self.range.start + j * PAGE_SIZE) as usize,
242            ),
243    {
244        // Trigger the distinctness forall at `(i, j)`.
245        let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
246        let _ = frame_to_index((self.range.start + j * PAGE_SIZE) as usize);
247    }
248}
249
250impl<M: AnyFrameMeta + ?Sized> Segment<M> {
251    /// The well-formedness relation between a [`Segment`] and its owner:
252    ///
253    /// - the segment's range matches the range tracked by the owner;
254    /// - the number of permissions in the owner matches the number of frames in the segment;
255    /// - the physical address of each permission matches the corresponding frame in the segment.
256    ///
257    /// This is *only* the cross-object relation; callers are expected to also
258    /// state `self.inv()` (and where relevant `owner.inv()`) alongside. With
259    /// `owner.inv()` the perm-address and length conjuncts are consequences of
260    /// `self.range == owner.range`, but they are kept here for trigger
261    /// availability at sites that don't invoke `owner.inv()`.
262    ///
263    /// Interested readers are encouraged to see [`frame_to_index`] and [`meta_addr`] for how
264    /// we convert between physical addresses and meta region indices.
265    pub open spec fn wf(&self, owner: &SegmentOwner<M>) -> bool {
266        &&& self.range == owner.range
267    }
268
269    /// The bundled invariant for [`Segment`] operations that thread an `owner`
270    /// and the global `regions`: the segment's own invariant, its
271    /// well-formedness against the owner, the owner's invariant, the region
272    /// invariant, and the cross-object relation tying the owner to `regions`.
273    ///
274    /// Mirrors the `invariants` bundles used throughout the page-table / cursor
275    /// code — it collapses the five clauses repeated verbatim across `split`,
276    /// `slice`, `into_raw`, `next`, and `drop` into one predicate.
277    pub open spec fn invariants(&self, owner: &SegmentOwner<M>, regions: MetaRegionOwners) -> bool {
278        &&& self.inv()
279        &&& self.wf(owner)
280        &&& owner.inv()
281        &&& regions.inv()
282        &&& owner.relate_regions(regions)
283    }
284
285    /// Whether a [`MemView`] covers the segment through the kernel direct mapping.
286    ///
287    /// This predicate only describes the virtual-to-physical relation and the
288    /// presence of initialized backing frame contents.
289    pub open spec fn kernel_mem_view_covers(&self, view: &MemView) -> bool {
290        &&& self.inv()
291        &&& view.mappings.finite()
292        &&& view.mappings_are_disjoint()
293        &&& forall|vaddr: Vaddr|
294            #![trigger view.addr_transl(vaddr)]
295            paddr_to_vaddr(self.start_paddr()) <= vaddr < paddr_to_vaddr(self.start_paddr())
296                + self.end_paddr() - self.start_paddr() ==> {
297                &&& view.addr_transl(vaddr) is Some
298                &&& view.memory.contains_key(view.addr_transl(vaddr).unwrap().0)
299                &&& view.memory[view.addr_transl(vaddr).unwrap().0].inv()
300                &&& view.memory[view.addr_transl(vaddr).unwrap().0].contents[view.addr_transl(
301                    vaddr,
302                ).unwrap().1 as int] is Init
303            }
304        &&& forall|paddr: Paddr|
305            #![trigger paddr_to_vaddr(paddr)]
306            self.start_paddr() <= paddr < self.end_paddr() ==> {
307                let vaddr = paddr_to_vaddr(paddr);
308                &&& view.addr_transl(vaddr) is Some
309                &&& view.addr_transl(vaddr).unwrap().0 <= paddr
310                &&& paddr < view.addr_transl(vaddr).unwrap().0 + view.memory[view.addr_transl(
311                    vaddr,
312                ).unwrap().0].size@
313                &&& view.addr_transl(vaddr).unwrap().1 == paddr - view.addr_transl(vaddr).unwrap().0
314                &&& view.memory.contains_key(view.addr_transl(vaddr).unwrap().0)
315                &&& view.memory[view.addr_transl(vaddr).unwrap().0].inv()
316                &&& view.memory[view.addr_transl(vaddr).unwrap().0].contents[view.addr_transl(
317                    vaddr,
318                ).unwrap().1 as int] is Init
319            }
320    }
321}
322
323/// Helper spec: the slot index of the j-th frame in a segment whose physical
324/// range starts at `range_start`. Unlike a let-bound ghost closure (which Verus
325/// treats opaquely under SMT), a `spec fn` is auto-unfolded so equalities
326/// between `frame_idx_at(...)` and `frame_to_index(...)` are derivable.
327#[verifier::inline]
328pub open spec fn frame_idx_at(range_start: usize, j: int) -> usize {
329    frame_to_index((range_start + j * PAGE_SIZE) as usize)
330}
331
332/// The exact `frame_obligations` effect of recording one forgotten reference
333/// per frame for the first `n` frames of a segment starting at `range_start`:
334///
335/// - every segment frame's count grows by at least one (its recorded
336///   reference), and
337/// - the *frame condition*: every slot that is NOT a segment frame is left
338///   untouched.
339///
340/// The frame condition is the load-bearing part — it pins the *support* of the
341/// change to the segment's slots, so a caller's ledger accounting telescopes
342/// (it can conclude this only touched the segment's slots). Shared by
343/// [`tracked_mint_seg_obligations`] (which establishes it) and
344/// [`Segment::from_unused`] (which advertises it).
345pub open spec fn seg_obligations_minted(
346    pre: MetaRegionOwners,
347    post: MetaRegionOwners,
348    range_start: usize,
349    n: int,
350) -> bool {
351    // Each segment frame gains at least one entry.
352    &&& forall|i: int|
353        #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
354        0 <= i < n ==> post.frame_obligations.count(
355            frame_to_index((range_start + i * PAGE_SIZE) as usize),
356        ) >= pre.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
357            + 1
358        // Frame condition: every slot that is NOT a segment frame is untouched.
359    &&& forall|jdx: usize|
360        #![trigger post.frame_obligations.count(jdx)]
361        (forall|i: int|
362            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
363            0 <= i < n ==> jdx != frame_to_index((range_start + i * PAGE_SIZE) as usize))
364            ==> post.frame_obligations.count(jdx) == pre.frame_obligations.count(jdx)
365}
366
367/// Mints one per-frame `frame_obligations` entry for each of the first `n`
368/// frames of a segment starting at `range_start`. Used by
369/// [`Segment::from_unused`] to record the segment's forgotten per-frame
370/// references *after* the construction loop (which is net-zero on the ledger:
371/// each frame's `Frame::from_unused` mint is cancelled by its `ManuallyDrop`).
372///
373/// Per-frame obligations replace the old single range-keyed `obligations`
374/// ledger entry. Because `frame_obligations` is a multiset and minting only
375/// ever increases counts, no distinctness hypothesis on the frame indices is
376/// needed.
377///
378/// EXACT accounting (Tier A): the ensures pin the *support* of the change —
379/// every segment frame's count grows by at least one, and every *other* slot
380/// is untouched. This frame condition is what lets a caller's accounting
381/// telescope (it can conclude this call touched only the segment's slots).
382/// Each mint targets a segment index, so a non-segment slot is simply never a
383/// mint target — hence the frame condition needs no injectivity argument.
384pub proof fn tracked_mint_seg_obligations(
385    tracked regions: &mut MetaRegionOwners,
386    range_start: usize,
387    n: int,
388)
389    requires
390        0 <= n,
391        old(regions).inv(),
392    ensures
393        final(regions).inv(),
394        final(regions).slots =~= old(regions).slots,
395        final(regions).slot_owners =~= old(regions).slot_owners,
396        // Counts only grow.
397        forall|idx: usize|
398            #![trigger final(regions).frame_obligations.count(idx)]
399            final(regions).frame_obligations.count(idx) >= old(regions).frame_obligations.count(
400                idx,
401            ),
402        // The exact per-frame mint effect (segment frames +≥1, all else fixed).
403        seg_obligations_minted(*old(regions), *final(regions), range_start, n),
404    decreases n,
405{
406    let ghost g0 = *regions;
407    if n > 0 {
408        tracked_mint_seg_obligations(regions, range_start, n - 1);
409        let ghost gmid = *regions;
410        let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
411        let tracked _ = regions.tracked_mint_frame_obligation(idx);
412        // `regions.frame_obligations == gmid.frame_obligations.insert(idx)`,
413        // and the recursion already proved the strengthened ensures for the
414        // first `n-1` frames against `g0`. Bridge each ensures to `n`.
415        // Frame condition: a non-segment slot is untouched by the recursion
416        // (it omits the slot from `[0, n-1)`) and is not the mint target.
417        assert forall|jdx: usize|
418            #![trigger regions.frame_obligations.count(jdx)]
419            (forall|i: int|
420                #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
421                0 <= i < n ==> jdx != frame_to_index(
422                    (range_start + i * PAGE_SIZE) as usize,
423                )) implies regions.frame_obligations.count(jdx) == g0.frame_obligations.count(
424            jdx,
425        ) by {
426            assert(jdx != idx);
427            assert(gmid.frame_obligations.count(jdx) == g0.frame_obligations.count(jdx));
428        };
429        // Each segment frame gained at least one entry.
430        assert forall|i: int|
431            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
432            0 <= i < n implies regions.frame_obligations.count(
433            frame_to_index((range_start + i * PAGE_SIZE) as usize),
434        ) >= g0.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
435            + 1 by {
436            // i < n-1: recursion gives `gmid.count(idx_i) >= g0.count(idx_i)+1`;
437            // the mint only grows counts. i == n-1: `gmid.count(idx) >= g0.count(idx)`
438            // (monotone), and the mint adds exactly one at `idx`.
439            let _ = frame_to_index((range_start + i * PAGE_SIZE) as usize);
440        };
441    }
442}
443
444/// Redeems the per-frame `frame_obligations` entry for each of the first `n`
445/// frames of a segment starting at `range_start` — the inverse of
446/// [`tracked_mint_seg_obligations`]. Used by [`Segment::drop`] to drain the
447/// segment's retained per-frame references *before* the per-frame teardown
448/// loop, so that loop sees `count == 0` (as it did before the migration) and
449/// its `from_raw`(+1)/`frame.drop`(-1) pair nets to zero unchanged.
450///
451/// Unlike minting, redeeming requires the frame indices be *distinct*
452/// (redeeming one frame must not drop another's count below 1) and each count
453/// be `>= 1` up front — both supplied by [`SegmentOwner::relate_regions`].
454/// Leaves `slots`, `slot_owners`, and the (vestigial) range `obligations`
455/// ledger untouched.
456pub proof fn tracked_redeem_seg_obligations(
457    tracked regions: &mut MetaRegionOwners,
458    range_start: usize,
459    n: int,
460)
461    requires
462        0 <= n,
463        old(regions).inv(),
464        forall|i: int|
465            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
466            0 <= i < n ==> old(regions).frame_obligations.count(
467                frame_to_index((range_start + i * PAGE_SIZE) as usize),
468            ) >= 1,
469        forall|i: int, j: int|
470            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize),
471                frame_to_index((range_start + j * PAGE_SIZE) as usize)]
472            0 <= i < j < n ==> frame_to_index((range_start + i * PAGE_SIZE) as usize)
473                != frame_to_index((range_start + j * PAGE_SIZE) as usize),
474    ensures
475        final(regions).inv(),
476        final(regions).slots =~= old(regions).slots,
477        final(regions).slot_owners =~= old(regions).slot_owners,
478    decreases n,
479{
480    if n > 0 {
481        let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
482        let tracked tok = DropObligation::tracked_mint(idx);
483        regions.tracked_redeem_frame_obligation(tok);
484        // Redeeming `idx` (the n-1'th frame) left every earlier frame's count
485        // untouched, since the indices are distinct — so the `>= 1` hypothesis
486        // still holds for `[0, n-1)` and the recursive call's precondition is met.
487        assert forall|i: int|
488            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
489            0 <= i < n - 1 implies regions.frame_obligations.count(
490            frame_to_index((range_start + i * PAGE_SIZE) as usize),
491        ) >= 1 by {
492            assert(frame_to_index((range_start + i * PAGE_SIZE) as usize) != idx);
493        };
494        tracked_redeem_seg_obligations(regions, range_start, n - 1);
495    }
496}
497
498} // verus!