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!