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