Skip to main content

ostd/specs/mm/page_table/cursor/
invariant_preservation_lemmas.rs

1/// CursorOwner-level preservation lemmas for transient `MetaRegionOwners`
2/// updates that affect at most one slot. Phase 3b of the `paths_in_pt`
3/// refactor, where map/unmap operations insert or remove a single tree path
4/// from a single frame slot and must show that the global
5/// `Cursor::invariants` survives the edit.
6///
7/// Both directions are now proven: `metaregion_preserved_under_path_insert`
8/// (used by `map`, the huge-page split) and
9/// `metaregion_preserved_under_path_remove` (the foundational lemma the
10/// `unmap` `paths_in_pt.remove` rewrite — Phase 3b stages 2–4 — will use;
11/// the exec/postcondition rewrites that wire it in are still pending).
12///
13/// Each lemma here lifts [`EntryOwner`]-level preservation facts (from
14/// `entry_owners.rs`) over the full cursor tree.
15use vstd::prelude::*;
16
17use vstd_extra::ghost_tree::*;
18use vstd_extra::ownership::*;
19
20use crate::mm::frame::meta::REF_COUNT_UNUSED;
21use crate::mm::page_size;
22use crate::mm::page_table::*;
23use crate::specs::arch::*;
24use crate::specs::mm::frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners};
25use crate::specs::mm::page_table::Mapping;
26use crate::specs::mm::page_table::cursor::owners::{CursorContinuation, CursorOwner};
27use crate::specs::mm::page_table::node::entry_owners::EntryOwner;
28use crate::specs::mm::page_table::owners::{OwnerSubtree, PageTableOwner, vaddr_of};
29
30verus! {
31
32impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> {
33    /// Tree-wide predicate: no page-table node in either the cursor's tree
34    /// children or its continuation path has metadata slot index `idx`.
35    /// Used as the "sanity" precondition of the path-insert preservation
36    /// lemma: the only kind of entry it can touch with the new path is a
37    /// frame. Callers satisfy it by observing that page-table node metadata
38    /// lives in `FRAME_METADATA_RANGE`, which is disjoint from any data-frame
39    /// paddr (where `paths_in_pt` inserts happen).
40    pub open spec fn no_node_at_idx(self, idx: usize) -> bool {
41        &&& self.map_full_tree(
42            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
43                e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
44                    e.meta_slot_paddr()->0,
45                ) != idx,
46        )
47        &&& forall|i: int|
48            #![trigger self.continuations[i]]
49            self.level - 1 <= i < NR_LEVELS ==> {
50                let e = self.continuations[i].entry_own;
51                e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
52                    e.meta_slot_paddr()->0,
53                ) != idx
54            }
55    }
56
57    /// Pointwise conjunction of two tree-wide predicates: if `self.map_full_tree(f)`
58    /// and `self.map_full_tree(guard)` hold, then `self.map_full_tree(f && guard)`
59    /// holds. A thin wrapper around `OwnerSubtree::map_implies_and` applied per
60    /// continuation + per child; extracted so callers don't have to re-derive
61    /// the same nested `assert forall ... by { map_implies_and(...) }` block.
62    pub proof fn and_map_full_tree(
63        self,
64        f: spec_fn(EntryOwner<C>, TreePath<NR_ENTRIES>) -> bool,
65        guard: spec_fn(EntryOwner<C>, TreePath<NR_ENTRIES>) -> bool,
66    )
67        requires
68            self.inv(),
69            self.map_full_tree(f),
70            self.map_full_tree(guard),
71        ensures
72            self.map_full_tree(|e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| f(e, p) && guard(e, p)),
73    {
74        let combined = |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| f(e, p) && guard(e, p);
75        assert forall|i: int|
76            #![trigger self.continuations[i]]
77            self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(
78            combined,
79        ) by {
80            let cont = self.continuations[i];
81            reveal(CursorContinuation::inv_children);
82            assert forall|j: int|
83                #![trigger cont.children[j]]
84                0 <= j < cont.children.len()
85                    && cont.children[j] is Some implies cont.children[j].unwrap().tree_predicate_map(
86            cont.path().push_tail(j as usize), combined) by {
87                cont.inv_children_unroll(j);
88                OwnerSubtree::map_implies_and(
89                    cont.children[j].unwrap(),
90                    cont.path().push_tail(j as usize),
91                    f,
92                    guard,
93                    combined,
94                );
95            };
96        };
97    }
98
99    /// Discharge `no_node_at_idx(changed_idx)` from the observation that
100    /// `changed_idx` is the index of a slot currently sitting in the free
101    /// pool (`regions.slots.contains_key(changed_idx)`). The argument uses
102    /// `EntryOwner::active_entry_not_in_free_pool`: an active node's
103    /// metadata slot is never simultaneously in the free pool, so any node
104    /// in the cursor tree must have a different slot index than
105    /// `changed_idx`.
106    ///
107    /// Callers doing a `paths_in_pt.insert` at a frame's data-slot
108    /// (e.g., `map` and the huge-page split) use this helper to
109    /// satisfy the precondition of
110    /// [`Self::metaregion_preserved_under_path_insert`].
111    pub proof fn no_node_at_idx_from_slot_key(self, regions: MetaRegionOwners, changed_idx: usize)
112        requires
113            self.inv(),
114            regions.inv(),
115            self.metaregion_sound(regions),
116            regions.slots.contains_key(changed_idx),
117        ensures
118            self.no_node_at_idx(changed_idx),
119    {
120        let msp = PageTableOwner::<C>::metaregion_sound_pred(regions);
121        let target = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
122            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
123                e.meta_slot_paddr().unwrap(),
124            ) != changed_idx;
125
126        assert(OwnerSubtree::implies(msp, target)) by {
127            assert forall|entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
128                entry.inv() && msp(entry, path) implies #[trigger] target(entry, path) by {
129                if entry.is_node() && entry.meta_slot_paddr() is Some {
130                    EntryOwner::<C>::active_entry_not_in_free_pool(entry, regions, changed_idx);
131                }
132            };
133        };
134        self.map_children_implies(msp, target);
135
136        assert forall|i: int|
137            #![trigger self.continuations[i]]
138            self.level - 1 <= i < NR_LEVELS implies {
139            let e = self.continuations[i].entry_own;
140            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
141                e.meta_slot_paddr().unwrap(),
142            ) != changed_idx
143        } by {
144            let entry = self.continuations[i].entry_own;
145            if entry.is_node() && entry.meta_slot_paddr() is Some {
146                EntryOwner::<C>::active_entry_not_in_free_pool(entry, regions, changed_idx);
147            }
148        };
149    }
150
151    /// Preservation of the cursor-level metaregion invariants when the only
152    /// change to `regions` is the **insertion** of a new path into the
153    /// `paths_in_pt` set at a single slot.
154    pub proof fn metaregion_preserved_under_path_insert(
155        self,
156        regions0: MetaRegionOwners,
157        regions1: MetaRegionOwners,
158        changed_idx: usize,
159        new_path: TreePath<NR_ENTRIES>,
160    )
161        requires
162            self.inv(),
163            self.metaregion_sound(regions0),
164            regions0.inv(),
165            regions0.slot_owners.contains_key(changed_idx),
166            regions1.slots == regions0.slots,
167            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
168            forall|i: usize|
169                #![trigger regions1.slot_owners[i]]
170                i != changed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
171            regions1.slot_owners[changed_idx].inner_perms
172                == regions0.slot_owners[changed_idx].inner_perms,
173            regions1.slot_owners[changed_idx].slot_vaddr
174                == regions0.slot_owners[changed_idx].slot_vaddr,
175            regions1.slot_owners[changed_idx].usage == regions0.slot_owners[changed_idx].usage,
176            regions1.slot_owners[changed_idx].paths_in_pt
177                == regions0.slot_owners[changed_idx].paths_in_pt.insert(new_path),
178            self.no_node_at_idx(changed_idx),
179        ensures
180            self.metaregion_sound(regions1),
181    {
182        let f = PageTableOwner::<C>::metaregion_sound_pred(regions0);
183        let g = PageTableOwner::<C>::metaregion_sound_pred(regions1);
184        let guard = |entry: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
185            entry.is_node() && entry.meta_slot_paddr() is Some ==> frame_to_index(
186                entry.meta_slot_paddr().unwrap(),
187            ) != changed_idx;
188        let f_strong = |entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
189            f(entry, path) && guard(entry, path);
190
191        assert(OwnerSubtree::implies(f_strong, g)) by {
192            assert forall|entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
193                entry.inv() && f_strong(entry, path) implies #[trigger] g(entry, path) by {
194                if entry.meta_slot_paddr() is Some {
195                    let eidx = frame_to_index(entry.meta_slot_paddr().unwrap());
196                    if eidx != changed_idx {
197                        // Discharge the sub-page precondition: for a huge frame whose
198                        // sub_idx == changed_idx, either the sub-slot is MMIO
199                        // (no rc constraint) or the inner_perms at changed_idx are
200                        // preserved (this lemma only changes paths_in_pt), so the
201                        // r0 facts (from frame_sub_pages_valid) carry to r1.
202                        assert(entry.is_frame() && entry.parent_level > 1 ==> {
203                            let pa = entry.frame().mapped_pa;
204                            let nr_pages = page_size(entry.parent_level) / PAGE_SIZE;
205                            forall|j: usize|
206                                0 < j < nr_pages ==> {
207                                    let sub_idx = #[trigger] frame_to_index(
208                                        (pa + j * PAGE_SIZE) as usize,
209                                    );
210                                    sub_idx != changed_idx
211                                        || regions1.slot_owners[sub_idx].usage is MMIO || (
212                                    regions1.slots.contains_key(sub_idx)
213                                        && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
214                                        != REF_COUNT_UNUSED
215                                        && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
216                                        > 0)
217                                }
218                        });
219                        entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
220                    } else {
221                        if entry.parent_level > 1 {
222                        }
223                    }
224                }
225            };
226        };
227
228        self.and_map_full_tree(f, guard);
229        self.map_children_implies(f_strong, g);
230
231        assert forall|i: int|
232            #![trigger self.continuations[i]]
233            self.level - 1 <= i
234                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
235            let cont_entry = self.continuations[i].entry_own;
236            if cont_entry.meta_slot_paddr() is Some {
237                // Same sub-page bridge as above (continuations branch).
238                assert(cont_entry.is_frame() && cont_entry.parent_level > 1 ==> {
239                    let pa = cont_entry.frame().mapped_pa;
240                    let nr_pages = page_size(cont_entry.parent_level) / PAGE_SIZE;
241                    forall|j: usize|
242                        0 < j < nr_pages ==> {
243                            let sub_idx = #[trigger] frame_to_index((pa + j * PAGE_SIZE) as usize);
244                            sub_idx != changed_idx || regions1.slot_owners[sub_idx].usage is MMIO
245                                || (regions1.slots.contains_key(sub_idx)
246                                && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
247                                != REF_COUNT_UNUSED
248                                && regions1.slot_owners[sub_idx].inner_perms.ref_count.value() > 0)
249                        }
250                });
251                cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
252            }
253        };
254    }
255
256    /// Tree-wide guard for the **removal** of `removed_path` from
257    /// `paths_in_pt[changed_idx]`. Mirror of [`Self::no_node_at_idx`] but
258    /// also rules out any *frame* entry that still maps `changed_idx`
259    /// through exactly `removed_path` — removing that path would break
260    /// such an entry's `metaregion_sound` (`paths_in_pt.contains(path)`).
261    /// A node at `changed_idx` is also excluded (its `paths_in_pt` is a
262    /// singleton, so removal would falsify `== set![path]`). Callers
263    /// (the `unmap` site) satisfy this because the entry whose path is
264    /// being removed has just left the cursor tree.
265    pub open spec fn path_removable_at_idx(
266        self,
267        idx: usize,
268        removed_path: TreePath<NR_ENTRIES>,
269    ) -> bool {
270        &&& self.map_full_tree(
271            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
272                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
273                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path),
274        )
275        &&& forall|i: int|
276            #![trigger self.continuations[i]]
277            self.level - 1 <= i < NR_LEVELS ==> {
278                let e = self.continuations[i].entry_own;
279                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
280                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
281            }
282    }
283
284    /// Tree-wide predicate: no *frame* entry in the cursor tree has
285    /// tree path exactly `removed_path`. After `unmap`'s
286    /// `replace_cur_entry(Child::None)`, the entry at the cursor path
287    /// (`== removed_path`) is absent, so by tree-path correctness no
288    /// frame entry can carry that path. This is the one structural
289    /// residual of the `paths_in_pt`-removal refactor.
290    pub open spec fn no_frame_with_path(self, removed_path: TreePath<NR_ENTRIES>) -> bool {
291        &&& self.map_full_tree(
292            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>| e.is_frame() ==> e.path != removed_path,
293        )
294        &&& forall|i: int|
295            #![trigger self.continuations[i]]
296            self.level - 1 <= i < NR_LEVELS ==> {
297                let e = self.continuations[i].entry_own;
298                e.is_frame() ==> e.path != removed_path
299            }
300    }
301
302    /// Establishes [`Self::no_frame_with_path`] from the observation that
303    /// **no current view mapping starts at `vaddr_of(removed_path)`**.
304    ///
305    /// This is the standalone "lift `path_correct_pred` + uniqueness over
306    /// the cursor tree" lemma the `unmap` site needs. Each continuation
307    /// child subtree is `pt_inv` + path-correct (via
308    /// [`PageTableOwner::pt_inv_implies_path_correct`]), and its mappings
309    /// all bubble up into `self@.mappings`; so by
310    /// [`PageTableOwner::no_frame_with_path_rec`] a frame entry carrying
311    /// `removed_path` would force a mapping starting at
312    /// `vaddr_of(removed_path)` into `self@.mappings` — exactly what the
313    /// hypothesis forbids. Continuation *entries* are nodes, so the
314    /// continuations conjunct is vacuous.
315    pub proof fn no_frame_with_path_from_no_view_mapping(self, removed_path: TreePath<NR_ENTRIES>)
316        requires
317            self.inv(),
318            forall|m: Mapping|
319                #![trigger self@.mappings.contains(m)]
320                self@.mappings.contains(m) ==> m.va_range.start != vaddr_of::<C>(removed_path),
321        ensures
322            self.no_frame_with_path(removed_path),
323    {
324        broadcast use CursorContinuation::group_lemmas;
325
326        let g = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
327            e.is_frame() ==> e.path != removed_path;
328
329        // map_full_tree(g): each continuation child subtree is pt_inv,
330        // path-correct, and its mappings sit inside self@.mappings.
331        assert forall|i: int|
332            #![trigger self.continuations[i]]
333            self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(g) by {
334            self.inv_continuation(i);
335            let cont = self.continuations[i];
336            reveal(CursorContinuation::inv_children);
337            assert forall|j: int|
338                #![trigger cont.children[j]]
339                0 <= j < cont.children.len()
340                    && cont.children[j] is Some implies cont.children[j].unwrap().tree_predicate_map(
341            cont.path().push_tail(j as usize), g) by {
342                cont.inv_children_unroll(j);
343                cont.pt_inv_children_unroll(j);
344                cont.inv_children_rel_unroll(j);
345                let child = cont.children[j].unwrap();
346                let child_path = cont.path().push_tail(j as usize);
347                // child.value.path == child_path (inv_children_rel) and
348                // child.value.path.inv() (EntryOwner::inv_base).
349                // L1: pt_inv ⟹ tree-wide path correctness.
350                PageTableOwner::<C>::pt_inv_implies_path_correct(child, child_path);
351                // Every mapping of this child subtree is in self@.mappings.
352                assert forall|m: Mapping|
353                    #![trigger self@.mappings.contains(m)]
354                    PageTableOwner(child).view_rec(child_path).contains(
355                        m,
356                    ) implies self@.mappings.contains(m) by {
357                    self.lemma_view_mappings_intro(m, i);
358                };
359                // L-rec: no frame entry in this subtree carries removed_path.
360                PageTableOwner(child).no_frame_with_path_rec(
361                    child_path,
362                    removed_path,
363                    self@.mappings,
364                );
365            };
366        };
367
368        // Continuation entries are page-table nodes ⟹ not frames.
369        assert forall|i: int|
370            #![trigger self.continuations[i]]
371            self.level - 1 <= i < NR_LEVELS implies {
372            let e = self.continuations[i].entry_own;
373            e.is_frame() ==> e.path != removed_path
374        } by {
375            self.inv_continuation(i);
376        };
377    }
378
379    /// Bridge: `path_removable_at_idx` follows from the (mechanically
380    /// dischargeable) no-node-at-idx fact plus the structural
381    /// no-frame-with-`removed_path` fact. Per entry mapping `idx`:
382    /// `no_node_at_idx` forces `!is_node`, and `no_frame_with_path`
383    /// forces `is_frame ==> path != removed_path` — exactly the
384    /// `path_removable_at_idx` conjunction.
385    pub proof fn path_removable_from_no_node_and_no_frame_path(
386        self,
387        idx: usize,
388        removed_path: TreePath<NR_ENTRIES>,
389    )
390        requires
391            self.inv(),
392            self.no_node_at_idx(idx),
393            self.no_frame_with_path(removed_path),
394        ensures
395            self.path_removable_at_idx(idx, removed_path),
396    {
397        let nn = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
398            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
399                e.meta_slot_paddr().unwrap(),
400            ) != idx;
401        let nf = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
402            e.is_frame() ==> e.path != removed_path;
403        let r = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
404            e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
405                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path);
406
407        // Pointwise: nn(e) && nf(e) ==> r(e).
408        assert(OwnerSubtree::implies(
409            |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| nn(e, p) && nf(e, p),
410            r,
411        )) by {
412            assert forall|e: EntryOwner<C>, p: TreePath<NR_ENTRIES>|
413                e.inv() && nn(e, p) && nf(e, p) implies #[trigger] r(e, p) by {}
414        };
415        // map_full_tree halves -> combined -> r.
416        self.and_map_full_tree(nn, nf);
417        self.map_children_implies(
418            |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| nn(e, p) && nf(e, p),
419            r,
420        );
421
422        // Continuation-entry halves combine directly.
423        assert forall|i: int|
424            #![trigger self.continuations[i]]
425            self.level - 1 <= i < NR_LEVELS implies {
426            let e = self.continuations[i].entry_own;
427            e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
428                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
429        } by {
430            let e = self.continuations[i].entry_own;
431        }
432    }
433
434    /// Preservation of the cursor-level metaregion invariants when the
435    /// only change to `regions` is the **removal** of `removed_path`
436    /// from the `paths_in_pt` set at a single slot. Dual of
437    /// [`Self::metaregion_preserved_under_path_insert`]; the missing
438    /// half of the deferred `paths_in_pt` refactor that `unmap` needs.
439    ///
440    /// Removal is *not* monotone (unlike insert): the guard
441    /// [`Self::path_removable_at_idx`] is what makes it sound — no live
442    /// tree entry still needs `removed_path` at `changed_idx`.
443    pub proof fn metaregion_preserved_under_path_remove(
444        self,
445        regions0: MetaRegionOwners,
446        regions1: MetaRegionOwners,
447        changed_idx: usize,
448        removed_path: TreePath<NR_ENTRIES>,
449    )
450        requires
451            self.inv(),
452            self.metaregion_sound(regions0),
453            regions0.inv(),
454            regions0.slot_owners.contains_key(changed_idx),
455            regions1.slots == regions0.slots,
456            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
457            forall|i: usize|
458                #![trigger regions1.slot_owners[i]]
459                i != changed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
460            regions1.slot_owners[changed_idx].inner_perms
461                == regions0.slot_owners[changed_idx].inner_perms,
462            regions1.slot_owners[changed_idx].slot_vaddr
463                == regions0.slot_owners[changed_idx].slot_vaddr,
464            regions1.slot_owners[changed_idx].usage == regions0.slot_owners[changed_idx].usage,
465            regions1.slot_owners[changed_idx].paths_in_pt
466                == regions0.slot_owners[changed_idx].paths_in_pt.remove(removed_path),
467            self.path_removable_at_idx(changed_idx, removed_path),
468        ensures
469            self.metaregion_sound(regions1),
470    {
471        let f = PageTableOwner::<C>::metaregion_sound_pred(regions0);
472        let g = PageTableOwner::<C>::metaregion_sound_pred(regions1);
473        let guard = |entry: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
474            entry.meta_slot_paddr() is Some && frame_to_index(entry.meta_slot_paddr().unwrap())
475                == changed_idx ==> !entry.is_node() && (entry.is_frame() ==> entry.path
476                != removed_path);
477        let f_strong = |entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
478            f(entry, path) && guard(entry, path);
479
480        assert(OwnerSubtree::implies(f_strong, g)) by {
481            assert forall|entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
482                entry.inv() && f_strong(entry, path) implies #[trigger] g(entry, path) by {
483                if entry.meta_slot_paddr() is Some {
484                    let eidx = frame_to_index(entry.meta_slot_paddr().unwrap());
485                    if eidx != changed_idx {
486                        // Sub-page bridge: for a huge frame whose
487                        // sub_idx == changed_idx, only paths_in_pt changed
488                        // there (inner_perms / usage preserved), so the
489                        // r0 sub-page facts carry to r1.
490                        assert(entry.is_frame() && entry.parent_level > 1 ==> {
491                            let pa = entry.frame().mapped_pa;
492                            let nr_pages = page_size(entry.parent_level) / PAGE_SIZE;
493                            forall|j: usize|
494                                0 < j < nr_pages ==> {
495                                    let sub_idx = #[trigger] frame_to_index(
496                                        (pa + j * PAGE_SIZE) as usize,
497                                    );
498                                    sub_idx != changed_idx
499                                        || regions1.slot_owners[sub_idx].usage is MMIO || (
500                                    regions1.slots.contains_key(sub_idx)
501                                        && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
502                                        != REF_COUNT_UNUSED
503                                        && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
504                                        > 0)
505                                }
506                        });
507                        entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
508                    } else {
509                        // eidx == changed_idx: guard ⇒ not a node. If a
510                        // frame, its path ≠ removed_path so the shrunk
511                        // `paths_in_pt` still contains it; the only
512                        // cross-slot conjunct (`frame_sub_pages_valid`)
513                        // is carried by the dedicated own-slot lemma
514                        // (which has the sub-page arithmetic baked in).
515                        if entry.is_frame() {
516                            assert(regions0.slot_owners[changed_idx].paths_in_pt.contains(
517                                entry.path,
518                            ));
519                            assert(regions1.slot_owners[changed_idx].paths_in_pt.contains(
520                                entry.path,
521                            ));
522                            entry.frame_sub_pages_valid_preserved_at_own_slot(regions0, regions1);
523                        }
524                    }
525                }
526            };
527        };
528
529        self.and_map_full_tree(f, guard);
530        self.map_children_implies(f_strong, g);
531
532        assert forall|i: int|
533            #![trigger self.continuations[i]]
534            self.level - 1 <= i
535                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
536            let cont_entry = self.continuations[i].entry_own;
537            if cont_entry.meta_slot_paddr() is Some {
538                let eidx = frame_to_index(cont_entry.meta_slot_paddr().unwrap());
539                if eidx != changed_idx {
540                    assert(cont_entry.is_frame() && cont_entry.parent_level > 1 ==> {
541                        let pa = cont_entry.frame().mapped_pa;
542                        let nr_pages = page_size(cont_entry.parent_level) / PAGE_SIZE;
543                        forall|j: usize|
544                            0 < j < nr_pages ==> {
545                                let sub_idx = #[trigger] frame_to_index(
546                                    (pa + j * PAGE_SIZE) as usize,
547                                );
548                                sub_idx != changed_idx
549                                    || regions1.slot_owners[sub_idx].usage is MMIO || (
550                                regions1.slots.contains_key(sub_idx)
551                                    && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
552                                    != REF_COUNT_UNUSED
553                                    && regions1.slot_owners[sub_idx].inner_perms.ref_count.value()
554                                    > 0)
555                            }
556                    });
557                    cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
558                } else {
559                    if cont_entry.is_frame() {
560                        assert(regions0.slot_owners[changed_idx].paths_in_pt.contains(
561                            cont_entry.path,
562                        ));
563                        assert(regions1.slot_owners[changed_idx].paths_in_pt.contains(
564                            cont_entry.path,
565                        ));
566                        cont_entry.frame_sub_pages_valid_preserved_at_own_slot(regions0, regions1);
567                    }
568                }
569            }
570        };
571    }
572
573    /// Packages the `take_next` frame-unmap preservation proof after
574    /// `paths_in_pt` removes the unmapped frame path from one metadata slot.
575    pub proof fn take_next_remove_path_preserves_metaregion(
576        self,
577        owner_before_replace: Self,
578        regions0: MetaRegionOwners,
579        regions1: MetaRegionOwners,
580        removed_idx: usize,
581        removed_path: TreePath<NR_ENTRIES>,
582        target: Mapping,
583    )
584        requires
585            self.inv(),
586            owner_before_replace.inv(),
587            owner_before_replace.in_locked_range(),
588            self.metaregion_sound(regions0),
589            regions0.inv(),
590            regions0.slot_owners.contains_key(removed_idx),
591            regions0.slots.contains_key(removed_idx),
592            self@.mappings == owner_before_replace@.mappings - PageTableOwner(
593                owner_before_replace.cur_subtree(),
594            )@.mappings,
595            PageTableOwner(owner_before_replace.cur_subtree())@.mappings == set![target],
596            owner_before_replace.cur_subtree().value.path == removed_path,
597            regions1.slots == regions0.slots,
598            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
599            forall|i: usize|
600                #![trigger regions1.slot_owners[i]]
601                i != removed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
602            regions1.slot_owners[removed_idx].inner_perms
603                == regions0.slot_owners[removed_idx].inner_perms,
604            regions1.slot_owners[removed_idx].slot_vaddr
605                == regions0.slot_owners[removed_idx].slot_vaddr,
606            regions1.slot_owners[removed_idx].usage == regions0.slot_owners[removed_idx].usage,
607            regions1.slot_owners[removed_idx].paths_in_pt
608                == regions0.slot_owners[removed_idx].paths_in_pt.remove(removed_path),
609        ensures
610            self.metaregion_sound(regions1),
611    {
612        self.no_node_at_idx_from_slot_key(regions0, removed_idx);
613
614        owner_before_replace.cur_subtree_eq_filtered_mappings_path();
615        let ghost obr_subtree = PageTableOwner(owner_before_replace.cur_subtree())@.mappings;
616
617        let ghost sv = vaddr_of::<C>(removed_path) as int;
618        let ghost sz = page_size(owner_before_replace.level) as int;
619        assert(obr_subtree == owner_before_replace@.mappings.filter(
620            |mm: Mapping| sv <= mm.va_range.start < sv + sz,
621        ));
622        assert(sz > 0) by {
623            crate::specs::mm::page_table::cursor::page_size_lemmas::lemma_page_size_ge_page_size(
624                owner_before_replace.level,
625            );
626        };
627        assert forall|mm: Mapping| #[trigger] self@.mappings.contains(mm) implies mm.va_range.start
628            != sv by {
629            if mm.va_range.start == sv {
630                assert(owner_before_replace@.mappings.filter(
631                    |m2: Mapping| sv <= m2.va_range.start < sv + sz,
632                ).contains(mm));
633            }
634        };
635
636        self.no_frame_with_path_from_no_view_mapping(removed_path);
637        self.path_removable_from_no_node_and_no_frame_path(removed_idx, removed_path);
638        self.metaregion_preserved_under_path_remove(regions0, regions1, removed_idx, removed_path);
639    }
640}
641
642} // verus!