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 core::ops::Range;
4
5use vstd::prelude::*;
6
7use vstd_extra::{drop_tracking::*, ownership::*};
8
9use crate::specs::{
10    arch::PAGE_SIZE,
11    mm::{
12        frame::{
13            mapping::{frame_to_index, index_to_meta},
14            meta_region_owners::MetaRegionOwners,
15        },
16        virt_mem::MemView,
17    },
18};
19
20use crate::mm::{
21    Paddr, Vaddr,
22    frame::{AnyFrameMeta, Segment},
23    paddr_to_vaddr,
24};
25
26verus! {
27
28impl<M: AnyFrameMeta + ?Sized> TrackDrop for Segment<M> {
29    /// The tracked state for `ManuallyDrop` purposes is the global
30    /// [`MetaRegionOwners`]. The real per-segment obligation is represented
31    /// by one entry per frame in `MetaRegionOwners::frame_obligations`, not
32    /// by this `TrackDrop` impl. That keeps
33    /// `ManuallyDrop::new(self)` callable in places like
34    /// `Segment::split` / `Segment::into_raw` where the segment is
35    /// "temporarily forgotten" without an actual ledger event.
36    type State = MetaRegionOwners;
37
38    /// Real segment-range key. The token produced by `constructor_spec`
39    /// carries `self.range` as identity. The mint here does NOT insert
40    /// into `obligations` — the real per-segment entry is added by
41    /// [`Segment::from_unused`] and removed by [`Segment::drop`] directly.
42    /// Carrying `Range<Paddr>` on the token still strengthens the
43    /// discipline: a token forged for one segment can't masquerade as
44    /// belonging to another (the `consume_requires`/`drop_requires`
45    /// checks would refuse the mismatched key).
46    type Obligation = DropObligation<Range<Paddr>>;
47
48    open spec fn tracked_redeem_requires(self, s: Self::State) -> bool {
49        true
50    }
51
52    open spec fn tracked_redeem_ensures(
53        self,
54        s0: Self::State,
55        s1: Self::State,
56        obl: Self::Obligation,
57    ) -> bool {
58        &&& s0 =~= s1
59        &&& obl.value() == self.range
60    }
61
62    proof fn tracked_redeem(self, tracked s: &mut Self::State) -> (tracked obl: Self::Obligation) {
63        DropObligation::tracked_mint(self.range)
64    }
65
66    open spec fn drop_requires(self, s: Self::State, obl: Self::Obligation) -> bool {
67        &&& s.inv()
68        &&& obl.value() == self.range
69    }
70
71    open spec fn drop_ensures(
72        self,
73        s0: Self::State,
74        s1: Self::State,
75        obl: Self::Obligation,
76    ) -> bool {
77        true
78    }
79}
80
81/// Number of frames in a page-aligned physical range.
82#[verifier::inline]
83pub open spec fn seg_nframes(range: Range<Paddr>) -> int {
84    (range.end - range.start) / PAGE_SIZE as int
85}
86
87impl<M: AnyFrameMeta + ?Sized> Segment<M> {
88    /// The cross-object relation between a [`Segment`] and the global
89    /// [`MetaRegionOwners`].
90    ///
91    /// For every frame `i` in the segment, this asserts:
92    /// - the slot owner and canonical slot permission are present in `regions`,
93    /// - the slot's `slot_vaddr` is consistent with its index,
94    /// - the slot has a live, non-`UNUSED` reference count,
95    /// - the slot has a pending `frame_obligations` entry for this segment,
96    /// - the slot is a data-frame slot with no page-table paths,
97    /// - distinct frames in the segment map to distinct slot indices.
98    ///
99    /// This is an invariant preserved by operations that transform a
100    /// `Segment` together with `MetaRegionOwners`. The segment's own `range`
101    /// is the only identity source; there is no separate segment owner token.
102    pub open spec fn relate_regions(&self, regions: MetaRegionOwners) -> bool {
103        &&& forall|i: int|
104            #![trigger frame_to_index((self.range.start + i * PAGE_SIZE) as usize)]
105            0 <= i < seg_nframes(self.range) ==> {
106                let idx = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
107                // Per-frame linear-drop: the segment holds one (forgotten)
108                // reference per frame, recorded as a `frame_obligations` count.
109                &&& regions.frame_obligations.count(idx) >= 1
110                &&& regions.contains(idx)
111                &&& regions.slot_owners[idx].slot_vaddr == index_to_meta(idx)
112                &&& regions.slot_owners[idx].ref_count()
113                    > 0
114                // Segment frames are shared (never `UNIQUE`).
115                &&& regions.slot_owners[idx].ref_count()
116                    <= crate::mm::frame::meta::REF_COUNT_MAX
117                // A segment holds its frames as a unit; they are not
118                // mapped into any page table, so the slot carries no PTE
119                // paths. Needed to discharge `Frame::drop`'s strengthened
120                // precondition (`ref_count == 1 ==> paths_in_pt empty`)
121                // in the per-frame teardown loop.
122                &&& regions.slot_owners[idx].paths_in_pt.is_empty()
123                &&& regions.slot_owners[idx].usage is Frame
124            }
125        &&& forall|i: int, j: int|
126            #![trigger frame_to_index((self.range.start + i * PAGE_SIZE) as usize),
127                frame_to_index((self.range.start + j * PAGE_SIZE) as usize)]
128            0 <= i < j < seg_nframes(self.range) ==> frame_to_index(
129                (self.range.start + i * PAGE_SIZE) as usize,
130            ) != frame_to_index((self.range.start + j * PAGE_SIZE) as usize)
131    }
132
133    /// Manually instantiates the [`relate_regions`] forall at a specific index.
134    /// Use this to extract per-frame facts without fighting trigger inference.
135    pub proof fn relate_regions_at(&self, regions: MetaRegionOwners, i: int)
136        requires
137            self.relate_regions(regions),
138            0 <= i < seg_nframes(self.range),
139        ensures
140            ({
141                let idx = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
142                &&& regions.frame_obligations.count(idx) >= 1
143                &&& regions.contains(
144                    idx,
145                )
146                // Borrow-protocol transition: `raw_count` is dormant.
147                &&& regions.slot_owners[idx].slot_vaddr == index_to_meta(idx)
148                &&& regions.slot_owners[idx].ref_count() > 0
149                &&& regions.slot_owners[idx].ref_count() <= crate::mm::frame::meta::REF_COUNT_MAX
150                &&& regions.slot_owners[idx].paths_in_pt.is_empty()
151                &&& regions.slot_owners[idx].usage is Frame
152            }),
153    {
154        // Trigger the forall at index `i`.
155        let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
156    }
157
158    /// Manually instantiates the [`relate_regions`] distinctness forall at a
159    /// specific index pair: distinct in-range frames map to distinct slot
160    /// indices. Reusable lever for `from_unused`/`split`/`slice` proofs.
161    pub proof fn relate_regions_distinct(&self, regions: MetaRegionOwners, i: int, j: int)
162        requires
163            self.relate_regions(regions),
164            0 <= i < j < seg_nframes(self.range),
165        ensures
166            frame_to_index((self.range.start + i * PAGE_SIZE) as usize) != frame_to_index(
167                (self.range.start + j * PAGE_SIZE) as usize,
168            ),
169    {
170        // Trigger the distinctness forall at `(i, j)`.
171        let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
172        let _ = frame_to_index((self.range.start + j * PAGE_SIZE) as usize);
173    }
174
175    /// The bundled invariant for [`Segment`] operations that thread the global
176    /// `regions`: the segment's own invariant, the region invariant, and the
177    /// cross-object relation tying this segment's range to `regions`.
178    ///
179    /// Mirrors the `invariants` bundles used throughout the page-table / cursor
180    /// code — it collapses the clauses repeated across `split`, `slice`,
181    /// `into_raw`, `next`, and `drop` into one predicate.
182    pub open spec fn invariants(&self, regions: MetaRegionOwners) -> bool {
183        &&& self.inv()
184        &&& regions.inv()
185        &&& self.relate_regions(regions)
186    }
187
188    /// Whether a [`MemView`] covers the segment through the kernel direct mapping.
189    ///
190    /// This predicate only describes the virtual-to-physical relation and the
191    /// presence of initialized backing frame contents.
192    pub open spec fn kernel_mem_view_covers(&self, view: &MemView) -> bool {
193        &&& self.inv()
194        &&& view.mappings_are_disjoint()
195        &&& forall|vaddr: Vaddr|
196            #![trigger view.addr_transl(vaddr)]
197            paddr_to_vaddr(self.start_paddr()) <= vaddr < paddr_to_vaddr(self.start_paddr())
198                + self.end_paddr() - self.start_paddr() ==> {
199                &&& view.addr_transl(vaddr) is Some
200                &&& view.memory.contains_key((view.addr_transl(vaddr)->0).0)
201                &&& view.memory[(view.addr_transl(vaddr)->0).0].inv()
202                &&& view.memory[(view.addr_transl(vaddr)->0).0].contents[(view.addr_transl(
203                    vaddr,
204                )->0).1 as int] is Init
205            }
206        &&& forall|paddr: Paddr|
207            #![trigger paddr_to_vaddr(paddr)]
208            self.start_paddr() <= paddr < self.end_paddr() ==> {
209                let vaddr = paddr_to_vaddr(paddr);
210                &&& view.addr_transl(vaddr) is Some
211                &&& (view.addr_transl(vaddr)->0).0 <= paddr
212                &&& paddr < (view.addr_transl(vaddr)->0).0 + view.memory[(view.addr_transl(
213                    vaddr,
214                )->0).0].size@
215                &&& (view.addr_transl(vaddr)->0).1 == paddr - (view.addr_transl(vaddr)->0).0
216                &&& view.memory.contains_key((view.addr_transl(vaddr)->0).0)
217                &&& view.memory[(view.addr_transl(vaddr)->0).0].inv()
218                &&& view.memory[(view.addr_transl(vaddr)->0).0].contents[(view.addr_transl(
219                    vaddr,
220                )->0).1 as int] is Init
221            }
222    }
223}
224
225/// Helper spec: the slot index of the j-th frame in a segment whose physical
226/// range starts at `range_start`. Unlike a let-bound ghost closure (which Verus
227/// treats opaquely under SMT), a `spec fn` is auto-unfolded so equalities
228/// between `frame_idx_at(...)` and `frame_to_index(...)` are derivable.
229#[verifier::inline]
230pub open spec fn frame_idx_at(range_start: usize, j: int) -> int {
231    frame_to_index((range_start + j * PAGE_SIZE) as usize)
232}
233
234/// The exact `frame_obligations` effect of recording one forgotten reference
235/// per frame for the first `n` frames of a segment starting at `range_start`:
236///
237/// - every segment frame's count grows by at least one (its recorded
238///   reference), and
239/// - the *frame condition*: every slot that is NOT a segment frame is left
240///   untouched.
241///
242/// The frame condition is the load-bearing part — it pins the *support* of the
243/// change to the segment's slots, so a caller's ledger accounting telescopes
244/// (it can conclude this only touched the segment's slots). Shared by
245/// [`tracked_mint_seg_obligations`] (which establishes it) and
246/// [`Segment::from_unused`] (which advertises it).
247pub open spec fn seg_obligations_minted(
248    pre: MetaRegionOwners,
249    post: MetaRegionOwners,
250    range_start: usize,
251    n: int,
252) -> bool {
253    // Each segment frame gains at least one entry.
254    &&& forall|i: int|
255        #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
256        0 <= i < n ==> post.frame_obligations.count(
257            frame_to_index((range_start + i * PAGE_SIZE) as usize),
258        ) >= pre.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
259            + 1
260        // Frame condition: every slot that is NOT a segment frame is untouched.
261    &&& forall|jdx: int|
262        #![trigger post.frame_obligations.count(jdx)]
263        (forall|i: int|
264            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
265            0 <= i < n ==> jdx != frame_to_index((range_start + i * PAGE_SIZE) as usize))
266            ==> post.frame_obligations.count(jdx) == pre.frame_obligations.count(jdx)
267}
268
269/// Mints one per-frame `frame_obligations` entry for each of the first `n`
270/// frames of a segment starting at `range_start`. Used by
271/// [`Segment::from_unused`] to record the segment's forgotten per-frame
272/// references *after* the construction loop (which is net-zero on the ledger:
273/// each frame's `Frame::from_unused` mint is cancelled by its `ManuallyDrop`).
274///
275/// Per-frame obligations replace the old single range-keyed `obligations`
276/// ledger entry. Because `frame_obligations` is a multiset and minting only
277/// ever increases counts, no distinctness hypothesis on the frame indices is
278/// needed.
279///
280/// EXACT accounting (Tier A): the ensures pin the *support* of the change —
281/// every segment frame's count grows by at least one, and every *other* slot
282/// is untouched. This frame condition is what lets a caller's accounting
283/// telescope (it can conclude this call touched only the segment's slots).
284/// Each mint targets a segment index, so a non-segment slot is simply never a
285/// mint target — hence the frame condition needs no injectivity argument.
286pub proof fn tracked_mint_seg_obligations(
287    tracked regions: &mut MetaRegionOwners,
288    range_start: usize,
289    n: int,
290)
291    requires
292        0 <= n,
293        old(regions).inv(),
294    ensures
295        final(regions).inv(),
296        final(regions).slots == old(regions).slots,
297        final(regions).slot_owners == old(regions).slot_owners,
298        // Counts only grow.
299        forall|idx: int|
300            #![trigger final(regions).frame_obligations.count(idx)]
301            final(regions).frame_obligations.count(idx) >= old(regions).frame_obligations.count(
302                idx,
303            ),
304        // The exact per-frame mint effect (segment frames +≥1, all else fixed).
305        seg_obligations_minted(*old(regions), *final(regions), range_start, n),
306    decreases n,
307{
308    let ghost g0 = *regions;
309    if n > 0 {
310        tracked_mint_seg_obligations(regions, range_start, n - 1);
311        let ghost gmid = *regions;
312        let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
313        let tracked _ = regions.tracked_mint_frame_obligation(idx);
314        // `regions.frame_obligations == gmid.frame_obligations.insert(idx)`,
315        // and the recursion already proved the strengthened ensures for the
316        // first `n-1` frames against `g0`. Bridge each ensures to `n`.
317        // Frame condition: a non-segment slot is untouched by the recursion
318        // (it omits the slot from `[0, n-1)`) and is not the mint target.
319        assert forall|jdx: int|
320            #![trigger regions.frame_obligations.count(jdx)]
321            (forall|i: int|
322                #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
323                0 <= i < n ==> jdx != frame_to_index(
324                    (range_start + i * PAGE_SIZE) as usize,
325                )) implies regions.frame_obligations.count(jdx) == g0.frame_obligations.count(
326            jdx,
327        ) by {};
328        // Each segment frame gained at least one entry.
329        assert forall|i: int|
330            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
331            0 <= i < n implies regions.frame_obligations.count(
332            frame_to_index((range_start + i * PAGE_SIZE) as usize),
333        ) >= g0.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
334            + 1 by {
335            // i < n-1: recursion gives `gmid.count(idx_i) >= g0.count(idx_i)+1`;
336            // the mint only grows counts. i == n-1: `gmid.count(idx) >= g0.count(idx)`
337            // (monotone), and the mint adds exactly one at `idx`.
338        };
339    }
340}
341
342/// Redeems the per-frame `frame_obligations` entry for each of the first `n`
343/// frames of a segment starting at `range_start` — the inverse of
344/// [`tracked_mint_seg_obligations`]. Used by [`Segment::drop`] to drain the
345/// segment's retained per-frame references *before* the per-frame teardown
346/// loop, so that loop sees `count == 0` (as it did before the migration) and
347/// its `from_raw`(+1)/`frame.drop`(-1) pair nets to zero unchanged.
348///
349/// Unlike minting, redeeming requires the frame indices be *distinct*
350/// (redeeming one frame must not drop another's count below 1) and each count
351/// be `>= 1` up front — both supplied by [`Segment::relate_regions`].
352/// Leaves `slots`, `slot_owners`, and the (vestigial) range `obligations`
353/// ledger untouched.
354pub proof fn tracked_redeem_seg_obligations(
355    tracked regions: &mut MetaRegionOwners,
356    range_start: usize,
357    n: int,
358)
359    requires
360        0 <= n,
361        old(regions).inv(),
362        forall|i: int|
363            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
364            0 <= i < n ==> old(regions).frame_obligations.count(
365                frame_to_index((range_start + i * PAGE_SIZE) as usize),
366            ) >= 1,
367        forall|i: int, j: int|
368            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize),
369                frame_to_index((range_start + j * PAGE_SIZE) as usize)]
370            0 <= i < j < n ==> frame_to_index((range_start + i * PAGE_SIZE) as usize)
371                != frame_to_index((range_start + j * PAGE_SIZE) as usize),
372    ensures
373        final(regions).inv(),
374        final(regions).slots == old(regions).slots,
375        final(regions).slot_owners == old(regions).slot_owners,
376    decreases n,
377{
378    if n > 0 {
379        let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
380        let tracked tok = DropObligation::tracked_mint(idx);
381        regions.tracked_redeem_frame_obligation(tok);
382        // Redeeming `idx` (the n-1'th frame) left every earlier frame's count
383        // untouched, since the indices are distinct — so the `>= 1` hypothesis
384        // still holds for `[0, n-1)` and the recursive call's precondition is met.
385        assert forall|i: int|
386            #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
387            0 <= i < n - 1 implies regions.frame_obligations.count(
388            frame_to_index((range_start + i * PAGE_SIZE) as usize),
389        ) >= 1 by {};
390        tracked_redeem_seg_obligations(regions, range_start, n - 1);
391    }
392}
393
394} // verus!