From f158f4bda5f9b0272bee64f2648bcc689c3b210c Mon Sep 17 00:00:00 2001 From: Je5s1e Date: Tue, 8 Sep 2026 11:03:25 +0800 Subject: [PATCH 1/5] Integrate architecture paging model and fix verification --- ostd/specs/arch/mod.rs | 12 + ostd/specs/arch/model.rs | 59 +++ ostd/specs/arch/x86/mod.rs | 70 ++- ostd/specs/mm/embedding/mod.rs | 12 +- ostd/specs/mm/frame/meta_region_owners.rs | 5 +- .../mm/page_table/cursor/cursor_steps.rs | 11 +- ostd/specs/mm/page_table/cursor/owners.rs | 31 +- ostd/specs/mm/page_table/mod.rs | 6 + ostd/specs/mm/page_table/owners.rs | 495 ++++++++---------- ostd/specs/mod.rs | 1 - ostd/src/arch/x86/mm/mod.rs | 73 ++- ostd/src/mm/kspace/mod.rs | 5 +- ostd/src/mm/mod.rs | 79 ++- ostd/src/mm/page_table/cursor/mod.rs | 31 +- ostd/src/mm/page_table/mod.rs | 26 +- ostd/src/mm/page_table/node/entry.rs | 4 +- ostd/src/mm/page_table/node/mod.rs | 25 +- ostd/src/mm/vm_space.rs | 3 +- 18 files changed, 542 insertions(+), 406 deletions(-) create mode 100644 ostd/specs/arch/mod.rs create mode 100644 ostd/specs/arch/model.rs diff --git a/ostd/specs/arch/mod.rs b/ostd/specs/arch/mod.rs new file mode 100644 index 000000000..d9940a21d --- /dev/null +++ b/ostd/specs/arch/mod.rs @@ -0,0 +1,12 @@ +pub mod model; +pub use model::*; + +// Compatibility re-exports for proof modules that still use `specs::arch`. +// The authoritative values live in the executable memory/architecture modules. +pub use crate::{ + arch::mm::{NR_ENTRIES, NR_LEVELS}, + mm::{MAX_NR_PAGES, MAX_PADDR}, +}; + +mod x86; +pub use x86::*; diff --git a/ostd/specs/arch/model.rs b/ostd/specs/arch/model.rs new file mode 100644 index 000000000..b79972ce7 --- /dev/null +++ b/ostd/specs/arch/model.rs @@ -0,0 +1,59 @@ +use vstd::prelude::*; + +use crate::mm::{Paddr, PagingConstsTrait, Vaddr}; + +verus! { + +/// The paging-related part of an architecture contract. +/// +/// The associated paging constants are still supplied by the existing +/// `PagingConstsTrait`; this trait only adds the architecture-wide physical +/// address bound and the proof that the two contracts are compatible. +pub trait ArchPagingModel { + type C: PagingConstsTrait; + + /// The exclusive upper bound for physical frame addresses. + spec fn max_paddr_spec() -> Paddr; + + proof fn lemma_paging_model_requirements() + ensures + 0 < Self::max_paddr_spec(), + Self::C::BASE_PAGE_SIZE() <= Self::max_paddr_spec(), + Self::max_paddr_spec() % Self::C::BASE_PAGE_SIZE() == 0, + ; +} + +/// A physical address that can identify a base-page frame for architecture `A`. +pub open spec fn valid_frame_paddr_for(pa: Paddr) -> bool { + pa % A::C::BASE_PAGE_SIZE() == 0 && pa < A::max_paddr_spec() +} + +/// The address-space part of an architecture contract. +pub trait ArchAddressSpaceModel: ArchPagingModel { + /// The base of the kernel's physical-to-virtual linear mapping. + spec fn linear_mapping_base_vaddr_spec() -> Vaddr; + + /// The first virtual address reserved for vmalloc mappings. + spec fn vmalloc_base_vaddr_spec() -> Vaddr; + + proof fn lemma_address_space_model_requirements() + ensures + Self::linear_mapping_base_vaddr_spec() % Self::C::BASE_PAGE_SIZE() == 0, + Self::linear_mapping_base_vaddr_spec() < Self::vmalloc_base_vaddr_spec(), + Self::max_paddr_spec() < Self::vmalloc_base_vaddr_spec() + - Self::linear_mapping_base_vaddr_spec(), + Self::max_paddr_spec() + Self::linear_mapping_base_vaddr_spec() < usize::MAX, + ; +} + +/// Convert a physical address through architecture `A`'s linear mapping. +pub open spec fn paddr_to_vaddr_for(pa: Paddr) -> Vaddr { + (pa + A::linear_mapping_base_vaddr_spec()) as usize +} + +/// Convert a linear-mapped virtual address back to a physical address. +pub open spec fn vaddr_to_paddr_for(va: Vaddr) -> Paddr { + (va - A::linear_mapping_base_vaddr_spec()) as usize +} + +} // verus! diff --git a/ostd/specs/arch/x86/mod.rs b/ostd/specs/arch/x86/mod.rs index af7bd7a88..204850d21 100644 --- a/ostd/specs/arch/x86/mod.rs +++ b/ostd/specs/arch/x86/mod.rs @@ -3,13 +3,17 @@ use vstd::prelude::*; use vstd::arithmetic::power2::{lemma_pow2_adds, lemma2_to64, lemma2_to64_rest, pow2}; use vstd_extra::prelude::*; +use super::model::{self, ArchAddressSpaceModel, ArchPagingModel}; + +use crate::arch::mm::{NR_ENTRIES, NR_LEVELS}; use crate::specs::mm::{ frame::mapping::lemma_meta_to_frame_soundness, page_table::{nr_pte_index_bits_spec, pte_index_bit_offset_spec}, }; use crate::mm::{ - Paddr, PagingConstsTrait, Vaddr, + CurrentPagingConstsTrait, MAX_NR_PAGES, MAX_PADDR, Paddr, PagingConstsTrait, PagingLevel, + Vaddr, frame::meta::{META_SLOT_SIZE, mapping::meta_to_frame}, kspace::{FRAME_METADATA_RANGE, LINEAR_MAPPING_BASE_VADDR, VMALLOC_BASE_VADDR, paddr_to_vaddr}, page_size, @@ -22,37 +26,57 @@ global size_of usize == 8; global size_of isize == 8; -// The following constants are the same as those defined in `ostd::arch::mm::x86_64`, -// but we record their actual values for better proof automation. -/// Page size. -pub const PAGE_SIZE: usize = 4096; +/// Page size used by the current verification target. +pub const PAGE_SIZE: usize = crate::arch::mm::x86_base_page_size!(); -/// The maximum number of entries in a page table node -pub const NR_ENTRIES: usize = 512; +pub open spec fn valid_frame_paddr(paddr: Paddr) -> bool { + model::valid_frame_paddr_for::(paddr) +} -/// The maximum level of a page table node. -pub const NR_LEVELS: usize = 4; +/// The x86 instance of the architecture-wide specification contract. +pub ghost struct X86Arch; -/// Parameterized maximum physical address. -pub const MAX_PADDR: usize = 0x8000_0000; +impl ArchPagingModel for X86Arch { + type C = crate::arch::mm::PagingConsts; -pub const MAX_NR_PAGES: u64 = (MAX_PADDR / PAGE_SIZE) as u64; + open spec fn max_paddr_spec() -> Paddr { + MAX_PADDR + } -pub open spec fn valid_frame_paddr(paddr: Paddr) -> bool { - &&& paddr % PAGE_SIZE == 0 - &&& paddr < MAX_PADDR + proof fn lemma_paging_model_requirements() { + Self::C::lemma_paging_consts_requirements(); + } } -} // verus! -verus! { +impl ArchAddressSpaceModel for X86Arch { + open spec fn linear_mapping_base_vaddr_spec() -> Vaddr { + LINEAR_MAPPING_BASE_VADDR + } + + open spec fn vmalloc_base_vaddr_spec() -> Vaddr { + VMALLOC_BASE_VADDR + } + + proof fn lemma_address_space_model_requirements() { + Self::C::lemma_paging_consts_requirements(); + Self::lemma_paging_model_requirements(); + assert(Self::linear_mapping_base_vaddr_spec() % Self::C::BASE_PAGE_SIZE() == 0) + by (compute_only); + + assert(Self::max_paddr_spec() < Self::vmalloc_base_vaddr_spec() + - Self::linear_mapping_base_vaddr_spec()) by (compute_only); + } +} + +/// The architecture selected by the current verification target. +pub type CurrentArch = X86Arch; pub proof fn lemma_linear_mapping_base_vaddr_properties() ensures LINEAR_MAPPING_BASE_VADDR % PAGE_SIZE == 0, LINEAR_MAPPING_BASE_VADDR < VMALLOC_BASE_VADDR, { - assert(LINEAR_MAPPING_BASE_VADDR % PAGE_SIZE == 0) by (compute_only); - assert(LINEAR_MAPPING_BASE_VADDR < VMALLOC_BASE_VADDR) by (compute_only); + CurrentArch::lemma_address_space_model_requirements(); } /// There is not an executable version in the source code. @@ -61,7 +85,7 @@ pub open spec fn vaddr_to_paddr(va: Vaddr) -> usize recommends LINEAR_MAPPING_BASE_VADDR <= va < VMALLOC_BASE_VADDR, { - (va - LINEAR_MAPPING_BASE_VADDR) as usize + model::vaddr_to_paddr_for::(va) } pub broadcast proof fn lemma_paddr_to_vaddr_properties(pa: Paddr) @@ -87,8 +111,7 @@ pub proof fn lemma_max_paddr_range() MAX_PADDR < VMALLOC_BASE_VADDR - LINEAR_MAPPING_BASE_VADDR, MAX_PADDR + LINEAR_MAPPING_BASE_VADDR < usize::MAX, { - assert(MAX_PADDR < VMALLOC_BASE_VADDR - LINEAR_MAPPING_BASE_VADDR) by (compute_only); - assert(MAX_PADDR + LINEAR_MAPPING_BASE_VADDR < usize::MAX) by (compute_only); + CurrentArch::lemma_address_space_model_requirements(); } pub broadcast proof fn lemma_meta_frame_vaddr_properties(meta: Vaddr) @@ -113,7 +136,7 @@ pub broadcast proof fn lemma_meta_frame_vaddr_properties(meta: Vaddr) // Here are some architecture-specific const value properties. // Any use of this lemma in architecture-independent code should be removed. -pub(crate) proof fn lemma_arch_specific_consts_properties() +pub(crate) proof fn lemma_arch_specific_consts_properties() ensures C::BASE_PAGE_SIZE().ilog2() == 12u32, nr_pte_index_bits_spec::() == 9usize, @@ -127,6 +150,7 @@ pub(crate) proof fn lemma_arch_specific_consts_properties( 0xffff_int * 0x1_0000_0000_0000int + pow2(48) - 1 == 0xffff_ffff_ffff_ffffint, { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); lemma2_to64(); lemma2_to64_rest(); lemma_usize_pow2_ilog2(12); diff --git a/ostd/specs/mm/embedding/mod.rs b/ostd/specs/mm/embedding/mod.rs index 17c042468..1ef09d16b 100644 --- a/ostd/specs/mm/embedding/mod.rs +++ b/ostd/specs/mm/embedding/mod.rs @@ -2491,7 +2491,6 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: assert(old_regions.slot_owners[u_idx].paths_in_pt.is_empty()); assert(old_regions.slot_owners[u_idx].in_list_perm.value() == 0); // `u_idx` is a managed slot. - assert(valid_frame_paddr(s.unique_frames[u].paddr)); s.regions.lemma_contains_valid_frame_paddr(s.unique_frames[u].paddr); assert(s.regions.contains(u_idx)); // usage / in_list preserved universally by the unmap axiom. @@ -2829,7 +2828,6 @@ proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId // embedding-level `Frame::wf(state)`). lemma_frame_drop_pre_derivable(*s, fid); let ghost p = s.frames[fid].paddr; - assert(valid_frame_paddr(p)); s.regions.lemma_contains_valid_frame_paddr(p); let ghost idx_p = frame_to_index(p); // `fid ∈ s.frames` ⟹ `handle_count(s.frames, idx_p) ≥ 1`. Used @@ -3496,7 +3494,6 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme let u_paddr = s.unique_frames[u].paddr; let u_idx = frame_to_index(u_paddr); assert(old(s).unique_frames.dom().contains(u)); - assert(valid_frame_paddr(u_paddr)); s.regions.lemma_contains_valid_frame_paddr(u_paddr); // Old UNIQUE validity at `u`. assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); @@ -3725,6 +3722,7 @@ proof fn lemma_step_segment_split<'rcu>( /// pre: `pre raw == pre cover` at every idx. /// post at popped: `(pre raw - 1) == (pre cover - 1)`. ✓ /// post elsewhere: unchanged. +#[verifier::spinoff_prover] proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId) requires old(s).inv(), @@ -3756,7 +3754,6 @@ proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme assert(pre_rc != REF_COUNT_UNUSED); assert(pre_rc != REF_COUNT_UNIQUE); assert(old_regions.contains(target_idx)); - assert(valid_frame_paddr(paddr)); s.regions.lemma_contains_valid_frame_paddr(paddr); assert(s.regions.contains(target_idx)); // page-alignment + bound for shrink_front lemma. @@ -3957,7 +3954,6 @@ proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme let u_paddr = s.unique_frames[u].paddr; let u_idx = frame_to_index(u_paddr); assert(old(s).unique_frames.dom().contains(u)); - assert(valid_frame_paddr(u_paddr)); s.regions.lemma_contains_valid_frame_paddr(u_paddr); assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); assert(old_regions.slot_owners[u_idx].usage is Frame); @@ -4126,7 +4122,6 @@ proof fn lemma_step_segment_clone_range<'rcu>( // (`slot_owners.contains_key`) + `MetaRegionOwners::inv`'s // biimplication. Then the universal usage-preservation above // gives `s.regions` usage == old usage == Frame at cov_idx. - assert(valid_frame_paddr(paddr_c)); s.regions.lemma_contains_valid_frame_paddr(paddr_c); assert(s.regions.contains(cov_idx)); }; @@ -4140,7 +4135,6 @@ proof fn lemma_step_segment_clone_range<'rcu>( let other_idx = frame_to_index(s.frames[fid_other].paddr); assert(old_frames.dom().contains(fid_other)); assert(old_regions.slot_owners[other_idx].usage is Frame); - assert(valid_frame_paddr(s.frames[fid_other].paddr)); s.regions.lemma_contains_valid_frame_paddr(s.frames[fid_other].paddr); assert(s.regions.contains(other_idx)); // `other_0 <= idx < max_meta_slots()` (biimplication) ⟹ universal @@ -4236,7 +4230,6 @@ proof fn lemma_step_segment_clone_range<'rcu>( let u_paddr = s.unique_frames[u].paddr; let u_idx = frame_to_index(u_paddr); assert(old(s).unique_frames.dom().contains(u)); - assert(valid_frame_paddr(u_paddr)); s.regions.lemma_contains_valid_frame_paddr(u_paddr); assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); assert(old_regions.slot_owners[u_idx].usage is Frame); @@ -4525,7 +4518,6 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique // Slot facts from the structural unique-entry clause + the UNIQUE // branch of `MetaSlotOwner::inv`. - assert(valid_frame_paddr(paddr)); s.regions.lemma_contains_valid_frame_paddr(paddr); assert(s.regions.contains(idx)); assert(index_to_frame(idx) == paddr); @@ -4715,7 +4707,6 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique let ghost idx = frame_to_index(paddr); // Slot facts from the structural unique-entry clause + UNIQUE branch. - assert(valid_frame_paddr(paddr)); s.regions.lemma_contains_valid_frame_paddr(paddr); assert(s.regions.contains(idx)); assert(index_to_frame(idx) == paddr); @@ -4907,7 +4898,6 @@ proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: Fr let ghost idx = frame_to_index(paddr); // `fid` registered ⟹ in-bound, `usage == Frame`, and it contributes // to `handle_count` (so the slot is an active head). - assert(valid_frame_paddr(paddr)); s.regions.lemma_contains_valid_frame_paddr(paddr); assert(s.regions.contains(idx)); assert(index_to_frame(idx) == paddr); diff --git a/ostd/specs/mm/frame/meta_region_owners.rs b/ostd/specs/mm/frame/meta_region_owners.rs index b3a2fca17..ecd2bd32f 100644 --- a/ostd/specs/mm/frame/meta_region_owners.rs +++ b/ostd/specs/mm/frame/meta_region_owners.rs @@ -8,9 +8,8 @@ use vstd::{ }; use vstd_extra::{cast_ptr::Repr, drop_tracking::DropObligation, ownership::*}; -use crate::specs::arch::valid_frame_paddr; use crate::specs::{ - arch::{MAX_PADDR, PAGE_SIZE}, + arch::{ArchPagingModel, CurrentArch, MAX_PADDR, PAGE_SIZE, valid_frame_paddr}, mm::frame::mapping::{frame_to_index, index_to_meta, max_meta_slots}, }; @@ -174,6 +173,8 @@ impl MetaRegionOwners { ensures self.contains(frame_to_index(paddr)), { + CurrentArch::lemma_paging_model_requirements(); + assert(paddr % PAGE_SIZE == 0 && paddr < MAX_PADDR); } /// Rertuns the `MetaSlotOwner`, indexed by frame paddr. diff --git a/ostd/specs/mm/page_table/cursor/cursor_steps.rs b/ostd/specs/mm/page_table/cursor/cursor_steps.rs index 459d0a810..ca74b44ec 100644 --- a/ostd/specs/mm/page_table/cursor/cursor_steps.rs +++ b/ostd/specs/mm/page_table/cursor/cursor_steps.rs @@ -407,7 +407,8 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { } - #[verifier::rlimit(50)] + #[verifier::spinoff_prover] + #[verifier::rlimit(20)] pub proof fn push_level_owner_preserves_invs( self, guard: PageTableGuard<'rcu, C>, @@ -937,13 +938,7 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { // Same as the no-carry branch below: use align_up_advances_general. let inc = self.inc_index(); - assert(inc.va.inv()) by { - assert forall|i: int| 0 <= i < NR_LEVELS implies inc.va.index.contains_key(i) - && 0 <= #[trigger] inc.va.index[i] && inc.va.index[i] < NR_ENTRIES by { - if i != self.level - 1 { - } - }; - }; + self.lemma_inc_index_va_inv(); inc.va.align_down_concrete(self.level as int); let ps = page_size(self.level as PagingLevel) as nat; let self_va = self.va.to_vaddr() as nat; diff --git a/ostd/specs/mm/page_table/cursor/owners.rs b/ostd/specs/mm/page_table/cursor/owners.rs index 487043fe6..06f5ceed2 100644 --- a/ostd/specs/mm/page_table/cursor/owners.rs +++ b/ostd/specs/mm/page_table/cursor/owners.rs @@ -34,7 +34,7 @@ use crate::specs::{ use crate::arch::mm::PagingConsts; use crate::mm::{ - MAX_USERSPACE_VADDR, Paddr, PagingConstsTrait, PagingLevel, Vaddr, + CurrentPagingConstsTrait, MAX_USERSPACE_VADDR, Paddr, PagingConstsTrait, PagingLevel, Vaddr, frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, kspace::KernelPtConfig, nr_subpage_per_huge, @@ -1922,6 +1922,34 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { cont.inv_children_unroll(cont.idx as int) } + /// Constructs an absent subtree that fits the cursor's current slot. + pub proof fn tracked_new_absent_subtree(self) -> (tracked res: OwnerSubtree) + requires + self.inv(), + ensures + res.inv(), + res.value().is_absent(), + res.level() == self.continuations[self.level - 1].tree_level + 1, + res.value().path.len() <= INC_LEVELS - 1, + res.value().parent_level == self.continuations[self.level + - 1].child().value().parent_level, + res.value().path == self.continuations[self.level - 1].path().push_tail( + self.continuations[self.level - 1].idx as int, + ), + res == OwnerSubtree::new_val(res.value(), res.level() as nat), + { + let cont = self.continuations[self.level - 1]; + self.inv_continuation(self.level - 1); + + cont.inv_children_unroll(cont.idx as int); + cont.inv_children_rel_unroll(cont.idx as int); + + let tracked entry = EntryOwner::tracked_new_absent(self.cur_entry_owner().path, self.level); + + let tracked subtree = OwnerSubtree::tracked_new_val(entry, cont.tree_level + 1); + subtree + } + /// If the current entry is absent, `!self@.present()`. pub proof fn cur_entry_absent_not_present(self) requires @@ -2369,6 +2397,7 @@ pub proof fn lemma_view_in_vaddr_range<'rcu, C: PageTableConfig>(owner: &CursorO }, { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); C::lemma_page_table_config_constant_properties(); lemma_arch_specific_consts_properties::(); diff --git a/ostd/specs/mm/page_table/mod.rs b/ostd/specs/mm/page_table/mod.rs index cda810f20..cfa2bb91b 100644 --- a/ostd/specs/mm/page_table/mod.rs +++ b/ostd/specs/mm/page_table/mod.rs @@ -2013,6 +2013,12 @@ impl AbstractVaddr { if path.len() == 0 { let aligned = self.align_down(5); self.align_down_shape(4); + self.align_down(4).lemma_insert_zero_preserves_inv(3); + assert forall|i: int| 0 <= i < NR_LEVELS implies #[trigger] aligned.index[i] == 0 by { + if i < NR_LEVELS - 1 { + assert(self.align_down(4).index[i] == 0); + } + }; // align_down(5) zeroes index[3] on top of align_down(4), so all indices + offset are 0. assert(aligned.index[3] == 0) by { assert(aligned == AbstractVaddr { diff --git a/ostd/specs/mm/page_table/owners.rs b/ostd/specs/mm/page_table/owners.rs index 50e491dd6..56c4367d0 100644 --- a/ostd/specs/mm/page_table/owners.rs +++ b/ostd/specs/mm/page_table/owners.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, Range}; use vstd::prelude::*; -use vstd::{arithmetic::power2::pow2, seq::*, seq_lib::*, set_lib::*}; +use vstd::{seq::*, seq_lib::*, set_lib::*}; use vstd_extra::{drop_tracking::*, ghost_tree::*, ownership::*, prelude::TreeNodeValue}; use crate::specs::{ @@ -11,7 +11,8 @@ use crate::specs::{ frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners}, page_table::{ cursor::page_size_lemmas::{ - lemma_page_size_divides, lemma_page_size_ge_page_size, lemma_page_size_spec_values, + lemma_nr_entries_times_sub_page_size, lemma_page_size_divides, + lemma_page_size_ge_page_size, lemma_page_size_spec_values, }, *, }, @@ -21,7 +22,7 @@ use crate::specs::{ use crate::mm::{ Paddr, PagingConstsTrait, PagingLevel, Vaddr, frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, - page_size, + page_size, page_size_spec, page_table::{EntryOwner, EntryOwnerKind, PageTableEntryTrait, PageTableGuard}, }; @@ -35,7 +36,7 @@ pub open spec fn vaddr_shift_bits(idx: int) -> nat 0 < L, idx < L, { - (12 + 9 * (L - 1 - idx)) as nat + page_size_spec((L - idx) as PagingLevel).ilog2() as nat } #[verifier::inline] @@ -44,7 +45,7 @@ pub open spec fn vaddr_shift(idx: int) -> usize 0 < L, idx < L, { - pow2(vaddr_shift_bits::(idx)) as usize + page_size_spec((L - idx) as PagingLevel) } #[verifier::inline] @@ -52,9 +53,9 @@ pub open spec fn vaddr_make(idx: int, offset: usize) -> usize recommends 0 < L, idx < L, - 0 <= offset < 512, + offset < NR_ENTRIES, { - (vaddr_shift::(idx) * offset) as usize + (page_size_spec((L - idx) as PagingLevel) * offset) as usize } pub open spec fn rec_vaddr(path: TreePath, idx: int) -> usize @@ -73,6 +74,183 @@ pub open spec fn vaddr(path: TreePath) -> usize { rec_vaddr(path, 0) } +/// A path index advances by one page at the corresponding paging level. +proof fn lemma_vaddr_make_eq_page_size(idx: int, offset: int) + requires + 0 <= idx < NR_LEVELS, + 0 <= offset < NR_ENTRIES, + ensures + vaddr_make::(idx, offset as usize) == offset * page_size( + (NR_LEVELS - idx) as PagingLevel, + ), +{ + let entry_size = page_size((NR_LEVELS - idx) as PagingLevel) as int; + let parent_size = page_size((INC_LEVELS - idx) as PagingLevel) as int; + lemma_nr_entries_times_sub_page_size((INC_LEVELS - idx) as PagingLevel); + assert(offset * entry_size <= NR_ENTRIES * entry_size) by (nonlinear_arith) + requires + 0 <= offset <= NR_ENTRIES, + 0 <= entry_size, + ; + assert(NR_ENTRIES * entry_size == parent_size); + assert(parent_size <= usize::MAX); + assert(offset as usize == offset); + assert(page_size_spec((NR_LEVELS - idx) as PagingLevel) == entry_size); + assert(vaddr_make::(idx, offset as usize) == (entry_size * offset) as usize); + assert(entry_size * offset == offset * entry_size) by (nonlinear_arith); +} + +/// The suffix beginning at `idx`, together with the mapped page at the end of +/// the path, fits in one entry at the parent level. +proof fn lemma_rec_vaddr_suffix_bound(path: TreePath, idx: int) + requires + path.inv(), + 0 <= idx <= path.len() <= INC_LEVELS - 1, + ensures + rec_vaddr(path, idx) + page_size((INC_LEVELS - path.len()) as PagingLevel) <= page_size( + (INC_LEVELS - idx) as PagingLevel, + ), + decreases path.len() - idx, +{ + if idx == path.len() { + assert(rec_vaddr(path, idx) == 0); + } else { + path.lemma_index_satisfies_elem_inv(idx); + lemma_rec_vaddr_suffix_bound(path, idx + 1); + lemma_nr_entries_times_sub_page_size((INC_LEVELS - idx) as PagingLevel); + + let entry_size = page_size((NR_LEVELS - idx) as PagingLevel) as int; + let mapped_page_size = page_size((INC_LEVELS - path.len()) as PagingLevel) as int; + lemma_vaddr_make_eq_page_size(idx, path[idx]); + assert(path[idx] + 1 <= NR_ENTRIES); + assert((path[idx] + 1) * entry_size <= NR_ENTRIES * entry_size) by (nonlinear_arith) + requires + 0 <= path[idx] + 1 <= NR_ENTRIES, + 0 <= entry_size, + ; + assert(entry_size * path[idx] + rec_vaddr(path, idx + 1) + mapped_page_size <= (path[idx] + + 1) * entry_size) by (nonlinear_arith) + requires + rec_vaddr(path, idx + 1) + mapped_page_size <= entry_size, + 0 <= entry_size, + ; + assert(entry_size * path[idx] + rec_vaddr(path, idx + 1) <= usize::MAX); + assert(rec_vaddr(path, idx) == entry_size * path[idx] + rec_vaddr(path, idx + 1)); + assert(rec_vaddr(path, idx) + mapped_page_size <= (path[idx] + 1) * entry_size); + } +} + +/// Unfold one path entry without a truncating `usize` cast. +proof fn lemma_rec_vaddr_unfold(path: TreePath, idx: int) + requires + path.inv(), + 0 <= idx < path.len() <= INC_LEVELS - 1, + ensures + rec_vaddr(path, idx) == path[idx] * page_size((NR_LEVELS - idx) as PagingLevel) + rec_vaddr( + path, + idx + 1, + ), +{ + path.lemma_index_satisfies_elem_inv(idx); + lemma_rec_vaddr_suffix_bound(path, idx + 1); + lemma_vaddr_make_eq_page_size(idx, path[idx]); + lemma_nr_entries_times_sub_page_size((INC_LEVELS - idx) as PagingLevel); + + let entry_size = page_size((NR_LEVELS - idx) as PagingLevel) as int; + let parent_size = page_size((INC_LEVELS - idx) as PagingLevel) as int; + assert(path[idx] + 1 <= NR_ENTRIES); + assert(path[idx] * entry_size + rec_vaddr(path, idx + 1) <= (path[idx] + 1) * entry_size) + by (nonlinear_arith) + requires + rec_vaddr(path, idx + 1) <= entry_size, + 0 <= entry_size, + ; + assert((path[idx] + 1) * entry_size <= NR_ENTRIES * entry_size) by (nonlinear_arith) + requires + 0 <= path[idx] + 1 <= NR_ENTRIES, + 0 <= entry_size, + ; + assert(NR_ENTRIES * entry_size == parent_size); + assert(parent_size <= usize::MAX); + assert(path[idx] * entry_size + rec_vaddr(path, idx + 1) <= usize::MAX); +} + +/// Every path suffix is aligned to the page size represented by its last +/// entry. +proof fn lemma_rec_vaddr_aligned(path: TreePath, idx: int) + requires + path.inv(), + 0 <= idx <= path.len() <= INC_LEVELS - 1, + ensures + rec_vaddr(path, idx) % page_size((INC_LEVELS - path.len()) as PagingLevel) == 0, + decreases path.len() - idx, +{ + let mapped_level = (INC_LEVELS - path.len()) as PagingLevel; + let mapped_page_size = page_size(mapped_level) as int; + lemma_page_size_ge_page_size(mapped_level); + assert(0 < mapped_page_size); + if idx == path.len() { + assert(rec_vaddr(path, idx) == 0); + assert(0usize % page_size(mapped_level) == 0); + } else { + path.lemma_index_satisfies_elem_inv(idx); + lemma_rec_vaddr_aligned(path, idx + 1); + lemma_rec_vaddr_unfold(path, idx); + + let entry_level = (NR_LEVELS - idx) as PagingLevel; + let entry_size = page_size(entry_level) as int; + lemma_page_size_divides(mapped_level, entry_level); + assert(entry_size % mapped_page_size == 0); + vstd::arithmetic::div_mod::lemma_fundamental_div_mod(entry_size, mapped_page_size); + let entry_ratio = entry_size / mapped_page_size; + assert(entry_size == entry_ratio * mapped_page_size); + vstd::arithmetic::div_mod::lemma_mod_multiples_basic( + path[idx] * entry_ratio, + mapped_page_size, + ); + vstd::arithmetic::mul::lemma_mul_is_associative(path[idx], entry_ratio, mapped_page_size); + assert(path[idx] * entry_size == (path[idx] * entry_ratio) * mapped_page_size); + assert((path[idx] * entry_size) % mapped_page_size == 0); + vstd_extra::arithmetic::lemma_mod_0_add( + path[idx] * entry_size, + rec_vaddr(path, idx + 1) as int, + mapped_page_size, + ); + assert((path[idx] * entry_size + rec_vaddr(path, idx + 1)) % mapped_page_size == 0); + assert((rec_vaddr(path, idx) as int) % mapped_page_size == 0); + assert(rec_vaddr(path, idx) % page_size(mapped_level) == ((rec_vaddr(path, idx) as int) + % mapped_page_size) as usize); + } +} + +/// Appending a path index adds exactly one entry-sized offset. +proof fn lemma_rec_vaddr_push_tail(path: TreePath, i: int, idx: int) + requires + path.inv(), + path.len() < INC_LEVELS - 1, + 0 <= i < NR_ENTRIES, + 0 <= idx <= path.len(), + ensures + rec_vaddr(path.push_tail(i), idx) == rec_vaddr(path, idx) + i * page_size( + (NR_LEVELS - path.len()) as PagingLevel, + ), + decreases path.len() - idx, +{ + let pt = path.push_tail(i); + path.lemma_push_tail_len(i); + path.lemma_push_tail_index(i); + path.lemma_push_tail_preserves_inv(i); + if idx == path.len() { + assert(rec_vaddr(path, idx) == 0); + lemma_rec_vaddr_unfold(pt, idx); + assert(rec_vaddr(pt, idx + 1) == 0); + } else { + lemma_rec_vaddr_push_tail(path, i, idx + 1); + lemma_rec_vaddr_unfold(path, idx); + lemma_rec_vaddr_unfold(pt, idx); + } +} + /// Virtual address of `path` with `leading_bits` placed in bits `[48, 64)`. /// /// Matches `AbstractVaddr { offset: 0, index: , leading_bits } @@ -92,9 +270,9 @@ pub open spec fn vaddr_of(path: TreePath) -> usi vaddr_at(path, C::LEADING_BITS_spec() as int) } -/// `vaddr(path) < 2^48` for every valid path: each term in the positional -/// sum is `i_k * 2^(12 + 9·k)` with `i_k < 512 = 2^9`, so the sum is -/// strictly less than `2^48`. +/// The positional VA of every valid path fits in the current paging address +/// space. The concrete x86 bound is retained for callers that combine it with +/// canonical leading bits. #[verifier::spinoff_prover] pub proof fn lemma_vaddr_strict_bound(path: TreePath) requires @@ -103,108 +281,29 @@ pub proof fn lemma_vaddr_strict_bound(path: TreePath) ensures vaddr(path) < 0x1_0000_0000_0000int, { - vstd::arithmetic::power2::lemma2_to64(); - vstd::arithmetic::power2::lemma2_to64_rest(); - if path.len() == 0 { - } else if path.len() == 1 { - let i0 = path[0]; - assert(rec_vaddr(path, 1) == 0); - } else if path.len() == 2 { - let i0 = path[0]; - let i1 = path[1]; - assert(rec_vaddr(path, 2) == 0); - assert(rec_vaddr(path, 1) == vaddr_make::(1, i1 as usize) as usize); - } else if path.len() == 3 { - let i0 = path[0]; - let i1 = path[1]; - let i2 = path[2]; - assert(rec_vaddr(path, 3) == 0); - assert(rec_vaddr(path, 2) == vaddr_make::(2, i2 as usize) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize)) as usize); - } else { - let i0 = path[0]; - let i1 = path[1]; - let i2 = path[2]; - let i3 = path[3]; - assert(rec_vaddr(path, 4) == 0); - assert(rec_vaddr(path, 3) == vaddr_make::(3, i3 as usize) as usize); - assert(rec_vaddr(path, 2) == (vaddr_make::(2, i2 as usize) + vaddr_make::< - NR_LEVELS, - >(3, i3 as usize)) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize) + vaddr_make::(3, i3 as usize)) as usize); - assert(rec_vaddr(path, 0) == (vaddr_make::(0, i0 as usize) + vaddr_make::< - NR_LEVELS, - >(1, i1 as usize) + vaddr_make::(2, i2 as usize) + vaddr_make::( - 3, - i3 as usize, - )) as usize); - assert(0x80_0000_0000usize * i0 + 0x4000_0000usize * i1 + 0x20_0000usize * i2 + 0x1000usize - * i3 < 0x1_0000_0000_0000int) by (nonlinear_arith) - requires - i0 < 512, - i1 < 512, - i2 < 512, - i3 < 512, - ; - } + lemma_rec_vaddr_suffix_bound(path, 0); + lemma_page_size_ge_page_size((INC_LEVELS - path.len()) as PagingLevel); + lemma_page_size_spec_values(); } -/// The VA of any path is within the `2^39`-sized cell of its top-level index: -/// `path[0] * 2^39 <= vaddr(path)` and `vaddr(path) + page_size <= (path[0]+1) * 2^39`. -/// Pure VA arithmetic (x86 4-level paging). Used by `view_rec_top_index_va_bound`. +/// The VA of any path is within the cell selected by its top-level index. pub proof fn lemma_vaddr_top_index_cell(path: TreePath) requires path.inv(), 1 <= path.len() <= INC_LEVELS - 1, ensures - (path[0]) * 0x80_0000_0000int <= vaddr(path), + (path[0]) * page_size(NR_LEVELS as PagingLevel) <= vaddr(path), vaddr(path) + page_size((INC_LEVELS - path.len()) as PagingLevel) <= (path[0] + 1) - * 0x80_0000_0000int, + * page_size(NR_LEVELS as PagingLevel), { broadcast use TreePath::lemma_index_satisfies_elem_inv; + lemma_rec_vaddr_suffix_bound(path, 1); + lemma_rec_vaddr_unfold(path, 0); + lemma_nr_entries_times_sub_page_size(INC_LEVELS as PagingLevel); lemma_page_size_spec_values(); - vstd::arithmetic::power2::lemma2_to64(); - vstd::arithmetic::power2::lemma2_to64_rest(); let i0 = path[0]; - if path.len() == 1 { - assert(rec_vaddr(path, 1) == 0); - } else if path.len() == 2 { - let i1 = path[1]; - assert(rec_vaddr(path, 2) == 0); - assert(rec_vaddr(path, 1) == vaddr_make::(1, i1 as usize) as usize); - } else if path.len() == 3 { - let i1 = path[1]; - let i2 = path[2]; - assert(rec_vaddr(path, 3) == 0); - assert(rec_vaddr(path, 2) == vaddr_make::(2, i2 as usize) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize)) as usize); - } else { - let i1 = path[1]; - let i2 = path[2]; - let i3 = path[3]; - assert(rec_vaddr(path, 4) == 0); - assert(rec_vaddr(path, 3) == vaddr_make::(3, i3 as usize) as usize); - assert(rec_vaddr(path, 2) == (vaddr_make::(2, i2 as usize) + vaddr_make::< - NR_LEVELS, - >(3, i3 as usize)) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize) + vaddr_make::(3, i3 as usize)) as usize); - assert(0x80_0000_0000int * i0 + 0x4000_0000int * i1 + 0x20_0000int * i2 + 0x1000int * i3 - + 0x1000int <= (i0 + 1) * 0x80_0000_0000int) by (nonlinear_arith) - requires - i1 < 512, - i2 < 512, - i3 < 512, - ; - } + lemma_vaddr_make_eq_page_size(0, i0); } /// `vaddr_of::(path)` in `int` equals the unconditional sum — no usize @@ -782,8 +881,7 @@ impl PageTableOwner { ).view_rec(path.push_tail(i)).contains(m) } - /// Closed-form for `vaddr(path.push_tail(i))` by case-split on `path.len() ∈ {0,1,2,3}`. - #[verifier::rlimit(200)] + /// Appending an index advances the VA by the entry size at that depth. pub proof fn lemma_vaddr_push_tail_eq(path: TreePath, i: int) requires path.inv(), @@ -802,62 +900,14 @@ impl PageTableOwner { TreePath::lemma_index_satisfies_elem_inv, }; - lemma_page_size_spec_values(); - lemma_vaddr_strict_bound(path); - vstd::arithmetic::power2::lemma2_to64(); - vstd::arithmetic::power2::lemma2_to64_rest(); let pt = path.push_tail(i); - if path.len() >= 1 { - } - if path.len() == 0 { - assert(rec_vaddr(pt, 1) == 0); - assert(vaddr_make::(0, i as usize) == 0x80_0000_0000usize * i) by (compute); - assert(0x80_0000_0000usize * (i + 1) <= usize::MAX) by (nonlinear_arith) - requires - i < 512, - ; - } else if path.len() == 1 { - let i0 = path[0]; - assert(vaddr_make::(0, i0 as usize) == 0x80_0000_0000usize * i0); - assert(rec_vaddr(pt, 2) == 0); - } else if path.len() == 2 { - let i0 = path[0]; - let i1 = path[1]; - assert(rec_vaddr(path, 2) == 0); - assert(rec_vaddr(path, 1) == vaddr_make::(1, i1 as usize) as usize); - assert(rec_vaddr(pt, 3) == 0); - assert(rec_vaddr(pt, 2) == vaddr_make::(2, i as usize) as usize); - assert(rec_vaddr(pt, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i as usize)) as usize); - } else { - let i0 = path[0]; - let i1 = path[1]; - let i2 = path[2]; - assert(rec_vaddr(path, 3) == 0); - assert(rec_vaddr(path, 2) == vaddr_make::(2, i2 as usize) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize)) as usize); - assert(vaddr_make::(1, i1 as usize) == 0x4000_0000usize * i1) by (compute); - assert(rec_vaddr(pt, 4) == 0); - assert(rec_vaddr(pt, 3) == vaddr_make::(3, i as usize) as usize); - assert(rec_vaddr(pt, 2) == (vaddr_make::(2, i2 as usize) + vaddr_make::< - NR_LEVELS, - >(3, i as usize)) as usize); - assert(rec_vaddr(pt, 1) == (vaddr_make::(1, i1 as usize) + vaddr_make::< - NR_LEVELS, - >(2, i2 as usize) + vaddr_make::(3, i as usize)) as usize); - assert(vaddr_make::(3, i as usize) == 0x1000usize * i) by (compute); - assert(0x80_0000_0000usize * i0 + 0x4000_0000usize * i1 + 0x20_0000usize * i2 - + 0x1000usize * (i + 1) <= usize::MAX) by (nonlinear_arith) - requires - i0 < 512, - i1 < 512, - i2 < 512, - i < 512, - ; - } + lemma_rec_vaddr_push_tail(path, i, 0); + lemma_rec_vaddr_suffix_bound(pt, 0); + let entry_size = page_size((INC_LEVELS - path.len() - 1) as PagingLevel) as int; + assert(vaddr(path) + (i + 1) * entry_size == vaddr(pt) + entry_size) by (nonlinear_arith) + requires + vaddr(pt) == vaddr(path) + i * entry_size, + ; } pub proof fn view_rec_vaddr_range(self, path: TreePath, m: Mapping) @@ -906,9 +956,9 @@ impl PageTableOwner { Self::lemma_vaddr_push_tail_eq(path, i); let child_ps = page_size((INC_LEVELS - path.len() - 1) as PagingLevel) as int; - assert((i + 1) * child_ps <= 512 * child_ps) by (nonlinear_arith) + assert((i + 1) * child_ps <= NR_ENTRIES * child_ps) by (nonlinear_arith) requires - 0 <= i < 512, + 0 <= i < NR_ENTRIES, child_ps >= 0, ; lemma_vaddr_of_eq_int::(path.push_tail(i)); @@ -940,6 +990,7 @@ impl PageTableOwner { self.view_rec_vaddr_range(path, m); lemma_vaddr_of_eq_int::(path); lemma_vaddr_top_index_cell(path); + lemma_page_size_spec_values(); // `vaddr_of(path) == vaddr(path) + LEADING_BITS*2^48` (lemma_vaddr_of_eq_int); // the positional `vaddr(path)` lies in the top-index cell // `[index(0)*2^39, (index(0)+1)*2^39)` (lemma_vaddr_top_index_cell), and @@ -1167,8 +1218,8 @@ impl PageTableOwner { /// every `vaddr(path)` is aligned to `page_size(INC_LEVELS - path.len())` /// and `vaddr(path) + page_size(...)` cannot overflow usize. /// - /// Proved by case analysis on `path.len() ∈ {0, 1, 2, 3, 4}`, unrolling - /// `rec_vaddr` and using concrete `pow2` values. + /// Follows from the divisibility of adjacent architecture-defined page + /// sizes and the suffix bound used by the VA construction. #[verifier::rlimit(200)] proof fn lemma_vaddr_path_alignment_and_bound(path: TreePath) requires @@ -1179,88 +1230,8 @@ impl PageTableOwner { vaddr(path) % page_size((INC_LEVELS - path.len()) as PagingLevel) == 0, vaddr(path) + page_size((INC_LEVELS - path.len()) as PagingLevel) <= usize::MAX, { - lemma_page_size_spec_values(); - vstd::arithmetic::power2::lemma2_to64(); - vstd::arithmetic::power2::lemma2_to64_rest(); - broadcast use TreePath::lemma_index_satisfies_elem_inv; - // NR_LEVELS = 4; each index is < 512. - // rec_vaddr values per path.len(): - // 0: 0 - // 1: i0 * 2^39 - // 2: i0 * 2^39 + i1 * 2^30 - // 3: i0 * 2^39 + i1 * 2^30 + i2 * 2^21 - // 4: i0 * 2^39 + i1 * 2^30 + i2 * 2^21 + i3 * 2^12 - // page_size(INC_LEVELS - path.len()) per path.len(): - // 1: 2^39, 2: 2^30, 3: 2^21, 4: 2^12 - // In each case every term is a multiple of the smallest (= page_size). - - if path.len() == 0 { - assert(rec_vaddr(path, 0) == 0); - } else if path.len() == 1 { - let i0 = path[0]; - assert(rec_vaddr(path, 1) == 0); - assert(rec_vaddr(path, 0) == (vaddr_make::(0, i0 as usize) + rec_vaddr( - path, - 1, - )) as usize); - } else if path.len() == 2 { - let i0 = path[0]; - let i1 = path[1]; - assert(rec_vaddr(path, 2) == 0); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + rec_vaddr( - path, - 2, - )) as usize); - let s = 0x80_0000_0000usize * i0 + 0x4000_0000usize * i1; - } else if path.len() == 3 { - let i0 = path[0]; - let i1 = path[1]; - let i2 = path[2]; - assert(rec_vaddr(path, 3) == 0); - assert(rec_vaddr(path, 2) == (vaddr_make::(2, i2 as usize) + rec_vaddr( - path, - 3, - )) as usize); - assert(rec_vaddr(path, 0) == (vaddr_make::(0, i0 as usize) + rec_vaddr( - path, - 1, - )) as usize); - let s = 0x80_0000_0000usize * i0 + 0x4000_0000usize * i1 + 0x20_0000usize * i2; - assert(rec_vaddr(path, 0) == s); - assert(s % 0x20_0000 == 0) by (nonlinear_arith) - requires - s == 0x80_0000_0000 * i0 + 0x4000_0000 * i1 + 0x20_0000 * i2, - ; - } else { - assert(path.len() == 4); - let i0 = path[0]; - let i1 = path[1]; - let i2 = path[2]; - let i3 = path[3]; - assert(rec_vaddr(path, 4) == 0); - assert(rec_vaddr(path, 3) == (vaddr_make::(3, i3 as usize) + rec_vaddr( - path, - 4, - )) as usize); - assert(rec_vaddr(path, 1) == (vaddr_make::(1, i1 as usize) + rec_vaddr( - path, - 2, - )) as usize); - assert(rec_vaddr(path, 0) == (vaddr_make::(0, i0 as usize) + rec_vaddr( - path, - 1, - )) as usize); - let s = (0x80_0000_0000usize * i0 + 0x4000_0000usize * i1 + 0x20_0000usize * i2 - + 0x1000usize * i3) as int; - assert(s + 0x1000 <= usize::MAX) by (nonlinear_arith) - requires - s == 0x80_0000_0000 * i0 + 0x4000_0000 * i1 + 0x20_0000 * i2 + 0x1000 * i3, - i0 < 512, - i1 < 512, - i2 < 512, - i3 < 512, - ; - } + lemma_rec_vaddr_aligned(path, 0); + lemma_rec_vaddr_suffix_bound(path, 0); } /// Every mapping in `view_rec` satisfies `Mapping::inv()`. @@ -1309,32 +1280,23 @@ impl PageTableOwner { lemma_vaddr_strict_bound(path); let lb = C::LEADING_BITS_spec() as int; vstd::arithmetic::power2::lemma2_to64_rest(); - // (A) Alignment. For `ps ∈ {2^12, 2^21, 2^30}`, `ps | 2^48`, so - // `lb * 2^48 % ps == 0` and `vaddr(path) % ps == 0` gives + let limit = page_size(INC_LEVELS as PagingLevel) as int; + lemma_page_size_divides(pt_level, INC_LEVELS as PagingLevel); + // (A) Alignment. The mapped page size divides the full paging + // address-space size, so the leading-bit offset is aligned. // `vaddr_of(path) % ps == 0` via `lemma_mod_adds`. - assert(lb * 0x1_0000_0000_0000int % ps == 0) by (nonlinear_arith) - requires - lb >= 0, - (ps == 0x1000int || ps == 0x20_0000int || ps == 0x4000_0000int), - ; - vstd::arithmetic::div_mod::lemma_mod_adds( - vaddr(path) as int, - lb * 0x1_0000_0000_0000int, - ps, - ); + assert(limit % ps == 0); + vstd::arithmetic::div_mod::lemma_fundamental_div_mod(limit, ps); + let limit_ratio = limit / ps; + assert(limit == limit_ratio * ps); + vstd::arithmetic::mul::lemma_mul_is_associative(lb, limit_ratio, ps); + vstd::arithmetic::div_mod::lemma_mod_multiples_basic(lb * limit_ratio, ps); + assert(lb * limit == (lb * limit_ratio) * ps); + assert((lb * limit) % ps == 0); + vstd::arithmetic::div_mod::lemma_mod_adds(vaddr(path) as int, lb * limit, ps); // (B) Overflow: `vaddr_of(path) + ps <= 2^64`. // `vaddr(path) + ps <= 2^48`: from strict bound plus alignment. let v = vaddr(path) as int; - let limit = 0x1_0000_0000_0000int; - assert(limit % ps == 0) by { - if ps == 0x1000int { - assert(0x1_0000_0000_0000int % 0x1000int == 0) by (compute_only); - } else if ps == 0x20_0000int { - assert(0x1_0000_0000_0000int % 0x20_0000int == 0) by (compute_only); - } else { - assert(0x1_0000_0000_0000int % 0x4000_0000int == 0) by (compute_only); - } - }; vstd::arithmetic::div_mod::lemma_mod_equivalence(limit, v, ps); let diff = limit - v; let q = diff / ps; @@ -1346,7 +1308,7 @@ impl PageTableOwner { diff == ps * q, ; vstd::arithmetic::mul::lemma_mul_inequality(1, q, ps); - vstd::arithmetic::mul::lemma_mul_inequality(lb, 0xffffint, 0x1_0000_0000_0000int); + vstd::arithmetic::mul::lemma_mul_inequality(lb, 0xffffint, limit); assert(v + ps <= limit) by (nonlinear_arith) requires q >= 1, @@ -1354,8 +1316,7 @@ impl PageTableOwner { diff == ps * q, diff == limit - v, ; - assert(0xffffint * 0x1_0000_0000_0000int + 0x1_0000_0000_0000int - == 0x1_0000_0000_0000_0000int) by (compute_only); + assert(0xffffint * limit + limit == 0x1_0000_0000_0000_0000int); assert(lb * limit + v + ps <= 0x1_0000_0000_0000_0000int); vstd_extra::arithmetic::lemma_mod_0_add(m.va_range.start, ps, ps); assert(set![4096, 2097152, 1073741824].contains(m.page_size)); diff --git a/ostd/specs/mod.rs b/ostd/specs/mod.rs index bc8234611..f316bf901 100644 --- a/ostd/specs/mod.rs +++ b/ostd/specs/mod.rs @@ -3,7 +3,6 @@ #[allow(unused_braces)] #[allow(rustdoc::invalid_rust_codeblocks)] #[allow(rustdoc::invalid_html_tags)] -#[path = "arch/x86/mod.rs"] pub mod arch; #[allow(unused_parens)] #[allow(unused_braces)] diff --git a/ostd/src/arch/x86/mm/mod.rs b/ostd/src/arch/x86/mm/mod.rs index 4a2bc573d..d30b383cf 100644 --- a/ostd/src/arch/x86/mm/mod.rs +++ b/ostd/src/arch/x86/mm/mod.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 #![expect(dead_code)] -use crate::specs::arch::{MAX_PADDR, NR_ENTRIES, NR_LEVELS}; use vstd::arithmetic::power2::*; use vstd::prelude::*; use vstd_extra::panic::may_panic; @@ -15,12 +14,18 @@ use core::ops::Range; pub(crate) use util::{__memcpy_fallible, __memset_fallible}; //use x86_64::{instructions::tlb, structures::paging::PhysFrame, VirtAddr}; -use crate::specs::arch::PAGE_SIZE; +macro_rules! x86_base_page_size { + () => { + 4096usize + }; +} +pub(crate) use x86_base_page_size; + use crate::{ mm::{ page_prop::{CachePolicy, PageFlags, PageProperty, PrivilegedPageFlags as PrivFlags}, page_table::{PageTableEntryTrait, PageTableFrag}, - Paddr, PagingConstsTrait, PagingLevel, PodOnce, Vaddr, + CurrentPagingConstsTrait, Paddr, PagingConstsTrait, PagingLevel, PodOnce, Vaddr, MAX_PADDR, }, Pod, }; @@ -28,6 +33,28 @@ use crate::{ mod util; verus! { + +/// Size of a base page on x86-64. +pub const PAGE_SIZE: usize = x86_base_page_size!(); + +/// Size of an x86-64 page-table entry. +pub const PTE_SIZE: usize = 8; + +/// Number of entries in an x86-64 page-table node. +pub const NR_ENTRIES: usize = 512; + +/// Number of translation levels used by the current x86-64 configuration. +pub const NR_LEVELS: usize = 4; + +/// Width of canonical virtual addresses used by the current configuration. +pub const ADDRESS_WIDTH: usize = 48; + +/// Highest level at which a PTE may directly map a page. +pub const HIGHEST_TRANSLATION_LEVEL: PagingLevel = 2; + +/// Whether virtual addresses use sign extension. +pub const VA_SIGN_EXT: bool = true; + #[verifier::allow(autoderive_clone_without_spec)] #[derive(Clone, Debug, Default)] pub struct PagingConsts {} @@ -36,79 +63,83 @@ impl PagingConstsTrait for PagingConsts { // Expansion for BASE_PAGE_SIZE #[verifier::inline] open spec fn BASE_PAGE_SIZE_spec() -> usize { - 4096 + PAGE_SIZE } #[inline(always)] fn BASE_PAGE_SIZE() -> usize { - 4096 + PAGE_SIZE } // Expansion for NR_LEVELS #[verifier::inline] open spec fn NR_LEVELS_spec() -> PagingLevel { - 4 + NR_LEVELS as PagingLevel } #[inline(always)] fn NR_LEVELS() -> PagingLevel { - 4 + NR_LEVELS as PagingLevel } // Expansion for ADDRESS_WIDTH #[verifier::inline] open spec fn ADDRESS_WIDTH_spec() -> usize { - 48 + ADDRESS_WIDTH } #[inline(always)] fn ADDRESS_WIDTH() -> usize { - 48 + ADDRESS_WIDTH } // Expansion for HIGHEST_TRANSLATION_LEVEL #[verifier::inline] open spec fn HIGHEST_TRANSLATION_LEVEL_spec() -> PagingLevel { - 2 + HIGHEST_TRANSLATION_LEVEL } #[inline(always)] fn HIGHEST_TRANSLATION_LEVEL() -> PagingLevel { - 2 + HIGHEST_TRANSLATION_LEVEL } #[verifier::inline] open spec fn VA_SIGN_EXT_spec() -> bool { - true + VA_SIGN_EXT } #[inline(always)] fn VA_SIGN_EXT() -> bool { - true + VA_SIGN_EXT } // Expansion for PTE_SIZE #[verifier::inline] open spec fn PTE_SIZE_spec() -> usize { - 8 + PTE_SIZE } #[inline(always)] fn PTE_SIZE() -> (res: usize) { - 8 + PTE_SIZE } proof fn lemma_paging_consts_requirements() { + assert(Self::BASE_PAGE_SIZE() == PAGE_SIZE) by (compute_only); + assert(Self::NR_LEVELS() == NR_LEVELS as PagingLevel) by (compute_only); + assert(Self::PTE_SIZE() == PTE_SIZE) by (compute_only); + assert(Self::ADDRESS_WIDTH() == ADDRESS_WIDTH) by (compute_only); lemma_pow2_is_pow2_to64(); lemma2_to64(); lemma2_to64_rest(); - assert(usize::BITS == 64) by (compute); + vstd::layout::unsigned_int_max_values(); lemma_usize_pow2_ilog2(12); lemma_usize_pow2_ilog2(9); @@ -116,6 +147,15 @@ impl PagingConstsTrait for PagingConsts { } } +impl CurrentPagingConstsTrait for PagingConsts { + proof fn lemma_current_paging_consts_requirements() { + Self::lemma_paging_consts_requirements(); + assert(Self::BASE_PAGE_SIZE() == PAGE_SIZE) by (compute_only); + assert(Self::NR_LEVELS() == NR_LEVELS as PagingLevel) by (compute_only); + assert(Self::BASE_PAGE_SIZE() / Self::PTE_SIZE() == NR_ENTRIES) by (compute_only); + } +} + pub proof fn lemma_nr_subpage_per_huge_eq_nr_entries() ensures crate::mm::nr_subpage_per_huge::() == NR_ENTRIES, @@ -392,7 +432,6 @@ impl PageTableEntryTrait for PageTableEntry { fn paddr(&self) -> Paddr { proof { self.lemma_paddr_is_page_aligned(); - assume(self.0 & Self::PHYS_ADDR_MASK < MAX_PADDR); } self.0 & Self::PHYS_ADDR_MASK } diff --git a/ostd/src/mm/kspace/mod.rs b/ostd/src/mm/kspace/mod.rs index 4ecb4cfd4..9d792ae2d 100644 --- a/ostd/src/mm/kspace/mod.rs +++ b/ostd/src/mm/kspace/mod.rs @@ -44,7 +44,7 @@ pub(crate) mod kvirt_area; mod test; use super::{ - Paddr, PagingConstsTrait, Vaddr, + CurrentPagingConstsTrait, Paddr, PagingConstsTrait, Vaddr, frame::{ Frame, Segment, meta::{AnyFrameMeta, MetaPageMeta, MetaSlot, mapping}, @@ -135,7 +135,7 @@ pub const LINEAR_MAPPING_VADDR_RANGE: Range = LINEAR_MAPPING_BASE_VADDR.. /// Convert physical address to virtual address using offset, only available inside `ostd` pub open spec fn paddr_to_vaddr_spec(pa: Paddr) -> usize { - (pa + LINEAR_MAPPING_BASE_VADDR) as usize + model::paddr_to_vaddr_for::(pa) } #[verifier::when_used_as_spec(paddr_to_vaddr_spec)] @@ -177,6 +177,7 @@ unsafe impl PageTableConfig for KernelPtConfig { use crate::mm::nr_subpage_per_huge; use vstd::arithmetic::power2::{lemma2_to64, lemma2_to64_rest, lemma_pow2_adds, pow2}; Self::C::lemma_paging_consts_properties(); + Self::C::lemma_current_paging_consts_requirements(); PageTableEntry::lemma_layout(); lemma2_to64(); lemma2_to64_rest(); diff --git a/ostd/src/mm/mod.rs b/ostd/src/mm/mod.rs index d60bf1a5b..c1721d070 100644 --- a/ostd/src/mm/mod.rs +++ b/ostd/src/mm/mod.rs @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MPL-2.0 //! Virtual memory (VM). -use crate::specs::arch::*; use vstd::arithmetic::div_mod::group_div_basics; use vstd::arithmetic::power2::*; use vstd::prelude::*; @@ -46,6 +45,7 @@ pub(crate) use self::{ kspace::paddr_to_vaddr, page_prop::PrivilegedPageFlags, page_table::PageTable, }; pub(crate) use crate::arch::mm::PagingConsts; +pub use crate::arch::mm::{NR_ENTRIES, NR_LEVELS, PAGE_SIZE}; // Re-export largest_pages from page_table pub(crate) use page_table::largest_pages; @@ -55,6 +55,14 @@ pub type PagingLevel = u8; verus! { +/// Current verification upper bound for tracked physical addresses. +/// +/// This is a memory-model bound, not the architectural physical-address width. +pub const MAX_PADDR: Paddr = 0x8000_0000; + +/// Maximum number of base-page frames represented by the current memory model. +pub const MAX_NR_PAGES: u64 = (MAX_PADDR / PAGE_SIZE) as u64; + /// A minimal set of constants that determines the paging system. /// This provides an abstraction over most paging modes in common architectures. pub trait PagingConstsTrait: Clone + Debug + Send + Sync + 'static { @@ -134,17 +142,6 @@ pub trait PagingConstsTrait: Clone + Debug + Send + Sync + 'static { /// NOTE: The postcondition is designed to be minimal, to actually be used in proofs, call `lemma_paging_consts_properties` /// instead to get all the properties that are derived from the requirements. /// - /// FIXME: General architecture support. - /// All configs in vostd use the same value for the per-config - /// `NR_LEVELS()` as the architecture-level constant `NR_LEVELS` - /// (= 4 for x86_64). This is *implicit* in the cursor framework: - /// `CursorOwner::inv()` hardcodes `self.level <= NR_LEVELS` (const) - /// for cursors over any `C: PagingConstsTrait`, so a config whose - /// `NR_LEVELS_spec()` exceeded `NR_LEVELS` would be unusable. This - /// lemma exposes that equality as a usable fact so generic proofs - /// can chain `level != C::NR_LEVELS_spec()` to `level < NR_LEVELS` - /// (e.g. `Cursor::find_next_impl`'s PageTable-branch gate ⟹ - /// `CursorMut::take_next`'s `replace_cur_entry` discharge). proof fn lemma_paging_consts_requirements() ensures 0 < Self::BASE_PAGE_SIZE(), @@ -156,12 +153,6 @@ pub trait PagingConstsTrait: Clone + Debug + Send + Sync + 'static { Self::BASE_PAGE_SIZE().ilog2() + (Self::BASE_PAGE_SIZE() / Self::PTE_SIZE()).ilog2() * Self::NR_LEVELS() <= Self::ADDRESS_WIDTH(), Self::PTE_SIZE() == core::mem::size_of::(), - // The following statement holds for all architectures, - // but the actual value of the constants may vary. - // Maybe we can remove this requirement. - Self::BASE_PAGE_SIZE() == PAGE_SIZE, - Self::NR_LEVELS() == NR_LEVELS, - Self::BASE_PAGE_SIZE() / Self::PTE_SIZE() == NR_ENTRIES, ; /// The derived properties of the paging constants. @@ -174,7 +165,6 @@ pub trait PagingConstsTrait: Clone + Debug + Send + Sync + 'static { Self::BASE_PAGE_SIZE().ilog2() + (Self::BASE_PAGE_SIZE() / Self::PTE_SIZE()).ilog2() * ( Self::NR_LEVELS() - 1) <= Self::ADDRESS_WIDTH(), 0 < Self::BASE_PAGE_SIZE() / Self::PTE_SIZE() <= Self::BASE_PAGE_SIZE(), - NR_ENTRIES * Self::PTE_SIZE() == PAGE_SIZE, // Copied from the postcondition of `lemma_paging_consts_requirements` // so that we only need to call this lemma in proofs. 0 < Self::BASE_PAGE_SIZE(), @@ -186,25 +176,56 @@ pub trait PagingConstsTrait: Clone + Debug + Send + Sync + 'static { Self::BASE_PAGE_SIZE().ilog2() + (Self::BASE_PAGE_SIZE() / Self::PTE_SIZE()).ilog2() * Self::NR_LEVELS() <= Self::ADDRESS_WIDTH(), Self::PTE_SIZE() == core::mem::size_of::(), - // The following statement holds for all architectures, - // but the actual value of the constants may vary. - // Maybe we can remove this requirement. - Self::BASE_PAGE_SIZE() == PAGE_SIZE, - Self::NR_LEVELS() == NR_LEVELS, - Self::BASE_PAGE_SIZE() / Self::PTE_SIZE() == NR_ENTRIES, { Self::lemma_paging_consts_requirements(); broadcast use group_div_basics; + let base = Self::BASE_PAGE_SIZE() as int; + let pte = Self::PTE_SIZE() as int; + let levels = Self::NR_LEVELS() as int; + let base_bits = Self::BASE_PAGE_SIZE().ilog2() as int; + let index_bits = (Self::BASE_PAGE_SIZE() / Self::PTE_SIZE()).ilog2() as int; + assert(0 < base / pte) by { + vstd::arithmetic::div_mod::lemma_div_non_zero(base, pte); + }; + assert(base / pte <= base) by { + vstd::arithmetic::div_mod::lemma_div_is_ordered(0, base, pte); + }; + assert(base_bits + index_bits * (levels - 1) <= base_bits + index_bits * levels) + by (nonlinear_arith) + requires + 0 <= index_bits, + 1 <= levels, + ; } } -pub open spec fn page_size_spec(level: PagingLevel) -> usize { - (PAGE_SIZE * pow2( - (nr_subpage_per_huge::().ilog2() * (level - 1)) as nat, +/// Bridge between a paging configuration and the build-selected architecture. +/// +/// This is intentionally separate from [`PagingConstsTrait`]. Public paging +/// types still use build-selected constants in const-generic positions, while +/// generic paging specifications can range over any [`PagingConstsTrait`]. +pub trait CurrentPagingConstsTrait: PagingConstsTrait { + proof fn lemma_current_paging_consts_requirements() + ensures + Self::BASE_PAGE_SIZE() == PAGE_SIZE, + Self::NR_LEVELS() == NR_LEVELS as PagingLevel, + Self::BASE_PAGE_SIZE() / Self::PTE_SIZE() == NR_ENTRIES, + ; +} + +/// The page-size formula for an explicit paging configuration. +pub open spec fn page_size_for_spec(level: PagingLevel) -> usize { + (C::BASE_PAGE_SIZE_spec() * pow2( + (nr_subpage_per_huge::().ilog2() * (level - 1)) as nat, )) as usize } +/// The page-size formula for the architecture selected by this build. +pub open spec fn page_size_spec(level: PagingLevel) -> usize { + page_size_for_spec::(level) +} + // /// The page size // pub const PAGE_SIZE: usize = page_size::(1); /// The page size at a given level. @@ -242,7 +263,7 @@ pub fn page_size(level: PagingLevel) -> (ret: usize) #[verifier::inline] pub open spec fn nr_subpage_per_huge_spec() -> usize { - C::BASE_PAGE_SIZE() / C::PTE_SIZE() + C::BASE_PAGE_SIZE_spec() / C::PTE_SIZE_spec() } /// The number of sub pages in a huge page. diff --git a/ostd/src/mm/page_table/cursor/mod.rs b/ostd/src/mm/page_table/cursor/mod.rs index 3d9f62a4c..d5ab8e4d0 100644 --- a/ostd/src/mm/page_table/cursor/mod.rs +++ b/ostd/src/mm/page_table/cursor/mod.rs @@ -63,8 +63,9 @@ use crate::{ }; use super::{ - Child, ChildRef, Entry, EntryOwner, FrameView, PageTable, PageTableConfig, PageTableError, - PageTableGuard, PageTablePageMeta, PagingConstsTrait, PagingLevel, pte_index, + Child, ChildRef, CurrentPagingConstsTrait, Entry, EntryOwner, FrameView, PageTable, + PageTableConfig, PageTableError, PageTableGuard, PageTablePageMeta, PagingConstsTrait, + PagingLevel, pte_index, }; verus! { @@ -1026,6 +1027,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { } if !C::TOP_LEVEL_CAN_UNMAP_spec() { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); assert(self.level < NR_LEVELS); } } @@ -3494,34 +3496,11 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { } return None; } - let tracked mut absent_entry_owner = EntryOwner::tracked_new_absent( - owner.cur_entry_owner().path, - owner.level, - ); - let ghost subtree_level = (owner.continuations[owner.level - 1].tree_level + 1) as nat; - assert(absent_entry_owner.inv()) by { - reveal(::inv); - }; - assert(subtree_level < INC_LEVELS) by { - reveal(::inv); - }; - let tracked subtree = OwnerSubtree::tracked_new_val(absent_entry_owner, subtree_level); + let tracked subtree = owner.tracked_new_absent_subtree(); proof { owner.absent_not_in_tree(subtree.value()); } - assert(subtree.value().path.len() <= INC_LEVELS - 1) by { - reveal(::inv); - }; - assert(subtree.value().parent_level == owner.continuations[owner.level - - 1].child().value().parent_level) by { - reveal(::inv); - }; - assert(subtree.value().path == owner.continuations[owner.level - 1].path().push_tail( - owner.continuations[owner.level - 1].idx as int, - )) by { - reveal(::inv); - }; let ghost owner_before_replace = *owner; let ghost regions_before_replace = *regions; diff --git a/ostd/src/mm/page_table/mod.rs b/ostd/src/mm/page_table/mod.rs index cfe44c1df..504353937 100644 --- a/ostd/src/mm/page_table/mod.rs +++ b/ostd/src/mm/page_table/mod.rs @@ -29,7 +29,7 @@ use core::{ }; use super::{ - Paddr, PagingConstsTrait, PagingLevel, PodOnce, Vaddr, + CurrentPagingConstsTrait, Paddr, PagingConstsTrait, PagingLevel, PodOnce, Vaddr, kspace::KernelPtConfig, nr_subpage_per_huge, page_prop::{CachePolicy, PageProperty}, @@ -181,7 +181,7 @@ pub unsafe trait PageTableConfig: Clone + Debug + Send + Sync + 'static { type E: PageTableEntryTrait; /// The paging constants. - type C: PagingConstsTrait; + type C: CurrentPagingConstsTrait; /// The item that can be mapped into the virtual memory space using the /// page table. @@ -499,6 +499,7 @@ pub unsafe trait PageTableConfig: Clone + Debug + Send + Sync + 'static { ) == NR_ENTRIES, { Self::C::lemma_paging_consts_properties(); + Self::C::lemma_current_paging_consts_requirements(); Self::lemma_page_table_config_constant_requirements(); } } @@ -562,6 +563,12 @@ impl PagingConstsTrait for C { } } +impl CurrentPagingConstsTrait for C { + proof fn lemma_current_paging_consts_requirements() { + C::C::lemma_current_paging_consts_requirements(); + } +} + /// Splits the address range into largest page table items. /// /// Each of the returned items is a tuple of the physical address and the @@ -626,6 +633,7 @@ fn top_level_index_width() -> (ret: usize) { proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); C::lemma_page_table_config_constant_properties(); } @@ -640,6 +648,7 @@ fn pt_va_range_start() -> (ret: Vaddr) { proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); let ghost idx_start = C::TOP_LEVEL_INDEX_RANGE().start; let ghost offset = pte_index_bit_offset_spec::(C::NR_LEVELS()); crate::specs::mm::page_table::vaddr_range_proofs::lemma_pt_va_range_start_shift_facts::( @@ -666,6 +675,7 @@ fn pt_va_range_end() -> (ret: Vaddr) let idx_end = C::TOP_LEVEL_INDEX_RANGE().end; proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); } let offset = pte_index_bit_offset::(C::NR_LEVELS()); @@ -818,7 +828,7 @@ fn nr_pte_index_bits() -> usize } /// The index of a VA's PTE in a page table node at the given level. -fn pte_index(va: Vaddr, level: PagingLevel) -> (res: usize) +fn pte_index(va: Vaddr, level: PagingLevel) -> (res: usize) requires 1 <= level <= NR_LEVELS, ensures @@ -827,6 +837,7 @@ fn pte_index(va: Vaddr, level: PagingLevel) -> (res: usize proof { let offset = pte_index_bit_offset_spec::(level); C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); lemma_arch_specific_consts_properties::(); assert(0 <= offset < usize::BITS) by (nonlinear_arith) requires @@ -850,7 +861,7 @@ fn pte_index(va: Vaddr, level: PagingLevel) -> (res: usize /// This function returns the bit offset of the least significant bit. Take /// x86-64 as an example, the `pte_index_bit_offset(2)` should return 21, which /// is 12 (the 4KiB in-page offset) plus 9 (index width in the level-1 table). -fn pte_index_bit_offset(level: PagingLevel) -> usize +fn pte_index_bit_offset(level: PagingLevel) -> usize requires 1 <= level <= NR_LEVELS, returns @@ -858,6 +869,7 @@ fn pte_index_bit_offset(level: PagingLevel) -> usize { proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); lemma_arch_specific_consts_properties::(); assert(12 + 9 * (level - 1) <= 39) by (nonlinear_arith) requires @@ -1669,12 +1681,16 @@ pub trait PageTableEntryTrait: /// The physical address recorded in the PTE is either: /// - the physical address of the next-level page table, or /// - the physical address of the page that the PTE maps to. + /// + /// This getter only guarantees page alignment. `paddr < MAX_PADDR` is an + /// obligation of well-formed owned PTEs, not of an arbitrary encoded PTE + /// word. spec fn paddr_spec(&self) -> Paddr; #[verifier::when_used_as_spec(paddr_spec)] fn paddr(&self) -> (res: Paddr) ensures - valid_frame_paddr(res), + res % PAGE_SIZE == 0, returns self.paddr(), ; diff --git a/ostd/src/mm/page_table/node/entry.rs b/ostd/src/mm/page_table/node/entry.rs index 8ed0dc86a..0fad3a06d 100644 --- a/ostd/src/mm/page_table/node/entry.rs +++ b/ostd/src/mm/page_table/node/entry.rs @@ -12,7 +12,7 @@ use crate::mm::frame::{ meta::{REF_COUNT_MAX, REF_COUNT_UNUSED}, }; use crate::mm::page_table::*; -use crate::mm::{Paddr, PagingConstsTrait, PagingLevel, Vaddr}; +use crate::mm::{CurrentPagingConstsTrait, Paddr, PagingConstsTrait, PagingLevel, Vaddr}; use crate::specs::arch::{NR_ENTRIES, NR_LEVELS, PAGE_SIZE}; use crate::specs::mm::frame::{ mapping::{frame_to_index, group_page_meta, meta_to_index}, @@ -983,6 +983,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); assert(nr_subpage_per_huge_spec::() == NR_ENTRIES); } @@ -1074,6 +1075,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { proof { C::lemma_page_table_config_constant_properties(); C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); let ghost the_node = new_owner.value().node(); assert(0 <= i < NR_ENTRIES); diff --git a/ostd/src/mm/page_table/node/mod.rs b/ostd/src/mm/page_table/node/mod.rs index b14dcc5ef..289997517 100644 --- a/ostd/src/mm/page_table/node/mod.rs +++ b/ostd/src/mm/page_table/node/mod.rs @@ -72,6 +72,7 @@ use super::{PageTableConfig, PageTableEntryTrait, nr_subpage_per_huge}; use crate::{ mm::{ + CurrentPagingConstsTrait, PagingConstsTrait, PagingLevel, // FrameAllocOptions, Infallible, @@ -174,6 +175,7 @@ unsafe impl AnyFrameMeta for PageTablePageMeta { proof { C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); C::lemma_page_table_config_constant_properties(); vstd::arithmetic::mul::lemma_mul_inequality( range.start as int, @@ -217,6 +219,7 @@ unsafe impl AnyFrameMeta for PageTablePageMeta { proof { C::lemma_page_table_config_constant_properties(); C::lemma_paging_consts_properties(); + C::lemma_current_paging_consts_requirements(); vstd::arithmetic::mul::lemma_mul_is_distributive_sub_other_way( size_of_e, NR_ENTRIES as int, @@ -999,11 +1002,10 @@ impl PageTablePageMeta { ensures ({ let pte = Self::walk_pte_at_view(view, c); - pte.is_present() && pte.is_last(self.level) ==> C::raw_item_well_formed( - pte.paddr(), - self.level, - pte.prop(), - ) + pte.is_present() && pte.is_last(self.level) ==> { + &&& valid_frame_paddr(pte.paddr()) + &&& C::raw_item_well_formed(pte.paddr(), self.level, pte.prop()) + } }), { } @@ -1058,8 +1060,8 @@ impl PageTablePageMeta { } } - /// Every present leaf PTE encountered by the drop walk contains a canonical - /// raw item for the node's paging level. + /// Every present leaf PTE encountered by the drop walk contains a valid + /// frame address and a canonical raw item for the node's paging level. pub open spec fn walk_items_well_formed_from_view( self, reader: crate::mm::VmReader<'_, crate::mm::Infallible>, @@ -1072,11 +1074,10 @@ impl PageTablePageMeta { C::E, >() as int == 0 ==> { let pte = Self::walk_pte_at_view(view, c); - pte.is_present() && pte.is_last(self.level) ==> C::raw_item_well_formed( - pte.paddr(), - self.level, - pte.prop(), - ) + pte.is_present() && pte.is_last(self.level) ==> { + &&& valid_frame_paddr(pte.paddr()) + &&& C::raw_item_well_formed(pte.paddr(), self.level, pte.prop()) + } } } diff --git a/ostd/src/mm/vm_space.rs b/ostd/src/mm/vm_space.rs index b27d81e7f..fe3461da5 100644 --- a/ostd/src/mm/vm_space.rs +++ b/ostd/src/mm/vm_space.rs @@ -45,7 +45,7 @@ use crate::mm::tlb::*; use crate::specs::mm::cpu::{AtomicCpuSet, CpuSet}; use crate::mm::{ - MAX_USERSPACE_VADDR, Paddr, PagingConstsTrait, PagingLevel, Vaddr, + CurrentPagingConstsTrait, MAX_USERSPACE_VADDR, Paddr, PagingConstsTrait, PagingLevel, Vaddr, io::{Fallible, VmReader, VmWriter}, page_prop::PageProperty, }; @@ -1764,6 +1764,7 @@ unsafe impl PageTableConfig for UserPtConfig { lemma_pow2_adds(9, 39); PageTableEntry::lemma_layout(); Self::C::lemma_paging_consts_properties(); + Self::C::lemma_current_paging_consts_requirements(); assert(Self::LEADING_BITS_spec() == 0usize); } } From ab0701305f81ef520d4c0a179f078d05f047f9ef Mon Sep 17 00:00:00 2001 From: Je5s1e Date: Wed, 9 Sep 2026 15:31:19 +0800 Subject: [PATCH 2/5] Clean up page table owner proof imports --- ostd/specs/mm/page_table/owners.rs | 96 ++++++++++++++++-------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/ostd/specs/mm/page_table/owners.rs b/ostd/specs/mm/page_table/owners.rs index 6ff97b7c8..fc5f93192 100644 --- a/ostd/specs/mm/page_table/owners.rs +++ b/ostd/specs/mm/page_table/owners.rs @@ -1,31 +1,46 @@ use core::ops::{Deref, Range}; -use vstd::prelude::*; - -use vstd::{seq::*, seq_lib::*, set_lib::*}; -use vstd_extra::{drop_tracking::*, ghost_tree::*, ownership::*, prelude::TreeNodeValue}; +use vstd::{ + arithmetic::{ + div_mod::{ + lemma_fundamental_div_mod, lemma_mod_adds, lemma_mod_equivalence, + lemma_mod_multiples_basic, lemma_small_mod, + }, + mul::{lemma_mul_inequality, lemma_mul_is_associative}, + power2::lemma2_to64_rest, + }, + prelude::*, + seq::*, + seq_lib::*, + set_lib::*, +}; +use vstd_extra::{ + arithmetic::lemma_mod_0_add, drop_tracking::*, ghost_tree::*, ownership::*, + prelude::TreeNodeValue, +}; -use crate::specs::{ - arch::*, +use crate::{ mm::{ - frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners}, - page_table::{ - cursor::page_size_lemmas::{ - lemma_nr_entries_times_sub_page_size, lemma_page_size_divides, - lemma_page_size_ge_page_size, lemma_page_size_spec_values, + Paddr, PagingConstsTrait, PagingLevel, Vaddr, + frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, + page_size, page_size_spec, + page_table::{EntryOwner, EntryOwnerKind, PageTableEntryTrait, PageTableGuard}, + }, + specs::{ + arch::*, + mm::{ + frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners}, + page_table::{ + cursor::page_size_lemmas::{ + lemma_nr_entries_times_sub_page_size, lemma_page_size_divides, + lemma_page_size_ge_page_size, lemma_page_size_spec_values, + }, + *, }, - *, }, }, }; -use crate::mm::{ - Paddr, PagingConstsTrait, PagingLevel, Vaddr, - frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, - page_size, page_size_spec, - page_table::{EntryOwner, EntryOwnerKind, PageTableEntryTrait, PageTableGuard}, -}; - verus! { broadcast use group_ghost_tree_lemmas; @@ -201,21 +216,14 @@ proof fn lemma_rec_vaddr_aligned(path: TreePath, idx: int) let entry_size = page_size(entry_level) as int; lemma_page_size_divides(mapped_level, entry_level); assert(entry_size % mapped_page_size == 0); - vstd::arithmetic::div_mod::lemma_fundamental_div_mod(entry_size, mapped_page_size); + lemma_fundamental_div_mod(entry_size, mapped_page_size); let entry_ratio = entry_size / mapped_page_size; assert(entry_size == entry_ratio * mapped_page_size); - vstd::arithmetic::div_mod::lemma_mod_multiples_basic( - path[idx] * entry_ratio, - mapped_page_size, - ); - vstd::arithmetic::mul::lemma_mul_is_associative(path[idx], entry_ratio, mapped_page_size); + lemma_mod_multiples_basic(path[idx] * entry_ratio, mapped_page_size); + lemma_mul_is_associative(path[idx], entry_ratio, mapped_page_size); assert(path[idx] * entry_size == (path[idx] * entry_ratio) * mapped_page_size); assert((path[idx] * entry_size) % mapped_page_size == 0); - vstd_extra::arithmetic::lemma_mod_0_add( - path[idx] * entry_size, - rec_vaddr(path, idx + 1) as int, - mapped_page_size, - ); + lemma_mod_0_add(path[idx] * entry_size, rec_vaddr(path, idx + 1) as int, mapped_page_size); assert((path[idx] * entry_size + rec_vaddr(path, idx + 1)) % mapped_page_size == 0); assert((rec_vaddr(path, idx) as int) % mapped_page_size == 0); assert(rec_vaddr(path, idx) % page_size(mapped_level) == ((rec_vaddr(path, idx) as int) @@ -338,7 +346,7 @@ pub proof fn page_size_monotonic(a: PagingLevel, b: PagingLevel) assert(ps_a <= ps_b) by { if ps_b < ps_a { - vstd::arithmetic::div_mod::lemma_small_mod(ps_b as nat, ps_a as nat); + lemma_small_mod(ps_b as nat, ps_a as nat); assert(false); } } @@ -835,7 +843,7 @@ impl PageTableOwner { ensures self.view_rec(path).contains(m), { - broadcast use vstd::seq_lib::group_seq_properties; + broadcast use group_seq_properties; let mapped = self.view_rec_node_children(path); assert(mapped.to_set().contains(mapped[i])); @@ -857,7 +865,7 @@ impl PageTableOwner { path.push_tail(i), ).contains(m), { - broadcast use vstd::seq_lib::group_seq_properties; + broadcast use group_seq_properties; } @@ -1274,41 +1282,41 @@ impl PageTableOwner { }; let ps = page_size(pt_level) as int; - vstd_extra::arithmetic::lemma_mod_0_add(frame.mapped_pa as int, ps, ps); + lemma_mod_0_add(frame.mapped_pa as int, ps, ps); lemma_vaddr_of_eq_int::(path); C::lemma_page_table_config_constant_properties(); lemma_vaddr_strict_bound(path); let lb = C::LEADING_BITS_spec() as int; - vstd::arithmetic::power2::lemma2_to64_rest(); + lemma2_to64_rest(); let limit = page_size(INC_LEVELS as PagingLevel) as int; lemma_page_size_divides(pt_level, INC_LEVELS as PagingLevel); // (A) Alignment. The mapped page size divides the full paging // address-space size, so the leading-bit offset is aligned. // `vaddr_of(path) % ps == 0` via `lemma_mod_adds`. assert(limit % ps == 0); - vstd::arithmetic::div_mod::lemma_fundamental_div_mod(limit, ps); + lemma_fundamental_div_mod(limit, ps); let limit_ratio = limit / ps; assert(limit == limit_ratio * ps); - vstd::arithmetic::mul::lemma_mul_is_associative(lb, limit_ratio, ps); - vstd::arithmetic::div_mod::lemma_mod_multiples_basic(lb * limit_ratio, ps); + lemma_mul_is_associative(lb, limit_ratio, ps); + lemma_mod_multiples_basic(lb * limit_ratio, ps); assert(lb * limit == (lb * limit_ratio) * ps); assert((lb * limit) % ps == 0); - vstd::arithmetic::div_mod::lemma_mod_adds(vaddr(path) as int, lb * limit, ps); + lemma_mod_adds(vaddr(path) as int, lb * limit, ps); // (B) Overflow: `vaddr_of(path) + ps <= 2^64`. // `vaddr(path) + ps <= 2^48`: from strict bound plus alignment. let v = vaddr(path) as int; - vstd::arithmetic::div_mod::lemma_mod_equivalence(limit, v, ps); + lemma_mod_equivalence(limit, v, ps); let diff = limit - v; let q = diff / ps; - vstd::arithmetic::div_mod::lemma_fundamental_div_mod(diff, ps); + lemma_fundamental_div_mod(diff, ps); assert(q >= 1) by (nonlinear_arith) requires diff > 0, ps > 0, diff == ps * q, ; - vstd::arithmetic::mul::lemma_mul_inequality(1, q, ps); - vstd::arithmetic::mul::lemma_mul_inequality(lb, 0xffffint, limit); + lemma_mul_inequality(1, q, ps); + lemma_mul_inequality(lb, 0xffffint, limit); assert(v + ps <= limit) by (nonlinear_arith) requires q >= 1, @@ -1318,7 +1326,7 @@ impl PageTableOwner { ; assert(0xffffint * limit + limit == 0x1_0000_0000_0000_0000int); assert(lb * limit + v + ps <= 0x1_0000_0000_0000_0000int); - vstd_extra::arithmetic::lemma_mod_0_add(m.va_range.start, ps, ps); + lemma_mod_0_add(m.va_range.start, ps, ps); assert(set![4096, 2097152, 1073741824].contains(m.page_size)); assert(m.pa_range.start <= m.pa_range.end <= MAX_PADDR); assert forall|m2: Mapping| #[trigger] From 143f5813837f66456c9f0a79844592c70da32339 Mon Sep 17 00:00:00 2001 From: Je5s1e Date: Fri, 11 Sep 2026 14:02:43 +0800 Subject: [PATCH 3/5] resolve confilct --- .agents/skills/vostd-code-review/README.md | 79 + .agents/skills/vostd-code-review/SKILL.md | 274 +++ .../personas/maintainability.md | 82 + .../personas/proof-engineering.md | 113 + .../vostd-code-review/personas/workflow.md | 36 + .claude/skills/vostd-code-review | 1 + .github/workflows/ci-irc11.yml | 2 +- .gitignore | 9 +- ostd/specs/mm/embedding/cursor.rs | 91 +- ostd/specs/mm/embedding/frame.rs | 12 +- ostd/specs/mm/embedding/mod.rs | 1846 ++++------------- ostd/specs/mm/frame/frame_specs.rs | 24 +- ostd/specs/mm/frame/meta_region_owners.rs | 19 + ostd/specs/mm/frame/meta_specs.rs | 53 +- ostd/specs/mm/frame/unique.rs | 13 +- ostd/specs/mm/page_table/cursor/owners.rs | 1 + ostd/specs/mm/page_table/node/child.rs | 2 + ostd/specs/sync/mod.rs | 1 + ostd/specs/sync/rcu/mod.rs | 319 +++ ostd/specs/sync/rcu/root.rs | 409 ++++ ostd/src/mm/frame/linked_list.rs | 16 +- ostd/src/mm/frame/mod.rs | 6 +- ostd/src/mm/frame/segment.rs | 51 +- ostd/src/mm/frame/unique.rs | 30 +- ostd/src/mm/kspace/kvirt_area.rs | 5 +- ostd/src/mm/page_table/cursor/mod.rs | 125 +- ostd/src/mm/page_table/mod.rs | 2 +- ostd/src/mm/page_table/node/entry.rs | 30 +- ostd/src/mm/page_table/node/mod.rs | 2 +- ostd/src/util/mod.rs | 2 +- ostd/src/util/ops.rs | 120 +- verified_libs/vstd_extra/src/external/cmp.rs | 43 + verified_libs/vstd_extra/src/external/iter.rs | 25 + verified_libs/vstd_extra/src/external/mod.rs | 4 + .../vstd_extra/src/external/range.rs | 39 +- verified_libs/vstd_extra/src/lib.rs | 1 + verified_libs/vstd_extra/src/range.rs | 59 + 37 files changed, 2222 insertions(+), 1724 deletions(-) create mode 100644 .agents/skills/vostd-code-review/README.md create mode 100644 .agents/skills/vostd-code-review/SKILL.md create mode 100644 .agents/skills/vostd-code-review/personas/maintainability.md create mode 100644 .agents/skills/vostd-code-review/personas/proof-engineering.md create mode 100644 .agents/skills/vostd-code-review/personas/workflow.md create mode 120000 .claude/skills/vostd-code-review create mode 100644 ostd/specs/sync/rcu/mod.rs create mode 100644 ostd/specs/sync/rcu/root.rs create mode 100644 verified_libs/vstd_extra/src/external/cmp.rs create mode 100644 verified_libs/vstd_extra/src/external/iter.rs create mode 100644 verified_libs/vstd_extra/src/range.rs diff --git a/.agents/skills/vostd-code-review/README.md b/.agents/skills/vostd-code-review/README.md new file mode 100644 index 000000000..b51dae90f --- /dev/null +++ b/.agents/skills/vostd-code-review/README.md @@ -0,0 +1,79 @@ +# VOSTD Code Review + +`vostd-code-review` reviews either a Git change or selected Verus source +files against the VOSTD coding guidelines and writes a single, evidence-backed +Markdown report. It covers the maintainability and proof-engineering aspects, plus +two conditional workflow checks, in +[`docs/coding-guidelines`](../../../docs/coding-guidelines/README.md). + +## Usage + +The skill has two modes, both anchored at the current checkout (`HEAD`): + +```text +$vostd-code-review diff [] [--overwrite] +$vostd-code-review files [--overwrite] +``` + +Examples: + +```text +$vostd-code-review diff review.md +$vostd-code-review diff main review.md +$vostd-code-review diff origin/main review.md --overwrite +$vostd-code-review files ostd/src/sync/rwlock.rs review.md +$vostd-code-review files ostd/src/sync/rwlock.rs:120-240 review.md +``` + +`diff []` reviews the committed series +`merge-base(, HEAD)..HEAD`, oldest first. When `` is omitted it defaults to +`origin/main`, the `main` branch of the upstream `origin` +(https://github.com/asterinas/vostd); run `git fetch origin` first to review against the +latest upstream main. Each commit's message and diff are captured together so reviewers +can judge the code against that commit's intent. Uncommitted changes are excluded. To +review a historical endpoint, check it out first so it becomes `HEAD`. + +`files` reviews the current working-tree contents of the named files, including +staged, unstaged, and untracked target content. Targets use 1-based inclusive +line ranges. Repeat a path to review multiple ranges from the same file. If no +range is supplied, the whole file is in scope. + +In both modes, the output path is the final positional argument and is not +overwritten unless `--overwrite` is present. + +## What the review does + +The skill: + +1. snapshots the commit series or current working-tree target contents; +2. runs isolated reviews for maintainability and proof engineering; +3. runs a workflow review only when an `rlimit` changes or a `std`/`core`/`alloc` + external specification is newly added or materially changed; +4. checks important claims against the source, the complete active `vstd`, and existing + verified code throughout VOSTD; +5. consolidates the results by severity; and +6. writes an English Markdown report containing findings, compliant rules, + suggested fix order, and evidence-check details. + +The workflow pass checks only whether a changed rlimit exceeds `200` and whether a +newly added or materially changed project-local external specification for a +`std`/`core`/`alloc` API should be proposed upstream. It does not review CI or host +coverage, toolchain configuration, solver stability, proof decomposition, or other +workflow concerns. + +In `diff` mode, findings must be caused by the reviewed commits. In `files` +mode, findings must be rooted in the named files or requested ranges. The wider +repository may be read as context in both modes. + +## Safety and prerequisites + +- Run the skill from the VOSTD repository with its Verus toolchain available. +- The working tree is treated as read-only. Only the requested report is written + persistently; standalone proof experiments run in disposable locations. +- Existing report files are preserved unless `--overwrite` is specified. +- Review assumes the change has already passed CI and does not repeat focused or + repository-wide verification. + +For the complete orchestration rules, evidence schema, and report format, see +[`SKILL.md`](SKILL.md). The aspect-specific reviewer instructions live in +[`personas/`](personas/); the workflow persona is conditional. diff --git a/.agents/skills/vostd-code-review/SKILL.md b/.agents/skills/vostd-code-review/SKILL.md new file mode 100644 index 000000000..926a60ab3 --- /dev/null +++ b/.agents/skills/vostd-code-review/SKILL.md @@ -0,0 +1,274 @@ +--- +name: vostd-code-review +description: Review a Git change or selected Verus files against VOSTD's coding guidelines and write a Markdown report. Diff mode defaults to reviewing against the upstream asterinas/vostd main branch (origin/main). Always reviews maintainability and proof engineering; conditionally checks only rlimit caps and whether changed std/core/alloc external specs should be upstreamed. +--- + +# vostd-code-review + +Review a Git change or selected code against VOSTD's coding guidelines +— primarily `maintainability.md` and `proof-engineering.md`, with two narrowly +scoped checks from `workflow.md` +— and write one Markdown review file. There are two modes, both anchored at the +current checkout (`HEAD`): + +- **`diff `** reviews the committed series + `merge-base(, HEAD)..HEAD`, oldest first. Each commit's message and diff are + captured so its intent and code changes remain associated. Uncommitted edits are + not reviewed in this mode. +- **`files `** reviews the current working-tree bytes of the + named files, including staged, unstaged, and untracked target content. + +To review a historical commit or range, check out its desired endpoint first so it +becomes `HEAD`. In either mode, the question is *which rules does this scoped code +follow and which does it violate*, with each violation grounded in a cited rule and +quoted evidence. + +## Interface + +``` +diff [] [--overwrite] +files [--overwrite] +``` + +In `diff` mode, `` is optional. When it is omitted the review uses the default base +`origin/main` — the `main` branch of the upstream repository +(https://github.com/asterinas/vostd, remote `origin`). One positional after `diff` is the +`` with `` defaulted; two positionals are `` then ``. + +Wrap in double quotes any argument containing spaces. + +- `diff` / `files` — **required first positional**, selecting the review mode. +- `` — **optional in `diff` mode**; any Git ref or SHA. When omitted, defaults to + `origin/main` (the upstream https://github.com/asterinas/vostd `main`, remote `origin`). Review + `merge-base(, HEAD)..HEAD`; `HEAD` is always the endpoint. Run `git fetch origin` + first to review against the latest upstream main; the skill resolves `origin/main` to its + current local value and never fetches. +- `` — **required in `files` mode**, one or more targets in the + working tree. + A target is a path, optionally narrowed `path:N-M,K-L` (1-based, inclusive); + repeat a path to add ranges. Ranges for the same path form a union. With no range, + the whole file is in reporting scope. With ranges, read the whole file as context but + report a finding only when its primary violating location intersects that union; + out-of-range lines may be cited only as supporting context. +- `` — **required, last positional** (`cp src... dest` style); + refuse to overwrite unless `--overwrite`. +- `--overwrite` — replace the output file if it already exists. + +## Repository context + +Locate these once, before fanning out; the personas need the absolute paths: + +| Item | How to locate | +|------|---------------| +| Guideline pages | `docs/coding-guidelines/{README,maintainability,proof-engineering,workflow}.md`, plus `proof-patterns.md` as supporting (non-mandatory) examples. The workflow reviewer uses only `decompose-before-raising-rlimit`'s numeric cap and `upstream-reusable-specs`. | +| Existing verified code | The entire vendored vstd tree located from `grep '^vstd' Cargo.toml` (typically `tools/verus/source/vstd`), all of `verified_libs/`, `ostd/specs/`, and Verus-bearing files under `ostd/src/`. | +| Verus binary + Z3 | `tools/verus/source/target-verus/release/verus` and `tools/verus/source/z3`, present after `make verus`/`cargo dv bootstrap`; used only for the standalone vacuity-refutation experiment the proof-engineering persona may run. | +| History rationale | `git log --follow -p -- `. In `diff` mode, the captured commit series is the primary review input. | +| Default diff base | `origin/main` — `origin` is https://github.com/asterinas/vostd (confirm with `git remote -v`). `git fetch origin` refreshes it before a review; the skill resolves it to its current local value and does not fetch. | + +## Pipeline + +Run these steps in order. + +1. **Resolve the input.** + Resolve the mode first and capture one immutable review input in a disposable + directory outside the shared working tree: + + - In `diff` mode, default `` to `origin/main` (the upstream + https://github.com/asterinas/vostd `main` branch) when omitted, resolve it to its current + local SHA without fetching, then record `HEAD`, ``, and their merge-base. Capture + `git log --reverse -p --format=fuller ..HEAD` so every commit message + stays paired with its diff. Also record the commit IDs and changed paths. Refuse an + empty series. Do not include staged, unstaged, or untracked edits. + - In `files` mode, capture every target's current working-tree bytes and content hash + once. Store each target in a dedicated snapshot file and record its requested range + union. + + Use only these immutable inputs throughout the review. Read each input **in full** + before fanning out; do not silently refresh it from a changing worktree. Read the + `README.md`, `maintainability.md`, and `proof-engineering.md` live at review start; + they are the authority for those review aspects. Read only the two permitted + sections of `workflow.md`: the `200` rlimit cap under + `decompose-before-raising-rlimit` and `upstream-reusable-specs`. Resolve the + repository-context paths above and record relevant earlier history. + +2. **Activate personas.** + Activate the maintainability and proof-engineering personas for every reviewed + Verus path. Activate the workflow persona only when the immutable review input has + at least one of these changes in reporting scope: + + - an added, removed, or changed `#[verifier::rlimit(...)]`; or + - a newly added or materially changed external specification for a `std`, `core`, + or `alloc` API. A material change affects the specified API, contract, model, or + panic/unwind semantics; formatting, imports, and comments alone are not material. + + In `files` mode, determine whether the target changed by comparing its captured + bytes with `HEAD` (treat an untracked file as wholly added). If neither condition + applies, do not launch the workflow persona and do not report workflow Compliance + or N/A entries. Persona activation does not justify running verification or another + expensive experiment. + +3. **Fan out.** + Spawn the persona passes (see *Spawning*): by default **one isolated agent per + activated persona** — best recall. Each pass reads only its own persona block + (selective exposure), reviews the target itself, and returns its findings as + structured text under the *Evidence contract* below. + +4. **Verify.** + For each returned finding, isolate the key premise it rests on and try to **refute** + it by re-reading the cited code and checking named lemma signatures in + vstd/vstd_extra. Do not repeat persona experiments or run additional Verus + verification in this step. Assign a verdict: + + - **confirmed** — keep the finding unchanged. + - **uncertain** — keep it, but prefix the problem line with `(unverified) `. + - **refuted** — remove it, and list it under `## Retracted by verification` at the + foot of the report with a one-line reason. + + Remove only on confident refutation; an unsure check is `uncertain`, not `refuted`. + A finding also does not survive verification if the rule it cites no longer exists + on the current guideline page — a method may propose, the page decides what counts + as a violation. + Resolve cross-persona contradictions (two aspects claiming opposite facts about the + same lines) from the stronger cited evidence. If static evidence cannot decide, keep + the claim as `uncertain`; do not launch another experiment. Record the resolution in + the report's addendum. + +5. **Consolidate.** + Merge findings that share one root cause into one entry carrying every violated + rule's short name. Never drop a distinct guideline violation. Rank the survivors: + `high` first, then `medium`, then `low`. Keep the *Compliance* section with the same + rigor as the findings. + +6. **Write the output.** (see *Output format*) + +## Spawning a persona pass + +Launch all passes **in a single message** (parallel) with a no-history isolated fork +(`fork_turns: "none"`, or the environment's exact equivalent). Do not use the default +conversation-inheriting fork: each pass receives only its persona and the explicit +context below. +Each pass prompt is built the same way: + +1. the persona file's full text (`personas/.md`), verbatim, as the stable head; +2. the absolute paths of the repository-context table (guideline pages, existing + verified-code roots, Verus binary, verification-gate configuration, branch); +3. the mode and its immutable review input: in `diff` mode, the absolute captured-log + path, merge-base, HEAD, commit IDs, and changed paths; in `files` mode, each original + target path plus its absolute snapshot path, content hash, and reporting ranges. + Do not embed source or diff contents in the prompt. + +Pass rules stated in every prompt: + +- Read the immutable review input from the supplied paths; do not replace it with live + Git output or working-tree files and do not rely on summaries. Maintainability and + proof-engineering reviewers read their full guideline page; the workflow reviewer + reads only the two permitted rule sections named in its persona. The live repository + may be read only for surrounding context and verification. Report locations using + original paths and source line numbers, never disposable paths. +- Enforce reporting scope. In `diff` mode, a finding's primary violating location must + be introduced or materially changed by a reviewed commit; removed code may support a + finding about the resulting change. In `files` mode, the primary location must + intersect the target's requested range union. Context outside the scope cannot create + an independent finding. Scope Compliance claims the same way. +- For maintainability and proof engineering, the guideline page outranks the persona + file's method list: enumerate and check every current rule. The workflow persona is + the explicit exception: check only the triggered `rlimit > 200` condition and/or + whether a newly added or materially changed `std`/`core`/`alloc` external spec + should be proposed upstream. Do not inspect or report any other workflow concern. +- A method may only propose a finding; a finding stands only if a permitted rule on the + current page grounds it. For maintainability and proof engineering, if a page rule + fits no method, design the check on the spot and keep the finding format. +- A finding exists only with quoted evidence — code lines, comment lines, command + output, or named lemma/spec signatures checked in vstd/vstd_extra. State unknown + facts as unknown. +- Cite every violation by the guideline's kebab-case short name + (`docs/coding-guidelines/README.md`). +- Include a `Compliance` list of rules the code demonstrably follows, with line references. +- You may run read-only repository commands and standalone experiments in a disposable + directory. Never edit, back up, restore, or construct modified variants of files in + the shared working tree. +- Exhaust the static checks first (reading, grep, git history); every experiment must + be able to name the report outcome it can change — run none whose outcome changes + nothing. +- Do not run focused, module, repository-wide, or controlled-variant verification. Code + accepted for review is assumed to have already passed CI. Standalone proof experiments + used to check a specific logical premise are not CI verification and remain allowed. +- Return exactly the structured data defined by the *Evidence contract* below, with no + surrounding prose. + +## Evidence contract + +Every persona returns one YAML-shaped block with exactly two top-level keys. Keep every +field; use `null` or `[]` rather than omitting an unknown or empty value. + +```yaml +Findings: + - id: - + severity: high | medium | low + guideline: + location: + quoted_evidence: + problem: + key_premise: + experiment: + attempted: true | false + command: + exit_code: + result: + suggestion: +Compliance: + - guideline: + status: complies | not-applicable + locations: [, ...] + quoted_evidence: + compliance_or_na_reason: +``` + +For maintainability and proof engineering, each current rule from the persona's +guideline page appears at least once under either `Findings` or `Compliance`. For +workflow, include only the applicable triggered check(s); omit all non-triggered and +all other workflow rules rather than recording N/A entries. Findings use a primary +location inside the requested diff or file/range scope; out-of-scope support stays +inside `quoted_evidence`. Commit messages may establish intent but are not independently +reviewed for style unless a current VOSTD guideline explicitly requires it. Do not +assign the orchestrator's `confirmed` / `uncertain` / `refuted` verdict in persona +output. + +## Output format + +One Markdown file: + +```markdown +# Review: (branch ) + +> Produced from docs/coding-guidelines; CI verification is a review precondition. +> Scope: .. []` or +> `files captured from the working tree; = []`>. + +**Bottom line:** two to five sentences naming the single most important problem. + +## High (one `###` per finding) +## Medium (one `###` per finding) +## Low (one row per finding: Finding | Guideline | Detail) +## Compliance +## Suggested fix order +## Retracted by verification (only if any) +## Addendum: cross-persona resolution (only if any) +## Evidence check record (static checks and standalone logical experiments) +``` + +Every finding states: the guideline short-name, location (`file:line`), the quoted +evidence, the problem in one or two sentences, and a concrete suggestion. +Severity: `high` = a reviewer would block the PR on it; +`medium` = should be fixed before merge; +`low` = nit or readiness note. +The report is always in English. + +## Ground rules + +- The review stays delegated: one sub-agent per activated persona; the orchestrator + synthesizes but does not review inline. +- Treat the existing working tree as read-only. The only persistent file this skill + writes there is ``; experiment artifacts belong in a disposable directory + outside it and must not alter or restore user files. diff --git a/.agents/skills/vostd-code-review/personas/maintainability.md b/.agents/skills/vostd-code-review/personas/maintainability.md new file mode 100644 index 000000000..b34fd4995 --- /dev/null +++ b/.agents/skills/vostd-code-review/personas/maintainability.md @@ -0,0 +1,82 @@ +# Maintainability persona + +**Review section:** High/Medium/Low findings on shape, layout, and documentation +**Remit:** Can the next reader see what runs, what models the mathematics, +and what exists only to prove something — without archaeology? + +**Your guideline page (the authority — read it in full first):** +`docs/coding-guidelines/maintainability.md` + +Open the page now and enumerate every rule it currently contains, by kebab-case short +name. Check every one of them against the target; the page outranks everything below. +If the page has rules this list does not name, review them through the closest method +below; if a rule named below no longer exists on the page, drop it. + +**Concerns, in order:** + +1. Take the page's current rule list as your checklist; clear every rule with a finding + or an explicit compliance note citing line references. +2. Executable shape (`preserve-exec-code`): recover the original executable code from + `git log -p` and the upstream history. Flag moved items, rewritten expressions whose + original form is not shown in the change, and equivalences that cannot be checked from + the review text alone. Flag reversed conversion lemmas, reconstructed values, runtime + clones, and caller-facing API changes introduced only to ease a proof. When Verus + forces an executable change, it must stay minimal and the original form must be shown + in the review; the page mandates no specific comment format, so do not require one. +3. Mode separation and layout (`separate-verus-modes`): executable code, specifications, + and proofs sit in visually distinct groups, and adjacent verified items share one + `verus!` block when no ordinary Rust item separates them. +4. Proof-body hygiene, mapping each check to its rule: + - `organize-proof-imports` / `group-imports-by-crate`: audit `reveal`, + `reveal_with_fuel`, and `broadcast use` for repeated long paths that an import would + replace, and confirm definitions from one crate share a single `use` group even across + modules. Retain a qualified path only where the page permits — ambiguity or a one-off + reference — and keep `reveal`/`reveal_with_fuel` minimal. A `reveal` of an `open` spec + fn is still load-bearing, so remove one only when verification stays green, and leave a + one-line note on a non-obvious `reveal` or fuel value that must remain. + - `use-chained-comparisons`: contiguous bounds that share intermediate expressions are + one chained comparison, in contracts, invariants, predicates, and assertions — but + only where the chain is logically equivalent and no relation is invented or contract + strengthened to form it. + - `use-returns-for-exact-results`: exact return values use `returns expr` (type-matched, + with required casts such as `as usize` kept and justified); unused named binders and + `-> (ret: ())` declarations are removed. + - `bind-option-payloads`: two or more facts over the same `Option` bind the payload once + with `matches` and group the facts in one implication; leave a lone implication + ungrouped when binding would not aid clarity. + - `qualified-verus-spec-calls`: a `#[verus_spec(...)]` call site uses a qualified path + when an import would block Verus from finding the specification, rather than adding an + import solely to change resolution. +5. Naming and modes (`name-proof-roles`, `avoid-redundant-mode-markers`, + `prefer-ghost-model-structs`): `lemma_`/`tracked_`/`axiom_` prefixes and resource names + that state their ownership role; `ghost`/`tracked`/plain mode chosen deliberately per + struct and confirmed by verification (`ghost struct` first for proof-only models and + zero-sized generic arguments, `tracked struct` for linear permissions, plain struct for + runtime state); markers used only where they communicate or enforce a mode boundary, + with `tracked_`/`ghost_` field prefixes inside executable types and no redundant marker + repeated on every field of a `ghost struct`. +6. Documentation (`document-verified-apis`): preserve original runtime docs. For a public + executable API, append a `Verified Properties` section — `Safety` (classes of undefined + behavior ruled out and remaining trusted boundaries, claiming only what verification + establishes), `Functional Correctness` when applicable, `Preconditions`, and + `Postconditions` (including proved absence of panic). For a `spec fn`, document the + mathematical meaning of the value it denotes; for a `proof fn`, one sentence summarizing + the proved fact, with further prose only for a non-obvious obligation or guarantee. Do + **not** add `Preconditions`/`Postconditions` to spec or proof functions — their + `requires`/`ensures` clauses already state those formally. A verified module gets a + `Verified Properties` paragraph covering verification design, critical invariants, + safety, and verified functional correctness. +7. Placement (`right-size-spec-placement`): grep the repo for users of each spec fn the + target defines; a small model beside its single user is correct, a shared or growing + model belongs under `ostd/specs/`. +8. Debt and lint (`document-real-proof-debt`, `narrow-lint-suppressions`): proof comments + tied to current, non-obvious constraints and their consequences; shared trust boundaries + documented once at module level; a `TODO` for temporary limitations needing follow-up; no + restating of code or speculation about tool limits. Flag a doc claim whose supporting + assert a later commit removed (`git log -p` tells). Lints suppressed at the smallest + item or expression scope, preferring `#[expect(...)]` for deliberately triggered lints + over crate- or module-wide `allow`. + +You own readability and structure, not contract completeness or proof reuse +(Proof-engineering persona), the `rlimit > 200` threshold, or whether a changed +standard-library external spec should be proposed upstream (Workflow persona). diff --git a/.agents/skills/vostd-code-review/personas/proof-engineering.md b/.agents/skills/vostd-code-review/personas/proof-engineering.md new file mode 100644 index 000000000..09cc9c62e --- /dev/null +++ b/.agents/skills/vostd-code-review/personas/proof-engineering.md @@ -0,0 +1,113 @@ +# Proof-engineering persona + +**Review section:** High/Medium/Low findings on contracts, trust, and models +**Remit:** Is every caller-visible contract stateable and dischargeable, +is every trusted fact stated at the right boundary, +and is every model the simplest one that already exists? + +**Your guideline page (the authority — read it in full first):** +`docs/coding-guidelines/proof-engineering.md` + +Open the page now and enumerate every rule it currently contains, by kebab-case short +name. Check every one of them against the target; the page outranks everything below. +If the page has rules this list does not name, review them through the closest method +below; if a rule named below no longer exists on the page, drop it. `proof-patterns.md` +records recurring VOSTD proof shapes as supporting examples, not mandatory rules — never +report a finding merely because the code diverges from a pattern. + +**Concerns, in order:** + +1. Take the page's current rule list as your checklist; clear every rule with a finding + or an explicit compliance note citing line references. +2. Contract-completeness sweep (`complete-external-contracts`). Enumerate every trusted + external specification the proof relies on (imports from `vstd_extra::external`, and + non-`pub` vstd items the proof unfolds). For each, check against the exact dependency + version's source, API docs, and comments: equivalence clauses are bidirectional; the + direction of each axiom and spec impl actually supplies the direction the proof needs + (a missing converse is a gap); preconditions, postconditions, well-formedness, and + panic/`no_unwind` claims match what the spec promises, with documented panic conditions + excluded by explicit preconditions rather than a bare `may_panic`; closure-driven + adapters require the closure's per-element preconditions only where invoked and promise + neither termination nor `no_unwind` unless the contract requires the closure to + terminate and not panic; size bounds use the library's real representation limits + (e.g. `usize::MAX / 8`, not just `usize::MAX`). +3. Vacuity check (`complete-external-contracts`). For `ensures ... ==> ...` + clauses the target introduces, first inspect verified lemmas and real callers that + establish the antecedent. A failure to prove the antecedent at the definition site is + not evidence of vacuity: callers may have stronger facts. Confirm vacuity only when a + checked argument shows that, under the original `requires`, the antecedent is false for + every legal call (for example, a faithful standalone proof of `!` from those + preconditions). Preserve all relevant definitions and trusted assumptions in a + standalone experiment, run it with the repository Verus binary (`--crate-type lib`, + `VERUS_Z3_PATH` set to the vendored z3), and try to refute the claim with a legal + witness or caller before reporting it. If real callers merely cannot establish the + antecedent, report a caller-usability or contract-completeness gap instead of vacuity; + choose severity from its impact rather than assigning `high` automatically. Also list + unconditionally provable facts missing from the contract. For a closure carrying + `#[verus_spec(...)]`, distinguish ambient facts legitimately used to establish a + self-contained closure contract at construction from obligations that future invocations + need. Report a finding only when the caller-visible closure contract omits such a + required obligation. +4. Trust-boundary sweep (`centralize-trusted-boundaries`, `restrict-generic-trusted-models`). + Grep the target for `assume|admit|external_body|uninterp|broadcast axiom|axiom`; a + trusted fact kept beside a caller instead of in `vstd_extra::external` is a finding. + Prefer `assume_specification` matching the original generic signature, trait bounds, and + associated types; before adding an external function wrapper, test the direct form with + the active toolchain and record any concrete obstacle. Note unused or superseded helpers + in the boundary modules the target points at. When a trusted model is generic, trait + bounds and external trait declarations do not by themselves establish its semantic laws: + inspect associated types, aliasing, and interior mutability, and guard its guarantees + with a model-validity predicate admitted only for reviewed type and architecture + combinations (e.g. the immutable `Seq` model for `bitvec` is limited to storage + `u8`/`u32`/`usize`/`u64` on 64-bit targets with `Lsb0`; `BitStore` alone is insufficient + because some implementations mutate through shared references). If the external + signature must stay generic, the validity predicate must name the relevant storage, + ordering, and index types. +5. Reuse sweep (`reuse-existing-specifications`). Inventory every spec fn, proof fn, + lemma, axiom, model, and external specification introduced or materially changed in + the reporting scope. Search for equivalent semantics and signatures across all active + verified-code roots, not a hand-picked file list: + - the entire vendored `vstd` source tree; + - all of `verified_libs/`, including `vstd_extra`; + - `ostd/specs/`; and + - other Verus-bearing files under `ostd/src/`. + Before modeling a standard-library API that `vstd` does not yet cover, also check the + Verus upstream for accepted or in-progress models, and reuse or wait rather than fork a + competing local spec (a second trust source causes model drift). Batch name and + signature searches across these roots, then inspect semantic candidates even when their + names differ. Report a duplicate only with quoted signatures or definitions showing the + overlap; name similarity alone is not evidence. Prefer the existing verified operation + (e.g. `saturating_add`) or extend the narrowest reusable layer instead of adding an + overlapping local model. When a checked proof replaces an `assume`/`admit`/`=~= cheat or + a bridge lemma, call the verified fact directly and remove the cheat so the trusted + surface net-shrinks. Also flag restated postconditions that merely unfold the lemma's + own `requires` and inflate the SMT goal for every downstream lemma. +6. Model-choice review (`canonical-spec-models`, `distinguish-spec-and-exec-indexing`, + `quantifiers-and-triggers`). Is each model the simplest standard mathematical type + (`Range` for an integer range, `Map` for a map view, a sequence plus a position for + an ordered cursor); when both an operational spec and a set-level spec exist, is each + justified; is bound narrowing (`PartialOrd` vs `Ord` plus obeying-laws `requires`) + principled and are redundant `requires` conjuncts flagged (check the vstd law definitions + — one conjunct may imply the others); are type-level properties independent of irrelevant + value arguments, and well-formedness predicates made methods when they describe one + model. For indexing: `Seq::spec_index` is total with unspecified out-of-bounds values, + so spec helpers may rely on that, but a property needing a valid index keeps bounds and + explicit triggers; executable indexing still needs non-panicking bounds (`requires` or + `IndexSpec::index_req`), and a model of executable `get` preserves `Option` + success/failure semantics — unspecified spec values do not replace that contract. For + quantifiers: prefer standard predicates such as `Seq::all` (their predicate-based + triggers can reduce spurious instantiations vs broad index triggers like `s[i]`), use a + subrange predicate when it reads better, and do not add an axiom for a fact derivable + from the sequence definition. +7. Invariant modeling (`implement-inv-for-models`). When the target defines spec structs, + check whether intrinsic validity is expressed as an `impl Inv` through `inv()` rather + than a stand-alone `wf(...)` predicate; `wf(...)` is only for well-formedness that + depends on another value. Require `inv()` before operations that assume a valid state + and ensure it after those that promise to preserve it (`old(self).inv()` / + `final(self).inv()` for mutable operations). Private fields give representation hiding + but do not make Verus establish `inv()` automatically; when no intrinsic invariant + applies, record N/A with the reason. + +You own contract completeness, trust placement, and model reuse — not documentation +phrasing (Maintainability persona), the `rlimit > 200` threshold, or whether a changed +standard-library external spec should be proposed upstream (Workflow persona). diff --git a/.agents/skills/vostd-code-review/personas/workflow.md b/.agents/skills/vostd-code-review/personas/workflow.md new file mode 100644 index 000000000..2f6cde986 --- /dev/null +++ b/.agents/skills/vostd-code-review/personas/workflow.md @@ -0,0 +1,36 @@ +# Workflow persona + +**Review section:** findings on excessive rlimits and upstreamable standard-library +external specifications + +**Remit:** Perform only the triggered checks below. Do not review host coverage, +toolchain configuration, solver stability, proof decomposition, CI configuration, or +any other workflow concern. + +Read only the relevant text of these two rules in +`docs/coding-guidelines/workflow.md`: + +- `decompose-before-raising-rlimit`, solely for its `rlimit <= 200` threshold; +- `upstream-reusable-specs`, solely to decide whether a changed project-local external + specification should be proposed to upstream Verus. + +## Checks + +1. For every added, removed, or changed `#[verifier::rlimit(...)]` in reporting scope, + inspect the new value. Report a finding under `decompose-before-raising-rlimit` only + when the new value is greater than `200`; otherwise record compliance. Do not judge + whether the rlimit is necessary, whether the proof should be decomposed, or any + other solver setting. A removed rlimit complies. +2. For every newly added or materially changed external specification for a `std`, + `core`, or `alloc` API in reporting scope, decide whether it is generally reusable + beyond VOSTD. A material change affects the specified API, contract, model, or + panic/unwind semantics; formatting, imports, and comments alone do not qualify. + Report a finding under `upstream-reusable-specs` when a generally reusable + project-local specification should be proposed upstream and the review input does + not record that plan; otherwise record compliance. Do not expand this into contract + correctness, caller validation, dependency, placement, or repository-wide reuse + checks; those belong elsewhere. + +Return entries only for checks actually triggered by the immutable review input. Do +not enumerate the workflow guideline page and do not emit N/A entries for untriggered +or excluded rules. diff --git a/.claude/skills/vostd-code-review b/.claude/skills/vostd-code-review new file mode 120000 index 000000000..45a29c01a --- /dev/null +++ b/.claude/skills/vostd-code-review @@ -0,0 +1 @@ +../../.agents/skills/vostd-code-review/ \ No newline at end of file diff --git a/.github/workflows/ci-irc11.yml b/.github/workflows/ci-irc11.yml index bcd336a00..38ffc761c 100644 --- a/.github/workflows/ci-irc11.yml +++ b/.github/workflows/ci-irc11.yml @@ -49,7 +49,7 @@ jobs: env: CARGO_TERM_COLOR: always VERUS_REPOSITORY: https://github.com/asterinas/verus.git - VERUS_BASE_COMMIT: bf70c19fc4aea17fb38aeee6b0686d13ec400d7a + VERUS_BASE_COMMIT: 2ceabf8168c994d61995ce860e0d8b7cab0f9c1b VERUS_IRC11_PATCH: tools/patches/verus-irc11.patch VERUS_COMPAT_PATCH: tools/patches/verus-irc11-vstd.patch diff --git a/.gitignore b/.gitignore index 9d2bd96dd..209b0bf5f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,9 +52,14 @@ doc *.rlib # Agents -.agents/ +.agents/* +!/.agents/skills/ +.agents/skills/* +!/.agents/skills/vostd-code-review/ +.claude/** +!/.claude/skills/ +!/.claude/skills/vostd-code-review .codex/ -.claude/ .copilot/ .github/copilot-instructions.md .github/instructions.md \ No newline at end of file diff --git a/ostd/specs/mm/embedding/cursor.rs b/ostd/specs/mm/embedding/cursor.rs index 9f4fe1f62..e1f7619c2 100644 --- a/ostd/specs/mm/embedding/cursor.rs +++ b/ostd/specs/mm/embedding/cursor.rs @@ -117,8 +117,8 @@ pub axiom fn vm_space_cursor_embedded<'a, 'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] final(regions).slot_owners[i] != old(regions).slot_owners[i] ==> { - &&& old(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& old(regions).ref_count(i) == REF_COUNT_UNUSED + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED &&& final(regions).slot_owners[i].usage !is Frame }, forall|c: CursorOwner<'rcu, UserPtConfig>| @@ -162,8 +162,8 @@ pub axiom fn vm_space_cursor_mut_embedded<'a, 'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] final(regions).slot_owners[i] != old(regions).slot_owners[i] ==> { - &&& old(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& old(regions).ref_count(i) == REF_COUNT_UNUSED + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED &&& final(regions).slot_owners[i].usage !is Frame }, forall|c: CursorOwner<'rcu, UserPtConfig>| @@ -345,14 +345,12 @@ pub axiom fn cursor_mut_map_embedded<'rcu>( final(regions).slot_owners[i].in_list_perm == old(regions).slot_owners[i].in_list_perm, forall|i: int| #![trigger final(regions).slot_owners[i]] - i != frame_to_index(paddr) && old(regions).slot_owners[i].ref_count() - != REF_COUNT_UNUSED ==> final(regions).slot_owners[i] == old( - regions, - ).slot_owners[i], + i != frame_to_index(paddr) && old(regions).ref_count(i) != REF_COUNT_UNUSED + ==> final(regions).slot_owners[i] == old(regions).slot_owners[i], forall|i: int| - #![trigger final(regions).slot_owners[i].ref_count()] - old(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED - ==> final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED, + #![trigger final(regions).ref_count(i)] + old(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).ref_count(i) + != REF_COUNT_UNUSED, // **`ref_count` PRESERVED at the mapped slot. final(regions).slot_owner(paddr).ref_count() == old(regions).slot_owner(paddr).ref_count(), // **`paths_in_pt.len() += 1` at the mapped slot.** @@ -366,12 +364,12 @@ pub axiom fn cursor_mut_map_embedded<'rcu>( // Slots that stay UNUSED are fully preserved. forall|i: int| #![trigger final(regions).slot_owners[i]] - final(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - ==> final(regions).slot_owners[i] == old(regions).slot_owners[i], + final(regions).ref_count(i) == REF_COUNT_UNUSED ==> final(regions).slot_owners[i] + == old(regions).slot_owners[i], forall|i: int| #![trigger final(regions).slot_owners[i]] - i != frame_to_index(paddr) && old(regions).slot_owners[i].ref_count() - == REF_COUNT_UNUSED && final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + i != frame_to_index(paddr) && old(regions).ref_count(i) == REF_COUNT_UNUSED + && final(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).slot_owners[i].usage !is Frame, forall|c: CursorOwner<'rcu, UserPtConfig>| #![auto] @@ -417,11 +415,10 @@ pub axiom fn cursor_mut_unmap_embedded<'rcu>( regions, ).slot_owners[i].vtable_ptr_perm() // `rc` doesn't bump to UNIQUE. - &&& old(regions).slot_owners[i].ref_count() != REF_COUNT_UNIQUE - ==> final(regions).slot_owners[i].ref_count() + &&& old(regions).ref_count(i) != REF_COUNT_UNIQUE ==> final(regions).ref_count(i) != REF_COUNT_UNIQUE // Storage preserved at slots that end non-UNUSED. - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).slot_owners[i].storage_perm() == old( regions, ).slot_owners[i].storage_perm() @@ -435,17 +432,13 @@ pub axiom fn cursor_mut_unmap_embedded<'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] old(regions).slot_owners[i].usage is Frame ==> { - &&& final(regions).slot_owners[i].ref_count() + old( - regions, - ).slot_owners[i].paths_in_pt.len() == old(regions).slot_owners[i].ref_count() - + final(regions).slot_owners[i].paths_in_pt.len() - &&& final(regions).slot_owners[i].ref_count() <= old( - regions, - ).slot_owners[i].ref_count() + &&& final(regions).ref_count(i) + old(regions).slot_owners[i].paths_in_pt.len() + == old(regions).ref_count(i) + final(regions).slot_owners[i].paths_in_pt.len() + &&& final(regions).ref_count(i) <= old(regions).ref_count(i) &&& final(regions).slot_owners[i].paths_in_pt.len() <= old( regions, ).slot_owners[i].paths_in_pt.len() - &&& final(regions).slot_owners[i].ref_count() != 0 + &&& final(regions).ref_count(i) != 0 }, // MMIO slots untouched.* forall|i: int| @@ -486,8 +479,8 @@ pub(super) proof fn open_cursor_step<'a, 'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] final(regions).slot_owners[i] != old(regions).slot_owners[i] ==> { - &&& old(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& old(regions).ref_count(i) == REF_COUNT_UNUSED + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED &&& final(regions).slot_owners[i].usage !is Frame }, forall|c: CursorOwner<'rcu, UserPtConfig>| @@ -534,8 +527,8 @@ pub(super) proof fn open_cursor_mut_step<'a, 'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] final(regions).slot_owners[i] != old(regions).slot_owners[i] ==> { - &&& old(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& old(regions).ref_count(i) == REF_COUNT_UNUSED + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED &&& final(regions).slot_owners[i].usage !is Frame }, forall|c: CursorOwner<'rcu, UserPtConfig>| @@ -741,9 +734,9 @@ pub(super) proof fn cursor_mut_regions_step<'rcu>( &&& final(regions).slot_owners[i].vtable_ptr_perm() == old( regions, ).slot_owners[i].vtable_ptr_perm() - &&& old(regions).slot_owners[i].ref_count() != REF_COUNT_UNIQUE - ==> final(regions).slot_owners[i].ref_count() != REF_COUNT_UNIQUE - &&& final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + &&& old(regions).ref_count(i) != REF_COUNT_UNIQUE ==> final(regions).ref_count(i) + != REF_COUNT_UNIQUE + &&& final(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).slot_owners[i].storage_perm() == old( regions, ).slot_owners[i].storage_perm() @@ -758,17 +751,13 @@ pub(super) proof fn cursor_mut_regions_step<'rcu>( forall|i: int| #![trigger final(regions).slot_owners[i]] old(regions).slot_owners[i].usage is Frame ==> { - &&& final(regions).slot_owners[i].ref_count() + old( - regions, - ).slot_owners[i].paths_in_pt.len() == old(regions).slot_owners[i].ref_count() - + final(regions).slot_owners[i].paths_in_pt.len() - &&& final(regions).slot_owners[i].ref_count() <= old( - regions, - ).slot_owners[i].ref_count() + &&& final(regions).ref_count(i) + old(regions).slot_owners[i].paths_in_pt.len() + == old(regions).ref_count(i) + final(regions).slot_owners[i].paths_in_pt.len() + &&& final(regions).ref_count(i) <= old(regions).ref_count(i) &&& final(regions).slot_owners[i].paths_in_pt.len() <= old( regions, ).slot_owners[i].paths_in_pt.len() - &&& final(regions).slot_owners[i].ref_count() != 0 + &&& final(regions).ref_count(i) != 0 }, forall|i: int| #![trigger final(regions).slot_owners[i]] @@ -814,14 +803,12 @@ pub(super) proof fn map_step<'rcu>( final(regions).slot_owners[i].in_list_perm == old(regions).slot_owners[i].in_list_perm, forall|i: int| #![trigger final(regions).slot_owners[i]] - i != frame_to_index(paddr) && old(regions).slot_owners[i].ref_count() - != REF_COUNT_UNUSED ==> final(regions).slot_owners[i] == old( - regions, - ).slot_owners[i], + i != frame_to_index(paddr) && old(regions).ref_count(i) != REF_COUNT_UNUSED + ==> final(regions).slot_owners[i] == old(regions).slot_owners[i], forall|i: int| - #![trigger final(regions).slot_owners[i].ref_count()] - old(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED - ==> final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED, + #![trigger final(regions).ref_count(i)] + old(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).ref_count(i) + != REF_COUNT_UNUSED, final(regions).slot_owner(paddr).ref_count() == old(regions).slot_owner(paddr).ref_count(), final(regions).slot_owner(paddr).paths_in_pt.len() == old(regions).slot_owner( paddr, @@ -832,12 +819,12 @@ pub(super) proof fn map_step<'rcu>( ).storage_perm(), forall|i: int| #![trigger final(regions).slot_owners[i]] - final(regions).slot_owners[i].ref_count() == REF_COUNT_UNUSED - ==> final(regions).slot_owners[i] == old(regions).slot_owners[i], + final(regions).ref_count(i) == REF_COUNT_UNUSED ==> final(regions).slot_owners[i] + == old(regions).slot_owners[i], forall|i: int| #![trigger final(regions).slot_owners[i]] - i != frame_to_index(paddr) && old(regions).slot_owners[i].ref_count() - == REF_COUNT_UNUSED && final(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + i != frame_to_index(paddr) && old(regions).ref_count(i) == REF_COUNT_UNUSED + && final(regions).ref_count(i) != REF_COUNT_UNUSED ==> final(regions).slot_owners[i].usage !is Frame, forall|c: CursorOwner<'rcu, UserPtConfig>| #![auto] diff --git a/ostd/specs/mm/embedding/frame.rs b/ostd/specs/mm/embedding/frame.rs index d71be29bb..20745d1fb 100644 --- a/ostd/specs/mm/embedding/frame.rs +++ b/ostd/specs/mm/embedding/frame.rs @@ -89,11 +89,7 @@ pub axiom fn frame_from_in_use_embedded( ensures final(regions).inv(), !valid_frame_paddr(paddr) ==> res is None, - res is Some ==> MetaSlot::inc_frame_reference_region_spec( - paddr, - *old(regions), - *final(regions), - ), + res is Some ==> old(regions).inc_frame_reference_region_spec(paddr, *final(regions)), res is None ==> *final(regions) == *old(regions), res is Some ==> { let so = final(regions).slot_owner(paddr); @@ -203,11 +199,7 @@ pub(super) proof fn from_in_use_step( final(regions).inv(), !valid_frame_paddr(paddr) ==> res is None, res matches Some(e) ==> e.paddr == paddr, - res is Some ==> MetaSlot::inc_frame_reference_region_spec( - paddr, - *old(regions), - *final(regions), - ), + res is Some ==> old(regions).inc_frame_reference_region_spec(paddr, *final(regions)), res is None ==> *final(regions) == *old(regions), res is Some ==> { let so = final(regions).slot_owner(paddr); diff --git a/ostd/specs/mm/embedding/mod.rs b/ostd/specs/mm/embedding/mod.rs index 6c2386767..9a872bab1 100644 --- a/ostd/specs/mm/embedding/mod.rs +++ b/ostd/specs/mm/embedding/mod.rs @@ -28,126 +28,6 @@ //! `ensures` clause of one public exec function. Naming is the only //! mechanism keeping the axiom in sync with its exec counterpart; //! reviewers touching either side should grep for the partner. -//! -//! # Roadmap — DONE / open work -//! -//! All five originally-deferred items have landed. Shape B for -//! segments is fully active: `Op::SegmentFromUnused` / -//! `Op::SegmentDrop` are in the dispatch, [`accounting_inv`] has the -//! generalised `rc == H + P + cover_count` equation, and -//! [`structural_inv`] carries `raw_count == segment_cover_count` + -//! segment-covered ⟹ Frame-usage + segment range well-formedness. -//! -//! 1. **Strengthen [`crate::specs::mm::frame::meta_owners::MetaSlotOwner::inv`]'s -//! SHARED branch** — DONE. The branch (`0 < rc <= REF_COUNT_MAX`) -//! now carries `storage_perm().is_init()` and -//! `in_list_perm.value() == 0`. The `rc == 1 ⟹ ...` guards -//! on `storage`/`in_list` in -//! [`crate::mm::frame::Frame::drop_requires`] were dropped. -//! -//! 2. **`Frame::wf(state)`** — DONE at both layers. -//! - **Embedding layer**: [`lemma_frame_drop_pre_derivable`] -//! derives all of [`frame::drop_pre`]'s residuals (rc not in -//! sentinels, `rc <= REF_COUNT_MAX`, `storage.is_init`, -//! `in_list == 0`, `rc == 1 ⟹ paths empty`) plus the -//! `rc == 1 ⟹ handle_count == 1` clause from `s.inv()` + the -//! `FrameEntry`'s registration + the segment-cover hypothesis. -//! `op_pre[FrameDrop]` and `lemma_step_frame_drop` shrink to -//! id-existence + segment-cover only. -//! - **Exec layer**: [`crate::mm::frame::Frame::wf_with_region`] -//! packages the per-handle cross-object validity (slot/pointer -//! identity + SHARED rc bounds — `> 0 ∧ ≠ UNUSED ∧ ≠ UNIQUE -//! ∧ ≤ MAX`). `Frame::drop_requires` is refactored to read -//! `self.wf_with_region(s) ∧ raw_count == 0 ∧ rc == 1 ⟹ paths empty`, -//! which keeps the drop-specific bits explicit while -//! consolidating the static "this Frame is valid against -//! this state" conjuncts. -//! - `clone_requires` not refactored: would cascade into -//! `PageTableConfig::lemma_clone_requires_concrete` (a trait method -//! with multiple implementors); left explicit to keep the -//! change local. -//! - **Preservation of `wf_with_region` (FUTURE).** `Frame::wf_with_region`'s -//! preservation across drops of *other* handles at the same slot -//! is currently informal (claimed in the docstring; no -//! machine-checked proof). To prove it, every `Frame` needs a -//! tracked ghost "reference-count share" certificate that proves -//! "I contribute 1 to my slot's `rc`," combined with an aggregate -//! invariant on `MetaSlotOwner` saying `held_shares == rc.value()`. -//! Recommended primitive: -//! [`vstd_extra::resource::ghost_resource::count_ghost::Token`] -//! (alias for `CountGhost<(), TOTAL>`) with -//! `TOTAL = REF_COUNT_MAX`. The resource framework provides -//! `split` / `combine` / `agree` / `bounded` pre-proven; the -//! Frame side adds a `Tracked>` field and the -//! `MetaSlotOwner` side adds a -//! `CountGhostResource<(), MAX>` aggregate of remaining shares -//! with the linking invariant `rc.value() + remaining == MAX`. -//! Cursor map / unmap axioms gain share-juggling clauses -//! (`paths_in_pt += 1` ↔ split off 1 share). The math is proven -//! by vstd_extra; the integration is a multi-day refactor with -//! cascading effects on every `MetaRegionOwners` consumer. -//! The embedding's `handle_count` already provides the equivalent -//! property at the abstract level, so this is only needed if -//! downstream code outside the embedding's tracking needs -//! `Frame::wf_with_region` preservation proofs. -//! -//! 3a. **Op::Map consumes a `FrameId`** — DONE. `Op::Map { c, fid, -//! prop }` extracts the matching `FrameEntry` (so `H` at the -//! mapped slot decrements by 1, paired with the cursor axiom's -//! `paths_in_pt += 1` at the same slot). Combined with the -//! `cursor_mut_map_embedded` axiom's per-slot ensures (rc / usage -//! / storage preserved at target, changed-slots ⟹ PT-node ⟹ -//! `usage != Frame`), [`accounting_inv`]'s Frame-scoped equation -//! `rc == H + P` chains. -//! -//! 3b. **Op::Query clone modeling** — DONE. The `cursor_query_embedded` -//! axiom now returns `Option`: `Some(paddr)` when query -//! resolved a tracked leaf and `clone_item` bumped `rc` at that -//! slot; `None` otherwise (out-of-range / no leaf / MMIO leaf). -//! [`lemma_step_query`] consumes the paddr to register a fresh -//! `FrameEntry` so `H` at the cloned slot grows in lockstep with -//! `rc`, keeping `accounting_inv`'s `rc == H + P` chained. -//! -//! 4. **Strengthen `cursor_mut_unmap_embedded`** — DONE. The axiom -//! now mirrors exec: universal preservation of -//! `raw_count`/`in_list`/`usage`/`slot_vaddr`/`vtable_ptr`; -//! storage preserved at slots ending non-UNUSED; rc doesn't bump -//! to UNIQUE; at Frame slots the "non-mapping count" -//! `rc - paths.len` is invariant with both monotonically non- -//! increasing, and post `rc != 0` (Frame teardown collapses -//! `rc==0` to `REF_COUNT_UNUSED` atomically); MMIO slots are -//! untouched (preserving the `MetaSlotOwner::inv` MMIO exception -//! that allows non-empty `paths_in_pt` at UNUSED). -//! [`lemma_step_unmap`] discharges accounting via case-splits on -//! Frame / non-Frame / MMIO. -//! -//! 5. **Shape-B segments** — base + split landed; the rest is -//! documented with status per op. -//! - **`from_unused` / `drop`** — DONE. Allocate a segment over a -//! contiguous range of UNUSED frames, and release the segment's -//! forgotten references with per-frame teardown. -//! - **`split`** — DONE. Partition the segment at a page-aligned -//! offset; `regions` is unchanged because per-paddr -//! `cover_count` is invariant under the partition. -//! [`lemma_segment_cover_split`] proves the per-paddr -//! invariance. -//! - **`clone`** — DONE. Produce a second handle covering the same -//! range as `sid`; per covered paddr `cover_count += 1` and -//! `rc += 1` (`H` unchanged), so the accounting equation chains. -//! - **`next`** — DONE. The conversion bridge between -//! segment-held forgotten references and user-held `Frame` -//! handles. Per-paddr at the popped slot: `raw_count -= 1`, -//! `cover_count -= 1`, `H += 1`, `rc` unchanged. The -//! accounting equation `rc == H + P + cover_count` chains in -//! lockstep because H and cover decrement/increment together; -//! structural `raw_count == cover_count` chains via -//! [`lemma_segment_cover_shrink_front`]. -//! - **`slice`** — DONE. Like `clone` but over a sub-range: insert a -//! fresh `SegmentEntry` covering `sub_range` and bump `cover_count` -//! / `rc` for each frame inside it. `clone` is the special case -//! `sub_range == sid`'s range. -//! - **`into_raw` / `from_raw`** — `pub(crate)` only in exec, so -//! the embedding can ignore them. pub mod cursor; pub mod frame; pub mod io; @@ -227,12 +107,6 @@ pub tracked struct FrameEntry { /// Per-Segment entry in the store. Represents one outstanding /// `Segment` covering the contiguous physical range `range`. /// -/// Per exec [`Segment::relate_regions`], every frame in `range` is owned by -/// the segment. The frame's `ref_count >= 1` is bumped by that reference -/// (one per frame); the segment does *not* hold a separate `Frame` -/// handle, so the embedding's `frames` map is unrelated to per-segment -/// frame refcounting. -/// /// Multiple `SegmentEntry`s may overlap (e.g. after `clone`); each /// independently contributes `+1` to every covered slot's obligation /// count and ref_count`. @@ -242,26 +116,13 @@ pub tracked struct SegmentEntry { pub ghost range: Range, } -/// Per-`UniqueFrame` entry in the store. Represents the sole exclusive -/// handle to the slot at `paddr` — i.e., the slot is held at the -/// `REF_COUNT_UNIQUE` sentinel with no shared users (no `FrameEntry`, -/// no `SegmentEntry` coverage, no live PTE). At most one `UniqueEntry` -/// exists per slot (enforced by [`VmStore::structural_inv`]'s -/// injectivity clause), mirroring the exec exclusivity of -/// `UniqueFrame`. +/// Per-`UniqueFrame` entry in the store. pub tracked struct UniqueEntry { pub ghost paddr: Paddr, } /// Number of outstanding `Segment` handles covering the frame slot -/// at `paddr` — i.e., `#{ sid : segments[sid].range covers paddr }`. -/// This is the per-slot `raw_count` term contributed by segments -/// (Design B: each segment holds one forgotten reference per frame -/// in its range, so `raw_count == segment_cover_count(segments, ...)`). -/// Intended to be called on page-aligned paddrs (e.g. via -/// `index_to_frame(idx)`); segment ranges are themselves page- -/// aligned so the resulting count is the same for any paddr within -/// a given page. +/// at `paddr`. pub open spec fn segment_cover_count(segments: Map, paddr: Paddr) -> nat { segments.dom().filter( |sid: SegmentId| segments[sid].range.start <= paddr && paddr < segments[sid].range.end, @@ -269,9 +130,7 @@ pub open spec fn segment_cover_count(segments: Map, pad } /// A positive segment-cover count exhibits a witnessing segment id whose -/// range covers `paddr`. Used to lift `segment_cover_count(..) > 0` into -/// the structural `covered ⟹ usage == Frame` clause (which is keyed by a -/// concrete `(sid, paddr)`), replacing the retired `raw_count` cache. +/// range covers `paddr`. pub proof fn lemma_segment_cover_witness( segments: Map, paddr: Paddr, @@ -279,7 +138,7 @@ pub proof fn lemma_segment_cover_witness( requires segment_cover_count(segments, paddr) > 0, ensures - segments.dom().contains(sid), + segments.contains_key(sid), segments[sid].range.start <= paddr < segments[sid].range.end, { let covering = segments.dom().filter( @@ -291,9 +150,7 @@ pub proof fn lemma_segment_cover_witness( } /// Number of outstanding `Frame` handles whose paddr maps to slot -/// `idx` — i.e. the `#handles(idx)` term of the exact reference-count -/// accounting `ref_count(idx) == #handles(idx) + paths_in_pt(idx).len()` -/// (Stage 5 / full #4). +/// `idx`. pub open spec fn handle_count(frames: Map, idx: int) -> nat { frames.dom().filter(|fid: FrameId| frame_to_index(frames[fid].paddr) == idx).len() } @@ -309,7 +166,7 @@ pub proof fn lemma_handle_count_insert_fresh( idx: int, ) requires - !frames.dom().contains(id), + !frames.contains_key(id), ensures handle_count(frames.insert(id, entry), idx) == handle_count(frames, idx) + ( if frame_to_index(entry.paddr) == idx { @@ -368,7 +225,7 @@ pub proof fn lemma_handle_count_insert_fresh( /// at `fid` mapped to `idx`), unchanged elsewhere. pub proof fn lemma_handle_count_remove(frames: Map, fid: FrameId, idx: int) requires - frames.dom().contains(fid), + frames.contains_key(fid), ensures handle_count(frames.remove(fid), idx) == handle_count(frames, idx) - (if frame_to_index( frames[fid].paddr, @@ -417,39 +274,10 @@ pub proof fn lemma_handle_count_remove(frames: Map, fid: Fr } } -/// **Embedding-level `Frame::wf(state)`.** Derives the full -/// [`frame::drop_pre`] residual (rc / storage / in_list / paths-empty -/// conjuncts) plus the `rc == 1 ⟹ handle_count == 1` clause from -/// `s.inv()`, given only: -/// - `fid` is a registered handle, -/// - no `SegmentEntry` covers the slot -/// (`segment_cover_count == 0`). -/// -/// Replaces the residual `drop_pre` baggage on `op_pre[FrameDrop]` / -/// `lemma_step_frame_drop` with a single tracked invariant chain. Every -/// conjunct is recovered from a specific `VmStore::inv` clause: -/// - `slots.contains_key`: structural slot-perm coverage. -/// - `raw_count == 0`: structural `raw_count == segment_cover_count` -/// + the `segment_cover_count == 0` hypothesis. -/// - `rc > 0` / `rc != UNUSED` / `rc != UNIQUE` / `rc == H + P`: -/// accounting clause 4 (active head: H >= 1 since `fid` is -/// registered + structural FrameId⟹Frame-usage). -/// - `rc <= REF_COUNT_MAX`: clause 4 (`rc != UNIQUE`) + -/// `MetaSlotOwner::inv`'s forbidden-range empty -/// (`MAX < rc < UNIQUE ⟹ false`). -/// - `rc == 1 ⟹ storage.is_init ∧ in_list == 0`: -/// `MetaSlotOwner::inv`'s SHARED branch (`0 < rc <= MAX`), -/// which is the Item 1 strengthening. -/// - `rc == 1 ⟹ paths_in_pt.is_empty()`: clause 4 + `H >= 1` -/// gives `1 == H + P` ⟹ `P == 0` ⟹ `paths.len == 0` ⟹ -/// `paths.is_empty()`. -/// - `rc == 1 ⟹ handle_count == 1`: clause 4 with `rc == 1` -/// gives `1 == H + P`; with `H >= 1` and `P >= 0`, `H == 1` -/// and `P == 0`. pub proof fn lemma_frame_drop_pre_derivable<'rcu>(s: VmStore<'rcu>, fid: FrameId) requires s.inv(), - s.frames.dom().contains(fid), + s.frames.contains_key(fid), segment_cover_count(s.segments, s.frames[fid].paddr) == 0, ensures frame::drop_pre(s.regions, s.frames[fid].paddr), @@ -458,6 +286,8 @@ pub proof fn lemma_frame_drop_pre_derivable<'rcu>(s: VmStore<'rcu>, fid: FrameId frame_to_index(s.frames[fid].paddr), ) == 1, { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let paddr = s.frames[fid].paddr; let idx = frame_to_index(paddr); assert(s.regions.slot_owners[idx].ref_count() @@ -475,23 +305,6 @@ pub enum VmIoKind { } /// Per-VmIo entry in the store. -/// -/// `vm_space` is `None` for VmIoOwners that have no parent `VmSpace` — -/// kernel-space readers/writers from `VmReader::from_kernel_space` / -/// `VmWriter::from_kernel_space`, and val_owners produced by -/// `read`. `Some(vs)` for entries created by `VmSpace::reader` / -/// `writer`. -/// -/// View state is fully determined by `vm_space` + `kind`: -/// - `Some(_)` (userspace, Fallible): `mem_view: None`, exactly as -/// `VmSpace::reader`/`writer` ensure ([vm_space.rs:323/382](crate::mm::vm_space)). -/// Fallible methods are handle-only — no owner-side activation step -/// exists or is needed. -/// - `None && Reader` (kernel reader): `read_view_initialized()`, per -/// `VmReader::from_kernel_space` ensures. -/// - `None && Writer` (kernel writer or `consumed_w` val_owner from -/// `read`): `has_write_view()`, per `from_kernel_space` / -/// [`io::read_step`] ensures. pub tracked struct VmIoEntry { pub ghost vm_space: Option, pub ghost kind: VmIoKind, @@ -513,16 +326,6 @@ impl VmIoEntry { } } - /// Operand-typing for the Infallible `read`/`write` ops. Exec - /// `VmReader::::read` / `VmWriter::::write` - /// are *typed* on kernel (`Infallible`) reader/writer handles; the - /// embedding proxies "kernel/Infallible" with `vm_space is None` and - /// reader-vs-writer with `kind`. These are not runtime preconditions - /// — a userspace (Fallible) handle simply cannot be passed where the - /// type system demands a kernel one — so they read as a - /// well-formedness check on the operand, not a checkable obligation. - /// (`inv` already gives `read_view_initialized` / `has_write_view` - /// for these cases, exactly what `vm_reader_read_embedded` consumes.) pub open spec fn is_kernel_reader(self) -> bool { &&& self.vm_space is None &&& self.kind == VmIoKind::Reader @@ -557,13 +360,6 @@ pub tracked struct CursorEntry<'rcu> { } impl<'rcu> CursorEntry<'rcu> { - /// The portion of the exec `Cursor::invariants(owner, regions, guards)` - /// expressible from the entry alone (no `regions`). - /// - /// Mirrors `crate::mm::page_table::Cursor::invariants` minus - /// `regions.inv()`, `metaregion_sound(regions)`, and the exec-handle - /// pieces (`self.inv()` / `self.wf(owner)`). Those live in - /// [`VmStore::inv`] (regions-touching) and are MODEL GAPS (handle). pub open spec fn inv(self) -> bool { &&& self.owner.inv() &&& self.owner.children_not_locked(self.guards) @@ -574,11 +370,6 @@ impl<'rcu> CursorEntry<'rcu> { /// Resource store: the abstract state visible to a caller of the /// VmSpace + VmReader/VmWriter API. -/// -/// `tlb_model` is the global TLB model; mirrors the per-CPU `TlbModel` -/// that `CursorMut::map`/`unmap` and `flusher` operate on. We keep one -/// per store on the conservative assumption that any cursor mutation -/// interacts with it. pub tracked struct VmStore<'rcu> { pub regions: MetaRegionOwners, pub tlb_model: TlbModel, @@ -592,17 +383,8 @@ pub tracked struct VmStore<'rcu> { impl<'a, 'rcu> VmStore<'rcu> { /// The store's top-level invariant. - /// - /// Decomposed into [`structural_inv`] (everything generic store - /// helpers can preserve when they only touch one of `frames` / - /// `cursors` / `vm_ios` / `vm_spaces`) and [`accounting_inv`] (the - /// exact reference-count equation, which couples `frames` with - /// `regions.slot_owners` and can only be re-established by a *step* - /// that pairs the two changes — see [`tracked_extract_frame`] / - /// [`lemma_insert_frame`] for why the frame-only helpers must require / - /// ensure only the structural part). pub open spec fn inv(self) -> bool { - self.structural_inv() && self.accounting_inv() + self.structural_inv() && self.accounting_inv() && self.regions.inv() } /// Everything in [`inv`] **except** the accounting equation. @@ -610,226 +392,74 @@ impl<'a, 'rcu> VmStore<'rcu> { /// `regions.slot_owners`, since the accounting equation is the only /// clause that mentions both. Frame-only helpers /// ([`tracked_extract_frame`] / [`lemma_insert_frame`]) require / ensure this. + #[verifier::opaque] pub open spec fn structural_inv(self) -> bool { - &&& self.regions.inv() - // Slot-perm coverage (Design B). Every in-region slot keeps its - // `simple_pptr::PointsTo` parked in `regions.slots`. - // `MetaRegionOwners::inv` only gives the *forward* direction - // (`slots.contains_key(i) ==> 0 <= i < max_meta_slots()`); the reverse - // is NOT globally true (`UniqueFrame` / `into_raw` / linked-list - // permanently extract a slot perm). It IS true here because the - // embedding's `Op` surface contains *no* perm-extracting - // operation: `FrameFromUnused` re-parks the perm (modeled in - // [`frame::frame_from_unused_embedded`]), `FrameFromInUse` / - // `FrameDrop` / `Segment` only shared-borrow it, and every - // region-mutating cursor op (`Map`/`Unmap`/`ProtectNext`) touches - // `slot_owners` (refcount / `paths_in_pt`) but never the `slots` - // map domain. This is what lets [`op_pre`] for `FrameFromUnused` - // / `FrameFromInUse` be literally `true` (#2 / #3b fully - // resolved): the `valid_frame_paddr`-guarded slot-perm precondition - // of the relaxed exec / axiom is recovered from this clause for - // the in-bound case and is vacuous out-of-bound. - // Slot-perm coverage exception for page-table nodes: a slot whose - // perm is NOT parked in `regions.slots` must be a page-table node - // (`usage == PageTable`). This is exactly the new user PT *root* - // allocated by `VmSpace::new` (`empty_with_owner` permanently - // extracts the root's slot perm into the page table; see - // [`vm_space::vm_space_new_embedded`]). Phrased in terms of - // `regions` alone (NOT `vm_spaces` membership), so a `VmSpace` - // drop — which never re-parks the root (there is no exec `Drop`) - // and leaves `regions` untouched — preserves it for free, and any - // op that preserves `usage` preserves the exception. Data-frame - // ops recover `slots.contains_key` from this clause: a - // `usage == Frame` slot fails the exception, so its perm is - // parked; ops on possibly-unparked slots (frame/segment - // `from_unused`, `from_in_use`) instead guard on - // `slots.contains_key` directly. &&& forall|idx: int| 0 <= idx < max_meta_slots() ==> #[trigger] self.regions.slots.contains_key(idx) || ( self.regions.slot_owners[idx].usage is PageTable - && self.regions.slot_owners[idx].ref_count() - != REF_COUNT_UNUSED) - // Segment-cover info is sourced directly from the `segments` map - // via `segment_cover_count` (see `accounting_inv`'s rc equation). - // The per-slot `raw_count` cache that previously mirrored it has - // been retired. + && self.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED) &&& forall|idx: int| 0 <= idx < max_meta_slots() ==> #[trigger] self.regions.slot_owners[idx].in_list_perm.value() == 0 &&& self.tlb_model.inv() &&& forall|id: VmSpaceId| #[trigger] - self.vm_spaces.dom().contains(id) ==> self.vm_spaces[id].inv() + self.vm_spaces.contains_key(id) ==> self.vm_spaces[id].inv() + &&& forall|id: CursorId| #[trigger] self.cursors.contains_key(id) ==> self.cursors[id].inv() &&& forall|id: CursorId| #[trigger] - self.cursors.dom().contains(id) ==> self.cursors[id].inv() - &&& forall|id: CursorId| #[trigger] - self.cursors.dom().contains(id) ==> self.cursors[id].owner.metaregion_sound( - self.regions, - ) + self.cursors.contains_key(id) ==> self.cursors[id].owner.metaregion_sound(self.regions) &&& forall|id: CursorId| #[trigger] - self.cursors.dom().contains(id) ==> self.vm_spaces.dom().contains( - self.cursors[id].vm_space, - ) - &&& forall|id: VmIoId| #[trigger] self.vm_ios.dom().contains(id) ==> self.vm_ios[id].inv() + self.cursors.contains_key(id) ==> self.vm_spaces.contains_key(self.cursors[id].vm_space) + &&& forall|id: VmIoId| #[trigger] self.vm_ios.contains_key(id) ==> self.vm_ios[id].inv() &&& forall|id: VmIoId| #[trigger] - self.vm_ios.dom().contains(id) ==> (self.vm_ios[id].vm_space matches Some(vs) - ==> self.vm_spaces.dom().contains(vs)) + self.vm_ios.contains_key(id) ==> (self.vm_ios[id].vm_space matches Some(vs) + ==> self.vm_spaces.contains_key(vs)) &&& forall|id: VmIoId| #[trigger] - self.vm_ios.dom().contains(id) ==> self.vm_ios[id].vm_space is Some ==> ( + self.vm_ios.contains_key(id) ==> self.vm_ios[id].vm_space is Some ==> ( self.vm_ios[id].vaddr as nat) + (self.vm_ios[id].len as nat) <= MAX_USERSPACE_VADDR as nat - // `frames` is bookkeeping for outstanding `Frame` handles. Every - // registered handle came from a *successful* `from_unused` / - // `from_in_use`, which (post-relaxation) returns `None` unless - // `valid_frame_paddr(paddr)` — so every live `FrameEntry`'s paddr is - // in-bound. With the slot-perm / `raw_count` / `in_list` - // coverage clauses above, this discharges `drop_pre`'s - // `slots.contains_key` (#4-a), `raw_count == 0` (#4-b), - // `!= REF_COUNT_UNUSED` (#4-d, from the bound), and the - // `in_list == 0` half of the last-ref conjunct (#4-f). &&& forall|fid: FrameId| #[trigger] - self.frames.dom().contains(fid) ==> valid_frame_paddr( - self.frames[fid].paddr, - ) - // Every registered handle's slot has `usage is Frame`. - // True by construction: every `Op` that adds a `FrameId` - // (`FrameFromUnused`, `FrameFromInUse`, `Query` on a tracked - // leaf) commits to a Frame-usage slot. Carrying this in - // `structural_inv` makes accounting_inv's Frame-scoped clauses - // apply automatically at registered handles' paddrs and - // simplifies `op_pre[Map]` / `lemma_step_query` / the Item 4 unmap - // axiom (no need for the caller to re-establish usage). + self.frames.contains_key(fid) ==> valid_frame_paddr(self.frames[fid].paddr) &&& forall|fid: FrameId| #[trigger] - self.frames.dom().contains(fid) ==> self.regions.slot_owner( + self.frames.contains_key(fid) ==> self.regions.slot_owner( self.frames[fid].paddr, ).usage is Frame - // Every registered segment has a well-formed range - // (page-aligned, in-bound, non-empty). Enforced by - // `op_pre[SegmentFromUnused]`; carried as an invariant so - // `lemma_step_segment_drop` can discharge `segment::drop_step`'s - // alignment preconditions from `s.inv()` alone. &&& forall|sid: SegmentId| #[trigger] - self.segments.dom().contains(sid) ==> { + self.segments.contains_key(sid) ==> { let r = self.segments[sid].range; &&& r.start % PAGE_SIZE == 0 &&& r.end % PAGE_SIZE == 0 &&& r.start < r.end &&& r.end <= MAX_PADDR } - // Every segment-covered slot has `usage is Frame`. - // True by construction: `Op::SegmentFromUnused` sets the - // covered slots' usage to Frame, and no op transitions a - // segment-covered slot back to non-Frame (frame_drop is gated - // on `segment_cover_count == 0` via `op_pre[FrameDrop]`). - // Carried here so `lemma_step_segment_drop` can derive the per-slot - // SHARED+Frame conditions from `s.inv()` alone. &&& forall|sid: SegmentId, paddr: Paddr| #![trigger - self.segments.dom().contains(sid), + self.segments.contains_key(sid), frame_to_index(paddr)] - self.segments.dom().contains(sid) && self.segments[sid].range.start <= paddr + self.segments.contains_key(sid) && self.segments[sid].range.start <= paddr < self.segments[sid].range.end && paddr % PAGE_SIZE == 0 - ==> self.regions.slot_owner( - paddr, - ).usage is Frame - // `unique_frames.dom()` is finite (built by finitely many - // `lemma_insert_unique`), needed wherever the embedding reasons - // about the unique-handle set as a whole. - // Every registered `UniqueEntry`'s paddr is in-bound. + ==> self.regions.slot_owner(paddr).usage is Frame &&& forall|uid: UniqueId| #[trigger] - self.unique_frames.dom().contains(uid) ==> valid_frame_paddr( + self.unique_frames.contains_key(uid) ==> valid_frame_paddr( self.unique_frames[uid].paddr, ) - // Every `UniqueEntry`'s slot is held exclusively: a `Frame`-usage - // slot at the `REF_COUNT_UNIQUE` sentinel, off the free-list - // (`in_list == 0`) and with no PTE mappings. (Storage-init is - // recovered on demand from `MetaSlotOwner::inv`'s UNIQUE branch.) &&& forall|uid: UniqueId| #[trigger] - self.unique_frames.dom().contains(uid) ==> { + self.unique_frames.contains_key(uid) ==> { let so = self.regions.slot_owner(self.unique_frames[uid].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE &&& so.in_list_perm.value() == 0 &&& so.paths_in_pt.is_empty() } - // At most one `UniqueEntry` per slot — the exclusivity of - // `UniqueFrame`. Keeps `Op::UniqueDrop` well-defined: tearing - // down a unique slot cannot leave a second entry dangling at it. &&& forall|uid1: UniqueId, uid2: UniqueId| #![trigger - self.unique_frames.dom().contains(uid1), - self.unique_frames.dom().contains(uid2)] - self.unique_frames.dom().contains(uid1) && self.unique_frames.dom().contains(uid2) + self.unique_frames.contains_key(uid1), + self.unique_frames.contains_key(uid2)] + self.unique_frames.contains_key(uid1) && self.unique_frames.contains_key(uid2) && self.unique_frames[uid1].paddr == self.unique_frames[uid2].paddr ==> uid1 == uid2 } - /// Stage 5 / full #4 — EXACT reference-count accounting. - /// - /// Scoped to *active-head* tracked data frames: `usage == Frame` - /// (excludes PT nodes — different rc semantics — and MMIO), and the - /// slot is an active head (`#handles > 0 || #mappings > 0`). The - /// active-head restriction sidesteps huge-page sub-page slots - /// (j>0): those have `H==0`, `paths.len()==0`, yet `rc>0` via - /// `frame_sub_pages_valid`, so they are *not* active heads and the - /// equation does not apply to them (and `op_pre[FrameDrop]` never - /// targets a sub-page — a `FrameEntry` paddr is always a head). - /// - /// For an active head: `rc` is neither sentinel, equals - /// `#handles + #mappings`, and the slot's metadata storage is - /// initialised (it is in use). - /// - /// The exact equation is *Frame-scoped*. For non-Frame `FrameEntry` - /// slots, the residual `drop_pre` obligation (rc/storage/in_list/ - /// paths) is carried directly in `op_pre[FrameDrop]` (un-doing - /// part of #4) until the deferred main-verification refactor - /// strengthens `MetaSlotOwner::inv` and adds `Frame::wf(state)`. - /// - /// **Why split from `structural_inv`:** the equation references - /// *both* `self.frames` (via `handle_count`) *and* - /// `self.regions.slot_owners` (via `rc` and `paths_in_pt`), so any - /// helper that mutates one without the other can break it - /// transiently. The frame-only store helpers [`tracked_extract_frame`] / - /// [`lemma_insert_frame`] therefore cannot ensure this clause alone — a - /// step that pairs a frame change with the matching regions change - /// (via a frame / cursor `_embedded` axiom) re-establishes it. + #[verifier::opaque] pub open spec fn accounting_inv(self) -> bool { - // Stage 5.5c absorption clauses (couple `frames` + `regions`). - // - // The earlier usage-independent **handle clause** (Stage 5 / 2b, - // `H > 0 ⟹ rc ∉ {UNUSED, UNIQUE} ∧ rc ≥ H ∧ storage.is_init`) - // was **dropped**. Two reasons: - // - // (a) It was load-bearing only via Verus SMT heuristics across - // `step_cursor_method`/`lemma_step_map`/`lemma_step_unmap`: the cursor - // `_embedded` axioms don't actually constrain `rc`/`storage` - // at the touched slot, and accounting_inv preservation - // across those steps was working by coincidence. Segments - // (Shape B) perturbed the SMT context and broke the chain - // — the fragility was always there. - // - // (b) The semantically right home for these conjuncts is the - // *exec layer*: `MetaSlotOwner::inv`'s SHARED branch should - // carry `storage.is_init() ∧ in_list.value() == 0` for any - // in-use rc (they're universally true, see the lifecycle - // analysis), and `Frame` should have a `wf(state)` - // predicate carrying "the slot I refer to is in a valid - // state with `rc ≥ handles_for_this_slot`." Then the - // embedding's accounting could shrink to just the - // Frame-scoped equation (clauses below), and the cursor - // axiom interaction goes away because there's nothing - // handle-keyed to chain. - // - // Until the main-verification refactor in (b) lands, - // `op_pre[FrameDrop]` carries the full residual `drop_pre` - // directly. Frame-usage callers discharge it from the - // Frame-scoped equation clauses below; non-Frame callers carry - // their own reasoning. - // - // See the TODO in segment.rs for the full plan. - // **UNUSED ⟹ no users.** A live PTE bumps `rc`, so reaching - // `UNUSED` requires `paths_in_pt.is_empty()`. With segments - // (Shape B), reaching UNUSED also requires no segment covers - // the slot — each segment contributes 1 to rc via its - // forgotten Frame handle. &&& forall|idx: int| #![trigger self.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && self.regions.slot_owners[idx].ref_count() @@ -837,12 +467,7 @@ impl<'a, 'rcu> VmStore<'rcu> { && self.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count( self.segments, index_to_frame(idx), - ) - == 0 - // **Frame in valid rc range ⟹ active head.** Inverse of the - // active-head guard below — absorbs the pre-active-head assume - // in `lemma_step_frame_from_in_use`. With segments, "active" includes - // the segment-cover contribution. + ) == 0 &&& forall|idx: int| #![trigger self.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && self.regions.slot_owners[idx].usage is Frame @@ -853,15 +478,7 @@ impl<'a, 'rcu> VmStore<'rcu> { ) > 0 || self.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count( self.segments, index_to_frame(idx), - ) - > 0 - // **Frame-slot accounting equation.** Generalised to include - // segment forgotten references: `rc == H + P + cover_count`. - // Each segment in `segments` whose range covers the frame - // contributes +1 to `rc` (via its `ManuallyDrop`'d Frame - // handle); user-held handles contribute via `H`; live PTEs - // contribute via `P`. With `segments` empty (pre-activation), - // `cover_count == 0` and the equation reduces to `rc == H + P`. + ) > 0 &&& forall|idx: int| #![trigger self.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && self.regions.slot_owners[idx].usage is Frame && ( @@ -901,9 +518,7 @@ pub enum Op { NewKernelWriter { vaddr: Vaddr, len: usize }, DropReader { vio: VmIoId }, DropWriter { vio: VmIoId }, - /// Fallible `VmReader::read_val`. The exec spec carries no - /// tracked owner params (handle MODEL GAP); the embedding step - /// is consequently a no-op on `VmStore`. + /// Fallible `VmReader::read_val`. ReaderReadVal { source: VmIoId }, /// Fallible `VmReader::collect`. Same shape as `ReaderReadVal`. ReaderCollect { source: VmIoId }, @@ -919,91 +534,49 @@ pub enum Op { /// Infallible `VmReader::read`. Produces a `consumed_w` val_owner /// (registered as a fresh activated Writer entry). Read { source: VmIoId, dest: VmIoId }, - /// Infallible `VmWriter::write`. The exec no longer surfaces - /// `consumed_w`; the embedding does NOT create a fresh entry. + /// Infallible `VmWriter::write`. Write { source: VmIoId, dest: VmIoId }, /// `Frame::from_unused`: try to allocate a fresh handle on a - /// previously-unused slot. Registers a [`FrameEntry`] on success. + /// previously-unused slot. FrameFromUnused { paddr: Paddr }, /// `Frame::from_in_use`: try to acquire a new handle on an - /// in-use slot. Registers a [`FrameEntry`] on success - /// (refcount of the slot increments by one). + /// in-use slot. FrameFromInUse { paddr: Paddr }, - /// Drop one outstanding `Frame` handle. There is exactly one drop; - /// the step branches internally on the live refcount (mirroring - /// exec `drop`): `>= 2` decrements (slot stays SHARED), `== 1` - /// tears down to UNUSED (requires the slot detached from the page - /// table — `paths_in_pt.is_empty()`). See [`frame::drop_pre`]. + /// Drop one outstanding `Frame` handle. FrameDrop { fid: FrameId }, /// `Segment::from_unused`: allocate a fresh segment over a range - /// of previously-unused slots. Each frame in `range` transitions - /// `usage == Unused` → `Frame`, `rc` 0 → 1, `raw_count` 0 → 1. - /// Registers a [`SegmentEntry`] on success. + /// of previously-unused slots. SegmentFromUnused { range: Range }, - /// Drop a `Segment` handle. Releases the segment's forgotten - /// reference at each frame in the range; frames whose `rc` - /// reaches 1 transition to UNUSED. + /// Drop a `Segment` handle. SegmentDrop { sid: SegmentId }, /// `Segment::split`: split a segment at a page-aligned byte /// `offset` from its start, producing two segments covering the - /// disjoint halves. `regions` is unchanged (per-paddr - /// `cover_count` is invariant — each covered paddr lands in - /// exactly one half). Removes `sid` from `s.segments`, inserts - /// two fresh `SegmentEntry`s. + /// disjoint halves. SegmentSplit { sid: SegmentId, offset: usize }, /// `Segment::next`: pop the front frame off `sid`'s range, - /// producing a fresh `Frame` handle (a new `FrameEntry` - /// registered in `s.frames`). The segment's range shrinks by one - /// page from the front; if it becomes empty, `sid` is removed - /// from `s.segments`. The conversion bridge between segment-held - /// forgotten references and user-held Frame handles: at the - /// popped paddr `raw_count -= 1`, `cover_count -= 1`, `H += 1`, - /// `rc` unchanged. + /// producing a fresh `Frame` handle. SegmentNext { sid: SegmentId }, /// `Segment::clone`: produce a second handle covering the *same* - /// range as `sid`. Inserts a fresh `SegmentEntry` mirroring `sid`'s - /// range and bumps every covered frame's `rc` by 1 (Arc-style, via - /// `inc_frame_ref_count`). Per covered paddr: `cover_count += 1`, - /// `rc += 1`, `H` unchanged — the `accounting_inv` equation chains. + /// range as `sid`. SegmentClone { sid: SegmentId }, /// `Segment::slice`: produce a handle covering the sub-range - /// `sub_range` (an absolute, page-aligned physical range contained - /// in `sid`'s range). Inserts a fresh `SegmentEntry` covering - /// `sub_range` and bumps the `rc` of every frame *inside* - /// `sub_range` by 1. Clone is the special case `sub_range == sid`'s - /// range. + /// `sub_range`. SegmentSlice { sid: SegmentId, sub_range: Range }, /// `UniqueFrame::from_unused`: allocate a fresh *exclusive* handle on - /// a previously-unused slot. The slot transitions - /// `usage == Unused, rc == UNUSED` → `usage == Frame, rc == UNIQUE`. - /// Registers a [`UniqueEntry`] on success. + /// a previously-unused slot. UniqueFromUnused { paddr: Paddr }, - /// Drop a `UniqueFrame` handle. Tears the exclusive slot down - /// (`rc == UNIQUE` → `rc == UNUSED`), uninitialising its metadata - /// storage. Removes `uid` from `s.unique_frames`. + /// Drop a `UniqueFrame` handle. UniqueDrop { uid: UniqueId }, /// `Frame::from_unique`: convert the exclusive handle `uid` into a - /// shared `Frame`. The slot's `rc` drops `UNIQUE → 1`; the - /// `UniqueEntry` is consumed and a fresh `FrameEntry` registered - /// (`H: 0 → 1`). + /// shared `Frame`. FromUnique { uid: UniqueId }, /// `UniqueFrame::try_from_shared`: try to convert the shared handle - /// `fid` back into an exclusive one. Succeeds only when `fid` is the - /// sole reference (`rc == 1`): then `rc` rises `1 → UNIQUE`, the - /// `FrameEntry` is consumed and a fresh `UniqueEntry` registered. - /// Otherwise (`rc != 1`) the CAS fails and the store is unchanged. + /// `fid` back into an exclusive one. TryFromShared { fid: FrameId }, } /// Per-op precondition — the conjunction of facts about the store that -/// must hold for an `Op` to be applied. Encodes id-existence, -/// distinctness, cross-store ref-integrity, and the *expressible* -/// portion of the exec-method preconditions (per-op `requires` from -/// the verus_spec annotations). MODEL GAPS (handle inv/wf, -/// `tlb_model.inv()` is in `VmStore::inv`, closure preconditions on -/// `protect_next`, `size_of::()` range bounds on -/// `read_val`/`write_val`/`collect`) are documented in -/// [`super::cursor`] and [`super::io`] axiom comments. +/// must hold for an `Op` to be applied. /// /// [`lemma_step`] requires `op_pre(*old(s), op)`. Callers must establish the /// precondition for the specific Op variant they're about to apply. @@ -1015,142 +588,68 @@ pub enum Op { pub open spec fn op_pre<'rcu>(s: VmStore<'rcu>, op: Op) -> bool { match op { Op::NewVmSpace => true, - Op::DropVmSpace { vs } => s.vm_spaces.dom().contains(vs) && (forall|c: CursorId| #[trigger] - s.cursors.dom().contains(c) ==> s.cursors[c].vm_space != vs) && (forall|v: VmIoId| + Op::DropVmSpace { vs } => s.vm_spaces.contains_key(vs) && (forall|c: CursorId| #[trigger] + s.cursors.contains_key(c) ==> s.cursors[c].vm_space != vs) && (forall|v: VmIoId| #[trigger] - s.vm_ios.dom().contains(v) ==> s.vm_ios[v].vm_space != Some(vs)), - Op::OpenCursor { vs, va: _ } => s.vm_spaces.dom().contains(vs), - Op::OpenCursorMut { vs, va: _ } => s.vm_spaces.dom().contains(vs), - Op::DropCursor { c } => s.cursors.dom().contains(c), - Op::Query { c } => s.cursors.dom().contains(c), - Op::FindNext { c, len: _ } => s.cursors.dom().contains(c), - Op::Jump { c, va: _ } => s.cursors.dom().contains(c), - Op::VirtAddr { c } => s.cursors.dom().contains(c), - // Op::Map consumes the FrameEntry for the mapped frame. The - // consumed handle's reference at the slot is "transferred" to - // the new PTE — exec map ManuallyDrops the input UFrame - // (raw_count++ avoided since rc stays bumped) while the PTE - // adds 1 to rc; net 0 at the mapped slot. This is exactly what - // lets `accounting_inv`'s clause 4 (`rc == H + P`) chain - // across map: H decrements (entry consumed), P increments (path - // inserted), rc unchanged. - // - // (Once required `usage == Frame` at the mapped slot; that - // clause now lives in `structural_inv`'s FrameId⟹Frame-usage - // invariant, automatically discharged from `s.frames.contains(fid)`.) - Op::Map { c, fid, prop: _ } => s.cursors.dom().contains(c) && s.frames.dom().contains(fid), - Op::Unmap { c, len: _ } => s.cursors.dom().contains(c), - Op::ProtectNext { c, len: _ } => s.cursors.dom().contains(c), - Op::NewReader { vs, vaddr: _, len: _ } => s.vm_spaces.dom().contains(vs), - Op::NewWriter { vs, vaddr: _, len: _ } => s.vm_spaces.dom().contains(vs), + s.vm_ios.contains_key(v) ==> s.vm_ios[v].vm_space != Some(vs)), + Op::OpenCursor { vs, va: _ } => s.vm_spaces.contains_key(vs), + Op::OpenCursorMut { vs, va: _ } => s.vm_spaces.contains_key(vs), + Op::DropCursor { c } => s.cursors.contains_key(c), + Op::Query { c } => s.cursors.contains_key(c), + Op::FindNext { c, len: _ } => s.cursors.contains_key(c), + Op::Jump { c, va: _ } => s.cursors.contains_key(c), + Op::VirtAddr { c } => s.cursors.contains_key(c), + Op::Map { c, fid, prop: _ } => s.cursors.contains_key(c) && s.frames.contains_key(fid), + Op::Unmap { c, len: _ } => s.cursors.contains_key(c), + Op::ProtectNext { c, len: _ } => s.cursors.contains_key(c), + Op::NewReader { vs, vaddr: _, len: _ } => s.vm_spaces.contains_key(vs), + Op::NewWriter { vs, vaddr: _, len: _ } => s.vm_spaces.contains_key(vs), Op::NewKernelReader { vaddr: _, len: _ } => true, Op::NewKernelWriter { vaddr: _, len: _ } => true, - Op::DropReader { vio } => s.vm_ios.dom().contains(vio), - Op::DropWriter { vio } => s.vm_ios.dom().contains(vio), - Op::ReaderReadVal { source } => s.vm_ios.dom().contains(source), - Op::ReaderCollect { source } => s.vm_ios.dom().contains(source), - Op::ReaderLimit { vio, max: _ } => s.vm_ios.dom().contains(vio), - Op::ReaderSkip { vio, n: _ } => s.vm_ios.dom().contains(vio), - Op::ReaderQuery { vio } => s.vm_ios.dom().contains(vio), - Op::WriterWriteVal { writer } => s.vm_ios.dom().contains(writer), - Op::WriterFillZeros { vio, len: _ } => s.vm_ios.dom().contains(vio), - Op::WriterLimit { vio, max: _ } => s.vm_ios.dom().contains(vio), - Op::WriterSkip { vio, n: _ } => s.vm_ios.dom().contains(vio), - Op::WriterQuery { vio } => s.vm_ios.dom().contains(vio), - // exec Infallible `read` is *typed* `VmReader` → - // `VmWriter`: `source`/`dest` must be a kernel - // reader/writer (operand well-formedness, not a runtime check — - // see `VmIoEntry::is_kernel_reader`). `source != dest` keeps the - // two tracked `&mut` borrows disjoint. - Op::Read { source, dest } => s.vm_ios.dom().contains(source) && s.vm_ios.dom().contains( - dest, - ) && source != dest && s.vm_ios[source].is_kernel_reader() + Op::DropReader { vio } => s.vm_ios.contains_key(vio), + Op::DropWriter { vio } => s.vm_ios.contains_key(vio), + Op::ReaderReadVal { source } => s.vm_ios.contains_key(source), + Op::ReaderCollect { source } => s.vm_ios.contains_key(source), + Op::ReaderLimit { vio, max: _ } => s.vm_ios.contains_key(vio), + Op::ReaderSkip { vio, n: _ } => s.vm_ios.contains_key(vio), + Op::ReaderQuery { vio } => s.vm_ios.contains_key(vio), + Op::WriterWriteVal { writer } => s.vm_ios.contains_key(writer), + Op::WriterFillZeros { vio, len: _ } => s.vm_ios.contains_key(vio), + Op::WriterLimit { vio, max: _ } => s.vm_ios.contains_key(vio), + Op::WriterSkip { vio, n: _ } => s.vm_ios.contains_key(vio), + Op::WriterQuery { vio } => s.vm_ios.contains_key(vio), + Op::Read { source, dest } => s.vm_ios.contains_key(source) && s.vm_ios.contains_key(dest) + && source != dest && s.vm_ios[source].is_kernel_reader() && s.vm_ios[dest].is_kernel_writer(), - // exec Infallible `write`: same operand typing as `read`. - Op::Write { source, dest } => s.vm_ios.dom().contains(source) && s.vm_ios.dom().contains( - dest, - ) && source != dest && s.vm_ios[source].is_kernel_reader() + Op::Write { source, dest } => s.vm_ios.contains_key(source) && s.vm_ios.contains_key(dest) + && source != dest && s.vm_ios[source].is_kernel_reader() && s.vm_ios[dest].is_kernel_writer(), Op::FrameFromUnused { paddr: _ } => true, Op::FrameFromInUse { paddr: _ } => true, - // `op_pre[FrameDrop]` is just id-existence + the segment-cover - // constraint. All other `drop_pre` conjuncts (rc not in - // sentinels, rc <= MAX, storage.is_init, in_list == 0, - // rc == 1 ⟹ paths empty / handle_count == 1) plus the - // handle-clause are derived inside [`lemma_step_frame_drop`] from - // `s.inv()` via [`lemma_frame_drop_pre_derivable`] — the - // embedding-level `Frame::wf(state)` (Item 2 in the module- - // docs roadmap). The lemma chains: structural FrameId⟹Frame - // + structural raw_count == segment_cover_count + accounting - // clause 4 + `MetaSlotOwner::inv` SHARED branch (Item 1) - // covers every residual. - // - // The remaining `segment_cover_count == 0` is a real per-op - // obligation — it's the same shape as Item 5 segment - // disjointness — so it stays in `op_pre` until segments are - // activated and we can tie it to the segment store directly. - Op::FrameDrop { fid } => s.frames.dom().contains(fid) && segment_cover_count( + Op::FrameDrop { fid } => s.frames.contains_key(fid) && segment_cover_count( s.segments, s.frames[fid].paddr, ) == 0, - // `Segment::from_unused`: no precondition. The exec returns `Err` - // (NotAligned/OutOfBound) or rolls back a partial allocation when - // a frame in `range` is not free, leaving `regions` unchanged in - // every failure case; `lemma_step_segment_from_unused` branches - // internally on success (aligned + in-bound + non-empty + - // every covered slot UNUSED) and is a no-op otherwise. Op::SegmentFromUnused { range: _ } => true, - // `Segment` drop: id-existence + range well-formedness is - // satisfied by every registered `SegmentEntry`; the per-slot - // SHARED+Frame conditions are derived inside `lemma_step_segment_drop` - // from `s.inv()` (analogue of `lemma_frame_drop_pre_derivable` - // for segments). - Op::SegmentDrop { sid } => s.segments.dom().contains(sid), - // `Segment::split`: id-existence + offset must be page-aligned - // and strictly between 0 and the segment's size (mirroring - // exec `assert!`s). Range well-formedness comes from - // `structural_inv`. - Op::SegmentSplit { sid, offset } => s.segments.dom().contains(sid) && offset % PAGE_SIZE - == 0 && 0 < offset && offset < (s.segments[sid].range.end - - s.segments[sid].range.start), - // `Segment::next`: id-existence. Range well-formedness from - // `structural_inv` (range.start < range.end + page-aligned). - Op::SegmentNext { sid } => s.segments.dom().contains(sid), - Op::SegmentClone { sid } => s.segments.dom().contains(sid) && forall|paddr: Paddr| + Op::SegmentDrop { sid } => s.segments.contains_key(sid), + Op::SegmentSplit { sid, offset } => s.segments.contains_key(sid) && offset % PAGE_SIZE == 0 + && 0 < offset && offset < (s.segments[sid].range.end - s.segments[sid].range.start), + Op::SegmentNext { sid } => s.segments.contains_key(sid), + Op::SegmentClone { sid } => s.segments.contains_key(sid) && forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (s.segments[sid].range.start <= paddr < s.segments[sid].range.end && paddr % PAGE_SIZE == 0) ==> s.regions.slot_owner(paddr).ref_count() + 1 <= REF_COUNT_MAX, - // `Segment::slice`: id-existence + the sub-range is a - // page-aligned, non-empty, absolute physical range contained in - // `sid`'s range (mirroring exec `slice`'s `assert!`s on the - // offset range), plus the same per-frame saturation freedom as - // clone over the sub-range. - Op::SegmentSlice { sid, sub_range } => s.segments.dom().contains(sid) && sub_range.start + Op::SegmentSlice { sid, sub_range } => s.segments.contains_key(sid) && sub_range.start % PAGE_SIZE == 0 && sub_range.end % PAGE_SIZE == 0 && s.segments[sid].range.start <= sub_range.start && sub_range.start < sub_range.end && sub_range.end <= s.segments[sid].range.end && forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (sub_range.start <= paddr < sub_range.end && paddr % PAGE_SIZE == 0) ==> s.regions.slot_owner(paddr).ref_count() + 1 <= REF_COUNT_MAX, - // `UniqueFrame::from_unused`: no precondition (mirrors - // `FrameFromUnused`). The exec returns `Err` and leaves the slot - // untouched unless the target is genuinely a free frame slot; - // `lemma_step_unique_from_unused` branches internally on that condition - // (`valid_frame_paddr` + slot managed + `usage is Unused` + - // `rc == REF_COUNT_UNUSED`) and both outcomes preserve `s.inv()`. Op::UniqueFromUnused { paddr: _ } => true, - // `UniqueFrame` drop: id-existence. The per-slot UNIQUE / in_list - // / storage / paths-empty teardown preconditions are derived - // inside `lemma_step_unique_drop` from `s.inv()` (the structural - // unique-entry clause + `MetaSlotOwner::inv`'s UNIQUE branch). - Op::UniqueDrop { uid } => s.unique_frames.dom().contains(uid), - // `Frame::from_unique`: id-existence. The UNIQUE-slot facts are - // derived inside `lemma_step_from_unique` from `s.inv()`. - Op::FromUnique { uid } => s.unique_frames.dom().contains(uid), - // `UniqueFrame::try_from_shared`: id-existence. The step branches - // internally on whether `fid`'s slot is the sole reference - // (`rc == 1`); both outcomes preserve `s.inv()`. - Op::TryFromShared { fid } => s.frames.dom().contains(fid), + Op::UniqueDrop { uid } => s.unique_frames.contains_key(uid), + Op::FromUnique { uid } => s.unique_frames.contains_key(uid), + Op::TryFromShared { fid } => s.frames.contains_key(fid), } } @@ -1167,11 +666,11 @@ impl<'rcu> VmStore<'rcu> { VmSpaceOwner) requires old(self).inv(), - old(self).vm_spaces.dom().contains(vs), + old(self).vm_spaces.contains_key(vs), forall|c: CursorId| #[trigger] - old(self).cursors.dom().contains(c) ==> old(self).cursors[c].vm_space != vs, + old(self).cursors.contains_key(c) ==> old(self).cursors[c].vm_space != vs, forall|v: VmIoId| #[trigger] - old(self).vm_ios.dom().contains(v) ==> old(self).vm_ios[v].vm_space != Some(vs), + old(self).vm_ios.contains_key(v) ==> old(self).vm_ios[v].vm_space != Some(vs), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1184,6 +683,8 @@ impl<'rcu> VmStore<'rcu> { res == old(self).vm_spaces[vs], final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.vm_spaces.tracked_remove(vs) } @@ -1196,7 +697,7 @@ impl<'rcu> VmStore<'rcu> { ) requires old(self).inv(), - !old(self).vm_spaces.dom().contains(vs), + !old(self).vm_spaces.contains_key(vs), owner.inv(), ensures final(self).regions == old(self).regions, @@ -1209,6 +710,8 @@ impl<'rcu> VmStore<'rcu> { final(self).unique_frames == old(self).unique_frames, final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.vm_spaces.tracked_insert(vs, owner); } @@ -1217,7 +720,7 @@ impl<'rcu> VmStore<'rcu> { CursorEntry<'rcu>) requires old(self).inv(), - old(self).cursors.dom().contains(c), + old(self).cursors.contains_key(c), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1230,6 +733,8 @@ impl<'rcu> VmStore<'rcu> { res == old(self).cursors[c], final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.cursors.tracked_remove(c) } @@ -1244,10 +749,10 @@ impl<'rcu> VmStore<'rcu> { ) requires old(self).inv(), - !old(self).cursors.dom().contains(c), + !old(self).cursors.contains_key(c), entry.inv(), entry.owner.metaregion_sound(old(self).regions), - old(self).vm_spaces.dom().contains(entry.vm_space), + old(self).vm_spaces.contains_key(entry.vm_space), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1259,6 +764,8 @@ impl<'rcu> VmStore<'rcu> { final(self).unique_frames == old(self).unique_frames, final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.cursors.tracked_insert(c, entry); } @@ -1266,7 +773,7 @@ impl<'rcu> VmStore<'rcu> { pub proof fn tracked_extract_vm_io(tracked &mut self, vio: VmIoId) -> (tracked res: VmIoEntry) requires old(self).inv(), - old(self).vm_ios.dom().contains(vio), + old(self).vm_ios.contains_key(vio), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1279,22 +786,18 @@ impl<'rcu> VmStore<'rcu> { res == old(self).vm_ios[vio], final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.vm_ios.tracked_remove(vio) } - /// Inserts a VmIo entry at the given fresh id. Requires the id is - /// not already used, the entry satisfies its inv, the entry's - /// `vm_space` (if `Some`) refers to a live VmSpace, the range - /// bound holds when `vm_space` is `Some`, and (if the entry is - /// activated) its owner range is disjoint from every existing - /// activated entry's owner range (preserves the pairwise-disjoint - /// invariant in [`VmStore::inv`]). + /// Inserts a VmIo entry at the given fresh id. pub proof fn lemma_insert_vm_io(tracked &mut self, vio: VmIoId, tracked entry: VmIoEntry) requires old(self).inv(), - !old(self).vm_ios.dom().contains(vio), + !old(self).vm_ios.contains_key(vio), entry.inv(), - entry.vm_space matches Some(vs) ==> old(self).vm_spaces.dom().contains(vs), + entry.vm_space matches Some(vs) ==> old(self).vm_spaces.contains_key(vs), entry.vm_space is Some ==> (entry.vaddr as nat) + (entry.len as nat) <= MAX_USERSPACE_VADDR as nat, ensures @@ -1308,21 +811,16 @@ impl<'rcu> VmStore<'rcu> { final(self).unique_frames == old(self).unique_frames, final(self).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); self.vm_ios.tracked_insert(vio, entry); } /// Removes the FrameEntry at `fid` from the store. - /// - /// Requires / ensures only [`structural_inv`] — not full [`inv`]. - /// Removing a frame handle without coordinating with the slot's - /// `ref_count` breaks [`accounting_inv`] transiently; the *step* - /// that calls this is responsible for pairing it with the matching - /// `frame::drop_step` (or `cursor::map_step` once Op::Map consumes - /// a tracked frame) and re-establishing accounting at the end. pub proof fn tracked_extract_frame(tracked &mut self, fid: FrameId) -> (tracked res: FrameEntry) requires old(self).structural_inv(), - old(self).frames.dom().contains(fid), + old(self).frames.contains_key(fid), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1335,26 +833,16 @@ impl<'rcu> VmStore<'rcu> { res == old(self).frames[fid], final(self).structural_inv(), { + reveal(VmStore::structural_inv); self.frames.tracked_remove(fid) } - /// Inserts a FrameEntry at the given fresh id. Requires the entry's - /// paddr be `valid_frame_paddr` — the per-`FrameEntry` clause of - /// [`VmStore::inv`] (#4). Every caller establishes this from the - /// `from_*` axioms' `!valid_frame_paddr ==> None` (a registered handle - /// is necessarily in-bound). - /// - /// Requires / ensures only [`structural_inv`] — see [`tracked_extract_frame`] - /// for the accounting/structural split rationale. + /// Inserts a FrameEntry at the given fresh id. pub proof fn lemma_insert_frame(tracked &mut self, fid: FrameId, tracked entry: FrameEntry) requires old(self).structural_inv(), - !old(self).frames.dom().contains(fid), + !old(self).frames.contains_key(fid), valid_frame_paddr(entry.paddr), - // The slot we're registering a handle at must be Frame-usage: - // structural_inv's FrameId⟹Frame-usage clause. Every caller - // discharges this from the `from_*` / query axioms which - // commit to Frame-usage at the cloned slot. old(self).regions.slot_owner(entry.paddr).usage is Frame, ensures final(self).regions == old(self).regions, @@ -1367,16 +855,15 @@ impl<'rcu> VmStore<'rcu> { final(self).unique_frames == old(self).unique_frames, final(self).structural_inv(), { + reveal(VmStore::structural_inv); self.frames.tracked_insert(fid, entry); } - /// Removes the UniqueEntry at `uid` from the store. **Does NOT** - /// ensure `structural_inv` — the caller must pair this with the - /// regions UNIQUE→UNUSED teardown before observing `s.inv()`. + /// Removes the UniqueEntry at `uid` from the store. pub proof fn tracked_extract_unique(tracked &mut self, uid: UniqueId) -> (tracked res: UniqueEntry) requires - old(self).unique_frames.dom().contains(uid), + old(self).unique_frames.contains_key(uid), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1391,14 +878,10 @@ impl<'rcu> VmStore<'rcu> { self.unique_frames.tracked_remove(uid) } - /// Inserts a UniqueEntry at a fresh id. **Does NOT** ensure - /// `structural_inv` — the caller must pair this with the regions - /// UNUSED→UNIQUE transition (via - /// [`unique::unique_from_unused_embedded`]) before observing - /// `s.inv()`. + /// Inserts a UniqueEntry at a fresh id. pub proof fn lemma_insert_unique(tracked &mut self, uid: UniqueId, tracked entry: UniqueEntry) requires - !old(self).unique_frames.dom().contains(uid), + !old(self).unique_frames.contains_key(uid), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1412,16 +895,11 @@ impl<'rcu> VmStore<'rcu> { self.unique_frames.tracked_insert(uid, entry); } - /// Removes the SegmentEntry at `sid` from the store. **Does NOT** - /// ensure `structural_inv` — extracting a segment without a paired - /// `regions` decrement breaks the - /// `raw_count == segment_cover_count` clause at every paddr the - /// segment covered. The caller's step proof must restore it via - /// [`segment::drop_step`] before observing `s.inv()` again. + /// Removes the SegmentEntry at `sid` from the store. pub proof fn tracked_extract_segment(tracked &mut self, sid: SegmentId) -> (tracked res: SegmentEntry) requires - old(self).segments.dom().contains(sid), + old(self).segments.contains_key(sid), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1436,17 +914,14 @@ impl<'rcu> VmStore<'rcu> { self.segments.tracked_remove(sid) } - /// Inserts a SegmentEntry at a fresh id. **Does NOT** ensure - /// `structural_inv` — the caller must pair this with a `regions` - /// `raw_count` bump at every covered paddr (via - /// [`segment::from_unused_step`]) before observing `s.inv()`. + /// Inserts a SegmentEntry at a fresh id. pub proof fn lemma_insert_segment( tracked &mut self, sid: SegmentId, tracked entry: SegmentEntry, ) requires - !old(self).segments.dom().contains(sid), + !old(self).segments.contains_key(sid), ensures final(self).regions == old(self).regions, final(self).tlb_model == old(self).tlb_model, @@ -1461,6 +936,79 @@ impl<'rcu> VmStore<'rcu> { } } +// Narrow elimination lemmas keep opaque store invariants out of large step contexts. +proof fn lemma_structural_inv_cursor_frame<'rcu>(s: VmStore<'rcu>, c: CursorId, fid: FrameId) + requires + s.structural_inv(), + s.cursors.contains_key(c), + s.frames.contains_key(fid), + ensures + s.tlb_model.inv(), + s.cursors[c].inv(), + s.cursors[c].owner.metaregion_sound(s.regions), + s.vm_spaces.contains_key(s.cursors[c].vm_space), + valid_frame_paddr(s.frames[fid].paddr), + s.regions.slot_owner(s.frames[fid].paddr).usage is Frame, +{ + reveal(VmStore::structural_inv); +} + +proof fn lemma_structural_inv_frame<'rcu>(s: VmStore<'rcu>, fid: FrameId) + requires + s.structural_inv(), + s.frames.contains_key(fid), + ensures + valid_frame_paddr(s.frames[fid].paddr), + s.regions.slot_owner(s.frames[fid].paddr).usage is Frame, +{ + reveal(VmStore::structural_inv); +} + +proof fn lemma_structural_inv_segment<'rcu>(s: VmStore<'rcu>, sid: SegmentId, paddr: Paddr) + requires + s.structural_inv(), + s.segments.contains_key(sid), + s.segments[sid].range.start <= paddr < s.segments[sid].range.end, + paddr % PAGE_SIZE == 0, + ensures + valid_frame_paddr(paddr), + s.regions.slot_owner(paddr).usage is Frame, +{ + reveal(VmStore::structural_inv); +} + +proof fn lemma_accounting_inv_at<'rcu>(s: VmStore<'rcu>, idx: int) + requires + s.accounting_inv(), + 0 <= idx < max_meta_slots(), + ensures + s.regions.slot_owners[idx].ref_count() == REF_COUNT_UNUSED ==> handle_count(s.frames, idx) + == 0 && s.regions.slot_owners[idx].paths_in_pt.is_empty() && segment_cover_count( + s.segments, + index_to_frame(idx), + ) == 0, + s.regions.slot_owners[idx].usage is Frame && s.regions.slot_owners[idx].ref_count() + != REF_COUNT_UNUSED && s.regions.slot_owners[idx].ref_count() != REF_COUNT_UNIQUE + ==> handle_count(s.frames, idx) > 0 || s.regions.slot_owners[idx].paths_in_pt.len() > 0 + || segment_cover_count(s.segments, index_to_frame(idx)) > 0, + s.regions.slot_owners[idx].usage is Frame && (handle_count(s.frames, idx) > 0 + || s.regions.slot_owners[idx].paths_in_pt.len() > 0 || segment_cover_count( + s.segments, + index_to_frame(idx), + ) > 0) ==> { + let so = s.regions.slot_owners[idx]; + let rc = so.ref_count(); + &&& rc != REF_COUNT_UNUSED + &&& rc != REF_COUNT_UNIQUE + &&& rc == handle_count(s.frames, idx) + so.paths_in_pt.len() + segment_cover_count( + s.segments, + index_to_frame(idx), + ) + }, +{ + reveal(VmStore::accounting_inv); +} + // ============================================================================= // One-step soundness theorem. // ============================================================================= @@ -1562,21 +1110,6 @@ pub proof fn lemma_step<'rcu>(tracked s: &mut VmStore<'rcu>, op: Op) } } -// --- Per-arm proof helpers (kept individually so SMT context stays small) --- -/// Stage 5.3: [`accounting_inv`] survives a step that only allocates -/// fresh page-table nodes. `VmSpace::new` / `VmSpace::cursor*` mutate -/// `regions` solely by spinning up PT nodes — their `_embedded` axioms -/// guarantee every *changed* slot went `UNUSED → non-UNUSED, non-Frame` -/// (the changed-slots clause) and left `frames` untouched. -/// -/// Under those two facts every slot an accounting clause cares about is -/// provably *unchanged*: a slot carrying a handle, a Frame-usage slot, -/// and a non-UNUSED slot each contradict one hypothesis of the -/// `UNUSED → non-UNUSED, non-Frame` transition, so the old clause -/// carries verbatim. -/// -/// Shared by [`lemma_step_new_vm_space`], [`lemma_step_open_cursor`] and -/// [`lemma_step_open_cursor_mut`]. proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_new: VmStore<'rcu>) requires s_old.inv(), @@ -1592,25 +1125,20 @@ proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_ne }, ensures s_new.accounting_inv(), - // PT-alloc preserves the FrameId⟹Frame-usage structural clause: - // every existing registered handle's slot was non-UNUSED pre - // (rc != UNUSED from clause 4 with H >= 1), so PT-alloc's - // requires (only UNUSED slots may change) leaves it untouched. forall|fid: FrameId| #[trigger] - s_new.frames.dom().contains(fid) ==> s_new.regions.slot_owner( + s_new.frames.contains_key(fid) ==> s_new.regions.slot_owner( s_new.frames[fid].paddr, ).usage is Frame, - // Likewise for segment-covered ⟹ Frame-usage. forall|sid: SegmentId, paddr: Paddr| #![trigger - s_new.segments.dom().contains(sid), + s_new.segments.contains_key(sid), frame_to_index(paddr)] - s_new.segments.dom().contains(sid) && s_new.segments[sid].range.start <= paddr + s_new.segments.contains_key(sid) && s_new.segments[sid].range.start <= paddr < s_new.segments[sid].range.end && paddr % PAGE_SIZE == 0 ==> s_new.regions.slot_owner(paddr).usage is Frame, { - // Clause 2 — UNUSED ⟹ no users. An UNUSED slot in `s_new` is - // unchanged (a transitioned slot is non-UNUSED in `s_new`). + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); assert forall|idx: int| #![trigger s_new.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s_new.regions.slot_owners[idx].ref_count() @@ -1621,8 +1149,6 @@ proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_ne ) == 0 by { assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); }; - // Clause 3 — Frame ∧ non-sentinel ⟹ active head. A Frame-usage slot - // in `s_new` is unchanged (a transitioned slot is non-Frame). assert forall|idx: int| #![trigger s_new.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s_new.regions.slot_owners[idx].usage is Frame @@ -1636,8 +1162,6 @@ proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_ne ) > 0 by { assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); }; - // Clause 4 — the accounting equation. Same: a Frame-usage slot in - // `s_new` is unchanged, so the old equation carries. assert forall|idx: int| #![trigger s_new.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s_new.regions.slot_owners[idx].usage is Frame && ( @@ -1654,58 +1178,37 @@ proof fn lemma_accounting_preserved_by_pt_alloc<'rcu>(s_old: VmStore<'rcu>, s_ne } by { assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); }; - // Discharge segment-covered ⟹ Frame-usage. Covered slots have - // cover_count >= 1 ⟹ accounting clause 3 with usage == Frame - // (from old structural) ⟹ rc != UNUSED. PT-alloc's requires - // ⟹ slot unchanged ⟹ usage still Frame. assert forall|sid: SegmentId, paddr: Paddr| #![trigger - s_new.segments.dom().contains(sid), + s_new.segments.contains_key(sid), frame_to_index(paddr)] - s_new.segments.dom().contains(sid) && s_new.segments[sid].range.start <= paddr + s_new.segments.contains_key(sid) && s_new.segments[sid].range.start <= paddr < s_new.segments[sid].range.end && paddr % PAGE_SIZE == 0 implies s_new.regions.slot_owner(paddr).usage is Frame by { let idx = frame_to_index(paddr); - // From old structural: covered ⟹ Frame. assert(s_old.regions.slot_owners[idx].usage is Frame); - // From old accounting clause 4: cover >= 1 ⟹ active head ⟹ - // rc ∈ valid SHARED range ⟹ rc != UNUSED. lemma_segment_cover_contains(s_old.segments, sid, paddr); assert(s_old.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED); - // PT-alloc unchanged. assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); }; - // Discharge the FrameId⟹Frame-usage clause. For every registered - // handle, pre rc != UNUSED (from `s_old.accounting_inv` clause 4 - // with H >= 1 + usage == Frame from `s_old.structural_inv`), so - // PT-alloc's requires (changed ⟹ pre UNUSED) leaves the slot - // untouched and usage stays Frame. assert forall|fid: FrameId| #[trigger] - s_new.frames.dom().contains(fid) implies s_new.regions.slot_owner( + s_new.frames.contains_key(fid) implies s_new.regions.slot_owner( s_new.frames[fid].paddr, ).usage is Frame by { let idx = frame_to_index(s_new.frames[fid].paddr); - // pre H >= 1 since `fid` is in `s_old.frames.dom()`. assert(s_old.frames.dom().filter( |gid: FrameId| frame_to_index(s_old.frames[gid].paddr) == idx, ).contains(fid)); assert(handle_count(s_old.frames, idx) >= 1); - // pre accounting_inv clause 4 ⟹ pre rc != UNUSED. assert(s_old.regions.slot_owners[idx].usage is Frame); assert(s_old.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED); - // PT-alloc's requires: changed ⟹ pre UNUSED. Contrapositive: - // pre non-UNUSED ⟹ unchanged. assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); }; } /// Re-establish `structural_inv`'s slot-perm coverage exception for an op /// that preserves the `slots` map (`slots == old slots`) and leaves every -/// UNPARKED slot's `slot_owner` untouched. Such ops (`unmap`, segment -/// drop / `from_unused`) only ever mutate parked, in-`slots` slots; the -/// unparked PT-root slots keep their active-`PageTable` status. Each -/// caller discharges the two hypotheses from its `_embedded` axiom's -/// `slots == old` + `unparked ⟹ slot_owner unchanged` ensures. +/// UNPARKED slot's `slot_owner` untouched. proof fn lemma_coverage_preserved_slots_eq<'rcu>(s_old: VmStore<'rcu>, s_new: VmStore<'rcu>) requires s_old.structural_inv(), @@ -1720,13 +1223,12 @@ proof fn lemma_coverage_preserved_slots_eq<'rcu>(s_old: VmStore<'rcu>, s_new: Vm s_new.regions.slot_owners[idx].usage is PageTable && s_new.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED), { + reveal(VmStore::structural_inv); assert forall|idx: int| 0 <= idx < max_meta_slots() implies #[trigger] s_new.regions.slots.contains_key(idx) || ( s_new.regions.slot_owners[idx].usage is PageTable && s_new.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED) by { if !s_new.regions.slots.contains_key(idx) { - // `slots == old` ⟹ unparked in `s_old` too ⟹ old coverage's - // PageTable-node disjunct ⟹ (slot unchanged) carries. assert(!s_old.regions.slots.contains_key(idx)); assert(s_new.regions.slot_owners[idx] == s_old.regions.slot_owners[idx]); } @@ -1739,33 +1241,21 @@ proof fn lemma_step_new_vm_space<'rcu>(tracked s: &mut VmStore<'rcu>) ensures final(s).inv(), { + reveal(VmStore::structural_inv); let ghost s_before = *s; let tracked owner = vm_space::new_vm_space_step(&mut s.regions); let ghost id = fresh_vm_space_id(s.vm_spaces); lemma_fresh_vm_space_id_not_in_dom(s.vm_spaces); - // `VmSpace::new` only allocates fresh PT nodes; accounting carries - // (every changed slot went UNUSED → non-UNUSED PT node). lemma_accounting_preserved_by_pt_alloc(s_before, *s); - // Re-establish `structural_inv`'s slot-perm coverage after the root's - // slot perm was extracted from `regions.slots`. The root slot now - // satisfies the PageTable-node exception (`usage == PageTable`, from - // the axiom); every OTHER slot kept its `slots` membership (only the - // root was removed), so its coverage carries from the old store. let ghost root_idx = vm_space::vm_space_root_idx(owner); assert forall|idx: int| 0 <= idx < max_meta_slots() implies #[trigger] s.regions.slots.contains_key(idx) || ( s.regions.slot_owners[idx].usage is PageTable && s.regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED) by { if idx == root_idx { - // The extracted root is an active PageTable node (axiom). } else { - // Only the root left `slots`; this slot's membership is - // unchanged. assert(s.regions.slots.contains_key(idx) == s_before.regions.slots.contains_key(idx)); if s.regions.slot_owners[idx] != s_before.regions.slot_owners[idx] { - // A changed non-root slot was pre-UNUSED (axiom) ⟹ by old - // coverage's contrapositive it was parked, and stays - // parked (only the root left `slots`). assert(s_before.regions.slot_owners[idx].ref_count() == REF_COUNT_UNUSED); assert(s_before.regions.slots.contains_key(idx)); } @@ -1777,11 +1267,11 @@ proof fn lemma_step_new_vm_space<'rcu>(tracked s: &mut VmStore<'rcu>) proof fn lemma_step_drop_vm_space<'rcu>(tracked s: &mut VmStore<'rcu>, vs: VmSpaceId) requires old(s).inv(), - old(s).vm_spaces.dom().contains(vs), + old(s).vm_spaces.contains_key(vs), forall|c: CursorId| #[trigger] - old(s).cursors.dom().contains(c) ==> old(s).cursors[c].vm_space != vs, + old(s).cursors.contains_key(c) ==> old(s).cursors[c].vm_space != vs, forall|v: VmIoId| #[trigger] - old(s).vm_ios.dom().contains(v) ==> old(s).vm_ios[v].vm_space != Some(vs), + old(s).vm_ios.contains_key(v) ==> old(s).vm_ios[v].vm_space != Some(vs), ensures final(s).inv(), { @@ -1796,10 +1286,11 @@ proof fn lemma_step_open_cursor<'rcu>( ) requires old(s).inv(), - old(s).vm_spaces.dom().contains(vs), + old(s).vm_spaces.contains_key(vs), ensures final(s).inv(), { + reveal(VmStore::structural_inv); let ghost s_before = *s; let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs); let tracked res = cursor::open_cursor_step(vm_space_ref, &mut s.regions, vs, va); @@ -1823,10 +1314,11 @@ proof fn lemma_step_open_cursor_mut<'rcu>( ) requires old(s).inv(), - old(s).vm_spaces.dom().contains(vs), + old(s).vm_spaces.contains_key(vs), ensures final(s).inv(), { + reveal(VmStore::structural_inv); let ghost s_before = *s; let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs); let tracked res = cursor::open_cursor_mut_step(vm_space_ref, &mut s.regions, vs, va); @@ -1846,7 +1338,7 @@ proof fn lemma_step_open_cursor_mut<'rcu>( proof fn lemma_step_drop_cursor<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { @@ -1857,40 +1349,28 @@ proof fn lemma_step_drop_cursor<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_frames = s.frames; let ghost old_regions = s.regions; let tracked mut entry = s.tracked_extract_cursor(c); let ghost res = cursor::cursor_query_step(&mut entry, &mut s.regions); match res { Option::None => { - // No clone happened — slot_owners fully preserved per axiom, - // s.frames unchanged. accounting_inv chains directly. s.lemma_insert_cursor(c, entry); }, Option::Some(paddr) => { - // Exec query cloned a tracked leaf at `paddr` (rc++ at the - // leaf slot). Register a fresh `FrameEntry` so `H` at that - // slot grows by 1 in lockstep with `rc`, keeping - // `accounting_inv`'s clause 4 (`rc == H + P`) chained. let ghost target_idx = frame_to_index(paddr); s.regions.lemma_contains_valid_frame_paddr(paddr); let ghost id = fresh_frame_id(s.frames); lemma_fresh_frame_id_not_in_dom(s.frames); let tracked frame_entry = tracked_frame_entry_new(paddr); s.lemma_insert_frame(id, frame_entry); - // Pre target_idx: usage == Frame (axiom), so by pre clause 3 - // either H_pre > 0 or paths_pre > 0; clause 4 gives - // pre rc != UNUSED ∧ pre rc != UNIQUE ∧ - // pre rc == pre H + pre paths ∧ pre storage.is_init. - // The cursor axiom on Some bumps rc to pre rc + 1 (≤ MAX), - // preserves usage / paths / storage at target_idx, and - // preserves all other slots fully. assert(s.regions.slot_owners[target_idx].usage is Frame); - // Discharge accounting_inv on (new regions, new frames). assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -1901,14 +1381,8 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) ) == 0 by { lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx); if idx == target_idx { - // post rc = pre rc + 1; pre rc != UNUSED (clause 4), - // so post rc > 1 ≠ UNUSED. Contradiction. assert(false); } else { - // Other slot: fully preserved (cursor axiom), so - // pre UNUSED ⟹ pre H=0 ∧ pre paths empty ∧ cover==0; - // H unchanged at idx != target_idx (lemma); segments - // unchanged. assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; @@ -1924,7 +1398,6 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) ) > 0 by { lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx); if idx == target_idx { - // The freshly inserted handle gives H > 0 at target. assert(handle_count(s.frames, target_idx) >= 1); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); @@ -1947,10 +1420,6 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) lemma_handle_count_insert_fresh(old_frames, id, frame_entry, idx); if idx == target_idx { if old_regions.slot_owners[target_idx].ref_count() == REF_COUNT_UNUSED { - // Pre UNUSED at Frame slot: clause 1 ⟹ pre paths - // empty ∧ pre H == 0 ∧ pre cover == 0. - // Post H == 1, paths preserved, cover preserved. - // Post rc = pre rc + 1 = UNUSED + 1. assert(REF_COUNT_UNUSED == 0u32); assert(s.regions.slot_owners[target_idx].ref_count() == 1); assert(handle_count(s.frames, target_idx) == 1); @@ -1961,8 +1430,6 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) } else if old_regions.slot_owners[target_idx].ref_count() == REF_COUNT_UNIQUE { assert(false); } else { - // Pre non-sentinel SHARED rc: pre clause 4 applies - // with the new cover term. let pre_so = old_regions.slot_owners[target_idx]; let pre_rc = pre_so.ref_count(); let pre_paths = pre_so.paths_in_pt.len(); @@ -1971,9 +1438,6 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) if pre_H == 0 && pre_paths == 0 && pre_cover == 0 { assert(false); } else { - // pre rc == pre_H + pre_paths + pre_cover. - // post rc = pre rc + 1, post H = pre_H + 1, - // post paths = pre_paths, post cover = pre_cover. assert(pre_rc == pre_H + pre_paths + pre_cover); assert(handle_count(s.frames, target_idx) == pre_H + 1); } @@ -1990,10 +1454,12 @@ proof fn lemma_step_query<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId) proof fn lemma_step_find_next<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let tracked mut entry = s.tracked_extract_cursor(c); cursor::cursor_find_next_step(&mut entry, &mut s.regions, len); s.lemma_insert_cursor(c, entry); @@ -2002,10 +1468,12 @@ proof fn lemma_step_find_next<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, proof fn lemma_step_jump<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, va: Vaddr) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let tracked mut entry = s.tracked_extract_cursor(c); cursor::cursor_jump_step(&mut entry, &mut s.regions, va); s.lemma_insert_cursor(c, entry); @@ -2014,10 +1482,12 @@ proof fn lemma_step_jump<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, va: V proof fn lemma_step_protect_next<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let tracked mut entry = s.tracked_extract_cursor(c); cursor::cursor_protect_next_step(&mut entry, &mut s.regions, len); s.lemma_insert_cursor(c, entry); @@ -2032,75 +1502,45 @@ proof fn lemma_step_map<'rcu>( ) requires old(s).inv(), - old(s).cursors.dom().contains(c), - old(s).frames.dom().contains(fid), + old(s).cursors.contains_key(c), + old(s).frames.contains_key(fid), ensures final(s).inv(), { - hide(VmStore::inv); - hide(VmStore::structural_inv); - hide(VmStore::accounting_inv); - assert(s.structural_inv()) by { - reveal(VmStore::inv); - }; - assert(s.accounting_inv()) by { - reveal(VmStore::inv); - }; + let ghost s_before = *s; + lemma_structural_inv_cursor_frame(*s, c, fid); assert(s.regions.inv() && s.tlb_model.inv() && s.cursors[c].inv() - && s.cursors[c].owner.metaregion_sound(s.regions) && s.vm_spaces.dom().contains( + && s.cursors[c].owner.metaregion_sound(s.regions) && s.vm_spaces.contains_key( s.cursors[c].vm_space, - )) by { - reveal(VmStore::structural_inv); - }; + )); // `usage == Frame` at the mapped slot from `structural_inv`'s // FrameId⟹Frame-usage clause. - assert(s.regions.slot_owner(s.frames[fid].paddr).usage is Frame) by { - reveal(VmStore::structural_inv); - }; + assert(s.regions.slot_owner(s.frames[fid].paddr).usage is Frame); let ghost paddr = s.frames[fid].paddr; let ghost target_idx = frame_to_index(paddr); let ghost old_frames = s.frames; let ghost old_regions = s.regions; - // From `structural_inv`: every registered handle's paddr is in-bound. - assert(valid_frame_paddr(paddr)) by { - reveal(VmStore::structural_inv); - }; + assert(valid_frame_paddr(paddr)); s.regions.lemma_contains_valid_frame_paddr(paddr); assert(s.regions.contains(target_idx)); assert(s.regions.slots[target_idx].addr() == index_to_meta(target_idx)); - // Pre target_idx: we hold a FrameEntry at this paddr, so - // `handle_count(old_frames, target_idx) >= 1`. assert(old_frames.dom().filter( |gid: FrameId| frame_to_index(old_frames[gid].paddr) == target_idx, ).contains(fid)); assert(handle_count(old_frames, target_idx) >= 1); - // Pre target_idx is usage == Frame (op_pre) and active head - // (H >= 1), so pre `accounting_inv` clauses 3 and 4 apply. let ghost pre_rc_target = old_regions.slot_owners[target_idx].ref_count(); let ghost pre_paths_target = old_regions.slot_owners[target_idx].paths_in_pt.len(); let ghost pre_cover_target = segment_cover_count(s.segments, index_to_frame(target_idx)); + lemma_accounting_inv_at(*s, target_idx); assert(pre_rc_target != REF_COUNT_UNUSED && pre_rc_target != REF_COUNT_UNIQUE && pre_rc_target - == handle_count(old_frames, target_idx) + pre_paths_target + pre_cover_target) by { - reveal(VmStore::accounting_inv); - }; + == handle_count(old_frames, target_idx) + pre_paths_target + pre_cover_target); let tracked mut entry = s.tracked_extract_cursor(c); - // Consume the FrameEntry: the UFrame's handle ref-count - // contribution moves to the new PTE; the embedding's `H` at - // target_idx decrements by 1 in lockstep with `P` incrementing by 1. - assert(s.structural_inv()) by { - reveal(VmStore::inv); - }; let tracked _frame_entry = s.tracked_extract_frame(fid); assert(entry.inv()); assert(entry.owner.metaregion_sound(s.regions)); assert(s.regions.inv()); assert(s.tlb_model.inv()); cursor::map_step(&mut entry, &mut s.regions, &mut s.tlb_model, paddr, prop); - // Discharge `accounting_inv` clause-by-clause. The cursor-map axiom - // gives: rc/usage/storage preserved at target_idx, paths += 1 at - // target_idx; non-mapped pre-non-UNUSED slots fully preserved; - // post-UNUSED slots fully preserved; newly-non-UNUSED slots are - // non-Frame (PT nodes). `s.segments` is unchanged across map. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -2109,7 +1549,7 @@ proof fn lemma_step_map<'rcu>( s.segments, index_to_frame(idx), ) == 0 by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); // post-UNUSED ⟹ slot fully preserved (cursor axiom). assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); lemma_handle_count_remove(old_frames, fid, idx); @@ -2131,17 +1571,13 @@ proof fn lemma_step_map<'rcu>( s.segments, index_to_frame(idx), ) > 0 by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); lemma_handle_count_remove(old_frames, fid, idx); if idx == target_idx { - // post rc preserved at target_idx, paths += 1 ⟹ paths.len > 0. assert(s.regions.slot_owners[idx].paths_in_pt.len() == pre_paths_target + 1); } else if old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNUSED { - // Newly-non-UNUSED slot ⟹ usage != Frame (changed-slots clause). assert(s.regions.slot_owners[idx].usage !is Frame); } else { - // Non-mapped pre-non-UNUSED slot ⟹ fully preserved; pre - // clause 3 carries forward. assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; @@ -2163,14 +1599,9 @@ proof fn lemma_step_map<'rcu>( index_to_frame(idx), ) } by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); lemma_handle_count_remove(old_frames, fid, idx); if idx == target_idx { - // Pre clause 4 (new): rc == H_pre + P_pre + cover_pre. - // Post: rc/usage/storage preserved; H_post = H_pre - 1; - // P_post = P_pre + 1; cover_post = cover_pre. - // So rc_post = pre_rc_target = H_pre + P_pre + cover_pre - // = H_post + P_post + cover_post. assert(s.regions.slot_owners[idx].ref_count() == pre_rc_target); assert(s.regions.slot_owners[idx].paths_in_pt.len() == pre_paths_target + 1); assert(handle_count(s.frames, idx) == (handle_count(old_frames, idx) - 1) as nat); @@ -2180,31 +1611,18 @@ proof fn lemma_step_map<'rcu>( assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; - // Discharge structural_inv's FrameId⟹Frame-usage clause. - // For every remaining fid: pre slot was Frame (old structural_inv); - // cursor preserves usage at target_idx and at non-mapped pre-non- - // UNUSED slots (Frame slots are non-UNUSED by old clause 4 with H or - // P > 0). assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { - reveal(VmStore::structural_inv); - reveal(VmStore::accounting_inv); let other_idx = frame_to_index(s.frames[fid_other].paddr); - // pre: usage == Frame from old structural_inv. + lemma_structural_inv_frame(s_before, fid_other); + lemma_accounting_inv_at(s_before, other_idx); assert(old_regions.slot_owners[other_idx].usage is Frame); if other_idx == target_idx { - // Cursor preserves usage at target_idx. assert(s.regions.slot_owners[target_idx].usage == old_regions.slot_owners[target_idx].usage); } else { - // pre rc != UNUSED at Frame slots with active head (H or P - // > 0). Need to invoke H_pre >= 1 (fid_other counts) or - // pre_paths > 0; here fid_other is still in s.frames - // (which == old_frames.remove(fid)), so unless fid_other == - // fid, fid_other is also in old_frames. Hence pre H >= 1 - // at other_idx, so pre clause 4 ⟹ pre rc != UNUSED. assert(old_frames.dom().filter( |gid: FrameId| frame_to_index(old_frames[gid].paddr) == other_idx, ).contains(fid_other)); @@ -2213,21 +1631,18 @@ proof fn lemma_step_map<'rcu>( assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]); } }; - // Discharge segment-covered ⟹ Frame-usage. Same shape: covered - // slots are non-UNUSED pre (cover >= 1 + clause 4 ⟹ active); - // cursor preserves Frame slots fully (target_idx via map axiom, - // others via the "non-mapped pre-non-UNUSED" clause). assert forall|sid: SegmentId, paddr_c: Paddr| #![trigger - s.segments.dom().contains(sid), + s.segments.contains_key(sid), frame_to_index(paddr_c)] - s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c + s.segments.contains_key(sid) && s.segments[sid].range.start <= paddr_c < s.segments[sid].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner( paddr_c, ).usage is Frame by { - reveal(VmStore::structural_inv); - reveal(VmStore::accounting_inv); let cov_idx = frame_to_index(paddr_c); + lemma_structural_inv_segment(s_before, sid, paddr_c); + s_before.regions.lemma_contains_valid_frame_paddr(paddr_c); + lemma_accounting_inv_at(s_before, cov_idx); // pre cover >= 1 at cov_idx ⟹ pre slot is Frame + non-UNUSED. lemma_segment_cover_contains(old_regions_segments_helper(s), sid, paddr_c); assert(old_regions.slot_owners[cov_idx].usage is Frame); @@ -2245,10 +1660,7 @@ proof fn lemma_step_map<'rcu>( assert(s.structural_inv()) by { reveal(VmStore::structural_inv); }; - assert(s.inv()) by { - reveal(VmStore::inv); - }; - assert(s.vm_spaces.dom().contains(entry.vm_space)); + assert(s.vm_spaces.contains_key(entry.vm_space)); s.lemma_insert_cursor(c, entry); } @@ -2262,10 +1674,12 @@ spec fn old_regions_segments_helper<'rcu>(s: &VmStore<'rcu>) -> Map(tracked s: &mut VmStore<'rcu>, c: CursorId, len: usize) requires old(s).inv(), - old(s).cursors.dom().contains(c), + old(s).cursors.contains_key(c), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost s_before = *s; let ghost old_regions = s.regions; let ghost old_frames = s.frames; @@ -2276,14 +1690,7 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: &mut s.tlb_model, cursor::CursorMutRegionsMethod::Unmap(len), ); - // Slot-perm coverage: unmap preserves `slots` and never touches an - // unparked PT-root slot, so the coverage exception carries. lemma_coverage_preserved_slots_eq(s_before, *s); - // Discharge `accounting_inv` clause-by-clause. The unmap axiom - // gives: usage/raw_count/in_list/slot_vaddr/vtable_ptr preserved - // universally; rc doesn't bump to UNIQUE; storage preserved at - // post-non-UNUSED; at Frame slots, `rc - paths.len` is invariant - // with both monotonically non-increasing. `s.frames` is unchanged. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -2292,56 +1699,27 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: s.segments, index_to_frame(idx), ) == 0 by { - // From `regions.inv()`: idx < max_meta_slots ⟹ slot_owners[idx] - // satisfies MetaSlotOwner::inv (so UNUSED ∧ non-MMIO ⟹ paths - // empty fires). assert(s.regions.contains(idx)); - // Post cover == 0. Unmap leaves `s.segments` untouched, so post - // cover == pre cover. If pre cover >= 1: a witnessing segment + - // structural `covered ⟹ Frame` gives pre usage == Frame, and pre - // `accounting_inv` clause #4 (active head) gives pre rc <= - // REF_COUNT_MAX; the unmap rc-paths clause then gives post rc <= - // pre rc <= MAX < UNUSED — contradicting post UNUSED. Hence pre - // cover == 0 (segment covers survive unmap, which removes only - // PTE paths). assert(segment_cover_count(s.segments, index_to_frame(idx)) == 0) by { if segment_cover_count(old(s).segments, index_to_frame(idx)) > 0 { let pa = index_to_frame(idx); let sid = lemma_segment_cover_witness(old(s).segments, pa); - // Paddr round-trip / alignment so the structural - // `covered ⟹ Frame` clause (keyed by `frame_to_index`) - // fires at `(sid, pa)`. assert(pa == (idx * PAGE_SIZE) as usize); assert(pa % PAGE_SIZE == 0); assert(frame_to_index(pa) == idx); - // structural `covered ⟹ Frame` at the witness (old state). assert(old_regions.slot_owners[idx].usage is Frame); - // active head (cover > 0 ∧ Frame) ⟹ pre rc != UNUSED, <= MAX. assert(old_regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED); assert(old_regions.slot_owners[idx].ref_count() <= REF_COUNT_MAX); - // unmap (Frame): post rc <= pre rc <= MAX < UNUSED. assert(s.regions.slot_owners[idx].ref_count() <= REF_COUNT_MAX); } }; // Case-split on pre.usage: usage is preserved by the axiom. if old_regions.slot_owners[idx].usage is Frame { - // Post.paths empty: Frame ∧ post UNUSED + MetaSlotOwner::inv - // (UNUSED ∧ non-MMIO ⟹ paths empty). assert(s.regions.slot_owners[idx].usage != PageUsage::MMIO); assert(s.regions.slot_owners[idx].paths_in_pt == Set::empty()); - // Post.H == 0: at Frame post UNUSED, pre rc == pre paths - // (from rc-paths invariant: post rc + pre paths = pre rc + - // post paths ⟹ 0 + pre paths = pre rc + 0). If pre rc != - // UNUSED: pre active head (rc > 0), pre clause 4 ⟹ pre rc - // == pre H + pre paths ⟹ pre H == 0. If pre rc == UNUSED: - // pre clause 1 ⟹ pre H == 0. } else if old_regions.slot_owners[idx].usage == PageUsage::MMIO { - // MMIO slots are fully preserved (axiom). Pre clause 1 - // gives the conjunction for pre UNUSED MMIO directly. assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } else { - // Non-Frame non-MMIO (PT-node): MetaSlotOwner::inv UNUSED - // gives paths empty. H == 0 from no-FrameId-at-non-Frame. assert(s.regions.slot_owners[idx].usage != PageUsage::MMIO); assert(s.regions.slot_owners[idx].paths_in_pt == Set::empty()); assert(handle_count(s.frames, idx) == 0) by { @@ -2349,10 +1727,7 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: |gid: FrameId| frame_to_index(s.frames[gid].paddr) == idx, ); assert forall|fid: FrameId| #[trigger] filt.contains(fid) implies false by { - // s.frames == old_frames (unmap doesn't touch frames). - // structural ⟹ pre slot's usage == Frame, but we're - // in the non-Frame branch — contradiction. - assert(s.frames.dom().contains(fid)); + assert(s.frames.contains_key(fid)); assert(frame_to_index(s.frames[fid].paddr) == idx); assert(s.regions.slot_owners[idx].usage is Frame); }; @@ -2371,41 +1746,19 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: s.segments, index_to_frame(idx), ) > 0 by { - // Post usage == Frame ⟹ pre usage == Frame (usage preserved). - // At Frame slots, the rc-paths invariant `post rc + pre paths - // == pre rc + post paths` combined with `post paths.len ≤ pre - // paths.len` forces pre UNUSED ⟹ post UNUSED (since pre UNUSED - // gives pre paths == 0 via MetaSlotOwner::inv, the equation - // becomes post rc == pre rc + post paths but post rc ≤ pre rc - // ⟹ post paths == 0 ⟹ post rc == pre rc == UNUSED). So at - // post non-UNUSED Frame slot, pre rc != UNUSED. assert(s.regions.contains(idx)); assert(old_regions.slot_owners[idx].ref_count() != REF_COUNT_UNUSED) by { if old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNUSED { - // Trigger MetaSlotOwner::inv on pre at this idx. assert(old_regions.contains(idx)); assert(old_regions.slot_owners[idx].paths_in_pt == Set::empty()); - // rc-paths invariant: post rc + 0 == UNUSED + post paths - // ⟹ post rc == UNUSED + post paths. - // post rc <= pre rc == UNUSED ⟹ post paths == 0 - // ⟹ post rc == UNUSED. - // But post rc != UNUSED by assumption. Contradiction. assert(s.regions.slot_owners[idx].paths_in_pt.len() == 0); assert(false); } }; - // Pre non-UNUSED Frame: clause 3 gives pre H > 0 OR pre paths > 0 - // OR pre cover > 0. Segments unchanged ⟹ post cover == pre cover. if handle_count(old_frames, idx) > 0 { assert(handle_count(s.frames, idx) > 0); } else if segment_cover_count(s.segments, index_to_frame(idx)) > 0 { - // Cover > 0 directly satisfies the new disjunct. } else { - // pre H == 0 ∧ pre cover == 0. Clause 3 ⟹ pre paths > 0. - // Clause 4 (active head pre) ⟹ pre rc == pre H + pre paths - // + pre cover == pre paths. From rc-paths invariant: - // post paths == pre paths - pre rc + post rc == post rc. - // post rc != UNUSED ⟹ post rc > 0 ⟹ post paths > 0. assert(s.regions.slot_owners[idx].paths_in_pt.len() > 0); } }; @@ -2424,54 +1777,20 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: index_to_frame(idx), ) } by { - // Post is active head. H unchanged ⟹ pre H == post H. Pre - // usage == Frame (preserved). Either pre H > 0 (pre active head) - // or post paths > 0 with post H == 0 ⟹ pre paths >= post paths - // > 0 (pre active head). Either way, pre clause 4 applies: - // pre rc != UNUSED ∧ pre rc != UNIQUE ∧ - // pre rc == pre H + pre paths ∧ pre storage init. if handle_count(s.frames, idx) > 0 { - // pre H > 0 ⟹ pre active head ⟹ pre clause 4. assert(handle_count(old_frames, idx) > 0); } else { - // post H == 0, post paths > 0. Frame-slot axiom: post rc + - // pre paths == pre rc + post paths. post rc != UNUSED - // (otherwise contradicts active head), so post rc > 0. - // pre paths == pre rc + post paths - post rc. Combined with - // pre paths >= post paths (monotonic): pre rc >= post rc > 0. - // So pre rc != UNUSED ⟹ pre clause 3 ⟹ pre H > 0 OR pre - // paths > 0. pre H == 0 (H unchanged), so pre paths > 0 ⟹ - // pre active head ⟹ pre clause 4. assert(old_regions.slot_owners[idx].paths_in_pt.len() > 0); } - // Now pre clause 4 gives: pre rc == pre H + pre paths, - // pre rc != UNUSED, pre rc != UNIQUE, - // pre storage.is_init. - // Frame-slot axiom: post rc + pre paths == pre rc + post paths - // ⟹ post rc == pre rc + post paths - pre paths - // ⟹ post rc == (pre H + pre paths) + post paths - pre paths - // ⟹ post rc == pre H + post paths - // ⟹ post rc == post H + post paths. ✓ - // post rc != UNUSED: from active head assumption. - // post rc != UNIQUE: axiom's pre != UNIQUE ⟹ post != UNIQUE. - // storage init: axiom's "post non-UNUSED ⟹ storage preserved". }; - // Discharge structural_inv's FrameId⟹Frame-usage clause. Unmap - // preserves usage universally, so it holds trivially. assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { let other_idx = frame_to_index(s.frames[fid_other].paddr); assert(s.regions.slot_owners[other_idx].usage == old_regions.slot_owners[other_idx].usage); }; - // Discharge the structural unique-entry validity clause. Unmap never - // touches a UNIQUE slot: such a slot is `usage == Frame` with empty - // `paths_in_pt`, so the Frame rc-paths invariant (`post rc - post - // paths.len == pre rc - pre paths.len`, paths monotonically - // non-increasing) forces post `paths` empty and post `rc == pre rc - // == UNIQUE`; `usage` / `in_list` are preserved universally. - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -2479,7 +1798,7 @@ proof fn lemma_step_unmap<'rcu>(tracked s: &mut VmStore<'rcu>, c: CursorId, len: &&& so.paths_in_pt.is_empty() } by { let u_idx = frame_to_index(s.unique_frames[u].paddr); - assert(old(s).unique_frames.dom().contains(u)); + assert(old(s).unique_frames.contains_key(u)); // Old validity at `u`. assert(old_regions.slot_owners[u_idx].usage is Frame); assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); @@ -2512,10 +1831,11 @@ proof fn lemma_step_new_vm_io<'rcu>( ) requires old(s).inv(), - old(s).vm_spaces.dom().contains(vs), + old(s).vm_spaces.contains_key(vs), ensures final(s).inv(), { + reveal(VmStore::structural_inv); let tracked vm_space_ref = s.vm_spaces.tracked_borrow(vs); let tracked res = io::new_vm_io_step(vm_space_ref, Some(vs), vaddr, len, kind); match res { @@ -2548,7 +1868,7 @@ proof fn lemma_step_new_kernel_vm_io<'rcu>( proof fn lemma_step_drop_vm_io<'rcu>(tracked s: &mut VmStore<'rcu>, vio: VmIoId) requires old(s).inv(), - old(s).vm_ios.dom().contains(vio), + old(s).vm_ios.contains_key(vio), ensures final(s).inv(), { @@ -2563,10 +1883,11 @@ proof fn lemma_step_vm_io_method<'rcu>( ) requires old(s).inv(), - old(s).vm_ios.dom().contains(vio), + old(s).vm_ios.contains_key(vio), ensures final(s).inv(), { + reveal(VmStore::structural_inv); let tracked mut entry = s.tracked_extract_vm_io(vio); io::vm_io_method_step(&mut entry, method); s.lemma_insert_vm_io(vio, entry); @@ -2575,8 +1896,8 @@ proof fn lemma_step_vm_io_method<'rcu>( proof fn lemma_step_read<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, dest: VmIoId) requires old(s).inv(), - old(s).vm_ios.dom().contains(source), - old(s).vm_ios.dom().contains(dest), + old(s).vm_ios.contains_key(source), + old(s).vm_ios.contains_key(dest), source != dest, old(s).vm_ios[source].vm_space is None, old(s).vm_ios[source].kind == VmIoKind::Reader, @@ -2585,6 +1906,7 @@ proof fn lemma_step_read<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, de ensures final(s).inv(), { + reveal(VmStore::structural_inv); let tracked mut src = s.tracked_extract_vm_io(source); let tracked mut dst = s.tracked_extract_vm_io(dest); let tracked val = io::read_step(&mut src, &mut dst); @@ -2598,8 +1920,8 @@ proof fn lemma_step_read<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, de proof fn lemma_step_write<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, dest: VmIoId) requires old(s).inv(), - old(s).vm_ios.dom().contains(source), - old(s).vm_ios.dom().contains(dest), + old(s).vm_ios.contains_key(source), + old(s).vm_ios.contains_key(dest), source != dest, old(s).vm_ios[source].vm_space is None, old(s).vm_ios[source].kind == VmIoKind::Reader, @@ -2608,6 +1930,7 @@ proof fn lemma_step_write<'rcu>(tracked s: &mut VmStore<'rcu>, source: VmIoId, d ensures final(s).inv(), { + reveal(VmStore::structural_inv); let tracked mut src = s.tracked_extract_vm_io(source); let tracked mut dst = s.tracked_extract_vm_io(dest); s.lemma_insert_vm_io(source, src); @@ -2620,12 +1943,8 @@ proof fn lemma_step_frame_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, paddr ensures final(s).inv(), { - // `op_pre` is `true`: any `paddr` is accepted, a bad one just fails. - // `from_unused_step` requires `valid_frame_paddr ==> slots.contains_key`; - // after the `VmSpace::new` coverage change a slot perm may be absent - // (held as a PT root), so guard on it directly — an unparked slot - // means the frame is held elsewhere and the real `from_unused` fails - // (modeled here as a no-op). + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_frames = s.frames; let ghost old_regions = s.regions; if !valid_frame_paddr(paddr) || s.regions.slots.contains_key(frame_to_index(paddr)) { @@ -2639,13 +1958,9 @@ proof fn lemma_step_frame_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, paddr s.lemma_insert_frame(id, entry); assert(s.frames[id].paddr == paddr); - // Pre target_idx was rc=UNUSED ⟹ pre H==0 ∧ pre paths.empty() - // (via old accounting_inv's UNUSED clause). assert(handle_count(old_frames, target_idx) == 0); assert(old_regions.slot_owners[target_idx].paths_in_pt.is_empty()); - // 5.5c new clause: "UNUSED ⟹ no users". Other idx unchanged - // (lemma + slot_owner preservation); target_idx is now rc=1. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -2653,16 +1968,11 @@ proof fn lemma_step_frame_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, paddr && s.regions.slot_owners[idx].paths_in_pt.is_empty() by { lemma_handle_count_insert_fresh(old_frames, id, entry, idx); if idx == target_idx { - // post rc=1 != UNUSED, antecedent false. assert(false); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; - - // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". Other - // idx unchanged (so old clause carries); target post is - // active (H=1). assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].usage is Frame @@ -2699,18 +2009,13 @@ proof fn lemma_step_frame_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, paddr assert(old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNUSED); assert(handle_count(old_frames, idx) == 0); assert(handle_count(s.frames, idx) == 1); - // Pre clause 2 (UNUSED) gives pre cover == 0; - // segments unchanged ⟹ post cover == 0. assert(segment_cover_count(s.segments, index_to_frame(idx)) == 0); } else { - // Other slot: slot_owner preserved by from_unused - // (forall i != target_idx clause in reparked_spec). assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; }, Option::None => { - // regions unchanged ⇒ accounting preserved from old. assert(s.regions == old_regions); }, } @@ -2723,10 +2028,8 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr ensures final(s).inv(), { - // See `lemma_step_frame_from_unused`: `op_pre` is `true`. `from_in_use_step` - // requires `valid_frame_paddr ==> slots.contains_key`; guard on it - // directly (an unparked slot ⟹ the frame is held elsewhere ⟹ the - // real `from_in_use` fails, a no-op). + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_frames = s.frames; let ghost old_regions = s.regions; if !valid_frame_paddr(paddr) || s.regions.slots.contains_key(frame_to_index(paddr)) { @@ -2739,8 +2042,6 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr s.lemma_insert_frame(id, entry); assert(s.frames[id].paddr == paddr); - // 5.5c new clause: "UNUSED ⟹ no users". For target: post - // rc = pre rc + 1 != UNUSED. For other idx: unchanged. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -2754,8 +2055,6 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr } }; - // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". For - // target post: H = pre + 1 ≥ 1 → active. For other: unchanged. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].usage is Frame @@ -2774,7 +2073,6 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr } }; - // Per-slot accounting (forall covers active heads only). assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].usage is Frame && ( @@ -2789,9 +2087,6 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr } by { lemma_handle_count_insert_fresh(old_frames, id, entry, idx); if idx == target_idx { - // Pre usage(target)==Frame: `get_from_in_use` - // preserves `usage`. Pre active-head fires from - // pre H >= 1 (or pre paths > 0, or pre cover > 0). assert(old_regions.slot_owners[idx].usage is Frame); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); @@ -2808,24 +2103,17 @@ proof fn lemma_step_frame_from_in_use<'rcu>(tracked s: &mut VmStore<'rcu>, paddr proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId) requires old(s).inv(), - old(s).frames.dom().contains(fid), - // No segment forgot a reference to this slot. The other - // `drop_pre` conjuncts (rc, storage, in_list, paths-empty - // residuals) are derived from `old(s).inv()` via - // [`lemma_frame_drop_pre_derivable`]. + old(s).frames.contains_key(fid), segment_cover_count(old(s).segments, old(s).frames[fid].paddr) == 0, ensures final(s).inv(), { - // Derive `drop_pre` + handle-clause from `s.inv()` (Item 2: - // embedding-level `Frame::wf(state)`). + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); lemma_frame_drop_pre_derivable(*s, fid); let ghost p = s.frames[fid].paddr; s.regions.lemma_contains_valid_frame_paddr(p); let ghost idx_p = frame_to_index(p); - // `fid ∈ s.frames` ⟹ `handle_count(s.frames, idx_p) ≥ 1`. Used - // below to chain `lemma_handle_count_remove` and re-establish - // accounting_inv's Frame-scoped clauses. assert(s.frames.dom().filter( |gid: FrameId| frame_to_index(s.frames[gid].paddr) == idx_p, ).contains(fid)); @@ -2835,15 +2123,6 @@ proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId let ghost old_regions = s.regions; let tracked entry = s.tracked_extract_frame(fid); frame::drop_step(&mut s.regions, entry); - - // Discharge accounting_inv on the post-drop state. Handle clause - // is gone; only clauses 2 (UNUSED), 3 (Frame active head), 4 - // (Frame equation) remain. - - // 5.5c new clause: "UNUSED ⟹ no users". For non-target: unchanged. - // For target: if drop teardown (rc 1→UNUSED), need post H==0 and - // paths empty. Both hold: pre eqn 1==H+P with H>=1 ⟹ H==1, P==0 - // ⟹ post H==0 (fid removed) and post paths == pre paths == empty. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -2854,23 +2133,15 @@ proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId ) == 0 by { lemma_handle_count_remove(old_frames, fid, idx); if idx == target_idx { - // Post rc==UNUSED ⟹ pre rc was 1 (drop_step rc transition). assert(old_regions.slot_owners[idx].ref_count() == 1); - // Old handle clause: pre rc (== 1) >= pre handle_count, and - // `fid` contributes ⟹ pre handle_count == 1 ⟹ post == 0. assert(handle_count(old_frames, idx) == 1); assert(handle_count(s.frames, idx) == 0); - // pre rc == 1 ⟹ `drop_step` leaves `paths_in_pt` empty. assert(s.regions.slot_owners[idx].paths_in_pt.is_empty()); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; - // 5.5c new clause: "Frame ∧ non-sentinel ⟹ active". For target - // post in rc>1 case: rc-1 in [1,MAX-1] non-sentinel; H-=1 or P - // preserved. Pre H+P=pre rc; if post H>=1, active; else pre H=1 - // so pre P=pre rc-1 >= 1 (rc>1), post P >= 1, active. ✓ assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].usage is Frame @@ -2884,10 +2155,6 @@ proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId ) > 0 by { lemma_handle_count_remove(old_frames, fid, idx); if idx == target_idx { - // Post rc != UNUSED ⟹ drop_step did rc-1 (not teardown). - // ⟹ pre rc > 1. Pre H==1+ + pre P; if pre H > 1: post H>=1 - // ✓. If pre H == 1: pre P = pre rc - 1 >= 1; post P preserved - // >= 1 ✓. assert(handle_count(old_frames, idx) >= 1); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); @@ -2914,54 +2181,37 @@ proof fn lemma_step_frame_drop<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId } by { lemma_handle_count_remove(old_frames, fid, idx); if idx == target_idx { - // Pre fid contributes ⇒ pre H >= 1 ⇒ pre active head. - // Pre `usage == Frame`: `drop_step` preserves `usage`, - // and the clause antecedent gives post `usage == Frame`. assert(old_regions.slot_owners[idx].usage is Frame); assert(handle_count(old_frames, idx) > 0); let ghost pre_rc = old_regions.slot_owners[idx].ref_count(); let ghost pre_h = handle_count(old_frames, idx); let ghost pre_p = old_regions.slot_owners[idx].paths_in_pt.len(); assert(pre_rc == pre_h + pre_p); - // Residual `drop_pre`: pre rc <= MAX, pre rc >= 1, != UNUSED/UNIQUE. let ghost post_h = handle_count(s.frames, idx); assert(post_h == (pre_h - 1) as nat); - // drop_step now exposes paths preservation at idx. let ghost post_p = s.regions.slot_owners[idx].paths_in_pt.len(); assert(post_p == pre_p); let ghost post_rc = s.regions.slot_owners[idx].ref_count(); if pre_rc > 1 { - // drop_step rc>1 branch: post rc = pre - 1, storage preserved. assert(post_rc == (pre_rc - 1) as u64); assert(post_rc as nat == post_h + post_p); assert(s.regions.slot_owners[idx].storage_perm() == old_regions.slot_owners[idx].storage_perm()); } else { - // pre_rc == 1: pre eqn 1 == pre_h + pre_p with - // pre_h >= 1 forces pre_h = 1, pre_p = 0. assert(pre_h == 1); assert(pre_p == 0); assert(post_h == 0); assert(post_p == 0); - // drop_step rc==1 branch: post rc = UNUSED. assert(post_rc == REF_COUNT_UNUSED); - // ⇒ post is NOT active head at idx, so we're not - // actually inside this body in this case - // (antecedent false). Contradicts the implies guard. assert(false); } } else { - // Other slot: slot_owner preserved by drop_step - // (forall i != target_idx clause in ensures). assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; } /// Discharges the post-state `accounting_inv` for `lemma_step_segment_from_unused`. -/// Isolated into its own prover query so the three per-`idx` universal clauses -/// (each matching `s.regions.slot_owners[idx]`) do not blow up the SMT context -/// of the step's main body check. #[verifier::spinoff_prover] proof fn lemma_step_segment_from_unused_accounting<'rcu>( s_after: VmStore<'rcu>, @@ -2973,19 +2223,15 @@ proof fn lemma_step_segment_from_unused_accounting<'rcu>( requires s_after.regions.inv(), s_after.frames == old_store.frames, - // Exactly one fresh segment was inserted. - !old_store.segments.dom().contains(id), + !old_store.segments.contains_key(id), s_after.segments == old_store.segments.insert(id, entry), entry.range == range, - // Range is aligned and in-bounds. range.start % PAGE_SIZE == 0, range.end % PAGE_SIZE == 0, range.start < range.end, range.end <= MAX_PADDR, - // Pre-state accounting holds over the old snapshot. old_store.accounting_inv(), old_store.regions.inv(), - // In-range slots: post usage/rc/paths/storage from the allocation axiom. forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (range.start <= paddr < range.end && paddr % PAGE_SIZE == 0) ==> { @@ -2996,12 +2242,10 @@ proof fn lemma_step_segment_from_unused_accounting<'rcu>( &&& so.paths_in_pt.is_empty() &&& so.storage_perm().is_init() }, - // Outside-range slots are fully preserved by the allocation axiom. forall|i: int| #![trigger s_after.regions.slot_owners[i]] i < max_meta_slots() && !(range.start <= index_to_frame(i) < range.end) ==> s_after.regions.slot_owners[i] == old_store.regions.slot_owners[i], - // Old regions' in-range slots were UNUSED (precondition of the step). forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (range.start <= paddr < range.end && paddr % PAGE_SIZE == 0) @@ -3009,6 +2253,7 @@ proof fn lemma_step_segment_from_unused_accounting<'rcu>( ensures s_after.accounting_inv(), { + reveal(VmStore::accounting_inv); let old_regions = old_store.regions; let old_frames = old_store.frames; let old_segments = old_store.segments; @@ -3061,18 +2306,13 @@ proof fn lemma_step_segment_from_unused_accounting<'rcu>( let paddr = index_to_frame(idx); if range.start <= paddr < range.end { lemma_segment_cover_insert_inside(old_segments, id, entry, paddr); - // In-range post slot: rc == 1 (allocation axiom), H == 0 (frames - // unchanged and pre UNUSED ⟹ pre H == 0), paths empty, cover == 1. } else { lemma_segment_cover_insert_outside(old_segments, id, entry, paddr); } }; } -/// `Op::SegmentFromUnused` step. Allocates a fresh `SegmentEntry` -/// covering `range` on success. Discharges `accounting_inv` from the -/// post-state's per-slot ensures (every covered slot transitions -/// `UNUSED → Frame, rc=1, raw_count=1`). +/// `Op::SegmentFromUnused` step. #[verifier::spinoff_prover] proof fn lemma_step_segment_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, range: Range) requires @@ -3080,13 +2320,8 @@ proof fn lemma_step_segment_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, ran ensures final(s).inv(), { - hide(VmStore::accounting_inv); - // Exec `Segment::from_unused` returns `Err` (NotAligned/OutOfBound) - // or rolls back its partial allocation (when some frame in `range` - // is not free), leaving `regions` unchanged in every failure case. - // Only an aligned, in-bound, non-empty range whose every covered - // slot is genuinely UNUSED produces a fresh segment; the step - // branches on that condition and is a no-op otherwise. + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); if range.start % PAGE_SIZE == 0 && range.end % PAGE_SIZE == 0 && range.start < range.end && range.end <= MAX_PADDR && (forall|paddr: Paddr| #![trigger frame_to_index(paddr)] @@ -3097,29 +2332,13 @@ proof fn lemma_step_segment_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, ran let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; - // Slot-perm coverage in `range`: each range slot is `rc == UNUSED`, - // which fails the PageTable-node coverage exception, so its perm - // is parked (`slots.contains_key`). let tracked res = segment::from_unused_step(&mut s.regions, range); match res { Option::Some(entry) => { let ghost id = fresh_segment_id(s.segments); lemma_fresh_segment_id_not_in_dom(s.segments); s.lemma_insert_segment(id, entry); - // Slot-perm coverage: allocation preserves `slots` and - // never touches an unparked PT-root slot. - // Discharge accounting_inv on the post-state via an isolated - // helper query (the three per-`idx` universal clauses are the - // SMT-cost hot spot of this step). lemma_step_segment_from_unused_accounting(*s, s_before, range, id, entry); - // structural FrameId⟹Frame-usage: every existing fid's - // slot's usage preserved. Frame-usage slots are non-UNUSED - // pre (clause 4), so they're outside `range` (which is all - // UNUSED pre). Axiom fully preserves outside-range slots. - // Discharge the structural unique-entry validity clause. A - // UNIQUE slot is `usage == Frame` at `rc == REF_COUNT_UNIQUE` - // (`!= UNUSED`), so it is not in the freshly-allocated `range` - // (all-UNUSED) and the axiom preserves it fully. }, Option::None => {}, } @@ -3174,7 +2393,7 @@ proof fn lemma_drop_segment_with_store_inv<'rcu>( ) requires store.inv(), - store.segments.dom().contains(sid), + store.segments.contains_key(sid), entry == store.segments[sid], *old(regions) == store.regions, ensures @@ -3210,6 +2429,8 @@ proof fn lemma_drop_segment_with_store_inv<'rcu>( #![auto] c.metaregion_sound(*old(regions)) ==> c.metaregion_sound(*final(regions)), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); assert forall|paddr: Paddr| #![trigger store.regions.slot_owner(paddr)] (entry.range.start <= paddr < entry.range.end && paddr % PAGE_SIZE == 0) implies { @@ -3230,32 +2451,18 @@ proof fn lemma_drop_segment_with_store_inv<'rcu>( segment::drop_step(regions, entry); } -/// `Op::SegmentDrop` step. Removes the `SegmentEntry` at `sid` and -/// releases the segment's forgotten reference at each covered frame. -/// Frames whose `rc` reaches 1 transition to UNUSED. +/// `Op::SegmentDrop` step. #[verifier::spinoff_prover] #[verifier::rlimit(50)] proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), ensures final(s).inv(), { - hide(MetaSlotOwner::storage_perm); - hide(MetaSlotOwner::vtable_ptr_perm); - hide(VmStore::inv); - hide(VmStore::structural_inv); - hide(VmStore::accounting_inv); - assert(s.structural_inv()) by { - reveal(VmStore::inv); - }; - assert(s.accounting_inv()) by { - reveal(VmStore::inv); - }; - assert(s.regions.inv()) by { - reveal(VmStore::structural_inv); - }; + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost s_before = *s; let ghost old_regions = s.regions; let ghost old_frames = s.frames; @@ -3264,26 +2471,8 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme let tracked entry = s.tracked_extract_segment(sid); assert(entry.range == range); lemma_drop_segment_with_store_inv(&mut s.regions, entry, s_before, sid); - // Slot-perm coverage: drop preserves `slots` and never touches an - // unparked PT-root slot, so the coverage exception carries. lemma_coverage_preserved_slots_eq(s_before, *s); - // Re-establish structural_inv + accounting_inv on the post-state. - // Per-slot reasoning: - // - Slot in `range`: post raw_count = pre - 1; segments lost - // `sid` whose range covered this paddr, so post cover = pre - 1. - // ⟹ post raw_count == post cover. ✓ - // For accounting: pre eq was `rc == H + P + cover`. Post rc: - // if pre rc > 1: post rc = pre rc - 1. - // if pre rc == 1: post rc = UNUSED (teardown). - // Post H = pre H, post P = pre P, post cover = pre cover - 1. - // If pre rc > 1: post rc = pre rc - 1 = H + P + (cover - 1) = post eq ✓. - // If pre rc == 1: pre H == 0 ∧ pre P == 0 ∧ pre cover == 1 - // (from rc == 1). Post H = 0, post P = 0, post cover = 0, - // post rc = UNUSED. Clause 1 (UNUSED) fires; equation vacuous. - // - Slot outside `range`: fully preserved (axiom + segment removal - // leaves cover unchanged at outside paddrs). - assert forall|idx: int| 0 <= idx < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].in_list_perm.value() @@ -3294,12 +2483,10 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme assert(paddr % PAGE_SIZE == 0); assert(frame_to_index(paddr) == idx); if range.start <= paddr < range.end { - // Axiom preserves in_list at in-range slots. } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); } }; - // Discharge accounting_inv clauses. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -3314,19 +2501,12 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme assert(paddr % PAGE_SIZE == 0); assert(frame_to_index(paddr) == idx); if range.start <= paddr < range.end { - // Post UNUSED at in-range ⟹ pre rc == 1 (axiom transition). - // Pre eq: 1 == H + P + cover, cover >= 1 ⟹ cover == 1, - // H == 0, P == 0. Frames unchanged ⟹ post H == 0. - // Paths preserved ⟹ post P == 0 ⟹ post paths empty. - // Segments removed sid (whose range covered paddr) ⟹ - // post cover == 0. lemma_segment_cover_contains(old_segments, sid, paddr); lemma_segment_cover_remove_inside(old_segments, sid, paddr); assert(old_regions.slot_owners[idx].ref_count() == 1); assert(handle_count(old_frames, idx) == 0); assert(s.regions.slot_owners[idx].paths_in_pt == Set::empty()); } else { - // Outside: fully preserved; segments removal doesn't affect cover. assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); assert(!(entry.range.start <= paddr < entry.range.end)); lemma_segment_cover_remove_outside(old_segments, sid, paddr); @@ -3349,10 +2529,6 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme assert(paddr % PAGE_SIZE == 0); assert(frame_to_index(paddr) == idx); if range.start <= paddr < range.end { - // Post non-UNUSED at in-range ⟹ pre rc > 1 (axiom). - // Pre eq: pre rc == H + P + cover. Pre rc > 1 ⟹ at least - // one of H, P, (cover-1) > 0. Post H == pre H, post P == - // pre P, post cover == pre cover - 1. lemma_segment_cover_contains(old_segments, sid, paddr); lemma_segment_cover_remove_inside(old_segments, sid, paddr); } else { @@ -3406,9 +2582,6 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme == old_regions.slot_owners[idx].paths_in_pt); assert(handle_count(s.frames, idx) == pre_H); assert(segment_cover_count(s.segments, paddr) == (pre_cover - 1) as nat); - // post rc <= MAX (pre rc was, post = pre - 1, still in range). - // storage.is_init at post: post rc ∈ SHARED (1 <= post rc <= MAX) - // ⟹ MetaSlotOwner::inv SHARED branch ⟹ storage.is_init. assert(s.regions.contains(idx)); assert(s.regions.slot_owners[idx].metadata_perm.not_empty() ==> s.regions.slot_owners[idx].storage_perm().is_init()); @@ -3418,61 +2591,40 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme lemma_segment_cover_remove_outside(old_segments, sid, paddr); } }; - // Structural FrameId⟹Frame-usage: frames unchanged; for fid_other's - // slot, usage preserved (covered slots remain Frame; in-range slots - // either stay non-UNUSED (rc-1) or become UNUSED — UNUSED ones had - // H == 0, so no fid points there). assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { reveal(VmStore::structural_inv); reveal(VmStore::accounting_inv); let other_idx = frame_to_index(s.frames[fid_other].paddr); let other_paddr = index_to_frame(other_idx); - // Pre fid_other's slot: usage == Frame (old structural). assert(old_regions.slot_owners[other_idx].usage is Frame); - // Pre H >= 1 at other_idx (fid_other contributes). assert(old_frames.dom().filter( |gid: FrameId| frame_to_index(old_frames[gid].paddr) == other_idx, ).contains(fid_other)); assert(handle_count(old_frames, other_idx) >= 1); - // Pre clause 4: pre rc == H + P + cover ≥ 1 ⟹ rc != UNUSED. assert(old_regions.slot_owners[other_idx].ref_count() >= 1); - // Axiom preserves usage (universal). if range.start <= other_paddr < range.end { - // In-range: usage preserved by axiom. } else { - // Outside: fully preserved. assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]); } }; - // Structural segment-covered ⟹ Frame-usage: for any remaining - // segment sid_other ≠ sid, usage at every covered paddr is - // preserved (usage universally preserved by axiom). assert forall|sid_other: SegmentId, paddr_c: Paddr| #![trigger - s.segments.dom().contains(sid_other), + s.segments.contains_key(sid_other), frame_to_index(paddr_c)] - s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c + s.segments.contains_key(sid_other) && s.segments[sid_other].range.start <= paddr_c < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner(paddr_c).usage is Frame by { reveal(VmStore::structural_inv); let cov_idx = frame_to_index(paddr_c); - // sid_other != sid (since sid was removed from s.segments). assert(sid_other != sid); - // sid_other was in old_segments too. - assert(old_segments.dom().contains(sid_other)); + assert(old_segments.contains_key(sid_other)); assert(old_segments[sid_other] == s.segments[sid_other]); - // Pre covered ⟹ pre Frame from old structural. assert(old_regions.slot_owners[cov_idx].usage is Frame); - // Axiom preserves usage universally. }; - // Discharge the structural unique-entry validity clause. A UNIQUE - // slot is `rc == REF_COUNT_UNIQUE`, so by the accounting equation - // (`cover_count > 0 ⟹ rc != UNIQUE`) it is uncovered; hence outside - // the dropped segment's range, and the teardown axiom preserves it. - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -3483,7 +2635,8 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme reveal(VmStore::accounting_inv); let u_paddr = s.unique_frames[u].paddr; let u_idx = frame_to_index(u_paddr); - assert(old(s).unique_frames.dom().contains(u)); + assert(old(s).unique_frames.contains_key(u)); + assert(valid_frame_paddr(u_paddr)); s.regions.lemma_contains_valid_frame_paddr(u_paddr); // Old UNIQUE validity at `u`. assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); @@ -3501,15 +2654,9 @@ proof fn lemma_step_segment_drop<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme assert(s.structural_inv()) by { reveal(VmStore::structural_inv); }; - assert(s.inv()) by { - reveal(VmStore::inv); - }; } -/// `Op::SegmentSplit` step. Replaces `sid` with two fresh segment -/// entries covering the disjoint halves; `regions` is unchanged. -/// `accounting_inv` chains because per-paddr `cover_count` is -/// invariant under the partition (see [`lemma_segment_cover_split`]). +/// `Op::SegmentSplit` step. proof fn lemma_step_segment_split<'rcu>( tracked s: &mut VmStore<'rcu>, sid: SegmentId, @@ -3517,13 +2664,15 @@ proof fn lemma_step_segment_split<'rcu>( ) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), offset % PAGE_SIZE == 0, 0 < offset, offset < (old(s).segments[sid].range.end - old(s).segments[sid].range.start), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; @@ -3531,11 +2680,6 @@ proof fn lemma_step_segment_split<'rcu>( let ghost mid = (range.start + offset) as Paddr; let ghost entry_left = SegmentEntry { range: range.start..mid }; let ghost entry_right = SegmentEntry { range: mid..range.end }; - // Pick fresh ids BEFORE the extract so they are guaranteed - // distinct from `sid` (which is still in `s.segments`). Choose - // `id_left` first, then `id_right` from the - // `s.segments.insert(id_left, _)`-extended domain so they are - // distinct from each other and from `sid`. let ghost id_left = fresh_segment_id(s.segments); lemma_fresh_segment_id_not_in_dom(s.segments); assert(id_left != sid); @@ -3546,16 +2690,12 @@ proof fn lemma_step_segment_split<'rcu>( assert(id_right != id_left); // Now extract and insert. let tracked _orig = s.tracked_extract_segment(sid); - assert(!s.segments.dom().contains(id_left)); + assert(!s.segments.contains_key(id_left)); let tracked entry_l = tracked_segment_entry_new(range.start..mid); s.lemma_insert_segment(id_left, entry_l); - assert(!s.segments.dom().contains(id_right)); + assert(!s.segments.contains_key(id_right)); let tracked entry_r = tracked_segment_entry_new(mid..range.end); s.lemma_insert_segment(id_right, entry_r); - // Re-establish structural_inv + accounting_inv. Regions is - // unchanged; the partition lemma gives per-paddr cover_count - // preservation; so every invariant clause carries over from - // `s_old`. assert(s.regions == old_regions); assert forall|paddr: Paddr| #[trigger] frame_to_index(paddr) < max_meta_slots() implies segment_cover_count(s.segments, paddr) @@ -3570,41 +2710,28 @@ proof fn lemma_step_segment_split<'rcu>( paddr, ); }; - // Each invariant clause that mentions `cover_count` chains via the - // per-paddr equality above. `slot_owners` / `slots` / `frames` / - // `tlb_model` / `vm_spaces` / `cursors` / `vm_ios` unchanged ⟹ - // their clauses carry verbatim from `old(s).inv()`. - - // Segment range well-formedness for the two new entries. assert(entry_left.range.start % PAGE_SIZE == 0); assert(entry_right.range.start % PAGE_SIZE == 0); assert(entry_left.range.end % PAGE_SIZE == 0); assert(entry_right.range.end % PAGE_SIZE == 0); - // segment-covered ⟹ Frame-usage: covered paddrs by the new - // entries are the same set as covered by the original ⟹ usage - // was Frame pre, still Frame post (regions unchanged). assert forall|sid_other: SegmentId, paddr_c: Paddr| #![trigger - s.segments.dom().contains(sid_other), + s.segments.contains_key(sid_other), frame_to_index(paddr_c)] - s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c + s.segments.contains_key(sid_other) && s.segments[sid_other].range.start <= paddr_c < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner(paddr_c).usage is Frame by { if sid_other == id_left { - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_segments[sid].range.start <= paddr_c < old_segments[sid].range.end); } else if sid_other == id_right { - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_segments[sid].range.start <= paddr_c < old_segments[sid].range.end); } else { - assert(old_segments.dom().contains(sid_other)); + assert(old_segments.contains_key(sid_other)); assert(old_segments[sid_other] == s.segments[sid_other]); } }; - // Discharge accounting_inv's three clauses. Regions unchanged ⟹ - // every per-slot value (rc, paths, usage, etc.) preserved; frames - // unchanged ⟹ handle_count preserved; cover_count preserved - // per-paddr via lemma_segment_cover_split. assert forall|idx: int| #![trigger s.regions.slot_owners[idx]] 0 <= idx < max_meta_slots() && s.regions.slot_owners[idx].ref_count() @@ -3681,45 +2808,20 @@ proof fn lemma_step_segment_split<'rcu>( paddr, ); }; - // `regions` is unchanged by split, so the structural unique-entry - // validity clause is preserved verbatim from `old(s).inv()`. } -/// `Op::SegmentNext` step. Pops the front frame off `sid`'s range, -/// registering a fresh `FrameEntry` at `paddr = range.start`. The -/// segment's range shrinks by one page from the front; if it -/// becomes empty, `sid` is removed. -/// -/// **The conversion bridge** between segment-held forgotten -/// references and user-held `Frame` handles. Per-paddr at the -/// popped slot: -/// `raw_count: pre - 1` (segment lost one forgotten ref via -/// `Frame::from_raw`), -/// `cover_count: pre - 1` (segment's range shrunk past paddr), -/// `H: pre + 1` (fresh `FrameEntry` registered), -/// `rc: pre` (`from_raw` doesn't touch rc; the new -/// `Frame` handle inherits the rc that -/// the segment was holding). -/// -/// Accounting equation `rc == H + P + cover_count`: -/// `pre rc == pre H + pre P + pre cover` -/// `post rc == pre rc -/// == (post H - 1) + post P + (post cover + 1) -/// == post H + post P + post cover`. ✓ -/// -/// Structural `raw_count == cover_count`: -/// pre: `pre raw == pre cover` at every idx. -/// post at popped: `(pre raw - 1) == (pre cover - 1)`. ✓ -/// post elsewhere: unchanged. +/// `Op::SegmentNext` step. #[verifier::spinoff_prover] #[verifier::rlimit(200)] proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; @@ -3753,9 +2855,6 @@ proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme } assert(s.frames == old_frames.insert(fid, frame_entry)); - // Per-paddr cover delta (from the shrink-front lemma): cover_post - // == cover_pre - (1 at popped else 0). - // Cleaner per-paddr facts: at popped, cover decreased by 1; elsewhere unchanged. assert forall|paddr_c: Paddr| paddr_c % PAGE_SIZE == 0 && paddr_c == paddr implies #[trigger] segment_cover_count( s.segments, @@ -3832,10 +2931,7 @@ proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme } else { } }; - // Discharge the structural unique-entry validity clause. A UNIQUE - // slot is `rc == REF_COUNT_UNIQUE` ⟹ uncovered ⟹ not the popped - // (covered) front slot `target_idx`, so the pop axiom preserves it. - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -3845,7 +2941,6 @@ proof fn lemma_step_segment_next<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segme } #[verifier::spinoff_prover] -#[verifier::rlimit(200)] proof fn lemma_step_segment_clone_range<'rcu>( tracked s: &mut VmStore<'rcu>, sid: SegmentId, @@ -3853,7 +2948,7 @@ proof fn lemma_step_segment_clone_range<'rcu>( ) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), sub_range.start % PAGE_SIZE == 0, sub_range.end % PAGE_SIZE == 0, old(s).segments[sid].range.start <= sub_range.start, @@ -3867,38 +2962,20 @@ proof fn lemma_step_segment_clone_range<'rcu>( ensures final(s).inv(), { - hide(VmStore::inv); - hide(VmStore::structural_inv); - hide(VmStore::accounting_inv); - assert(s.structural_inv()) by { - reveal(VmStore::inv); - }; - assert(s.accounting_inv()) by { - reveal(VmStore::inv); - }; - assert(s.regions.inv()) by { - reveal(VmStore::structural_inv); - }; + // Keep the opaque pre-state invariants pointwise throughout this proof. + let ghost s_before = *s; + assert(s.regions.inv()); let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; let ghost sid_range = s.segments[sid].range; let ghost new_entry_ghost = SegmentEntry { range: sub_range }; - // `sub_range ⊆ sid`'s range, and `sid`'s range is in-bound, so the - // new entry's range is well-formed (aligned / non-empty / bound). assert(sid_range.end <= MAX_PADDR) by { reveal(VmStore::structural_inv); }; assert(sub_range.end <= MAX_PADDR); - // Derive the clone axiom's per-frame preconditions from `s.inv()`: - // every paddr in `sub_range` is covered by `sid`, hence - // `usage == Frame` (structural covered⟹Frame) and `rc >= 1` - // (accounting active head: `cover_count >= 1`). The non-saturation - // `rc + 1 <= REF_COUNT_MAX` (i.e. `rc < REF_COUNT_MAX`, matching the - // exec `inc_frame_ref_count` saturation guard) comes from this fn's - // `requires`. assert forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (sub_range.start <= paddr < sub_range.end && paddr % PAGE_SIZE == 0) implies { @@ -3907,11 +2984,13 @@ proof fn lemma_step_segment_clone_range<'rcu>( &&& so.ref_count() >= 1 &&& so.ref_count() + 1 <= REF_COUNT_MAX } by { - reveal(VmStore::structural_inv); - reveal(VmStore::accounting_inv); // `paddr` is covered by `sid` (sub_range ⊆ sid's range). - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(sid_range.start <= paddr < sid_range.end); + lemma_structural_inv_segment(s_before, sid, paddr); + s_before.regions.lemma_contains_valid_frame_paddr(paddr); + let idx = frame_to_index(paddr); + lemma_accounting_inv_at(s_before, idx); lemma_segment_cover_contains(old_segments, sid, paddr); assert(segment_cover_count(old_segments, paddr) >= 1); // Active head (cover > 0) ⟹ accounting equation gives rc >= cover >= 1. @@ -3950,7 +3029,6 @@ proof fn lemma_step_segment_clone_range<'rcu>( assert forall|idx: int| 0 <= idx < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].usage == old_regions.slot_owners[idx].usage by { - reveal(VmStore::structural_inv); let aligned = index_to_frame(idx); assert(aligned == (idx * PAGE_SIZE) as usize); assert(frame_to_index(aligned) == idx); @@ -3965,7 +3043,9 @@ proof fn lemma_step_segment_clone_range<'rcu>( 0 <= idx < max_meta_slots() implies #[trigger] s.regions.slot_owners[idx].in_list_perm.value() == 0 by { - reveal(VmStore::structural_inv); + assert(old_regions.slot_owners[idx].in_list_perm.value() == 0) by { + reveal(VmStore::structural_inv); + }; let aligned = index_to_frame(idx); assert(aligned == (idx * PAGE_SIZE) as usize); assert(frame_to_index(aligned) == idx); @@ -3979,45 +3059,41 @@ proof fn lemma_step_segment_clone_range<'rcu>( // --- structural: segment-covered ⟹ Frame-usage --- assert forall|sid_other: SegmentId, paddr_c: Paddr| #![trigger - s.segments.dom().contains(sid_other), + s.segments.contains_key(sid_other), frame_to_index(paddr_c)] - s.segments.dom().contains(sid_other) && s.segments[sid_other].range.start <= paddr_c + s.segments.contains_key(sid_other) && s.segments[sid_other].range.start <= paddr_c < s.segments[sid_other].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner(paddr_c).usage is Frame by { - reveal(VmStore::structural_inv); let cov_idx = frame_to_index(paddr_c); if sid_other == sid2 { // Covered by the new entry ⟹ in sub_range ⊆ sid's range. assert(s.segments[sid2].range == sub_range); - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(sid_range.start <= paddr_c < sid_range.end); - assert(old_regions.slot_owners[cov_idx].usage is Frame); + lemma_structural_inv_segment(s_before, sid, paddr_c); } else { - assert(old_segments.dom().contains(sid_other)); + assert(old_segments.contains_key(sid_other)); assert(old_segments[sid_other] == s.segments[sid_other]); - assert(old_regions.slot_owners[cov_idx].usage is Frame); + lemma_structural_inv_segment(s_before, sid_other, paddr_c); } // `cov_0 <= idx < max_meta_slots()` via `lemma_contains_valid_frame_paddr` // (`slot_owners.contains_key`) + `MetaRegionOwners::inv`'s // biimplication. Then the universal usage-preservation above // gives `s.regions` usage == old usage == Frame at cov_idx. + assert(valid_frame_paddr(paddr_c)); s.regions.lemma_contains_valid_frame_paddr(paddr_c); assert(s.regions.contains(cov_idx)); }; // --- structural: FrameId ⟹ Frame-usage (frames unchanged) --- assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { - reveal(VmStore::structural_inv); let other_idx = frame_to_index(s.frames[fid_other].paddr); - assert(old_frames.dom().contains(fid_other)); - assert(old_regions.slot_owners[other_idx].usage is Frame); + lemma_structural_inv_frame(s_before, fid_other); s.regions.lemma_contains_valid_frame_paddr(s.frames[fid_other].paddr); assert(s.regions.contains(other_idx)); - // `other_0 <= idx < max_meta_slots()` (biimplication) ⟹ universal - // usage-preservation above gives Frame-usage at other_idx. }; // --- accounting clause 1: UNUSED ⟹ no users --- @@ -4029,7 +3105,7 @@ proof fn lemma_step_segment_clone_range<'rcu>( s.segments, index_to_frame(idx), ) == 0 by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); let aligned = index_to_frame(idx); assert(aligned == (idx * PAGE_SIZE) as usize); assert(frame_to_index(aligned) == idx); @@ -4052,7 +3128,7 @@ proof fn lemma_step_segment_clone_range<'rcu>( s.segments, index_to_frame(idx), ) > 0 by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); let aligned = index_to_frame(idx); assert(aligned == (idx * PAGE_SIZE) as usize); assert(frame_to_index(aligned) == idx); @@ -4081,7 +3157,7 @@ proof fn lemma_step_segment_clone_range<'rcu>( index_to_frame(idx), ) } by { - reveal(VmStore::accounting_inv); + lemma_accounting_inv_at(s_before, idx); let aligned = index_to_frame(idx); assert(aligned == (idx * PAGE_SIZE) as usize); assert(frame_to_index(aligned) == idx); @@ -4089,8 +3165,6 @@ proof fn lemma_step_segment_clone_range<'rcu>( assert(s.regions.slots.contains_key(idx)); assert(s.regions.slot_owners[idx].inv()); if sub_range.start <= aligned < sub_range.end { - // covered: rc += 1, cover += 1, H & P preserved. Pre was an - // active head (cover_pre >= 1), so the old equation applies. assert(0 < s.regions.slot_owners[idx].ref_count() <= REF_COUNT_MAX); } else { assert(s.regions.slot_owners[idx] == old_regions.slot_owners[idx]); @@ -4103,25 +3177,23 @@ proof fn lemma_step_segment_clone_range<'rcu>( )); assert(0 < rc <= REF_COUNT_MAX); }; - // Discharge the structural unique-entry validity clause. A UNIQUE - // slot is `rc == REF_COUNT_UNIQUE` ⟹ uncovered (accounting: - // `cover_count > 0 ⟹ rc != UNIQUE`) ⟹ not in `sub_range` (⊆ `sid`'s - // range), so `segment_clone_embedded` preserves it fully. - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE &&& so.in_list_perm.value() == 0 &&& so.paths_in_pt.is_empty() } by { - reveal(VmStore::structural_inv); - reveal(VmStore::accounting_inv); let u_paddr = s.unique_frames[u].paddr; let u_idx = frame_to_index(u_paddr); - assert(old(s).unique_frames.dom().contains(u)); + assert(s_before.unique_frames.contains_key(u)); + assert(valid_frame_paddr(u_paddr) && old_regions.slot_owners[u_idx].ref_count() + == REF_COUNT_UNIQUE && old_regions.slot_owners[u_idx].usage is Frame + && old_regions.slot_owners[u_idx].in_list_perm.value() == 0 + && old_regions.slot_owners[u_idx].paths_in_pt.is_empty()) by { + reveal(VmStore::structural_inv); + }; s.regions.lemma_contains_valid_frame_paddr(u_paddr); - assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); - assert(old_regions.slot_owners[u_idx].usage is Frame); assert(!(sub_range.start <= u_paddr < sub_range.end)) by { if sub_range.start <= u_paddr < sub_range.end { // u_paddr ∈ sub_range ⊆ sid_range ⟹ sid covers u_paddr. @@ -4135,18 +3207,13 @@ proof fn lemma_step_segment_clone_range<'rcu>( assert(s.structural_inv()) by { reveal(VmStore::structural_inv); }; - assert(s.inv()) by { - reveal(VmStore::inv); - }; } -/// `Op::SegmentClone` step. Produces a second handle covering the same -/// range as `sid` (a fresh `SegmentEntry` mirroring `sid`'s range, with -/// every covered frame's `rc` bumped by 1). +/// `Op::SegmentClone` step. proof fn lemma_step_segment_clone<'rcu>(tracked s: &mut VmStore<'rcu>, sid: SegmentId) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), forall|paddr: Paddr| #![trigger frame_to_index(paddr)] (old(s).segments[sid].range.start <= paddr < old(s).segments[sid].range.end && paddr @@ -4155,9 +3222,7 @@ proof fn lemma_step_segment_clone<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segm ensures final(s).inv(), { - // Clone is `lemma_step_segment_clone_range` over `sid`'s whole range. The - // range's well-formedness (aligned / non-empty / in-bound) comes - // from `structural_inv`'s per-segment range clause. + reveal(VmStore::structural_inv); let ghost r = s.segments[sid].range; assert(r.start % PAGE_SIZE == 0); assert(r.end % PAGE_SIZE == 0); @@ -4166,8 +3231,7 @@ proof fn lemma_step_segment_clone<'rcu>(tracked s: &mut VmStore<'rcu>, sid: Segm lemma_step_segment_clone_range(s, sid, r); } -/// `Op::SegmentSlice` step. Produces a handle covering `sub_range` -/// (⊆ `sid`'s range), bumping the `rc` of every frame inside it. +/// `Op::SegmentSlice` step. proof fn lemma_step_segment_slice<'rcu>( tracked s: &mut VmStore<'rcu>, sid: SegmentId, @@ -4175,7 +3239,7 @@ proof fn lemma_step_segment_slice<'rcu>( ) requires old(s).inv(), - old(s).segments.dom().contains(sid), + old(s).segments.contains_key(sid), sub_range.start % PAGE_SIZE == 0, sub_range.end % PAGE_SIZE == 0, old(s).segments[sid].range.start <= sub_range.start, @@ -4198,9 +3262,8 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd ensures final(s).inv(), { - // Exec `UniqueFrame::from_unused` returns `Err(GetFrameError)` and - // leaves the slot untouched unless the target is genuinely an unused - // frame slot; only the success branch mutates the store. + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); if valid_frame_paddr(paddr) && s.regions.slots.contains_key(frame_to_index(paddr)) && s.regions.slot_owner(paddr).usage is Unused && s.regions.slot_owner(paddr).ref_count() == REF_COUNT_UNUSED { @@ -4243,11 +3306,11 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd }; // --- structural: FrameId ⟹ Frame-usage --- assert forall|fid: FrameId| #[trigger] - s.frames.dom().contains(fid) implies s.regions.slot_owner( + s.frames.contains_key(fid) implies s.regions.slot_owner( s.frames[fid].paddr, ).usage is Frame by { let other_idx = frame_to_index(s.frames[fid].paddr); - assert(old_frames.dom().contains(fid)); + assert(old_frames.contains_key(fid)); assert(old_regions.slot_owners[other_idx].usage is Frame); if other_idx == idx { // Pre `idx` was `Unused`-usage — no `FrameEntry` maps there. @@ -4256,19 +3319,19 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd }; // --- structural: segment-covered ⟹ Frame-usage --- assert forall|sid: SegmentId, paddr_c: Paddr| - #![trigger s.segments.dom().contains(sid), frame_to_index(paddr_c)] - s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c + #![trigger s.segments.contains_key(sid), frame_to_index(paddr_c)] + s.segments.contains_key(sid) && s.segments[sid].range.start <= paddr_c < s.segments[sid].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner(paddr_c).usage is Frame by { let cov_idx = frame_to_index(paddr_c); - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_regions.slot_owners[cov_idx].usage is Frame); if cov_idx == idx { assert(false); } }; // --- structural: unique-entry validity --- - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -4280,7 +3343,7 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd assert(s.unique_frames[u].paddr == paddr); assert(u_idx == idx); } else { - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); assert(s.unique_frames[u] == old_unique[u]); assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); assert(u_idx != idx); @@ -4289,33 +3352,31 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd }; // --- structural: unique valid_frame_paddr --- assert forall|u: UniqueId| #[trigger] - s.unique_frames.dom().contains(u) implies valid_frame_paddr( - s.unique_frames[u].paddr, - ) by { + s.unique_frames.contains_key(u) implies valid_frame_paddr(s.unique_frames[u].paddr) by { if u != uid { - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); } }; // --- structural: unique injectivity --- assert forall|u1: UniqueId, u2: UniqueId| - #![trigger s.unique_frames.dom().contains(u1), s.unique_frames.dom().contains(u2)] - s.unique_frames.dom().contains(u1) && s.unique_frames.dom().contains(u2) + #![trigger s.unique_frames.contains_key(u1), s.unique_frames.contains_key(u2)] + s.unique_frames.contains_key(u1) && s.unique_frames.contains_key(u2) && s.unique_frames[u1].paddr == s.unique_frames[u2].paddr implies u1 == u2 by { if u1 == uid && u2 != uid { - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u2)); assert(s.unique_frames[u2].paddr == paddr); assert(frame_to_index(s.unique_frames[u2].paddr) == idx); assert(old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNIQUE); assert(false); } else if u2 == uid && u1 != uid { - assert(old_unique.dom().contains(u1)); + assert(old_unique.contains_key(u1)); assert(s.unique_frames[u1].paddr == paddr); assert(frame_to_index(s.unique_frames[u1].paddr) == idx); assert(old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNIQUE); assert(false); } else if u1 != uid && u2 != uid { - assert(old_unique.dom().contains(u1)); - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u1)); + assert(old_unique.contains_key(u2)); } }; @@ -4348,13 +3409,11 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd index_to_frame(i), ) > 0 by { if i == idx { - // post rc at `idx` is UNIQUE — antecedent false. assert(false); } else { assert(s.regions.slot_owners[i] == old_regions.slot_owners[i]); } }; - // --- accounting clause 3: the rc equation --- assert forall|i: int| #![trigger s.regions.slot_owners[i]] 0 <= i < max_meta_slots() && s.regions.slot_owners[i].usage is Frame && (handle_count( @@ -4393,10 +3452,12 @@ proof fn lemma_step_unique_from_unused<'rcu>(tracked s: &mut VmStore<'rcu>, padd proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: UniqueId) requires old(s).inv(), - old(s).unique_frames.dom().contains(uid), + old(s).unique_frames.contains_key(uid), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; @@ -4414,9 +3475,6 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique assert(s.regions.slot_owners[idx].in_list_perm.value() == 0); assert(s.regions.slot_owners[idx].paths_in_pt.is_empty()); - // Pre "no users" facts at the UNIQUE slot, *derived* from the - // equation clause: a user (H>0 / cover>0) at a `usage == Frame` slot - // forces `rc != REF_COUNT_UNIQUE`, contradicting the unique slot. assert(handle_count(old_frames, idx) == 0) by { if handle_count(old_frames, idx) > 0 { assert(old_regions.slot_owners[idx].ref_count() != REF_COUNT_UNIQUE); @@ -4430,14 +3488,12 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique } }; - // Remove the entry, then tear the slot down. let tracked _entry = s.tracked_extract_unique(uid); unique::unique_drop_embedded(&mut s.regions, paddr); assert(s.unique_frames =~= old_unique.remove(uid)); assert(s.frames == old_frames); assert(s.segments == old_segments); - // --- structural: in_list == 0 everywhere --- assert forall|i: int| 0 <= i < max_meta_slots() implies #[trigger] s.regions.slot_owners[i].in_list_perm.value() == 0 by { @@ -4445,34 +3501,31 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique assert(s.regions.slot_owners[i] == old_regions.slot_owners[i]); } }; - // --- structural: FrameId ⟹ Frame-usage (usage preserved at idx) --- - assert forall|fid: FrameId| #[trigger] - s.frames.dom().contains(fid) implies s.regions.slot_owner( + assert forall|fid: FrameId| #[trigger] s.frames.contains_key(fid) implies s.regions.slot_owner( s.frames[fid].paddr, ).usage is Frame by { let other_idx = frame_to_index(s.frames[fid].paddr); - assert(old_frames.dom().contains(fid)); + assert(old_frames.contains_key(fid)); assert(old_regions.slot_owners[other_idx].usage is Frame); if other_idx != idx { assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]); } }; - // --- structural: segment-covered ⟹ Frame-usage --- assert forall|sid: SegmentId, paddr_c: Paddr| - #![trigger s.segments.dom().contains(sid), frame_to_index(paddr_c)] - s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c + #![trigger s.segments.contains_key(sid), frame_to_index(paddr_c)] + s.segments.contains_key(sid) && s.segments[sid].range.start <= paddr_c < s.segments[sid].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner( paddr_c, ).usage is Frame by { let cov_idx = frame_to_index(paddr_c); - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_regions.slot_owners[cov_idx].usage is Frame); if cov_idx != idx { assert(s.regions.slot_owners[cov_idx] == old_regions.slot_owners[cov_idx]); } }; // --- structural: unique-entry validity (remaining entries) --- - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -4480,7 +3533,7 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique &&& so.paths_in_pt.is_empty() } by { let u_idx = frame_to_index(s.unique_frames[u].paddr); - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); assert(u != uid); // Injectivity (old): only `uid` sat at `paddr`/`idx`, so u_idx != idx. if u_idx == idx { @@ -4493,16 +3546,17 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique assert(s.regions.slot_owners[u_idx] == old_regions.slot_owners[u_idx]); }; // --- structural: unique valid_frame_paddr / injectivity (subset of old) --- - assert forall|u: UniqueId| #[trigger] - s.unique_frames.dom().contains(u) implies valid_frame_paddr(s.unique_frames[u].paddr) by { - assert(old_unique.dom().contains(u)); + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies valid_frame_paddr( + s.unique_frames[u].paddr, + ) by { + assert(old_unique.contains_key(u)); }; assert forall|u1: UniqueId, u2: UniqueId| - #![trigger s.unique_frames.dom().contains(u1), s.unique_frames.dom().contains(u2)] - s.unique_frames.dom().contains(u1) && s.unique_frames.dom().contains(u2) + #![trigger s.unique_frames.contains_key(u1), s.unique_frames.contains_key(u2)] + s.unique_frames.contains_key(u1) && s.unique_frames.contains_key(u2) && s.unique_frames[u1].paddr == s.unique_frames[u2].paddr implies u1 == u2 by { - assert(old_unique.dom().contains(u1)); - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u1)); + assert(old_unique.contains_key(u2)); }; // --- accounting clause 1: UNUSED ⟹ no users --- @@ -4573,18 +3627,16 @@ proof fn lemma_step_unique_drop<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique }; } -/// `Op::FromUnique` step. Converts the exclusive handle `uid` to a -/// shared one: `rc` drops `UNIQUE → 1`, the `UniqueEntry` is consumed, -/// and a fresh `FrameEntry` registered (`H: 0 → 1`). The slot becomes a -/// SHARED active head with `rc == 1 == H + P + cover` (`P == cover == 0` -/// derived from the pre-UNIQUE no-users facts). +/// `Op::FromUnique` step. proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: UniqueId) requires old(s).inv(), - old(s).unique_frames.dom().contains(uid), + old(s).unique_frames.contains_key(uid), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost old_regions = s.regions; let ghost old_frames = s.frames; let ghost old_segments = s.segments; @@ -4638,7 +3690,7 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique }; // --- structural: FrameId ⟹ Frame-usage --- assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { let other_idx = frame_to_index(s.frames[fid_other].paddr); @@ -4646,7 +3698,7 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique assert(s.frames[fid_other].paddr == paddr); assert(other_idx == idx); } else { - assert(old_frames.dom().contains(fid_other)); + assert(old_frames.contains_key(fid_other)); assert(s.frames[fid_other] == old_frames[fid_other]); assert(old_regions.slot_owners[other_idx].usage is Frame); if other_idx != idx { @@ -4656,20 +3708,20 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique }; // --- structural: segment-covered ⟹ Frame-usage --- assert forall|sid: SegmentId, paddr_c: Paddr| - #![trigger s.segments.dom().contains(sid), frame_to_index(paddr_c)] - s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c + #![trigger s.segments.contains_key(sid), frame_to_index(paddr_c)] + s.segments.contains_key(sid) && s.segments[sid].range.start <= paddr_c < s.segments[sid].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner( paddr_c, ).usage is Frame by { let cov_idx = frame_to_index(paddr_c); - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_regions.slot_owners[cov_idx].usage is Frame); if cov_idx != idx { assert(s.regions.slot_owners[cov_idx] == old_regions.slot_owners[cov_idx]); } }; // --- structural: unique-entry validity (remaining entries) --- - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -4677,7 +3729,7 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique &&& so.paths_in_pt.is_empty() } by { let u_idx = frame_to_index(s.unique_frames[u].paddr); - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); assert(u != uid); if u_idx == idx { assert(old_unique[u].paddr == s.unique_frames[u].paddr); @@ -4686,16 +3738,17 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique } assert(s.regions.slot_owners[u_idx] == old_regions.slot_owners[u_idx]); }; - assert forall|u: UniqueId| #[trigger] - s.unique_frames.dom().contains(u) implies valid_frame_paddr(s.unique_frames[u].paddr) by { - assert(old_unique.dom().contains(u)); + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies valid_frame_paddr( + s.unique_frames[u].paddr, + ) by { + assert(old_unique.contains_key(u)); }; assert forall|u1: UniqueId, u2: UniqueId| - #![trigger s.unique_frames.dom().contains(u1), s.unique_frames.dom().contains(u2)] - s.unique_frames.dom().contains(u1) && s.unique_frames.dom().contains(u2) + #![trigger s.unique_frames.contains_key(u1), s.unique_frames.contains_key(u2)] + s.unique_frames.contains_key(u1) && s.unique_frames.contains_key(u2) && s.unique_frames[u1].paddr == s.unique_frames[u2].paddr implies u1 == u2 by { - assert(old_unique.dom().contains(u1)); - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u1)); + assert(old_unique.contains_key(u2)); }; // --- accounting clause 1: UNUSED ⟹ no users --- @@ -4774,10 +3827,12 @@ proof fn lemma_step_from_unique<'rcu>(tracked s: &mut VmStore<'rcu>, uid: Unique proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: FrameId) requires old(s).inv(), - old(s).frames.dom().contains(fid), + old(s).frames.contains_key(fid), ensures final(s).inv(), { + reveal(VmStore::structural_inv); + reveal(VmStore::accounting_inv); let ghost paddr = s.frames[fid].paddr; let ghost idx = frame_to_index(paddr); // `fid` registered ⟹ in-bound, `usage == Frame`, and it contributes @@ -4834,11 +3889,11 @@ proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: Fr // The converted slot keeps `usage == Frame`; all other slots are // unchanged. (No remaining `FrameEntry` sits at `idx`: `H == 0`.) assert forall|fid_other: FrameId| #[trigger] - s.frames.dom().contains(fid_other) implies s.regions.slot_owner( + s.frames.contains_key(fid_other) implies s.regions.slot_owner( s.frames[fid_other].paddr, ).usage is Frame by { let other_idx = frame_to_index(s.frames[fid_other].paddr); - assert(old_frames.dom().contains(fid_other)); + assert(old_frames.contains_key(fid_other)); assert(old_regions.slot_owners[other_idx].usage is Frame); if other_idx != idx { assert(s.regions.slot_owners[other_idx] == old_regions.slot_owners[other_idx]); @@ -4846,19 +3901,19 @@ proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: Fr }; // --- structural: segment-covered ⟹ Frame-usage --- assert forall|sid: SegmentId, paddr_c: Paddr| - #![trigger s.segments.dom().contains(sid), frame_to_index(paddr_c)] - s.segments.dom().contains(sid) && s.segments[sid].range.start <= paddr_c + #![trigger s.segments.contains_key(sid), frame_to_index(paddr_c)] + s.segments.contains_key(sid) && s.segments[sid].range.start <= paddr_c < s.segments[sid].range.end && paddr_c % PAGE_SIZE == 0 implies s.regions.slot_owner(paddr_c).usage is Frame by { let cov_idx = frame_to_index(paddr_c); - assert(old_segments.dom().contains(sid)); + assert(old_segments.contains_key(sid)); assert(old_regions.slot_owners[cov_idx].usage is Frame); if cov_idx != idx { assert(s.regions.slot_owners[cov_idx] == old_regions.slot_owners[cov_idx]); } }; // --- structural: unique-entry validity --- - assert forall|u: UniqueId| #[trigger] s.unique_frames.dom().contains(u) implies { + assert forall|u: UniqueId| #[trigger] s.unique_frames.contains_key(u) implies { let so = s.regions.slot_owner(s.unique_frames[u].paddr); &&& so.usage is Frame &&& so.ref_count() == REF_COUNT_UNIQUE @@ -4870,7 +3925,7 @@ proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: Fr assert(s.unique_frames[u].paddr == paddr); assert(u_idx == idx); } else { - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); assert(s.unique_frames[u] == old_unique[u]); // old entry's slot was UNIQUE (≠ idx, which was rc==1). assert(old_regions.slot_owners[u_idx].ref_count() == REF_COUNT_UNIQUE); @@ -4879,32 +3934,30 @@ proof fn lemma_step_try_from_shared<'rcu>(tracked s: &mut VmStore<'rcu>, fid: Fr } }; assert forall|u: UniqueId| #[trigger] - s.unique_frames.dom().contains(u) implies valid_frame_paddr( - s.unique_frames[u].paddr, - ) by { + s.unique_frames.contains_key(u) implies valid_frame_paddr(s.unique_frames[u].paddr) by { if u != uid { - assert(old_unique.dom().contains(u)); + assert(old_unique.contains_key(u)); } }; assert forall|u1: UniqueId, u2: UniqueId| - #![trigger s.unique_frames.dom().contains(u1), s.unique_frames.dom().contains(u2)] - s.unique_frames.dom().contains(u1) && s.unique_frames.dom().contains(u2) + #![trigger s.unique_frames.contains_key(u1), s.unique_frames.contains_key(u2)] + s.unique_frames.contains_key(u1) && s.unique_frames.contains_key(u2) && s.unique_frames[u1].paddr == s.unique_frames[u2].paddr implies u1 == u2 by { if u1 == uid && u2 != uid { - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u2)); assert(s.unique_frames[u2].paddr == paddr); assert(frame_to_index(s.unique_frames[u2].paddr) == idx); assert(old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNIQUE); assert(false); } else if u2 == uid && u1 != uid { - assert(old_unique.dom().contains(u1)); + assert(old_unique.contains_key(u1)); assert(s.unique_frames[u1].paddr == paddr); assert(frame_to_index(s.unique_frames[u1].paddr) == idx); assert(old_regions.slot_owners[idx].ref_count() == REF_COUNT_UNIQUE); assert(false); } else if u1 != uid && u2 != uid { - assert(old_unique.dom().contains(u1)); - assert(old_unique.dom().contains(u2)); + assert(old_unique.contains_key(u1)); + assert(old_unique.contains_key(u2)); } }; @@ -4987,7 +4040,7 @@ pub proof fn lemma_segment_cover_insert_inside( paddr: Paddr, ) requires - !segments.dom().contains(sid), + !segments.contains_key(sid), entry.range.start <= paddr < entry.range.end, ensures segment_cover_count(segments.insert(sid, entry), paddr) == segment_cover_count( @@ -5031,7 +4084,7 @@ pub proof fn lemma_segment_cover_insert_outside( paddr: Paddr, ) requires - !segments.dom().contains(sid), + !segments.contains_key(sid), !(entry.range.start <= paddr < entry.range.end), ensures segment_cover_count(segments.insert(sid, entry), paddr) == segment_cover_count( @@ -5073,7 +4126,7 @@ pub proof fn lemma_segment_cover_contains( paddr: Paddr, ) requires - segments.dom().contains(sid), + segments.contains_key(sid), segments[sid].range.start <= paddr < segments[sid].range.end, ensures segment_cover_count(segments, paddr) >= 1, @@ -5092,7 +4145,7 @@ pub proof fn lemma_segment_cover_remove_inside( paddr: Paddr, ) requires - segments.dom().contains(sid), + segments.contains_key(sid), segments[sid].range.start <= paddr < segments[sid].range.end, ensures segment_cover_count(segments.remove(sid), paddr) == (segment_cover_count(segments, paddr) @@ -5137,7 +4190,7 @@ pub proof fn lemma_segment_cover_shrink_front( paddr_check: Paddr, ) requires - segments.dom().contains(sid), + segments.contains_key(sid), // Original segment is non-empty (the caller guarantees this // from structural_inv). segments[sid].range.start < segments[sid].range.end, @@ -5210,9 +4263,6 @@ pub proof fn lemma_segment_cover_shrink_front( paddr_check, )); } else if sid_pre_covers { - // paddr_check ∈ [popped, range.end), paddr_check != popped, - // paddr_check page-aligned + popped page-aligned ⟹ - // paddr_check >= popped + PAGE_SIZE ⟹ in new_entry.range. assert(new_covers); lemma_segment_cover_contains(segments, sid, paddr_check); lemma_segment_cover_insert_inside(segments.remove(sid), sid, new_entry, paddr_check); @@ -5221,7 +4271,6 @@ pub proof fn lemma_segment_cover_shrink_front( paddr_check, )); } else { - // !sid_pre_covers ⟹ !new_covers (new_range ⊆ pre range). assert(!new_covers); lemma_segment_cover_insert_outside(segments.remove(sid), sid, new_entry, paddr_check); assert(segment_cover_count(new_segments, paddr_check) == segment_cover_count( @@ -5230,7 +4279,6 @@ pub proof fn lemma_segment_cover_shrink_front( )); } } else { - // new range empty; segments is just remove(sid). let new_segments = segments.remove(sid); if paddr_check == popped { assert(sid_pre_covers); @@ -5240,12 +4288,8 @@ pub proof fn lemma_segment_cover_shrink_front( paddr_check, )); } else if sid_pre_covers { - // popped + PAGE_SIZE == range.end (empty new range). - // paddr_check in [popped, range.end), paddr_check != popped, - // page-aligned ⟹ paddr_check >= range.end. Contradiction. assert(false); } else { - // cover_post == cover_pre. assert(segment_cover_count(new_segments, paddr_check) == segment_cover_count( segments, paddr_check, @@ -5269,14 +4313,14 @@ pub proof fn lemma_segment_cover_split( paddr: Paddr, ) requires - segments.dom().contains(sid), + segments.contains_key(sid), // `new_left` and `new_right` are fresh and distinct from each // other and from `sid`. new_left != sid, new_right != sid, new_left != new_right, - !segments.remove(sid).dom().contains(new_left), - !segments.remove(sid).dom().contains(new_right), + !segments.remove(sid).contains_key(new_left), + !segments.remove(sid).contains_key(new_right), // The two halves partition `sid`'s range at `mid`. entry_left.range.start == segments[sid].range.start, entry_left.range.end == entry_right.range.start, @@ -5292,7 +4336,7 @@ pub proof fn lemma_segment_cover_split( let mid_segments = segments.remove(sid); let with_left = mid_segments.insert(new_left, entry_left); assert(with_left.dom() == mid_segments.dom().insert(new_left)); - assert(!with_left.dom().contains(new_right)); + assert(!with_left.contains_key(new_right)); let sid_covers = segments[sid].range.start <= paddr && paddr < segments[sid].range.end; let left_covers = entry_left.range.start <= paddr && paddr < entry_left.range.end; let right_covers = entry_right.range.start <= paddr && paddr < entry_right.range.end; @@ -5361,7 +4405,7 @@ pub proof fn lemma_segment_cover_remove_outside( paddr: Paddr, ) requires - segments.dom().contains(sid), + segments.contains_key(sid), !(segments[sid].range.start <= paddr < segments[sid].range.end), ensures segment_cover_count(segments.remove(sid), paddr) == segment_cover_count(segments, paddr), @@ -5395,48 +4439,48 @@ pub proof fn lemma_segment_cover_remove_outside( /// Picks an id not currently in `m.dom()`. Since the key type is `int`, /// an unused id always exists. pub open spec fn fresh_vm_space_id<'a>(m: Map) -> VmSpaceId { - choose|id: VmSpaceId| !m.dom().contains(id) + choose|id: VmSpaceId| !m.contains_key(id) } /// Picks a cursor id not currently in `m.dom()`. pub open spec fn fresh_cursor_id<'rcu>(m: Map>) -> CursorId { - choose|id: CursorId| !m.dom().contains(id) + choose|id: CursorId| !m.contains_key(id) } /// Picks a [`VmIoId`] not currently in `m.dom()`. pub open spec fn fresh_vm_io_id<'a>(m: Map) -> VmIoId { - choose|id: VmIoId| !m.dom().contains(id) + choose|id: VmIoId| !m.contains_key(id) } /// Picks a [`FrameId`] not currently in `m.dom()`. pub open spec fn fresh_frame_id(m: Map) -> FrameId { - choose|id: FrameId| !m.dom().contains(id) + choose|id: FrameId| !m.contains_key(id) } pub proof fn lemma_fresh_vm_space_id_not_in_dom<'a>(m: Map) ensures - !m.dom().contains(fresh_vm_space_id(m)), + !m.contains_key(fresh_vm_space_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } pub proof fn lemma_fresh_cursor_id_not_in_dom<'rcu>(m: Map>) ensures - !m.dom().contains(fresh_cursor_id(m)), + !m.contains_key(fresh_cursor_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } pub proof fn lemma_fresh_vm_io_id_not_in_dom<'a>(m: Map) ensures - !m.dom().contains(fresh_vm_io_id(m)), + !m.contains_key(fresh_vm_io_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } pub proof fn lemma_fresh_frame_id_not_in_dom(m: Map) ensures - !m.dom().contains(fresh_frame_id(m)), + !m.contains_key(fresh_frame_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } @@ -5495,12 +4539,12 @@ pub proof fn tracked_segment_entry_new(range: Range) -> tracked SegmentEn /// Fresh-id helper for the segment id space. pub open spec fn fresh_segment_id(m: Map) -> SegmentId { - choose|id: SegmentId| !m.dom().contains(id) + choose|id: SegmentId| !m.contains_key(id) } pub proof fn lemma_fresh_segment_id_not_in_dom(m: Map) ensures - !m.dom().contains(fresh_segment_id(m)), + !m.contains_key(fresh_segment_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } @@ -5516,12 +4560,12 @@ pub proof fn tracked_unique_entry_new(paddr: Paddr) -> tracked UniqueEntry /// Picks a [`UniqueId`] not currently in `m.dom()`. pub open spec fn fresh_unique_id(m: Map) -> UniqueId { - choose|id: UniqueId| !m.dom().contains(id) + choose|id: UniqueId| !m.contains_key(id) } pub proof fn lemma_fresh_unique_id_not_in_dom(m: Map) ensures - !m.dom().contains(fresh_unique_id(m)), + !m.contains_key(fresh_unique_id(m)), { lemma_finite_int_set_has_unused(m.dom()); } diff --git a/ostd/specs/mm/frame/frame_specs.rs b/ostd/specs/mm/frame/frame_specs.rs index 398dbed9f..36dc31ad7 100644 --- a/ostd/specs/mm/frame/frame_specs.rs +++ b/ostd/specs/mm/frame/frame_specs.rs @@ -1,6 +1,9 @@ +use core::marker::PhantomData; + use vstd::cell::CellId; -use vstd::{prelude::*, simple_pptr}; +use vstd::prelude::*; +use vstd::simple_pptr::{self, PPtr, PointsTo}; use vstd_extra::{cast_ptr::*, ownership::*}; use crate::specs::{ @@ -40,7 +43,7 @@ impl Frame { } /// Accessor for the [`MetaSlot`] permission tracked by this `Frame` handle. - pub open spec fn slot_perm(self) -> simple_pptr::PointsTo { + pub open spec fn slot_perm(self) -> PointsTo { *self.tracked_slot_perm@ } @@ -134,4 +137,21 @@ impl Frame { } } +impl Frame { + pub open spec fn from_raw_spec( + paddr: Paddr, + slot_perm: &'static PointsTo, + metadata_perm: Option, + ) -> Self { + Frame:: { + ptr: PPtr::(frame_to_meta(paddr), PhantomData), + _marker: PhantomData, + #[cfg(verus_keep_ghost_body)] + tracked_slot_perm: Tracked(slot_perm), + #[cfg(verus_keep_ghost_body)] + tracked_metadata_perm: Tracked(metadata_perm), + } + } +} + } // verus! diff --git a/ostd/specs/mm/frame/meta_region_owners.rs b/ostd/specs/mm/frame/meta_region_owners.rs index eb5da1d9b..93f00cff0 100644 --- a/ostd/specs/mm/frame/meta_region_owners.rs +++ b/ostd/specs/mm/frame/meta_region_owners.rs @@ -149,6 +149,25 @@ impl MetaRegionOwners { self.lemma_contains_valid_frame_paddr(paddr); self.slot_owners.tracked_borrow_mut(frame_to_index(paddr)) } + + /// The metadata region transition of claiming a currently shared slot. + pub open spec fn inc_frame_reference_region_spec(self, paddr: Paddr, post: Self) -> bool { + let idx = frame_to_index(paddr); + let pre_owner = self.slot_owners[idx]; + let post_owner = post.slot_owners[idx]; + { + &&& post.ref_count(idx) == self.ref_count(idx) + 1 + &&& post_owner.ref_count_perm.id() == pre_owner.ref_count_perm.id() + &&& post_owner.metadata_perm.id() == pre_owner.metadata_perm.id() + &&& post_owner.metadata_perm.frac() + 1 == pre_owner.metadata_perm.frac() + &&& post_owner.metadata_perm@ == pre_owner.metadata_perm@ + &&& post_owner.in_list_perm == pre_owner.in_list_perm + &&& post_owner.slot_vaddr == pre_owner.slot_vaddr + &&& post_owner.usage == pre_owner.usage + &&& post_owner.paths_in_pt == pre_owner.paths_in_pt + &&& post =~= self.insert_slot_owner(paddr, post_owner) + } + } } } // verus! diff --git a/ostd/specs/mm/frame/meta_specs.rs b/ostd/specs/mm/frame/meta_specs.rs index d4c151b45..667f8b4b8 100644 --- a/ostd/specs/mm/frame/meta_specs.rs +++ b/ostd/specs/mm/frame/meta_specs.rs @@ -1,16 +1,12 @@ -use core::marker::PhantomData; - use vstd::prelude::*; -use vstd::{ - atomic::*, - simple_pptr::{self, PPtr}, -}; +use vstd::{atomic::*, simple_pptr::PointsTo}; use vstd_extra::{cast_ptr::*, ownership::*, sum::Sum}; use crate::specs::{ arch::*, mm::frame::{ + frame_specs::*, mapping::{frame_to_index, index_to_meta}, meta_region_owners::MetaRegionOwners, }, @@ -40,7 +36,7 @@ impl MetaSlot { /// The relation between the [`MetaSlot`] permission and a metadata permission fraction. #[verifier::inline] pub open spec fn perms_related( - slot_perm: vstd::simple_pptr::PointsTo, + slot_perm: PointsTo, metadata_perm: MetadataPerm, ) -> bool { &&& metadata_perm.storage_perm.is_init() @@ -163,34 +159,11 @@ impl MetaSlot { rc_perm.value() >= REF_COUNT_MAX } - pub open spec fn frame_paddr_safety_cond(perm: vstd::simple_pptr::PointsTo) -> bool { + pub open spec fn frame_paddr_safety_cond(perm: PointsTo) -> bool { &&& FRAME_METADATA_RANGE.start <= perm.addr() < FRAME_METADATA_RANGE.end &&& perm.addr() % META_SLOT_SIZE == 0 } - /// The metadata region transition of claiming a currently shared slot. - pub open spec fn inc_frame_reference_region_spec( - paddr: Paddr, - pre: MetaRegionOwners, - post: MetaRegionOwners, - ) -> bool { - let idx = frame_to_index(paddr); - let pre_owner = pre.slot_owners[idx]; - let post_owner = post.slot_owners[idx]; - { - &&& post.ref_count(idx) == pre.ref_count(idx) + 1 - &&& post_owner.ref_count_perm.id() == pre_owner.ref_count_perm.id() - &&& post_owner.metadata_perm.id() == pre_owner.metadata_perm.id() - &&& post_owner.metadata_perm.frac() + 1 == pre_owner.metadata_perm.frac() - &&& post_owner.metadata_perm@ == pre_owner.metadata_perm@ - &&& post_owner.in_list_perm == pre_owner.in_list_perm - &&& post_owner.slot_vaddr == pre_owner.slot_vaddr - &&& post_owner.usage == pre_owner.usage - &&& post_owner.paths_in_pt == pre_owner.paths_in_pt - &&& post =~= pre.insert_slot_owner(paddr, post_owner) - } - } - pub open spec fn get_from_in_use_success_spec( paddr: Paddr, pre: MetaRegionOwners, @@ -199,7 +172,7 @@ impl MetaSlot { ) -> bool { let idx = frame_to_index(paddr); { - &&& Self::inc_frame_reference_region_spec(paddr, pre, post) + &&& pre.inc_frame_reference_region_spec(paddr, post) &&& metadata_perm.frac() == 1 &&& metadata_perm.id() == post.slot_owners[idx].metadata_perm.id() &&& Self::perms_related(*post.slots[idx], metadata_perm.resource()) @@ -216,20 +189,4 @@ impl MetaSlot { } } -impl + OwnerOf> Frame { - pub open spec fn from_raw_spec( - paddr: Paddr, - slot_perm: &'static vstd::simple_pptr::PointsTo, - ) -> Self { - Frame:: { - ptr: PPtr::(frame_to_meta(paddr), PhantomData), - _marker: PhantomData, - #[cfg(verus_keep_ghost_body)] - tracked_slot_perm: Tracked(slot_perm), - #[cfg(verus_keep_ghost_body)] - tracked_metadata_perm: Tracked(None), - } - } -} - } // verus! diff --git a/ostd/specs/mm/frame/unique.rs b/ostd/specs/mm/frame/unique.rs index 133aedae7..84a6c8cd8 100644 --- a/ostd/specs/mm/frame/unique.rs +++ b/ostd/specs/mm/frame/unique.rs @@ -133,13 +133,6 @@ impl + OwnerOf> UniqueFrameOwner { ) } - /// Borrow-model global invariant: the frame's permission is parked in - /// `regions.slots[slot_index]` (NOT owned by the frame), and the - /// concrete storage and representation permissions decode to metadata - /// matching `meta_own`. A `UniqueFrame` is the sole live reference to its - /// slot, so the slot sits at `REF_COUNT_UNIQUE` — the unique-frame analog - /// of the segment's `0 < ref_count <= REF_COUNT_MAX` regime in - /// [`Segment::relate_regions`]. pub open spec fn global_inv(self, regions: MetaRegionOwners) -> bool { &&& regions.contains(self.slot_index) &&& self.meta_wf(regions) @@ -175,11 +168,11 @@ impl + OwnerOf> UniqueFrameOwner { Self { meta_own, repr_perm: Some(repr_perm), slot_index } } - pub proof fn tracked_borrow_repr_perm(tracked &self) -> (tracked res: &M::ReprPerm) + pub proof fn tracked_borrow_repr_perm(tracked &self) -> tracked &M::ReprPerm requires self.repr_perm is Some, - ensures - *res == self.repr_perm->0, + returns + self.repr_perm->0, { self.repr_perm.tracked_borrow() } diff --git a/ostd/specs/mm/page_table/cursor/owners.rs b/ostd/specs/mm/page_table/cursor/owners.rs index b6b59c16e..58ed47a1b 100644 --- a/ostd/specs/mm/page_table/cursor/owners.rs +++ b/ostd/specs/mm/page_table/cursor/owners.rs @@ -1147,6 +1147,7 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { let entry = self.cur_entry_owner(); let idx = frame_to_index(pa); + self.cur_subtree_inv(); EntryOwner::::axiom_frame_is_tracked_iff_not_mmio(entry); assert(entry.inv_base()); C::lemma_clone_requires_concrete(item, pa, level, prop, regions); diff --git a/ostd/specs/mm/page_table/node/child.rs b/ostd/specs/mm/page_table/node/child.rs index 7f062f769..1b7331307 100644 --- a/ostd/specs/mm/page_table/node/child.rs +++ b/ostd/specs/mm/page_table/node/child.rs @@ -91,6 +91,7 @@ impl Child { PageTableNode::from_raw_spec( pte.paddr(), regions.slots[crate::specs::mm::frame::mapping::frame_to_index(pte.paddr())], + None, ), ) } @@ -105,6 +106,7 @@ impl Child { PageTableNode::from_raw_spec( paddr, regions.slots[crate::specs::mm::frame::mapping::frame_to_index(paddr)], + None, ), ) } diff --git a/ostd/specs/sync/mod.rs b/ostd/specs/sync/mod.rs index 5faf84536..fd2997b4c 100644 --- a/ostd/specs/sync/mod.rs +++ b/ostd/specs/sync/mod.rs @@ -1 +1,2 @@ //pub mod examples; +pub mod rcu; diff --git a/ostd/specs/sync/rcu/mod.rs b/ostd/specs/sync/rcu/mod.rs new file mode 100644 index 000000000..8cde51823 --- /dev/null +++ b/ostd/specs/sync/rcu/mod.rs @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Allocation registration for RCU proofs. +//! +//! # Verified Properties +//! +//! Each registration receives a fresh allocation ID within its domain. Persistent +//! block information preserves that ID across publications, including after the +//! physical address is reused. A separate linear permission belongs to the same +//! registration and will be consumed by the retirement protocol. +//! +//! Registration records identity only: neither block information nor a base +//! retire permission grants access to the allocation or permission to reclaim it. +//! The client must separately retain physical ownership and read leases. +use vstd::{ + prelude::*, + resource::{ + Loc, + map::{GhostMapAuth, GhostPersistentPointsTo, GhostPointsTo}, + }, +}; +use vstd_extra::ownership::Inv; + +#[cfg(feature = "irc11")] +pub mod root; + +verus! { + +/// Authoritative allocation registry for one RCU protection domain. +pub tracked struct RcuDomainAuth { + objects: GhostMapAuth, + retire_perms: GhostMapAuth, + ghost next_obj: nat, +} + +impl Inv for RcuDomainAuth { + closed spec fn inv(self) -> bool { + &&& self.objects@ == self.retire_perms@ + &&& forall|obj: nat| #[trigger] self.objects@.contains_key(obj) ==> obj < self.next_obj + } +} + +impl RcuDomainAuth { + /// Stable identity of this protection domain. + pub closed spec fn id(self) -> Loc { + self.objects.id() + } + + /// Registered allocation IDs and their physical addresses. + pub closed spec fn objects(self) -> Map { + self.objects@ + } + + /// Fresh ID reserved for the next registration. + pub closed spec fn next_obj(self) -> nat { + self.next_obj + } + + /// Resource registry that owns the unique base retire permissions. + pub closed spec fn retire_registry(self) -> Loc { + self.retire_perms.id() + } + + /// Creates an empty RCU protection domain. + /// + /// # Postconditions + /// The invariant holds and the first registration receives ID zero. + pub proof fn tracked_new() -> (tracked res: Self) + ensures + res.inv(), + res.objects() == Map::::empty(), + res.next_obj() == 0, + { + let tracked (objects, _) = GhostMapAuth::new(Map::empty()); + let tracked (retire_perms, _) = GhostMapAuth::new(Map::empty()); + RcuDomainAuth { objects, retire_perms, next_obj: 0 } + } + + /// Registers a non-null pointer with a fresh allocation ID. + /// + /// # Preconditions + /// The domain invariant holds and the pointer is non-null. Registering an + /// address does not establish that it points to a live allocation. + /// + /// # Postconditions + /// Existing registrations are preserved. The returned persistent identity + /// and linear permission describe the same new registration. + pub proof fn tracked_register(tracked &mut self, ptr: *mut T) -> (tracked res: + RcuRegistration) + requires + old(self).inv(), + ptr.addr() != 0, + ensures + final(self).inv(), + final(self).id() == old(self).id(), + final(self).retire_registry() == old(self).retire_registry(), + final(self).next_obj() == old(self).next_obj() + 1, + final(self).objects() == old(self).objects().insert(old(self).next_obj(), ptr.addr()), + !old(self).objects().contains_key(res.0.obj()), + res.0.domain() == final(self).id(), + res.0.obj() == old(self).next_obj(), + res.0.ptr() == ptr, + res.0.addr() == ptr.addr(), + res.0.inv(), + res.1.domain() == res.0.domain(), + res.1.obj() == res.0.obj(), + res.1.ptr() == ptr, + res.1.inv(), + res.1.belongs_to(*final(self)), + { + let ghost obj = self.next_obj; + let tracked object = self.objects.insert(obj, ptr.addr()); + let tracked info = object.persist(); + let tracked perm = self.retire_perms.insert(obj, ptr.addr()); + self.next_obj = self.next_obj + 1; + + assert forall|registered: nat| #[trigger] + self.objects@.contains_key(registered) implies registered < self.next_obj by { + if registered != obj { + assert(old(self).objects().contains_key(registered)); + } + }; + + (RcuBlockInfo { info, ptr }, RcuBaseRetirePerm { domain: self.id(), perm, ptr }) + } + + /// Agrees a persistent registration with its authoritative address entry. + /// + /// # Preconditions + /// The block information belongs to this protection domain. + /// + /// # Postconditions + /// The domain contains the recorded allocation ID and address. + pub proof fn lemma_block_info_agree(tracked &self, tracked info: &RcuBlockInfo) + requires + info.domain() == self.id(), + ensures + self.objects().contains_pair(info.obj(), info.addr()), + { + info.info.agree(&self.objects); + } +} + +/// Persistent identity of one registered allocation, without physical ownership. +#[verifier::reject_recursive_types(T)] +pub tracked struct RcuBlockInfo { + info: GhostPersistentPointsTo, + ghost ptr: *mut T, +} + +impl Inv for RcuBlockInfo { + closed spec fn inv(self) -> bool { + &&& self.addr() == self.ptr().addr() + &&& self.ptr().addr() != 0 + } +} + +impl RcuBlockInfo { + /// Protection domain in which the allocation was registered. + pub closed spec fn domain(self) -> Loc { + self.info.id() + } + + /// Allocation ID, independent of address and publication timestamp. + pub closed spec fn obj(self) -> nat { + self.info.key() + } + + /// Typed pointer supplied at registration. + pub closed spec fn ptr(self) -> *mut T { + self.ptr + } + + /// Physical address recorded in the authoritative registry. + pub closed spec fn addr(self) -> usize { + self.info.value() + } + + /// Exposes the address facts hidden by the registration invariant. + /// + /// # Preconditions + /// The block information satisfies its invariant. + /// + /// # Postconditions + /// Its recorded address equals the pointer address and is nonzero. + pub proof fn lemma_address(tracked &self) + requires + self.inv(), + ensures + self.addr() == self.ptr().addr(), + self.ptr().addr() != 0, + { + } + + /// Duplicates persistent block information for another publication. + /// + /// # Postconditions + /// The duplicate describes exactly the same registration. + pub proof fn tracked_duplicate(tracked &self) -> (tracked res: Self) + ensures + res.domain() == self.domain(), + res.obj() == self.obj(), + res.ptr() == self.ptr(), + res.addr() == self.addr(), + res.inv() == self.inv(), + { + let tracked info = self.info.duplicate(); + RcuBlockInfo { info, ptr: self.ptr } + } +} + +/// Unique base permission retained until the allocation is retired. +/// +/// This token supplies linear identity, not evidence of detachment or a grace +/// period. It cannot justify reclaiming physical ownership by itself. +#[verifier::reject_recursive_types(T)] +pub tracked struct RcuBaseRetirePerm { + ghost domain: Loc, + perm: GhostPointsTo, + ghost ptr: *mut T, +} + +impl Inv for RcuBaseRetirePerm { + closed spec fn inv(self) -> bool { + &&& self.addr() == self.ptr().addr() + &&& self.ptr().addr() != 0 + } +} + +impl RcuBaseRetirePerm { + /// Protection domain of the matching block information. + pub closed spec fn domain(self) -> Loc { + self.domain + } + + /// Allocation ID for which this permission is unique. + pub closed spec fn obj(self) -> nat { + self.perm.key() + } + + /// Typed pointer supplied at registration. + pub closed spec fn ptr(self) -> *mut T { + self.ptr + } + + /// Physical address of the registered allocation. + pub closed spec fn addr(self) -> usize { + self.perm.value() + } + + /// Binds the permission to both registries owned by the domain. + pub closed spec fn belongs_to(self, domain: RcuDomainAuth) -> bool { + &&& self.domain() == domain.id() + &&& self.perm.id() == domain.retire_registry() + } + + /// Proves that two base retire permissions cannot name the same allocation. + /// + /// # Preconditions + /// Both permissions belong to the same domain. + /// + /// # Postconditions + /// Their allocation IDs differ and this permission is preserved. + pub proof fn lemma_distinct(tracked &mut self, tracked other: &Self, domain: RcuDomainAuth) + requires + old(self).belongs_to(domain), + other.belongs_to(domain), + ensures + final(self).domain() == old(self).domain(), + final(self).obj() == old(self).obj(), + final(self).ptr() == old(self).ptr(), + final(self).addr() == old(self).addr(), + final(self).inv() == old(self).inv(), + final(self).belongs_to(domain), + final(self).obj() != other.obj(), + { + self.perm.disjoint(&other.perm); + } +} + +/// Persistent identity and linear base retire permission from one registration. +pub type RcuRegistration = (RcuBlockInfo, RcuBaseRetirePerm); + +/// Proves that address reuse creates a new ID while duplication preserves it. +/// +/// # Preconditions +/// The pointer has a nonzero address. +/// +/// # Postconditions +/// Two copies of the first registration agree, while a second registration at +/// that same address has a different allocation ID in the same domain. +pub proof fn lemma_registration_distinguishes_reused_address(ptr: *mut T) -> (tracked res: ( + RcuBlockInfo, + RcuBlockInfo, + RcuBlockInfo, +)) + requires + ptr.addr() != 0, + ensures + res.0.domain() == res.1.domain(), + res.0.domain() == res.2.domain(), + res.0.obj() == res.1.obj(), + res.0.obj() < res.2.obj(), + res.0.addr() == res.1.addr() == res.2.addr() == ptr.addr(), +{ + let tracked mut domain = RcuDomainAuth::tracked_new(); + let tracked (first, _) = domain.tracked_register(ptr); + let tracked history_copy = first.tracked_duplicate(); + let tracked (second, _) = domain.tracked_register(ptr); + assert(first.domain() == history_copy.domain()); + assert(first.domain() == second.domain()); + assert(first.obj() == history_copy.obj()); + assert(first.obj() < second.obj()); + assert(first.addr() == history_copy.addr() == second.addr() == ptr.addr()); + domain.lemma_block_info_agree(&history_copy); + assert(domain.objects().contains_pair(first.obj(), ptr.addr())); + (first, history_copy, second) +} + +} // verus! diff --git a/ostd/specs/sync/rcu/root.rs b/ostd/specs/sync/rcu/root.rs new file mode 100644 index 000000000..bb762ffef --- /dev/null +++ b/ostd/specs/sync/rcu/root.rs @@ -0,0 +1,409 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Allocation identities for a native IRC11 root publication history. +//! +//! # Verified Properties +//! +//! Every non-null message names a registered allocation; null messages have no +//! allocation ID. Appending a newly registered allocation preserves earlier +//! messages, while publishing the same registration again preserves its ID. +//! Timestamps may have gaps and are distinct from allocation IDs. +//! +//! This layer tracks publication identity only. Physical ownership, reader +//! protection, and detachment evidence must be supplied by the RCU protocol. +//! In particular, persistent block information alone does not make a load safe +//! or authorize publishing a previously reclaimed allocation. +use vstd::{prelude::*, raw_ptr::ptr_null_mut, resource::Loc}; +use vstd_extra::{ + atomic_irc11::{AtomicHistory, ThreadView}, + ownership::Inv, +}; + +use super::{RcuBlockInfo, RcuDomainAuth, RcuRegistration}; + +verus! { + +broadcast use vstd::atomic_weak::group_view_history; + +/// Allocation identity attached to a non-null publication. +pub ghost struct RcuPublishedObject { + pub domain: Loc, + pub obj: nat, + pub addr: usize, +} + +/// Registration metadata paired with an atomic root's modification history. +pub tracked struct RcuRootGhost { + domain: RcuDomainAuth, + ghost publications: Map>, + ghost current_timestamp: nat, +} + +/// Agreement of persistent identity, linear permission, and publication. +pub open spec fn registration_matches_publication( + registration: RcuRegistration, + object: RcuPublishedObject, +) -> bool { + &&& registration.0.inv() + &&& registration.1.inv() + &&& registration.0.domain() == object.domain + &&& registration.0.obj() == object.obj + &&& registration.0.addr() == object.addr + &&& registration.0.obj() == registration.1.obj() + &&& registration.0.domain() == registration.1.domain() + &&& registration.0.ptr() == registration.1.ptr() +} + +/// The current publication's registration, including its unique permission. +pub open spec fn current_registration_matches( + root: RcuRootGhost, + registration: Option>, +) -> bool { + match (root.current(), registration) { + (None, None) => true, + (Some(object), Some(registration)) => { + &&& registration_matches_publication(registration, object) + &&& registration.1.belongs_to(root.domain_auth()) + }, + _ => false, + } +} + +/// Agreement between a native pointer history and its allocation registry. +pub open spec fn rcu_root_history_inv( + history: AtomicHistory<*mut T>, + root: RcuRootGhost, +) -> bool { + &&& root.domain_auth().inv() + &&& root.publications().dom() == history.dom() + &&& history.is_max_timestamp(root.current_timestamp()) + &&& forall|ts: nat| + history.contains_timestamp(ts) ==> { + match #[trigger] root.publications()[ts] { + None => history.value(ts).addr() == 0, + Some(obj) => { + &&& history.value(ts).addr() != 0 + &&& root.objects().contains_pair(obj, history.value(ts).addr()) + }, + } + } +} + +impl RcuRootGhost { + /// Domain authority retained by this root's publication registry. + pub closed spec fn domain_auth(self) -> RcuDomainAuth { + self.domain + } + + /// Stable identity of the root's allocation domain. + pub closed spec fn domain(self) -> Loc { + self.domain.id() + } + + /// All allocation registrations, including historical publications. + pub closed spec fn objects(self) -> Map { + self.domain.objects() + } + + /// Allocation ID recorded for each timestamp, or `None` for a null message. + pub closed spec fn publications(self) -> Map> { + self.publications + } + + /// Timestamp of the latest publication. + pub closed spec fn current_timestamp(self) -> nat { + self.current_timestamp + } + + /// Resolves a history message to its registered allocation identity. + pub open spec fn published_at(self, timestamp: nat) -> Option + recommends + self.publications().contains_key(timestamp), + { + match self.publications()[timestamp] { + Some(obj) => Some( + RcuPublishedObject { domain: self.domain(), obj, addr: self.objects()[obj] }, + ), + None => None, + } + } + + /// Allocation identity of the latest message. + pub open spec fn current(self) -> Option + recommends + self.publications().contains_key(self.current_timestamp()), + { + self.published_at(self.current_timestamp()) + } + + /// Initializes publication metadata for a newly created root atomic. + /// + /// # Preconditions + /// The atomic history contains exactly the supplied initial message. + /// + /// # Postconditions + /// History and registration agree. A non-null pointer receives one fresh + /// registration whose linear permission is returned to the caller. + pub proof fn tracked_initial( + ptr: *mut T, + history: AtomicHistory<*mut T>, + timestamp: nat, + message_view: ThreadView, + ) -> (tracked res: (Self, Option>)) + requires + history.is_singleton(timestamp, (ptr, message_view)), + ensures + rcu_root_history_inv(history, res.0), + res.0.current_timestamp() == timestamp, + current_registration_matches(res.0, res.1), + (res.1 is Some) == (ptr.addr() != 0), + res.1 matches Some(registration) ==> { + &&& registration.0.ptr() == ptr + &&& res.0.publications()[timestamp] == Some(registration.0.obj()) + }, + match res.1 { + Some(registration) => res.0.objects() == Map::empty().insert( + registration.0.obj(), + ptr.addr(), + ), + None => res.0.objects() == Map::empty(), + }, + { + let tracked mut domain = RcuDomainAuth::tracked_new(); + assert(history.is_max_timestamp(timestamp)); + assert(history.dom() == Set::empty().insert(timestamp)) by { + assert forall|ts: nat| + history.dom().contains(ts) <==> Set::empty().insert(timestamp).contains(ts) by { + if history.dom().contains(ts) { + assert(history.contains_timestamp(ts)); + assert(ts == timestamp); + } + }; + }; + if ptr.addr() == 0 { + ( + RcuRootGhost { + domain, + publications: Map::empty().insert(timestamp, None), + current_timestamp: timestamp, + }, + None, + ) + } else { + let tracked registration = domain.tracked_register(ptr); + let ghost obj = registration.0.obj(); + ( + RcuRootGhost { + domain, + publications: Map::empty().insert(timestamp, Some(obj)), + current_timestamp: timestamp, + }, + Some(registration), + ) + } + } + + /// Appends a publication with a fresh registration, or a null message. + /// + /// # Preconditions + /// The previous history satisfies the invariant. The new message is appended + /// after its maximum timestamp; this transition does not cover stores that + /// insert a message earlier in modification order. + /// + /// # Postconditions + /// Prior messages keep their allocation identities. The returned registration + /// describes the new current pointer, even if its address appeared before. + pub proof fn tracked_push_fresh( + tracked &mut self, + prev: AtomicHistory<*mut T>, + next: AtomicHistory<*mut T>, + new_timestamp: nat, + value: *mut T, + message_view: ThreadView, + ) -> (tracked res: Option>) + requires + rcu_root_history_inv(prev, *old(self)), + old(self).current_timestamp() < new_timestamp, + next == prev.insert(new_timestamp, value, message_view), + ensures + rcu_root_history_inv(next, *final(self)), + final(self).domain() == old(self).domain(), + final(self).domain_auth().retire_registry() == old( + self, + ).domain_auth().retire_registry(), + final(self).current_timestamp() == new_timestamp, + current_registration_matches(*final(self), res), + (res is Some) == (value.addr() != 0), + res matches Some(registration) ==> { + &&& registration.0.ptr() == value + &&& !old(self).objects().contains_key(registration.0.obj()) + }, + final(self).publications() == old(self).publications().insert( + new_timestamp, + match res { + Some(registration) => Some(registration.0.obj()), + None => None, + }, + ), + match res { + Some(registration) => final(self).objects() == old(self).objects().insert( + registration.0.obj(), + value.addr(), + ), + None => final(self).objects() == old(self).objects(), + }, + { + let tracked res = if value.addr() == 0 { + self.publications = self.publications.insert(new_timestamp, None); + None + } else { + let tracked registration = self.domain.tracked_register(value); + self.publications = self.publications.insert(new_timestamp, Some(registration.0.obj())); + Some(registration) + }; + self.current_timestamp = new_timestamp; + + assert forall|ts: nat| next.contains_timestamp(ts) implies { + match #[trigger] self.publications()[ts] { + None => next.value(ts).addr() == 0, + Some(obj) => { + &&& next.value(ts).addr() != 0 + &&& self.objects().contains_pair(obj, next.value(ts).addr()) + }, + } + } by { + if ts != new_timestamp { + assert(prev.contains_timestamp(ts)); + assert(next.value(ts) == prev.value(ts)); + assert(self.publications()[ts] == old(self).publications()[ts]); + } + }; + res + } + + /// Appends another message for an existing registration. + /// + /// # Preconditions + /// The history satisfies the invariant, the timestamp is later than every + /// existing message, and valid block information binds this exact pointer to + /// this domain. The caller must separately justify publishing the allocation. + /// + /// # Postconditions + /// The new message carries the same allocation ID. The domain and all earlier + /// messages are preserved, and no new retire permission is created. + pub proof fn tracked_push_registered( + tracked &mut self, + prev: AtomicHistory<*mut T>, + next: AtomicHistory<*mut T>, + new_timestamp: nat, + value: *mut T, + message_view: ThreadView, + tracked info: &RcuBlockInfo, + ) + requires + rcu_root_history_inv(prev, *old(self)), + old(self).current_timestamp() < new_timestamp, + next == prev.insert(new_timestamp, value, message_view), + info.domain() == old(self).domain(), + info.ptr() == value, + info.inv(), + ensures + rcu_root_history_inv(next, *final(self)), + final(self).domain_auth() == old(self).domain_auth(), + final(self).domain() == old(self).domain(), + final(self).objects() == old(self).objects(), + final(self).current_timestamp() == new_timestamp, + final(self).publications() == old(self).publications().insert( + new_timestamp, + Some(info.obj()), + ), + { + self.domain.lemma_block_info_agree(info); + info.lemma_address(); + self.publications = self.publications.insert(new_timestamp, Some(info.obj())); + self.current_timestamp = new_timestamp; + + assert forall|ts: nat| next.contains_timestamp(ts) implies { + match #[trigger] self.publications()[ts] { + None => next.value(ts).addr() == 0, + Some(obj) => { + &&& next.value(ts).addr() != 0 + &&& self.objects().contains_pair(obj, next.value(ts).addr()) + }, + } + } by { + if ts != new_timestamp { + assert(prev.contains_timestamp(ts)); + assert(next.value(ts) == prev.value(ts)); + assert(self.publications()[ts] == old(self).publications()[ts]); + } + }; + } +} + +/// Proves that republishing an allocation preserves its ID across timestamp gaps. +/// +/// # Preconditions +/// The pointer is non-null. +/// +/// # Postconditions +/// Messages at timestamps 3 and 8 identify the same registration. +pub proof fn lemma_republication_preserves_allocation_id(ptr: *mut T) -> (tracked res: ( + RcuRootGhost, + RcuRegistration, +)) + requires + ptr.addr() != 0, + ensures + res.0.publications().dom() == Set::empty().insert(3nat).insert(8nat), + res.0.publications()[3] == Some(res.1.0.obj()), + res.0.publications()[8] == Some(res.1.0.obj()), + current_registration_matches(res.0, Some(res.1)), +{ + let ghost view = ThreadView::empty(); + let ghost initial = AtomicHistory(Map::empty().insert(3nat, (ptr, view))); + let tracked (mut root, registration) = RcuRootGhost::tracked_initial(ptr, initial, 3, view); + let tracked registration = registration.tracked_unwrap(); + let ghost next = initial.insert(8, ptr, view); + root.tracked_push_registered(initial, next, 8, ptr, view, ®istration.0); + (root, registration) +} + +/// Proves that a null publication and later address reuse preserve old identities. +/// +/// # Preconditions +/// The pointer is non-null. +/// +/// # Postconditions +/// The two non-null messages have different allocation IDs at the same address; +/// the intervening null message has no allocation ID. +pub proof fn lemma_history_distinguishes_reused_address(ptr: *mut T) -> (tracked res: ( + RcuRootGhost, + RcuRegistration, + RcuRegistration, +)) + requires + ptr.addr() != 0, + ensures + res.0.publications().dom() == Set::empty().insert(3nat).insert(8nat).insert(13nat), + res.0.publications()[3] == Some(res.1.0.obj()), + res.0.publications()[8] is None, + res.0.publications()[13] == Some(res.2.0.obj()), + res.1.0.obj() != res.2.0.obj(), + res.1.0.addr() == res.2.0.addr() == ptr.addr(), + current_registration_matches(res.0, Some(res.2)), +{ + let ghost view = ThreadView::empty(); + let ghost initial = AtomicHistory(Map::empty().insert(3nat, (ptr, view))); + let tracked (mut root, first) = RcuRootGhost::tracked_initial(ptr, initial, 3, view); + let tracked first = first.tracked_unwrap(); + let ghost null = ptr_null_mut::(); + let ghost removed = initial.insert(8, null, view); + let tracked no_registration = root.tracked_push_fresh(initial, removed, 8, null, view); + assert(no_registration is None); + let ghost reused = removed.insert(13, ptr, view); + let tracked second = root.tracked_push_fresh(removed, reused, 13, ptr, view); + let tracked second = second.tracked_unwrap(); + (root, first, second) +} + +} // verus! diff --git a/ostd/src/mm/frame/linked_list.rs b/ostd/src/mm/frame/linked_list.rs index ef8a1c24b..5a6854483 100644 --- a/ostd/src/mm/frame/linked_list.rs +++ b/ostd/src/mm/frame/linked_list.rs @@ -976,9 +976,9 @@ impl<'a, M: AnyFrameMeta + Repr> CursorMut<'a, M> { } by {} } - let next_ptr = (#[verus_spec(with Tracked(&frame_own), Tracked(&*regions))] + let next_ptr = (#[verus_spec(with Tracked(&frame_own))] frame.meta()).next; - let prev_ptr = (#[verus_spec(with Tracked(&frame_own), Tracked(&*regions))] + let prev_ptr = (#[verus_spec(with Tracked(&frame_own))] frame.meta()).prev; if let Some(prev) = prev_ptr { @@ -1066,9 +1066,9 @@ impl<'a, M: AnyFrameMeta + Repr> CursorMut<'a, M> { } } - (#[verus_spec(with Tracked(&mut frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(&mut frame_own))] frame.meta_mut()).next = None; - (#[verus_spec(with Tracked(&mut frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(&mut frame_own))] frame.meta_mut()).prev = None; let tracked mut frame_so = regions.slot_owners.tracked_borrow_mut(idx); @@ -1269,9 +1269,9 @@ impl<'a, M: AnyFrameMeta + Repr> CursorMut<'a, M> { if let Some(prev_link) = opt_prev_link { let prev = prev_link; - (#[verus_spec(with Tracked(frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(frame_own))] frame.meta_mut()).prev = Some(prev_link); - (#[verus_spec(with Tracked(frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(frame_own))] frame.meta_mut()).next = Some(current); let ghost prev_idx = meta_to_index(owner.list_own.list[nn - 1].paddr); @@ -1306,7 +1306,7 @@ impl<'a, M: AnyFrameMeta + Repr> CursorMut<'a, M> { ); current_meta.prev = Some(frame_ptr); } else { - (#[verus_spec(with Tracked(frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(frame_own))] frame.meta_mut()).next = Some(current); let ghost current_idx = meta_to_index(owner.list_own.list[nn].paddr); @@ -1325,7 +1325,7 @@ impl<'a, M: AnyFrameMeta + Repr> CursorMut<'a, M> { } } else { if let Some(back) = self.list.back { - (#[verus_spec(with Tracked(frame_own), Tracked(regions))] + (#[verus_spec(with Tracked(frame_own))] frame.meta_mut()).prev = Some(back); let ghost back_idx = meta_to_index(owner.list_own.list[nn - 1].paddr); diff --git a/ostd/src/mm/frame/mod.rs b/ostd/src/mm/frame/mod.rs index b950e2d7b..52d048b44 100644 --- a/ostd/src/mm/frame/mod.rs +++ b/ostd/src/mm/frame/mod.rs @@ -310,7 +310,7 @@ impl> Frame { ensures final(regions).inv(), res matches Ok(res) ==> { - &&& MetaSlot::inc_frame_reference_region_spec(paddr, *old(regions), *final(regions)) + &&& old(regions).inc_frame_reference_region_spec(paddr, *final(regions)) &&& res.inv() &&& res.start_paddr_spec() == paddr &&& res.wf_with_region(*final(regions)) @@ -627,7 +627,7 @@ impl> RCClone for Frame { res: Self, ) -> bool { let idx = self.index(); - &&& MetaSlot::inc_frame_reference_region_spec(self.start_paddr_spec(), pre, post) + &&& pre.inc_frame_reference_region_spec(self.start_paddr_spec(), post) &&& post.inv() &&& res.tracked_metadata_perm@ is Some &&& res.tracked_metadata_perm@->0.frac() == 1 @@ -827,7 +827,7 @@ impl TryFrom> for UFrame { permission@.frac() == 1, permission@.id() == final(regions).slot_owner(paddr).metadata_perm.id(), permission@.resource() == old(regions).slot_owner(paddr).metadata_perm@, - MetaSlot::inc_frame_reference_region_spec(paddr,*old(regions),*final(regions)), + old(regions).inc_frame_reference_region_spec(paddr, *final(regions)), )] pub(in crate::mm) unsafe fn inc_frame_ref_count(paddr: Paddr) -> (permission: Tracked< FracMetadataPerm, diff --git a/ostd/src/mm/frame/segment.rs b/ostd/src/mm/frame/segment.rs index 868da666f..6a83035f0 100644 --- a/ostd/src/mm/frame/segment.rs +++ b/ostd/src/mm/frame/segment.rs @@ -54,41 +54,6 @@ pub struct Segment { tracked_slot_perms: Tracked>>>, } -#[verifier::reject_recursive_types(M)] -pub closed spec fn segment_iter_frame>( - paddr: Paddr, - slot_perm: &'static PointsTo, - permission: FracMetadataPerm, -) -> Frame { - Frame { - ptr: PPtr(frame_to_meta(paddr), core::marker::PhantomData), - _marker: core::marker::PhantomData, - #[cfg(verus_keep_ghost_body)] - tracked_slot_perm: Tracked(slot_perm), - #[cfg(verus_keep_ghost_body)] - tracked_metadata_perm: Tracked(Some(permission)), - } -} - -#[verifier::reject_recursive_types(M)] -pub closed spec fn segment_iter_remaining>( - range: Range, - slot_perms: Seq<&'static PointsTo>, - permissions: Seq, -) -> Seq> { - Seq::new( - permissions.len() as nat, - |i: int| - { - segment_iter_frame::( - (range.start + i * PAGE_SIZE) as usize, - slot_perms[i], - permissions[i], - ) - }, - ) -} - /* impl Debug for Segment { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { @@ -1008,7 +973,17 @@ impl + OwnerOf> IteratorSpecImpl for Seg #[verifier::prophetic] closed spec fn remaining(&self) -> Seq { - segment_iter_remaining::(self.range, self.slot_perms(), self.permissions()) + Seq::new( + self.permissions().len() as nat, + |i: int| + { + Frame::::from_raw_spec( + (self.range().start + i * PAGE_SIZE) as usize, + self.slot_perms()[i], + Some(self.permissions()[i]), + ) + }, + ) } #[verifier::prophetic] @@ -1023,10 +998,10 @@ impl + OwnerOf> IteratorSpecImpl for Seg open spec fn peek(&self, index: int) -> Option { if 0 <= index < self.permissions().len() { Some( - segment_iter_frame::( + Frame::::from_raw_spec( (self.range().start + index * PAGE_SIZE) as usize, self.slot_perms()[index], - self.permissions()[index], + Some(self.permissions()[index]), ), ) } else { diff --git a/ostd/src/mm/frame/unique.rs b/ostd/src/mm/frame/unique.rs index 6e6578a73..d4b3ceec5 100644 --- a/ostd/src/mm/frame/unique.rs +++ b/ostd/src/mm/frame/unique.rs @@ -246,12 +246,11 @@ impl + OwnerOf> UniqueFrame { #[verus_spec(l => with Tracked(owner): Tracked<&'a UniqueFrameOwner>, - Tracked(regions): Tracked<&'a MetaRegionOwners>, requires owner.inv(), - regions.inv(), self.inv(), - self.wf_with_region(*owner, *regions), + self.wf(*owner), + self.meta_wf(*owner), ensures self.meta_value(*owner) == l, )] @@ -280,12 +279,11 @@ impl + OwnerOf> UniqueFrame { #[verus_spec(res => with Tracked(owner): Tracked<&'a mut UniqueFrameOwner>, - Tracked(regions): Tracked<&'a mut MetaRegionOwners>, requires - old(self).wf_with_region(*owner, *old(regions)), old(self).inv(), + old(self).wf(*owner), + old(self).meta_wf(*owner), owner.inv(), - regions.inv(), ensures *res == old(self).meta_value(*old(owner)), *final(res) == final(self).meta_value(*final(owner)), @@ -297,26 +295,6 @@ impl + OwnerOf> UniqueFrame { final(owner).inv(), final(self).meta_wf(*final(owner)), (*final(self)).wf(*final(owner)), - final(regions).inv(), - final(regions).slots == old(regions).slots, - final(regions).slots.dom() == old(regions).slots.dom(), - final(regions).slot_owners.dom() == old(regions).slot_owners.dom(), - forall|j: int| - #![trigger final(regions).slot_owners[j]] - j != old(owner).slot_index - ==> final(regions).slot_owners[j] == old(regions).slot_owners[j], - final(regions).slot_owners[final(owner).slot_index].slot_vaddr - == old(regions).slot_owners[old(owner).slot_index].slot_vaddr, - final(regions).slot_owners[final(owner).slot_index].usage - == old(regions).slot_owners[old(owner).slot_index].usage, - final(regions).slot_owners[final(owner).slot_index].ref_count_perm - == old(regions).slot_owners[old(owner).slot_index].ref_count_perm, - final(regions).slot_owners[final(owner).slot_index].in_list_perm - == old(regions).slot_owners[old(owner).slot_index].in_list_perm, - final(regions).slot_owners[final(owner).slot_index].paths_in_pt - == old(regions).slot_owners[old(owner).slot_index].paths_in_pt, - ::wf(final(self).meta_value(*final(owner)), final(owner).meta_own) - ==> final(self).wf_with_region(*final(owner), *final(regions)), )] pub fn meta_mut<'a>(&'a mut self) -> &'a mut M { let tracked points_to = *self.tracked_slot_perm.borrow(); diff --git a/ostd/src/mm/kspace/kvirt_area.rs b/ostd/src/mm/kspace/kvirt_area.rs index 6256f9d5c..22c0c1554 100644 --- a/ostd/src/mm/kspace/kvirt_area.rs +++ b/ostd/src/mm/kspace/kvirt_area.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! Kernel virtual memory allocation use vstd::prelude::*; +use vstd::set_lib::FiniteRange; use vstd_extra::arithmetic::nat_align_down; use vstd_extra::assert; @@ -724,7 +725,7 @@ impl KVirtArea { ||| pa_range.end % PAGE_SIZE != 0 ||| area_size % PAGE_SIZE != 0 ||| map_offset % PAGE_SIZE != 0 - ||| map_offset + vstd_extra::external::range::range_usize_len(pa_range) > area_size + ||| map_offset + usize::range_len(pa_range.start, pa_range.end) > area_size } /// Full panic condition for [`Self::map_untracked_frames`] = bounds OR OOM. @@ -791,7 +792,7 @@ impl KVirtArea { owner.pt_owner.metaregion_sound(*old(regions)), owner.pt_owner.0.value().node().relate_guard(root_guard), Self::untracked_range_slots_in_regions(&pa_range, *old(regions)), - map_offset + vstd_extra::external::range::range_usize_len(&pa_range) <= usize::MAX, + map_offset + usize::range_len(pa_range.start, pa_range.end) <= usize::MAX, forall|pa: Paddr, level: PagingLevel| #[trigger] ::new_page_req( diff --git a/ostd/src/mm/page_table/cursor/mod.rs b/ostd/src/mm/page_table/cursor/mod.rs index 7d885a5a9..7698432c3 100644 --- a/ostd/src/mm/page_table/cursor/mod.rs +++ b/ostd/src/mm/page_table/cursor/mod.rs @@ -229,7 +229,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { final(regions).slots == old(regions).slots, final(regions).slot_owners.dom() == old(regions).slot_owners.dom(), C::item_into_raw(*item).3@ is Some ==> { - MetaSlot::inc_frame_reference_region_spec(pa, *old(regions), *final(regions)) + old(regions).inc_frame_reference_region_spec(pa, *final(regions)) }, C::item_into_raw(*item).3@ is None ==> *final(regions) == *old(regions), )] @@ -275,49 +275,41 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { &&& r.unwrap().1@.continuations[3].path() == pt_own.0.value().path }, !Self::cursor_new_success_conditions(*va) ==> r is Err, - // Cursor::new inherits lock_range's weakened preservation: only - // slots that were non-UNUSED before the call keep their - // paths_in_pt (new PT allocations come from UNUSED slots). forall|idx: int| #![trigger final(regions).slot_owners[idx].paths_in_pt] - old(regions).slot_owners[idx].ref_count() - != REF_COUNT_UNUSED + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> final(regions).slot_owners[idx].paths_in_pt == old(regions).slot_owners[idx].paths_in_pt, forall|idx: int| #![trigger final(regions).slot_owners[idx]] old(regions).contains(idx) - && old(regions).slot_owners[idx].ref_count() - != REF_COUNT_UNUSED - ==> final(regions).slot_owners[idx].ref_count() - == old(regions).slot_owners[idx].ref_count() + && old(regions).ref_count(idx) != REF_COUNT_UNUSED + ==> final(regions).ref_count(idx) == old(regions).ref_count(idx) && final(regions).slot_owners[idx].usage == old(regions).slot_owners[idx].usage, forall|idx: int| #![trigger final(regions).slot_owners[idx].ref_count()] - final(regions).slot_owners[idx].ref_count() - >= REF_COUNT_MAX - ==> old(regions).slot_owners[idx].ref_count() - == final(regions).slot_owners[idx].ref_count(), + final(regions).ref_count(idx) >= REF_COUNT_MAX + ==> old(regions).ref_count(idx) + == final(regions).ref_count(idx), forall|idx: int| #![trigger old(regions).slot_owners[idx].ref_count()] - old(regions).slot_owners[idx].ref_count() + old(regions).ref_count(idx) >= REF_COUNT_MAX - ==> final(regions).slot_owners[idx].ref_count() - == old(regions).slot_owners[idx].ref_count(), + ==> final(regions).ref_count(idx) + == old(regions).ref_count(idx), forall|item: C::Item| #![trigger CursorMut::::item_not_mapped(item, *old(regions))] CursorMut::::item_not_mapped(item, *old(regions)) ==> CursorMut::::item_not_mapped(item, *final(regions)), // Non-saturation preservation. (forall |i: int| #![trigger old(regions).slot_owners[i]] old(regions).contains(i) - && old(regions).slot_owners[i].ref_count() + && old(regions).ref_count(i) != REF_COUNT_UNUSED - ==> old(regions).slot_owners[i].ref_count() + 1 + ==> old(regions).ref_count(i) + 1 < REF_COUNT_MAX) ==> (forall |i: int| #![trigger final(regions).slot_owners[i]] final(regions).contains(i) - && final(regions).slot_owners[i].ref_count() + && final(regions).ref_count(i) != REF_COUNT_UNUSED - ==> final(regions).slot_owners[i].ref_count() + 1 - < REF_COUNT_MAX), + ==> final(regions).ref_count(i) + 1 < REF_COUNT_MAX), )] pub fn new(pt: &'rcu PageTable, guard: &'rcu A, va: &Range) -> Result< (Self, Tracked>), @@ -434,9 +426,9 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { forall|i: int| #![trigger regions.slot_owners[i]] old(regions).contains(i) - ==> regions.slot_owners[i].ref_count() == old( + ==> regions.ref_count(i) == old( regions, - ).slot_owners[i].ref_count(), + ).ref_count(i), regions.slot_owners.dom() == old(regions).slot_owners.dom(), forall|idx: int| #![trigger regions.slot_owners[idx]] @@ -451,13 +443,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { &&& regions.slot_owners[idx].ref_count_perm.id() == old( regions, ).slot_owners[idx].ref_count_perm.id() - &&& regions.slot_owners[idx].ref_count() >= old( - regions, - ).slot_owners[idx].ref_count() - &&& regions.slot_owners[idx].ref_count() - != REF_COUNT_UNUSED || old( - regions, - ).slot_owners[idx].ref_count() == REF_COUNT_UNUSED + &&& regions.ref_count(idx) >= old(regions).ref_count(idx) &&& regions.slot_owners[idx].metadata_perm == old( regions, ).slot_owners[idx].metadata_perm @@ -608,7 +594,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { old(regions).lemma_contains_valid_frame_paddr(pa); assert(regions.slot_owners.contains_key(idx)); assert(owner_before_permission_take.cur_entry_owner().inv_base()); - if C::item_into_raw(item).3@ is Some && regions.slot_owners[idx].ref_count() + if C::item_into_raw(item).3@ is Some && regions.ref_count(idx) >= REF_COUNT_MAX { EntryOwner::::axiom_frame_is_tracked_iff_not_mmio( owner_before_permission_take.cur_entry_owner(), @@ -2213,16 +2199,16 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { // PT-node allocations come from UNUSED slots, so any slot that // was already in use keeps its paths_in_pt. forall |idx: int| #![trigger final(regions).slot_owners[idx].paths_in_pt] - old(regions).slot_owners[idx].ref_count() + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> final(regions).slot_owners[idx].paths_in_pt == old(regions).slot_owners[idx].paths_in_pt, forall|idx: int| #![trigger final(regions).slot_owners[idx]] old(regions).contains(idx) - && old(regions).slot_owners[idx].ref_count() + && old(regions).ref_count(idx) != REF_COUNT_UNUSED - ==> final(regions).slot_owners[idx].ref_count() - == old(regions).slot_owners[idx].ref_count() + ==> final(regions).ref_count(idx) + == old(regions).ref_count(idx) && final(regions).slot_owners[idx].usage == old(regions).slot_owners[idx].usage, )] @@ -2499,7 +2485,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { Self::item_slot_in_regions(item, *final(regions)), (level <= old(self).0.level && old(owner).cur_entry_owner().is_absent()) ==> final(owner).cur_entry_owner().is_absent(), forall|idx: int| - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED ==> + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> (#[trigger] final(regions).slot_owners[idx]) == old(regions).slot_owners[idx], // `regions.slots` is monotonic — PT-node allocation removes-and-re-inserts // each slot it touches, so all old keys are preserved. @@ -2547,7 +2533,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { self.0.level < level ==> self.0.level >= owner0.level, self.0.level < level ==> owner@ == owner0@, forall|idx: int| - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> (#[trigger] regions.slot_owners[idx]) == old(regions).slot_owners[idx], forall|idx: int| #![trigger regions.slots.contains_key(idx)] @@ -2777,8 +2763,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { assert(eo.metaregion_sound(regions_after_ref)); let eo_idx = frame_to_index(eo.meta_slot_paddr().unwrap()); assert(eo_idx == eo.node().slot_index); - assert(regions_after_ref.slot_owners[eo_idx].ref_count() - != REF_COUNT_UNUSED); + assert(regions_after_ref.ref_count(eo_idx) != REF_COUNT_UNUSED); assert(eo_idx != new_pt_idx); assert(regions.slot_owners[eo_idx] == regions_after_ref.slot_owners[eo_idx]); @@ -2860,11 +2845,11 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { assert(regions.slot_owners.contains_key(idx)); }; assert forall|idx: int| - regions0.slot_owners[idx].ref_count() + regions0.ref_count(idx) != REF_COUNT_UNUSED implies #[trigger] regions.slot_owners[idx] == regions0.slot_owners[idx] by {}; assert forall|idx: int| - regions0.contains(idx) && regions0.slot_owners[idx].ref_count() + regions0.contains(idx) && regions0.ref_count(idx) != REF_COUNT_UNUSED implies #[trigger] regions.slots[idx] == regions0.slots[idx] by {}; Self::all_item_slots_preserved(regions0, *regions); @@ -2941,11 +2926,11 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { assert(regions.slot_owners.contains_key(idx)); }; assert forall|idx: int| - regions0.slot_owners[idx].ref_count() + regions0.ref_count(idx) != REF_COUNT_UNUSED implies #[trigger] regions.slot_owners[idx] == regions0.slot_owners[idx] by {}; assert forall|idx: int| - regions0.contains(idx) && regions0.slot_owners[idx].ref_count() + regions0.contains(idx) && regions0.ref_count(idx) != REF_COUNT_UNUSED implies #[trigger] regions.slots[idx] == regions0.slots[idx] by {}; Self::all_item_slots_preserved(regions0, *regions); @@ -2958,7 +2943,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { }; } assert forall|idx: int| - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED implies ( + old(regions).ref_count(idx) != REF_COUNT_UNUSED implies ( #[trigger] regions.slot_owners[idx]) == old(regions).slot_owners[idx] by { assert(regions0.slot_owners[idx] == old(regions).slot_owners[idx]); assert(regions_after_ref.slot_owners[idx] == regions0.slot_owners[idx]); @@ -3018,7 +3003,6 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { /// - **Correctness**: if the old entry was absent, the result is `Ok(())`. /// - **Correctness**: `paths_in_pt` is preserved for all metadata slots /// other than the newly mapped frame. - /// ## Safety #[verus_spec(res => with Tracked(owner): Tracked<&mut CursorOwner<'rcu, C>>, Tracked(entry_owner): Tracked>, @@ -3028,8 +3012,6 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { old(self).0.invariants(*old(owner), *old(regions), *old(guards)), old(self).item_wf(item, entry_owner), Self::item_slot_in_regions(item, *old(regions)), - // The runtime `assert!`s diverge unless the VA is in range and the - // item's level/alignment are valid ([`Self::map_panic_conditions`]). old(self).map_panic_conditions(item) ==> may_panic(), ensures !old(self).map_panic_conditions(item), @@ -3045,32 +3027,31 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { forall|idx: int| #![trigger final(regions).slot_owners[idx].paths_in_pt] old(regions).contains(idx) && idx != frame_to_index(C::item_into_raw(item).0) && - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED ==> + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> final(regions).slot_owners[idx].paths_in_pt == old(regions).slot_owners[idx].paths_in_pt, forall|idx: int| #![trigger final(regions).slot_owners[idx].ref_count()] old(regions).contains(idx) && - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED ==> - final(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED, + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> + final(regions).ref_count(idx) != REF_COUNT_UNUSED, forall|idx: int| #![trigger final(regions).slot_owners[idx].ref_count()] old(regions).contains(idx) && idx != frame_to_index(C::item_into_raw(item).0) && - old(regions).slot_owners[idx].ref_count() != REF_COUNT_UNUSED ==> - final(regions).slot_owners[idx].ref_count() - == old(regions).slot_owners[idx].ref_count(), + old(regions).ref_count(idx) != REF_COUNT_UNUSED ==> + final(regions).ref_count(idx) + == old(regions).ref_count(idx), (C::item_into_raw(item).3@ is Some && old(regions).contains(frame_to_index(C::item_into_raw(item).0)) - && old(regions).slot_owners[ - frame_to_index(C::item_into_raw(item).0)].ref_count() > 0) + && old(regions).ref_count(frame_to_index(C::item_into_raw(item).0)) > 0) ==> - final(regions).slot_owners[ - frame_to_index(C::item_into_raw(item).0)].ref_count() > 0, + final(regions).ref_count( + frame_to_index(C::item_into_raw(item).0)) > 0, (C::item_into_raw(item).3@ is Some - && old(regions).slot_owners[ - frame_to_index(C::item_into_raw(item).0)].ref_count() + && old(regions).ref_count( + frame_to_index(C::item_into_raw(item).0)) <= REF_COUNT_MAX) ==> - final(regions).slot_owners[ - frame_to_index(C::item_into_raw(item).0)].ref_count() + final(regions).ref_count( + frame_to_index(C::item_into_raw(item).0)) <= REF_COUNT_MAX, forall|idx: int| #![trigger final(regions).contains(idx)] old(regions).contains(idx) ==> final(regions).contains(idx), @@ -3301,24 +3282,22 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { }; let ghost pa_idx2 = frame_to_index(C::item_into_raw(item).0); assert forall|idx: int| - old(regions).contains(idx) && idx != pa_idx2 && old( - regions, - ).slot_owners[idx].ref_count() + old(regions).contains(idx) && idx != pa_idx2 && old(regions).ref_count(idx) != REF_COUNT_UNUSED implies #[trigger] regions.slot_owners[idx].paths_in_pt == old(regions).slot_owners[idx].paths_in_pt by { assert(regions_after_new_child.slot_owners == regions_before_new_child.slot_owners); }; assert(C::item_into_raw(item).3@ is Some && old(regions).contains(pa_idx2) && old( regions, - ).slot_owners[pa_idx2].ref_count() > 0 ==> { - &&& regions.slot_owners[pa_idx2].ref_count() > 0 + ).ref_count(pa_idx2) > 0 ==> { + &&& regions.ref_count(pa_idx2) > 0 }) by { if C::item_into_raw(item).3@ is Some && old(regions).contains(pa_idx2) && old( regions, - ).slot_owners[pa_idx2].ref_count() > 0 { - assert(regions_before_new_child.slot_owners[pa_idx2].ref_count() > 0); - assert(regions_after_new_child.slot_owners[pa_idx2].ref_count() > 0); - assert(regions_after_replace.slot_owners[pa_idx2].ref_count() > 0); + ).ref_count(pa_idx2) > 0 { + assert(regions_before_new_child.ref_count(pa_idx2) > 0); + assert(regions_after_new_child.ref_count(pa_idx2) > 0); + assert(regions_after_replace.ref_count(pa_idx2) > 0); } }; @@ -3856,9 +3835,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { old(regions).contains(idx) ==> final(regions).contains(idx), forall|idx: int| #![trigger final(regions).slot_owners[idx].ref_count()] - final(regions).slot_owners[idx].ref_count() == old( - regions, - ).slot_owners[idx].ref_count(), + final(regions).ref_count(idx) == old(regions).ref_count(idx), res is None ==> final(regions).slots == old(regions).slots, res is Some && res->0 is Mapped && new_owner.value().is_absent() ==> forall|idx: int| #![trigger final(regions).slot_owners[idx]] diff --git a/ostd/src/mm/page_table/mod.rs b/ostd/src/mm/page_table/mod.rs index 629dd065d..e742b039a 100644 --- a/ostd/src/mm/page_table/mod.rs +++ b/ostd/src/mm/page_table/mod.rs @@ -437,7 +437,7 @@ pub unsafe trait PageTableConfig: Clone + Debug + Send + Sync + 'static { Self::item_into_raw(res).2 == Self::item_into_raw(item).2, (Self::item_into_raw(res).3@ is Some) == (Self::item_into_raw(item).3@ is Some), Self::item_into_raw(item).3@ is Some ==> { - MetaSlot::inc_frame_reference_region_spec(pa, old_regions, new_regions) + old_regions.inc_frame_reference_region_spec(pa, new_regions) }, Self::item_into_raw(item).3@ is None ==> new_regions == old_regions, ; diff --git a/ostd/src/mm/page_table/node/entry.rs b/ostd/src/mm/page_table/node/entry.rs index cd78035a1..49bb3b4a0 100644 --- a/ostd/src/mm/page_table/node/entry.rs +++ b/ostd/src/mm/page_table/node/entry.rs @@ -324,10 +324,10 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { k, ), forall|idx: int| - #![trigger final(regions).slot_owners[idx].ref_count()] - final(regions).slot_owners[idx].ref_count() == old( + #![trigger final(regions).ref_count(idx)] + final(regions).ref_count(idx) == old( regions, - ).slot_owners[idx].ref_count(), + ).ref_count(idx), forall|idx: int| #![trigger final(regions).slot_owners[idx].ref_count_perm] final(regions).slot_owners[idx].same_permissions( @@ -990,9 +990,9 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { != crate::specs::mm::frame::meta_owners::PageUsage::PageTable &&& regions.slot_owners[sub_idx].usage != crate::specs::mm::frame::meta_owners::PageUsage::MMIO ==> { - &&& regions.slot_owners[sub_idx].ref_count() != REF_COUNT_UNUSED - &&& regions.slot_owners[sub_idx].ref_count() > 0 - &&& regions.slot_owners[sub_idx].ref_count() <= REF_COUNT_MAX + &&& regions.ref_count(sub_idx) != REF_COUNT_UNUSED + &&& regions.ref_count(sub_idx) > 0 + &&& regions.ref_count(sub_idx) <= REF_COUNT_MAX } } by { let sub_idx = frame_to_index((pa + j * PAGE_SIZE) as usize); @@ -1080,9 +1080,9 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { &&& regions.slots.contains_key(sub_idx) &&& regions.slot_owners[sub_idx].usage !is PageTable &&& regions.slot_owners[sub_idx].usage !is MMIO ==> { - &&& regions.slot_owners[sub_idx].ref_count() != REF_COUNT_UNUSED - &&& regions.slot_owners[sub_idx].ref_count() > 0 - &&& regions.slot_owners[sub_idx].ref_count() <= REF_COUNT_MAX + &&& regions.ref_count(sub_idx) != REF_COUNT_UNUSED + &&& regions.ref_count(sub_idx) > 0 + &&& regions.ref_count(sub_idx) <= REF_COUNT_MAX } }, regions.slots.contains_key(frame_to_index(pa)), @@ -1153,9 +1153,9 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { let sub_idx = frame_to_index((small_pa + j_prime * PAGE_SIZE) as usize); &&& regions.slots.contains_key(sub_idx) &&& regions.slot_owners[sub_idx].usage !is MMIO ==> { - &&& regions.slot_owners[sub_idx].ref_count() != REF_COUNT_UNUSED - &&& regions.slot_owners[sub_idx].ref_count() > 0 - &&& regions.slot_owners[sub_idx].ref_count() <= REF_COUNT_MAX + &&& regions.ref_count(sub_idx) != REF_COUNT_UNUSED + &&& regions.ref_count(sub_idx) > 0 + &&& regions.ref_count(sub_idx) <= REF_COUNT_MAX } } by { let sub_pages_per_subframe = page_size((level - 1) as PagingLevel) @@ -1570,10 +1570,10 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { forall|k: int| old(regions).slots.contains_key(k) ==> #[trigger] final(regions).slots.contains_key(k), forall|slot: int| - #![trigger final(regions).slot_owners[slot].ref_count()] - final(regions).slot_owners[slot].ref_count() == old( + #![trigger final(regions).ref_count(slot)] + final(regions).ref_count(slot) == old( regions, - ).slot_owners[slot].ref_count(), + ).ref_count(slot), forall|slot: int| #![trigger final(regions).slot_owners[slot].ref_count_perm] final(regions).slot_owners[slot].same_permissions( diff --git a/ostd/src/mm/page_table/node/mod.rs b/ostd/src/mm/page_table/node/mod.rs index 4db791066..fe011d84f 100644 --- a/ostd/src/mm/page_table/node/mod.rs +++ b/ostd/src/mm/page_table/node/mod.rs @@ -272,7 +272,7 @@ impl PageTableNode { meta_to_frame(owner@.value().node().meta_vaddr())), owner@.value().metaregion_sound(*final(regions)), forall|i: int| - #[trigger] old(regions).slot_owners[i].ref_count() != REF_COUNT_UNUSED + #[trigger] old(regions).ref_count(i) != REF_COUNT_UNUSED ==> i != meta_to_index(owner@.value().node().meta_vaddr()), owner@.value().match_pte(C::E::new_pt_spec(meta_to_frame(owner@.value().node().meta_vaddr())), level as PagingLevel), final(parent_owner).meta_own == old(parent_owner).meta_own, diff --git a/ostd/src/util/mod.rs b/ostd/src/util/mod.rs index 03282f0c8..2c6cadffe 100644 --- a/ostd/src/util/mod.rs +++ b/ostd/src/util/mod.rs @@ -2,7 +2,7 @@ //! Utility types and methods. mod either; //mod macros; -//pub(crate) mod ops; +pub(crate) mod ops; pub(crate) mod range_alloc; pub use either::Either; diff --git a/ostd/src/util/ops.rs b/ostd/src/util/ops.rs index 5a8611523..62dbedfa2 100644 --- a/ostd/src/util/ops.rs +++ b/ostd/src/util/ops.rs @@ -1,6 +1,81 @@ // SPDX-License-Identifier: MPL-2.0 +use vstd::{ + laws_cmp::{ + obeys_cmp, obeys_cmp_ord, obeys_cmp_partial_ord, obeys_partial_cmp_spec_properties, + }, + laws_eq::obeys_eq_spec_properties, + prelude::*, + set_lib::{FiniteRange, range_set_properties}, + std_specs::{ + cmp::PartialOrdIs, + iter::{IteratorSpec, filter_keep, filter_post, filter_postcondition}, + }, +}; +use vstd_extra::{ + external::{cmp::*, iter::*, range::*}, + range::{ + RangeExtraFns, finite_range_matches_ord, lemma_seq_range_union_contains, seq_range_union, + }, +}; + use core::ops::Range; +verus! { + +/// Operational spec of `range_difference`. +pub open spec fn range_difference_spec(a: Range, b: Range) -> Seq> { + let left = if !b.start.is_lt(&b.end) { + a + } else { + Range { start: a.start, end: spec_ord_min(a.end, b.start) } + }; + let right = if !b.start.is_lt(&b.end) { + b + } else { + Range { start: spec_ord_max(a.start, b.end), end: a.end } + }; + if left.start.is_lt(&left.end) { + if right.start.is_lt(&right.end) { + seq![left, right] + } else { + seq![left] + } + } else if right.start.is_lt(&right.end) { + seq![right] + } else { + Seq::empty() + } +} + +pub proof fn lemma_range_difference_set(a: Range, b: Range) + requires + obeys_cmp::(), + finite_range_matches_ord::(), + ensures + seq_range_union(range_difference_spec(a, b)) == a.view_set().difference(b.view_set()), +{ + broadcast use range_set_properties; + + reveal(obeys_partial_cmp_spec_properties); + reveal(obeys_cmp_ord); + reveal(obeys_eq_spec_properties); + let s = range_difference_spec(a, b); + assert forall|x: T| #[trigger] + seq_range_union(s).contains(x) <==> ((s.len() >= 1 && s[0].view_set().contains(x)) || ( + s.len() == 2 && s[1].view_set().contains(x))) by { + lemma_seq_range_union_contains(s, x); + let pred = |r: Range| r.view_set().contains(x); + if s.any(pred) { + let i = choose|i: int| #![auto] 0 <= i < s.len() && pred(s[i]); + assert(i == 0 || i == 1); + } + } + assert forall|x: T| + #![trigger a.view_set().contains(x)] + seq_range_union(s).contains(x) <==> a.view_set().difference(b.view_set()).contains(x) by {} +} + +} // verus! /// Calculates the [difference] of two [`Range`]s, i.e., `a - b`. /// /// This method will return 0, 1, or 2 ranges. All returned ranges are @@ -8,19 +83,60 @@ use core::ops::Range; /// will be sorted in ascending order. /// /// [difference]: https://en.wikipedia.org/wiki/Set_(mathematics)#Set_difference -pub fn range_difference( +#[verus_verify(spinoff_prover, rlimit(50))] +#[verus_spec(ret => + requires + obeys_cmp::(), + finite_range_matches_ord::(), + ensures + ret.obeys_prophetic_iter_laws() && ret.will_return_none() ==> { + &&& ret.remaining() == range_difference_spec(*a, *b) + &&& ret.remaining().len() <= 2 + &&& ret.remaining().all( + |range: Range| range.start.is_lt(&range.end), + ) + &&& forall|i: int| + 0 <= i < ret.remaining().len() - 1 ==> ( + #[trigger] ret.remaining()[i]).end.is_le(&ret.remaining()[i + 1].start) + &&& seq_range_union(ret.remaining()) == a.view_set().difference(b.view_set()) + }, +)] +pub fn range_difference( a: &Range, b: &Range, ) -> impl Iterator> { use core::cmp::{max, min}; + proof! { + reveal(obeys_cmp_partial_ord); + reveal(obeys_cmp_ord); + reveal(obeys_partial_cmp_spec_properties); + reveal_with_fuel(Seq::filter_index, 3); + } let r = if b.is_empty() { [a.clone(), b.clone()] } else { [a.start..min(a.end, b.start), max(a.start, b.end)..a.end] }; - r.into_iter().filter(|v| !v.is_empty()) + // Original execution: `r.into_iter().filter(|v| !v.is_empty())`. + // Name the operands to instantiate `filter_postcondition` explicitly. + let iter = r.into_iter(); + let pred = #[verus_spec(keep: bool => + ensures + keep == v.start.is_lt(&v.end), + )] + |v: &Range| !v.is_empty(); + proof! { + lemma_range_difference_set(*a, *b); + assert forall|ret: core::iter::Filter<_, _>| + #[trigger] filter_post(iter, pred, ret) && + ret.will_return_none() implies + ret.remaining() == range_difference_spec(*a, *b) by { + filter_postcondition(iter, pred, ret); + } + } + iter.filter(pred) } #[cfg(ktest)] diff --git a/verified_libs/vstd_extra/src/external/cmp.rs b/verified_libs/vstd_extra/src/external/cmp.rs new file mode 100644 index 000000000..00cc5613c --- /dev/null +++ b/verified_libs/vstd_extra/src/external/cmp.rs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Specifications for the free comparison functions missing from `vstd`. +use vstd::{prelude::*, std_specs::cmp::OrdSpec}; + +use core::cmp::Ordering; + +verus! { + +/// Returns `y` when it compares less than `x`, and returns `x` otherwise. +pub open spec fn spec_ord_min(x: T, y: T) -> T { + match y.cmp_spec(&x) { + Ordering::Less => y, + Ordering::Equal => x, + Ordering::Greater => x, + } +} + +/// Returns `x` when `y` compares less than it, and returns `y` otherwise. +pub open spec fn spec_ord_max(x: T, y: T) -> T { + match y.cmp_spec(&x) { + Ordering::Less => x, + Ordering::Equal => y, + Ordering::Greater => y, + } +} + +/// Returns the minimum, choosing the first argument when they compare equal. +/// See [`std::cmp::min`](https://doc.rust-lang.org/std/cmp/fn.min.html). +#[verifier::when_used_as_spec(spec_ord_min)] +pub assume_specification[ core::cmp::min ](x: T, y: T) -> (ret: T) + ensures + T::obeys_cmp_spec() ==> ret == spec_ord_min(x, y), +; + +/// Returns the maximum, choosing the second argument when they compare equal. +/// See [`std::cmp::max`](https://doc.rust-lang.org/std/cmp/fn.max.html). +#[verifier::when_used_as_spec(spec_ord_max)] +pub assume_specification[ core::cmp::max ](x: T, y: T) -> (ret: T) + ensures + T::obeys_cmp_spec() ==> ret == spec_ord_max(x, y), +; + +} // verus! diff --git a/verified_libs/vstd_extra/src/external/iter.rs b/verified_libs/vstd_extra/src/external/iter.rs new file mode 100644 index 000000000..273ee9808 --- /dev/null +++ b/verified_libs/vstd_extra/src/external/iter.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Specification for owned-array iteration not yet modeled by `vstd`. +use vstd::{prelude::*, std_specs::iter::IteratorSpec}; + +verus! { + +/// Verus proxy for the standard library's array `IntoIter`. +#[verifier::external_type_specification] +#[verifier::external_body] +#[verifier::reject_recursive_types(T)] +pub struct ExArrayIntoIter(core::array::IntoIter); + +/// The array iterator yields the array view from left to right and terminates. +/// See [`array::into_iter`](https://doc.rust-lang.org/std/primitive.array.html#method.into_iter). +pub assume_specification[ <[T; N] as IntoIterator>::into_iter ]( + array: [T; N], +) -> (iter: <[T; N] as IntoIterator>::IntoIter) + ensures + IteratorSpec::obeys_prophetic_iter_laws(&iter), + IteratorSpec::will_return_none(&iter), + IteratorSpec::remaining(&iter) == array@, + IteratorSpec::decrease(&iter) == Some(N as nat), +; + +} // verus! diff --git a/verified_libs/vstd_extra/src/external/mod.rs b/verified_libs/vstd_extra/src/external/mod.rs index f4071583b..d9ae589f6 100644 --- a/verified_libs/vstd_extra/src/external/mod.rs +++ b/verified_libs/vstd_extra/src/external/mod.rs @@ -3,10 +3,12 @@ //! These specifications are determined with careful inspection of the std library source code and documentation, and trusted as TCB. //! They are subject to change if `vstd` covers more cases in the future. pub mod btree; +pub mod cmp; pub mod convert; pub mod deref; pub mod ilog2; pub mod int_specs; +pub mod iter; pub mod nonnull; pub mod ptr; pub mod range; @@ -15,8 +17,10 @@ pub mod smart_ptr; pub mod time; pub use btree::*; +pub use cmp::*; pub use ilog2::*; pub use int_specs::*; +pub use iter::*; pub use nonnull::*; pub use ptr::*; pub use range::*; diff --git a/verified_libs/vstd_extra/src/external/range.rs b/verified_libs/vstd_extra/src/external/range.rs index 9703a16cb..62b66a712 100644 --- a/verified_libs/vstd_extra/src/external/range.rs +++ b/verified_libs/vstd_extra/src/external/range.rs @@ -1,34 +1,12 @@ +use vstd::{ + prelude::*, + std_specs::cmp::{PartialOrdIs, PartialOrdSpec}, +}; + use core::ops::{Range, RangeInclusive}; -use vstd::prelude::*; verus! { -/// Length of a `Range`. Malformed ranges (`start > end`) are length 0, -/// matching `ExactSizeIterator::len` for `Range` where `Step::steps_between` -/// returns `None` on `end < start`, collapsed to 0. -pub open spec fn range_usize_len_spec(r: &Range) -> usize { - if r.start < r.end { - (r.end - r.start) as usize - } else { - 0usize - } -} - -/// Exec-mode `len` for a `Range`: use in place of `r.len()` which is an -/// `ExactSizeIterator` provided method and can't be specced with -/// `assume_specification`. -#[verifier::when_used_as_spec(range_usize_len_spec)] -pub fn range_usize_len(r: &Range) -> (ret: usize) - ensures - ret == range_usize_len_spec(r), -{ - if r.start < r.end { - r.end - r.start - } else { - 0 - } -} - /// Whether a `Range` is empty. Malformed ranges (`start > end`) are empty. pub open spec fn range_usize_is_empty_spec(r: &Range) -> bool { !(r.start < r.end) @@ -45,6 +23,13 @@ pub fn range_usize_is_empty(r: &Range) -> (ret: bool) !(r.start < r.end) } +/// See [`Range::is_empty`](https://doc.rust-lang.org/std/ops/struct.Range.html#method.is_empty). +pub assume_specification>[ Range::::is_empty ](r: &Range) -> (res: + bool) where Idx: PartialOrd + ensures + >::obeys_partial_cmp_spec() ==> res == !r.start.is_lt(&r.end), +; + pub assume_specification[ RangeInclusive::start ](r: &RangeInclusive) -> (ret: &Idx) ensures *ret == r@.start, diff --git a/verified_libs/vstd_extra/src/lib.rs b/verified_libs/vstd_extra/src/lib.rs index 91fe86a59..9fa472683 100644 --- a/verified_libs/vstd_extra/src/lib.rs +++ b/verified_libs/vstd_extra/src/lib.rs @@ -37,6 +37,7 @@ pub mod trans_macros; pub mod map_extra; pub mod prelude; +pub mod range; pub mod raw_ptr_extra; pub mod rcu_read_lease; pub mod resource_invariant; diff --git a/verified_libs/vstd_extra/src/range.rs b/verified_libs/vstd_extra/src/range.rs new file mode 100644 index 000000000..1cc276358 --- /dev/null +++ b/verified_libs/vstd_extra/src/range.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Finite-set models and proof lemmas for half-open ranges. +use vstd::{prelude::*, set_lib::FiniteRange, std_specs::cmp::PartialOrdIs}; + +use core::ops::Range; + +verus! { + +/// Specification helpers for half-open ranges. +pub trait RangeExtraFns { + /// The finite set denoted by this range. + spec fn view_set(self) -> Set; +} + +impl RangeExtraFns for Range { + open spec fn view_set(self) -> Set { + T::range_set(self.start, self.end) + } +} + +/// The union of the sets denoted by a sequence of ranges. +pub open spec fn seq_range_union(s: Seq>) -> Set { + s.map_values(|r: Range| r.view_set()).to_set().flatten() +} + +/// Whether the finite-range model agrees with the ordering model. +pub open spec fn finite_range_matches_ord() -> bool { + forall|x: T, lo: T, hi: T| T::in_range(x, lo, hi) <==> lo.is_le(&x) && x.is_lt(&hi) +} + +/// An element belongs to the union of a sequence of ranges if and only if it +/// belongs to at least one of those ranges. +pub proof fn lemma_seq_range_union_contains(s: Seq>, x: T) + ensures + seq_range_union(s).contains(x) <==> s.any(|r: Range| r.view_set().contains(x)), +{ + broadcast use {Seq::to_set_ensures, Set::lemma_flatten_contains}; + + let pred = |r: Range| r.view_set().contains(x); + let range_sets = s.map_values(|r: Range| r.view_set()); + + if seq_range_union(s).contains(x) { + range_sets.to_set().lemma_flatten_contains(x); + let range_set = choose|range_set: Set| + #![trigger range_sets.to_set().contains(range_set)] + range_sets.to_set().contains(range_set) && range_set.contains(x); + let i = choose|i: int| 0 <= i < range_sets.len() && range_sets[i] == range_set; + + assert(pred(s[i])); + assert(s.any(pred)); + } else if s.any(pred) { + let i = choose|i: int| #![auto] 0 <= i < s.len() && pred(s[i]); + + assert(range_sets.to_set().contains(range_sets[i])); + range_sets.to_set().lemma_flatten_contains(x); + } +} + +} // verus! From 64f9d499305b6ff1c052b8696ccb56a1b07f67f0 Mon Sep 17 00:00:00 2001 From: Je5s1e Date: Mon, 14 Sep 2026 17:46:56 +0800 Subject: [PATCH 4/5] resolve conflicts --- ostd/specs/mm/page_table/cursor/owners.rs | 6 +++--- ostd/src/mm/page_table/cursor/mod.rs | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ostd/specs/mm/page_table/cursor/owners.rs b/ostd/specs/mm/page_table/cursor/owners.rs index 6f953634e..b8ddf734e 100644 --- a/ostd/specs/mm/page_table/cursor/owners.rs +++ b/ostd/specs/mm/page_table/cursor/owners.rs @@ -1860,10 +1860,10 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { res == OwnerSubtree::new_val(res.value(), res.level() as nat), { let cont = self.continuations[self.level - 1]; - self.inv_continuation(self.level - 1); + self.lemma_inv_continuation(self.level - 1); - cont.inv_children_unroll(cont.idx as int); - cont.inv_children_rel_unroll(cont.idx as int); + cont.lemma_inv_children_unroll(cont.idx as int); + cont.lemma_inv_children_rel_unroll(cont.idx as int); let tracked entry = EntryOwner::tracked_new_absent(self.cur_entry_owner().path, self.level); diff --git a/ostd/src/mm/page_table/cursor/mod.rs b/ostd/src/mm/page_table/cursor/mod.rs index 363e8ab2b..5109bf113 100644 --- a/ostd/src/mm/page_table/cursor/mod.rs +++ b/ostd/src/mm/page_table/cursor/mod.rs @@ -3178,6 +3178,9 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { let tracked subtree = owner.tracked_new_absent_subtree(); proof { + assert(subtree.value().inv()) by { + reveal(TreeNode::inv); + }; owner.absent_not_in_tree(subtree.value()); } @@ -3225,6 +3228,12 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> CursorMut<'rcu, C, A> { assert(cur_st.value().inv()) by { reveal(TreeNode::inv); }; + owner_before_replace.lemma_inv_continuation(owner_before_replace.level - 1); + let cont = owner_before_replace.continuations[owner_before_replace.level - 1]; + assert(cont.all_some()) by { + reveal(::inv); + }; + cont.lemma_inv_children_rel_unroll(cont.idx as int); owner_before_replace.lemma_new_child_mappings_eq_target( cur_st, cur_st.value().frame().mapped_pa, From 02f2f3569b079e5742649e65ef6c4af3dbdaf5d2 Mon Sep 17 00:00:00 2001 From: Je5s1e Date: Tue, 15 Sep 2026 15:57:54 +0800 Subject: [PATCH 5/5] address architecture paging review feedback --- dv | 2 +- ostd/specs/arch/mod.rs | 2 +- ostd/specs/arch/x86/mod.rs | 5 +---- ostd/specs/mm/page_table/owners.rs | 31 ++++++++++++++-------------- ostd/src/arch/x86/mm/mod.rs | 9 +------- ostd/src/mm/page_table/cursor/mod.rs | 4 ++++ 6 files changed, 23 insertions(+), 30 deletions(-) diff --git a/dv b/dv index 6d6502d14..a8834cbb0 160000 --- a/dv +++ b/dv @@ -1 +1 @@ -Subproject commit 6d6502d142b049f691055d9de76709ef47c664d6 +Subproject commit a8834cbb0dc227e83d05033a452dc95875d94fd5 diff --git a/ostd/specs/arch/mod.rs b/ostd/specs/arch/mod.rs index d9940a21d..929d2d772 100644 --- a/ostd/specs/arch/mod.rs +++ b/ostd/specs/arch/mod.rs @@ -4,7 +4,7 @@ pub use model::*; // Compatibility re-exports for proof modules that still use `specs::arch`. // The authoritative values live in the executable memory/architecture modules. pub use crate::{ - arch::mm::{NR_ENTRIES, NR_LEVELS}, + arch::mm::{NR_ENTRIES, NR_LEVELS, PAGE_SIZE}, mm::{MAX_NR_PAGES, MAX_PADDR}, }; diff --git a/ostd/specs/arch/x86/mod.rs b/ostd/specs/arch/x86/mod.rs index 204850d21..aba148894 100644 --- a/ostd/specs/arch/x86/mod.rs +++ b/ostd/specs/arch/x86/mod.rs @@ -5,7 +5,7 @@ use vstd_extra::prelude::*; use super::model::{self, ArchAddressSpaceModel, ArchPagingModel}; -use crate::arch::mm::{NR_ENTRIES, NR_LEVELS}; +use crate::arch::mm::{NR_ENTRIES, NR_LEVELS, PAGE_SIZE}; use crate::specs::mm::{ frame::mapping::lemma_meta_to_frame_soundness, page_table::{nr_pte_index_bits_spec, pte_index_bit_offset_spec}, @@ -26,9 +26,6 @@ global size_of usize == 8; global size_of isize == 8; -/// Page size used by the current verification target. -pub const PAGE_SIZE: usize = crate::arch::mm::x86_base_page_size!(); - pub open spec fn valid_frame_paddr(paddr: Paddr) -> bool { model::valid_frame_paddr_for::(paddr) } diff --git a/ostd/specs/mm/page_table/owners.rs b/ostd/specs/mm/page_table/owners.rs index 646b258a2..293061877 100644 --- a/ostd/specs/mm/page_table/owners.rs +++ b/ostd/specs/mm/page_table/owners.rs @@ -19,24 +19,23 @@ use vstd_extra::{ prelude::TreeNodeValue, }; -use crate::{ +use crate::mm::{ + Paddr, PagingConstsTrait, PagingLevel, Vaddr, + frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, + page_size, page_size_spec, + page_table::{EntryOwner, EntryOwnerKind, PageTableEntryTrait, PageTableGuard}, +}; + +use crate::specs::{ + arch::*, mm::{ - Paddr, PagingConstsTrait, PagingLevel, Vaddr, - frame::meta::{REF_COUNT_MAX, REF_COUNT_UNIQUE, REF_COUNT_UNUSED}, - page_size, page_size_spec, - page_table::{EntryOwner, EntryOwnerKind, PageTableEntryTrait, PageTableGuard}, - }, - specs::{ - arch::*, - mm::{ - frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners}, - page_table::{ - cursor::page_size_lemmas::{ - lemma_nr_entries_times_sub_page_size, lemma_page_size_divides, - lemma_page_size_ge_page_size, lemma_page_size_spec_values, - }, - *, + frame::{mapping::frame_to_index, meta_region_owners::MetaRegionOwners}, + page_table::{ + cursor::page_size_lemmas::{ + lemma_nr_entries_times_sub_page_size, lemma_page_size_divides, + lemma_page_size_ge_page_size, lemma_page_size_spec_values, }, + *, }, }, }; diff --git a/ostd/src/arch/x86/mm/mod.rs b/ostd/src/arch/x86/mm/mod.rs index d30b383cf..0161ffe93 100644 --- a/ostd/src/arch/x86/mm/mod.rs +++ b/ostd/src/arch/x86/mm/mod.rs @@ -14,13 +14,6 @@ use core::ops::Range; pub(crate) use util::{__memcpy_fallible, __memset_fallible}; //use x86_64::{instructions::tlb, structures::paging::PhysFrame, VirtAddr}; -macro_rules! x86_base_page_size { - () => { - 4096usize - }; -} -pub(crate) use x86_base_page_size; - use crate::{ mm::{ page_prop::{CachePolicy, PageFlags, PageProperty, PrivilegedPageFlags as PrivFlags}, @@ -35,7 +28,7 @@ mod util; verus! { /// Size of a base page on x86-64. -pub const PAGE_SIZE: usize = x86_base_page_size!(); +pub const PAGE_SIZE: usize = 4096; /// Size of an x86-64 page-table entry. pub const PTE_SIZE: usize = 8; diff --git a/ostd/src/mm/page_table/cursor/mod.rs b/ostd/src/mm/page_table/cursor/mod.rs index 5109bf113..664f6fd0f 100644 --- a/ostd/src/mm/page_table/cursor/mod.rs +++ b/ostd/src/mm/page_table/cursor/mod.rs @@ -598,6 +598,10 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { EntryOwner::::axiom_frame_is_tracked_iff_not_mmio( owner_before_permission_take.cur_entry_owner(), ); + // Relate the resolved frame to the query's initial panic condition. + assert(old(owner)@.query_mapping().pa_range.start == pa); + old(regions).lemma_contains_valid_frame_paddr(pa); + assert(old(regions).ref_count(idx) == regions.ref_count(idx)); } owner_before_permission_take.lemma_cur_frame_clone_requires( item,