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 REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED, get_slot_spec,
9 mapping::{META_SLOT_SIZE, frame_to_index, meta_to_frame},
10};
11use crate::mm::frame::*;
12use crate::mm::{Paddr, PagingLevel, Vaddr};
13use crate::specs::arch::kspace::FRAME_METADATA_RANGE;
14use crate::specs::arch::mm::{MAX_NR_PAGES, MAX_PADDR, PAGE_SIZE};
15use crate::specs::mm::frame::meta_owners::{MetaPerm, MetaSlotStorage};
16use crate::specs::mm::frame::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)].self_addr == 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)].self_addr == 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
111// Unbounded so the PT-node `on_drop` body can use `Frame::<Self>::from_raw` /
112// `Drop for Frame<Self>` without forcing trait resolution back through the
113// in-flight `AnyFrameMeta for PageTablePageMeta<C>` impl. Body is pure
114// pointer arithmetic — no M-specific machinery.
115impl<M: ?Sized> Frame<M> {
116 pub open spec fn paddr(self) -> usize {
117 meta_to_frame(self.ptr.addr())
118 }
119
120 pub open spec fn index(self) -> usize {
121 frame_to_index(self.paddr())
122 }
123}
124
125impl<M: ?Sized> Frame<M> {
126 /// Cross-object well-formedness predicate: this `Frame` handle and
127 /// the supplied [`MetaRegionOwners`] state are mutually consistent.
128 /// Packages the static "Frame ⟷ state" conjuncts (slot/pointer
129 /// identity, slot in-use range) so that consumer specs
130 /// ([`drop_requires`], [`clone_requires`]) read uniformly.
131 ///
132 /// **Name**: `inv_with_regions` (not just `wf`) to avoid clashing with the
133 /// `OwnerOf::wf(self, Self::Owner)` impl that
134 /// [`PageTableNode<C> = Frame<PageTablePageMeta<C>>`] inherits — the
135 /// two predicates take different argument types and serve different
136 /// purposes (per-handle vs. per-owner well-formedness).
137 ///
138 /// The rc range (`> 0 ∧ ≠ UNUSED ∧ ≠ UNIQUE ∧ ≤ MAX`) captures the
139 /// fact that holding a `Frame<M>` is itself evidence that the slot
140 /// is in the SHARED state — no UNUSED, no UNIQUE (which is reserved
141 /// for [`UniqueFrame`]). Combined with
142 /// [`MetaSlotOwner::inv`]'s SHARED branch (post Item 1), `inv_with_regions`
143 /// implies `storage.is_init`, `in_list == 0`, and `vtable_ptr.is_init`
144 /// at the slot, so consumers don't have to repeat those.
145 ///
146 /// **Not preserved by `drop` for `self`**: dropping `self` releases
147 /// the reference; for *other* handles to the same slot, `inv_with_regions`
148 /// is preserved by `drop`'s `>1` branch (post rc ∈ [1, MAX-1]) and
149 /// vacuous in the `==1` branch (no other handles to break).
150 pub open spec fn inv_with_regions(self, s: MetaRegionOwners) -> bool {
151 let idx = self.index();
152 let slot_own = s.slot_owners[idx];
153 &&& self.inv()
154 &&& s.inv()
155 &&& s.slots.contains_key(idx)
156 &&& s.slots[idx].pptr() == self.ptr
157 &&& s.slot_owners.contains_key(idx)
158 &&& slot_own.inner_perms.ref_count.value() != REF_COUNT_UNUSED
159 &&& slot_own.inner_perms.ref_count.value() != REF_COUNT_UNIQUE
160 &&& slot_own.inner_perms.ref_count.value() > 0
161 &&& slot_own.inner_perms.ref_count.value() <= REF_COUNT_MAX
162 }
163}
164
165/// We need to keep track of when frames are forgotten with `ManuallyDrop`.
166/// We maintain a counter for each frame of how many times it has been forgotten (`raw_count`).
167/// Calling `ManuallyDrop::new` increments the counter. It is technically safe to forget a frame multiple times,
168/// and this will happen with read-only `FrameRef`s. All such references need to be dropped by the time
169/// `from_raw` is called. So, `ManuallyDrop::drop` decrements the counter when the reference is dropped,
170/// and `from_raw` may only be called when the counter is 1.
171impl<M: ?Sized> TrackDrop for Frame<M> {
172 type State = MetaRegionOwners;
173
174 /// Slot index. Lets the obligation token identify *which* slot it
175 /// belongs to — `Drop::drop`'s precondition then refuses a token
176 /// from one slot being used to drop a Frame at another slot.
177 /// (Full per-instance ledger enforcement is a follow-up; for now
178 /// `consume_obligation` is a no-op so the token's identity is
179 /// documentary rather than gated against a multiset.)
180 type Key = usize;
181
182 open spec fn key(self) -> Self::Key {
183 frame_to_index(meta_to_frame(self.ptr.addr()))
184 }
185
186 open spec fn constructor_requires(self, s: Self::State) -> bool {
187 &&& s.slot_owners.contains_key(frame_to_index(meta_to_frame(self.ptr.addr())))
188 &&& s.inv()
189 }
190
191 open spec fn constructor_ensures(
192 self,
193 s0: Self::State,
194 s1: Self::State,
195 obl_key: Self::Key,
196 ) -> bool {
197 let slot_own = s0.slot_owners[frame_to_index(meta_to_frame(self.ptr.addr()))];
198 &&& s1.slot_owners[frame_to_index(meta_to_frame(self.ptr.addr()))] == slot_own
199 &&& forall|i: usize|
200 #![trigger s1.slot_owners[i]]
201 i != frame_to_index(meta_to_frame(self.ptr.addr())) ==> s1.slot_owners[i]
202 == s0.slot_owners[i]
203 &&& s1.slots =~= s0.slots
204 &&& s1.slot_owners.dom()
205 =~= s0.slot_owners.dom()
206 // Linear-drop pilot: minting a `Frame` (bumping `raw_count`) does
207 // not affect the segment obligation ledger.
208 // Frame-side ledger: `constructor_spec` adds one entry at the
209 // slot index via the paired mint axiom (multiset semantics).
210 &&& s1.frame_obligations =~= s0.frame_obligations.insert(obl_key)
211 }
212
213 proof fn constructor_spec(self, tracked s: &mut Self::State) -> (tracked obl: DropObligation<
214 Self::Key,
215 >) {
216 let meta_addr = self.ptr.addr();
217 let index = frame_to_index(meta_to_frame(meta_addr));
218 let tracked mut slot_own = s.slot_owners.tracked_remove(index);
219 s.slot_owners.tracked_insert(index, slot_own);
220 // Paired mint axiom: produces the token AND adds its Loc to
221 // `frame_obligations`. Replaces the prior ledger-less
222 // `DropObligation::tracked_mint(index)`.
223 s.tracked_mint_frame_obligation(index)
224 }
225
226 // It is unsound to drop a `Frame` while raw paddrs to it remain
227 // outstanding (`raw_count > 0`), since those raw paddrs could be revived
228 // via `from_raw` after the slot has been torn down. Hence the drop is
229 // only permitted when `raw_count == 0`.
230 open spec fn drop_requires(self, s: Self::State) -> bool {
231 let idx = frame_to_index(meta_to_frame(self.ptr.addr()));
232 let slot_own = s.slot_owners[idx];
233 // Cross-object validity: this Frame is consistent with `s` and
234 // the slot is in the SHARED rc range. `inv_with_regions` carries the
235 // slot identity + pointer agreement + `rc ∈ (0, MAX] ∧ ≠ UNIQUE`
236 // bounds.
237 &&& self.inv_with_regions(
238 s,
239 )
240 // Borrow-protocol transition: `raw_count` is dormant. The
241 // "outstanding raw paddrs must be drained before drop" guarantee
242 // is now carried by the `frame_obligations` ledger together with
243 // `from_raw`'s `ref_count >= 1` safety check (a torn-down slot is
244 // `UNUSED` and cannot be `from_raw`'d).
245 // At `ref_count == 1` the teardown branch of `drop_last_in_place`
246 // runs, requiring an empty `paths_in_pt` (the strengthened
247 // `MetaSlotOwner::inv` UNUSED branch demands it post-teardown,
248 // and `drop_last_in_place` doesn't touch paths). Sound: at
249 // `ref_count == 1` the `Frame` being dropped is the sole
250 // reference, so there is no live PTE mapping (a mapping would
251 // be a further reference, forcing `ref_count >= 2`).
252 //
253 // The other `drop_last_in_place_safety_cond` conjuncts
254 // (`storage.is_init`, `in_list == 0`) are subsumed by the
255 // strengthened `MetaSlotOwner::inv` SHARED branch
256 // (`0 < rc <= REF_COUNT_MAX`) — they hold universally for any
257 // in-use slot, not just at `rc == 1`.
258 &&& slot_own.inner_perms.ref_count.value() == 1 ==> {
259 &&& slot_own.paths_in_pt.is_empty()
260 }
261 }
262
263 open spec fn drop_ensures(self, s0: Self::State, s1: Self::State, obl_key: Self::Key) -> bool {
264 let idx = frame_to_index(meta_to_frame(self.ptr.addr()));
265 let so0 = s0.slot_owners[idx];
266 let so1 = s1.slot_owners[idx];
267 &&& s1.inv()
268 &&& forall|i: usize|
269 #![trigger s1.slot_owners[i]]
270 i != idx ==> s1.slot_owners[i] == s0.slot_owners[i]
271 &&& s1.slots =~= s0.slots
272 &&& s1.slot_owners.dom()
273 =~= s0.slot_owners.dom()
274 // The slot's identity / page-table linkage is preserved by a
275 // drop (it only adjusts refcount and, on teardown, storage).
276 &&& so1.self_addr == so0.self_addr
277 &&& so1.usage == so0.usage
278 &&& so1.paths_in_pt
279 == so0.paths_in_pt
280 // Refcount transition. `drop_requires` guarantees the old value
281 // is in `[1, REF_COUNT_MAX]`, so these cases are exhaustive:
282 // - last reference (== 1): the slot is torn down to UNUSED.
283 // - otherwise (> 1): the refcount is decremented by one.
284 &&& so0.inner_perms.ref_count.value() == 1 ==> so1.inner_perms.ref_count.value()
285 == REF_COUNT_UNUSED
286 &&& so0.inner_perms.ref_count.value() > 1 ==> so1.inner_perms.ref_count.value() == (
287 so0.inner_perms.ref_count.value()
288 - 1) as u64
289 // Linear-drop pilot: `Frame::drop` doesn't redeem segment-level
290 // obligations, so the segment ledger is preserved.
291 // Frame-side ledger: routed through `consume_obligation` (called
292 // by Drop::drop's body first), the count at `obl_key` shrinks
293 // by 1.
294 &&& s1.frame_obligations =~= s0.frame_obligations.remove(obl_key)
295 }
296
297 /// `ManuallyDrop::new` / `Drop::drop` require the ledger to contain
298 /// at least one entry at this slot — preventing a forged token
299 /// from being used to "consume" a non-existent obligation.
300 open spec fn consume_requires(self, s: Self::State, obl_key: Self::Key) -> bool {
301 s.frame_obligations.count(obl_key) > 0
302 }
303
304 open spec fn consume_ensures(
305 self,
306 s0: Self::State,
307 s1: Self::State,
308 obl_key: Self::Key,
309 ) -> bool {
310 // Multiset count at the slot shrinks by 1; everything else
311 // (slots, slot_owners, segment ledger) is preserved.
312 &&& s1.frame_obligations =~= s0.frame_obligations.remove(obl_key)
313 &&& s1.slots =~= s0.slots
314 &&& s1.slot_owners =~= s0.slot_owners
315 }
316
317 proof fn consume_obligation(
318 self,
319 tracked s: &mut Self::State,
320 tracked obl: DropObligation<Self::Key>,
321 ) {
322 // Paired redeem axiom: removes one entry at `obl.value()` from
323 // `frame_obligations`. Leaves `slot_owners` (including
324 // `raw_count`) untouched — the deliberate-leak semantic.
325 s.tracked_redeem_frame_obligation(obl);
326 }
327}
328
329} // verus!