Skip to main content

ostd/specs/mm/embedding/
mod.rs

1//! Deep embedding of the `VmSpace` and `VmReader`/`VmWriter` API.
2//!
3//! `VmStore` is the abstract state of a caller of these APIs: it holds
4//! the [`MetaRegionOwners`] plus a registry of every owner object the
5//! caller currently has access to.
6//!
7//! [`Op`] is an ADT enumerating the public exec API. [`step`] is the
8//! single proof-mode dispatcher; it requires `s.inv()` *and* the
9//! per-op precondition [`op_pre`] (which says the ids referenced in
10//! `op` resolve to existing entries with the right cross-store
11//! relationships). `op_pre` contains all preconditions necessary
12//! to dispatch each operation, which makes it the cornerstone of soundness.
13//! See its documentation for analysis.
14//!
15//! # Module layout
16//!
17//! - [`vm_space`]: ops on the [`crate::mm::vm_space::VmSpace`] type
18//!   (`new`, drop).
19//! - [`cursor`]: ops on `Cursor` / `CursorMut` (open, drop, `query`,
20//!   `find_next`, `jump`, `map`, `unmap`, `protect_next`).
21//! - [`io`]: ops on `VmReader` / `VmWriter` (creation, drop, the
22//!   user-space and kernel-space IO methods).
23//! - [`trace`]: explicit-induction theorems over `Seq<Op>`.
24//!
25//! # Soundness boundary: `_embedded` axioms
26//!
27//! Each axiom named `<exec_function_path>_embedded` mirrors the
28//! `ensures` clause of one public exec function. Naming is the only
29//! mechanism keeping the axiom in sync with its exec counterpart;
30//! reviewers touching either side should grep for the partner.
31//!
32//! # Roadmap — DONE / open work
33//!
34//! All five originally-deferred items have landed. Shape B for
35//! segments is fully active: `Op::SegmentFromUnused` /
36//! `Op::SegmentDrop` are in the dispatch, [`accounting_inv`] has the
37//! generalised `rc == H + P + cover_count` equation, and
38//! [`structural_inv`] carries `raw_count == segment_cover_count` +
39//! segment-covered ⟹ Frame-usage + segment range well-formedness.
40//!
41//! 1. **Strengthen [`crate::specs::mm::frame::meta_owners::MetaSlotOwner::inv`]'s
42//!    SHARED branch** — DONE. The branch (`0 < rc <= REF_COUNT_MAX`)
43//!    now carries `inner_perms.storage.is_init()` and
44//!    `inner_perms.in_list.value() == 0`. The `rc == 1 ⟹ ...` guards
45//!    on `storage`/`in_list` in
46//!    [`crate::mm::frame::Frame::drop_requires`] were dropped.
47//!
48//! 2. **`Frame::wf(state)`** — DONE at both layers.
49//!    - **Embedding layer**: [`lemma_frame_drop_pre_derivable`]
50//!      derives all of [`frame::drop_pre`]'s residuals (rc not in
51//!      sentinels, `rc <= REF_COUNT_MAX`, `storage.is_init`,
52//!      `in_list == 0`, `rc == 1 ⟹ paths empty`) plus the
53//!      `rc == 1 ⟹ handle_count == 1` clause from `s.inv()` + the
54//!      `FrameEntry`'s registration + the segment-cover hypothesis.
55//!      `op_pre[FrameDrop]` and `step_frame_drop` shrink to
56//!      id-existence + segment-cover only.
57//!    - **Exec layer**: [`crate::mm::frame::Frame::inv_with_regions`]
58//!      packages the per-handle cross-object validity (slot/pointer
59//!      identity + SHARED rc bounds — `> 0 ∧ ≠ UNUSED ∧ ≠ UNIQUE
60//!      ∧ ≤ MAX`). `Frame::drop_requires` is refactored to read
61//!      `self.inv_with_regions(s) ∧ raw_count == 0 ∧ rc == 1 ⟹ paths empty`,
62//!      which keeps the drop-specific bits explicit while
63//!      consolidating the static "this Frame is valid against
64//!      this state" conjuncts.
65//!    - `clone_requires` not refactored: would cascade into
66//!      `PageTableConfig::clone_requires_concrete` (a trait method
67//!      with multiple implementors); left explicit to keep the
68//!      change local.
69//!    - **Preservation of `inv_with_regions` (FUTURE).** `Frame::inv_with_regions`'s
70//!      preservation across drops of *other* handles at the same slot
71//!      is currently informal (claimed in the docstring; no
72//!      machine-checked proof). To prove it, every `Frame<M>` needs a
73//!      tracked ghost "reference-count share" certificate that proves
74//!      "I contribute 1 to my slot's `rc`," combined with an aggregate
75//!      invariant on `MetaSlotOwner` saying `held_shares == rc.value()`.
76//!      Recommended primitive:
77//!      [`vstd_extra::resource::ghost_resource::tokens::Token`]
78//!      (alias for `CountGhost<(), TOTAL>`) with
79//!      `TOTAL = REF_COUNT_MAX`. The resource framework provides
80//!      `split` / `combine` / `agree` / `bounded` pre-proven; the
81//!      Frame side adds a `Tracked<Token<MAX>>` field and the
82//!      `MetaSlotOwner` side adds a
83//!      `CountGhostResource<(), MAX>` aggregate of remaining shares
84//!      with the linking invariant `rc.value() + remaining == MAX`.
85//!      Cursor map / unmap axioms gain share-juggling clauses
86//!      (`paths_in_pt += 1` ↔ split off 1 share). The math is proven
87//!      by vstd_extra; the integration is a multi-day refactor with
88//!      cascading effects on every `MetaRegionOwners` consumer.
89//!      The embedding's `handle_count` already provides the equivalent
90//!      property at the abstract level, so this is only needed if
91//!      downstream code outside the embedding's tracking needs
92//!      `Frame::inv_with_regions` preservation proofs.
93//!
94//! 3a. **Op::Map consumes a `FrameId`** — DONE. `Op::Map { c, fid,
95//!     prop }` extracts the matching `FrameEntry` (so `H` at the
96//!     mapped slot decrements by 1, paired with the cursor axiom's
97//!     `paths_in_pt += 1` at the same slot). Combined with the
98//!     `cursor_mut_map_embedded` axiom's per-slot ensures (rc / usage
99//!     / storage preserved at target, changed-slots ⟹ PT-node ⟹
100//!     `usage != Frame`), [`accounting_inv`]'s Frame-scoped equation
101//!     `rc == H + P` chains.
102//!
103//! 3b. **Op::Query clone modeling** — DONE. The `cursor_query_embedded`
104//!     axiom now returns `Option<Paddr>`: `Some(paddr)` when query
105//!     resolved a tracked leaf and `clone_item` bumped `rc` at that
106//!     slot; `None` otherwise (out-of-range / no leaf / MMIO leaf).
107//!     [`step_query`] consumes the paddr to register a fresh
108//!     `FrameEntry` so `H` at the cloned slot grows in lockstep with
109//!     `rc`, keeping `accounting_inv`'s `rc == H + P` chained.
110//!
111//! 4. **Strengthen `cursor_mut_unmap_embedded`** — DONE. The axiom
112//!    now mirrors exec: universal preservation of
113//!    `raw_count`/`in_list`/`usage`/`self_addr`/`vtable_ptr`;
114//!    storage preserved at slots ending non-UNUSED; rc doesn't bump
115//!    to UNIQUE; at Frame slots the "non-mapping count"
116//!    `rc - paths.len` is invariant with both monotonically non-
117//!    increasing, and post `rc != 0` (Frame teardown collapses
118//!    `rc==0` to `REF_COUNT_UNUSED` atomically); MMIO slots are
119//!    untouched (preserving the `MetaSlotOwner::inv` MMIO exception
120//!    that allows non-empty `paths_in_pt` at UNUSED).
121//!    [`step_unmap`] discharges accounting via case-splits on
122//!    Frame / non-Frame / MMIO.
123//!
124//! 5. **Shape-B segments** — base + split landed; the rest is
125//!    documented with status per op.
126//!    - **`from_unused` / `drop`** — DONE. Allocate a segment over a
127//!      contiguous range of UNUSED frames, and release the segment's
128//!      forgotten references with per-frame teardown.
129//!    - **`split`** — DONE. Partition the segment at a page-aligned
130//!      offset; `regions` is unchanged because per-paddr
131//!      `cover_count` is invariant under the partition.
132//!      [`lemma_segment_cover_split`] proves the per-paddr
133//!      invariance.
134//!    - **`clone`** — NOT modeled, pending exec fix. Today exec
135//!      `Segment::clone` bumps `rc` but not `raw_count` and doesn't
136//!      produce a new `SegmentOwner`, so the clone is un-droppable
137//!      from verified code. Planned fix (separate session):
138//!      generalise `Frame::from_raw` to `raw_count >= 1` + decrement;
139//!      weaken `SegmentOwner::relate_regions` to `raw_count >= 1`;
140//!      replace `RCClone for Segment` with an inherent `clone` that
141//!      returns `(Self, Tracked<SegmentOwner<M>>)` and per-frame
142//!      `from_in_use + ManuallyDrop::new`. Once it ships, the
143//!      embedding adds `Op::SegmentClone { sid }` that inserts a
144//!      fresh `SegmentEntry` mirroring `sid`'s range.
145//!    - **`next`** — DONE. The conversion bridge between
146//!      segment-held forgotten references and user-held `Frame<M>`
147//!      handles. Per-paddr at the popped slot: `raw_count -= 1`,
148//!      `cover_count -= 1`, `H += 1`, `rc` unchanged. The
149//!      accounting equation `rc == H + P + cover_count` chains in
150//!      lockstep because H and cover decrement/increment together;
151//!      structural `raw_count == cover_count` chains via
152//!      [`lemma_segment_cover_shrink_front`].
153//!    - **`slice`** — NOT modeled, deferred for the same reason as
154//!      `clone` (and probably HARDER, despite initial appearances).
155//!      Both APIs produce an un-droppable handle (no `SegmentOwner`
156//!      returned) and don't bump `raw_count`. A faithful fix
157//!      requires generalising `Frame::from_raw`'s precondition from
158//!      `raw_count <= 1` to `raw_count >= 1` (with decrement-not-
159//!      zero body) so that sliced/cloned segments — which produce
160//!      multiple forgotten refs at the same slot — can each be
161//!      drop-reclaimed. **An attempted exec fix in this session
162//!      revealed unanticipated cascade**: `borrow` / `borrow_paddr`
163//!      bridge through `lemma_from_raw_manuallydrop_general` which
164//!      assumes the single-forgotten-ref case, and the PT-node
165//!      `child_perms_embedding` invariant carries `raw_count <= 1`
166//!      without supplying a `>= 1` companion. Tightening one half
167//!      breaks the other. Properly resolving this requires either:
168//!      (a) per-handle ghost certificates (the `FrameCert` route
169//!          documented in the Item 2 future-work note above), or
170//!      (b) splitting `from_raw` into two variants — one for the
171//!          single-forgotten case (existing) and one for
172//!          multi-forgotten (new) — and threading the
173//!          discriminator through `SegmentOwner` so `drop` can pick
174//!          the right one.
175//!      Either path is half-day to multi-day work. `slice` and
176//!      `clone` should be tackled together when that engineering
177//!      effort is funded; the embedding side is ready to receive
178//!      a `cover_count + 1` / `raw_count + 1` Op as soon as the
179//!      exec ships the API.
180//!    - **`into_raw` / `from_raw`** — `pub(crate)` only in exec, so
181//!      the embedding can ignore them.
182pub mod cursor;
183pub mod frame;
184pub mod io;
185pub mod segment;
186pub mod trace;
187pub mod vm_space;
188
189use core::ops::Range;
190
191use vstd::prelude::*;
192use vstd_extra::ownership::*;
193
194use crate::mm::frame::{MetaSlot, UFrame, has_safe_slot};
195use crate::mm::page_prop::PageProperty;
196use crate::mm::vm_space::UserPtConfig;
197use crate::mm::vm_space::vm_space_specs::VmSpaceOwner;
198use crate::mm::{MAX_USERSPACE_VADDR, Paddr, Vaddr};
199use crate::specs::arch::mm::{MAX_PADDR, PAGE_SIZE};
200use crate::specs::mm::frame::mapping::{frame_to_index, index_to_frame, max_meta_slots};
201use crate::specs::mm::frame::meta_owners::{
202    PageUsage, REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED,
203};
204use crate::specs::mm::frame::meta_region_owners::MetaRegionOwners;
205use crate::specs::mm::io::VmIoOwner;
206use crate::specs::mm::page_table::cursor::owners::CursorOwner;
207use crate::specs::mm::page_table::node::Guards;
208use crate::specs::mm::tlb::TlbModel;
209
210verus! {
211
212// =============================================================================
213// Types
214// =============================================================================
215/// Logical identifier for a [`VmSpaceOwner`] in the store.
216pub type VmSpaceId = int;
217
218/// Logical identifier for a [`CursorOwner`] in the store.
219pub type CursorId = int;
220
221/// Logical identifier for a [`VmIoOwner`] in the store.
222pub type VmIoId = int;
223
224/// Logical identifier for a held [`crate::mm::frame::Frame`] handle in the store.
225pub type FrameId = int;
226
227/// Logical identifier for a held [`crate::mm::frame::Segment`] handle in
228/// the store.
229pub type SegmentId = int;
230
231/// Per-Frame entry in the store. Represents one outstanding handle to
232/// the slot at `paddr` — i.e., one unit of refcount in
233/// `regions.slot_owners[frame_to_index(paddr)]`.
234///
235/// Multiple `FrameEntry`s may share the same `paddr`; each contributes
236/// `+1` to that slot's `inner_perms.ref_count`.
237pub tracked struct FrameEntry {
238    pub paddr: Paddr,
239}
240
241/// Per-Segment entry in the store. Represents one outstanding
242/// `Segment<M>` covering the contiguous physical range `range`.
243///
244/// Per exec [`SegmentOwner::relate_regions`]: every frame slot in
245/// `range` carries one *forgotten* reference for this segment — i.e.,
246/// `raw_count` at the slot equals the number of `SegmentEntry`s
247/// covering it (see [`segment_cover_count`]). The frame's
248/// `ref_count >= 1` is bumped by the segment's owning reference (one
249/// per frame); the segment does *not* hold a separate `Frame` handle,
250/// so the embedding's `frames` map is unrelated to per-segment frame
251/// refcounting.
252///
253/// Multiple `SegmentEntry`s may overlap (e.g. after `clone`); each
254/// independently contributes `+1` to every covered slot's `raw_count`
255/// and `ref_count`.
256///
257/// [`SegmentOwner::relate_regions`]: crate::specs::mm::frame::segment::SegmentOwner::relate_regions
258pub tracked struct SegmentEntry {
259    pub range: Range<Paddr>,
260}
261
262/// Number of outstanding `Segment` handles covering the frame slot
263/// at `paddr` — i.e., `#{ sid : segments[sid].range covers paddr }`.
264/// This is the per-slot `raw_count` term contributed by segments
265/// (Design B: each segment holds one forgotten reference per frame
266/// in its range, so `raw_count == segment_cover_count(segments, ...)`).
267/// Intended to be called on page-aligned paddrs (e.g. via
268/// `index_to_frame(idx)`); segment ranges are themselves page-
269/// aligned so the resulting count is the same for any paddr within
270/// a given page.
271pub open spec fn segment_cover_count(segments: Map<SegmentId, SegmentEntry>, paddr: Paddr) -> nat {
272    segments.dom().filter(
273        |sid: SegmentId| segments[sid].range.start <= paddr && paddr < segments[sid].range.end,
274    ).len()
275}
276
277/// A positive segment-cover count exhibits a witnessing segment id whose
278/// range covers `paddr`. Used to lift `segment_cover_count(..) > 0` into
279/// the structural `covered ⟹ usage == Frame` clause (which is keyed by a
280/// concrete `(sid, paddr)`), replacing the retired `raw_count` cache.
281pub proof fn lemma_segment_cover_witness(
282    segments: Map<SegmentId, SegmentEntry>,
283    paddr: Paddr,
284) -> (sid: SegmentId)
285    requires
286        segment_cover_count(segments, paddr) > 0,
287    ensures
288        segments.dom().contains(sid),
289        segments[sid].range.start <= paddr < segments[sid].range.end,
290{
291    let covering = segments.dom().filter(
292        |sid: SegmentId| segments[sid].range.start <= paddr && paddr < segments[sid].range.end,
293    );
294    assert(covering.len() > 0);
295    let sid = covering.choose();
296    assert(covering.contains(sid));
297    sid
298}
299
300/// Number of outstanding `Frame` handles whose paddr maps to slot
301/// `idx` — i.e. the `#handles(idx)` term of the exact reference-count
302/// accounting `ref_count(idx) == #handles(idx) + paths_in_pt(idx).len()`
303/// (Stage 5 / full #4). Well-defined as a `nat` only when
304/// `frames.dom()` is finite, which [`VmStore::inv`] maintains.
305pub open spec fn handle_count(frames: Map<FrameId, FrameEntry>, idx: usize) -> nat {
306    frames.dom().filter(|fid: FrameId| frame_to_index(frames[fid].paddr) == idx).len()
307}
308
309/// Handle-count delta under [`Map::insert`] at a fresh id: +1 at the
310/// inserted entry's slot, unchanged elsewhere. Discharges the Set /
311/// filter arithmetic once so the per-step accounting proofs need only
312/// invoke it.
313pub proof fn lemma_handle_count_insert_fresh(
314    frames: Map<FrameId, FrameEntry>,
315    id: FrameId,
316    entry: FrameEntry,
317    idx: usize,
318)
319    requires
320        !frames.dom().contains(id),
321        frames.dom().finite(),
322    ensures
323        handle_count(frames.insert(id, entry), idx) == handle_count(frames, idx) + (
324        if frame_to_index(entry.paddr) == idx {
325            1nat
326        } else {
327            0nat
328        }),
329{
330    let frames2 = frames.insert(id, entry);
331    let new_filt = frames2.dom().filter(|fid: FrameId| frame_to_index(frames2[fid].paddr) == idx);
332    let old_filt = frames.dom().filter(|fid: FrameId| frame_to_index(frames[fid].paddr) == idx);
333    assert(frames2.dom() =~= frames.dom().insert(id));
334    if frame_to_index(entry.paddr) == idx {
335        assert(new_filt =~= old_filt.insert(id)) by {
336            assert forall|fid: FrameId| #[trigger] new_filt.contains(fid) implies old_filt.insert(
337                id,
338            ).contains(fid) by {
339                if fid != id {
340                    assert(frames2[fid] == frames[fid]);
341                }
342            };
343            assert forall|fid: FrameId| #[trigger]
344                old_filt.insert(id).contains(fid) implies new_filt.contains(fid) by {
345                if fid != id {
346                    assert(frames2[fid] == frames[fid]);
347                } else {
348                    assert(frames2[id] == entry);
349                }
350            };
351        };
352        assert(!old_filt.contains(id));
353        assert(old_filt.finite());
354        assert(new_filt.len() == old_filt.len() + 1);
355    } else {
356        assert(new_filt =~= old_filt) by {
357            assert forall|fid: FrameId| #[trigger] new_filt.contains(fid) implies old_filt.contains(
358                fid,
359            ) by {
360                if fid != id {
361                    assert(frames2[fid] == frames[fid]);
362                } else {
363                    assert(frames2[id] == entry);
364                }
365            };
366            assert forall|fid: FrameId| #[trigger] old_filt.contains(fid) implies new_filt.contains(
367                fid,
368            ) by {
369                assert(fid != id);
370                assert(frames2[fid] == frames[fid]);
371            };
372        };
373    }
374}
375
376/// Handle-count delta under [`Map::remove`]: -1 at the removed entry's
377/// slot if it was the only one present (or generally `-1` if the entry
378/// at `fid` mapped to `idx`), unchanged elsewhere.
379pub proof fn lemma_handle_count_remove(frames: Map<FrameId, FrameEntry>, fid: FrameId, idx: usize)
380    requires
381        frames.dom().contains(fid),
382        frames.dom().finite(),
383    ensures
384        handle_count(frames.remove(fid), idx) == handle_count(frames, idx) - (if frame_to_index(
385            frames[fid].paddr,
386        ) == idx {
387            1nat
388        } else {
389            0nat
390        }),
391{
392    let frames2 = frames.remove(fid);
393    let new_filt = frames2.dom().filter(|gid: FrameId| frame_to_index(frames2[gid].paddr) == idx);
394    let old_filt = frames.dom().filter(|gid: FrameId| frame_to_index(frames[gid].paddr) == idx);
395    assert(frames2.dom() =~= frames.dom().remove(fid));
396    if frame_to_index(frames[fid].paddr) == idx {
397        assert(old_filt.contains(fid));
398        assert(new_filt =~= old_filt.remove(fid)) by {
399            assert forall|gid: FrameId| #[trigger] new_filt.contains(gid) implies old_filt.remove(
400                fid,
401            ).contains(gid) by {
402                assert(gid != fid);
403                assert(frames2[gid] == frames[gid]);
404            };
405            assert forall|gid: FrameId| #[trigger]
406                old_filt.remove(fid).contains(gid) implies new_filt.contains(gid) by {
407                assert(gid != fid);
408                assert(frames2[gid] == frames[gid]);
409            };
410        };
411        assert(old_filt.finite());
412        assert(new_filt.len() == (old_filt.len() - 1) as nat);
413    } else {
414        assert(!old_filt.contains(fid));
415        assert(new_filt =~= old_filt) by {
416            assert forall|gid: FrameId| #[trigger] new_filt.contains(gid) implies old_filt.contains(
417                gid,
418            ) by {
419                assert(gid != fid);
420                assert(frames2[gid] == frames[gid]);
421            };
422            assert forall|gid: FrameId| #[trigger] old_filt.contains(gid) implies new_filt.contains(
423                gid,
424            ) by {
425                assert(gid != fid);
426                assert(frames2[gid] == frames[gid]);
427            };
428        };
429    }
430}
431
432/// **Embedding-level `Frame::wf(state)`.** Derives the full
433/// [`frame::drop_pre`] residual (rc / storage / in_list / paths-empty
434/// conjuncts) plus the `rc == 1 ⟹ handle_count == 1` clause from
435/// `s.inv()`, given only:
436///   - `fid` is a registered handle,
437///   - no `SegmentEntry` covers the slot
438///     (`segment_cover_count == 0`).
439///
440/// Replaces the residual `drop_pre` baggage on `op_pre[FrameDrop]` /
441/// `step_frame_drop` with a single tracked invariant chain. Every
442/// conjunct is recovered from a specific `VmStore::inv` clause:
443///   - `slots.contains_key`: structural slot-perm coverage.
444///   - `raw_count == 0`: structural `raw_count == segment_cover_count`
445///     + the `segment_cover_count == 0` hypothesis.
446///   - `rc > 0` / `rc != UNUSED` / `rc != UNIQUE` / `rc == H + P`:
447///     accounting clause 4 (active head: H >= 1 since `fid` is
448///     registered + structural FrameId⟹Frame-usage).
449///   - `rc <= REF_COUNT_MAX`: clause 4 (`rc != UNIQUE`) +
450///     `MetaSlotOwner::inv`'s forbidden-range empty
451///     (`MAX < rc < UNIQUE ⟹ false`).
452///   - `rc == 1 ⟹ storage.is_init ∧ in_list == 0`:
453///     `MetaSlotOwner::inv`'s SHARED branch (`0 < rc <= MAX`),
454///     which is the Item 1 strengthening.
455///   - `rc == 1 ⟹ paths_in_pt.is_empty()`: clause 4 + `H >= 1`
456///     gives `1 == H + P` ⟹ `P == 0` ⟹ `paths.len == 0` ⟹
457///     `paths.is_empty()`.
458///   - `rc == 1 ⟹ handle_count == 1`: clause 4 with `rc == 1`
459///     gives `1 == H + P`; with `H >= 1` and `P >= 0`, `H == 1`
460///     and `P == 0`.
461pub proof fn lemma_frame_drop_pre_derivable<'rcu>(s: VmStore<'rcu>, fid: FrameId)
462    requires
463        s.inv(),
464        s.frames.dom().contains(fid),
465        segment_cover_count(s.segments, s.frames[fid].paddr) == 0,
466    ensures
467        frame::drop_pre(s.regions, s.frames[fid].paddr),
468        s.regions.slot_owners[frame_to_index(s.frames[fid].paddr)].inner_perms.ref_count.value()
469            == 1 ==> handle_count(s.frames, frame_to_index(s.frames[fid].paddr)) == 1,
470{
471    let paddr = s.frames[fid].paddr;
472    let idx = frame_to_index(paddr);
473    // `fid` registered ⟹ paddr in-bound ⟹ idx is a managed slot.
474    assert(has_safe_slot(paddr));
475    s.regions.inv_implies_correct_addr(paddr);
476    assert(idx < max_meta_slots());
477    // `fid` registered ⟹ handle_count(s.frames, idx) >= 1.
478    assert(s.frames.dom().filter(
479        |gid: FrameId| frame_to_index(s.frames[gid].paddr) == idx,
480    ).contains(fid));
481    assert(handle_count(s.frames, idx) >= 1);
482    // Structural FrameId⟹Frame-usage at idx.
483    assert(s.regions.slot_owners[idx].usage == PageUsage::Frame);
484    // Accounting clause 3 + 4 (active head Frame): pin rc, storage.
485    let so = s.regions.slot_owners[idx];
486    let rc = so.inner_perms.ref_count.value();
487    assert(rc != REF_COUNT_UNUSED);
488    assert(rc != REF_COUNT_UNIQUE);
489    assert(rc == handle_count(s.frames, idx) + so.paths_in_pt.len());
490    assert(so.inner_perms.storage.is_init());
491    // rc != UNUSED ⟹ rc > 0 (UNUSED is u64::MAX; rc could still be 0,
492    // but clause 4 gives rc == H + P ≥ 1).
493    assert(rc > 0);
494    // `MetaSlotOwner::inv` SHARED branch: 0 < rc <= MAX ⟹ storage.is_init,
495    // in_list == 0, vtable_ptr.is_init.
496    assert(s.regions.slot_owners.contains_key(idx));
497    assert(s.regions.slot_owners[idx].inv());
498    // rc != UNUSED ∧ rc != UNIQUE ∧ rc > 0 ⟹ rc ∈ [1, MAX]
499    // (forbidden range MAX < rc < UNIQUE is empty per MetaSlotOwner::inv).
500    assert(rc <= REF_COUNT_MAX);
501    assert(so.inner_perms.in_list.value() == 0);
502    // rc == 1 ⟹ paths empty (from rc == H + P and H >= 1).
503    if rc == 1 {
504        assert(handle_count(s.frames, idx) + so.paths_in_pt.len() == 1);
505        assert(so.paths_in_pt.len() == 0);
506        assert(so.paths_in_pt =~= Set::empty());
507        assert(handle_count(s.frames, idx) == 1);
508    }
509}
510
511/// Whether a [`VmIoOwner`] backs a `VmReader` or a `VmWriter`.
512pub enum VmIoKind {
513    Reader,
514    Writer,
515}
516
517/// Per-VmIo entry in the store.
518///
519/// `vm_space` is `None` for VmIoOwners that have no parent `VmSpace` —
520/// kernel-space readers/writers from `VmReader::from_kernel_space` /
521/// `VmWriter::from_kernel_space`, and val_owners produced by
522/// `read`. `Some(vs)` for entries created by `VmSpace::reader` /
523/// `writer`.
524///
525/// View state is fully determined by `vm_space` + `kind`:
526/// - `Some(_)` (userspace, Fallible): `mem_view: None`, exactly as
527///   `VmSpace::reader`/`writer` ensure ([vm_space.rs:323/382](crate::mm::vm_space)).
528///   Fallible methods are handle-only — no owner-side activation step
529///   exists or is needed.
530/// - `None && Reader` (kernel reader): `read_view_initialized()`, per
531///   `VmReader<Infallible>::from_kernel_space` ensures.
532/// - `None && Writer` (kernel writer or `consumed_w` val_owner from
533///   `read`): `has_write_view()`, per `from_kernel_space` /
534///   [`io::read_step`] ensures.
535pub tracked struct VmIoEntry {
536    pub vm_space: Option<VmSpaceId>,
537    pub kind: VmIoKind,
538    pub vaddr: Vaddr,
539    pub len: usize,
540    pub owner: VmIoOwner,
541}
542
543impl VmIoEntry {
544    /// Per-entry invariant: derives view state from `vm_space` + `kind`.
545    pub open spec fn inv(self) -> bool {
546        &&& self.owner.inv()
547        &&& match self.vm_space {
548            Some(_) => self.owner.mem_view is None,
549            None => match self.kind {
550                VmIoKind::Reader => self.owner.read_view_initialized(),
551                VmIoKind::Writer => self.owner.has_write_view(),
552            },
553        }
554    }
555
556    /// Operand-typing for the Infallible `read`/`write` ops. Exec
557    /// `VmReader::<Infallible>::read` / `VmWriter::<Infallible>::write`
558    /// are *typed* on kernel (`Infallible`) reader/writer handles; the
559    /// embedding proxies "kernel/Infallible" with `vm_space is None` and
560    /// reader-vs-writer with `kind`. These are not runtime preconditions
561    /// — a userspace (Fallible) handle simply cannot be passed where the
562    /// type system demands a kernel one — so they read as a
563    /// well-formedness check on the operand, not a checkable obligation.
564    /// (`inv` already gives `read_view_initialized` / `has_write_view`
565    /// for these cases, exactly what `vm_reader_read_embedded` consumes.)
566    pub open spec fn is_kernel_reader(self) -> bool {
567        &&& self.vm_space is None
568        &&& self.kind == VmIoKind::Reader
569    }
570
571    pub open spec fn is_kernel_writer(self) -> bool {
572        &&& self.vm_space is None
573        &&& self.kind == VmIoKind::Writer
574    }
575}
576
577/// Whether a cursor is a read-only [`Cursor`] or a mutable [`CursorMut`].
578///
579/// [`Cursor`]: crate::mm::vm_space::Cursor
580/// [`CursorMut`]: crate::mm::vm_space::CursorMut
581pub enum CursorKind {
582    ReadOnly,
583    Mutable,
584}
585
586/// Per-cursor entry in the store.
587///
588/// `guards` is the lock-protocol state for the page-table nodes the
589/// cursor holds locked; mirrors what the exec `Cursor` carries via
590/// `path: [Option<PageTableGuard<'rcu, C>>; NR_LEVELS]`.
591pub tracked struct CursorEntry<'rcu> {
592    pub vm_space: VmSpaceId,
593    pub kind: CursorKind,
594    pub va: Range<Vaddr>,
595    pub owner: CursorOwner<'rcu, UserPtConfig>,
596    pub guards: Guards<'rcu>,
597}
598
599impl<'rcu> CursorEntry<'rcu> {
600    /// The portion of the exec `Cursor::invariants(owner, regions, guards)`
601    /// expressible from the entry alone (no `regions`).
602    ///
603    /// Mirrors `crate::mm::page_table::Cursor::invariants` minus
604    /// `regions.inv()`, `metaregion_sound(regions)`, and the exec-handle
605    /// pieces (`self.inv()` / `self.wf(owner)`). Those live in
606    /// [`VmStore::inv`] (regions-touching) and are MODEL GAPS (handle).
607    pub open spec fn inv(self) -> bool {
608        &&& self.owner.inv()
609        &&& self.owner.children_not_locked(self.guards)
610        &&& self.owner.nodes_locked(self.guards)
611        &&& !self.owner.popped_too_high
612    }
613}
614
615/// Resource store: the abstract state visible to a caller of the
616/// VmSpace + VmReader/VmWriter API.
617///
618/// `tlb_model` is the global TLB model; mirrors the per-CPU `TlbModel`
619/// that `CursorMut::map`/`unmap` and `flusher` operate on. We keep one
620/// per store on the conservative assumption that any cursor mutation
621/// interacts with it.
622pub tracked struct VmStore<'rcu> {
623    pub regions: MetaRegionOwners,
624    pub tlb_model: TlbModel,
625    pub vm_spaces: Map<VmSpaceId, VmSpaceOwner>,
626    pub cursors: Map<CursorId, CursorEntry<'rcu>>,
627    pub vm_ios: Map<VmIoId, VmIoEntry>,
628    pub frames: Map<FrameId, FrameEntry>,
629    pub segments: Map<SegmentId, SegmentEntry>,
630}
631
632impl<'a, 'rcu> VmStore<'rcu> {
633    /// The store's top-level invariant.
634    ///
635    /// Decomposed into [`structural_inv`] (everything generic store
636    /// helpers can preserve when they only touch one of `frames` /
637    /// `cursors` / `vm_ios` / `vm_spaces`) and [`accounting_inv`] (the
638    /// exact reference-count equation, which couples `frames` with
639    /// `regions.slot_owners` and can only be re-established by a *step*
640    /// that pairs the two changes — see [`extract_frame`] /
641    /// [`insert_frame`] for why the frame-only helpers must require /
642    /// ensure only the structural part).
643    pub open spec fn inv(self) -> bool {
644        self.structural_inv() && self.accounting_inv()
645    }
646
647    /// Everything in [`inv`] **except** the accounting equation.
648    /// Preserved by any helper that touches at most one of `frames` /
649    /// `regions.slot_owners`, since the accounting equation is the only
650    /// clause that mentions both. Frame-only helpers
651    /// ([`extract_frame`] / [`insert_frame`]) require / ensure this.
652    pub open spec fn structural_inv(self) -> bool {
653        &&& self.regions.inv()
654        // Slot-perm coverage (Design B). Every in-region slot keeps its
655        // `simple_pptr::PointsTo<MetaSlot>` parked in `regions.slots`.
656        // `MetaRegionOwners::inv` only gives the *forward* direction
657        // (`slots.contains_key(i) ==> i < max_meta_slots()`); the reverse
658        // is NOT globally true (`UniqueFrame` / `into_raw` / linked-list
659        // permanently extract a slot perm). It IS true here because the
660        // embedding's `Op` surface contains *no* perm-extracting
661        // operation: `FrameFromUnused` re-parks the perm (modeled in
662        // [`frame::frame_from_unused_embedded`]), `FrameFromInUse` /
663        // `FrameDrop` / `Segment` only shared-borrow it, and every
664        // region-mutating cursor op (`Map`/`Unmap`/`ProtectNext`) touches
665        // `slot_owners` (refcount / `paths_in_pt`) but never the `slots`
666        // map domain. This is what lets [`op_pre`] for `FrameFromUnused`
667        // / `FrameFromInUse` be literally `true` (#2 / #3b fully
668        // resolved): the `has_safe_slot`-guarded slot-perm precondition
669        // of the relaxed exec / axiom is recovered from this clause for
670        // the in-bound case and is vacuous out-of-bound.
671        &&& forall|idx: usize|
672            idx < max_meta_slots() ==> #[trigger] self.regions.slots.contains_key(
673                idx,
674            )
675        // Segment-cover info is sourced directly from the `segments` map
676        // via `segment_cover_count` (see `accounting_inv`'s rc equation).
677        // The per-slot `raw_count` cache that previously mirrored it has
678        // been retired.
679        &&& forall|idx: usize|
680            idx < max_meta_slots()
681                ==> #[trigger] self.regions.slot_owners[idx].inner_perms.in_list.value() == 0
682        &&& self.tlb_model.inv()
683        &&& forall|id: VmSpaceId| #[trigger]
684            self.vm_spaces.dom().contains(id) ==> self.vm_spaces[id].inv()
685        &&& forall|id: CursorId| #[trigger]
686            self.cursors.dom().contains(id) ==> self.cursors[id].inv()
687        &&& forall|id: CursorId| #[trigger]
688            self.cursors.dom().contains(id) ==> self.cursors[id].owner.metaregion_sound(
689                self.regions,
690            )
691        &&& forall|id: CursorId| #[trigger]
692            self.cursors.dom().contains(id) ==> self.vm_spaces.dom().contains(
693                self.cursors[id].vm_space,
694            )
695        &&& forall|id: VmIoId| #[trigger] self.vm_ios.dom().contains(id) ==> self.vm_ios[id].inv()
696        &&& forall|id: VmIoId| #[trigger]
697            self.vm_ios.dom().contains(id) ==> (self.vm_ios[id].vm_space matches Some(vs)
698                ==> self.vm_spaces.dom().contains(vs))
699        &&& forall|id: VmIoId| #[trigger]
700            self.vm_ios.dom().contains(id) ==> self.vm_ios[id].vm_space is Some ==> (
701            self.vm_ios[id].vaddr as nat) + (self.vm_ios[id].len as nat)
702                <= MAX_USERSPACE_VADDR as nat
703            // `frames` is bookkeeping for outstanding `Frame` handles. Every
704            // registered handle came from a *successful* `from_unused` /
705            // `from_in_use`, which (post-relaxation) returns `None` unless
706            // `has_safe_slot(paddr)` — so every live `FrameEntry`'s paddr is
707            // in-bound. With the slot-perm / `raw_count` / `in_list`
708            // coverage clauses above, this discharges `drop_pre`'s
709            // `slots.contains_key` (#4-a), `raw_count == 0` (#4-b),
710            // `!= REF_COUNT_UNUSED` (#4-d, from the bound), and the
711            // `in_list == 0` half of the last-ref conjunct (#4-f).
712        &&& forall|fid: FrameId| #[trigger]
713            self.frames.dom().contains(fid) ==> has_safe_slot(
714                self.frames[fid].paddr,
715            )
716        // Every registered handle's slot has `usage == PageUsage::Frame`.
717        // True by construction: every `Op` that adds a `FrameId`
718        // (`FrameFromUnused`, `FrameFromInUse`, `Query` on a tracked
719        // leaf) commits to a Frame-usage slot. Carrying this in
720        // `structural_inv` makes accounting_inv's Frame-scoped clauses
721        // apply automatically at registered handles' paddrs and
722        // simplifies `op_pre[Map]` / `step_query` / the Item 4 unmap
723        // axiom (no need for the caller to re-establish usage).
724        &&& forall|fid: FrameId| #[trigger]
725            self.frames.dom().contains(fid) ==> self.regions.slot_owners[frame_to_index(
726                self.frames[fid].paddr,
727            )].usage
728                == PageUsage::Frame
729            // `frames.dom()` is finite (built by finitely many `insert_frame`
730            // from an empty map; never an infinite `choose`). Needed for
731            // [`handle_count`] (`Set::len`) to be a well-defined `nat` in the
732            // Stage-5 accounting clause.
733        &&& self.frames.dom().finite()
734        // Same for `segments.dom()` — needed for
735        // [`segment_cover_count`] to be a well-defined `nat`.
736        &&& self.segments.dom().finite()
737        // Every registered segment has a well-formed range
738        // (page-aligned, in-bound, non-empty). Enforced by
739        // `op_pre[SegmentFromUnused]`; carried as an invariant so
740        // `step_segment_drop` can discharge `segment::drop_step`'s
741        // alignment preconditions from `s.inv()` alone.
742        &&& forall|sid: SegmentId| #[trigger]
743            self.segments.dom().contains(sid) ==> {
744                let r = self.segments[sid].range;
745                &&& r.start % PAGE_SIZE == 0
746                &&& r.end % PAGE_SIZE == 0
747                &&& r.start < r.end
748                &&& r.end <= MAX_PADDR
749            }
750            // Every segment-covered slot has `usage == PageUsage::Frame`.
751            // True by construction: `Op::SegmentFromUnused` sets the
752            // covered slots' usage to Frame, and no op transitions a
753            // segment-covered slot back to non-Frame (frame_drop is gated
754            // on `segment_cover_count == 0` via `op_pre[FrameDrop]`).
755            // Carried here so `step_segment_drop` can derive the per-slot
756            // SHARED+Frame conditions from `s.inv()` alone.
757        &&& forall|sid: SegmentId, paddr: Paddr|
758            #![trigger
759                    self.segments.dom().contains(sid),
760                    frame_to_index(paddr)]
761            self.segments.dom().contains(sid) && self.segments[sid].range.start <= paddr
762                < self.segments[sid].range.end && paddr % PAGE_SIZE == 0
763                ==> self.regions.slot_owners[frame_to_index(paddr)].usage
764                == PageUsage::Frame
765            // `paths_in_pt.finite()` is now a per-slot fact in
766            // `MetaSlotOwner::inv` (main verification), implied here via
767            // `regions.inv() → slot_owners[i].inv()` for any `i <
768            // max_meta_slots()`.
769
770    }
771
772    /// Stage 5 / full #4 — EXACT reference-count accounting.
773    ///
774    /// Scoped to *active-head* tracked data frames: `usage == Frame`
775    /// (excludes PT nodes — different rc semantics — and MMIO), and the
776    /// slot is an active head (`#handles > 0 || #mappings > 0`). The
777    /// active-head restriction sidesteps huge-page sub-page slots
778    /// (j>0): those have `H==0`, `paths.len()==0`, yet `rc>0` via
779    /// `frame_sub_pages_valid`, so they are *not* active heads and the
780    /// equation does not apply to them (and `op_pre[FrameDrop]` never
781    /// targets a sub-page — a `FrameEntry` paddr is always a head).
782    ///
783    /// For an active head: `rc` is neither sentinel, equals
784    /// `#handles + #mappings`, and the slot's metadata storage is
785    /// initialised (it is in use).
786    ///
787    /// The exact equation is *Frame-scoped*. For non-Frame `FrameEntry`
788    /// slots, the residual `drop_pre` obligation (rc/storage/in_list/
789    /// paths) is carried directly in `op_pre[FrameDrop]` (un-doing
790    /// part of #4) until the deferred main-verification refactor
791    /// strengthens `MetaSlotOwner::inv` and adds `Frame::wf(state)`.
792    ///
793    /// **Why split from `structural_inv`:** the equation references
794    /// *both* `self.frames` (via `handle_count`) *and*
795    /// `self.regions.slot_owners` (via `rc` and `paths_in_pt`), so any
796    /// helper that mutates one without the other can break it
797    /// transiently. The frame-only store helpers [`extract_frame`] /
798    /// [`insert_frame`] therefore cannot ensure this clause alone — a
799    /// step that pairs a frame change with the matching regions change
800    /// (via a frame / cursor `_embedded` axiom) re-establishes it.
801    pub open spec fn accounting_inv(self) -> bool {
802        // Stage 5.5c absorption clauses (couple `frames` + `regions`).
803        //
804        // The earlier usage-independent **handle clause** (Stage 5 / 2b,
805        // `H > 0 ⟹ rc ∉ {UNUSED, UNIQUE} ∧ rc ≥ H ∧ storage.is_init`)
806        // was **dropped**. Two reasons:
807        //
808        // (a) It was load-bearing only via Verus SMT heuristics across
809        //     `step_cursor_method`/`step_map`/`step_unmap`: the cursor
810        //     `_embedded` axioms don't actually constrain `rc`/`storage`
811        //     at the touched slot, and accounting_inv preservation
812        //     across those steps was working by coincidence. Segments
813        //     (Shape B) perturbed the SMT context and broke the chain
814        //     — the fragility was always there.
815        //
816        // (b) The semantically right home for these conjuncts is the
817        //     *exec layer*: `MetaSlotOwner::inv`'s SHARED branch should
818        //     carry `storage.is_init() ∧ in_list.value() == 0` for any
819        //     in-use rc (they're universally true, see the lifecycle
820        //     analysis), and `Frame<M>` should have a `wf(state)`
821        //     predicate carrying "the slot I refer to is in a valid
822        //     state with `rc ≥ handles_for_this_slot`." Then the
823        //     embedding's accounting could shrink to just the
824        //     Frame-scoped equation (clauses below), and the cursor
825        //     axiom interaction goes away because there's nothing
826        //     handle-keyed to chain.
827        //
828        // Until the main-verification refactor in (b) lands,
829        // `op_pre[FrameDrop]` carries the full residual `drop_pre`
830        // directly. Frame-usage callers discharge it from the
831        // Frame-scoped equation clauses below; non-Frame callers carry
832        // their own reasoning.
833        //
834        // See the TODO in segment.rs for the full plan.
835        // **UNUSED ⟹ no users.** A live PTE bumps `rc`, so reaching
836        // `UNUSED` requires `paths_in_pt.is_empty()`. With segments
837        // (Shape B), reaching UNUSED also requires no segment covers
838        // the slot — each segment contributes 1 to rc via its
839        // forgotten Frame handle.
840        &&& forall|idx: usize|
841            #![trigger self.regions.slot_owners[idx]]
842            idx < max_meta_slots() && self.regions.slot_owners[idx].inner_perms.ref_count.value()
843                == REF_COUNT_UNUSED ==> handle_count(self.frames, idx) == 0
844                && self.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
845                self.segments,
846                index_to_frame(idx),
847            )
848                == 0
849            // **Frame in valid rc range ⟹ active head.** Inverse of the
850            // active-head guard below — absorbs the pre-active-head assume
851            // in `step_frame_from_in_use`. With segments, "active" includes
852            // the segment-cover contribution.
853        &&& forall|idx: usize|
854            #![trigger self.regions.slot_owners[idx]]
855            idx < max_meta_slots() && self.regions.slot_owners[idx].usage == PageUsage::Frame
856                && self.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
857                && self.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNIQUE
858                ==> handle_count(self.frames, idx) > 0
859                || self.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
860                self.segments,
861                index_to_frame(idx),
862            )
863                > 0
864            // **Frame-slot accounting equation.** Generalised to include
865            // segment forgotten references: `rc == H + P + cover_count`.
866            // Each segment in `segments` whose range covers the frame
867            // contributes +1 to `rc` (via its `ManuallyDrop`'d Frame
868            // handle); user-held handles contribute via `H`; live PTEs
869            // contribute via `P`. With `segments` empty (pre-activation),
870            // `cover_count == 0` and the equation reduces to `rc == H + P`.
871        &&& forall|idx: usize|
872            #![trigger self.regions.slot_owners[idx]]
873            idx < max_meta_slots() && self.regions.slot_owners[idx].usage == PageUsage::Frame && (
874            handle_count(self.frames, idx) > 0 || self.regions.slot_owners[idx].paths_in_pt.len()
875                > 0 || segment_cover_count(self.segments, index_to_frame(idx)) > 0) ==> {
876                let so = self.regions.slot_owners[idx];
877                let rc = so.inner_perms.ref_count.value();
878                &&& rc != REF_COUNT_UNUSED
879                &&& rc != REF_COUNT_UNIQUE
880                &&& rc == handle_count(self.frames, idx) + so.paths_in_pt.len()
881                    + segment_cover_count(self.segments, index_to_frame(idx))
882                &&& so.inner_perms.storage.is_init()
883            }
884    }
885}
886
887// =============================================================================
888// Op enum + per-op precondition
889// =============================================================================
890/// Public exec API of `ostd::mm::vm_space` and `ostd::mm::io`, lifted
891/// to data.
892pub enum Op {
893    NewVmSpace,
894    DropVmSpace { vs: VmSpaceId },
895    OpenCursor { vs: VmSpaceId, va: Range<Vaddr> },
896    OpenCursorMut { vs: VmSpaceId, va: Range<Vaddr> },
897    DropCursor { c: CursorId },
898    Query { c: CursorId },
899    FindNext { c: CursorId, len: usize },
900    Jump { c: CursorId, va: Vaddr },
901    VirtAddr { c: CursorId },
902    Map { c: CursorId, fid: FrameId, prop: PageProperty },
903    Unmap { c: CursorId, len: usize },
904    ProtectNext { c: CursorId, len: usize },
905    NewReader { vs: VmSpaceId, vaddr: Vaddr, len: usize },
906    NewWriter { vs: VmSpaceId, vaddr: Vaddr, len: usize },
907    NewKernelReader { vaddr: Vaddr, len: usize },
908    NewKernelWriter { vaddr: Vaddr, len: usize },
909    DropReader { vio: VmIoId },
910    DropWriter { vio: VmIoId },
911    /// Fallible `VmReader::read_val<T>`. The exec spec carries no
912    /// tracked owner params (handle MODEL GAP); the embedding step
913    /// is consequently a no-op on `VmStore`.
914    ReaderReadVal { source: VmIoId },
915    /// Fallible `VmReader::collect`. Same shape as `ReaderReadVal`.
916    ReaderCollect { source: VmIoId },
917    ReaderLimit { vio: VmIoId, max: usize },
918    ReaderSkip { vio: VmIoId, n: usize },
919    ReaderQuery { vio: VmIoId },
920    /// Fallible `VmWriter::write_val<T>`. Same shape as `ReaderReadVal`.
921    WriterWriteVal { writer: VmIoId },
922    WriterFillZeros { vio: VmIoId, len: usize },
923    WriterLimit { vio: VmIoId, max: usize },
924    WriterSkip { vio: VmIoId, n: usize },
925    WriterQuery { vio: VmIoId },
926    /// Infallible `VmReader::read`. Produces a `consumed_w` val_owner
927    /// (registered as a fresh activated Writer entry).
928    Read { source: VmIoId, dest: VmIoId },
929    /// Infallible `VmWriter::write`. The exec no longer surfaces
930    /// `consumed_w`; the embedding does NOT create a fresh entry.
931    Write { source: VmIoId, dest: VmIoId },
932    /// `Frame::from_unused`: try to allocate a fresh handle on a
933    /// previously-unused slot. Registers a [`FrameEntry`] on success.
934    FrameFromUnused { paddr: Paddr },
935    /// `Frame::from_in_use`: try to acquire a new handle on an
936    /// in-use slot. Registers a [`FrameEntry`] on success
937    /// (refcount of the slot increments by one).
938    FrameFromInUse { paddr: Paddr },
939    /// Drop one outstanding `Frame` handle. There is exactly one drop;
940    /// the step branches internally on the live refcount (mirroring
941    /// exec `drop`): `>= 2` decrements (slot stays SHARED), `== 1`
942    /// tears down to UNUSED (requires the slot detached from the page
943    /// table — `paths_in_pt.is_empty()`). See [`frame::drop_pre`].
944    FrameDrop { fid: FrameId },
945    /// `Segment::from_unused`: allocate a fresh segment over a range
946    /// of previously-unused slots. Each frame in `range` transitions
947    /// `usage == Unused` → `Frame`, `rc` 0 → 1, `raw_count` 0 → 1.
948    /// Registers a [`SegmentEntry`] on success.
949    SegmentFromUnused { range: Range<Paddr> },
950    /// Drop a `Segment` handle. Releases the segment's forgotten
951    /// reference at each frame in the range; frames whose `rc`
952    /// reaches 1 transition to UNUSED.
953    SegmentDrop { sid: SegmentId },
954    /// `Segment::split`: split a segment at a page-aligned byte
955    /// `offset` from its start, producing two segments covering the
956    /// disjoint halves. `regions` is unchanged (per-paddr
957    /// `cover_count` is invariant — each covered paddr lands in
958    /// exactly one half). Removes `sid` from `s.segments`, inserts
959    /// two fresh `SegmentEntry`s.
960    SegmentSplit { sid: SegmentId, offset: usize },
961    // **Op::SegmentNext is NOT modeled.** It would be the
962    // conversion bridge between segment-held forgotten references
963    // and user-held Frame handles — at the popped paddr:
964    // `raw_count -= 1`, `cover_count -= 1`, `H += 1`, `rc` unchanged.
965    // The model is laid out in [`segment::segment_next_embedded`]
966    // (axiom present, proof-discharge scaffolded but ends in
967    // `assume(false)`); reaching a real `s.inv()` discharge needs
968    // SMT-chaining cleanup at the per-paddr assert-forall sites
969    // that this pass didn't finish. Tracked as future work alongside
970    // `SegmentSlice`.
971    /// `Segment::next`: pop the front frame off `sid`'s range,
972    /// producing a fresh `Frame<M>` handle (a new `FrameEntry`
973    /// registered in `s.frames`). The segment's range shrinks by one
974    /// page from the front; if it becomes empty, `sid` is removed
975    /// from `s.segments`. The conversion bridge between segment-held
976    /// forgotten references and user-held Frame handles: at the
977    /// popped paddr `raw_count -= 1`, `cover_count -= 1`, `H += 1`,
978    /// `rc` unchanged.
979    SegmentNext {
980        sid: SegmentId,
981    },
982    // **Op::SegmentSlice is NOT modeled — same exec gap as
983    // `SegmentClone`, plus weaker ensures.**
984    //
985    // Exec [`crate::mm::frame::Segment::slice`] does
986    // `inc_frame_ref_count` per frame in the sub-range (bumps `rc`
987    // only, not `raw_count`) and returns just `Self` — no
988    // `SegmentOwner`, so the sliced segment is un-droppable from
989    // verified code. Compounding this, its `ensures` only commits to
990    // `slots`/`slot_owners.dom()` preservation; the per-frame `rc`
991    // bumps aren't surfaced. A faithful embedding axiom would need
992    // both:
993    //   1. The same exec fix as `SegmentClone`
994    //      (`Frame::from_raw` precondition relaxation, weakened
995    //      `relate_regions`, owner-producing API).
996    //   2. Stronger exec ensures pinning the per-frame `rc`/
997    //      `raw_count` deltas at sliced paddrs.
998    //
999    // Once both ship, the embedding can add `Op::SegmentSlice {
1000    // sid, sub_range }` that inserts a fresh `SegmentEntry` with the
1001    // sub-range (per-paddr `cover_count += 1` inside sub-range,
1002    // unchanged outside) and discharges accounting via the same
1003    // shrink-from-front machinery used by `next`.
1004    // **Op::SegmentClone is NOT modeled — pending exec fix.**
1005    //
1006    // Exec [`crate::mm::frame::Segment::clone`] today bumps `rc` per
1007    // frame (via `inc_frame_ref_count`) but doesn't bump `raw_count`
1008    // and doesn't produce a new `SegmentOwner`. The cloned `Segment`
1009    // is therefore un-droppable from verified code: `Segment::drop`
1010    // requires a `SegmentOwner` with `relate_regions` (currently
1011    // pinning `raw_count == 1` per covered slot), and the clone path
1012    // produces neither the owner nor the matching `raw_count` bump.
1013    //
1014    // The planned exec fix (own branch / session):
1015    //   1. `Frame::from_raw`: generalise `raw_count <= 1` precondition
1016    //      to `raw_count >= 1` and decrement (not zero) on call.
1017    //   2. `SegmentOwner::relate_regions`: weaken `raw_count == 1`
1018    //      to `raw_count >= 1`.
1019    //   3. Remove `RCClone for Segment`; add inherent
1020    //      `Segment::clone(&self, …) -> (Self, Tracked<SegmentOwner<M>>)`
1021    //      whose body per-frame is `from_in_use + ManuallyDrop::new`
1022    //      (bumping both `rc` and `raw_count`).
1023    //   4. Rework `Segment::drop` / `Segment::next` loop invariants
1024    //      for the `raw_count >= 1` form.
1025    //
1026    // Once the exec ships those, the embedding can add an `Op` variant
1027    // that inserts a fresh `SegmentEntry` with the same range as `sid`
1028    // and bumps the per-paddr `rc`/`raw_count` to match — the
1029    // `accounting_inv` equation `rc == H + P + cover_count` chains
1030    // naturally because both `rc` and `cover_count` increment together
1031    // at each covered paddr.
1032}
1033
1034/// Per-op precondition — the conjunction of facts about the store that
1035/// must hold for an `Op` to be applied. Encodes id-existence,
1036/// distinctness, cross-store ref-integrity, and the *expressible*
1037/// portion of the exec-method preconditions (per-op `requires` from
1038/// the verus_spec annotations). MODEL GAPS (handle inv/wf,
1039/// `tlb_model.inv()` is in `VmStore::inv`, closure preconditions on
1040/// `protect_next`, `size_of::<T>()` range bounds on
1041/// `read_val`/`write_val`/`collect`) are documented in
1042/// [`super::cursor`] and [`super::io`] axiom comments.
1043///
1044/// [`step`] requires `op_pre(*old(s), op)`. Callers must establish the
1045/// precondition for the specific Op variant they're about to apply.
1046///
1047/// SOUNDNESS: when we're done building this model, `op_pre` must be
1048/// permissive enough to permit every possible call trace. That means
1049/// that these conditions should reduce to
1050/// "the relevant objects exist in the store".
1051pub open spec fn op_pre<'rcu>(s: VmStore<'rcu>, op: Op) -> bool {
1052    match op {
1053        Op::NewVmSpace => true,
1054        Op::DropVmSpace { vs } => s.vm_spaces.dom().contains(vs) && (forall|c: CursorId| #[trigger]
1055            s.cursors.dom().contains(c) ==> s.cursors[c].vm_space != vs) && (forall|v: VmIoId|
1056         #[trigger]
1057            s.vm_ios.dom().contains(v) ==> s.vm_ios[v].vm_space != Some(vs)),
1058        Op::OpenCursor { vs, va: _ } => s.vm_spaces.dom().contains(vs),
1059        Op::OpenCursorMut { vs, va: _ } => s.vm_spaces.dom().contains(vs),
1060        Op::DropCursor { c } => s.cursors.dom().contains(c),
1061        Op::Query { c } => s.cursors.dom().contains(c),
1062        Op::FindNext { c, len: _ } => s.cursors.dom().contains(c),
1063        Op::Jump { c, va: _ } => s.cursors.dom().contains(c),
1064        Op::VirtAddr { c } => s.cursors.dom().contains(c),
1065        // Op::Map consumes the FrameEntry for the mapped frame. The
1066        // consumed handle's reference at the slot is "transferred" to
1067        // the new PTE — exec map ManuallyDrops the input UFrame
1068        // (raw_count++ avoided since rc stays bumped) while the PTE
1069        // adds 1 to rc; net 0 at the mapped slot. This is exactly what
1070        // lets `accounting_inv`'s clause 4 (`rc == H + P`) chain
1071        // across map: H decrements (entry consumed), P increments (path
1072        // inserted), rc unchanged.
1073        //
1074        // (Once required `usage == Frame` at the mapped slot; that
1075        // clause now lives in `structural_inv`'s FrameId⟹Frame-usage
1076        // invariant, automatically discharged from `s.frames.contains(fid)`.)
1077        Op::Map { c, fid, prop: _ } => s.cursors.dom().contains(c) && s.frames.dom().contains(fid),
1078        Op::Unmap { c, len: _ } => s.cursors.dom().contains(c),
1079        Op::ProtectNext { c, len: _ } => s.cursors.dom().contains(c),
1080        Op::NewReader { vs, vaddr: _, len: _ } => s.vm_spaces.dom().contains(vs),
1081        Op::NewWriter { vs, vaddr: _, len: _ } => s.vm_spaces.dom().contains(vs),
1082        Op::NewKernelReader { vaddr: _, len: _ } => true,
1083        Op::NewKernelWriter { vaddr: _, len: _ } => true,
1084        Op::DropReader { vio } => s.vm_ios.dom().contains(vio),
1085        Op::DropWriter { vio } => s.vm_ios.dom().contains(vio),
1086        Op::ReaderReadVal { source } => s.vm_ios.dom().contains(source),
1087        Op::ReaderCollect { source } => s.vm_ios.dom().contains(source),
1088        Op::ReaderLimit { vio, max: _ } => s.vm_ios.dom().contains(vio),
1089        Op::ReaderSkip { vio, n: _ } => s.vm_ios.dom().contains(vio),
1090        Op::ReaderQuery { vio } => s.vm_ios.dom().contains(vio),
1091        Op::WriterWriteVal { writer } => s.vm_ios.dom().contains(writer),
1092        Op::WriterFillZeros { vio, len: _ } => s.vm_ios.dom().contains(vio),
1093        Op::WriterLimit { vio, max: _ } => s.vm_ios.dom().contains(vio),
1094        Op::WriterSkip { vio, n: _ } => s.vm_ios.dom().contains(vio),
1095        Op::WriterQuery { vio } => s.vm_ios.dom().contains(vio),
1096        // exec Infallible `read` is *typed* `VmReader<Infallible>` →
1097        // `VmWriter<Infallible>`: `source`/`dest` must be a kernel
1098        // reader/writer (operand well-formedness, not a runtime check —
1099        // see `VmIoEntry::is_kernel_reader`). `source != dest` keeps the
1100        // two tracked `&mut` borrows disjoint.
1101        Op::Read { source, dest } => s.vm_ios.dom().contains(source) && s.vm_ios.dom().contains(
1102            dest,
1103        ) && source != dest && s.vm_ios[source].is_kernel_reader()
1104            && s.vm_ios[dest].is_kernel_writer(),
1105        // exec Infallible `write`: same operand typing as `read`.
1106        Op::Write { source, dest } => s.vm_ios.dom().contains(source) && s.vm_ios.dom().contains(
1107            dest,
1108        ) && source != dest && s.vm_ios[source].is_kernel_reader()
1109            && s.vm_ios[dest].is_kernel_writer(),
1110        Op::FrameFromUnused { paddr: _ } => true,
1111        Op::FrameFromInUse { paddr: _ } => true,
1112        // `op_pre[FrameDrop]` is just id-existence + the segment-cover
1113        // constraint. All other `drop_pre` conjuncts (rc not in
1114        // sentinels, rc <= MAX, storage.is_init, in_list == 0,
1115        // rc == 1 ⟹ paths empty / handle_count == 1) plus the
1116        // handle-clause are derived inside [`step_frame_drop`] from
1117        // `s.inv()` via [`lemma_frame_drop_pre_derivable`] — the
1118        // embedding-level `Frame::wf(state)` (Item 2 in the module-
1119        // docs roadmap). The lemma chains: structural FrameId⟹Frame
1120        // + structural raw_count == segment_cover_count + accounting
1121        // clause 4 + `MetaSlotOwner::inv` SHARED branch (Item 1)
1122        // covers every residual.
1123        //
1124        // The remaining `segment_cover_count == 0` is a real per-op
1125        // obligation — it's the same shape as Item 5 segment
1126        // disjointness — so it stays in `op_pre` until segments are
1127        // activated and we can tie it to the segment store directly.
1128        Op::FrameDrop { fid } => s.frames.dom().contains(fid) && segment_cover_count(
1129            s.segments,
1130            s.frames[fid].paddr,
1131        ) == 0,
1132        // `Segment::from_unused`: aligned, in-bound, non-empty range;
1133        // every frame in `range` is currently UNUSED (so we can
1134        // allocate fresh references). The UNUSED condition is what
1135        // `accounting_inv` clause 1 ⟹ no users gives us at the slot
1136        // level; combined with `structural_inv`'s slot-perm coverage,
1137        // it discharges the `segment_from_unused_embedded` axiom's
1138        // requires verbatim.
1139        Op::SegmentFromUnused { range } => range.start % PAGE_SIZE == 0 && range.end % PAGE_SIZE
1140            == 0 && range.start < range.end && range.end <= MAX_PADDR && forall|paddr: Paddr|
1141            #![trigger frame_to_index(paddr)]
1142            (range.start <= paddr < range.end && paddr % PAGE_SIZE == 0)
1143                ==> s.regions.slot_owners[frame_to_index(paddr)].inner_perms.ref_count.value()
1144                == REF_COUNT_UNUSED,
1145        // `Segment` drop: id-existence + range well-formedness is
1146        // satisfied by every registered `SegmentEntry`; the per-slot
1147        // SHARED+Frame conditions are derived inside `step_segment_drop`
1148        // from `s.inv()` (analogue of `lemma_frame_drop_pre_derivable`
1149        // for segments).
1150        Op::SegmentDrop { sid } => s.segments.dom().contains(sid),
1151        // `Segment::split`: id-existence + offset must be page-aligned
1152        // and strictly between 0 and the segment's size (mirroring
1153        // exec `assert!`s). Range well-formedness comes from
1154        // `structural_inv`.
1155        Op::SegmentSplit { sid, offset } => s.segments.dom().contains(sid) && offset % PAGE_SIZE
1156            == 0 && 0 < offset && offset < (s.segments[sid].range.end
1157            - s.segments[sid].range.start),
1158        // `Segment::next`: id-existence. Range well-formedness from
1159        // `structural_inv` (range.start < range.end + page-aligned).
1160        Op::SegmentNext { sid } => s.segments.dom().contains(sid),
1161    }
1162}
1163
1164// =============================================================================
1165// Store helpers: extract / insert. These are the *only* functions that
1166// have preconditions about store membership; per-op steps don't.
1167// =============================================================================
1168impl<'rcu> VmStore<'rcu> {
1169    /// Removes the VmSpaceOwner at `vs` from the store and returns it.
1170    /// Requires no cursor or VmIo refers to `vs`, and no activated
1171    /// ranges remain on `vs` (otherwise `inv` would break after the
1172    /// removal).
1173    pub proof fn extract_vm_space(tracked &mut self, vs: VmSpaceId) -> (tracked res: VmSpaceOwner)
1174        requires
1175            old(self).inv(),
1176            old(self).vm_spaces.dom().contains(vs),
1177            forall|c: CursorId| #[trigger]
1178                old(self).cursors.dom().contains(c) ==> old(self).cursors[c].vm_space != vs,
1179            forall|v: VmIoId| #[trigger]
1180                old(self).vm_ios.dom().contains(v) ==> old(self).vm_ios[v].vm_space != Some(vs),
1181        ensures
1182            final(self).regions == old(self).regions,
1183            final(self).tlb_model == old(self).tlb_model,
1184            final(self).vm_spaces == old(self).vm_spaces.remove(vs),
1185            final(self).cursors == old(self).cursors,
1186            final(self).vm_ios == old(self).vm_ios,
1187            final(self).frames == old(self).frames,
1188            final(self).segments == old(self).segments,
1189            res == old(self).vm_spaces[vs],
1190            final(self).inv(),
1191    {
1192        self.vm_spaces.tracked_remove(vs)
1193    }
1194
1195    /// Inserts a VmSpaceOwner at the given fresh id. Requires the id is
1196    /// not already used and the owner satisfies its invariant.
1197    pub proof fn insert_vm_space(tracked &mut self, vs: VmSpaceId, tracked owner: VmSpaceOwner)
1198        requires
1199            old(self).inv(),
1200            !old(self).vm_spaces.dom().contains(vs),
1201            owner.inv(),
1202        ensures
1203            final(self).regions == old(self).regions,
1204            final(self).tlb_model == old(self).tlb_model,
1205            final(self).vm_spaces == old(self).vm_spaces.insert(vs, owner),
1206            final(self).cursors == old(self).cursors,
1207            final(self).vm_ios == old(self).vm_ios,
1208            final(self).frames == old(self).frames,
1209            final(self).segments == old(self).segments,
1210            final(self).inv(),
1211    {
1212        self.vm_spaces.tracked_insert(vs, owner);
1213    }
1214
1215    /// Removes the cursor entry at `c` from the store and returns it.
1216    pub proof fn extract_cursor(tracked &mut self, c: CursorId) -> (tracked res: CursorEntry<'rcu>)
1217        requires
1218            old(self).inv(),
1219            old(self).cursors.dom().contains(c),
1220        ensures
1221            final(self).regions == old(self).regions,
1222            final(self).tlb_model == old(self).tlb_model,
1223            final(self).vm_spaces == old(self).vm_spaces,
1224            final(self).cursors == old(self).cursors.remove(c),
1225            final(self).vm_ios == old(self).vm_ios,
1226            final(self).frames == old(self).frames,
1227            final(self).segments == old(self).segments,
1228            res == old(self).cursors[c],
1229            final(self).inv(),
1230    {
1231        self.cursors.tracked_remove(c)
1232    }
1233
1234    /// Inserts a cursor entry at the given fresh id. Requires the id is
1235    /// not already used, the entry satisfies its inv, the entry's
1236    /// `vm_space` is in the store, and the entry's owner is sound w.r.t.
1237    /// the store's regions.
1238    pub proof fn insert_cursor(tracked &mut self, c: CursorId, tracked entry: CursorEntry<'rcu>)
1239        requires
1240            old(self).inv(),
1241            !old(self).cursors.dom().contains(c),
1242            entry.inv(),
1243            entry.owner.metaregion_sound(old(self).regions),
1244            old(self).vm_spaces.dom().contains(entry.vm_space),
1245        ensures
1246            final(self).regions == old(self).regions,
1247            final(self).tlb_model == old(self).tlb_model,
1248            final(self).vm_spaces == old(self).vm_spaces,
1249            final(self).cursors == old(self).cursors.insert(c, entry),
1250            final(self).vm_ios == old(self).vm_ios,
1251            final(self).frames == old(self).frames,
1252            final(self).segments == old(self).segments,
1253            final(self).inv(),
1254    {
1255        self.cursors.tracked_insert(c, entry);
1256    }
1257
1258    /// Removes the VmIo entry at `vio` from the store and returns it.
1259    pub proof fn extract_vm_io(tracked &mut self, vio: VmIoId) -> (tracked res: VmIoEntry)
1260        requires
1261            old(self).inv(),
1262            old(self).vm_ios.dom().contains(vio),
1263        ensures
1264            final(self).regions == old(self).regions,
1265            final(self).tlb_model == old(self).tlb_model,
1266            final(self).vm_spaces == old(self).vm_spaces,
1267            final(self).cursors == old(self).cursors,
1268            final(self).vm_ios == old(self).vm_ios.remove(vio),
1269            final(self).frames == old(self).frames,
1270            final(self).segments == old(self).segments,
1271            res == old(self).vm_ios[vio],
1272            final(self).inv(),
1273    {
1274        self.vm_ios.tracked_remove(vio)
1275    }
1276
1277    /// Inserts a VmIo entry at the given fresh id. Requires the id is
1278    /// not already used, the entry satisfies its inv, the entry's
1279    /// `vm_space` (if `Some`) refers to a live VmSpace, the range
1280    /// bound holds when `vm_space` is `Some`, and (if the entry is
1281    /// activated) its owner range is disjoint from every existing
1282    /// activated entry's owner range (preserves the pairwise-disjoint
1283    /// invariant in [`VmStore::inv`]).
1284    pub proof fn insert_vm_io(tracked &mut self, vio: VmIoId, tracked entry: VmIoEntry)
1285        requires
1286            old(self).inv(),
1287            !old(self).vm_ios.dom().contains(vio),
1288            entry.inv(),
1289            entry.vm_space matches Some(vs) ==> old(self).vm_spaces.dom().contains(vs),
1290            entry.vm_space is Some ==> (entry.vaddr as nat) + (entry.len as nat)
1291                <= MAX_USERSPACE_VADDR as nat,
1292        ensures
1293            final(self).regions == old(self).regions,
1294            final(self).tlb_model == old(self).tlb_model,
1295            final(self).vm_spaces == old(self).vm_spaces,
1296            final(self).cursors == old(self).cursors,
1297            final(self).vm_ios == old(self).vm_ios.insert(vio, entry),
1298            final(self).frames == old(self).frames,
1299            final(self).segments == old(self).segments,
1300            final(self).inv(),
1301    {
1302        self.vm_ios.tracked_insert(vio, entry);
1303    }
1304
1305    /// Removes the FrameEntry at `fid` from the store.
1306    ///
1307    /// Requires / ensures only [`structural_inv`] — not full [`inv`].
1308    /// Removing a frame handle without coordinating with the slot's
1309    /// `ref_count` breaks [`accounting_inv`] transiently; the *step*
1310    /// that calls this is responsible for pairing it with the matching
1311    /// `frame::drop_step` (or `cursor::map_step` once Op::Map consumes
1312    /// a tracked frame) and re-establishing accounting at the end.
1313    pub proof fn extract_frame(tracked &mut self, fid: FrameId) -> (tracked res: FrameEntry)
1314        requires
1315            old(self).structural_inv(),
1316            old(self).frames.dom().contains(fid),
1317        ensures
1318            final(self).regions == old(self).regions,
1319            final(self).tlb_model == old(self).tlb_model,
1320            final(self).vm_spaces == old(self).vm_spaces,
1321            final(self).cursors == old(self).cursors,
1322            final(self).vm_ios == old(self).vm_ios,
1323            final(self).frames == old(self).frames.remove(fid),
1324            final(self).segments == old(self).segments,
1325            res == old(self).frames[fid],
1326            final(self).structural_inv(),
1327    {
1328        self.frames.tracked_remove(fid)
1329    }
1330
1331    /// Inserts a FrameEntry at the given fresh id. Requires the entry's
1332    /// paddr be `has_safe_slot` — the per-`FrameEntry` clause of
1333    /// [`VmStore::inv`] (#4). Every caller establishes this from the
1334    /// `from_*` axioms' `!has_safe_slot ==> None` (a registered handle
1335    /// is necessarily in-bound).
1336    ///
1337    /// Requires / ensures only [`structural_inv`] — see [`extract_frame`]
1338    /// for the accounting/structural split rationale.
1339    pub proof fn insert_frame(tracked &mut self, fid: FrameId, tracked entry: FrameEntry)
1340        requires
1341            old(self).structural_inv(),
1342            !old(self).frames.dom().contains(fid),
1343            has_safe_slot(entry.paddr),
1344            // The slot we're registering a handle at must be Frame-usage:
1345            // structural_inv's FrameId⟹Frame-usage clause. Every caller
1346            // discharges this from the `from_*` / query axioms which
1347            // commit to Frame-usage at the cloned slot.
1348            old(self).regions.slot_owners[frame_to_index(entry.paddr)].usage == PageUsage::Frame,
1349        ensures
1350            final(self).regions == old(self).regions,
1351            final(self).tlb_model == old(self).tlb_model,
1352            final(self).vm_spaces == old(self).vm_spaces,
1353            final(self).cursors == old(self).cursors,
1354            final(self).vm_ios == old(self).vm_ios,
1355            final(self).frames == old(self).frames.insert(fid, entry),
1356            final(self).segments == old(self).segments,
1357            final(self).structural_inv(),
1358    {
1359        self.frames.tracked_insert(fid, entry);
1360    }
1361
1362    /// Removes the SegmentEntry at `sid` from the store. **Does NOT**
1363    /// ensure `structural_inv` — extracting a segment without a paired
1364    /// `regions` decrement breaks the
1365    /// `raw_count == segment_cover_count` clause at every paddr the
1366    /// segment covered. The caller's step proof must restore it via
1367    /// [`segment::drop_step`] before observing `s.inv()` again.
1368    pub proof fn extract_segment(tracked &mut self, sid: SegmentId) -> (tracked res: SegmentEntry)
1369        requires
1370            old(self).segments.dom().contains(sid),
1371            old(self).segments.dom().finite(),
1372        ensures
1373            final(self).regions == old(self).regions,
1374            final(self).tlb_model == old(self).tlb_model,
1375            final(self).vm_spaces == old(self).vm_spaces,
1376            final(self).cursors == old(self).cursors,
1377            final(self).vm_ios == old(self).vm_ios,
1378            final(self).frames == old(self).frames,
1379            final(self).segments == old(self).segments.remove(sid),
1380            res == old(self).segments[sid],
1381    {
1382        self.segments.tracked_remove(sid)
1383    }
1384
1385    /// Inserts a SegmentEntry at a fresh id. **Does NOT** ensure
1386    /// `structural_inv` — the caller must pair this with a `regions`
1387    /// `raw_count` bump at every covered paddr (via
1388    /// [`segment::from_unused_step`]) before observing `s.inv()`.
1389    pub proof fn insert_segment(tracked &mut self, sid: SegmentId, tracked entry: SegmentEntry)
1390        requires
1391            !old(self).segments.dom().contains(sid),
1392        ensures
1393            final(self).regions == old(self).regions,
1394            final(self).tlb_model == old(self).tlb_model,
1395            final(self).vm_spaces == old(self).vm_spaces,
1396            final(self).cursors == old(self).cursors,
1397            final(self).vm_ios == old(self).vm_ios,
1398            final(self).frames == old(self).frames,
1399            final(self).segments == old(self).segments.insert(sid, entry),
1400    {
1401        self.segments.tracked_insert(sid, entry);
1402    }
1403}
1404
1405// =============================================================================
1406// One-step soundness theorem.
1407// =============================================================================
1408/// One-step soundness theorem.
1409///
1410/// `op_pre(*old(s), op)` is the per-op precondition. Each match arm
1411/// extracts the relevant entries from the store, calls the per-op step
1412/// (which has neither preconditions nor `if`-guards on store membership),
1413/// and inserts any modified or freshly-produced entries back.
1414pub proof fn step<'rcu>(tracked s: &mut VmStore<'rcu>, op: Op)
1415    requires
1416        old(s).inv(),
1417        op_pre(*old(s), op),
1418    ensures
1419        final(s).inv(),
1420{
1421    match op {
1422        Op::NewVmSpace => step_new_vm_space(s),
1423        Op::DropVmSpace { vs } => step_drop_vm_space(s, vs),
1424        Op::OpenCursor { vs, va } => step_open_cursor(s, vs, va),
1425        Op::OpenCursorMut { vs, va } => step_open_cursor_mut(s, vs, va),
1426        Op::DropCursor { c } => step_drop_cursor(s, c),
1427        Op::Query { c } => step_query(s, c),
1428        Op::FindNext { c, len } => step_find_next(s, c, len),
1429        Op::Jump { c, va } => step_jump(s, c, va),
1430        Op::VirtAddr { c: _ } => {},
1431        Op::Map { c, fid, prop } => step_map(s, c, fid, prop),
1432        Op::Unmap { c, len } => step_unmap(s, c, len),
1433        Op::ProtectNext { c, len } => step_protect_next(s, c, len),
1434        Op::NewReader { vs, vaddr, len } => step_new_vm_io(s, vs, vaddr, len, VmIoKind::Reader),
1435        Op::NewWriter { vs, vaddr, len } => step_new_vm_io(s, vs, vaddr, len, VmIoKind::Writer),
1436        Op::NewKernelReader { vaddr, len } => step_new_kernel_vm_io(
1437            s,
1438            vaddr,
1439            len,
1440            VmIoKind::Reader,
1441        ),
1442        Op::NewKernelWriter { vaddr, len } => step_new_kernel_vm_io(
1443            s,
1444            vaddr,
1445            len,
1446            VmIoKind::Writer,
1447        ),
1448        Op::DropReader { vio } => step_drop_vm_io(s, vio),
1449        Op::DropWriter { vio } => step_drop_vm_io(s, vio),
1450        // Fallible variants: handle-only, no embedding state changes.
1451        Op::ReaderReadVal { source: _ } => {},
1452        Op::ReaderCollect { source: _ } => {},
1453        Op::WriterWriteVal { writer: _ } => {},
1454        Op::ReaderLimit { vio, max } => step_vm_io_method(s, vio, io::VmIoMethod::ReaderLimit(max)),
1455        Op::ReaderSkip { vio, n } => step_vm_io_method(s, vio, io::VmIoMethod::ReaderSkip(n)),
1456        Op::ReaderQuery { vio: _ } => {},
1457        Op::WriterFillZeros { vio, len } => step_vm_io_method(
1458            s,
1459            vio,
1460            io::VmIoMethod::WriterFillZeros(len),
1461        ),
1462        Op::WriterLimit { vio, max } => step_vm_io_method(s, vio, io::VmIoMethod::WriterLimit(max)),
1463        Op::WriterSkip { vio, n } => step_vm_io_method(s, vio, io::VmIoMethod::WriterSkip(n)),
1464        Op::WriterQuery { vio: _ } => {},
1465        // Infallible `read`: produces a fresh activated-Writer val_owner.
1466        Op::Read { source, dest } => step_read(s, source, dest),
1467        // Infallible `write`: no longer surfaces consumed_w; just
1468        // mutates source/dest owners.
1469        Op::Write { source, dest } => step_write(s, source, dest),
1470        Op::FrameFromUnused { paddr } => step_frame_from_unused(s, paddr),
1471        Op::FrameFromInUse { paddr } => step_frame_from_in_use(s, paddr),
1472        Op::FrameDrop { fid } => step_frame_drop(s, fid),
1473        Op::SegmentFromUnused { range } => step_segment_from_unused(s, range),
1474        Op::SegmentDrop { sid } => step_segment_drop(s, sid),
1475        Op::SegmentSplit { sid, offset } => step_segment_split(s, sid, offset),
1476        Op::SegmentNext { sid } => step_segment_next(s, sid),
1477    }
1478}
1479
1480// --- Per-arm proof helpers (kept individually so SMT context stays small) ---
1481/// Stage 5.3: [`accounting_inv`] survives a step that only allocates
1482/// fresh page-table nodes. `VmSpace::new` / `VmSpace::cursor*` mutate
1483/// `regions` solely by spinning up PT nodes — their `_embedded` axioms
1484/// guarantee every *changed* slot went `UNUSED → non-UNUSED, non-Frame`
1485/// (the changed-slots clause) and left `frames` untouched.
1486///
1487/// Under those two facts every slot an accounting clause cares about is
1488/// provably *unchanged*: a slot carrying a handle, a Frame-usage slot,
1489/// and a non-UNUSED slot each contradict one hypothesis of the
1490/// `UNUSED → non-UNUSED, non-Frame` transition, so the old clause
1491/// carries verbatim.
1492///
1493/// Shared by [`step_new_vm_space`], [`step_open_cursor`] and
1494/// [`step_open_cursor_mut`].
1495proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_new: VmStore<'rcu>)
1496    requires
1497        s_old.inv(),
1498        s_new.frames == s_old.frames,
1499        // Segments unchanged ⟹ `segment_cover_count` unchanged.
1500        s_new.segments == s_old.segments,
1501        forall|i: usize|
1502            #![trigger s_new.regions.slot_owners[i]]
1503            s_new.regions.slot_owners[i] != s_old.regions.slot_owners[i] ==> {
1504                &&& s_old.regions.slot_owners[i].inner_perms.ref_count.value() == REF_COUNT_UNUSED
1505                &&& s_new.regions.slot_owners[i].inner_perms.ref_count.value() != REF_COUNT_UNUSED
1506                &&& s_new.regions.slot_owners[i].usage != PageUsage::Frame
1507            },
1508    ensures
1509        s_new.accounting_inv(),
1510        // PT-alloc preserves the FrameId⟹Frame-usage structural clause:
1511        // every existing registered handle's slot was non-UNUSED pre
1512        // (rc != UNUSED from clause 4 with H >= 1), so PT-alloc's
1513        // requires (only UNUSED slots may change) leaves it untouched.
1514        forall|fid: FrameId| #[trigger]
1515            s_new.frames.dom().contains(fid) ==> s_new.regions.slot_owners[frame_to_index(
1516                s_new.frames[fid].paddr,
1517            )].usage == PageUsage::Frame,
1518        // Likewise for segment-covered ⟹ Frame-usage.
1519        forall|sid: SegmentId, paddr: Paddr|
1520            #![trigger
1521                s_new.segments.dom().contains(sid),
1522                frame_to_index(paddr)]
1523            s_new.segments.dom().contains(sid) && s_new.segments[sid].range.start <= paddr
1524                < s_new.segments[sid].range.end && paddr % PAGE_SIZE == 0
1525                ==> s_new.regions.slot_owners[frame_to_index(paddr)].usage == PageUsage::Frame,
1526{
1527    // Clause 2 — UNUSED ⟹ no users. An UNUSED slot in `s_new` is
1528    // unchanged (a transitioned slot is non-UNUSED in `s_new`).
1529    assert forall|idx: usize|
1530        #![trigger s_new.regions.slot_owners[idx]]
1531        idx < max_meta_slots() && s_new.regions.slot_owners[idx].inner_perms.ref_count.value()
1532            == REF_COUNT_UNUSED implies handle_count(s_new.frames, idx) == 0
1533        && s_new.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
1534        s_new.segments,
1535        index_to_frame(idx),
1536    ) == 0 by {
1537        assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]);
1538    };
1539    // Clause 3 — Frame ∧ non-sentinel ⟹ active head. A Frame-usage slot
1540    // in `s_new` is unchanged (a transitioned slot is non-Frame).
1541    assert forall|idx: usize|
1542        #![trigger s_new.regions.slot_owners[idx]]
1543        idx < max_meta_slots() && s_new.regions.slot_owners[idx].usage == PageUsage::Frame
1544            && s_new.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
1545            && s_new.regions.slot_owners[idx].inner_perms.ref_count.value()
1546            != REF_COUNT_UNIQUE implies handle_count(s_new.frames, idx) > 0
1547        || s_new.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
1548        s_new.segments,
1549        index_to_frame(idx),
1550    ) > 0 by {
1551        assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]);
1552    };
1553    // Clause 4 — the accounting equation. Same: a Frame-usage slot in
1554    // `s_new` is unchanged, so the old equation carries.
1555    assert forall|idx: usize|
1556        #![trigger s_new.regions.slot_owners[idx]]
1557        idx < max_meta_slots() && s_new.regions.slot_owners[idx].usage == PageUsage::Frame && (
1558        handle_count(s_new.frames, idx) > 0 || s_new.regions.slot_owners[idx].paths_in_pt.len() > 0
1559            || segment_cover_count(s_new.segments, index_to_frame(idx)) > 0) implies {
1560        let so = s_new.regions.slot_owners[idx];
1561        let rc = so.inner_perms.ref_count.value();
1562        &&& rc != REF_COUNT_UNUSED
1563        &&& rc != REF_COUNT_UNIQUE
1564        &&& rc == handle_count(s_new.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
1565            s_new.segments,
1566            index_to_frame(idx),
1567        )
1568        &&& so.inner_perms.storage.is_init()
1569    } by {
1570        assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]);
1571    };
1572    // Discharge segment-covered ⟹ Frame-usage. Covered slots have
1573    // cover_count >= 1 ⟹ accounting clause 3 with usage == Frame
1574    // (from old structural) ⟹ rc != UNUSED. PT-alloc's requires
1575    // ⟹ slot unchanged ⟹ usage still Frame.
1576    assert forall|sid: SegmentId, paddr: Paddr|
1577        #![trigger
1578            s_new.segments.dom().contains(sid),
1579            frame_to_index(paddr)]
1580        s_new.segments.dom().contains(sid) && s_new.segments[sid].range.start <= paddr
1581            < s_new.segments[sid].range.end && paddr % PAGE_SIZE
1582            == 0 implies s_new.regions.slot_owners[frame_to_index(paddr)].usage
1583        == PageUsage::Frame by {
1584        let idx = frame_to_index(paddr);
1585        // From old structural: covered ⟹ Frame.
1586        assert(s_old.regions.slot_owners[idx].usage == PageUsage::Frame);
1587        // From old accounting clause 4: cover >= 1 ⟹ active head ⟹
1588        // rc ∈ valid SHARED range ⟹ rc != UNUSED.
1589        lemma_segment_cover_contains(s_old.segments, sid, paddr);
1590        assert(s_old.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED);
1591        // PT-alloc unchanged.
1592        assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]);
1593    };
1594    // Discharge the FrameId⟹Frame-usage clause. For every registered
1595    // handle, pre rc != UNUSED (from `s_old.accounting_inv` clause 4
1596    // with H >= 1 + usage == Frame from `s_old.structural_inv`), so
1597    // PT-alloc's requires (changed ⟹ pre UNUSED) leaves the slot
1598    // untouched and usage stays Frame.
1599    assert forall|fid: FrameId| #[trigger]
1600        s_new.frames.dom().contains(fid) implies s_new.regions.slot_owners[frame_to_index(
1601        s_new.frames[fid].paddr,
1602    )].usage == PageUsage::Frame by {
1603        let idx = frame_to_index(s_new.frames[fid].paddr);
1604        // pre H >= 1 since `fid` is in `s_old.frames.dom()`.
1605        assert(s_old.frames.dom().filter(
1606            |gid: FrameId| frame_to_index(s_old.frames[gid].paddr) == idx,
1607        ).contains(fid));
1608        assert(handle_count(s_old.frames, idx) >= 1);
1609        // pre accounting_inv clause 4 ⟹ pre rc != UNUSED.
1610        assert(s_old.regions.slot_owners[idx].usage == PageUsage::Frame);
1611        assert(s_old.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED);
1612        // PT-alloc's requires: changed ⟹ pre UNUSED. Contrapositive:
1613        // pre non-UNUSED ⟹ unchanged.
1614        assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]);
1615    };
1616}
1617
1618proof fn step_new_vm_space<'rcu>(tracked s: &mut VmStore<'rcu>)
1619    requires
1620        old(s).inv(),
1621    ensures
1622        final(s).inv(),
1623{
1624    let ghost s_before = *s;
1625    let tracked owner = vm_space::new_vm_space_step(&mut s.regions);
1626    let ghost id = fresh_vm_space_id(s.vm_spaces);
1627    axiom_fresh_vm_space_id_not_in_dom(s.vm_spaces);
1628    // `VmSpace::new` only allocates fresh PT nodes; accounting carries
1629    // (every changed slot went UNUSED → non-UNUSED PT node).
1630    lemma_accounting_preserved_by_pt_alloc(s_before, *s);
1631    s.insert_vm_space(id, owner);
1632}
1633
1634proof fn step_drop_vm_space<'rcu>(tracked s: &mut VmStore<'rcu>, vs: VmSpaceId)
1635    requires
1636        old(s).inv(),
1637        old(s).vm_spaces.dom().contains(vs),
1638        forall|c: CursorId| #[trigger]
1639            old(s).cursors.dom().contains(c) ==> old(s).cursors[c].vm_space != vs,
1640        forall|v: VmIoId| #[trigger]
1641            old(s).vm_ios.dom().contains(v) ==> old(s).vm_ios[v].vm_space != Some(vs),
1642    ensures
1643        final(s).inv(),
1644{
1645    let tracked owner = s.extract_vm_space(vs);
1646    vm_space::drop_vm_space_step(owner);
1647}
1648
1649proof fn step_open_cursor<'rcu>(tracked s: &mut VmStore<'rcu>, vs: VmSpaceId, va: Range<Vaddr>)
1650    requires
1651        old(s).inv(),
1652        old(s).vm_spaces.dom().contains(vs),
1653    ensures
1654        final(s).inv(),
1655{
1656    let ghost s_before = *s;
1657    let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs);
1658    let tracked res = cursor::open_cursor_step(vm_space_ref, &mut s.regions, vs, va);
1659    // `VmSpace::cursor` only allocates fresh PT nodes; accounting
1660    // carries (every changed slot went UNUSED → non-UNUSED PT node).
1661    lemma_accounting_preserved_by_pt_alloc(s_before, *s);
1662    match res {
1663        Option::Some(entry) => {
1664            let ghost id = fresh_cursor_id(s.cursors);
1665            axiom_fresh_cursor_id_not_in_dom(s.cursors);
1666            s.insert_cursor(id, entry);
1667        },
1668        Option::None => {},
1669    }
1670}
1671
1672proof fn step_open_cursor_mut<'rcu>(tracked s: &mut VmStore<'rcu>, vs: VmSpaceId, va: Range<Vaddr>)
1673    requires
1674        old(s).inv(),
1675        old(s).vm_spaces.dom().contains(vs),
1676    ensures
1677        final(s).inv(),
1678{
1679    let ghost s_before = *s;
1680    let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs);
1681    let tracked res = cursor::open_cursor_mut_step(vm_space_ref, &mut s.regions, vs, va);
1682    // `VmSpace::cursor_mut` only allocates fresh PT nodes; accounting
1683    // carries (every changed slot went UNUSED → non-UNUSED PT node).
1684    lemma_accounting_preserved_by_pt_alloc(s_before, *s);
1685    match res {
1686        Option::Some(entry) => {
1687            let ghost id = fresh_cursor_id(s.cursors);
1688            axiom_fresh_cursor_id_not_in_dom(s.cursors);
1689            s.insert_cursor(id, entry);
1690        },
1691        Option::None => {},
1692    }
1693}
1694
1695proof fn step_drop_cursor<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId)
1696    requires
1697        old(s).inv(),
1698        old(s).cursors.dom().contains(c),
1699    ensures
1700        final(s).inv(),
1701{
1702    let tracked entry = s.extract_cursor(c);
1703    cursor::drop_cursor_step(entry);
1704}
1705
1706proof fn step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId)
1707    requires
1708        old(s).inv(),
1709        old(s).cursors.dom().contains(c),
1710    ensures
1711        final(s).inv(),
1712{
1713    let ghost old_frames = s.frames;
1714    let ghost old_regions = s.regions;
1715    let tracked mut entry = s.extract_cursor(c);
1716    let ghost res = cursor::cursor_query_step(&mut entry, &mut s.regions);
1717    match res {
1718        Option::None => {
1719            // No clone happened — slot_owners fully preserved per axiom,
1720            // s.frames unchanged. accounting_inv chains directly.
1721            s.insert_cursor(c, entry);
1722        },
1723        Option::Some(paddr) => {
1724            // Exec query cloned a tracked leaf at `paddr` (rc++ at the
1725            // leaf slot). Register a fresh `FrameEntry` so `H` at that
1726            // slot grows by 1 in lockstep with `rc`, keeping
1727            // `accounting_inv`'s clause 4 (`rc == H + P`) chained.
1728            let ghost target_idx = frame_to_index(paddr);
1729            s.regions.inv_implies_correct_addr(paddr);
1730            let ghost id = fresh_frame_id(s.frames);
1731            axiom_fresh_frame_id_not_in_dom(s.frames);
1732            let ghost entry_paddr = paddr;
1733            let tracked frame_entry = axiom_frame_entry_new(paddr);
1734            s.insert_frame(id, frame_entry);
1735            // Pre target_idx: usage == Frame (axiom), so by pre clause 3
1736            // either H_pre > 0 or paths_pre > 0; clause 4 gives
1737            // pre rc != UNUSED ∧ pre rc != UNIQUE ∧
1738            // pre rc == pre H + pre paths ∧ pre storage.is_init.
1739            // The cursor axiom on Some bumps rc to pre rc + 1 (≤ MAX),
1740            // preserves usage / paths / storage at target_idx, and
1741            // preserves all other slots fully.
1742            assert(s.regions.slot_owners[target_idx].usage == PageUsage::Frame);
1743            // Discharge accounting_inv on (new regions, new frames).
1744            assert forall|idx: usize|
1745                #![trigger s.regions.slot_owners[idx]]
1746                idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
1747                    == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
1748                && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
1749                s.segments,
1750                index_to_frame(idx),
1751            ) == 0 by {
1752                lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx);
1753                if idx == target_idx {
1754                    // post rc = pre rc + 1; pre rc != UNUSED (clause 4),
1755                    // so post rc > 1 ≠ UNUSED. Contradiction.
1756                    assert(false);
1757                } else {
1758                    // Other slot: fully preserved (cursor axiom), so
1759                    // pre UNUSED ⟹ pre H=0 ∧ pre paths empty ∧ cover==0;
1760                    // H unchanged at idx != target_idx (lemma); segments
1761                    // unchanged.
1762                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
1763                }
1764            };
1765            assert forall|idx: usize|
1766                #![trigger s.regions.slot_owners[idx]]
1767                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
1768                    && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
1769                    && s.regions.slot_owners[idx].inner_perms.ref_count.value()
1770                    != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
1771                || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
1772                s.segments,
1773                index_to_frame(idx),
1774            ) > 0 by {
1775                lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx);
1776                if idx == target_idx {
1777                    // The freshly inserted handle gives H > 0 at target.
1778                    assert(handle_count(s.frames, target_idx) >= 1);
1779                } else {
1780                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
1781                }
1782            };
1783            assert forall|idx: usize|
1784                #![trigger s.regions.slot_owners[idx]]
1785                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
1786                handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
1787                    || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
1788                let so = s.regions.slot_owners[idx];
1789                let rc = so.inner_perms.ref_count.value();
1790                &&& rc != REF_COUNT_UNUSED
1791                &&& rc != REF_COUNT_UNIQUE
1792                &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
1793                    s.segments,
1794                    index_to_frame(idx),
1795                )
1796                &&& so.inner_perms.storage.is_init()
1797            } by {
1798                lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx);
1799                if idx == target_idx {
1800                    if old_regions.slot_owners[target_idx].inner_perms.ref_count.value()
1801                        == REF_COUNT_UNUSED {
1802                        // Pre UNUSED at Frame slot: clause 1 ⟹ pre paths
1803                        // empty ∧ pre H == 0 ∧ pre cover == 0.
1804                        // Post H == 1, paths preserved, cover preserved.
1805                        // Post rc = pre rc + 1 = UNUSED + 1.
1806                        assert(REF_COUNT_UNUSED == 0u32);
1807                        assert(s.regions.slot_owners[target_idx].inner_perms.ref_count.value()
1808                            == 1);
1809                        assert(handle_count(s.frames, target_idx) == 1);
1810                        assert(s.regions.slot_owners[target_idx].paths_in_pt.len()
1811                            == old_regions.slot_owners[target_idx].paths_in_pt.len());
1812                        assert(old_regions.slot_owners[target_idx].paths_in_pt.len() == 0);
1813                        assert(segment_cover_count(s.segments, index_to_frame(target_idx)) == 0);
1814                    } else if old_regions.slot_owners[target_idx].inner_perms.ref_count.value()
1815                        == REF_COUNT_UNIQUE {
1816                        assert(false);
1817                    } else {
1818                        // Pre non-sentinel SHARED rc: pre clause 4 applies
1819                        // with the new cover term.
1820                        let pre_so = old_regions.slot_owners[target_idx];
1821                        let pre_rc = pre_so.inner_perms.ref_count.value();
1822                        let pre_paths = pre_so.paths_in_pt.len();
1823                        let pre_H = handle_count(old_frames, target_idx);
1824                        let pre_cover = segment_cover_count(s.segments, index_to_frame(target_idx));
1825                        if pre_H == 0 && pre_paths == 0 && pre_cover == 0 {
1826                            assert(false);
1827                        } else {
1828                            // pre rc == pre_H + pre_paths + pre_cover.
1829                            // post rc = pre rc + 1, post H = pre_H + 1,
1830                            // post paths = pre_paths, post cover = pre_cover.
1831                            assert(pre_rc == pre_H + pre_paths + pre_cover);
1832                            assert(handle_count(s.frames, target_idx) == pre_H + 1);
1833                        }
1834                    }
1835                } else {
1836                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
1837                }
1838            };
1839            s.insert_cursor(c, entry);
1840        },
1841    }
1842}
1843
1844proof fn step_find_next<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize)
1845    requires
1846        old(s).inv(),
1847        old(s).cursors.dom().contains(c),
1848    ensures
1849        final(s).inv(),
1850{
1851    let tracked mut entry = s.extract_cursor(c);
1852    cursor::cursor_find_next_step(&mut entry, &mut s.regions, len);
1853    s.insert_cursor(c, entry);
1854}
1855
1856proof fn step_jump<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, va: Vaddr)
1857    requires
1858        old(s).inv(),
1859        old(s).cursors.dom().contains(c),
1860    ensures
1861        final(s).inv(),
1862{
1863    let tracked mut entry = s.extract_cursor(c);
1864    cursor::cursor_jump_step(&mut entry, &mut s.regions, va);
1865    s.insert_cursor(c, entry);
1866}
1867
1868proof fn step_protect_next<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize)
1869    requires
1870        old(s).inv(),
1871        old(s).cursors.dom().contains(c),
1872    ensures
1873        final(s).inv(),
1874{
1875    let tracked mut entry = s.extract_cursor(c);
1876    cursor::cursor_protect_next_step(&mut entry, &mut s.regions, len);
1877    s.insert_cursor(c, entry);
1878}
1879
1880proof fn step_map<'rcu>(
1881    tracked s: &mut VmStore<'rcu>,
1882    c: CursorId,
1883    fid: FrameId,
1884    prop: PageProperty,
1885)
1886    requires
1887        old(s).inv(),
1888        old(s).cursors.dom().contains(c),
1889        old(s).frames.dom().contains(fid),
1890    ensures
1891        final(s).inv(),
1892{
1893    // `usage == Frame` at the mapped slot from `structural_inv`'s
1894    // FrameId⟹Frame-usage clause.
1895    assert(s.regions.slot_owners[frame_to_index(s.frames[fid].paddr)].usage == PageUsage::Frame);
1896    let ghost paddr = s.frames[fid].paddr;
1897    let ghost target_idx = frame_to_index(paddr);
1898    let ghost old_frames = s.frames;
1899    let ghost old_regions = s.regions;
1900    // From `structural_inv`: every registered handle's paddr is in-bound.
1901    assert(has_safe_slot(paddr));
1902    s.regions.inv_implies_correct_addr(paddr);
1903    // Pre target_idx: we hold a FrameEntry at this paddr, so
1904    // `handle_count(old_frames, target_idx) >= 1`.
1905    assert(old_frames.dom().filter(
1906        |gid: FrameId| frame_to_index(old_frames[gid].paddr) == target_idx,
1907    ).contains(fid));
1908    assert(handle_count(old_frames, target_idx) >= 1);
1909    // Pre target_idx is usage == Frame (op_pre) and active head
1910    // (H >= 1), so pre `accounting_inv` clauses 3 and 4 apply.
1911    let ghost pre_rc_target = old_regions.slot_owners[target_idx].inner_perms.ref_count.value();
1912    let ghost pre_paths_target = old_regions.slot_owners[target_idx].paths_in_pt.len();
1913    let ghost pre_cover_target = segment_cover_count(s.segments, index_to_frame(target_idx));
1914    assert(pre_rc_target != REF_COUNT_UNUSED);
1915    assert(pre_rc_target != REF_COUNT_UNIQUE);
1916    assert(pre_rc_target == handle_count(old_frames, target_idx) + pre_paths_target
1917        + pre_cover_target);
1918    assert(old_regions.slot_owners[target_idx].inner_perms.storage.is_init());
1919    let tracked mut entry = s.extract_cursor(c);
1920    // Consume the FrameEntry: the UFrame's handle ref-count
1921    // contribution moves to the new PTE; the embedding's `H` at
1922    // target_idx decrements by 1 in lockstep with `P` incrementing by 1.
1923    let tracked _frame_entry = s.extract_frame(fid);
1924    cursor::map_step(&mut entry, &mut s.regions, &mut s.tlb_model, paddr, prop);
1925    // Discharge `accounting_inv` clause-by-clause. The cursor-map axiom
1926    // gives: rc/usage/storage preserved at target_idx, paths += 1 at
1927    // target_idx; non-mapped pre-non-UNUSED slots fully preserved;
1928    // post-UNUSED slots fully preserved; newly-non-UNUSED slots are
1929    // non-Frame (PT nodes). `s.segments` is unchanged across map.
1930    assert forall|idx: usize|
1931        #![trigger s.regions.slot_owners[idx]]
1932        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
1933            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
1934        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
1935        s.segments,
1936        index_to_frame(idx),
1937    ) == 0 by {
1938        // post-UNUSED ⟹ slot fully preserved (cursor axiom).
1939        assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
1940        lemma_handle_count_remove(old_frames, fid, idx);
1941        if idx == target_idx {
1942            // post-UNUSED at target_idx contradicts rc preserved at
1943            // target_idx + pre_rc_target != UNUSED.
1944            assert(s.regions.slot_owners[idx].inner_perms.ref_count.value() == pre_rc_target);
1945            assert(false);
1946        }
1947    };
1948    assert forall|idx: usize|
1949        #![trigger s.regions.slot_owners[idx]]
1950        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
1951            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
1952            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
1953            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
1954        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
1955        s.segments,
1956        index_to_frame(idx),
1957    ) > 0 by {
1958        lemma_handle_count_remove(old_frames, fid, idx);
1959        if idx == target_idx {
1960            // post rc preserved at target_idx, paths += 1 ⟹ paths.len > 0.
1961            assert(s.regions.slot_owners[idx].paths_in_pt.len() == pre_paths_target + 1);
1962        } else if old_regions.slot_owners[idx].inner_perms.ref_count.value() == REF_COUNT_UNUSED {
1963            // Newly-non-UNUSED slot ⟹ usage != Frame (changed-slots clause).
1964            assert(s.regions.slot_owners[idx].usage != PageUsage::Frame);
1965        } else {
1966            // Non-mapped pre-non-UNUSED slot ⟹ fully preserved; pre
1967            // clause 3 carries forward.
1968            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
1969        }
1970    };
1971    assert forall|idx: usize|
1972        #![trigger s.regions.slot_owners[idx]]
1973        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
1974        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
1975            || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
1976        let so = s.regions.slot_owners[idx];
1977        let rc = so.inner_perms.ref_count.value();
1978        &&& rc != REF_COUNT_UNUSED
1979        &&& rc != REF_COUNT_UNIQUE
1980        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
1981            s.segments,
1982            index_to_frame(idx),
1983        )
1984        &&& so.inner_perms.storage.is_init()
1985    } by {
1986        lemma_handle_count_remove(old_frames, fid, idx);
1987        if idx == target_idx {
1988            // Pre clause 4 (new): rc == H_pre + P_pre + cover_pre.
1989            // Post: rc/usage/storage preserved; H_post = H_pre - 1;
1990            //       P_post = P_pre + 1; cover_post = cover_pre.
1991            // So rc_post = pre_rc_target = H_pre + P_pre + cover_pre
1992            //                            = H_post + P_post + cover_post.
1993            assert(s.regions.slot_owners[idx].inner_perms.ref_count.value() == pre_rc_target);
1994            assert(s.regions.slot_owners[idx].paths_in_pt.len() == pre_paths_target + 1);
1995            assert(handle_count(s.frames, idx) == (handle_count(old_frames, idx) - 1) as nat);
1996        } else if old_regions.slot_owners[idx].inner_perms.ref_count.value() == REF_COUNT_UNUSED {
1997            assert(s.regions.slot_owners[idx].usage != PageUsage::Frame);
1998        } else {
1999            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2000        }
2001    };
2002    // Discharge structural_inv's FrameId⟹Frame-usage clause.
2003    // For every remaining fid: pre slot was Frame (old structural_inv);
2004    // cursor preserves usage at target_idx and at non-mapped pre-non-
2005    // UNUSED slots (Frame slots are non-UNUSED by old clause 4 with H or
2006    // P > 0).
2007    assert forall|fid_other: FrameId| #[trigger]
2008        s.frames.dom().contains(fid_other) implies s.regions.slot_owners[frame_to_index(
2009        s.frames[fid_other].paddr,
2010    )].usage == PageUsage::Frame by {
2011        let other_idx = frame_to_index(s.frames[fid_other].paddr);
2012        // pre: usage == Frame from old structural_inv.
2013        assert(old_regions.slot_owners[other_idx].usage == PageUsage::Frame);
2014        if other_idx == target_idx {
2015            // Cursor preserves usage at target_idx.
2016            assert(s.regions.slot_owners[target_idx].usage
2017                == old_regions.slot_owners[target_idx].usage);
2018        } else {
2019            // pre rc != UNUSED at Frame slots with active head (H or P
2020            // > 0). Need to invoke H_pre >= 1 (fid_other counts) or
2021            // pre_paths > 0; here fid_other is still in s.frames
2022            // (which == old_frames.remove(fid)), so unless fid_other ==
2023            // fid, fid_other is also in old_frames. Hence pre H >= 1
2024            // at other_idx, so pre clause 4 ⟹ pre rc != UNUSED.
2025            assert(old_frames.dom().filter(
2026                |gid: FrameId| frame_to_index(old_frames[gid].paddr) == other_idx,
2027            ).contains(fid_other));
2028            assert(handle_count(old_frames, other_idx) >= 1);
2029            assert(old_regions.slot_owners[other_idx].inner_perms.ref_count.value()
2030                != REF_COUNT_UNUSED);
2031            assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]);
2032        }
2033    };
2034    // Discharge segment-covered ⟹ Frame-usage. Same shape: covered
2035    // slots are non-UNUSED pre (cover >= 1 + clause 4 ⟹ active);
2036    // cursor preserves Frame slots fully (target_idx via map axiom,
2037    // others via the "non-mapped pre-non-UNUSED" clause).
2038    assert forall|sid: SegmentId, paddr_c: Paddr|
2039        #![trigger
2040            s.segments.dom().contains(sid),
2041            frame_to_index(paddr_c)]
2042        s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c
2043            < s.segments[sid].range.end && paddr_c % PAGE_SIZE
2044            == 0 implies s.regions.slot_owners[frame_to_index(paddr_c)].usage
2045        == PageUsage::Frame by {
2046        let cov_idx = frame_to_index(paddr_c);
2047        // pre cover >= 1 at cov_idx ⟹ pre slot is Frame + non-UNUSED.
2048        lemma_segment_cover_contains(old_regions_segments_helper(s), sid, paddr_c);
2049        assert(old_regions.slot_owners[cov_idx].usage == PageUsage::Frame);
2050        assert(old_regions.slot_owners[cov_idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED);
2051        if cov_idx == target_idx {
2052            // Map preserves usage at target.
2053            assert(s.regions.slot_owners[target_idx].usage
2054                == old_regions.slot_owners[target_idx].usage);
2055        } else {
2056            // Non-mapped pre-non-UNUSED slot ⟹ fully preserved.
2057            assert(s.regions.slot_owners[cov_idx] == old_regions.slot_owners[cov_idx]);
2058        }
2059    };
2060    s.insert_cursor(c, entry);
2061}
2062
2063// Helper: snapshot the pre-step segments map. Defined as a no-op
2064// inline spec to give the discharge proofs a stable handle on the
2065// pre-state when `s.segments` is unchanged.
2066spec fn old_regions_segments_helper<'rcu>(s: &VmStore<'rcu>) -> Map<SegmentId, SegmentEntry> {
2067    s.segments
2068}
2069
2070proof fn step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize)
2071    requires
2072        old(s).inv(),
2073        old(s).cursors.dom().contains(c),
2074    ensures
2075        final(s).inv(),
2076{
2077    let ghost old_regions = s.regions;
2078    let ghost old_frames = s.frames;
2079    let tracked mut entry = s.extract_cursor(c);
2080    cursor::cursor_mut_regions_step(
2081        &mut entry,
2082        &mut s.regions,
2083        &mut s.tlb_model,
2084        cursor::CursorMutRegionsMethod::Unmap(len),
2085    );
2086    // Discharge `accounting_inv` clause-by-clause. The unmap axiom
2087    // gives: usage/raw_count/in_list/self_addr/vtable_ptr preserved
2088    // universally; rc doesn't bump to UNIQUE; storage preserved at
2089    // post-non-UNUSED; at Frame slots, `rc - paths.len` is invariant
2090    // with both monotonically non-increasing. `s.frames` is unchanged.
2091    assert forall|idx: usize|
2092        #![trigger s.regions.slot_owners[idx]]
2093        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2094            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
2095        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
2096        s.segments,
2097        index_to_frame(idx),
2098    ) == 0 by {
2099        // From `regions.inv()`: idx < max_meta_slots ⟹ slot_owners[idx]
2100        // satisfies MetaSlotOwner::inv (so UNUSED ∧ non-MMIO ⟹ paths
2101        // empty fires).
2102        assert(s.regions.slot_owners.contains_key(idx));
2103        assert(s.regions.slot_owners[idx].inv());
2104        // Post cover == 0. Unmap leaves `s.segments` untouched, so post
2105        // cover == pre cover. If pre cover >= 1: a witnessing segment +
2106        // structural `covered ⟹ Frame` gives pre usage == Frame, and pre
2107        // `accounting_inv` clause #4 (active head) gives pre rc <=
2108        // REF_COUNT_MAX; the unmap rc-paths clause then gives post rc <=
2109        // pre rc <= MAX < UNUSED — contradicting post UNUSED. Hence pre
2110        // cover == 0 (segment covers survive unmap, which removes only
2111        // PTE paths).
2112        assert(s.segments == old(s).segments);
2113        assert(old_regions == old(s).regions);
2114        assert(segment_cover_count(s.segments, index_to_frame(idx)) == 0) by {
2115            if segment_cover_count(old(s).segments, index_to_frame(idx)) > 0 {
2116                let pa = index_to_frame(idx);
2117                let sid = lemma_segment_cover_witness(old(s).segments, pa);
2118                // Paddr round-trip / alignment so the structural
2119                // `covered ⟹ Frame` clause (keyed by `frame_to_index`)
2120                // fires at `(sid, pa)`.
2121                assert(pa == (idx * PAGE_SIZE) as usize);
2122                assert(pa % PAGE_SIZE == 0);
2123                assert(frame_to_index(pa) == idx);
2124                // structural `covered ⟹ Frame` at the witness (old state).
2125                assert(old_regions.slot_owners[idx].usage == PageUsage::Frame);
2126                // active head (cover > 0 ∧ Frame) ⟹ pre rc != UNUSED, <= MAX.
2127                assert(old_regions.slot_owners[idx].inner_perms.ref_count.value()
2128                    != REF_COUNT_UNUSED);
2129                assert(old_regions.slot_owners[idx].inner_perms.ref_count.value() <= REF_COUNT_MAX);
2130                // unmap (Frame): post rc <= pre rc <= MAX < UNUSED.
2131                assert(s.regions.slot_owners[idx].inner_perms.ref_count.value() <= REF_COUNT_MAX);
2132            }
2133        };
2134        // Case-split on pre.usage: usage is preserved by the axiom.
2135        if old_regions.slot_owners[idx].usage == PageUsage::Frame {
2136            // Post.paths empty: Frame ∧ post UNUSED + MetaSlotOwner::inv
2137            // (UNUSED ∧ non-MMIO ⟹ paths empty).
2138            assert(s.regions.slot_owners[idx].usage != PageUsage::MMIO);
2139            assert(s.regions.slot_owners[idx].paths_in_pt =~= Set::empty());
2140            // Post.H == 0: at Frame post UNUSED, pre rc == pre paths
2141            // (from rc-paths invariant: post rc + pre paths = pre rc +
2142            // post paths ⟹ 0 + pre paths = pre rc + 0). If pre rc !=
2143            // UNUSED: pre active head (rc > 0), pre clause 4 ⟹ pre rc
2144            // == pre H + pre paths ⟹ pre H == 0. If pre rc == UNUSED:
2145            // pre clause 1 ⟹ pre H == 0.
2146        } else if old_regions.slot_owners[idx].usage == PageUsage::MMIO {
2147            // MMIO slots are fully preserved (axiom). Pre clause 1
2148            // gives the conjunction for pre UNUSED MMIO directly.
2149            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2150        } else {
2151            // Non-Frame non-MMIO (PT-node): MetaSlotOwner::inv UNUSED
2152            // gives paths empty. H == 0 from no-FrameId-at-non-Frame.
2153            assert(s.regions.slot_owners[idx].usage != PageUsage::MMIO);
2154            assert(s.regions.slot_owners[idx].paths_in_pt =~= Set::empty());
2155            assert(handle_count(s.frames, idx) == 0) by {
2156                let filt = s.frames.dom().filter(
2157                    |gid: FrameId| frame_to_index(s.frames[gid].paddr) == idx,
2158                );
2159                assert forall|fid: FrameId| #[trigger] filt.contains(fid) implies false by {
2160                    // s.frames == old_frames (unmap doesn't touch frames).
2161                    // structural ⟹ pre slot's usage == Frame, but we're
2162                    // in the non-Frame branch — contradiction.
2163                    assert(s.frames.dom().contains(fid));
2164                    assert(frame_to_index(s.frames[fid].paddr) == idx);
2165                    assert(s.regions.slot_owners[idx].usage == PageUsage::Frame);
2166                };
2167                assert(filt =~= Set::empty());
2168            };
2169        }
2170    };
2171    assert forall|idx: usize|
2172        #![trigger s.regions.slot_owners[idx]]
2173        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
2174            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
2175            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2176            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
2177        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
2178        s.segments,
2179        index_to_frame(idx),
2180    ) > 0 by {
2181        // Post usage == Frame ⟹ pre usage == Frame (usage preserved).
2182        // At Frame slots, the rc-paths invariant `post rc + pre paths
2183        // == pre rc + post paths` combined with `post paths.len ≤ pre
2184        // paths.len` forces pre UNUSED ⟹ post UNUSED (since pre UNUSED
2185        // gives pre paths == 0 via MetaSlotOwner::inv, the equation
2186        // becomes post rc == pre rc + post paths but post rc ≤ pre rc
2187        // ⟹ post paths == 0 ⟹ post rc == pre rc == UNUSED). So at
2188        // post non-UNUSED Frame slot, pre rc != UNUSED.
2189        assert(s.regions.slot_owners.contains_key(idx));
2190        assert(s.regions.slot_owners[idx].inv());
2191        assert(old_regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED) by {
2192            if old_regions.slot_owners[idx].inner_perms.ref_count.value() == REF_COUNT_UNUSED {
2193                // Trigger MetaSlotOwner::inv on pre at this idx.
2194                assert(old_regions.slot_owners.contains_key(idx));
2195                assert(old_regions.slot_owners[idx].inv());
2196                assert(old_regions.slot_owners[idx].paths_in_pt =~= Set::empty());
2197                // rc-paths invariant: post rc + 0 == UNUSED + post paths
2198                //                  ⟹ post rc == UNUSED + post paths.
2199                // post rc <= pre rc == UNUSED ⟹ post paths == 0
2200                //                            ⟹ post rc == UNUSED.
2201                // But post rc != UNUSED by assumption. Contradiction.
2202                assert(s.regions.slot_owners[idx].paths_in_pt.len() == 0);
2203                assert(false);
2204            }
2205        };
2206        // Pre non-UNUSED Frame: clause 3 gives pre H > 0 OR pre paths > 0
2207        // OR pre cover > 0. Segments unchanged ⟹ post cover == pre cover.
2208        if handle_count(old_frames, idx) > 0 {
2209            assert(handle_count(s.frames, idx) > 0);
2210        } else if segment_cover_count(s.segments, index_to_frame(idx)) > 0 {
2211            // Cover > 0 directly satisfies the new disjunct.
2212        } else {
2213            // pre H == 0 ∧ pre cover == 0. Clause 3 ⟹ pre paths > 0.
2214            // Clause 4 (active head pre) ⟹ pre rc == pre H + pre paths
2215            // + pre cover == pre paths. From rc-paths invariant:
2216            // post paths == pre paths - pre rc + post rc == post rc.
2217            // post rc != UNUSED ⟹ post rc > 0 ⟹ post paths > 0.
2218            assert(s.regions.slot_owners[idx].paths_in_pt.len() > 0);
2219        }
2220    };
2221    assert forall|idx: usize|
2222        #![trigger s.regions.slot_owners[idx]]
2223        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
2224        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len()
2225            > 0) implies {
2226        let so = s.regions.slot_owners[idx];
2227        let rc = so.inner_perms.ref_count.value();
2228        &&& rc != REF_COUNT_UNUSED
2229        &&& rc != REF_COUNT_UNIQUE
2230        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2231            s.segments,
2232            index_to_frame(idx),
2233        )
2234        &&& so.inner_perms.storage.is_init()
2235    } by {
2236        // Post is active head. H unchanged ⟹ pre H == post H. Pre
2237        // usage == Frame (preserved). Either pre H > 0 (pre active head)
2238        // or post paths > 0 with post H == 0 ⟹ pre paths >= post paths
2239        // > 0 (pre active head). Either way, pre clause 4 applies:
2240        // pre rc != UNUSED ∧ pre rc != UNIQUE ∧
2241        // pre rc == pre H + pre paths ∧ pre storage init.
2242        if handle_count(s.frames, idx) > 0 {
2243            // pre H > 0 ⟹ pre active head ⟹ pre clause 4.
2244            assert(handle_count(old_frames, idx) > 0);
2245        } else {
2246            // post H == 0, post paths > 0. Frame-slot axiom: post rc +
2247            // pre paths == pre rc + post paths. post rc != UNUSED
2248            // (otherwise contradicts active head), so post rc > 0.
2249            // pre paths == pre rc + post paths - post rc. Combined with
2250            // pre paths >= post paths (monotonic): pre rc >= post rc > 0.
2251            // So pre rc != UNUSED ⟹ pre clause 3 ⟹ pre H > 0 OR pre
2252            // paths > 0. pre H == 0 (H unchanged), so pre paths > 0 ⟹
2253            // pre active head ⟹ pre clause 4.
2254            assert(old_regions.slot_owners[idx].paths_in_pt.len() > 0);
2255        }
2256        // Now pre clause 4 gives: pre rc == pre H + pre paths,
2257        //                         pre rc != UNUSED, pre rc != UNIQUE,
2258        //                         pre storage.is_init.
2259        // Frame-slot axiom: post rc + pre paths == pre rc + post paths
2260        //                 ⟹ post rc == pre rc + post paths - pre paths
2261        //                 ⟹ post rc == (pre H + pre paths) + post paths - pre paths
2262        //                 ⟹ post rc == pre H + post paths
2263        //                 ⟹ post rc == post H + post paths.  ✓
2264        // post rc != UNUSED: from active head assumption.
2265        // post rc != UNIQUE: axiom's pre != UNIQUE ⟹ post != UNIQUE.
2266        // storage init: axiom's "post non-UNUSED ⟹ storage preserved".
2267    };
2268    // Discharge structural_inv's FrameId⟹Frame-usage clause. Unmap
2269    // preserves usage universally, so it holds trivially.
2270    assert forall|fid_other: FrameId| #[trigger]
2271        s.frames.dom().contains(fid_other) implies s.regions.slot_owners[frame_to_index(
2272        s.frames[fid_other].paddr,
2273    )].usage == PageUsage::Frame by {
2274        let other_idx = frame_to_index(s.frames[fid_other].paddr);
2275        assert(s.regions.slot_owners[other_idx].usage == old_regions.slot_owners[other_idx].usage);
2276    };
2277    s.insert_cursor(c, entry);
2278}
2279
2280proof fn step_new_vm_io<'rcu>(
2281    tracked s: &mut VmStore<'rcu>,
2282    vs: VmSpaceId,
2283    vaddr: Vaddr,
2284    len: usize,
2285    kind: VmIoKind,
2286)
2287    requires
2288        old(s).inv(),
2289        old(s).vm_spaces.dom().contains(vs),
2290    ensures
2291        final(s).inv(),
2292{
2293    let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs);
2294    let tracked res = io::new_vm_io_step(vm_space_ref, Some(vs), vaddr, len, kind);
2295    match res {
2296        Option::Some(entry) => {
2297            let ghost id = fresh_vm_io_id(s.vm_ios);
2298            axiom_fresh_vm_io_id_not_in_dom(s.vm_ios);
2299            s.insert_vm_io(id, entry);
2300        },
2301        Option::None => {},
2302    }
2303}
2304
2305proof fn step_new_kernel_vm_io<'rcu>(
2306    tracked s: &mut VmStore<'rcu>,
2307    vaddr: Vaddr,
2308    len: usize,
2309    kind: VmIoKind,
2310)
2311    requires
2312        old(s).inv(),
2313    ensures
2314        final(s).inv(),
2315{
2316    let tracked entry = io::new_kernel_vm_io_step(vaddr, len, kind);
2317    let ghost id = fresh_vm_io_id(s.vm_ios);
2318    axiom_fresh_vm_io_id_not_in_dom(s.vm_ios);
2319    s.insert_vm_io(id, entry);
2320}
2321
2322proof fn step_drop_vm_io<'rcu>(tracked s: &mut VmStore<'rcu>, vio: VmIoId)
2323    requires
2324        old(s).inv(),
2325        old(s).vm_ios.dom().contains(vio),
2326    ensures
2327        final(s).inv(),
2328{
2329    let tracked entry = s.extract_vm_io(vio);
2330    io::drop_vm_io_step(entry);
2331}
2332
2333proof fn step_vm_io_method<'rcu>(tracked s: &mut VmStore<'rcu>, vio: VmIoId, method: io::VmIoMethod)
2334    requires
2335        old(s).inv(),
2336        old(s).vm_ios.dom().contains(vio),
2337    ensures
2338        final(s).inv(),
2339{
2340    let tracked mut entry = s.extract_vm_io(vio);
2341    io::vm_io_method_step(&mut entry, method);
2342    s.insert_vm_io(vio, entry);
2343}
2344
2345proof fn step_read<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, dest: VmIoId)
2346    requires
2347        old(s).inv(),
2348        old(s).vm_ios.dom().contains(source),
2349        old(s).vm_ios.dom().contains(dest),
2350        source != dest,
2351        old(s).vm_ios[source].vm_space is None,
2352        old(s).vm_ios[source].kind == VmIoKind::Reader,
2353        old(s).vm_ios[dest].vm_space is None,
2354        old(s).vm_ios[dest].kind == VmIoKind::Writer,
2355    ensures
2356        final(s).inv(),
2357{
2358    let tracked mut src = s.extract_vm_io(source);
2359    let tracked mut dst = s.extract_vm_io(dest);
2360    let tracked val = io::read_step(&mut src, &mut dst);
2361    s.insert_vm_io(source, src);
2362    s.insert_vm_io(dest, dst);
2363    let ghost id = fresh_vm_io_id(s.vm_ios);
2364    axiom_fresh_vm_io_id_not_in_dom(s.vm_ios);
2365    s.insert_vm_io(id, val);
2366}
2367
2368proof fn step_write<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, dest: VmIoId)
2369    requires
2370        old(s).inv(),
2371        old(s).vm_ios.dom().contains(source),
2372        old(s).vm_ios.dom().contains(dest),
2373        source != dest,
2374        old(s).vm_ios[source].vm_space is None,
2375        old(s).vm_ios[source].kind == VmIoKind::Reader,
2376        old(s).vm_ios[dest].vm_space is None,
2377        old(s).vm_ios[dest].kind == VmIoKind::Writer,
2378    ensures
2379        final(s).inv(),
2380{
2381    let tracked mut src = s.extract_vm_io(source);
2382    let tracked mut dst = s.extract_vm_io(dest);
2383    io::write_step(&mut src, &mut dst);
2384    s.insert_vm_io(source, src);
2385    s.insert_vm_io(dest, dst);
2386}
2387
2388proof fn step_frame_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, paddr: Paddr)
2389    requires
2390        old(s).inv(),
2391    ensures
2392        final(s).inv(),
2393{
2394    // `op_pre` is now `true` for this op (#2 fully resolved): *any*
2395    // `paddr` is accepted, a bad one just fails. The axiom's only
2396    // slot-perm precondition is `has_safe_slot`-guarded; we discharge
2397    // it for the in-bound case from `VmStore::inv`'s coverage clause
2398    // (`inv_implies_correct_addr` → `idx < max_meta_slots()` → coverage
2399    // → `slots.contains_key`), and it is vacuous out-of-bound.
2400    if has_safe_slot(paddr) {
2401        s.regions.inv_implies_correct_addr(paddr);
2402        assert(s.regions.slots.contains_key(frame_to_index(paddr)));
2403    }
2404    let ghost old_frames = s.frames;
2405    let ghost old_regions = s.regions;
2406    let tracked res = frame::from_unused_step(&mut s.regions, paddr);
2407    match res {
2408        Option::Some(entry) => {
2409            let ghost id = fresh_frame_id(s.frames);
2410            axiom_fresh_frame_id_not_in_dom(s.frames);
2411            let ghost target_idx = frame_to_index(paddr);
2412            let ghost entry_paddr = entry.paddr;
2413            s.insert_frame(id, entry);
2414            assert(s.frames[id].paddr == paddr);
2415
2416            // Pre target_idx was rc=UNUSED ⟹ pre H==0 ∧ pre paths.empty()
2417            // (via old accounting_inv's UNUSED clause).
2418            assert(handle_count(old_frames, target_idx) == 0);
2419            assert(old_regions.slot_owners[target_idx].paths_in_pt.is_empty());
2420
2421            // 5.5c new clause: "UNUSED ⟹ no users". Other idx unchanged
2422            // (lemma + slot_owner preservation); target_idx is now rc=1.
2423            assert forall|idx: usize|
2424                #![trigger s.regions.slot_owners[idx]]
2425                idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2426                    == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
2427                && s.regions.slot_owners[idx].paths_in_pt.is_empty() by {
2428                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2429                if idx == target_idx {
2430                    // post rc=1 != UNUSED, antecedent false.
2431                    assert(false);
2432                } else {
2433                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2434                }
2435            };
2436
2437            // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". Other
2438            // idx unchanged (so old clause carries); target post is
2439            // active (H=1).
2440            assert forall|idx: usize|
2441                #![trigger s.regions.slot_owners[idx]]
2442                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
2443                    && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
2444                    && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2445                    != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
2446                || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
2447                s.segments,
2448                index_to_frame(idx),
2449            ) > 0 by {
2450                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2451                if idx == target_idx {
2452                    assert(handle_count(s.frames, idx) == 1);
2453                } else {
2454                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2455                }
2456            };
2457
2458            // Per-slot accounting (forall covers active heads only).
2459            assert forall|idx: usize|
2460                #![trigger s.regions.slot_owners[idx]]
2461                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
2462                handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
2463                    || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
2464                let so = s.regions.slot_owners[idx];
2465                let rc = so.inner_perms.ref_count.value();
2466                &&& rc != REF_COUNT_UNUSED
2467                &&& rc != REF_COUNT_UNIQUE
2468                &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2469                    s.segments,
2470                    index_to_frame(idx),
2471                )
2472                &&& so.inner_perms.storage.is_init()
2473            } by {
2474                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2475                if idx == target_idx {
2476                    assert(old_regions.slot_owners[idx].inner_perms.ref_count.value()
2477                        == REF_COUNT_UNUSED);
2478                    assert(handle_count(old_frames, idx) == 0);
2479                    assert(handle_count(s.frames, idx) == 1);
2480                    // Pre clause 2 (UNUSED) gives pre cover == 0;
2481                    // segments unchanged ⟹ post cover == 0.
2482                    assert(segment_cover_count(s.segments, index_to_frame(idx)) == 0);
2483                } else {
2484                    // Other slot: slot_owner preserved by from_unused
2485                    // (forall i != target_idx clause in reparked_spec).
2486                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2487                }
2488            };
2489        },
2490        Option::None => {
2491            // regions unchanged ⇒ accounting preserved from old.
2492            assert(s.regions =~= old_regions);
2493        },
2494    }
2495}
2496
2497proof fn step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr: Paddr)
2498    requires
2499        old(s).inv(),
2500    ensures
2501        final(s).inv(),
2502{
2503    // See `step_frame_from_unused`: `op_pre` is `true` (#3b resolved);
2504    // the `has_safe_slot`-guarded slot-perm precondition is recovered
2505    // from `VmStore::inv`'s coverage clause for the in-bound case and
2506    // is vacuous out-of-bound.
2507    if has_safe_slot(paddr) {
2508        s.regions.inv_implies_correct_addr(paddr);
2509        assert(s.regions.slots.contains_key(frame_to_index(paddr)));
2510    }
2511    let ghost old_frames = s.frames;
2512    let ghost old_regions = s.regions;
2513    let tracked res = frame::from_in_use_step(&mut s.regions, paddr);
2514    match res {
2515        Option::Some(entry) => {
2516            let ghost id = fresh_frame_id(s.frames);
2517            axiom_fresh_frame_id_not_in_dom(s.frames);
2518            let ghost target_idx = frame_to_index(paddr);
2519            s.insert_frame(id, entry);
2520            assert(s.frames[id].paddr == paddr);
2521
2522            // 5.5c new clause: "UNUSED ⟹ no users". For target: post
2523            // rc = pre rc + 1 != UNUSED. For other idx: unchanged.
2524            assert forall|idx: usize|
2525                #![trigger s.regions.slot_owners[idx]]
2526                idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2527                    == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
2528                && s.regions.slot_owners[idx].paths_in_pt.is_empty() by {
2529                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2530                if idx == target_idx {
2531                    assert(false);
2532                } else {
2533                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2534                }
2535            };
2536
2537            // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". For
2538            // target post: H = pre + 1 ≥ 1 → active. For other: unchanged.
2539            assert forall|idx: usize|
2540                #![trigger s.regions.slot_owners[idx]]
2541                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
2542                    && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
2543                    && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2544                    != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
2545                || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
2546                s.segments,
2547                index_to_frame(idx),
2548            ) > 0 by {
2549                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2550                if idx == target_idx {
2551                    assert(handle_count(s.frames, idx) >= 1);
2552                } else {
2553                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2554                }
2555            };
2556
2557            // Per-slot accounting (forall covers active heads only).
2558            assert forall|idx: usize|
2559                #![trigger s.regions.slot_owners[idx]]
2560                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
2561                handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
2562                    || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
2563                let so = s.regions.slot_owners[idx];
2564                let rc = so.inner_perms.ref_count.value();
2565                &&& rc != REF_COUNT_UNUSED
2566                &&& rc != REF_COUNT_UNIQUE
2567                &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2568                    s.segments,
2569                    index_to_frame(idx),
2570                )
2571                &&& so.inner_perms.storage.is_init()
2572            } by {
2573                lemma_handle_count_insert_fresh(old_frames, id, entry, idx);
2574                if idx == target_idx {
2575                    // Pre usage(target)==Frame: `get_from_in_use`
2576                    // preserves `usage`. Pre active-head fires from
2577                    // pre H >= 1 (or pre paths > 0, or pre cover > 0).
2578                    assert(old_regions.slot_owners[idx].usage == PageUsage::Frame);
2579                } else {
2580                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2581                }
2582            };
2583        },
2584        Option::None => {
2585            assert(s.regions =~= old_regions);
2586        },
2587    }
2588}
2589
2590proof fn step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId)
2591    requires
2592        old(s).inv(),
2593        old(s).frames.dom().contains(fid),
2594        // No segment forgot a reference to this slot. The other
2595        // `drop_pre` conjuncts (rc, storage, in_list, paths-empty
2596        // residuals) are derived from `old(s).inv()` via
2597        // [`lemma_frame_drop_pre_derivable`].
2598        segment_cover_count(old(s).segments, old(s).frames[fid].paddr) == 0,
2599    ensures
2600        final(s).inv(),
2601{
2602    // Derive `drop_pre` + handle-clause from `s.inv()` (Item 2:
2603    // embedding-level `Frame::wf(state)`).
2604    lemma_frame_drop_pre_derivable(*s, fid);
2605    let ghost p = s.frames[fid].paddr;
2606    assert(has_safe_slot(p));
2607    s.regions.inv_implies_correct_addr(p);
2608    let ghost idx_p = frame_to_index(p);
2609    assert(idx_p < max_meta_slots());
2610    // `fid ∈ s.frames` ⟹ `handle_count(s.frames, idx_p) ≥ 1`. Used
2611    // below to chain `lemma_handle_count_remove` and re-establish
2612    // accounting_inv's Frame-scoped clauses.
2613    assert(s.frames.dom().filter(
2614        |gid: FrameId| frame_to_index(s.frames[gid].paddr) == idx_p,
2615    ).contains(fid));
2616    assert(handle_count(s.frames, idx_p) >= 1);
2617    let ghost target_idx = frame_to_index(p);
2618    let ghost old_frames = s.frames;
2619    let ghost old_regions = s.regions;
2620    let tracked entry = s.extract_frame(fid);
2621    frame::drop_step(&mut s.regions, entry);
2622
2623    // Discharge accounting_inv on the post-drop state. Handle clause
2624    // is gone; only clauses 2 (UNUSED), 3 (Frame active head), 4
2625    // (Frame equation) remain.
2626
2627    // 5.5c new clause: "UNUSED ⟹ no users". For non-target: unchanged.
2628    // For target: if drop teardown (rc 1→UNUSED), need post H==0 and
2629    // paths empty. Both hold: pre eqn 1==H+P with H>=1 ⟹ H==1, P==0
2630    // ⟹ post H==0 (fid removed) and post paths == pre paths == empty.
2631    assert forall|idx: usize|
2632        #![trigger s.regions.slot_owners[idx]]
2633        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2634            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
2635        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
2636        s.segments,
2637        index_to_frame(idx),
2638    ) == 0 by {
2639        lemma_handle_count_remove(old_frames, fid, idx);
2640        if idx == target_idx {
2641            // Post rc==UNUSED ⟹ pre rc was 1 (drop_step rc transition).
2642            assert(old_regions.slot_owners[idx].inner_perms.ref_count.value() == 1);
2643            // Old handle clause: pre rc (== 1) >= pre handle_count, and
2644            // `fid` contributes ⟹ pre handle_count == 1 ⟹ post == 0.
2645            assert(handle_count(old_frames, idx) == 1);
2646            assert(handle_count(s.frames, idx) == 0);
2647            // pre rc == 1 ⟹ `drop_step` leaves `paths_in_pt` empty.
2648            assert(s.regions.slot_owners[idx].paths_in_pt.is_empty());
2649        } else {
2650            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2651        }
2652    };
2653
2654    // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". For target
2655    // post in rc>1 case: rc-1 in [1,MAX-1] non-sentinel; H-=1 or P
2656    // preserved. Pre H+P=pre rc; if post H>=1, active; else pre H=1
2657    // so pre P=pre rc-1 >= 1 (rc>1), post P >= 1, active. ✓
2658    assert forall|idx: usize|
2659        #![trigger s.regions.slot_owners[idx]]
2660        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
2661            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
2662            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2663            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
2664        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
2665        s.segments,
2666        index_to_frame(idx),
2667    ) > 0 by {
2668        lemma_handle_count_remove(old_frames, fid, idx);
2669        if idx == target_idx {
2670            // Post rc != UNUSED ⟹ drop_step did rc-1 (not teardown).
2671            // ⟹ pre rc > 1. Pre H==1+ + pre P; if pre H > 1: post H>=1
2672            // ✓. If pre H == 1: pre P = pre rc - 1 >= 1; post P preserved
2673            // >= 1 ✓.
2674            assert(handle_count(old_frames, idx) >= 1);
2675        } else {
2676            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2677        }
2678    };
2679
2680    assert forall|idx: usize|
2681        #![trigger s.regions.slot_owners[idx]]
2682        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
2683        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
2684            || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
2685        let so = s.regions.slot_owners[idx];
2686        let rc = so.inner_perms.ref_count.value();
2687        &&& rc != REF_COUNT_UNUSED
2688        &&& rc != REF_COUNT_UNIQUE
2689        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2690            s.segments,
2691            index_to_frame(idx),
2692        )
2693        &&& so.inner_perms.storage.is_init()
2694    } by {
2695        lemma_handle_count_remove(old_frames, fid, idx);
2696        if idx == target_idx {
2697            // Pre fid contributes ⇒ pre H >= 1 ⇒ pre active head.
2698            // Pre `usage == Frame`: `drop_step` preserves `usage`,
2699            // and the clause antecedent gives post `usage == Frame`.
2700            assert(old_regions.slot_owners[idx].usage == PageUsage::Frame);
2701            assert(handle_count(old_frames, idx) > 0);
2702            let ghost pre_rc = old_regions.slot_owners[idx].inner_perms.ref_count.value();
2703            let ghost pre_h = handle_count(old_frames, idx);
2704            let ghost pre_p = old_regions.slot_owners[idx].paths_in_pt.len();
2705            assert(pre_rc == pre_h + pre_p);
2706            // Residual `drop_pre`: pre rc <= MAX, pre rc >= 1, != UNUSED/UNIQUE.
2707            let ghost post_h = handle_count(s.frames, idx);
2708            assert(post_h == (pre_h - 1) as nat);
2709            // drop_step now exposes paths preservation at idx.
2710            let ghost post_p = s.regions.slot_owners[idx].paths_in_pt.len();
2711            assert(post_p == pre_p);
2712            let ghost post_rc = s.regions.slot_owners[idx].inner_perms.ref_count.value();
2713            if pre_rc > 1 {
2714                // drop_step rc>1 branch: post rc = pre - 1, storage preserved.
2715                assert(post_rc == (pre_rc - 1) as u64);
2716                assert(post_rc as nat == post_h + post_p);
2717                assert(post_rc >= 1);
2718                assert(post_rc <= REF_COUNT_MAX);
2719                assert(s.regions.slot_owners[idx].inner_perms.storage
2720                    == old_regions.slot_owners[idx].inner_perms.storage);
2721            } else {
2722                // pre_rc == 1: pre eqn 1 == pre_h + pre_p with
2723                // pre_h >= 1 forces pre_h = 1, pre_p = 0.
2724                assert(pre_h == 1);
2725                assert(pre_p == 0);
2726                assert(post_h == 0);
2727                assert(post_p == 0);
2728                // drop_step rc==1 branch: post rc = UNUSED.
2729                assert(post_rc == REF_COUNT_UNUSED);
2730                // ⇒ post is NOT active head at idx, so we're not
2731                // actually inside this body in this case
2732                // (antecedent false). Contradicts the implies guard.
2733                assert(false);
2734            }
2735        } else {
2736            // Other slot: slot_owner preserved by drop_step
2737            // (forall i != target_idx clause in ensures).
2738            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2739        }
2740    };
2741}
2742
2743/// `Op::SegmentFromUnused` step. Allocates a fresh `SegmentEntry`
2744/// covering `range` on success. Discharges `accounting_inv` from the
2745/// post-state's per-slot ensures (every covered slot transitions
2746/// `UNUSED → Frame, rc=1, raw_count=1`).
2747proof fn step_segment_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, range: Range<Paddr>)
2748    requires
2749        old(s).inv(),
2750        range.start % PAGE_SIZE == 0,
2751        range.end % PAGE_SIZE == 0,
2752        range.start < range.end,
2753        range.end <= MAX_PADDR,
2754        forall|paddr: Paddr|
2755            #![trigger frame_to_index(paddr)]
2756            (range.start <= paddr < range.end && paddr % PAGE_SIZE == 0) ==> old(
2757                s,
2758            ).regions.slot_owners[frame_to_index(paddr)].inner_perms.ref_count.value()
2759                == REF_COUNT_UNUSED,
2760    ensures
2761        final(s).inv(),
2762{
2763    let ghost old_regions = s.regions;
2764    let ghost old_frames = s.frames;
2765    let ghost old_segments = s.segments;
2766    // Slot-perm coverage in `range` follows from structural_inv (every
2767    // `idx < max_meta_slots()` has `slots.contains_key(idx)`).
2768    assert forall|paddr: Paddr|
2769        #![trigger frame_to_index(paddr)]
2770        (range.start <= paddr < range.end && paddr % PAGE_SIZE
2771            == 0) implies s.regions.slots.contains_key(frame_to_index(paddr)) by {
2772        s.regions.inv_implies_correct_addr(paddr);
2773    };
2774    let tracked res = segment::from_unused_step(&mut s.regions, range);
2775    match res {
2776        Option::Some(entry) => {
2777            let ghost id = fresh_segment_id(s.segments);
2778            axiom_fresh_segment_id_not_in_dom(s.segments);
2779            s.insert_segment(id, entry);
2780            // Discharge accounting_inv on the post-state.
2781            // Per-slot reasoning:
2782            //   - Slot in `range`: pre rc=UNUSED ⟹ pre H=0, pre cover=0
2783            //     (pre clause 1). Post rc=1, post H=0 (frames unchanged),
2784            //     post cover=1 (one new segment covers this paddr).
2785            //     Equation: post rc == post H + post paths + post cover
2786            //                == 0 + 0 + 1 == 1. ✓
2787            //   - Slot outside `range`: fully preserved (axiom); segments
2788            //     gained one entry whose range doesn't cover this paddr,
2789            //     so cover unchanged. Accounting carries from pre.
2790            assert forall|idx: usize|
2791                #![trigger s.regions.slot_owners[idx]]
2792                idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2793                    == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
2794                && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
2795                s.segments,
2796                index_to_frame(idx),
2797            ) == 0 by {
2798                let paddr = index_to_frame(idx);
2799                // Round-trip: idx < max ⟹ paddr aligned, in-bound.
2800                assert(paddr == (idx * PAGE_SIZE) as usize);
2801                assert(paddr % PAGE_SIZE == 0);
2802                assert(frame_to_index(paddr) == idx);
2803                if range.start <= paddr < range.end {
2804                    // Slot in range: post rc=1 ≠ UNUSED — contradiction.
2805                    assert(false);
2806                } else {
2807                    // Slot outside range: fully preserved by axiom.
2808                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2809                    // segments gained one entry; cover at this paddr is
2810                    // the same as before (entry's range doesn't cover paddr).
2811                    assert(!(entry.range.start <= paddr < entry.range.end));
2812                    lemma_segment_cover_insert_outside(old_segments, id, entry, paddr);
2813                }
2814            };
2815            assert forall|idx: usize|
2816                #![trigger s.regions.slot_owners[idx]]
2817                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
2818                    && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
2819                    && s.regions.slot_owners[idx].inner_perms.ref_count.value()
2820                    != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
2821                || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
2822                s.segments,
2823                index_to_frame(idx),
2824            ) > 0 by {
2825                let paddr = index_to_frame(idx);
2826                assert(paddr == (idx * PAGE_SIZE) as usize);
2827                assert(paddr % PAGE_SIZE == 0);
2828                assert(frame_to_index(paddr) == idx);
2829                if range.start <= paddr < range.end {
2830                    assert(entry.range == range);
2831                    lemma_segment_cover_insert_inside(old_segments, id, entry, paddr);
2832                } else {
2833                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2834                    assert(!(entry.range.start <= paddr < entry.range.end));
2835                    lemma_segment_cover_insert_outside(old_segments, id, entry, paddr);
2836                }
2837            };
2838            assert forall|idx: usize|
2839                #![trigger s.regions.slot_owners[idx]]
2840                idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
2841                handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
2842                    || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
2843                let so = s.regions.slot_owners[idx];
2844                let rc = so.inner_perms.ref_count.value();
2845                &&& rc != REF_COUNT_UNUSED
2846                &&& rc != REF_COUNT_UNIQUE
2847                &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2848                    s.segments,
2849                    index_to_frame(idx),
2850                )
2851                &&& so.inner_perms.storage.is_init()
2852            } by {
2853                let paddr = index_to_frame(idx);
2854                assert(paddr == (idx * PAGE_SIZE) as usize);
2855                assert(paddr % PAGE_SIZE == 0);
2856                assert(frame_to_index(paddr) == idx);
2857                if range.start <= paddr < range.end {
2858                    assert(entry.range == range);
2859                    lemma_segment_cover_insert_inside(old_segments, id, entry, paddr);
2860                    // H == 0 at idx because pre UNUSED ⟹ pre H == 0
2861                    // (pre clause 1) and frames unchanged.
2862                    assert(handle_count(s.frames, idx) == 0);
2863                } else {
2864                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2865                    assert(!(entry.range.start <= paddr < entry.range.end));
2866                    lemma_segment_cover_insert_outside(old_segments, id, entry, paddr);
2867                }
2868            };
2869            // Discharge structural_inv's `in_list == 0` clause.
2870            assert forall|idx: usize|
2871                idx
2872                    < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].inner_perms.in_list.value()
2873                == 0 by {
2874                let paddr = index_to_frame(idx);
2875                assert(paddr == (idx * PAGE_SIZE) as usize);
2876                assert(paddr % PAGE_SIZE == 0);
2877                assert(frame_to_index(paddr) == idx);
2878                if range.start <= paddr < range.end {
2879                    // Axiom: in_list == 0 post for in-range slots.
2880                } else {
2881                    // Outside: fully preserved.
2882                    assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
2883                }
2884            };
2885            // structural FrameId⟹Frame-usage: every existing fid's
2886            // slot's usage preserved. Frame-usage slots are non-UNUSED
2887            // pre (clause 4), so they're outside `range` (which is all
2888            // UNUSED pre). Axiom fully preserves outside-range slots.
2889            assert forall|fid_other: FrameId| #[trigger]
2890                s.frames.dom().contains(fid_other) implies s.regions.slot_owners[frame_to_index(
2891                s.frames[fid_other].paddr,
2892            )].usage == PageUsage::Frame by {
2893                let other_idx = frame_to_index(s.frames[fid_other].paddr);
2894                let other_paddr = index_to_frame(other_idx);
2895                // pre fid_other's slot usage == Frame from old structural.
2896                assert(old_regions.slot_owners[other_idx].usage == PageUsage::Frame);
2897                // pre rc != UNUSED at fid_other's slot (clause 4 + H>=1).
2898                assert(old_frames.dom().filter(
2899                    |gid: FrameId| frame_to_index(old_frames[gid].paddr) == other_idx,
2900                ).contains(fid_other));
2901                assert(handle_count(old_frames, other_idx) >= 1);
2902                assert(old_regions.slot_owners[other_idx].inner_perms.ref_count.value()
2903                    != REF_COUNT_UNUSED);
2904                // pre rc != UNUSED ⟹ paddr not in `range` (range slots are
2905                // all UNUSED).
2906                // ⟹ axiom preserves the slot fully.
2907                assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]);
2908            };
2909        },
2910        Option::None => {
2911            assert(s.regions =~= old_regions);
2912            assert(s.segments =~= old_segments);
2913        },
2914    }
2915}
2916
2917/// `Op::SegmentDrop` step. Removes the `SegmentEntry` at `sid` and
2918/// releases the segment's forgotten reference at each covered frame.
2919/// Frames whose `rc` reaches 1 transition to UNUSED.
2920proof fn step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId)
2921    requires
2922        old(s).inv(),
2923        old(s).segments.dom().contains(sid),
2924    ensures
2925        final(s).inv(),
2926{
2927    let ghost old_regions = s.regions;
2928    let ghost old_frames = s.frames;
2929    let ghost old_segments = s.segments;
2930    let ghost range = s.segments[sid].range;
2931    // Discharge segment::drop_step's preconditions from `s.inv()`
2932    // BEFORE extracting (we need old_regions and old_segments).
2933    assert forall|paddr: Paddr|
2934        #![trigger frame_to_index(paddr)]
2935        (range.start <= paddr < range.end && paddr % PAGE_SIZE == 0) implies {
2936        let so = old_regions.slot_owners[frame_to_index(paddr)];
2937        &&& so.inner_perms.ref_count.value() >= 1
2938        &&& so.inner_perms.ref_count.value() <= crate::specs::mm::frame::meta_owners::REF_COUNT_MAX
2939        &&& so.usage == PageUsage::Frame
2940        &&& so.inner_perms.ref_count.value() == 1 ==> so.paths_in_pt.is_empty()
2941    } by {
2942        let idx = frame_to_index(paddr);
2943        // Cover >= 1 at paddr (this segment covers it).
2944        lemma_segment_cover_contains(old_segments, sid, paddr);
2945        // Usage == Frame from structural segment-covered ⟹ Frame clause.
2946        assert(old_regions.slot_owners[idx].usage == PageUsage::Frame);
2947        // Accounting clause 4: active head (cover > 0) ⟹ rc != UNUSED,
2948        // rc != UNIQUE, rc == H + P + cover, storage init.
2949        let so = old_regions.slot_owners[idx];
2950        let rc = so.inner_perms.ref_count.value();
2951        assert(rc != REF_COUNT_UNUSED);
2952        assert(rc != REF_COUNT_UNIQUE);
2953        assert(rc == handle_count(old_frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2954            old_segments,
2955            paddr,
2956        ));
2957        // rc >= 1 since cover >= 1 ⟹ H + P + cover >= 1.
2958        assert(rc >= 1);
2959        // Triggers MetaSlotOwner::inv's SHARED branch (Item 1): rc in
2960        // [1, MAX] ⟹ storage init, in_list == 0.
2961        assert(old_regions.slot_owners.contains_key(idx));
2962        assert(old_regions.slot_owners[idx].inv());
2963        assert(rc <= crate::specs::mm::frame::meta_owners::REF_COUNT_MAX);
2964        // rc == 1 case: rc = H + P + cover = 1, cover >= 1 ⟹ cover == 1
2965        // and H == 0 and P == 0 ⟹ paths empty.
2966        if rc == 1 {
2967            assert(handle_count(old_frames, idx) + so.paths_in_pt.len() + segment_cover_count(
2968                old_segments,
2969                paddr,
2970            ) == 1);
2971            assert(so.paths_in_pt.len() == 0);
2972            assert(so.paths_in_pt =~= Set::empty());
2973        }
2974    };
2975    let tracked entry = s.extract_segment(sid);
2976    segment::drop_step(&mut s.regions, entry);
2977
2978    // Re-establish structural_inv + accounting_inv on the post-state.
2979    // Per-slot reasoning:
2980    //   - Slot in `range`: post raw_count = pre - 1; segments lost
2981    //     `sid` whose range covered this paddr, so post cover = pre - 1.
2982    //     ⟹ post raw_count == post cover. ✓
2983    //     For accounting: pre eq was `rc == H + P + cover`. Post rc:
2984    //       if pre rc > 1: post rc = pre rc - 1.
2985    //       if pre rc == 1: post rc = UNUSED (teardown).
2986    //     Post H = pre H, post P = pre P, post cover = pre cover - 1.
2987    //     If pre rc > 1: post rc = pre rc - 1 = H + P + (cover - 1) = post eq ✓.
2988    //     If pre rc == 1: pre H == 0 ∧ pre P == 0 ∧ pre cover == 1
2989    //       (from rc == 1). Post H = 0, post P = 0, post cover = 0,
2990    //       post rc = UNUSED. Clause 1 (UNUSED) fires; equation vacuous.
2991    //   - Slot outside `range`: fully preserved (axiom + segment removal
2992    //     leaves cover unchanged at outside paddrs).
2993
2994    assert forall|idx: usize|
2995        idx
2996            < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].inner_perms.in_list.value()
2997        == 0 by {
2998        let paddr = index_to_frame(idx);
2999        assert(paddr == (idx * PAGE_SIZE) as usize);
3000        assert(paddr % PAGE_SIZE == 0);
3001        assert(frame_to_index(paddr) == idx);
3002        if range.start <= paddr < range.end {
3003            // Axiom preserves in_list at in-range slots.
3004        } else {
3005            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3006        }
3007    };
3008    // Discharge accounting_inv clauses.
3009    assert forall|idx: usize|
3010        #![trigger s.regions.slot_owners[idx]]
3011        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3012            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
3013        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
3014        s.segments,
3015        index_to_frame(idx),
3016    ) == 0 by {
3017        let paddr = index_to_frame(idx);
3018        assert(paddr == (idx * PAGE_SIZE) as usize);
3019        assert(paddr % PAGE_SIZE == 0);
3020        assert(frame_to_index(paddr) == idx);
3021        if range.start <= paddr < range.end {
3022            // Post UNUSED at in-range ⟹ pre rc == 1 (axiom transition).
3023            // Pre eq: 1 == H + P + cover, cover >= 1 ⟹ cover == 1,
3024            // H == 0, P == 0. Frames unchanged ⟹ post H == 0.
3025            // Paths preserved ⟹ post P == 0 ⟹ post paths empty.
3026            // Segments removed sid (whose range covered paddr) ⟹
3027            // post cover == 0.
3028            lemma_segment_cover_contains(old_segments, sid, paddr);
3029            lemma_segment_cover_remove_inside(old_segments, sid, paddr);
3030            assert(old_regions.slot_owners[idx].inner_perms.ref_count.value() == 1);
3031            assert(handle_count(old_frames, idx) == 0);
3032            assert(s.regions.slot_owners[idx].paths_in_pt =~= Set::empty());
3033        } else {
3034            // Outside: fully preserved; segments removal doesn't affect cover.
3035            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3036            assert(!(entry.range.start <= paddr < entry.range.end));
3037            lemma_segment_cover_remove_outside(old_segments, sid, paddr);
3038        }
3039    };
3040    assert forall|idx: usize|
3041        #![trigger s.regions.slot_owners[idx]]
3042        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
3043            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
3044            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3045            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
3046        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
3047        s.segments,
3048        index_to_frame(idx),
3049    ) > 0 by {
3050        let paddr = index_to_frame(idx);
3051        assert(paddr == (idx * PAGE_SIZE) as usize);
3052        assert(paddr % PAGE_SIZE == 0);
3053        assert(frame_to_index(paddr) == idx);
3054        if range.start <= paddr < range.end {
3055            // Post non-UNUSED at in-range ⟹ pre rc > 1 (axiom).
3056            // Pre eq: pre rc == H + P + cover. Pre rc > 1 ⟹ at least
3057            // one of H, P, (cover-1) > 0. Post H == pre H, post P ==
3058            // pre P, post cover == pre cover - 1.
3059            lemma_segment_cover_contains(old_segments, sid, paddr);
3060            lemma_segment_cover_remove_inside(old_segments, sid, paddr);
3061        } else {
3062            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3063            assert(!(entry.range.start <= paddr < entry.range.end));
3064            lemma_segment_cover_remove_outside(old_segments, sid, paddr);
3065        }
3066    };
3067    assert forall|idx: usize|
3068        #![trigger s.regions.slot_owners[idx]]
3069        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
3070        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
3071            || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
3072        let so = s.regions.slot_owners[idx];
3073        let rc = so.inner_perms.ref_count.value();
3074        &&& rc != REF_COUNT_UNUSED
3075        &&& rc != REF_COUNT_UNIQUE
3076        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
3077            s.segments,
3078            index_to_frame(idx),
3079        )
3080        &&& so.inner_perms.storage.is_init()
3081    } by {
3082        let paddr = index_to_frame(idx);
3083        assert(paddr == (idx * PAGE_SIZE) as usize);
3084        assert(paddr % PAGE_SIZE == 0);
3085        assert(frame_to_index(paddr) == idx);
3086        if range.start <= paddr < range.end {
3087            lemma_segment_cover_contains(old_segments, sid, paddr);
3088            lemma_segment_cover_remove_inside(old_segments, sid, paddr);
3089            // Pre eq: pre rc == pre H + pre P + pre cover.
3090            let pre_rc = old_regions.slot_owners[idx].inner_perms.ref_count.value();
3091            let pre_H = handle_count(old_frames, idx);
3092            let pre_P = old_regions.slot_owners[idx].paths_in_pt.len();
3093            let pre_cover = segment_cover_count(old_segments, paddr);
3094            assert(pre_rc == pre_H + pre_P + pre_cover);
3095            assert(pre_rc != REF_COUNT_UNIQUE);
3096            let post_rc = s.regions.slot_owners[idx].inner_perms.ref_count.value();
3097            assert(post_rc != REF_COUNT_UNUSED);
3098            assert(pre_rc > 1) by {
3099                if pre_rc == 1 {
3100                    assert(post_rc == REF_COUNT_UNUSED);
3101                }
3102            };
3103            assert(post_rc == (pre_rc - 1) as u64);
3104            assert(s.regions.slot_owners[idx].paths_in_pt
3105                == old_regions.slot_owners[idx].paths_in_pt);
3106            assert(handle_count(s.frames, idx) == pre_H);
3107            assert(segment_cover_count(s.segments, paddr) == (pre_cover - 1) as nat);
3108            // post rc <= MAX (pre rc was, post = pre - 1, still in range).
3109            assert(post_rc <= crate::specs::mm::frame::meta_owners::REF_COUNT_MAX);
3110            // storage.is_init at post: post rc ∈ SHARED (1 <= post rc <= MAX)
3111            // ⟹ MetaSlotOwner::inv SHARED branch ⟹ storage.is_init.
3112            assert(s.regions.slot_owners.contains_key(idx));
3113            assert(s.regions.slot_owners[idx].inv());
3114            assert(post_rc >= 1);
3115            assert(s.regions.slot_owners[idx].inner_perms.storage.is_init());
3116        } else {
3117            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3118            assert(!(entry.range.start <= paddr < entry.range.end));
3119            lemma_segment_cover_remove_outside(old_segments, sid, paddr);
3120        }
3121    };
3122    // Structural FrameId⟹Frame-usage: frames unchanged; for fid_other's
3123    // slot, usage preserved (covered slots remain Frame; in-range slots
3124    // either stay non-UNUSED (rc-1) or become UNUSED — UNUSED ones had
3125    // H == 0, so no fid points there).
3126    assert forall|fid_other: FrameId| #[trigger]
3127        s.frames.dom().contains(fid_other) implies s.regions.slot_owners[frame_to_index(
3128        s.frames[fid_other].paddr,
3129    )].usage == PageUsage::Frame by {
3130        let other_idx = frame_to_index(s.frames[fid_other].paddr);
3131        let other_paddr = index_to_frame(other_idx);
3132        // Pre fid_other's slot: usage == Frame (old structural).
3133        assert(old_regions.slot_owners[other_idx].usage == PageUsage::Frame);
3134        // Pre H >= 1 at other_idx (fid_other contributes).
3135        assert(old_frames.dom().filter(
3136            |gid: FrameId| frame_to_index(old_frames[gid].paddr) == other_idx,
3137        ).contains(fid_other));
3138        assert(handle_count(old_frames, other_idx) >= 1);
3139        // Pre clause 4: pre rc == H + P + cover ≥ 1 ⟹ rc != UNUSED.
3140        assert(old_regions.slot_owners[other_idx].inner_perms.ref_count.value() >= 1);
3141        // Axiom preserves usage (universal).
3142        if range.start <= other_paddr < range.end {
3143            // In-range: usage preserved by axiom.
3144        } else {
3145            // Outside: fully preserved.
3146            assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]);
3147        }
3148    };
3149    // Structural segment-covered ⟹ Frame-usage: for any remaining
3150    // segment sid_other ≠ sid, usage at every covered paddr is
3151    // preserved (usage universally preserved by axiom).
3152    assert forall|sid_other: SegmentId, paddr_c: Paddr|
3153        #![trigger
3154            s.segments.dom().contains(sid_other),
3155            frame_to_index(paddr_c)]
3156        s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c
3157            < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE
3158            == 0 implies s.regions.slot_owners[frame_to_index(paddr_c)].usage
3159        == PageUsage::Frame by {
3160        let cov_idx = frame_to_index(paddr_c);
3161        // sid_other != sid (since sid was removed from s.segments).
3162        assert(sid_other != sid);
3163        // sid_other was in old_segments too.
3164        assert(old_segments.dom().contains(sid_other));
3165        assert(old_segments[sid_other] == s.segments[sid_other]);
3166        // Pre covered ⟹ pre Frame from old structural.
3167        assert(old_regions.slot_owners[cov_idx].usage == PageUsage::Frame);
3168        // Axiom preserves usage universally.
3169    };
3170}
3171
3172/// `Op::SegmentSplit` step. Replaces `sid` with two fresh segment
3173/// entries covering the disjoint halves; `regions` is unchanged.
3174/// `accounting_inv` chains because per-paddr `cover_count` is
3175/// invariant under the partition (see [`lemma_segment_cover_split`]).
3176proof fn step_segment_split<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId, offset: usize)
3177    requires
3178        old(s).inv(),
3179        old(s).segments.dom().contains(sid),
3180        offset % PAGE_SIZE == 0,
3181        0 < offset,
3182        offset < (old(s).segments[sid].range.end - old(s).segments[sid].range.start),
3183    ensures
3184        final(s).inv(),
3185{
3186    let ghost old_regions = s.regions;
3187    let ghost old_frames = s.frames;
3188    let ghost old_segments = s.segments;
3189    let ghost range = s.segments[sid].range;
3190    let ghost mid = (range.start + offset) as Paddr;
3191    let ghost entry_left = SegmentEntry { range: range.start..mid };
3192    let ghost entry_right = SegmentEntry { range: mid..range.end };
3193    // Pick fresh ids BEFORE the extract so they are guaranteed
3194    // distinct from `sid` (which is still in `s.segments`). Choose
3195    // `id_left` first, then `id_right` from the
3196    // `s.segments.insert(id_left, _)`-extended domain so they are
3197    // distinct from each other and from `sid`.
3198    let ghost id_left = fresh_segment_id(s.segments);
3199    axiom_fresh_segment_id_not_in_dom(s.segments);
3200    assert(id_left != sid);
3201    let ghost stub_entry = SegmentEntry { range: range.start..mid };
3202    let ghost id_right = fresh_segment_id(s.segments.insert(id_left, stub_entry));
3203    axiom_fresh_segment_id_not_in_dom(s.segments.insert(id_left, stub_entry));
3204    assert(id_right != sid);
3205    assert(id_right != id_left);
3206    // Now extract and insert.
3207    let tracked _orig = s.extract_segment(sid);
3208    assert(!s.segments.dom().contains(id_left));
3209    let tracked entry_l = axiom_segment_entry_new(range.start..mid);
3210    s.insert_segment(id_left, entry_l);
3211    assert(!s.segments.dom().contains(id_right));
3212    let tracked entry_r = axiom_segment_entry_new(mid..range.end);
3213    s.insert_segment(id_right, entry_r);
3214    // Re-establish structural_inv + accounting_inv. Regions is
3215    // unchanged; the partition lemma gives per-paddr cover_count
3216    // preservation; so every invariant clause carries over from
3217    // `s_old`.
3218    assert(s.regions == old_regions);
3219    assert forall|paddr: Paddr| #[trigger]
3220        frame_to_index(paddr) < max_meta_slots() implies segment_cover_count(s.segments, paddr)
3221        == segment_cover_count(old_segments, paddr) by {
3222        lemma_segment_cover_split(
3223            old_segments,
3224            sid,
3225            id_left,
3226            id_right,
3227            entry_left,
3228            entry_right,
3229            paddr,
3230        );
3231    };
3232    // Each invariant clause that mentions `cover_count` chains via the
3233    // per-paddr equality above. `slot_owners` / `slots` / `frames` /
3234    // `tlb_model` / `vm_spaces` / `cursors` / `vm_ios` unchanged ⟹
3235    // their clauses carry verbatim from `old(s).inv()`.
3236
3237    // Segment range well-formedness for the two new entries.
3238    assert(entry_left.range.start % PAGE_SIZE == 0);
3239    assert(entry_right.range.start % PAGE_SIZE == 0);
3240    assert(entry_left.range.end % PAGE_SIZE == 0);
3241    assert(entry_right.range.end % PAGE_SIZE == 0);
3242    // segment-covered ⟹ Frame-usage: covered paddrs by the new
3243    // entries are the same set as covered by the original ⟹ usage
3244    // was Frame pre, still Frame post (regions unchanged).
3245    assert forall|sid_other: SegmentId, paddr_c: Paddr|
3246        #![trigger
3247            s.segments.dom().contains(sid_other),
3248            frame_to_index(paddr_c)]
3249        s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c
3250            < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE
3251            == 0 implies s.regions.slot_owners[frame_to_index(paddr_c)].usage
3252        == PageUsage::Frame by {
3253        if sid_other == id_left {
3254            assert(old_segments.dom().contains(sid));
3255            assert(old_segments[sid].range.start <= paddr_c < old_segments[sid].range.end);
3256        } else if sid_other == id_right {
3257            assert(old_segments.dom().contains(sid));
3258            assert(old_segments[sid].range.start <= paddr_c < old_segments[sid].range.end);
3259        } else {
3260            assert(old_segments.dom().contains(sid_other));
3261            assert(old_segments[sid_other] == s.segments[sid_other]);
3262        }
3263    };
3264    // Discharge accounting_inv's three clauses. Regions unchanged ⟹
3265    // every per-slot value (rc, paths, usage, etc.) preserved; frames
3266    // unchanged ⟹ handle_count preserved; cover_count preserved
3267    // per-paddr via lemma_segment_cover_split.
3268    assert forall|idx: usize|
3269        #![trigger s.regions.slot_owners[idx]]
3270        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3271            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
3272        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
3273        s.segments,
3274        index_to_frame(idx),
3275    ) == 0 by {
3276        let paddr = index_to_frame(idx);
3277        assert(paddr == (idx * PAGE_SIZE) as usize);
3278        assert(frame_to_index(paddr) == idx);
3279        lemma_segment_cover_split(
3280            old_segments,
3281            sid,
3282            id_left,
3283            id_right,
3284            entry_left,
3285            entry_right,
3286            paddr,
3287        );
3288    };
3289    assert forall|idx: usize|
3290        #![trigger s.regions.slot_owners[idx]]
3291        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
3292            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
3293            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3294            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
3295        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
3296        s.segments,
3297        index_to_frame(idx),
3298    ) > 0 by {
3299        let paddr = index_to_frame(idx);
3300        assert(paddr == (idx * PAGE_SIZE) as usize);
3301        assert(frame_to_index(paddr) == idx);
3302        lemma_segment_cover_split(
3303            old_segments,
3304            sid,
3305            id_left,
3306            id_right,
3307            entry_left,
3308            entry_right,
3309            paddr,
3310        );
3311    };
3312    assert forall|idx: usize|
3313        #![trigger s.regions.slot_owners[idx]]
3314        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
3315        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
3316            || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
3317        let so = s.regions.slot_owners[idx];
3318        let rc = so.inner_perms.ref_count.value();
3319        &&& rc != REF_COUNT_UNUSED
3320        &&& rc != REF_COUNT_UNIQUE
3321        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
3322            s.segments,
3323            index_to_frame(idx),
3324        )
3325        &&& so.inner_perms.storage.is_init()
3326    } by {
3327        let paddr = index_to_frame(idx);
3328        assert(paddr == (idx * PAGE_SIZE) as usize);
3329        assert(frame_to_index(paddr) == idx);
3330        lemma_segment_cover_split(
3331            old_segments,
3332            sid,
3333            id_left,
3334            id_right,
3335            entry_left,
3336            entry_right,
3337            paddr,
3338        );
3339    };
3340}
3341
3342/// `Op::SegmentNext` step. Pops the front frame off `sid`'s range,
3343/// registering a fresh `FrameEntry` at `paddr = range.start`. The
3344/// segment's range shrinks by one page from the front; if it
3345/// becomes empty, `sid` is removed.
3346///
3347/// **The conversion bridge** between segment-held forgotten
3348/// references and user-held `Frame<M>` handles. Per-paddr at the
3349/// popped slot:
3350///   `raw_count: pre - 1`  (segment lost one forgotten ref via
3351///                          `Frame::from_raw`),
3352///   `cover_count: pre - 1` (segment's range shrunk past paddr),
3353///   `H: pre + 1`           (fresh `FrameEntry` registered),
3354///   `rc: pre`              (`from_raw` doesn't touch rc; the new
3355///                          `Frame` handle inherits the rc that
3356///                          the segment was holding).
3357///
3358/// Accounting equation `rc == H + P + cover_count`:
3359///   `pre rc == pre H + pre P + pre cover`
3360///   `post rc == pre rc
3361///            == (post H - 1) + post P + (post cover + 1)
3362///            == post H + post P + post cover`. ✓
3363///
3364/// Structural `raw_count == cover_count`:
3365///   pre: `pre raw == pre cover` at every idx.
3366///   post at popped: `(pre raw - 1) == (pre cover - 1)`. ✓
3367///   post elsewhere: unchanged.
3368proof fn step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId)
3369    requires
3370        old(s).inv(),
3371        old(s).segments.dom().contains(sid),
3372    ensures
3373        final(s).inv(),
3374{
3375    let ghost old_regions = s.regions;
3376    let ghost old_frames = s.frames;
3377    let ghost old_segments = s.segments;
3378    let ghost range = s.segments[sid].range;
3379    let ghost paddr = range.start;
3380    let ghost target_idx = frame_to_index(paddr);
3381    let ghost new_range_start = (paddr + PAGE_SIZE) as Paddr;
3382    let ghost new_range_end = range.end;
3383    let ghost will_become_empty = new_range_start >= new_range_end;
3384    let ghost new_entry_ghost = SegmentEntry { range: new_range_start..new_range_end };
3385
3386    // Establish facts about the popped slot from `s.inv()`.
3387    lemma_segment_cover_contains(old_segments, sid, paddr);
3388    assert(segment_cover_count(old_segments, paddr) >= 1);
3389    assert(old_regions.slot_owners[target_idx].usage == PageUsage::Frame);
3390    let ghost so_pre = old_regions.slot_owners[target_idx];
3391    let ghost pre_rc = so_pre.inner_perms.ref_count.value();
3392    let ghost pre_H = handle_count(old_frames, target_idx);
3393    let ghost pre_P = so_pre.paths_in_pt.len();
3394    let ghost pre_cover = segment_cover_count(old_segments, paddr);
3395    assert(pre_rc == pre_H + pre_P + pre_cover);
3396    assert(pre_rc != REF_COUNT_UNUSED);
3397    assert(pre_rc != REF_COUNT_UNIQUE);
3398    assert(pre_rc >= 1);
3399    assert(old_regions.slot_owners.contains_key(target_idx));
3400    assert(old_regions.slot_owners[target_idx].inv());
3401    assert(pre_rc <= crate::specs::mm::frame::meta_owners::REF_COUNT_MAX);
3402    assert(has_safe_slot(paddr));
3403    s.regions.inv_implies_correct_addr(paddr);
3404    assert(s.regions.slots.contains_key(target_idx));
3405    // page-alignment + bound for shrink_front lemma.
3406    assert(range.start % PAGE_SIZE == 0);
3407    assert(range.end <= MAX_PADDR);
3408    assert(range.start + PAGE_SIZE <= MAX_PADDR);
3409
3410    // Register the new FrameEntry FIRST (s.inv() still holds).
3411    let ghost fid = fresh_frame_id(s.frames);
3412    axiom_fresh_frame_id_not_in_dom(s.frames);
3413    let tracked frame_entry = axiom_frame_entry_new(paddr);
3414    s.insert_frame(fid, frame_entry);
3415    // Now segment manipulation.
3416    let tracked _old_entry = s.extract_segment(sid);
3417    segment::segment_next_embedded(&mut s.regions, paddr);
3418    if !will_become_empty {
3419        let tracked new_entry = axiom_segment_entry_new(new_range_start..new_range_end);
3420        s.insert_segment(sid, new_entry);
3421        assert(new_entry =~= new_entry_ghost);
3422        assert(s.segments =~= old_segments.remove(sid).insert(sid, new_entry_ghost));
3423    } else {
3424        assert(s.segments =~= old_segments.remove(sid));
3425    }
3426    assert(s.frames == old_frames.insert(fid, frame_entry));
3427
3428    // Per-paddr cover delta (from the shrink-front lemma): cover_post
3429    // == cover_pre - (1 at popped else 0).
3430    assert forall|paddr_c: Paddr| paddr_c % PAGE_SIZE == 0 implies #[trigger] segment_cover_count(
3431        s.segments,
3432        paddr_c,
3433    ) == (if paddr_c == paddr {
3434        1nat
3435    } else {
3436        0nat
3437    }) + 0nat
3438    // Workaround: we want
3439    //   cover_post == cover_pre - delta
3440    // Restated below as two separate forall conjuncts.
3441     || true by {
3442        lemma_segment_cover_shrink_front(old_segments, sid, new_entry_ghost, paddr_c);
3443    };
3444    // Cleaner per-paddr facts: at popped, cover decreased by 1; elsewhere unchanged.
3445    assert forall|paddr_c: Paddr|
3446        paddr_c % PAGE_SIZE == 0 && paddr_c == paddr implies #[trigger] segment_cover_count(
3447        s.segments,
3448        paddr_c,
3449    ) + 1 == segment_cover_count(old_segments, paddr_c) by {
3450        lemma_segment_cover_shrink_front(old_segments, sid, new_entry_ghost, paddr_c);
3451    };
3452    assert forall|paddr_c: Paddr|
3453        paddr_c % PAGE_SIZE == 0 && paddr_c != paddr implies #[trigger] segment_cover_count(
3454        s.segments,
3455        paddr_c,
3456    ) == segment_cover_count(old_segments, paddr_c) by {
3457        lemma_segment_cover_shrink_front(old_segments, sid, new_entry_ghost, paddr_c);
3458    };
3459
3460    // Structural in_list == 0.
3461    assert forall|idx: usize|
3462        idx
3463            < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].inner_perms.in_list.value()
3464        == 0 by {
3465        let paddr_c = index_to_frame(idx);
3466        assert(paddr_c == (idx * PAGE_SIZE) as usize);
3467        if idx == target_idx {
3468            // Axiom preserves in_list at paddr.
3469        } else {
3470            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3471        }
3472    };
3473    // Structural FrameId⟹Frame-usage. New fid points at paddr whose
3474    // slot is Frame-usage (preserved by axiom); other fids' slots
3475    // preserved.
3476    assert forall|fid_other: FrameId| #[trigger]
3477        s.frames.dom().contains(fid_other) implies s.regions.slot_owners[frame_to_index(
3478        s.frames[fid_other].paddr,
3479    )].usage == PageUsage::Frame by {
3480        let other_idx = frame_to_index(s.frames[fid_other].paddr);
3481        if fid_other == fid {
3482            assert(s.frames[fid_other].paddr == paddr);
3483            assert(other_idx == target_idx);
3484        } else {
3485            assert(old_frames.dom().contains(fid_other));
3486            assert(s.frames[fid_other] == old_frames[fid_other]);
3487            assert(old_regions.slot_owners[other_idx].usage == PageUsage::Frame);
3488        }
3489    };
3490    // Structural segment-covered ⟹ Frame-usage.
3491    assert forall|sid_other: SegmentId, paddr_c: Paddr|
3492        #![trigger
3493            s.segments.dom().contains(sid_other),
3494            frame_to_index(paddr_c)]
3495        s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c
3496            < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE
3497            == 0 implies s.regions.slot_owners[frame_to_index(paddr_c)].usage
3498        == PageUsage::Frame by {
3499        let cov_idx = frame_to_index(paddr_c);
3500        if !will_become_empty && sid_other == sid {
3501            // Covered paddr in new_range ⊆ original range.
3502            assert(old_segments.dom().contains(sid));
3503            assert(old_regions.slot_owners[cov_idx].usage == PageUsage::Frame);
3504        } else {
3505            assert(sid_other != sid);
3506            assert(old_segments.dom().contains(sid_other));
3507            assert(old_segments[sid_other] == s.segments[sid_other]);
3508            assert(old_regions.slot_owners[cov_idx].usage == PageUsage::Frame);
3509        }
3510    };
3511    // Segment range well-formedness for the re-inserted entry (if any).
3512    if !will_become_empty {
3513        assert(new_range_start % PAGE_SIZE == 0);
3514        assert(new_range_end % PAGE_SIZE == 0);
3515        assert(new_range_end <= MAX_PADDR);
3516    }
3517    // Accounting clauses.
3518
3519    assert forall|idx: usize|
3520        #![trigger s.regions.slot_owners[idx]]
3521        idx < max_meta_slots() && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3522            == REF_COUNT_UNUSED implies handle_count(s.frames, idx) == 0
3523        && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count(
3524        s.segments,
3525        index_to_frame(idx),
3526    ) == 0 by {
3527        let paddr_c = index_to_frame(idx);
3528        assert(paddr_c == (idx * PAGE_SIZE) as usize);
3529        assert(frame_to_index(paddr_c) == idx);
3530        if idx == target_idx {
3531            // post rc at target == pre rc != UNUSED (axiom). Antecedent false.
3532            assert(false);
3533        } else {
3534            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3535            lemma_handle_count_insert_fresh(old_frames, fid, frame_entry, idx);
3536        }
3537    };
3538    assert forall|idx: usize|
3539        #![trigger s.regions.slot_owners[idx]]
3540        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame
3541            && s.regions.slot_owners[idx].inner_perms.ref_count.value() != REF_COUNT_UNUSED
3542            && s.regions.slot_owners[idx].inner_perms.ref_count.value()
3543            != REF_COUNT_UNIQUE implies handle_count(s.frames, idx) > 0
3544        || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count(
3545        s.segments,
3546        index_to_frame(idx),
3547    ) > 0 by {
3548        let paddr_c = index_to_frame(idx);
3549        assert(paddr_c == (idx * PAGE_SIZE) as usize);
3550        if idx == target_idx {
3551            // New fid gives H >= 1.
3552            lemma_handle_count_insert_fresh(old_frames, fid, frame_entry, idx);
3553            assert(handle_count(s.frames, target_idx) >= 1);
3554        } else {
3555            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3556            lemma_handle_count_insert_fresh(old_frames, fid, frame_entry, idx);
3557        }
3558    };
3559    assert forall|idx: usize|
3560        #![trigger s.regions.slot_owners[idx]]
3561        idx < max_meta_slots() && s.regions.slot_owners[idx].usage == PageUsage::Frame && (
3562        handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0
3563            || segment_cover_count(s.segments, index_to_frame(idx)) > 0) implies {
3564        let so = s.regions.slot_owners[idx];
3565        let rc = so.inner_perms.ref_count.value();
3566        &&& rc != REF_COUNT_UNUSED
3567        &&& rc != REF_COUNT_UNIQUE
3568        &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count(
3569            s.segments,
3570            index_to_frame(idx),
3571        )
3572        &&& so.inner_perms.storage.is_init()
3573    } by {
3574        let paddr_c = index_to_frame(idx);
3575        assert(paddr_c == (idx * PAGE_SIZE) as usize);
3576        lemma_handle_count_insert_fresh(old_frames, fid, frame_entry, idx);
3577        if idx == target_idx {
3578            // post rc == pre rc; post H = pre H + 1; post P = pre P;
3579            // post cover = pre cover - 1.
3580            // post rc == pre H + pre P + pre cover
3581            //         == (post H - 1) + post P + (post cover + 1)
3582            //         == post H + post P + post cover.
3583        } else {
3584            assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]);
3585        }
3586    };
3587}
3588
3589/// Inserting a fresh segment whose range DOES cover `paddr` bumps
3590/// `segment_cover_count` by 1.
3591pub proof fn lemma_segment_cover_insert_inside(
3592    segments: Map<SegmentId, SegmentEntry>,
3593    sid: SegmentId,
3594    entry: SegmentEntry,
3595    paddr: Paddr,
3596)
3597    requires
3598        !segments.dom().contains(sid),
3599        segments.dom().finite(),
3600        entry.range.start <= paddr < entry.range.end,
3601    ensures
3602        segment_cover_count(segments.insert(sid, entry), paddr) == segment_cover_count(
3603            segments,
3604            paddr,
3605        ) + 1,
3606{
3607    let segments2 = segments.insert(sid, entry);
3608    let pred = |s: SegmentId| segments[s].range.start <= paddr && paddr < segments[s].range.end;
3609    let pred2 = |s: SegmentId| segments2[s].range.start <= paddr && paddr < segments2[s].range.end;
3610    let old_filt = segments.dom().filter(pred);
3611    let new_filt = segments2.dom().filter(pred2);
3612    assert(segments2.dom() =~= segments.dom().insert(sid));
3613    assert(!old_filt.contains(sid));
3614    assert(new_filt =~= old_filt.insert(sid)) by {
3615        assert forall|s: SegmentId| #[trigger] new_filt.contains(s) implies old_filt.insert(
3616            sid,
3617        ).contains(s) by {
3618            if s == sid {
3619            } else {
3620                assert(segments2[s] == segments[s]);
3621            }
3622        };
3623        assert forall|s: SegmentId| #[trigger]
3624            old_filt.insert(sid).contains(s) implies new_filt.contains(s) by {
3625            if s == sid {
3626                assert(segments2[s].range == entry.range);
3627            } else {
3628                assert(segments2[s] == segments[s]);
3629            }
3630        };
3631    };
3632    assert(old_filt.finite());
3633    assert(new_filt.len() == old_filt.len() + 1);
3634}
3635
3636/// Inserting a fresh segment whose range DOES NOT cover `paddr`
3637/// leaves `segment_cover_count(_, paddr)` unchanged.
3638pub proof fn lemma_segment_cover_insert_outside(
3639    segments: Map<SegmentId, SegmentEntry>,
3640    sid: SegmentId,
3641    entry: SegmentEntry,
3642    paddr: Paddr,
3643)
3644    requires
3645        !segments.dom().contains(sid),
3646        segments.dom().finite(),
3647        !(entry.range.start <= paddr < entry.range.end),
3648    ensures
3649        segment_cover_count(segments.insert(sid, entry), paddr) == segment_cover_count(
3650            segments,
3651            paddr,
3652        ),
3653{
3654    let segments2 = segments.insert(sid, entry);
3655    let pred = |s: SegmentId| segments[s].range.start <= paddr && paddr < segments[s].range.end;
3656    let pred2 = |s: SegmentId| segments2[s].range.start <= paddr && paddr < segments2[s].range.end;
3657    let old_filt = segments.dom().filter(pred);
3658    let new_filt = segments2.dom().filter(pred2);
3659    assert(segments2.dom() =~= segments.dom().insert(sid));
3660    assert(new_filt =~= old_filt) by {
3661        assert forall|s: SegmentId| #[trigger] new_filt.contains(s) implies old_filt.contains(
3662            s,
3663        ) by {
3664            if s == sid {
3665                // entry's range doesn't cover paddr ⟹ pred2(sid) false.
3666                assert(false);
3667            } else {
3668                assert(segments2[s] == segments[s]);
3669            }
3670        };
3671        assert forall|s: SegmentId| #[trigger] old_filt.contains(s) implies new_filt.contains(
3672            s,
3673        ) by {
3674            assert(s != sid);
3675            assert(segments2[s] == segments[s]);
3676        };
3677    };
3678}
3679
3680/// If `sid ∈ segments` and `segments[sid].range` covers `paddr`,
3681/// then `segment_cover_count >= 1` at `paddr`.
3682pub proof fn lemma_segment_cover_contains(
3683    segments: Map<SegmentId, SegmentEntry>,
3684    sid: SegmentId,
3685    paddr: Paddr,
3686)
3687    requires
3688        segments.dom().contains(sid),
3689        segments.dom().finite(),
3690        segments[sid].range.start <= paddr < segments[sid].range.end,
3691    ensures
3692        segment_cover_count(segments, paddr) >= 1,
3693{
3694    let filt = segments.dom().filter(
3695        |s: SegmentId| segments[s].range.start <= paddr && paddr < segments[s].range.end,
3696    );
3697    assert(filt.contains(sid));
3698    assert(filt.finite());
3699}
3700
3701/// Removing an existing segment whose range covers `paddr` decreases
3702/// `segment_cover_count` at `paddr` by exactly 1.
3703pub proof fn lemma_segment_cover_remove_inside(
3704    segments: Map<SegmentId, SegmentEntry>,
3705    sid: SegmentId,
3706    paddr: Paddr,
3707)
3708    requires
3709        segments.dom().contains(sid),
3710        segments.dom().finite(),
3711        segments[sid].range.start <= paddr < segments[sid].range.end,
3712    ensures
3713        segment_cover_count(segments.remove(sid), paddr) == (segment_cover_count(segments, paddr)
3714            - 1) as nat,
3715{
3716    let segments2 = segments.remove(sid);
3717    let pred = |s: SegmentId| segments[s].range.start <= paddr && paddr < segments[s].range.end;
3718    let pred2 = |s: SegmentId| segments2[s].range.start <= paddr && paddr < segments2[s].range.end;
3719    let old_filt = segments.dom().filter(pred);
3720    let new_filt = segments2.dom().filter(pred2);
3721    assert(segments2.dom() =~= segments.dom().remove(sid));
3722    assert(old_filt.contains(sid));
3723    assert(new_filt =~= old_filt.remove(sid)) by {
3724        assert forall|s: SegmentId| #[trigger] new_filt.contains(s) implies old_filt.remove(
3725            sid,
3726        ).contains(s) by {
3727            assert(s != sid);
3728            assert(segments2[s] == segments[s]);
3729        };
3730        assert forall|s: SegmentId| #[trigger]
3731            old_filt.remove(sid).contains(s) implies new_filt.contains(s) by {
3732            assert(s != sid);
3733            assert(segments2[s] == segments[s]);
3734        };
3735    };
3736    assert(old_filt.finite());
3737}
3738
3739/// **Next-pop helper.** `Segment::next` pops the front frame off
3740/// `sid`, shrinking its range from `[start, end)` to
3741/// `[start + PAGE_SIZE, end)`. The net effect on per-paddr
3742/// `segment_cover_count`: it decrements by 1 only at the popped
3743/// `paddr == start`; everywhere else it's invariant.
3744///
3745/// Models the two cases (segment becomes empty vs not) via the two
3746/// resulting map shapes: `remove(sid)` (when the new range is empty)
3747/// or `remove(sid).insert(sid, new_entry)` (when the new range still
3748/// has frames).
3749pub proof fn lemma_segment_cover_shrink_front(
3750    segments: Map<SegmentId, SegmentEntry>,
3751    sid: SegmentId,
3752    new_entry: SegmentEntry,
3753    paddr_check: Paddr,
3754)
3755    requires
3756        segments.dom().contains(sid),
3757        segments.dom().finite(),
3758        // Original segment is non-empty (the caller guarantees this
3759        // from structural_inv).
3760        segments[sid].range.start < segments[sid].range.end,
3761        // Page-aligned start (from structural_inv).
3762        segments[sid].range.start % PAGE_SIZE == 0,
3763        // No-overflow envelope for `start + PAGE_SIZE`.
3764        segments[sid].range.start + PAGE_SIZE <= MAX_PADDR,
3765        new_entry.range.start == (segments[sid].range.start + PAGE_SIZE) as Paddr,
3766        new_entry.range.end == segments[sid].range.end,
3767        new_entry.range.start <= new_entry.range.end,
3768        paddr_check % PAGE_SIZE == 0,
3769    ensures
3770// Non-empty new range case: cover at popped paddr drops by 1;
3771// elsewhere preserved.
3772
3773        new_entry.range.start < new_entry.range.end ==> ({
3774            let new_segments = segments.remove(sid).insert(sid, new_entry);
3775            paddr_check == segments[sid].range.start ==> segment_cover_count(
3776                new_segments,
3777                paddr_check,
3778            ) + 1 == segment_cover_count(segments, paddr_check)
3779        }),
3780        new_entry.range.start < new_entry.range.end ==> ({
3781            let new_segments = segments.remove(sid).insert(sid, new_entry);
3782            paddr_check != segments[sid].range.start ==> segment_cover_count(
3783                new_segments,
3784                paddr_check,
3785            ) == segment_cover_count(segments, paddr_check)
3786        }),
3787        // Empty new range case (popped frame was the only one).
3788        new_entry.range.start >= new_entry.range.end ==> ({
3789            let new_segments = segments.remove(sid);
3790            paddr_check == segments[sid].range.start ==> segment_cover_count(
3791                new_segments,
3792                paddr_check,
3793            ) + 1 == segment_cover_count(segments, paddr_check)
3794        }),
3795        new_entry.range.start >= new_entry.range.end ==> ({
3796            let new_segments = segments.remove(sid);
3797            paddr_check != segments[sid].range.start ==> segment_cover_count(
3798                new_segments,
3799                paddr_check,
3800            ) == segment_cover_count(segments, paddr_check)
3801        }),
3802{
3803    let popped = segments[sid].range.start;
3804    let range = segments[sid].range;
3805    // PAGE_SIZE > 0 + the new_entry well-formedness ⟹ range.start
3806    // < range.end (so the original segment was non-empty).
3807    assert(range.start < range.end);
3808    let sid_pre_covers = range.start <= paddr_check < range.end;
3809    let new_covers = new_entry.range.start <= paddr_check < new_entry.range.end;
3810    // Cover transition after `remove(sid)`.
3811    if sid_pre_covers {
3812        lemma_segment_cover_remove_inside(segments, sid, paddr_check);
3813    } else {
3814        lemma_segment_cover_remove_outside(segments, sid, paddr_check);
3815    }
3816    if new_entry.range.start < new_entry.range.end {
3817        let new_segments = segments.remove(sid).insert(sid, new_entry);
3818        if paddr_check == popped {
3819            // new_entry.range.start == popped + PAGE_SIZE > popped ⟹
3820            // paddr_check < new_entry.range.start ⟹ !new_covers.
3821            assert(!new_covers);
3822            assert(sid_pre_covers);
3823            lemma_segment_cover_insert_outside(segments.remove(sid), sid, new_entry, paddr_check);
3824            lemma_segment_cover_contains(segments, sid, paddr_check);
3825            assert(segment_cover_count(new_segments, paddr_check) + 1 == segment_cover_count(
3826                segments,
3827                paddr_check,
3828            ));
3829        } else if sid_pre_covers {
3830            // paddr_check ∈ [popped, range.end), paddr_check != popped,
3831            // paddr_check page-aligned + popped page-aligned ⟹
3832            // paddr_check >= popped + PAGE_SIZE ⟹ in new_entry.range.
3833            assert(new_covers);
3834            lemma_segment_cover_contains(segments, sid, paddr_check);
3835            lemma_segment_cover_insert_inside(segments.remove(sid), sid, new_entry, paddr_check);
3836            assert(segment_cover_count(new_segments, paddr_check) == segment_cover_count(
3837                segments,
3838                paddr_check,
3839            ));
3840        } else {
3841            // !sid_pre_covers ⟹ !new_covers (new_range ⊆ pre range).
3842            assert(!new_covers);
3843            lemma_segment_cover_insert_outside(segments.remove(sid), sid, new_entry, paddr_check);
3844            assert(segment_cover_count(new_segments, paddr_check) == segment_cover_count(
3845                segments,
3846                paddr_check,
3847            ));
3848        }
3849    } else {
3850        // new range empty; segments is just remove(sid).
3851        let new_segments = segments.remove(sid);
3852        if paddr_check == popped {
3853            assert(sid_pre_covers);
3854            lemma_segment_cover_contains(segments, sid, paddr_check);
3855            assert(segment_cover_count(new_segments, paddr_check) + 1 == segment_cover_count(
3856                segments,
3857                paddr_check,
3858            ));
3859        } else if sid_pre_covers {
3860            // popped + PAGE_SIZE == range.end (empty new range).
3861            // paddr_check in [popped, range.end), paddr_check != popped,
3862            // page-aligned ⟹ paddr_check >= range.end. Contradiction.
3863            assert(false);
3864        } else {
3865            // cover_post == cover_pre.
3866            assert(segment_cover_count(new_segments, paddr_check) == segment_cover_count(
3867                segments,
3868                paddr_check,
3869            ));
3870        }
3871    }
3872}
3873
3874/// **Split helper.** Partitioning a segment's range at `mid` and
3875/// replacing the original `sid` with two fresh entries covering
3876/// `[start, mid)` and `[mid, end)` leaves `segment_cover_count`
3877/// invariant at every `paddr`. Any paddr covered by the original is
3878/// covered by exactly one half; uncovered paddrs stay uncovered.
3879pub proof fn lemma_segment_cover_split(
3880    segments: Map<SegmentId, SegmentEntry>,
3881    sid: SegmentId,
3882    new_left: SegmentId,
3883    new_right: SegmentId,
3884    entry_left: SegmentEntry,
3885    entry_right: SegmentEntry,
3886    paddr: Paddr,
3887)
3888    requires
3889        segments.dom().contains(sid),
3890        segments.dom().finite(),
3891        // `new_left` and `new_right` are fresh and distinct from each
3892        // other and from `sid`.
3893        new_left != sid,
3894        new_right != sid,
3895        new_left != new_right,
3896        !segments.remove(sid).dom().contains(new_left),
3897        !segments.remove(sid).dom().contains(new_right),
3898        // The two halves partition `sid`'s range at `mid`.
3899        entry_left.range.start == segments[sid].range.start,
3900        entry_left.range.end == entry_right.range.start,
3901        entry_right.range.end == segments[sid].range.end,
3902        entry_left.range.start < entry_left.range.end,
3903        entry_right.range.start < entry_right.range.end,
3904    ensures
3905        segment_cover_count(
3906            segments.remove(sid).insert(new_left, entry_left).insert(new_right, entry_right),
3907            paddr,
3908        ) == segment_cover_count(segments, paddr),
3909{
3910    let mid_segments = segments.remove(sid);
3911    let with_left = mid_segments.insert(new_left, entry_left);
3912    // `with_left.remove(new_left) =~= mid_segments` (since new_left ∉
3913    // mid_segments.dom). Also `mid_segments.dom().finite()`.
3914    assert(mid_segments.dom().finite());
3915    assert(with_left.dom() =~= mid_segments.dom().insert(new_left));
3916    assert(with_left.dom().finite());
3917    assert(!with_left.dom().contains(new_right));
3918    let sid_covers = segments[sid].range.start <= paddr && paddr < segments[sid].range.end;
3919    let left_covers = entry_left.range.start <= paddr && paddr < entry_left.range.end;
3920    let right_covers = entry_right.range.start <= paddr && paddr < entry_right.range.end;
3921    // Step 1: remove sid.
3922    let cover_after_remove = segment_cover_count(mid_segments, paddr);
3923    if sid_covers {
3924        lemma_segment_cover_remove_inside(segments, sid, paddr);
3925        assert(cover_after_remove == (segment_cover_count(segments, paddr) - 1) as nat);
3926    } else {
3927        lemma_segment_cover_remove_outside(segments, sid, paddr);
3928        assert(cover_after_remove == segment_cover_count(segments, paddr));
3929    }
3930    // Step 2: insert new_left.
3931    let cover_after_left = segment_cover_count(with_left, paddr);
3932    if left_covers {
3933        lemma_segment_cover_insert_inside(mid_segments, new_left, entry_left, paddr);
3934        assert(cover_after_left == cover_after_remove + 1);
3935    } else {
3936        lemma_segment_cover_insert_outside(mid_segments, new_left, entry_left, paddr);
3937        assert(cover_after_left == cover_after_remove);
3938    }
3939    // Step 3: insert new_right.
3940    let final_segments = with_left.insert(new_right, entry_right);
3941    let cover_final = segment_cover_count(final_segments, paddr);
3942    if right_covers {
3943        lemma_segment_cover_insert_inside(with_left, new_right, entry_right, paddr);
3944        assert(cover_final == cover_after_left + 1);
3945    } else {
3946        lemma_segment_cover_insert_outside(with_left, new_right, entry_right, paddr);
3947        assert(cover_final == cover_after_left);
3948    }
3949    // Combine via partition property.
3950    let orig = segment_cover_count(segments, paddr);
3951    if sid_covers {
3952        // orig >= 1 (sid contributes).
3953        lemma_segment_cover_contains(segments, sid, paddr);
3954        assert(orig >= 1);
3955        assert(cover_after_remove == (orig - 1) as nat);
3956        assert(cover_after_remove + 1 == orig);
3957        if left_covers {
3958            assert(!right_covers);
3959            assert(cover_after_left == cover_after_remove + 1);
3960            assert(cover_final == cover_after_left);
3961            assert(cover_final == orig);
3962        } else {
3963            assert(right_covers);
3964            assert(cover_after_left == cover_after_remove);
3965            assert(cover_final == cover_after_left + 1);
3966            assert(cover_final == cover_after_remove + 1);
3967            assert(cover_final == orig);
3968        }
3969    } else {
3970        assert(!left_covers);
3971        assert(!right_covers);
3972        assert(cover_after_remove == orig);
3973        assert(cover_after_left == cover_after_remove);
3974        assert(cover_final == cover_after_left);
3975        assert(cover_final == orig);
3976    }
3977}
3978
3979/// Removing an existing segment whose range does NOT cover `paddr`
3980/// leaves `segment_cover_count` at `paddr` unchanged.
3981pub proof fn lemma_segment_cover_remove_outside(
3982    segments: Map<SegmentId, SegmentEntry>,
3983    sid: SegmentId,
3984    paddr: Paddr,
3985)
3986    requires
3987        segments.dom().contains(sid),
3988        segments.dom().finite(),
3989        !(segments[sid].range.start <= paddr < segments[sid].range.end),
3990    ensures
3991        segment_cover_count(segments.remove(sid), paddr) == segment_cover_count(segments, paddr),
3992{
3993    let segments2 = segments.remove(sid);
3994    let pred = |s: SegmentId| segments[s].range.start <= paddr && paddr < segments[s].range.end;
3995    let pred2 = |s: SegmentId| segments2[s].range.start <= paddr && paddr < segments2[s].range.end;
3996    let old_filt = segments.dom().filter(pred);
3997    let new_filt = segments2.dom().filter(pred2);
3998    assert(segments2.dom() =~= segments.dom().remove(sid));
3999    assert(!old_filt.contains(sid));
4000    assert(new_filt =~= old_filt) by {
4001        assert forall|s: SegmentId| #[trigger] new_filt.contains(s) implies old_filt.contains(
4002            s,
4003        ) by {
4004            assert(s != sid);
4005            assert(segments2[s] == segments[s]);
4006        };
4007        assert forall|s: SegmentId| #[trigger] old_filt.contains(s) implies new_filt.contains(
4008            s,
4009        ) by {
4010            assert(s != sid);
4011            assert(segments2[s] == segments[s]);
4012        };
4013    };
4014}
4015
4016// =============================================================================
4017// Internal helpers: fresh-id picking and tracked entry constructors.
4018// =============================================================================
4019/// Picks an id not currently in `m.dom()`. Since the key type is `int`,
4020/// an unused id always exists.
4021pub open spec fn fresh_vm_space_id<'a>(m: Map<VmSpaceId, VmSpaceOwner>) -> VmSpaceId {
4022    choose|id: VmSpaceId| !m.dom().contains(id)
4023}
4024
4025/// Picks a cursor id not currently in `m.dom()`.
4026pub open spec fn fresh_cursor_id<'rcu>(m: Map<CursorId, CursorEntry<'rcu>>) -> CursorId {
4027    choose|id: CursorId| !m.dom().contains(id)
4028}
4029
4030/// Picks a [`VmIoId`] not currently in `m.dom()`.
4031pub open spec fn fresh_vm_io_id<'a>(m: Map<VmIoId, VmIoEntry>) -> VmIoId {
4032    choose|id: VmIoId| !m.dom().contains(id)
4033}
4034
4035/// Picks a [`FrameId`] not currently in `m.dom()`.
4036pub open spec fn fresh_frame_id(m: Map<FrameId, FrameEntry>) -> FrameId {
4037    choose|id: FrameId| !m.dom().contains(id)
4038}
4039
4040pub axiom fn axiom_fresh_vm_space_id_not_in_dom<'a>(m: Map<VmSpaceId, VmSpaceOwner>)
4041    ensures
4042        !m.dom().contains(fresh_vm_space_id(m)),
4043;
4044
4045pub axiom fn axiom_fresh_cursor_id_not_in_dom<'rcu>(m: Map<CursorId, CursorEntry<'rcu>>)
4046    ensures
4047        !m.dom().contains(fresh_cursor_id(m)),
4048;
4049
4050pub axiom fn axiom_fresh_vm_io_id_not_in_dom<'a>(m: Map<VmIoId, VmIoEntry>)
4051    ensures
4052        !m.dom().contains(fresh_vm_io_id(m)),
4053;
4054
4055pub axiom fn axiom_fresh_frame_id_not_in_dom(m: Map<FrameId, FrameEntry>)
4056    ensures
4057        !m.dom().contains(fresh_frame_id(m)),
4058;
4059
4060/// Tracked constructor for [`CursorEntry`].
4061pub axiom fn axiom_cursor_entry_new<'rcu>(
4062    vm_space: VmSpaceId,
4063    kind: CursorKind,
4064    va: Range<Vaddr>,
4065    tracked owner: CursorOwner<'rcu, UserPtConfig>,
4066    tracked guards: Guards<'rcu>,
4067) -> (tracked res: CursorEntry<'rcu>)
4068    ensures
4069        res.vm_space == vm_space,
4070        res.kind == kind,
4071        res.va == va,
4072        res.owner == owner,
4073        res.guards == guards,
4074;
4075
4076/// Tracked constructor for [`VmIoEntry`].
4077pub axiom fn axiom_vm_io_entry_new<'a>(
4078    vm_space: Option<VmSpaceId>,
4079    kind: VmIoKind,
4080    vaddr: Vaddr,
4081    len: usize,
4082    tracked owner: VmIoOwner,
4083) -> (tracked res: VmIoEntry)
4084    ensures
4085        res.vm_space == vm_space,
4086        res.kind == kind,
4087        res.vaddr == vaddr,
4088        res.len == len,
4089        res.owner == owner,
4090;
4091
4092/// Tracked constructor for [`FrameEntry`].
4093pub axiom fn axiom_frame_entry_new(paddr: Paddr) -> (tracked res: FrameEntry)
4094    ensures
4095        res.paddr == paddr,
4096;
4097
4098/// Tracked constructor for [`SegmentEntry`].
4099pub axiom fn axiom_segment_entry_new(range: Range<Paddr>) -> (tracked res: SegmentEntry)
4100    ensures
4101        res.range == range,
4102;
4103
4104/// Fresh-id helper for the segment id space.
4105pub open spec fn fresh_segment_id(m: Map<SegmentId, SegmentEntry>) -> SegmentId {
4106    choose|id: SegmentId| !m.dom().contains(id)
4107}
4108
4109pub axiom fn axiom_fresh_segment_id_not_in_dom(m: Map<SegmentId, SegmentEntry>)
4110    ensures
4111        !m.dom().contains(fresh_segment_id(m)),
4112;
4113
4114} // verus!