Skip to main content

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!