ostd/specs/mm/frame/frame_specs.rs
1use vstd::prelude::*;
2
3use vstd_extra::cast_ptr::*;
4use vstd_extra::drop_tracking::*;
5use vstd_extra::ownership::*;
6
7use crate::mm::frame::meta::{
8 META_SLOT_SIZE, REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED,
9 mapping::{frame_to_meta, meta_to_frame},
10};
11use crate::mm::frame::*;
12use crate::mm::kspace::FRAME_METADATA_RANGE;
13use crate::mm::{Paddr, PagingLevel, Vaddr};
14use crate::specs::arch::*;
15
16use crate::specs::mm::frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners};
17
18use core::marker::PhantomData;
19
20verus! {
21
22// Unbounded so `from_raw` (which lives in an unbounded `impl Frame<M>` block
23// to break the AnyFrameMeta trait-resolution cycle in PT-node on_drop) can
24// reference these helpers via `Self::from_raw_*`.
25impl<'a, M: ?Sized> Frame<M> {
26 // ── from_raw precondition predicates ──
27 /// **Safety**: The frame exists, is addressable, and its slot is alive
28 /// (not torn down: `ref_count != REF_COUNT_UNUSED`). Under the
29 /// borrow-protocol redesign this liveness gate replaces the prior
30 /// `raw_count <= 1` check — a slot that has not been torn down is safe
31 /// to re-materialize as a `Frame` value. (`>= 1` is *not* the right
32 /// gate, since the `UNUSED` sentinel `u64::MAX` also satisfies it; and
33 /// the PT-node ownership model only exposes `!= UNUSED`.)
34 pub open spec fn from_raw_requires_safety(regions: MetaRegionOwners, paddr: Paddr) -> bool {
35 &&& regions.slot_owners.contains_key(frame_to_index(paddr))
36 &&& regions.slot_owners[frame_to_index(paddr)].slot_vaddr == frame_to_meta(paddr)
37 &&& has_safe_slot(paddr)
38 &&& regions.inv()
39 &&& regions.slot_owners[frame_to_index(paddr)].inner_perms.ref_count.value()
40 != REF_COUNT_UNUSED
41 }
42
43 pub open spec fn from_raw_ensures(
44 old_regions: MetaRegionOwners,
45 new_regions: MetaRegionOwners,
46 paddr: Paddr,
47 r: Self,
48 ) -> bool {
49 &&& new_regions.inv()
50 &&& new_regions.slots.contains_key(frame_to_index(paddr))
51 &&& new_regions.slot_owners[frame_to_index(paddr)]
52 =~= old_regions.slot_owners[frame_to_index(paddr)]
53 &&& new_regions.slot_owners[frame_to_index(paddr)].slot_vaddr == r.ptr.addr()
54 &&& forall|i: usize|
55 #![trigger new_regions.slot_owners[i], old_regions.slot_owners[i]]
56 i != frame_to_index(paddr) ==> new_regions.slot_owners[i] == old_regions.slot_owners[i]
57 &&& forall|i: usize|
58 i != frame_to_index(paddr) ==> new_regions.slots.contains_key(i)
59 == old_regions.slots.contains_key(i)
60 &&& r.ptr.addr() == frame_to_meta(paddr)
61 &&& r.paddr() == paddr
62 &&& r.inv()
63 // Borrow-protocol: `from_raw` mints exactly one entry in
64 // `frame_obligations` at the recovered slot's index. The returned
65 // `DropObligation` token is the receipt; the entry will be
66 // consumed by either `ManuallyDrop::new` (FrameRef-style borrow)
67 // or `Frame::drop` (reclaim-and-drop). Segment-level ledger is
68 // untouched.
69 &&& new_regions.frame_obligations =~= old_regions.frame_obligations.insert(
70 frame_to_index(paddr),
71 )
72 }
73
74 // ── into_raw precondition predicates ──
75 /// **Safety Invariant**: The frame's structural invariant must hold.
76 pub open spec fn into_raw_pre_frame_inv(self) -> bool {
77 self.inv()
78 }
79
80 /// **Bookkeeping**: The frame must be in use (not unused).
81 pub open spec fn into_raw_pre_not_unused(self, regions: MetaRegionOwners) -> bool {
82 regions.slot_owners[self.index()].inner_perms.ref_count.value() != REF_COUNT_UNUSED
83 }
84
85 /// **Safety**: Frames other than this one are not affected by the call.
86 pub open spec fn into_raw_post_noninterference(
87 self,
88 old_regions: MetaRegionOwners,
89 new_regions: MetaRegionOwners,
90 ) -> bool {
91 &&& forall|i: usize|
92 #![trigger new_regions.slots[i], old_regions.slots[i]]
93 i != self.index() && old_regions.slots.contains_key(i)
94 ==> new_regions.slots.contains_key(i) && new_regions.slots[i]
95 == old_regions.slots[i]
96 &&& forall|i: usize|
97 #![trigger new_regions.slot_owners[i], old_regions.slot_owners[i]]
98 i != self.index() ==> new_regions.slot_owners[i] == old_regions.slot_owners[i]
99 &&& new_regions.slot_owners.dom() =~= old_regions.slot_owners.dom()
100 }
101}
102
103impl<M: ?Sized> Inv for Frame<M> {
104 open spec fn inv(self) -> bool {
105 &&& self.ptr.addr() % META_SLOT_SIZE == 0
106 &&& FRAME_METADATA_RANGE.start <= self.ptr.addr() < FRAME_METADATA_RANGE.start
107 + MAX_NR_PAGES * META_SLOT_SIZE
108 }
109}
110
111impl<M: ?Sized> Frame<M> {
112 pub open spec fn paddr(self) -> usize {
113 meta_to_frame(self.ptr.addr())
114 }
115
116 pub open spec fn index(self) -> usize {
117 frame_to_index(self.paddr())
118 }
119}
120
121impl<M: ?Sized> Frame<M> {
122 /// Cross-object well-formedness predicate: this `Frame` handle and
123 /// the supplied [`MetaRegionOwners`] state are mutually consistent.
124 /// Packages the static "Frame ⟷ state" conjuncts (slot/pointer
125 /// identity, slot in-use range) so that consumer specs
126 /// ([`drop_requires`], [`clone_requires`]) read uniformly.
127 ///
128 /// **Name**: `wf_with_region` (not just `wf`) to avoid clashing with the
129 /// `OwnerOf::wf(self, Self::Owner)` impl that
130 /// [`PageTableNode<C> = Frame<PageTablePageMeta<C>>`] inherits — the
131 /// two predicates take different argument types and serve different
132 /// purposes (per-handle vs. per-owner well-formedness).
133 ///
134 /// The rc range (`> 0 ∧ ≠ UNUSED ∧ ≠ UNIQUE ∧ ≤ MAX`) captures the
135 /// fact that holding a `Frame<M>` is itself evidence that the slot
136 /// is in the SHARED state — no UNUSED, no UNIQUE (which is reserved
137 /// for [`UniqueFrame`]). Combined with
138 /// [`MetaSlotOwner::inv`]'s SHARED branch (post Item 1), `wf_with_region`
139 /// implies `storage.is_init`, `in_list == 0`, and `vtable_ptr.is_init`
140 /// at the slot, so consumers don't have to repeat those.
141 ///
142 /// **Not preserved by `drop` for `self`**: dropping `self` releases
143 /// the reference; for *other* handles to the same slot, `wf_with_region`
144 /// is preserved by `drop`'s `>1` branch (post rc ∈ [1, MAX-1]) and
145 /// vacuous in the `==1` branch (no other handles to break).
146 pub open spec fn wf_with_region(self, s: MetaRegionOwners) -> bool {
147 let idx = self.index();
148 let slot_own = s.slot_owners[idx];
149 &&& self.inv()
150 &&& s.inv()
151 &&& s.slots.contains_key(idx)
152 &&& s.slots[idx].pptr() == self.ptr
153 &&& s.slot_owners.contains_key(idx)
154 &&& slot_own.inner_perms.ref_count.value() != REF_COUNT_UNUSED
155 &&& slot_own.inner_perms.ref_count.value() != REF_COUNT_UNIQUE
156 &&& slot_own.inner_perms.ref_count.value() > 0
157 &&& slot_own.inner_perms.ref_count.value() <= REF_COUNT_MAX
158 }
159}
160
161/// We need to keep track of when frames are forgotten with `ManuallyDrop`.
162/// We maintain a counter for each frame of how many times it has been forgotten (`raw_count`).
163/// Calling `ManuallyDrop::new` increments the counter. It is technically safe to forget a frame multiple times,
164/// and this will happen with read-only `FrameRef`s. All such references need to be dropped by the time
165/// `from_raw` is called. So, `ManuallyDrop::drop` decrements the counter when the reference is dropped,
166/// and `from_raw` may only be called when the counter is 1.
167impl<M: ?Sized> TrackDrop for Frame<M> {
168 type State = MetaRegionOwners;
169
170 /// Slot index. Lets the obligation token identify *which* slot it
171 /// belongs to — `Drop::drop`'s precondition then refuses a token
172 /// from one slot being used to drop a Frame at another slot.
173 /// (Full per-instance ledger enforcement is a follow-up; for now
174 /// `consume_obligation` is a no-op so the token's identity is
175 /// documentary rather than gated against a multiset.)
176 type Key = usize;
177
178 open spec fn key(self) -> Self::Key {
179 self.index()
180 }
181
182 open spec fn constructor_requires(self, s: Self::State) -> bool {
183 &&& s.slot_owners.contains_key(self.index())
184 &&& s.inv()
185 }
186
187 open spec fn constructor_ensures(
188 self,
189 s0: Self::State,
190 s1: Self::State,
191 obl_key: Self::Key,
192 ) -> bool {
193 let slot_own = s0.slot_owners[self.index()];
194 &&& s1.slot_owners[self.index()] == slot_own
195 &&& forall|i: usize|
196 #![trigger s1.slot_owners[i]]
197 i != self.index() ==> s1.slot_owners[i] == s0.slot_owners[i]
198 &&& s1.slots =~= s0.slots
199 &&& s1.slot_owners.dom()
200 =~= s0.slot_owners.dom()
201 // Linear-drop pilot: minting a `Frame` (bumping `raw_count`) does
202 // not affect the segment obligation ledger.
203 // Frame-side ledger: `constructor_spec` adds one entry at the
204 // slot index via the paired mint axiom (multiset semantics).
205 &&& s1.frame_obligations =~= s0.frame_obligations.insert(obl_key)
206 }
207
208 proof fn constructor_spec(self, tracked s: &mut Self::State) -> (tracked obl: DropObligation<
209 Self::Key,
210 >) {
211 let meta_addr = self.ptr.addr();
212 let index = frame_to_index(meta_to_frame(meta_addr));
213 let tracked mut slot_own = s.slot_owners.tracked_remove(index);
214 s.slot_owners.tracked_insert(index, slot_own);
215 // Paired mint axiom: produces the token AND adds its Loc to
216 // `frame_obligations`. Replaces the prior ledger-less
217 // `DropObligation::tracked_mint(index)`.
218 s.tracked_mint_frame_obligation(index)
219 }
220
221 // It is unsound to drop a `Frame` while raw paddrs to it remain
222 // outstanding (`raw_count > 0`), since those raw paddrs could be revived
223 // via `from_raw` after the slot has been torn down. Hence the drop is
224 // only permitted when `raw_count == 0`.
225 open spec fn drop_requires(self, s: Self::State) -> bool {
226 let idx = frame_to_index(meta_to_frame(self.ptr.addr()));
227 let slot_own = s.slot_owners[idx];
228 // Cross-object validity: this Frame is consistent with `s` and
229 // the slot is in the SHARED rc range. `wf_with_region` carries the
230 // slot identity + pointer agreement + `rc ∈ (0, MAX] ∧ ≠ UNIQUE`
231 // bounds.
232 &&& self.wf_with_region(
233 s,
234 )
235 // Borrow-protocol transition: `raw_count` is dormant. The
236 // "outstanding raw paddrs must be drained before drop" guarantee
237 // is now carried by the `frame_obligations` ledger together with
238 // `from_raw`'s `ref_count >= 1` safety check (a torn-down slot is
239 // `UNUSED` and cannot be `from_raw`'d).
240 // At `ref_count == 1` the teardown branch of `drop_last_in_place`
241 // runs, requiring an empty `paths_in_pt` (the strengthened
242 // `MetaSlotOwner::inv` UNUSED branch demands it post-teardown,
243 // and `drop_last_in_place` doesn't touch paths). Sound: at
244 // `ref_count == 1` the `Frame` being dropped is the sole
245 // reference, so there is no live PTE mapping (a mapping would
246 // be a further reference, forcing `ref_count >= 2`).
247 //
248 // The other `drop_last_in_place_safety_cond` conjuncts
249 // (`storage.is_init`, `in_list == 0`) are subsumed by the
250 // strengthened `MetaSlotOwner::inv` SHARED branch
251 // (`0 < rc <= REF_COUNT_MAX`) — they hold universally for any
252 // in-use slot, not just at `rc == 1`.
253 &&& slot_own.inner_perms.ref_count.value() == 1 ==> {
254 &&& slot_own.paths_in_pt.is_empty()
255 }
256 }
257
258 open spec fn drop_ensures(self, s0: Self::State, s1: Self::State, obl_key: Self::Key) -> bool {
259 let idx = frame_to_index(meta_to_frame(self.ptr.addr()));
260 let so0 = s0.slot_owners[idx];
261 let so1 = s1.slot_owners[idx];
262 &&& s1.inv()
263 &&& forall|i: usize|
264 #![trigger s1.slot_owners[i]]
265 i != idx ==> s1.slot_owners[i] == s0.slot_owners[i]
266 &&& s1.slots =~= s0.slots
267 &&& s1.slot_owners.dom()
268 =~= s0.slot_owners.dom()
269 // The slot's identity / page-table linkage is preserved by a
270 // drop (it only adjusts refcount and, on teardown, storage).
271 &&& so1.slot_vaddr == so0.slot_vaddr
272 &&& so1.usage == so0.usage
273 &&& so1.paths_in_pt
274 == so0.paths_in_pt
275 // Refcount transition. `drop_requires` guarantees the old value
276 // is in `[1, REF_COUNT_MAX]`, so these cases are exhaustive:
277 // - last reference (== 1): the slot is torn down to UNUSED.
278 // - otherwise (> 1): the refcount is decremented by one.
279 &&& so0.inner_perms.ref_count.value() == 1 ==> so1.inner_perms.ref_count.value()
280 == REF_COUNT_UNUSED
281 &&& so0.inner_perms.ref_count.value() > 1 ==> so1.inner_perms.ref_count.value() == (
282 so0.inner_perms.ref_count.value()
283 - 1) as u64
284 // Linear-drop pilot: `Frame::drop` doesn't redeem segment-level
285 // obligations, so the segment ledger is preserved.
286 // Frame-side ledger: routed through `consume_obligation` (called
287 // by Drop::drop's body first), the count at `obl_key` shrinks
288 // by 1.
289 &&& s1.frame_obligations =~= s0.frame_obligations.remove(obl_key)
290 }
291
292 /// `ManuallyDrop::new` / `Drop::drop` require the ledger to contain
293 /// at least one entry at this slot — preventing a forged token
294 /// from being used to "consume" a non-existent obligation.
295 open spec fn consume_requires(self, s: Self::State, obl_key: Self::Key) -> bool {
296 s.frame_obligations.count(obl_key) > 0
297 }
298
299 open spec fn consume_ensures(
300 self,
301 s0: Self::State,
302 s1: Self::State,
303 obl_key: Self::Key,
304 ) -> bool {
305 // Multiset count at the slot shrinks by 1; everything else
306 // (slots, slot_owners, segment ledger) is preserved.
307 &&& s1.frame_obligations =~= s0.frame_obligations.remove(obl_key)
308 &&& s1.slots =~= s0.slots
309 &&& s1.slot_owners =~= s0.slot_owners
310 }
311
312 proof fn consume_obligation(
313 self,
314 tracked s: &mut Self::State,
315 tracked obl: DropObligation<Self::Key>,
316 ) {
317 // Paired redeem axiom: removes one entry at `obl.value()` from
318 // `frame_obligations`. Leaves `slot_owners` (including
319 // `raw_count`) untouched — the deliberate-leak semantic.
320 s.tracked_redeem_frame_obligation(obl);
321 }
322}
323
324} // verus!