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