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.slot_owners.contains_key(idx)
111 &&& regions.slots.contains_key(idx)
112 &&& regions.slot_owners[idx].slot_vaddr == index_to_meta(idx)
113 &&& regions.slot_owners[idx].inner_perms.ref_count.value()
114 > 0
115 // Segment frames are shared (never `UNIQUE`).
116 &&& regions.slot_owners[idx].inner_perms.ref_count.value()
117 <= crate::mm::frame::meta::REF_COUNT_MAX
118 // A segment holds its frames as a unit; they are not
119 // mapped into any page table, so the slot carries no PTE
120 // paths. Needed to discharge `Frame::drop`'s strengthened
121 // precondition (`ref_count == 1 ==> paths_in_pt empty`)
122 // in the per-frame teardown loop.
123 &&& regions.slot_owners[idx].paths_in_pt.is_empty()
124 &&& regions.slot_owners[idx].usage is Frame
125 }
126 &&& forall|i: int, j: int|
127 #![trigger frame_to_index((self.range.start + i * PAGE_SIZE) as usize),
128 frame_to_index((self.range.start + j * PAGE_SIZE) as usize)]
129 0 <= i < j < seg_nframes(self.range) ==> frame_to_index(
130 (self.range.start + i * PAGE_SIZE) as usize,
131 ) != frame_to_index((self.range.start + j * PAGE_SIZE) as usize)
132 }
133
134 /// Manually instantiates the [`relate_regions`] forall at a specific index.
135 /// Use this to extract per-frame facts without fighting trigger inference.
136 pub proof fn relate_regions_at(&self, regions: MetaRegionOwners, i: int)
137 requires
138 self.relate_regions(regions),
139 0 <= i < seg_nframes(self.range),
140 ensures
141 ({
142 let idx = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
143 &&& regions.frame_obligations.count(idx) >= 1
144 &&& regions.slot_owners.contains_key(idx)
145 &&& regions.slots.contains_key(
146 idx,
147 )
148 // Borrow-protocol transition: `raw_count` is dormant.
149 &&& regions.slot_owners[idx].slot_vaddr == index_to_meta(idx)
150 &&& regions.slot_owners[idx].inner_perms.ref_count.value() > 0
151 &&& regions.slot_owners[idx].inner_perms.ref_count.value()
152 <= crate::mm::frame::meta::REF_COUNT_MAX
153 &&& regions.slot_owners[idx].paths_in_pt.is_empty()
154 &&& regions.slot_owners[idx].usage is Frame
155 }),
156 {
157 // Trigger the forall at index `i`.
158 let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
159 }
160
161 /// Manually instantiates the [`relate_regions`] distinctness forall at a
162 /// specific index pair: distinct in-range frames map to distinct slot
163 /// indices. Reusable lever for `from_unused`/`split`/`slice` proofs.
164 pub proof fn relate_regions_distinct(&self, regions: MetaRegionOwners, i: int, j: int)
165 requires
166 self.relate_regions(regions),
167 0 <= i < j < seg_nframes(self.range),
168 ensures
169 frame_to_index((self.range.start + i * PAGE_SIZE) as usize) != frame_to_index(
170 (self.range.start + j * PAGE_SIZE) as usize,
171 ),
172 {
173 // Trigger the distinctness forall at `(i, j)`.
174 let _ = frame_to_index((self.range.start + i * PAGE_SIZE) as usize);
175 let _ = frame_to_index((self.range.start + j * PAGE_SIZE) as usize);
176 }
177
178 /// The bundled invariant for [`Segment`] operations that thread the global
179 /// `regions`: the segment's own invariant, the region invariant, and the
180 /// cross-object relation tying this segment's range to `regions`.
181 ///
182 /// Mirrors the `invariants` bundles used throughout the page-table / cursor
183 /// code — it collapses the clauses repeated across `split`, `slice`,
184 /// `into_raw`, `next`, and `drop` into one predicate.
185 pub open spec fn invariants(&self, regions: MetaRegionOwners) -> bool {
186 &&& self.inv()
187 &&& regions.inv()
188 &&& self.relate_regions(regions)
189 }
190
191 /// Whether a [`MemView`] covers the segment through the kernel direct mapping.
192 ///
193 /// This predicate only describes the virtual-to-physical relation and the
194 /// presence of initialized backing frame contents.
195 pub open spec fn kernel_mem_view_covers(&self, view: &MemView) -> bool {
196 &&& self.inv()
197 &&& view.mappings_are_disjoint()
198 &&& forall|vaddr: Vaddr|
199 #![trigger view.addr_transl(vaddr)]
200 paddr_to_vaddr(self.start_paddr()) <= vaddr < paddr_to_vaddr(self.start_paddr())
201 + self.end_paddr() - self.start_paddr() ==> {
202 &&& view.addr_transl(vaddr) is Some
203 &&& view.memory.contains_key((view.addr_transl(vaddr)->0).0)
204 &&& view.memory[(view.addr_transl(vaddr)->0).0].inv()
205 &&& view.memory[(view.addr_transl(vaddr)->0).0].contents[(view.addr_transl(
206 vaddr,
207 )->0).1 as int] is Init
208 }
209 &&& forall|paddr: Paddr|
210 #![trigger paddr_to_vaddr(paddr)]
211 self.start_paddr() <= paddr < self.end_paddr() ==> {
212 let vaddr = paddr_to_vaddr(paddr);
213 &&& view.addr_transl(vaddr) is Some
214 &&& (view.addr_transl(vaddr)->0).0 <= paddr
215 &&& paddr < (view.addr_transl(vaddr)->0).0 + view.memory[(view.addr_transl(
216 vaddr,
217 )->0).0].size@
218 &&& (view.addr_transl(vaddr)->0).1 == paddr - (view.addr_transl(vaddr)->0).0
219 &&& view.memory.contains_key((view.addr_transl(vaddr)->0).0)
220 &&& view.memory[(view.addr_transl(vaddr)->0).0].inv()
221 &&& view.memory[(view.addr_transl(vaddr)->0).0].contents[(view.addr_transl(
222 vaddr,
223 )->0).1 as int] is Init
224 }
225 }
226}
227
228/// Helper spec: the slot index of the j-th frame in a segment whose physical
229/// range starts at `range_start`. Unlike a let-bound ghost closure (which Verus
230/// treats opaquely under SMT), a `spec fn` is auto-unfolded so equalities
231/// between `frame_idx_at(...)` and `frame_to_index(...)` are derivable.
232#[verifier::inline]
233pub open spec fn frame_idx_at(range_start: usize, j: int) -> int {
234 frame_to_index((range_start + j * PAGE_SIZE) as usize)
235}
236
237/// The exact `frame_obligations` effect of recording one forgotten reference
238/// per frame for the first `n` frames of a segment starting at `range_start`:
239///
240/// - every segment frame's count grows by at least one (its recorded
241/// reference), and
242/// - the *frame condition*: every slot that is NOT a segment frame is left
243/// untouched.
244///
245/// The frame condition is the load-bearing part — it pins the *support* of the
246/// change to the segment's slots, so a caller's ledger accounting telescopes
247/// (it can conclude this only touched the segment's slots). Shared by
248/// [`tracked_mint_seg_obligations`] (which establishes it) and
249/// [`Segment::from_unused`] (which advertises it).
250pub open spec fn seg_obligations_minted(
251 pre: MetaRegionOwners,
252 post: MetaRegionOwners,
253 range_start: usize,
254 n: int,
255) -> bool {
256 // Each segment frame gains at least one entry.
257 &&& forall|i: int|
258 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
259 0 <= i < n ==> post.frame_obligations.count(
260 frame_to_index((range_start + i * PAGE_SIZE) as usize),
261 ) >= pre.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
262 + 1
263 // Frame condition: every slot that is NOT a segment frame is untouched.
264 &&& forall|jdx: int|
265 #![trigger post.frame_obligations.count(jdx)]
266 (forall|i: int|
267 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
268 0 <= i < n ==> jdx != frame_to_index((range_start + i * PAGE_SIZE) as usize))
269 ==> post.frame_obligations.count(jdx) == pre.frame_obligations.count(jdx)
270}
271
272/// Mints one per-frame `frame_obligations` entry for each of the first `n`
273/// frames of a segment starting at `range_start`. Used by
274/// [`Segment::from_unused`] to record the segment's forgotten per-frame
275/// references *after* the construction loop (which is net-zero on the ledger:
276/// each frame's `Frame::from_unused` mint is cancelled by its `ManuallyDrop`).
277///
278/// Per-frame obligations replace the old single range-keyed `obligations`
279/// ledger entry. Because `frame_obligations` is a multiset and minting only
280/// ever increases counts, no distinctness hypothesis on the frame indices is
281/// needed.
282///
283/// EXACT accounting (Tier A): the ensures pin the *support* of the change —
284/// every segment frame's count grows by at least one, and every *other* slot
285/// is untouched. This frame condition is what lets a caller's accounting
286/// telescope (it can conclude this call touched only the segment's slots).
287/// Each mint targets a segment index, so a non-segment slot is simply never a
288/// mint target — hence the frame condition needs no injectivity argument.
289pub proof fn tracked_mint_seg_obligations(
290 tracked regions: &mut MetaRegionOwners,
291 range_start: usize,
292 n: int,
293)
294 requires
295 0 <= n,
296 old(regions).inv(),
297 ensures
298 final(regions).inv(),
299 final(regions).slots == old(regions).slots,
300 final(regions).slot_owners == old(regions).slot_owners,
301 // Counts only grow.
302 forall|idx: int|
303 #![trigger final(regions).frame_obligations.count(idx)]
304 final(regions).frame_obligations.count(idx) >= old(regions).frame_obligations.count(
305 idx,
306 ),
307 // The exact per-frame mint effect (segment frames +≥1, all else fixed).
308 seg_obligations_minted(*old(regions), *final(regions), range_start, n),
309 decreases n,
310{
311 let ghost g0 = *regions;
312 if n > 0 {
313 tracked_mint_seg_obligations(regions, range_start, n - 1);
314 let ghost gmid = *regions;
315 let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
316 let tracked _ = regions.tracked_mint_frame_obligation(idx);
317 // `regions.frame_obligations == gmid.frame_obligations.insert(idx)`,
318 // and the recursion already proved the strengthened ensures for the
319 // first `n-1` frames against `g0`. Bridge each ensures to `n`.
320 // Frame condition: a non-segment slot is untouched by the recursion
321 // (it omits the slot from `[0, n-1)`) and is not the mint target.
322 assert forall|jdx: int|
323 #![trigger regions.frame_obligations.count(jdx)]
324 (forall|i: int|
325 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
326 0 <= i < n ==> jdx != frame_to_index(
327 (range_start + i * PAGE_SIZE) as usize,
328 )) implies regions.frame_obligations.count(jdx) == g0.frame_obligations.count(
329 jdx,
330 ) by {};
331 // Each segment frame gained at least one entry.
332 assert forall|i: int|
333 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
334 0 <= i < n implies regions.frame_obligations.count(
335 frame_to_index((range_start + i * PAGE_SIZE) as usize),
336 ) >= g0.frame_obligations.count(frame_to_index((range_start + i * PAGE_SIZE) as usize))
337 + 1 by {
338 // i < n-1: recursion gives `gmid.count(idx_i) >= g0.count(idx_i)+1`;
339 // the mint only grows counts. i == n-1: `gmid.count(idx) >= g0.count(idx)`
340 // (monotone), and the mint adds exactly one at `idx`.
341 };
342 }
343}
344
345/// Redeems the per-frame `frame_obligations` entry for each of the first `n`
346/// frames of a segment starting at `range_start` — the inverse of
347/// [`tracked_mint_seg_obligations`]. Used by [`Segment::drop`] to drain the
348/// segment's retained per-frame references *before* the per-frame teardown
349/// loop, so that loop sees `count == 0` (as it did before the migration) and
350/// its `from_raw`(+1)/`frame.drop`(-1) pair nets to zero unchanged.
351///
352/// Unlike minting, redeeming requires the frame indices be *distinct*
353/// (redeeming one frame must not drop another's count below 1) and each count
354/// be `>= 1` up front — both supplied by [`Segment::relate_regions`].
355/// Leaves `slots`, `slot_owners`, and the (vestigial) range `obligations`
356/// ledger untouched.
357pub proof fn tracked_redeem_seg_obligations(
358 tracked regions: &mut MetaRegionOwners,
359 range_start: usize,
360 n: int,
361)
362 requires
363 0 <= n,
364 old(regions).inv(),
365 forall|i: int|
366 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
367 0 <= i < n ==> old(regions).frame_obligations.count(
368 frame_to_index((range_start + i * PAGE_SIZE) as usize),
369 ) >= 1,
370 forall|i: int, j: int|
371 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize),
372 frame_to_index((range_start + j * PAGE_SIZE) as usize)]
373 0 <= i < j < n ==> frame_to_index((range_start + i * PAGE_SIZE) as usize)
374 != frame_to_index((range_start + j * PAGE_SIZE) as usize),
375 ensures
376 final(regions).inv(),
377 final(regions).slots == old(regions).slots,
378 final(regions).slot_owners == old(regions).slot_owners,
379 decreases n,
380{
381 if n > 0 {
382 let idx = frame_to_index((range_start + (n - 1) * PAGE_SIZE) as usize);
383 let tracked tok = DropObligation::tracked_mint(idx);
384 regions.tracked_redeem_frame_obligation(tok);
385 // Redeeming `idx` (the n-1'th frame) left every earlier frame's count
386 // untouched, since the indices are distinct — so the `>= 1` hypothesis
387 // still holds for `[0, n-1)` and the recursive call's precondition is met.
388 assert forall|i: int|
389 #![trigger frame_to_index((range_start + i * PAGE_SIZE) as usize)]
390 0 <= i < n - 1 implies regions.frame_obligations.count(
391 frame_to_index((range_start + i * PAGE_SIZE) as usize),
392 ) >= 1 by {};
393 tracked_redeem_seg_obligations(regions, range_start, n - 1);
394 }
395}
396
397} // verus!