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!