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::*, ownership::*};
18
19use crate::specs::{
20    arch::*,
21    mm::{
22        frame::{
23            mapping::frame_to_index, meta_owners::PageUsage, meta_region_owners::MetaRegionOwners,
24        },
25        page_table::{
26            Mapping,
27            cursor::owners::{CursorContinuation, CursorOwner},
28            node::entry_owners::EntryOwner,
29            owners::{OwnerSubtree, PageTableOwner, vaddr_of},
30        },
31    },
32};
33
34use crate::mm::{frame::meta::REF_COUNT_UNUSED, page_size, page_table::*};
35
36verus! {
37
38impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> {
39    /// Tree-wide predicate: no page-table node in either the cursor's tree
40    /// children or its continuation path has metadata slot index `idx`.
41    /// Used as the "sanity" precondition of the path-insert preservation
42    /// lemma: the only kind of entry it can touch with the new path is a
43    /// frame. Callers satisfy it by observing that page-table node metadata
44    /// lives in `FRAME_METADATA_RANGE`, which is disjoint from any data-frame
45    /// paddr (where `paths_in_pt` inserts happen).
46    pub open spec fn no_node_at_idx(self, idx: int) -> bool {
47        &&& self.map_full_tree(
48            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
49                e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
50                    e.meta_slot_paddr()->0,
51                ) != idx,
52        )
53        &&& forall|i: int|
54            #![trigger self.continuations[i]]
55            self.level - 1 <= i < NR_LEVELS ==> {
56                let e = self.continuations[i].entry_own;
57                e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
58                    e.meta_slot_paddr()->0,
59                ) != idx
60            }
61    }
62
63    /// Pointwise conjunction of two tree-wide predicates: if `self.map_full_tree(f)`
64    /// and `self.map_full_tree(guard)` hold, then `self.map_full_tree(f && guard)`
65    /// holds. A thin wrapper around `OwnerSubtree::lemma_subtree_satisfies_implies_and` applied per
66    /// continuation + per child; extracted so callers don't have to re-derive
67    /// the same nested `assert forall ... by { lemma_subtree_satisfies_implies_and(...) }` block.
68    pub proof fn and_map_full_tree(
69        self,
70        f: spec_fn(EntryOwner<C>, TreePath<NR_ENTRIES>) -> bool,
71        guard: spec_fn(EntryOwner<C>, TreePath<NR_ENTRIES>) -> bool,
72    )
73        requires
74            self.inv(),
75            self.map_full_tree(f),
76            self.map_full_tree(guard),
77        ensures
78            self.map_full_tree(|e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| f(e, p) && guard(e, p)),
79    {
80        let combined = |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| f(e, p) && guard(e, p);
81        assert forall|i: int|
82            #![trigger self.continuations[i]]
83            self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(
84            combined,
85        ) by {
86            let cont = self.continuations[i];
87            reveal(CursorContinuation::inv_children);
88            assert forall|j: int|
89                #![trigger cont.children[j]]
90                0 <= j < cont.children.len()
91                    && cont.children[j] is Some implies cont.children[j].unwrap().subtree_satisfies(
92                cont.path().push_tail(j),
93                combined,
94            ) by {
95                cont.inv_children_unroll(j);
96                OwnerSubtree::lemma_subtree_satisfies_implies_and(
97                    cont.children[j].unwrap(),
98                    cont.path().push_tail(j),
99                    f,
100                    guard,
101                    combined,
102                );
103            };
104        };
105    }
106
107    /// Discharge `no_node_at_idx(changed_idx)` from the observation that
108    /// `changed_idx` is not a page-table slot. Active node entries always
109    /// occupy metadata slots whose usage is `PageTable`, so any node in the
110    /// cursor tree must have a different slot index than `changed_idx`.
111    ///
112    /// Callers doing a `paths_in_pt.insert` at a frame's data-slot
113    /// (e.g., `map` and the huge-page split) use this helper to
114    /// satisfy the precondition of
115    /// [`Self::metaregion_preserved_under_path_insert`].
116    pub proof fn no_node_at_idx_from_slot_key(self, regions: MetaRegionOwners, changed_idx: int)
117        requires
118            self.inv(),
119            regions.inv(),
120            self.metaregion_sound(regions),
121            regions.slots.contains_key(changed_idx),
122            regions.slot_owners[changed_idx].usage !is PageTable,
123        ensures
124            self.no_node_at_idx(changed_idx),
125    {
126        let msp = PageTableOwner::<C>::metaregion_sound_pred(regions);
127        let target = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
128            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
129                e.meta_slot_paddr().unwrap(),
130            ) != changed_idx;
131
132        self.map_children_implies(msp, target);
133
134        assert forall|i: int|
135            #![trigger self.continuations[i]]
136            self.level - 1 <= i < NR_LEVELS implies {
137            let e = self.continuations[i].entry_own;
138            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
139                e.meta_slot_paddr().unwrap(),
140            ) != changed_idx
141        } by {
142            let entry = self.continuations[i].entry_own;
143            if entry.is_node() && entry.meta_slot_paddr() is Some {
144                let idx = frame_to_index(entry.meta_slot_paddr().unwrap());
145                if idx == changed_idx {
146                    assert(false);
147                }
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: int,
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: int|
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].slot_vaddr
175                == regions0.slot_owners[changed_idx].slot_vaddr,
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        self.and_map_full_tree(f, guard);
193        self.map_children_implies(f_strong, g);
194
195        assert forall|i: int|
196            #![trigger self.continuations[i]]
197            self.level - 1 <= i
198                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
199            let cont_entry = self.continuations[i].entry_own;
200            if cont_entry.meta_slot_paddr() is Some {
201                // Same sub-page bridge as above (continuations branch).
202                cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
203            }
204        };
205    }
206
207    /// Tree-wide guard for the **removal** of `removed_path` from
208    /// `paths_in_pt[changed_idx]`. Mirror of [`Self::no_node_at_idx`] but
209    /// also rules out any *frame* entry that still maps `changed_idx`
210    /// through exactly `removed_path` — removing that path would break
211    /// such an entry's `metaregion_sound` (`paths_in_pt.contains(path)`).
212    /// A node at `changed_idx` is also excluded (its `paths_in_pt` is a
213    /// singleton, so removal would falsify `== set![path]`). Callers
214    /// (the `unmap` site) satisfy this because the entry whose path is
215    /// being removed has just left the cursor tree.
216    pub open spec fn path_removable_at_idx(
217        self,
218        idx: int,
219        removed_path: TreePath<NR_ENTRIES>,
220    ) -> bool {
221        &&& self.map_full_tree(
222            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
223                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
224                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path),
225        )
226        &&& forall|i: int|
227            #![trigger self.continuations[i]]
228            self.level - 1 <= i < NR_LEVELS ==> {
229                let e = self.continuations[i].entry_own;
230                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
231                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
232            }
233    }
234
235    /// Tree-wide predicate: no *frame* entry in the cursor tree has
236    /// tree path exactly `removed_path`. After `unmap`'s
237    /// `replace_cur_entry(Child::None)`, the entry at the cursor path
238    /// (`== removed_path`) is absent, so by tree-path correctness no
239    /// frame entry can carry that path. This is the one structural
240    /// residual of the `paths_in_pt`-removal refactor.
241    pub open spec fn no_frame_with_path(self, removed_path: TreePath<NR_ENTRIES>) -> bool {
242        &&& self.map_full_tree(
243            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>| e.is_frame() ==> e.path != removed_path,
244        )
245        &&& forall|i: int|
246            #![trigger self.continuations[i]]
247            self.level - 1 <= i < NR_LEVELS ==> {
248                let e = self.continuations[i].entry_own;
249                e.is_frame() ==> e.path != removed_path
250            }
251    }
252
253    /// Establishes [`Self::no_frame_with_path`] from the observation that
254    /// **no current view mapping starts at `vaddr_of(removed_path)`**.
255    ///
256    /// This is the standalone "lift `path_correct_pred` + uniqueness over
257    /// the cursor tree" lemma the `unmap` site needs. Each continuation
258    /// child subtree is `pt_inv` + path-correct (via
259    /// [`PageTableOwner::pt_inv_implies_path_correct`]), and its mappings
260    /// all bubble up into `self@.mappings`; so by
261    /// [`PageTableOwner::no_frame_with_path_rec`] a frame entry carrying
262    /// `removed_path` would force a mapping starting at
263    /// `vaddr_of(removed_path)` into `self@.mappings` — exactly what the
264    /// hypothesis forbids. Continuation *entries* are nodes, so the
265    /// continuations conjunct is vacuous.
266    pub proof fn no_frame_with_path_from_no_view_mapping(self, removed_path: TreePath<NR_ENTRIES>)
267        requires
268            self.inv(),
269            forall|m: Mapping|
270                #![trigger self@.mappings.contains(m)]
271                self@.mappings.contains(m) ==> m.va_range.start != vaddr_of::<C>(removed_path),
272        ensures
273            self.no_frame_with_path(removed_path),
274    {
275        broadcast use CursorContinuation::group_lemmas;
276
277        let g = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
278            e.is_frame() ==> e.path != removed_path;
279
280        // map_full_tree(g): each continuation child subtree is pt_inv,
281        // path-correct, and its mappings sit inside self@.mappings.
282        assert forall|i: int|
283            #![trigger self.continuations[i]]
284            self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(g) by {
285            self.inv_continuation(i);
286            let cont = self.continuations[i];
287            reveal(CursorContinuation::inv_children);
288            assert forall|j: int|
289                #![trigger cont.children[j]]
290                0 <= j < cont.children.len()
291                    && cont.children[j] is Some implies cont.children[j].unwrap().subtree_satisfies(
292                cont.path().push_tail(j),
293                g,
294            ) by {
295                cont.inv_children_unroll(j);
296                cont.pt_inv_children_unroll(j);
297                cont.inv_children_rel_unroll(j);
298                let child = cont.children[j].unwrap();
299                let child_path = cont.path().push_tail(j);
300                // child.value.path == child_path (inv_children_rel) and
301                // child.value.path.inv() (EntryOwner::inv_base).
302                // L1: pt_inv ⟹ tree-wide path correctness.
303                PageTableOwner::<C>::pt_inv_implies_path_correct(child, child_path);
304                // Every mapping of this child subtree is in self@.mappings.
305                assert forall|m: Mapping|
306                    #![trigger self@.mappings.contains(m)]
307                    PageTableOwner(child).view_rec(child_path).contains(
308                        m,
309                    ) implies self@.mappings.contains(m) by {
310                    self.lemma_view_mappings_intro(m, i);
311                };
312                // L-rec: no frame entry in this subtree carries removed_path.
313                PageTableOwner(child).no_frame_with_path_rec(
314                    child_path,
315                    removed_path,
316                    self@.mappings,
317                );
318            };
319        };
320
321        // Continuation entries are page-table nodes ⟹ not frames.
322        assert forall|i: int|
323            #![trigger self.continuations[i]]
324            self.level - 1 <= i < NR_LEVELS implies {
325            let e = self.continuations[i].entry_own;
326            e.is_frame() ==> e.path != removed_path
327        } by {
328            self.inv_continuation(i);
329        };
330    }
331
332    /// Bridge: `path_removable_at_idx` follows from the (mechanically
333    /// dischargeable) no-node-at-idx fact plus the structural
334    /// no-frame-with-`removed_path` fact. Per entry mapping `idx`:
335    /// `no_node_at_idx` forces `!is_node`, and `no_frame_with_path`
336    /// forces `is_frame ==> path != removed_path` — exactly the
337    /// `path_removable_at_idx` conjunction.
338    pub proof fn path_removable_from_no_node_and_no_frame_path(
339        self,
340        idx: int,
341        removed_path: TreePath<NR_ENTRIES>,
342    )
343        requires
344            self.inv(),
345            self.no_node_at_idx(idx),
346            self.no_frame_with_path(removed_path),
347        ensures
348            self.path_removable_at_idx(idx, removed_path),
349    {
350        let nn = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
351            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
352                e.meta_slot_paddr().unwrap(),
353            ) != idx;
354        let nf = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
355            e.is_frame() ==> e.path != removed_path;
356        let r = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
357            e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
358                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path);
359
360        // Pointwise: nn(e) && nf(e) ==> r(e).
361        // map_full_tree halves -> combined -> r.
362        self.and_map_full_tree(nn, nf);
363        self.map_children_implies(
364            |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| nn(e, p) && nf(e, p),
365            r,
366        );
367
368        // Continuation-entry halves combine directly.
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.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
374                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
375        } by {
376            let e = self.continuations[i].entry_own;
377        }
378    }
379
380    /// Preservation of the cursor-level metaregion invariants when the
381    /// only change to `regions` is the **removal** of `removed_path`
382    /// from the `paths_in_pt` set at a single slot. Dual of
383    /// [`Self::metaregion_preserved_under_path_insert`]; the missing
384    /// half of the deferred `paths_in_pt` refactor that `unmap` needs.
385    ///
386    /// Removal is *not* monotone (unlike insert): the guard
387    /// [`Self::path_removable_at_idx`] is what makes it sound — no live
388    /// tree entry still needs `removed_path` at `changed_idx`.
389    pub proof fn metaregion_preserved_under_path_remove(
390        self,
391        regions0: MetaRegionOwners,
392        regions1: MetaRegionOwners,
393        changed_idx: int,
394        removed_path: TreePath<NR_ENTRIES>,
395    )
396        requires
397            self.inv(),
398            self.metaregion_sound(regions0),
399            regions0.inv(),
400            regions0.slot_owners.contains_key(changed_idx),
401            regions1.slots == regions0.slots,
402            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
403            forall|i: int|
404                #![trigger regions1.slot_owners[i]]
405                i != changed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
406            regions1.slot_owners[changed_idx].inner_perms
407                == regions0.slot_owners[changed_idx].inner_perms,
408            regions1.slot_owners[changed_idx].slot_vaddr
409                == regions0.slot_owners[changed_idx].slot_vaddr,
410            regions1.slot_owners[changed_idx].usage == regions0.slot_owners[changed_idx].usage,
411            regions1.slot_owners[changed_idx].paths_in_pt
412                == regions0.slot_owners[changed_idx].paths_in_pt.remove(removed_path),
413            self.path_removable_at_idx(changed_idx, removed_path),
414        ensures
415            self.metaregion_sound(regions1),
416    {
417        let f = PageTableOwner::<C>::metaregion_sound_pred(regions0);
418        let g = PageTableOwner::<C>::metaregion_sound_pred(regions1);
419        let guard = |entry: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
420            entry.meta_slot_paddr() is Some && frame_to_index(entry.meta_slot_paddr().unwrap())
421                == changed_idx ==> !entry.is_node() && (entry.is_frame() ==> entry.path
422                != removed_path);
423        let f_strong = |entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
424            f(entry, path) && guard(entry, path);
425
426        self.and_map_full_tree(f, guard);
427        self.map_children_implies(f_strong, g);
428
429        assert forall|i: int|
430            #![trigger self.continuations[i]]
431            self.level - 1 <= i
432                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
433            let cont_entry = self.continuations[i].entry_own;
434            if cont_entry.meta_slot_paddr() is Some {
435                let eidx = frame_to_index(cont_entry.meta_slot_paddr().unwrap());
436                if eidx != changed_idx {
437                    cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
438                } else {
439                    if cont_entry.is_frame() {
440                        cont_entry.frame_sub_pages_valid_preserved_at_own_slot(regions0, regions1);
441                    }
442                }
443            }
444        };
445    }
446
447    /// Packages the `take_next` frame-unmap preservation proof after
448    /// `paths_in_pt` removes the unmapped frame path from one metadata slot.
449    pub proof fn take_next_remove_path_preserves_metaregion(
450        self,
451        owner_before_replace: Self,
452        regions0: MetaRegionOwners,
453        regions1: MetaRegionOwners,
454        removed_idx: int,
455        removed_path: TreePath<NR_ENTRIES>,
456        target: Mapping,
457    )
458        requires
459            self.inv(),
460            owner_before_replace.inv(),
461            owner_before_replace.in_locked_range(),
462            self.metaregion_sound(regions0),
463            regions0.inv(),
464            regions0.slot_owners.contains_key(removed_idx),
465            regions0.slots.contains_key(removed_idx),
466            regions0.slot_owners[removed_idx].usage !is PageTable,
467            self@.mappings == owner_before_replace@.mappings - PageTableOwner(
468                owner_before_replace.cur_subtree(),
469            )@.mappings,
470            PageTableOwner(owner_before_replace.cur_subtree())@.mappings == set![target],
471            owner_before_replace.cur_subtree().value().path == removed_path,
472            regions1.slots == regions0.slots,
473            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
474            forall|i: int|
475                #![trigger regions1.slot_owners[i]]
476                i != removed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
477            regions1.slot_owners[removed_idx].inner_perms
478                == regions0.slot_owners[removed_idx].inner_perms,
479            regions1.slot_owners[removed_idx].slot_vaddr
480                == regions0.slot_owners[removed_idx].slot_vaddr,
481            regions1.slot_owners[removed_idx].usage == regions0.slot_owners[removed_idx].usage,
482            regions1.slot_owners[removed_idx].paths_in_pt
483                == regions0.slot_owners[removed_idx].paths_in_pt.remove(removed_path),
484        ensures
485            self.metaregion_sound(regions1),
486    {
487        self.no_node_at_idx_from_slot_key(regions0, removed_idx);
488
489        owner_before_replace.cur_subtree_eq_filtered_mappings_path();
490
491        let ghost sv = vaddr_of::<C>(removed_path) as int;
492        let ghost sz = page_size(owner_before_replace.level) as int;
493        assert(sz > 0) by {
494            crate::specs::mm::page_table::cursor::page_size_lemmas::lemma_page_size_ge_page_size(
495                owner_before_replace.level,
496            );
497        };
498        assert forall|mm: Mapping| #[trigger] self@.mappings.contains(mm) implies mm.va_range.start
499            != sv by {};
500
501        self.no_frame_with_path_from_no_view_mapping(removed_path);
502        self.path_removable_from_no_node_and_no_frame_path(removed_idx, removed_path);
503        self.metaregion_preserved_under_path_remove(regions0, regions1, removed_idx, removed_path);
504    }
505}
506
507} // verus!