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.contains(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.contains(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].same_permissions(regions0.slot_owners[changed_idx]),
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        self.and_map_full_tree(f, guard);
192        self.map_children_implies(f_strong, g);
193
194        assert forall|i: int|
195            #![trigger self.continuations[i]]
196            self.level - 1 <= i
197                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
198            let cont_entry = self.continuations[i].entry_own;
199            if cont_entry.meta_slot_paddr() is Some {
200                // Same sub-page bridge as above (continuations branch).
201                cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
202            }
203        };
204    }
205
206    /// Tree-wide guard for the **removal** of `removed_path` from
207    /// `paths_in_pt[changed_idx]`. Mirror of [`Self::no_node_at_idx`] but
208    /// also rules out any *frame* entry that still maps `changed_idx`
209    /// through exactly `removed_path` — removing that path would break
210    /// such an entry's `metaregion_sound` (`paths_in_pt.contains(path)`).
211    /// A node at `changed_idx` is also excluded (its `paths_in_pt` is a
212    /// singleton, so removal would falsify `== set![path]`). Callers
213    /// (the `unmap` site) satisfy this because the entry whose path is
214    /// being removed has just left the cursor tree.
215    pub open spec fn path_removable_at_idx(
216        self,
217        idx: int,
218        removed_path: TreePath<NR_ENTRIES>,
219    ) -> bool {
220        &&& self.map_full_tree(
221            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
222                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
223                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path),
224        )
225        &&& forall|i: int|
226            #![trigger self.continuations[i]]
227            self.level - 1 <= i < NR_LEVELS ==> {
228                let e = self.continuations[i].entry_own;
229                e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr()->0) == idx
230                    ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
231            }
232    }
233
234    /// Tree-wide predicate: no *frame* entry in the cursor tree has
235    /// tree path exactly `removed_path`. After `unmap`'s
236    /// `replace_cur_entry(Child::None)`, the entry at the cursor path
237    /// (`== removed_path`) is absent, so by tree-path correctness no
238    /// frame entry can carry that path. This is the one structural
239    /// residual of the `paths_in_pt`-removal refactor.
240    pub open spec fn no_frame_with_path(self, removed_path: TreePath<NR_ENTRIES>) -> bool {
241        &&& self.map_full_tree(
242            |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>| e.is_frame() ==> e.path != removed_path,
243        )
244        &&& forall|i: int|
245            #![trigger self.continuations[i]]
246            self.level - 1 <= i < NR_LEVELS ==> {
247                let e = self.continuations[i].entry_own;
248                e.is_frame() ==> e.path != removed_path
249            }
250    }
251
252    /// Establishes [`Self::no_frame_with_path`] from the observation that
253    /// **no current view mapping starts at `vaddr_of(removed_path)`**.
254    ///
255    /// This is the standalone "lift `path_correct_pred` + uniqueness over
256    /// the cursor tree" lemma the `unmap` site needs. Each continuation
257    /// child subtree is `pt_inv` + path-correct (via
258    /// [`PageTableOwner::pt_inv_implies_path_correct`]), and its mappings
259    /// all bubble up into `self@.mappings`; so by
260    /// [`PageTableOwner::no_frame_with_path_rec`] a frame entry carrying
261    /// `removed_path` would force a mapping starting at
262    /// `vaddr_of(removed_path)` into `self@.mappings` — exactly what the
263    /// hypothesis forbids. Continuation *entries* are nodes, so the
264    /// continuations conjunct is vacuous.
265    pub proof fn no_frame_with_path_from_no_view_mapping(self, removed_path: TreePath<NR_ENTRIES>)
266        requires
267            self.inv(),
268            forall|m: Mapping|
269                #![trigger self@.mappings.contains(m)]
270                self@.mappings.contains(m) ==> m.va_range.start != vaddr_of::<C>(removed_path),
271        ensures
272            self.no_frame_with_path(removed_path),
273    {
274        broadcast use CursorContinuation::group_lemmas;
275
276        let g = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
277            e.is_frame() ==> e.path != removed_path;
278
279        // map_full_tree(g): each continuation child subtree is pt_inv,
280        // path-correct, and its mappings sit inside self@.mappings.
281        assert forall|i: int|
282            #![trigger self.continuations[i]]
283            self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(g) by {
284            self.inv_continuation(i);
285            let cont = self.continuations[i];
286            reveal(CursorContinuation::inv_children);
287            assert forall|j: int|
288                #![trigger cont.children[j]]
289                0 <= j < cont.children.len()
290                    && cont.children[j] is Some implies cont.children[j].unwrap().subtree_satisfies(
291                cont.path().push_tail(j),
292                g,
293            ) by {
294                cont.inv_children_unroll(j);
295                cont.pt_inv_children_unroll(j);
296                cont.inv_children_rel_unroll(j);
297                let child = cont.children[j].unwrap();
298                let child_path = cont.path().push_tail(j);
299                // child.value.path == child_path (inv_children_rel) and
300                // child.value.path.inv() (EntryOwner::inv_base).
301                // L1: pt_inv ⟹ tree-wide path correctness.
302                PageTableOwner::<C>::pt_inv_implies_path_correct(child, child_path);
303                // Every mapping of this child subtree is in self@.mappings.
304                assert forall|m: Mapping|
305                    #![trigger self@.mappings.contains(m)]
306                    PageTableOwner(child).view_rec(child_path).contains(
307                        m,
308                    ) implies self@.mappings.contains(m) by {
309                    self.lemma_view_mappings_intro(m, i);
310                };
311                // L-rec: no frame entry in this subtree carries removed_path.
312                PageTableOwner(child).no_frame_with_path_rec(
313                    child_path,
314                    removed_path,
315                    self@.mappings,
316                );
317            };
318        };
319
320        // Continuation entries are page-table nodes ⟹ not frames.
321        assert forall|i: int|
322            #![trigger self.continuations[i]]
323            self.level - 1 <= i < NR_LEVELS implies {
324            let e = self.continuations[i].entry_own;
325            e.is_frame() ==> e.path != removed_path
326        } by {
327            self.inv_continuation(i);
328        };
329    }
330
331    /// Bridge: `path_removable_at_idx` follows from the (mechanically
332    /// dischargeable) no-node-at-idx fact plus the structural
333    /// no-frame-with-`removed_path` fact. Per entry mapping `idx`:
334    /// `no_node_at_idx` forces `!is_node`, and `no_frame_with_path`
335    /// forces `is_frame ==> path != removed_path` — exactly the
336    /// `path_removable_at_idx` conjunction.
337    pub proof fn path_removable_from_no_node_and_no_frame_path(
338        self,
339        idx: int,
340        removed_path: TreePath<NR_ENTRIES>,
341    )
342        requires
343            self.inv(),
344            self.no_node_at_idx(idx),
345            self.no_frame_with_path(removed_path),
346        ensures
347            self.path_removable_at_idx(idx, removed_path),
348    {
349        let nn = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
350            e.is_node() && e.meta_slot_paddr() is Some ==> frame_to_index(
351                e.meta_slot_paddr().unwrap(),
352            ) != idx;
353        let nf = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
354            e.is_frame() ==> e.path != removed_path;
355        let r = |e: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
356            e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
357                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path);
358
359        // Pointwise: nn(e) && nf(e) ==> r(e).
360        // map_full_tree halves -> combined -> r.
361        self.and_map_full_tree(nn, nf);
362        self.map_children_implies(
363            |e: EntryOwner<C>, p: TreePath<NR_ENTRIES>| nn(e, p) && nf(e, p),
364            r,
365        );
366
367        // Continuation-entry halves combine directly.
368        assert forall|i: int|
369            #![trigger self.continuations[i]]
370            self.level - 1 <= i < NR_LEVELS implies {
371            let e = self.continuations[i].entry_own;
372            e.meta_slot_paddr() is Some && frame_to_index(e.meta_slot_paddr().unwrap()) == idx
373                ==> !e.is_node() && (e.is_frame() ==> e.path != removed_path)
374        } by {
375            let e = self.continuations[i].entry_own;
376        }
377    }
378
379    /// Preservation of the cursor-level metaregion invariants when the
380    /// only change to `regions` is the **removal** of `removed_path`
381    /// from the `paths_in_pt` set at a single slot. Dual of
382    /// [`Self::metaregion_preserved_under_path_insert`]; the missing
383    /// half of the deferred `paths_in_pt` refactor that `unmap` needs.
384    ///
385    /// Removal is *not* monotone (unlike insert): the guard
386    /// [`Self::path_removable_at_idx`] is what makes it sound — no live
387    /// tree entry still needs `removed_path` at `changed_idx`.
388    pub proof fn metaregion_preserved_under_path_remove(
389        self,
390        regions0: MetaRegionOwners,
391        regions1: MetaRegionOwners,
392        changed_idx: int,
393        removed_path: TreePath<NR_ENTRIES>,
394    )
395        requires
396            self.inv(),
397            self.metaregion_sound(regions0),
398            regions0.inv(),
399            regions0.contains(changed_idx),
400            regions1.slots == regions0.slots,
401            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
402            forall|i: int|
403                #![trigger regions1.slot_owners[i]]
404                i != changed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
405            regions1.slot_owners[changed_idx].same_permissions(regions0.slot_owners[changed_idx]),
406            regions1.slot_owners[changed_idx].slot_vaddr
407                == regions0.slot_owners[changed_idx].slot_vaddr,
408            regions1.slot_owners[changed_idx].usage == regions0.slot_owners[changed_idx].usage,
409            regions1.slot_owners[changed_idx].paths_in_pt
410                == regions0.slot_owners[changed_idx].paths_in_pt.remove(removed_path),
411            self.path_removable_at_idx(changed_idx, removed_path),
412        ensures
413            self.metaregion_sound(regions1),
414    {
415        let f = PageTableOwner::<C>::metaregion_sound_pred(regions0);
416        let g = PageTableOwner::<C>::metaregion_sound_pred(regions1);
417        let guard = |entry: EntryOwner<C>, _p: TreePath<NR_ENTRIES>|
418            entry.meta_slot_paddr() is Some && frame_to_index(entry.meta_slot_paddr().unwrap())
419                == changed_idx ==> !entry.is_node() && (entry.is_frame() ==> entry.path
420                != removed_path);
421        let f_strong = |entry: EntryOwner<C>, path: TreePath<NR_ENTRIES>|
422            f(entry, path) && guard(entry, path);
423
424        self.and_map_full_tree(f, guard);
425        self.map_children_implies(f_strong, g);
426
427        assert forall|i: int|
428            #![trigger self.continuations[i]]
429            self.level - 1 <= i
430                < NR_LEVELS implies self.continuations[i].entry_own.metaregion_sound(regions1) by {
431            let cont_entry = self.continuations[i].entry_own;
432            if cont_entry.meta_slot_paddr() is Some {
433                let eidx = frame_to_index(cont_entry.meta_slot_paddr().unwrap());
434                if eidx != changed_idx {
435                    cont_entry.metaregion_sound_one_slot_changed(regions0, regions1, changed_idx);
436                } else {
437                    if cont_entry.is_frame() {
438                        cont_entry.frame_sub_pages_valid_preserved_at_own_slot(regions0, regions1);
439                    }
440                }
441            }
442        };
443    }
444
445    /// Packages the `take_next` frame-unmap preservation proof after
446    /// `paths_in_pt` removes the unmapped frame path from one metadata slot.
447    pub proof fn take_next_remove_path_preserves_metaregion(
448        self,
449        owner_before_replace: Self,
450        regions0: MetaRegionOwners,
451        regions1: MetaRegionOwners,
452        removed_idx: int,
453        removed_path: TreePath<NR_ENTRIES>,
454        target: Mapping,
455    )
456        requires
457            self.inv(),
458            owner_before_replace.inv(),
459            owner_before_replace.in_locked_range(),
460            self.metaregion_sound(regions0),
461            regions0.inv(),
462            regions0.contains(removed_idx),
463            regions0.slot_owners[removed_idx].usage !is PageTable,
464            self@.mappings == owner_before_replace@.mappings - PageTableOwner(
465                owner_before_replace.cur_subtree(),
466            )@.mappings,
467            PageTableOwner(owner_before_replace.cur_subtree())@.mappings == set![target],
468            owner_before_replace.cur_subtree().value().path == removed_path,
469            regions1.slots == regions0.slots,
470            regions1.slot_owners.dom() =~= regions0.slot_owners.dom(),
471            forall|i: int|
472                #![trigger regions1.slot_owners[i]]
473                i != removed_idx ==> regions0.slot_owners[i] == regions1.slot_owners[i],
474            regions1.slot_owners[removed_idx].same_permissions(regions0.slot_owners[removed_idx]),
475            regions1.slot_owners[removed_idx].slot_vaddr
476                == regions0.slot_owners[removed_idx].slot_vaddr,
477            regions1.slot_owners[removed_idx].usage == regions0.slot_owners[removed_idx].usage,
478            regions1.slot_owners[removed_idx].paths_in_pt
479                == regions0.slot_owners[removed_idx].paths_in_pt.remove(removed_path),
480        ensures
481            self.metaregion_sound(regions1),
482    {
483        self.no_node_at_idx_from_slot_key(regions0, removed_idx);
484
485        owner_before_replace.cur_subtree_eq_filtered_mappings_path();
486
487        let ghost sv = vaddr_of::<C>(removed_path) as int;
488        let ghost sz = page_size(owner_before_replace.level) as int;
489        assert(sz > 0) by {
490            crate::specs::mm::page_table::cursor::page_size_lemmas::lemma_page_size_ge_page_size(
491                owner_before_replace.level,
492            );
493        };
494        assert forall|mm: Mapping| #[trigger] self@.mappings.contains(mm) implies mm.va_range.start
495            != sv by {};
496
497        self.no_frame_with_path_from_no_view_mapping(removed_path);
498        self.path_removable_from_no_node_and_no_frame_path(removed_idx, removed_path);
499        self.metaregion_preserved_under_path_remove(regions0, regions1, removed_idx, removed_path);
500    }
501}
502
503} // verus!