From d24ab2d483ee5e4a11e169ea39c0aaec1e23fd0f Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 12 Aug 2026 20:13:26 +0800 Subject: [PATCH 01/30] prove: arch::pci (x86) --- ostd/src/arch/x86/device/io_port.rs | 34 ++++++++++++++ ostd/src/arch/x86/device/mod.rs | 4 +- ostd/src/arch/x86/mod.rs | 12 ++--- ostd/src/arch/x86/pci.rs | 36 +++++++++++++++ ostd/src/bus/mod.rs | 4 +- ostd/src/bus/pci/device_info.rs | 7 ++- ostd/src/bus/pci/mod.rs | 9 ++-- ostd/src/io/io_mem/allocator.rs | 20 ++++++-- ostd/src/io/io_mem/mod.rs | 71 +++++++++++++++++++++++++---- ostd/src/io/io_port/mod.rs | 34 +++++++++++--- ostd/src/io/mod.rs | 3 +- ostd/src/lib.rs | 16 +++---- ostd/src/prelude.rs | 4 +- 13 files changed, 211 insertions(+), 43 deletions(-) diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index 74f7bbadf..c86716257 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -7,3 +7,37 @@ pub use x86_64::{ }, structures::port::{PortRead, PortWrite}, }; + +use vstd::prelude::*; + +verus! { + +/// Opaque specification boundary for the third-party read/write access marker. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExReadWriteAccess(ReadWriteAccess); + +/// Opaque specification boundary for the third-party write-only access marker. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExWriteOnlyAccess(WriteOnlyAccess); + +/// Trusted specification boundary for values that can be read from an x86 I/O port. +#[verifier::external_trait_specification] +pub trait ExPortRead { + type ExternalTraitSpecificationFor: PortRead; + + /// A port read can produce any value supplied by the device. + unsafe fn read_from_port(port: u16) -> Self where Self: Sized; +} + +/// Trusted specification boundary for values that can be written to an x86 I/O port. +#[verifier::external_trait_specification] +pub trait ExPortWrite { + type ExternalTraitSpecificationFor: PortWrite; + + /// A port write has no modeled logical effect on kernel memory. + unsafe fn write_to_port(port: u16, value: Self) where Self: Sized; +} + +} // verus! diff --git a/ostd/src/arch/x86/device/mod.rs b/ostd/src/arch/x86/device/mod.rs index 5d3b7fe3c..5e260d494 100644 --- a/ostd/src/arch/x86/device/mod.rs +++ b/ostd/src/arch/x86/device/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 //! Device-related APIs. //! This module mainly contains the APIs that should exposed to the device driver like PCI, RTC -pub mod cmos; +/*pub mod cmos;*/ pub mod io_port; -pub mod serial; +/*pub mod serial;*/ diff --git a/ostd/src/arch/x86/mod.rs b/ostd/src/arch/x86/mod.rs index df7a81de1..ec85da40d 100644 --- a/ostd/src/arch/x86/mod.rs +++ b/ostd/src/arch/x86/mod.rs @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MPL-2.0 //! Platform-specific code for the x86 platform. /*pub mod boot; -pub(crate) mod cpu; +pub(crate) mod cpu;*/ pub mod device; -pub(crate) mod ex_table; + +/*pub(crate) mod ex_table; pub(crate) mod io; pub(crate) mod iommu;*/ pub(crate) mod irq; /* pub(crate) mod kernel; */ pub(crate) mod mm; -/*pub(crate) mod pci; -pub mod qemu; +pub(crate) mod pci; +/*pub mod qemu; pub(crate) mod serial; pub(crate) mod task; */ pub mod timer; @@ -212,7 +213,7 @@ pub(crate) fn enable_cpu_features() { *efer |= EferFlags::NO_EXECUTE_ENABLE; }); } -} +}*/ /// Inserts a TDX-specific code block. /// @@ -251,4 +252,3 @@ macro_rules! if_tdx_enabled { } pub use if_tdx_enabled; -*/ diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index e212be1f7..33832d995 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -1,30 +1,58 @@ // SPDX-License-Identifier: MPL-2.0 //! PCI bus access +use vstd::prelude::*; + use super::device::io_port::{ReadWriteAccess, WriteOnlyAccess}; use crate::{bus::pci::PciDeviceLocation, io::IoPort, prelude::*}; +verus! { + +/// x86 is little-endian, so converting a native-endian `u32` to little endian is the identity. +pub assume_specification[ u32::to_le ](value: u32) -> (result: u32) + ensures + result == value, +; + +} // verus! +#[verus_verify] static PCI_ADDRESS_PORT: IoPort = unsafe { IoPort::new(0x0CF8) }; +#[verus_verify] static PCI_DATA_PORT: IoPort = unsafe { IoPort::new(0x0CFC) }; +#[verus_verify] const BIT32_ALIGN_MASK: u32 = 0xFFFC; +#[verus_verify] +#[verus_spec(result => ensures result is Ok)] pub(crate) fn write32(location: &PciDeviceLocation, offset: u32, value: u32) -> Result<()> { PCI_ADDRESS_PORT.write(encode_as_port(location) | (offset & BIT32_ALIGN_MASK)); PCI_DATA_PORT.write(value.to_le()); Ok(()) } +#[verus_verify] +#[verus_spec(result => ensures result is Ok)] pub(crate) fn read32(location: &PciDeviceLocation, offset: u32) -> Result { PCI_ADDRESS_PORT.write(encode_as_port(location) | (offset & BIT32_ALIGN_MASK)); Ok(PCI_DATA_PORT.read().to_le()) } +#[verus_verify] +#[verus_spec(returns true)] pub(crate) fn has_pci_bus() -> bool { true } +#[verus_verify] pub(crate) const MSIX_DEFAULT_MSG_ADDR: u32 = 0xFEE0_0000; +#[verus_verify] +#[verus_spec(address => + ensures + address == MSIX_DEFAULT_MSG_ADDR | 0b1_1000 + | ((remapping_index & 0x7FFF) << 5) + | ((remapping_index & 0x8000) >> 13), +)] pub(crate) fn construct_remappable_msix_address(remapping_index: u32) -> u32 { // Use remappable format. The bits[4:3] should be always set to 1 according to the manual. let mut address = MSIX_DEFAULT_MSG_ADDR | 0b1_1000; @@ -37,6 +65,14 @@ pub(crate) fn construct_remappable_msix_address(remapping_index: u32) -> u32 { } /// Encodes the bus, device, and function into a port address for use with the PCI I/O port. +#[verus_verify] +#[verus_spec(port => + ensures + port == (1u32 << 31) + | ((location.bus as u32) << 16) + | (((location.device as u32) & 0b11111) << 11) + | (((location.function as u32) & 0b111) << 8), +)] fn encode_as_port(location: &PciDeviceLocation) -> u32 { // 1 << 31: Configuration enable (1 << 31) diff --git a/ostd/src/bus/mod.rs b/ostd/src/bus/mod.rs index 7b81924ce..b8898deb6 100644 --- a/ostd/src/bus/mod.rs +++ b/ostd/src/bus/mod.rs @@ -11,7 +11,7 @@ pub enum BusProbeError { ConfigurationSpaceError, } -/// Initializes the bus +/*/// Initializes the bus pub(crate) fn init() { pci::init(); -} +}*/ diff --git a/ostd/src/bus/pci/device_info.rs b/ostd/src/bus/pci/device_info.rs index 3d415c987..3bcc6c1a0 100644 --- a/ostd/src/bus/pci/device_info.rs +++ b/ostd/src/bus/pci/device_info.rs @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 //! PCI device Information +use vstd::prelude::*; + use core::iter; -use super::cfg_space::PciDeviceCommonCfgOffset; +/*use super::cfg_space::PciDeviceCommonCfgOffset;*/ +/* /// PCI device ID #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct PciDeviceId { @@ -48,8 +51,10 @@ impl PciDeviceId { } } } +*/ /// PCI device Location +#[verus_verify] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PciDeviceLocation { /// Bus number diff --git a/ostd/src/bus/pci/mod.rs b/ostd/src/bus/pci/mod.rs index 0de48b517..261ab6202 100644 --- a/ostd/src/bus/pci/mod.rs +++ b/ostd/src/bus/pci/mod.rs @@ -48,14 +48,15 @@ //! PCI_BUS.lock().register_driver(driver_a); //! } //! ``` -pub mod bus; +/*pub mod bus; pub mod capability; pub mod cfg_space; -pub mod common_device; +pub mod common_device;*/ mod device_info; -pub use device_info::{PciDeviceId, PciDeviceLocation}; +pub use device_info::{/* PciDeviceId, */ PciDeviceLocation}; +/* use self::{bus::PciBus, common_device::PciCommonDevice}; use crate::{arch::pci::has_pci_bus, sync::Mutex}; @@ -74,4 +75,4 @@ pub(crate) fn init() { }; lock.register_common_device(device); } -} +}*/ diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 0cd42a4e9..0e2ca94b1 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O Memory allocator. +use vstd::prelude::*; + use alloc::vec::Vec; use core::ops::Range; @@ -13,6 +15,7 @@ use crate::{ }; /// I/O memory allocator that allocates memory I/O access to device drivers. +#[verus_verify] pub struct IoMemAllocator { allocators: Vec, } @@ -29,7 +32,8 @@ impl IoMemAllocator { debug!("Acquiring MMIO range:{:x?}..{:x?}", range.start, range.end); // SAFETY: The created `IoMem` is guaranteed not to access physical memory or system device I/O. - unsafe { Some(IoMem::new(range, PageFlags::RW, CachePolicy::Uncacheable)) } + // Original Rust used the upstream bitflags-style associated constant `PageFlags::RW`. + unsafe { Some(IoMem::new(range, PageFlags::RW(), CachePolicy::Uncacheable)) } } /// Recycles an MMIO range. @@ -51,6 +55,7 @@ impl IoMemAllocator { /// # Safety /// /// User must ensure the range doesn't belong to physical memory or system device I/O. + #[verus_verify] unsafe fn new(allocators: Vec) -> Self { Self { allocators } } @@ -60,6 +65,7 @@ impl IoMemAllocator { /// /// The builder must contains the memory I/O regions that don't belong to the physical memory. Also, OSTD /// must exclude the memory I/O regions of the system device before building the `IoMemAllocator`. +#[verus_verify] pub(crate) struct IoMemAllocatorBuilder { allocators: Vec, } @@ -70,11 +76,12 @@ impl IoMemAllocatorBuilder { /// # Safety /// /// User must ensure the range doesn't belong to physical memory. + #[verus_verify] pub(crate) unsafe fn new(ranges: Vec>) -> Self { - info!( + /* info!( "Creating new I/O memory allocator builder, ranges: {:#x?}", ranges - ); + ); */ let mut allocators = Vec::with_capacity(ranges.len()); for range in ranges { allocators.push(RangeAllocator::new(range)); @@ -116,17 +123,24 @@ pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { IO_MEM_ALLOCATOR.call_once(|| unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); } +#[verus_verify] fn find_allocator<'a>( allocators: &'a [RangeAllocator], range: &Range, ) -> Option<&'a RangeAllocator> { for allocator in allocators.iter() { let allocator_range = allocator.fullrange(); + // Verus does not yet support `continue` in `for` loops. Original Rust: + /* if allocator_range.start >= range.end || allocator_range.end <= range.start { continue; } return Some(allocator); + */ + if allocator_range.start < range.end && allocator_range.end > range.start { + return Some(allocator); + } } None } diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index 3afa2d077..3ad8b3087 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -1,5 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O memory and its allocator that allocates memory I/O (MMIO) to device drivers. +use crate::specs::arch::PAGE_SIZE; +use crate::specs::{mm::virt_mem::VirtPtr, task::AnyAtomicGuard}; +use vstd::prelude::*; + mod allocator; use core::ops::{Deref, Range}; @@ -11,8 +15,8 @@ pub(super) use self::allocator::init; use crate::{ Error, mm::{ - FallibleVmRead, FallibleVmWrite, HasPaddr, Infallible, PAGE_SIZE, Paddr, PodOnce, VmIo, - VmIoOnce, VmReader, VmWriter, + FallibleVmRead, FallibleVmWrite, HasPaddr, Infallible, /*PAGE_SIZE,*/ Paddr, PodOnce, + VmIo, VmIoOnce, VmReader, VmWriter, kspace::kvirt_area::KVirtArea, page_prop::{CachePolicy, PageFlags, PageProperty, PrivilegedPageFlags}, }, @@ -21,6 +25,7 @@ use crate::{ /// I/O memory. #[derive(Debug, Clone)] +#[verus_verify] pub struct IoMem { kvirt_area: Arc, // The actually used range for MMIO is `kvirt_area.start + offset..kvirt_area.start + offset + limit` @@ -29,7 +34,29 @@ pub struct IoMem { pa: Paddr, } +verus! { + +impl IoMem { + /// Logical physical-address projection used by verified callers. + pub closed spec fn paddr_spec(&self) -> Paddr { + self.pa + } + + /// Logical byte length used by verified callers. + pub closed spec fn length_spec(&self) -> usize { + self.limit + } + + /// Logical offset into the page-aligned mapping. + pub closed spec fn offset_spec(&self) -> usize { + self.offset + } +} + +} // verus! +#[verus_verify] impl HasPaddr for IoMem { + #[verus_spec(result => ensures result == self.paddr_spec())] fn paddr(&self) -> Paddr { self.pa } @@ -46,11 +73,15 @@ impl IoMem { } /// Returns the physical address of the I/O memory. + #[verus_verify] + #[verus_spec(result => ensures result == self.paddr_spec())] pub fn paddr(&self) -> Paddr { self.pa } /// Returns the length of the I/O memory region. + #[verus_verify] + #[verus_spec(result => ensures result == self.length_spec())] pub fn length(&self) -> usize { self.limit } @@ -60,15 +91,31 @@ impl IoMem { /// # Panics /// /// This method will panic if the range is empty or out of bounds. + #[verus_verify] + #[verus_spec(result => + requires + range.start < range.end, + range.end <= self.length_spec(), + self.offset_spec() + range.start <= usize::MAX, + self.paddr_spec() + range.start <= usize::MAX, + ensures + result.offset_spec() == self.offset_spec() + range.start, + result.length_spec() == range.end - range.start, + result.paddr_spec() == self.paddr_spec() + range.start, + )] pub fn slice(&self, range: Range) -> Self { // This ensures `range.start < range.end` and `range.end <= limit`. + /* assert!(!range.is_empty() && range.end <= self.limit); + */ + vstd_extra::assert!(range.start < range.end && range.end <= self.limit); // We've checked the range is in bounds, so we can construct the new `IoMem` safely. Self { kvirt_area: self.kvirt_area.clone(), offset: self.offset + range.start, - limit: range.len(), + /* limit: range.len(), */ + limit: range.end - range.start, pa: self.pa + range.start, } } @@ -123,7 +170,11 @@ impl IoMem { // SAFETY: The caller of `IoMem::new()` ensures that the given // physical address range is I/O memory, so it is safe to map. - let kva = unsafe { KVirtArea::map_untracked_frames(area_size, 0, frames_range, prop) }; + // Original Rust: + // let kva = unsafe { KVirtArea::map_untracked_frames(area_size, 0, frames_range, prop) }; + let kva = unsafe { + KVirtArea::map_untracked_frames::(area_size, 0, frames_range, prop) + }; Self { kvirt_area: Arc::new(kva), @@ -146,8 +197,10 @@ impl IoMem { // SAFETY: The constructor of the `IoMem` structure has already ensured the // safety of reading from the mapped physical address, and the mapping is valid. unsafe { + // `from_kernel_space` in ostd/src/mm/io.rs is changed + // (self.kvirt_area.deref().start() + self.offset) as *mut u8 VmReader::from_kernel_space( - (self.kvirt_area.deref().start() + self.offset) as *mut u8, + VirtPtr::from_vaddr(self.kvirt_area.deref().start() + self.offset, self.limit), self.limit, ) } @@ -157,15 +210,17 @@ impl IoMem { // SAFETY: The constructor of the `IoMem` structure has already ensured the // safety of writing to the mapped physical address, and the mapping is valid. unsafe { + // Original Rust passed the raw pointer + // `(self.kvirt_area.deref().start() + self.offset) as *mut u8`. VmWriter::from_kernel_space( - (self.kvirt_area.deref().start() + self.offset) as *mut u8, + VirtPtr::from_vaddr(self.kvirt_area.deref().start() + self.offset, self.limit), self.limit, ) } } } -impl VmIo for IoMem { +/*impl VmIo for IoMem { fn read(&self, offset: usize, writer: &mut VmWriter) -> Result<()> { let offset = offset + self.offset; if self @@ -213,7 +268,7 @@ impl VmIoOnce for IoMem { fn write_once(&self, offset: usize, new_val: &T) -> Result<()> { self.writer().skip(offset).write_once(new_val) } -} +}*/ impl Drop for IoMem { fn drop(&mut self) { diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index 731ed3571..c6ea35bd2 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -1,11 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port and its allocator that allocates port I/O (PIO) to device drivers. +use vstd::prelude::*; + use crate::arch::device::io_port::{IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite}; -mod allocator; +/*mod allocator;*/ use core::{marker::PhantomData, mem::size_of}; -pub(super) use self::allocator::init; +/*pub(super) use self::allocator::init;*/ use crate::{Error, prelude::*}; /// An I/O port, representing a specific address in the I/O address of x86. @@ -20,23 +22,37 @@ use crate::{Error, prelude::*}; /// } /// ``` /// +#[verus_verify] pub struct IoPort { port: u16, value_marker: PhantomData, access_marker: PhantomData, } +verus! { + +impl View for IoPort { + type V = u16; + + closed spec fn view(&self) -> u16 { + self.port + } +} + +} // verus! +#[verus_verify] impl IoPort { /// Acquires an `IoPort` instance for the given range. - pub fn acquire(port: u16) -> Result> { + /*pub fn acquire(port: u16) -> Result> { allocator::IO_PORT_ALLOCATOR .get() .unwrap() .acquire(port) .ok_or(Error::AccessDenied) - } + }*/ /// Returns the port number. + #[verus_spec(returns self@)] pub const fn port(&self) -> u16 { self.port } @@ -52,6 +68,7 @@ impl IoPort { /// /// This function is marked unsafe as creating an I/O port is considered /// a privileged operation. + #[verus_spec(ret => ensures ret@ == port)] pub const unsafe fn new(port: u16) -> Self { Self { port, @@ -61,6 +78,8 @@ impl IoPort { } } +#[verus_verify] +#[verifier::allow(undeclared_external_trait)] impl IoPort { /// Reads from the I/O port #[inline] @@ -69,6 +88,8 @@ impl IoPort { } } +#[verus_verify] +#[verifier::allow(undeclared_external_trait)] impl IoPort { /// Writes to the I/O port #[inline] @@ -77,7 +98,7 @@ impl IoPort { } } -impl Drop for IoPort { +/*impl Drop for IoPort { fn drop(&mut self) { // SAFETY: The caller have ownership of the PIO region. unsafe { @@ -87,7 +108,7 @@ impl Drop for IoPort { .recycle(self.port..(self.port + size_of::() as u16)); } } -} +}*/ /// Reserves an I/O port range which may refer to the port I/O range used by the /// system device driver. @@ -162,6 +183,7 @@ pub(crate) use sensitive_io_port; #[doc(hidden)] #[derive(Debug, Clone, Copy)] #[repr(C)] +#[verus_verify] pub(crate) struct RawIoPortRange { pub(crate) begin: u16, pub(crate) end: u16, diff --git a/ostd/src/io/mod.rs b/ostd/src/io/mod.rs index a87940453..4de2932ff 100644 --- a/ostd/src/io/mod.rs +++ b/ostd/src/io/mod.rs @@ -20,6 +20,7 @@ cfg_if!( } ); +/* /// Initializes the static allocator based on builder. /// /// # Safety @@ -43,4 +44,4 @@ pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { unsafe { self::io_port::init() }; -} +}*/ diff --git a/ostd/src/lib.rs b/ostd/src/lib.rs index 8a90a3876..a4970bd77 100644 --- a/ostd/src/lib.rs +++ b/ostd/src/lib.rs @@ -38,16 +38,16 @@ pub mod arch; //#[path = "arch/riscv/mod.rs"] //pub mod arch; pub mod boot; -/*pub mod bus; -pub mod console; +pub mod bus; +/*pub mod console; pub mod cpu;*/ pub mod error; -/*pub mod io; -pub mod logger;*/ +pub mod io; +/*pub mod logger;*/ pub mod mm; -/*pub mod panic; +/*pub mod panic;*/ pub mod prelude; -pub mod smp;*/ +/*pub mod smp;*/ pub mod sync; pub mod task; pub mod timer; @@ -64,9 +64,9 @@ pub use ostd_macros::{ panic_handler, };*/ pub use ostd_pod::Pod; -/* -pub use self::{error::Error, prelude::Result}; +pub use self::{error::Error, prelude::Result}; +/* /// Initializes OSTD. /// /// This function represents the first phase booting up the system. It makes diff --git a/ostd/src/prelude.rs b/ostd/src/prelude.rs index f9ab829ef..bb1eeaf17 100644 --- a/ostd/src/prelude.rs +++ b/ostd/src/prelude.rs @@ -6,7 +6,7 @@ pub type Result = core::result::Result; pub(crate) use alloc::{boxed::Box, sync::Arc, vec::Vec}; - +/* #[cfg(ktest)] pub use ostd_macros::ktest; @@ -14,4 +14,4 @@ pub use crate::{ early_print as print, early_println as println, mm::{Paddr, UntypedMem, Vaddr}, panic::abort, -}; +}; */ From fffd33501958603bf78dbd932fd52413e7e789fb Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Thu, 13 Aug 2026 12:47:30 +0800 Subject: [PATCH 02/30] prove: io --- Cargo.lock | 8 ++ ostd/Cargo.toml | 2 +- ostd/libs/id-alloc/src/lib.rs | 1 - ostd/src/arch/x86/io.rs | 7 +- ostd/src/arch/x86/mod.rs | 4 +- ostd/src/io/io_mem/allocator.rs | 58 +++++++++---- ostd/src/io/io_mem/mod.rs | 142 +++++++++++++++++++++++++------ ostd/src/io/io_port/allocator.rs | 89 ++++++++++++++++++- ostd/src/io/io_port/mod.rs | 45 +++++++--- ostd/src/io/mod.rs | 6 +- 10 files changed, 297 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92c2f90e6..fe004aa9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,13 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "id-alloc" +version = "0.1.0" +dependencies = [ + "bitvec", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -310,6 +317,7 @@ dependencies = [ "fdt", "gimli 0.28.1", "iced-x86", + "id-alloc", "inherit-methods-macro", "log", "loongArch64", diff --git a/ostd/Cargo.toml b/ostd/Cargo.toml index f1344922b..830b3a55d 100644 --- a/ostd/Cargo.toml +++ b/ostd/Cargo.toml @@ -25,7 +25,7 @@ buddy_system_allocator = { version = "0.10", default-features = false, features bitflags_upstream = { package = "bitflags", version = "1.3" } cfg-if = "1.0" gimli = { version = "0.28", default-features = false, features = ["read-core"] } -#id-alloc = { path = "libs/id-alloc", version = "0.1.0" } +id-alloc = { path = "libs/id-alloc", version = "0.1.0" } inherit-methods-macro = { git = "https://github.com/asterinas/inherit-methods-macro", rev = "98f7e3e", version = "0.1.0" } #int-to-c-enum = { path = "../kernel/libs/int-to-c-enum", version = "0.1.0" } # intrusive-collections = { version = "0.9.6", features = ["nightly"] } diff --git a/ostd/libs/id-alloc/src/lib.rs b/ostd/libs/id-alloc/src/lib.rs index 85fe26b25..4721d94e5 100644 --- a/ostd/libs/id-alloc/src/lib.rs +++ b/ostd/libs/id-alloc/src/lib.rs @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MPL-2.0 - #![cfg_attr(not(test), no_std)] #![deny(unsafe_code)] diff --git a/ostd/src/arch/x86/io.rs b/ostd/src/arch/x86/io.rs index b78d04f26..35913c17f 100644 --- a/ostd/src/arch/x86/io.rs +++ b/ostd/src/arch/x86/io.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 -use alloc::vec::Vec; +use vstd::prelude::*; + +/*use alloc::vec::Vec; use align_ext::AlignExt; @@ -55,6 +57,7 @@ pub(super) fn construct_io_mem_allocator_builder() -> IoMemAllocatorBuilder { // SAFETY: The range is guaranteed not to access physical memory. unsafe { IoMemAllocatorBuilder::new(ranges) } } - +*/ /// Port I/O definition reference: . +#[verus_verify] pub const MAX_IO_PORT: u16 = u16::MAX; diff --git a/ostd/src/arch/x86/mod.rs b/ostd/src/arch/x86/mod.rs index ec85da40d..302df01ae 100644 --- a/ostd/src/arch/x86/mod.rs +++ b/ostd/src/arch/x86/mod.rs @@ -4,9 +4,9 @@ pub(crate) mod cpu;*/ pub mod device; -/*pub(crate) mod ex_table; +/*pub(crate) mod ex_table;*/ pub(crate) mod io; -pub(crate) mod iommu;*/ +/*pub(crate) mod iommu;*/ pub(crate) mod irq; /* pub(crate) mod kernel; */ pub(crate) mod mm; diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 0e2ca94b1..cfa98c8aa 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -1,12 +1,14 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O Memory allocator. +use crate::specs::arch::PAGE_SIZE; +use crate::sync::{OnceImpl, TrivialPred}; use vstd::prelude::*; use alloc::vec::Vec; use core::ops::Range; use log::{debug, info}; -use spin::Once; +/*use spin::Once;*/ use crate::{ io::io_mem::IoMem, @@ -20,16 +22,27 @@ pub struct IoMemAllocator { allocators: Vec, } +#[verus_verify] impl IoMemAllocator { /// Acquires the I/O memory access for `range`. /// /// If the range is not available, then the return value will be `None`. + #[verus_spec(result => + requires + vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + range.start < range.end, + range.end <= usize::MAX - (PAGE_SIZE - 1), + ensures + result is Some ==> result->Some_0.paddr_spec() == range.start, + result is Some ==> result->Some_0.length_spec() + == vstd_extra::external::range::range_usize_len_spec(&range), + )] pub fn acquire(&self, range: Range) -> Option { find_allocator(&self.allocators, &range)? .alloc_specific(&range) .ok()?; - debug!("Acquiring MMIO range:{:x?}..{:x?}", range.start, range.end); + /* debug!("Acquiring MMIO range:{:x?}..{:x?}", range.start, range.end); */ // SAFETY: The created `IoMem` is guaranteed not to access physical memory or system device I/O. // Original Rust used the upstream bitflags-style associated constant `PageFlags::RW`. @@ -42,10 +55,11 @@ impl IoMemAllocator { /// /// The caller must have ownership of the MMIO region through the `IoMemAllocator::get` interface. #[expect(dead_code)] + #[verifier::external_body] pub(in crate::io) unsafe fn recycle(&self, range: Range) { let allocator = find_allocator(&self.allocators, &range).unwrap(); - debug!("Recycling MMIO range:{:x}..{:x}", range.start, range.end); + /* debug!("Recycling MMIO range:{:x}..{:x}", range.start, range.end); */ allocator.free(range); } @@ -70,6 +84,7 @@ pub(crate) struct IoMemAllocatorBuilder { allocators: Vec, } +#[verus_verify] impl IoMemAllocatorBuilder { /// Initializes memory I/O region for devices. /// @@ -92,35 +107,42 @@ impl IoMemAllocatorBuilder { /// Removes access to a specific memory I/O range. /// /// All drivers in OSTD must use this method to prevent peripheral drivers from accessing illegal memory I/O range. + #[verus_spec( + requires + range.start < range.end, + vstd_extra::panic::may_panic(), + )] pub(crate) fn remove(&self, range: Range) { - let Some(allocator) = find_allocator(&self.allocators, &range) else { - panic!( - "Allocator for the system device's MMIO was not found. Range: {:x?}", - range - ); - }; - - if let Err(err) = allocator.alloc_specific(&range) { - panic!( - "An error occurred while trying to remove access to the system device's MMIO. Range: {:x?}. Error: {:?}", - range, err - ); - } + // Formatting machinery used by the original panic is not modeled by Verus. + // Original Rust used two formatted `panic!` branches here. + let allocator = find_allocator(&self.allocators, &range); + vstd_extra::assert!(allocator.is_some()); + let result = allocator.unwrap().alloc_specific(&range); + vstd_extra::assert!(result.is_ok()); } } /// The I/O Memory allocator of the system. -pub static IO_MEM_ALLOCATOR: Once = Once::new(); +verus! { +pub exec static IO_MEM_ALLOCATOR: OnceImpl + ensures + IO_MEM_ALLOCATOR.wf(), +{ + OnceImpl::new(Ghost(TrivialPred)) +} + +} // verus! /// Initializes the static `IO_MEM_ALLOCATOR` based on builder. /// /// # Safety /// /// User must ensure all the memory I/O regions that belong to the system device have been removed by calling the /// `remove` function. +#[verus_verify] pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { // SAFETY: The safety is upheld by the caller. - IO_MEM_ALLOCATOR.call_once(|| unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); + IO_MEM_ALLOCATOR.init(unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); } #[verus_verify] diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index 3ad8b3087..c92c2a947 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O memory and its allocator that allocates memory I/O (MMIO) to device drivers. use crate::specs::arch::PAGE_SIZE; -use crate::specs::{mm::virt_mem::VirtPtr, task::AnyAtomicGuard}; +use crate::specs::{ + mm::{io::VmIoOwner, virt_mem::VirtPtr}, + task::AnyAtomicGuard, +}; use vstd::prelude::*; mod allocator; @@ -36,6 +39,7 @@ pub struct IoMem { verus! { +#[verus_verify] impl IoMem { /// Logical physical-address projection used by verified callers. pub closed spec fn paddr_spec(&self) -> Paddr { @@ -62,12 +66,23 @@ impl HasPaddr for IoMem { } } +#[verus_verify] impl IoMem { /// Acquires an `IoMem` instance for the given range. + #[verus_spec(result => + requires + vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + range.start < range.end, + range.end <= usize::MAX - (PAGE_SIZE - 1), + ensures + result is Ok ==> result->Ok_0.paddr_spec() == range.start, + result is Ok ==> result->Ok_0.length_spec() + == vstd_extra::external::range::range_usize_len_spec(&range), + )] pub fn acquire(range: Range) -> Result { allocator::IO_MEM_ALLOCATOR .get() - .unwrap() + .ok_or(Error::AccessDenied)? .acquire(range) .ok_or(Error::AccessDenied) } @@ -115,7 +130,7 @@ impl IoMem { kvirt_area: self.kvirt_area.clone(), offset: self.offset + range.start, /* limit: range.len(), */ - limit: range.end - range.start, + limit: vstd_extra::external::range::range_usize_len(&range), pa: self.pa + range.start, } } @@ -127,6 +142,17 @@ impl IoMem { /// - The given physical address range must be in the I/O memory region. /// - Reading from or writing to I/O memory regions may have side effects. Those side effects /// must not cause soundness problems (e.g., they must not corrupt the kernel memory). + #[verifier::external_body] + #[verus_spec(result => + requires + vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + range.start <= range.end, + range.end <= usize::MAX - (PAGE_SIZE - 1), + ensures + result.paddr_spec() == range.start, + result.length_spec() + == vstd_extra::external::range::range_usize_len_spec(&range), + )] pub(crate) unsafe fn new(range: Range, flags: PageFlags, cache: CachePolicy) -> Self { let first_page_start = range.start.align_down(PAGE_SIZE); let last_page_end = range.end.align_up(PAGE_SIZE); @@ -220,40 +246,83 @@ impl IoMem { } } -/*impl VmIo for IoMem { - fn read(&self, offset: usize, writer: &mut VmWriter) -> Result<()> { +verus! { + +impl VmIo<()> for IoMem { + closed spec fn obeys_vmio_spec() -> bool { + false + } + + closed spec fn obeys_vmio_read_spec() -> bool { + false + } + + closed spec fn obeys_vmio_write_spec() -> bool { + false + } + + open spec fn read_spec( + self, + offset: usize, + old_writer: VmWriter<'_>, + new_writer: VmWriter<'_>, + old_writer_own: VmIoOwner, + new_writer_own: VmIoOwner, + old_owner: (), + new_owner: (), + r: Result<()>, + ) -> bool { + false + } + + open spec fn write_spec( + self, + offset: usize, + old_reader: VmReader<'_>, + new_reader: VmReader<'_>, + old_reader_own: VmIoOwner, + new_reader_own: VmIoOwner, + old_owner: (), + new_owner: (), + r: Result<()>, + ) -> bool { + false + } + + /// Device reads are a trusted hardware boundary; the range checks and cursor updates remain + /// identical to the original implementation. + #[verifier::external_body] + fn read( + &self, + offset: usize, + writer: &mut VmWriter, + Tracked(_writer_own): Tracked<&mut VmIoOwner>, + Tracked(_owner): Tracked<&mut ()>, + ) -> Result<()> { let offset = offset + self.offset; - if self - .limit - .checked_sub(offset) - .is_none_or(|remain| remain < writer.avail()) - { + if self.limit.checked_sub(offset).is_none_or(|remain| remain < writer.avail()) { return Err(Error::InvalidArgs); } - - self.reader() - .skip(offset) - .read_fallible(writer) - .map_err(|(e, _)| e)?; + self.reader().skip(offset).read_fallible(writer).map_err(|(e, _)| e)?; debug_assert!(!writer.has_avail()); Ok(()) } - fn write(&self, offset: usize, reader: &mut VmReader) -> Result<()> { + /// Device writes are a trusted hardware boundary for the same reason as [`Self::read`]. + #[verifier::external_body] + fn write( + &self, + offset: usize, + reader: &mut VmReader, + Tracked(_reader_own): Tracked<&mut VmIoOwner>, + Tracked(_owner): Tracked<&mut ()>, + ) -> Result<()> { let offset = offset + self.offset; - if self - .limit - .checked_sub(offset) - .is_none_or(|remain| remain < reader.remain()) - { + if self.limit.checked_sub(offset).is_none_or(|remain| remain < reader.remain()) { return Err(Error::InvalidArgs); } - - self.writer() - .skip(offset) - .write_fallible(reader) - .map_err(|(e, _)| e)?; + self.writer().skip(offset).write_fallible(reader).map_err(|(e, _)| e)?; debug_assert!(!reader.has_remain()); Ok(()) @@ -261,15 +330,34 @@ impl IoMem { } impl VmIoOnce for IoMem { + closed spec fn obeys_vmio_once_read_requires() -> bool { + false + } + + closed spec fn obeys_vmio_once_write_requires() -> bool { + false + } + + closed spec fn obeys_vmio_once_read_ensures() -> bool { + false + } + + closed spec fn obeys_vmio_once_write_ensures() -> bool { + false + } + + #[verifier::external_body] fn read_once(&self, offset: usize) -> Result { self.reader().skip(offset).read_once() } + #[verifier::external_body] fn write_once(&self, offset: usize, new_val: &T) -> Result<()> { self.writer().skip(offset).write_once(new_val) } -}*/ +} +} // verus! impl Drop for IoMem { fn drop(&mut self) { // TODO: Multiple `IoMem` instances should not overlap, we should refactor the driver code and diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 0e5b85e9a..f4bff93f6 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port allocator. +use vstd::prelude::*; + use core::ops::Range; use id_alloc::IdAlloc; @@ -12,7 +14,64 @@ use crate::{ sync::{LocalIrqDisabled, SpinLock}, }; +verus! { + +/// Verus model for the external bitmap-backed ID allocator. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExIdAlloc(IdAlloc); + +/// Opaque specification for the third-party one-time initialization primitive. +#[verifier::external_type_specification] +#[verifier::external_body] +#[verifier::reject_recursive_types(T)] +#[verifier::reject_recursive_types(R)] +pub struct ExOnce(spin::once::Once); + +/// IDs currently allocated by an external `IdAlloc`. +pub uninterp spec fn id_alloc_view(allocator: &IdAlloc) -> Set; + +/// Capacity configured for an external `IdAlloc`. +pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; + +pub assume_specification[ IdAlloc::with_capacity ](capacity: usize) -> (allocator: IdAlloc) + ensures + id_alloc_capacity(&allocator) == capacity, + id_alloc_view(&allocator) == Set::::empty(), +; + +pub assume_specification[ IdAlloc::is_allocated ](allocator: &IdAlloc, id: usize) -> (allocated: + bool) + ensures + id < id_alloc_capacity(allocator) ==> allocated == id_alloc_view(allocator).contains(id), +; + +pub assume_specification[ IdAlloc::alloc_specific ](allocator: &mut IdAlloc, id: usize) -> (result: + Option) + ensures + id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), + id < id_alloc_capacity(old(allocator)) && id_alloc_view(old(allocator)).contains(id) ==> { + &&& result is None + &&& id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)) + }, + id < id_alloc_capacity(old(allocator)) && !id_alloc_view(old(allocator)).contains(id) ==> { + &&& result == Some(id) + &&& id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id) + }, +; + +pub assume_specification[ IdAlloc::free_consecutive ](allocator: &mut IdAlloc, range: Range) + ensures + id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), + range.end <= id_alloc_capacity(old(allocator)) ==> forall|id: usize| #[trigger] + id_alloc_view(final(allocator)).contains(id) <==> id_alloc_view( + old(allocator), + ).contains(id) && !(range.start <= id < range.end), +; + +} // verus! /// I/O port allocator that allocates port I/O access to device drivers. +#[verus_verify] pub struct IoPortAllocator { /// Each ID indicates whether a Port I/O (1B) is allocated. /// @@ -21,12 +80,28 @@ pub struct IoPortAllocator { allocator: SpinLock, } +#[verus_verify] impl IoPortAllocator { /// Acquires the `IoPort`. Return None if any region in `port` cannot be allocated. + #[verus_spec(result => + requires + size_of::() <= u16::MAX, + port as usize + size_of::() <= u16::MAX, + ensures + result is Some ==> result->Some_0@ == port, + )] pub fn acquire(&self, port: u16) -> Option> { let mut allocator = self.allocator.lock(); let mut range = port..(port + size_of::() as u16); - if range.any(|i| allocator.is_allocated(i as usize)) { + // `Iterator::any` with a capturing closure is not supported by Verus. Original Rust: + // if range.any(|i| allocator.is_allocated(i as usize)) { return None; } + let mut already_allocated = false; + for i in range.clone() { + if allocator.is_allocated(i as usize) { + already_allocated = true; + } + } + if already_allocated { return None; } @@ -44,7 +119,7 @@ impl IoPortAllocator { /// /// The caller must have ownership of the PIO region through the `IoPortAllocator::acquire` interface. pub(in crate::io) unsafe fn recycle(&self, range: Range) { - debug!("Recycling MMIO range: {:#x?}", range); + /* debug!("Recycling MMIO range: {:#x?}", range); */ self.allocator .lock() @@ -52,6 +127,15 @@ impl IoPortAllocator { } } +verus! { + +/// Trusted boot-state fact required before accessing the global PIO allocator. +/// +/// Verus cannot currently mention an `exec static` in a specification, so this predicate is the +/// explicit specification boundary for the architecture's guarantee that [`init`] ran first. +pub uninterp spec fn io_port_allocator_initialized() -> bool; + +} // verus! pub(super) static IO_PORT_ALLOCATOR: Once = Once::new(); /// Initializes the static `IO_PORT_ALLOCATOR` and removes the system device I/O port regions. @@ -65,6 +149,7 @@ pub(super) static IO_PORT_ALLOCATOR: Once = Once::new(); /// /// 2. `MAX_IO_PORT` defined in `crate::arch::io` is guaranteed not to exceed the maximum /// value specified by architecture. +#[verifier::external_body] pub(crate) unsafe fn init() { // SAFETY: `MAX_IO_PORT` is guaranteed not to exceed the maximum value specified by architecture. let mut allocator = IdAlloc::with_capacity(crate::arch::io::MAX_IO_PORT as usize); diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index c6ea35bd2..54ac0e99e 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -3,11 +3,11 @@ use vstd::prelude::*; use crate::arch::device::io_port::{IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite}; -/*mod allocator;*/ +mod allocator; use core::{marker::PhantomData, mem::size_of}; -/*pub(super) use self::allocator::init;*/ +pub(super) use self::allocator::init; use crate::{Error, prelude::*}; /// An I/O port, representing a specific address in the I/O address of x86. @@ -40,16 +40,35 @@ impl View for IoPort { } } // verus! +/// Returns the initialized global PIO allocator. +/// +/// The executable body intentionally preserves the original `get().unwrap()` behavior. This +/// helper is trusted only because Verus cannot connect an `exec static` to a spec-level boot-state +/// predicate. +#[verifier::external_body] +#[verus_spec( + requires allocator::io_port_allocator_initialized(), +)] +fn initialized_allocator() -> &'static allocator::IoPortAllocator { + allocator::IO_PORT_ALLOCATOR.get().unwrap() +} + #[verus_verify] impl IoPort { /// Acquires an `IoPort` instance for the given range. - /*pub fn acquire(port: u16) -> Result> { - allocator::IO_PORT_ALLOCATOR - .get() - .unwrap() + #[verus_spec(result => + requires + size_of::() <= u16::MAX, + port as usize + size_of::() <= u16::MAX, + allocator::io_port_allocator_initialized(), + ensures + result is Ok ==> result->Ok_0@ == port, + )] + pub fn acquire(port: u16) -> Result> { + initialized_allocator() .acquire(port) .ok_or(Error::AccessDenied) - }*/ + } /// Returns the port number. #[verus_spec(returns self@)] @@ -68,7 +87,13 @@ impl IoPort { /// /// This function is marked unsafe as creating an I/O port is considered /// a privileged operation. - #[verus_spec(ret => ensures ret@ == port)] + #[verus_spec(ret => + requires + size_of::() <= u16::MAX, + port as usize + size_of::() <= u16::MAX, + ensures + ret@ == port, + )] pub const unsafe fn new(port: u16) -> Self { Self { port, @@ -98,7 +123,7 @@ impl IoPort { } } -/*impl Drop for IoPort { +impl Drop for IoPort { fn drop(&mut self) { // SAFETY: The caller have ownership of the PIO region. unsafe { @@ -108,7 +133,7 @@ impl IoPort { .recycle(self.port..(self.port + size_of::() as u16)); } } -}*/ +} /// Reserves an I/O port range which may refer to the port I/O range used by the /// system device driver. diff --git a/ostd/src/io/mod.rs b/ostd/src/io/mod.rs index 4de2932ff..b27636383 100644 --- a/ostd/src/io/mod.rs +++ b/ostd/src/io/mod.rs @@ -5,6 +5,8 @@ //! through _allocators_. There are two types of device I/O: //! - `IoMem` for memory I/O (MMIO). //! - `IoPort` for port I/O (PIO). +use vstd::prelude::*; + mod io_mem; use cfg_if::cfg_if; @@ -20,7 +22,6 @@ cfg_if!( } ); -/* /// Initializes the static allocator based on builder. /// /// # Safety @@ -35,6 +36,7 @@ cfg_if!( /// /// 3. `MAX_IO_PORT` defined in `crate::arch::io` is guaranteed not to /// exceed the maximum value specified by architecture. +#[verus_verify] pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { // SAFETY: The safety is upheld by the caller. unsafe { self::io_mem::init(io_mem_builder) }; @@ -44,4 +46,4 @@ pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { unsafe { self::io_port::init() }; -}*/ +} From 18d72c57fdc75d0d27b109c30694a119730eaa58 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Thu, 13 Aug 2026 17:40:16 +0800 Subject: [PATCH 03/30] prove: port allocation and access --- Cargo.lock | 1 + Cargo.toml | 1 + ostd/Cargo.toml | 1 + ostd/src/arch/x86/device/io_port.rs | 29 ++- ostd/src/arch/x86/pci.rs | 19 +- ostd/src/io/io_port/allocator.rs | 354 ++++++++++++++++++++++++++-- ostd/src/io/io_port/mod.rs | 99 +++++++- 7 files changed, 476 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe004aa9a..2ce4cdc68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,7 @@ dependencies = [ "spin", "tdx-guest", "unwinding", + "verus_state_machines_macros", "volatile 0.6.1", "vstd", "vstd_extra", diff --git a/Cargo.toml b/Cargo.toml index 0963ca334..2a3b5dca8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,3 +51,4 @@ codegen-units = 1 [workspace.dependencies] # Verus vstd = { path = "tools/verus/source/vstd", default-features = false, features = ["alloc"] } +verus_state_machines_macros = { path = "tools/verus/source/state_machines_macros" } \ No newline at end of file diff --git a/ostd/Cargo.toml b/ostd/Cargo.toml index 830b3a55d..e1e1e8591 100644 --- a/ostd/Cargo.toml +++ b/ostd/Cargo.toml @@ -18,6 +18,7 @@ targets = ["x86_64-unknown-none"] [dependencies] vstd = { workspace = true } vstd_extra = { path = "../verified_libs/vstd_extra" } +verus_state_machines_macros = { workspace = true } bitflags = { path = "../verified_libs/bitflags" } align_ext = { path = "libs/align_ext", version = "0.1.0" } bit_field = "0.10.1" diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index c86716257..10bc5b58f 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -10,8 +10,27 @@ pub use x86_64::{ use vstd::prelude::*; +use core::mem::size_of; + verus! { +/// Whether `port` is representable in the 16-bit x86 I/O-port address space. +/// +/// This is only the ISA-level validity condition. It does not claim that a device decodes the +/// port, that the current CPU context may access it, or that the caller owns it. +pub open spec fn valid_io_port_number(port: int) -> bool { + 0 <= port <= u16::MAX as int +} + +/// Whether an access of type `T` is fully contained in the x86 I/O-port address space. +/// +/// OSTD allocates one port byte for every byte in `T`, so this is stronger than merely checking +/// that the starting port is representable. +pub open spec fn valid_io_port_access(port: int) -> bool { + &&& valid_io_port_number(port) + &&& port + size_of::() <= u16::MAX as int + 1 +} + /// Opaque specification boundary for the third-party read/write access marker. #[verifier::external_type_specification] #[verifier::external_body] @@ -28,7 +47,10 @@ pub trait ExPortRead { type ExternalTraitSpecificationFor: PortRead; /// A port read can produce any value supplied by the device. - unsafe fn read_from_port(port: u16) -> Self where Self: Sized; + unsafe fn read_from_port(port: u16) -> Self where Self: Sized + requires + valid_io_port_access::(port as int), + ; } /// Trusted specification boundary for values that can be written to an x86 I/O port. @@ -37,7 +59,10 @@ pub trait ExPortWrite { type ExternalTraitSpecificationFor: PortWrite; /// A port write has no modeled logical effect on kernel memory. - unsafe fn write_to_port(port: u16, value: Self) where Self: Sized; + unsafe fn write_to_port(port: u16, value: Self) where Self: Sized + requires + valid_io_port_access::(port as int), + ; } } // verus! diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index 33832d995..889bbb5ee 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -13,12 +13,21 @@ pub assume_specification[ u32::to_le ](value: u32) -> (result: u32) result == value, ; -} // verus! -#[verus_verify] -static PCI_ADDRESS_PORT: IoPort = unsafe { IoPort::new(0x0CF8) }; -#[verus_verify] -static PCI_DATA_PORT: IoPort = unsafe { IoPort::new(0x0CFC) }; +exec static PCI_ADDRESS_PORT: IoPort + ensures + PCI_ADDRESS_PORT.well_formed(), +{ + unsafe { IoPort::new(0x0CF8) } +} +exec static PCI_DATA_PORT: IoPort + ensures + PCI_DATA_PORT.well_formed(), +{ + unsafe { IoPort::new(0x0CFC) } +} + +} // verus! #[verus_verify] const BIT32_ALIGN_MASK: u32 = 0xFFFC; diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index f4bff93f6..ff4cad016 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port allocator. +use verus_state_machines_macros::tokenized_state_machine; use vstd::prelude::*; +use vstd::tokens::{InstanceId, SetToken, ValueToken}; use core::ops::Range; @@ -8,7 +10,7 @@ use id_alloc::IdAlloc; use log::debug; use spin::Once; -use super::IoPort; +use super::{IoPort, lemma_port_id_set_contains, lemma_port_id_set_insert, port_id_set}; use crate::{ io::RawIoPortRange, sync::{LocalIrqDisabled, SpinLock}, @@ -34,6 +36,21 @@ pub uninterp spec fn id_alloc_view(allocator: &IdAlloc) -> Set; /// Capacity configured for an external `IdAlloc`. pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; +/// Identity assigned to the single global PIO allocator during trusted boot initialization. +pub uninterp spec fn io_port_allocator_instance_id() -> InstanceId; + +closed spec fn io_port_inner_inv_values( + instance_id: InstanceId, + allocated_instance_id: InstanceId, + allocated: Set, + allocator: &IdAlloc, +) -> bool { + &&& instance_id == io_port_allocator_instance_id() + &&& allocated_instance_id == io_port_allocator_instance_id() + &&& allocated.subset_of(id_alloc_view(allocator)) + &&& id_alloc_capacity(allocator) == crate::arch::io::MAX_IO_PORT as usize +} + pub assume_specification[ IdAlloc::with_capacity ](capacity: usize) -> (allocator: IdAlloc) ensures id_alloc_capacity(&allocator) == capacity, @@ -48,27 +65,154 @@ pub assume_specification[ IdAlloc::is_allocated ](allocator: &IdAlloc, id: usize pub assume_specification[ IdAlloc::alloc_specific ](allocator: &mut IdAlloc, id: usize) -> (result: Option) + requires + id < id_alloc_capacity(old(allocator)), ensures id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - id < id_alloc_capacity(old(allocator)) && id_alloc_view(old(allocator)).contains(id) ==> { + id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id), + id_alloc_view(old(allocator)).subset_of(id_alloc_view(final(allocator))), + id_alloc_view(old(allocator)).contains(id) ==> { &&& result is None - &&& id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)) }, - id < id_alloc_capacity(old(allocator)) && !id_alloc_view(old(allocator)).contains(id) ==> { + !id_alloc_view(old(allocator)).contains(id) ==> { &&& result == Some(id) - &&& id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id) }, + no_unwind ; pub assume_specification[ IdAlloc::free_consecutive ](allocator: &mut IdAlloc, range: Range) + requires + range.end <= id_alloc_capacity(old(allocator)), ensures id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - range.end <= id_alloc_capacity(old(allocator)) ==> forall|id: usize| #[trigger] + forall|id: usize| #[trigger] id_alloc_view(final(allocator)).contains(id) <==> id_alloc_view( old(allocator), ).contains(id) && !(range.start <= id < range.end), + no_unwind ; +} // verus! +/// Transparent facade used only to pass ghost frame facts to the third-party mutation. +#[repr(transparent)] +#[verus_verify] +struct ModeledIdAlloc { + inner: IdAlloc, +} + +#[verus_verify] +impl ModeledIdAlloc { + #[verus_spec(result => + with + Ghost(instance_id): Ghost, + Ghost(allocated_instance_id): Ghost, + Ghost(preserved): Ghost>, + requires + id < id_alloc_capacity(&old(self).inner), + io_port_inner_inv_values( + instance_id, + allocated_instance_id, + preserved, + &old(self).inner, + ), + ensures + id_alloc_capacity(&final(self).inner) == id_alloc_capacity(&old(self).inner), + id_alloc_view(&final(self).inner) == id_alloc_view(&old(self).inner).insert(id), + id_alloc_view(&old(self).inner).subset_of(id_alloc_view(&final(self).inner)), + preserved.subset_of(id_alloc_view(&final(self).inner)), + io_port_inner_inv_values( + instance_id, + allocated_instance_id, + preserved, + &final(self).inner, + ), + id_alloc_view(&old(self).inner).contains(id) ==> result is None, + !id_alloc_view(&old(self).inner).contains(id) ==> result == Some(id), + no_unwind + )] + fn alloc_specific(&mut self, id: usize) -> Option { + self.inner.alloc_specific(id) + } +} + +verus! { + +tokenized_state_machine! { + IoPortAllocationState { + fields { + #[sharding(variable)] + pub allocated: Set, + #[sharding(set)] + pub claims: Set, + } + + #[invariant] + pub fn allocated_matches_claims(&self) -> bool { + self.allocated =~= self.claims + } + + init! { + initialize() { + init allocated = Set::empty(); + init claims = Set::empty(); + } + } + + transition! { + allocate(ids: Set) { + require pre.allocated.disjoint(ids); + update allocated = pre.allocated.union(ids); + add claims += (ids) by { + assert(pre.claims =~= pre.allocated); + }; + } + } + + transition! { + release(ids: Set) { + update allocated = pre.allocated.difference(ids); + remove claims -= (ids); + } + } + + #[inductive(initialize)] + fn initialize_inductive(post: Self) {} + + #[inductive(allocate)] + fn allocate_inductive(pre: Self, post: Self, ids: Set) {} + + #[inductive(release)] + fn release_inductive(pre: Self, post: Self, ids: Set) {} + } +} + +} // verus! +pub(super) type IoPortClaim = IoPortAllocationState::claims_set; + +/// Lock-protected executable bitmap and the state-machine token that models it. +#[verus_verify] +struct IoPortAllocatorInner { + allocator: ModeledIdAlloc, + #[cfg(verus_keep_ghost_body)] + tracked_instance: Tracked, + #[cfg(verus_keep_ghost_body)] + tracked_allocated: Tracked, +} + +verus! { + +impl IoPortAllocatorInner { + #[verifier::type_invariant] + pub closed spec fn type_inv(self) -> bool { + io_port_inner_inv_values( + self.tracked_instance@.id(), + self.tracked_allocated@.instance_id(), + self.tracked_allocated@.value(), + &self.allocator.inner, + ) + } +} + } // verus! /// I/O port allocator that allocates port I/O access to device drivers. #[verus_verify] @@ -77,40 +221,171 @@ pub struct IoPortAllocator { /// /// Instead of using `RangeAllocator` like `IoMemAllocator` does, it is more reasonable to use `IdAlloc`, /// as PIO space includes only a small region; for example, x86 module in OSTD allows just 65536 I/O ports. - allocator: SpinLock, + allocator: SpinLock, } #[verus_verify] impl IoPortAllocator { /// Acquires the `IoPort`. Return None if any region in `port` cannot be allocated. #[verus_spec(result => + with + -> claim: Tracked>, requires + vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, ensures result is Some ==> result->Some_0@ == port, + result is Some ==> result->Some_0.well_formed(), + result is Some <==> claim@ is Some, + result is Some ==> claim@->Some_0.instance_id() == + io_port_allocator_instance_id(), + result is Some ==> result->Some_0.claim_matches_set(claim@->Some_0.set()), )] pub fn acquire(&self, port: u16) -> Option> { let mut allocator = self.allocator.lock(); + let allocator_inner = &mut *allocator; + proof! { + use_type_invariant(&*allocator_inner); + } let mut range = port..(port + size_of::() as u16); // `Iterator::any` with a capturing closure is not supported by Verus. Original Rust: // if range.any(|i| allocator.is_allocated(i as usize)) { return None; } let mut already_allocated = false; + #[verus_spec(scan_iter => + invariant + allocator_inner.type_inv(), + !already_allocated ==> forall|id: usize| + range.start as usize <= id < + (range.start as int + scan_iter.index()) as usize ==> + !id_alloc_view(&allocator_inner.allocator.inner).contains(id), + )] for i in range.clone() { - if allocator.is_allocated(i as usize) { + if allocator_inner.allocator.inner.is_allocated(i as usize) { already_allocated = true; } } + proof_decl! { + let tracked range_claim: IoPortClaim; + } if already_allocated { - return None; + allocator.drop(); + return { + proof_with!(|= Tracked(None)); + None + }; } + proof_decl! { + let ghost ids = port_id_set(range.start as usize, range.end as usize); + let ghost allocation_start_view = id_alloc_view(&allocator_inner.allocator.inner); + } + proof! { + assert(ids.disjoint(id_alloc_view(&allocator_inner.allocator.inner))) by { + assert forall|id: usize| #[trigger] ids.contains(id) implies + !id_alloc_view(&allocator_inner.allocator.inner).contains(id) by { + lemma_port_id_set_contains( + range.start as usize, + range.end as usize, + id, + ); + } + } + assert forall|id: usize| ids.contains(id) implies + id < id_alloc_capacity(&allocator_inner.allocator.inner) by { + lemma_port_id_set_contains( + range.start as usize, + range.end as usize, + id, + ); + } + assert forall|id: usize| + range.start as usize <= id < range.end as usize implies + !id_alloc_view(&allocator_inner.allocator.inner).contains(id) by { + lemma_port_id_set_contains( + range.start as usize, + range.end as usize, + id, + ); + } + } + #[verus_spec(allocation_iter => + invariant + range.end as usize <= id_alloc_capacity(&allocator_inner.allocator.inner), + allocator_inner.tracked_instance@.id() == io_port_allocator_instance_id(), + allocator_inner.tracked_allocated@.instance_id() == + io_port_allocator_instance_id(), + allocator_inner.tracked_allocated@.value().subset_of(allocation_start_view), + allocator_inner.tracked_allocated@.value().subset_of( + id_alloc_view(&allocator_inner.allocator.inner), + ), + id_alloc_capacity(&allocator_inner.allocator.inner) == + crate::arch::io::MAX_IO_PORT as usize, + id_alloc_view(&allocator_inner.allocator.inner) =~= + allocation_start_view.union( + port_id_set( + range.start as usize, + (range.start as int + allocation_iter.index()) as usize, + ), + ), + forall|id: usize| + (range.start as int + allocation_iter.index()) as usize <= id < + range.end as usize ==> + !id_alloc_view(&allocator_inner.allocator.inner).contains(id), + )] for i in range.clone() { - allocator.alloc_specific(i as usize); + proof_decl! { + let ghost old_view = id_alloc_view(&allocator_inner.allocator.inner); + } + proof! { + assert((i as usize) == + (range.start as int + allocation_iter.index()) as usize); + assert((i as usize) < id_alloc_capacity(&allocator_inner.allocator.inner)); + assert(!id_alloc_view(&allocator_inner.allocator.inner).contains(i as usize)); + assert(allocator_inner.tracked_allocated@.value().subset_of( + old_view.insert(i as usize), + )) by { + assert forall|id: usize| + allocator_inner.tracked_allocated@.value().contains(id) implies + old_view.insert(i as usize).contains(id) by { + } + } + assert(io_port_inner_inv_values( + allocator_inner.tracked_instance@.id(), + allocator_inner.tracked_allocated@.instance_id(), + allocator_inner.tracked_allocated@.value(), + &allocator_inner.allocator.inner, + )); + } + #[verus_spec(with + Ghost(allocator_inner.tracked_instance@.id()), + Ghost(allocator_inner.tracked_allocated@.instance_id()), + Ghost(allocator_inner.tracked_allocated@.value()), + )] + let _ = allocator_inner.allocator.alloc_specific(i as usize); + proof! { + lemma_port_id_set_insert(range.start as usize, i as usize); + assert(id_alloc_view(&allocator_inner.allocator.inner) =~= + old_view.insert(i as usize)); + } + } + proof! { + assert(ids.disjoint(allocator_inner.tracked_allocated@.value())) by { + assert forall|id: usize| #[trigger] ids.contains(id) implies + !allocator_inner.tracked_allocated@.value().contains(id) by { + } + } + range_claim = allocator_inner.tracked_instance.borrow().allocate( + ids, + allocator_inner.tracked_allocated.borrow_mut(), + ); } // SAFETY: The created IoPort is guaranteed not to access system device I/O - unsafe { Some(IoPort::new(port)) } + let result = unsafe { Some(IoPort::new(port)) }; + allocator.drop(); + proof_with!(|= Tracked(Some(range_claim))); + result } /// Recycles an PIO range. @@ -118,12 +393,47 @@ impl IoPortAllocator { /// # Safety /// /// The caller must have ownership of the PIO region through the `IoPortAllocator::acquire` interface. + #[verus_spec( + with + Tracked(claim): Tracked, + requires + claim.instance_id() == io_port_allocator_instance_id(), + claim.set() =~= port_id_set(range.start as usize, range.end as usize), + range.start <= range.end, + )] pub(in crate::io) unsafe fn recycle(&self, range: Range) { /* debug!("Recycling MMIO range: {:#x?}", range); */ - self.allocator - .lock() + let mut allocator = self.allocator.lock(); + let allocator_inner = &mut *allocator; + proof_decl! { + let ghost ids = port_id_set(range.start as usize, range.end as usize); + } + proof! { + use_type_invariant(&*allocator_inner); + assert(range.start as usize <= range.end as usize); + allocator_inner.tracked_instance.borrow().release( + ids, + allocator_inner.tracked_allocated.borrow_mut(), + claim, + ); + assert forall|id: usize| + #[trigger] allocator_inner.tracked_allocated@.value().contains(id) implies { + &&& id_alloc_view(&allocator_inner.allocator.inner).contains(id) + &&& !(range.start as usize <= id < range.end as usize) + } by { + lemma_port_id_set_contains( + range.start as usize, + range.end as usize, + id, + ); + } + } + allocator_inner + .allocator + .inner .free_consecutive(range.start as usize..range.end as usize); + allocator.drop(); } } @@ -177,7 +487,21 @@ pub(crate) unsafe fn init() { } } - IO_PORT_ALLOCATOR.call_once(|| IoPortAllocator { - allocator: SpinLock::new(allocator), + IO_PORT_ALLOCATOR.call_once(|| { + proof_decl! { + let tracked (Tracked(instance), Tracked(allocated), Tracked(_empty_claims)) = + IoPortAllocationState::Instance::initialize(); + } + let inner = IoPortAllocatorInner { + allocator: ModeledIdAlloc { inner: allocator }, + #[cfg(verus_keep_ghost_body)] + tracked_instance: Tracked::new(instance), + #[cfg(verus_keep_ghost_body)] + tracked_allocated: Tracked::new(allocated), + }; + // Original Rust: `IoPortAllocator { allocator: SpinLock::new(allocator) }`. + IoPortAllocator { + allocator: SpinLock::new(inner), + } }); } diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index 54ac0e99e..c541cb1b5 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -2,7 +2,9 @@ //! I/O port and its allocator that allocates port I/O (PIO) to device drivers. use vstd::prelude::*; -use crate::arch::device::io_port::{IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite}; +use crate::arch::device::io_port::{ + IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite, valid_io_port_access, +}; mod allocator; use core::{marker::PhantomData, mem::size_of}; @@ -39,6 +41,53 @@ impl View for IoPort { } } +impl IoPort { + /// The complete byte range occupied by this typed port lies in the x86 PIO address space. + #[verifier::type_invariant] + pub open spec fn well_formed(&self) -> bool { + valid_io_port_access::(self@ as int) + } + + /// Whether `claim` is the allocator-issued ownership token for this complete typed range. + pub open spec fn claim_matches_set(&self, claim: Set) -> bool { + claim =~= port_id_set(self@ as usize, (self@ as usize + size_of::()) as usize) + } +} + +/// Set of byte-sized PIO numbers in the half-open interval `[start, end)`. +pub open spec fn port_id_set(start: usize, end: usize) -> Set + decreases end - start, +{ + if start < end { + port_id_set(start, (end - 1) as usize).insert((end - 1) as usize) + } else { + Set::empty() + } +} + +/// Extending a PIO interval by one byte is equivalent to inserting its old endpoint. +pub proof fn lemma_port_id_set_insert(start: usize, end: usize) + requires + start <= end, + end < usize::MAX, + ensures + port_id_set(start, end).insert(end) == port_id_set(start, (end + 1) as usize), +{ +} + +/// Membership characterization for [`port_id_set`]. +pub proof fn lemma_port_id_set_contains(start: usize, end: usize, id: usize) + requires + start <= end, + ensures + port_id_set(start, end).contains(id) <==> start <= id < end, + decreases end - start, +{ + if start < end { + lemma_port_id_set_contains(start, (end - 1) as usize, id); + } +} + } // verus! /// Returns the initialized global PIO allocator. /// @@ -57,17 +106,30 @@ fn initialized_allocator() -> &'static allocator::IoPortAllocator { impl IoPort { /// Acquires an `IoPort` instance for the given range. #[verus_spec(result => + with + -> claim: Tracked>, requires + vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, allocator::io_port_allocator_initialized(), ensures result is Ok ==> result->Ok_0@ == port, + result is Ok ==> result->Ok_0.well_formed(), + result is Ok <==> claim@ is Some, + result is Ok ==> claim@->Some_0.instance_id() == + allocator::io_port_allocator_instance_id(), + result is Ok ==> result->Ok_0.claim_matches_set(claim@->Some_0.set()), )] pub fn acquire(port: u16) -> Result> { - initialized_allocator() - .acquire(port) - .ok_or(Error::AccessDenied) + proof_decl! { + let tracked claim: Option; + } + #[verus_spec(with => Tracked(claim))] + let port = initialized_allocator().acquire(port); + let result = port.ok_or(Error::AccessDenied); + proof_with!(|= Tracked(claim)); + result } /// Returns the port number. @@ -89,10 +151,12 @@ impl IoPort { /// a privileged operation. #[verus_spec(ret => requires + vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, ensures ret@ == port, + ret.well_formed(), )] pub const unsafe fn new(port: u16) -> Self { Self { @@ -101,6 +165,27 @@ impl IoPort { access_marker: PhantomData, } } + + /// Releases the allocator claim for this port. + /// + /// VERUS LIMITATION: this is called explicitly because Verus does not yet support proving the + /// standard `Drop` implementation below. + #[verus_spec( + with + Tracked(claim): Tracked, + requires + allocator::io_port_allocator_initialized(), + claim.instance_id() == allocator::io_port_allocator_instance_id(), + self.claim_matches_set(claim.set()), + self@ as usize + size_of::() <= u16::MAX, + )] + pub fn drop(self) { + let range = self.port..(self.port + size_of::() as u16); + unsafe { + #[verus_spec(with Tracked(claim))] + initialized_allocator().recycle(range); + } + } } #[verus_verify] @@ -108,6 +193,7 @@ impl IoPort { impl IoPort { /// Reads from the I/O port #[inline] + #[verus_spec(requires self.well_formed())] pub fn read(&self) -> T { unsafe { PortRead::read_from_port(self.port) } } @@ -118,12 +204,13 @@ impl IoPort { impl IoPort { /// Writes to the I/O port #[inline] + #[verus_spec(requires self.well_formed())] pub fn write(&self, value: T) { unsafe { PortWrite::write_to_port(self.port, value) } } } -impl Drop for IoPort { +/* impl Drop for IoPort { fn drop(&mut self) { // SAFETY: The caller have ownership of the PIO region. unsafe { @@ -133,7 +220,7 @@ impl Drop for IoPort { .recycle(self.port..(self.port + size_of::() as u16)); } } -} +} */ /// Reserves an I/O port range which may refer to the port I/O range used by the /// system device driver. From e4775af1b81849c282623e7efce18aaa4b6289ba Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Tue, 25 Aug 2026 10:45:53 +0800 Subject: [PATCH 04/30] refactor: move x86 I/O port specs to vstd_extra --- Cargo.lock | 1 + Cargo.toml | 2 +- ostd/src/arch/x86/device/io_port.rs | 60 +------------------ verified_libs/vstd_extra/Cargo.toml | 3 + .../vstd_extra/src/external/io_port.rs | 60 +++++++++++++++++++ verified_libs/vstd_extra/src/external/mod.rs | 4 ++ 6 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 verified_libs/vstd_extra/src/external/io_port.rs diff --git a/Cargo.lock b/Cargo.lock index 2ce4cdc68..b49ac284f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -672,6 +672,7 @@ name = "vstd_extra" version = "0.1.0" dependencies = [ "vstd", + "x86_64", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2a3b5dca8..b7836d68d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,4 +51,4 @@ codegen-units = 1 [workspace.dependencies] # Verus vstd = { path = "tools/verus/source/vstd", default-features = false, features = ["alloc"] } -verus_state_machines_macros = { path = "tools/verus/source/state_machines_macros" } \ No newline at end of file +verus_state_machines_macros = { path = "tools/verus/source/state_machines_macros" } diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index 10bc5b58f..99c6fade2 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port access. +pub use vstd_extra::external::{valid_io_port_access, valid_io_port_number}; pub use x86_64::{ instructions::port::{ PortReadAccess as IoPortReadAccess, PortWriteAccess as IoPortWriteAccess, ReadOnlyAccess, @@ -7,62 +8,3 @@ pub use x86_64::{ }, structures::port::{PortRead, PortWrite}, }; - -use vstd::prelude::*; - -use core::mem::size_of; - -verus! { - -/// Whether `port` is representable in the 16-bit x86 I/O-port address space. -/// -/// This is only the ISA-level validity condition. It does not claim that a device decodes the -/// port, that the current CPU context may access it, or that the caller owns it. -pub open spec fn valid_io_port_number(port: int) -> bool { - 0 <= port <= u16::MAX as int -} - -/// Whether an access of type `T` is fully contained in the x86 I/O-port address space. -/// -/// OSTD allocates one port byte for every byte in `T`, so this is stronger than merely checking -/// that the starting port is representable. -pub open spec fn valid_io_port_access(port: int) -> bool { - &&& valid_io_port_number(port) - &&& port + size_of::() <= u16::MAX as int + 1 -} - -/// Opaque specification boundary for the third-party read/write access marker. -#[verifier::external_type_specification] -#[verifier::external_body] -pub struct ExReadWriteAccess(ReadWriteAccess); - -/// Opaque specification boundary for the third-party write-only access marker. -#[verifier::external_type_specification] -#[verifier::external_body] -pub struct ExWriteOnlyAccess(WriteOnlyAccess); - -/// Trusted specification boundary for values that can be read from an x86 I/O port. -#[verifier::external_trait_specification] -pub trait ExPortRead { - type ExternalTraitSpecificationFor: PortRead; - - /// A port read can produce any value supplied by the device. - unsafe fn read_from_port(port: u16) -> Self where Self: Sized - requires - valid_io_port_access::(port as int), - ; -} - -/// Trusted specification boundary for values that can be written to an x86 I/O port. -#[verifier::external_trait_specification] -pub trait ExPortWrite { - type ExternalTraitSpecificationFor: PortWrite; - - /// A port write has no modeled logical effect on kernel memory. - unsafe fn write_to_port(port: u16, value: Self) where Self: Sized - requires - valid_io_port_access::(port as int), - ; -} - -} // verus! diff --git a/verified_libs/vstd_extra/Cargo.toml b/verified_libs/vstd_extra/Cargo.toml index 2f02cd467..1455796e1 100644 --- a/verified_libs/vstd_extra/Cargo.toml +++ b/verified_libs/vstd_extra/Cargo.toml @@ -15,3 +15,6 @@ std = ["vstd/std"] [dependencies] vstd = { workspace = true } + +[target.'cfg(target_arch = "x86_64")'.dependencies] +x86_64 = "0.14.13" diff --git a/verified_libs/vstd_extra/src/external/io_port.rs b/verified_libs/vstd_extra/src/external/io_port.rs new file mode 100644 index 000000000..96b810b22 --- /dev/null +++ b/verified_libs/vstd_extra/src/external/io_port.rs @@ -0,0 +1,60 @@ +//! Specifications for x86 I/O-port access types and traits. +use core::mem::size_of; + +use vstd::prelude::*; +use x86_64::{ + instructions::port::{ReadWriteAccess, WriteOnlyAccess}, + structures::port::{PortRead, PortWrite}, +}; + +verus! { + +/// Whether `port` is representable in the 16-bit x86 I/O-port address space. +/// +/// This is only the ISA-level validity condition. It does not claim that a device decodes the +/// port, that the current CPU context may access it, or that the caller owns it. +pub open spec fn valid_io_port_number(port: int) -> bool { + 0 <= port <= u16::MAX as int +} + +/// Whether an access of type `T` is fully contained in the x86 I/O-port address space. +pub open spec fn valid_io_port_access(port: int) -> bool { + &&& valid_io_port_number(port) + &&& port + size_of::() <= u16::MAX as int + 1 +} + +/// Opaque specification boundary for the third-party read/write access marker. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExReadWriteAccess(ReadWriteAccess); + +/// Opaque specification boundary for the third-party write-only access marker. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExWriteOnlyAccess(WriteOnlyAccess); + +/// Trusted specification boundary for values that can be read from an x86 I/O port. +#[verifier::external_trait_specification] +pub trait ExPortRead { + type ExternalTraitSpecificationFor: PortRead; + + /// A port read can produce any value supplied by the device. + unsafe fn read_from_port(port: u16) -> Self where Self: Sized + requires + valid_io_port_access::(port as int), + ; +} + +/// Trusted specification boundary for values that can be written to an x86 I/O port. +#[verifier::external_trait_specification] +pub trait ExPortWrite { + type ExternalTraitSpecificationFor: PortWrite; + + /// A port write has no modeled logical effect on kernel memory. + unsafe fn write_to_port(port: u16, value: Self) where Self: Sized + requires + valid_io_port_access::(port as int), + ; +} + +} // verus! diff --git a/verified_libs/vstd_extra/src/external/mod.rs b/verified_libs/vstd_extra/src/external/mod.rs index f4071583b..b399bf1e0 100644 --- a/verified_libs/vstd_extra/src/external/mod.rs +++ b/verified_libs/vstd_extra/src/external/mod.rs @@ -7,6 +7,8 @@ pub mod convert; pub mod deref; pub mod ilog2; pub mod int_specs; +#[cfg(target_arch = "x86_64")] +pub mod io_port; pub mod nonnull; pub mod ptr; pub mod range; @@ -17,6 +19,8 @@ pub mod time; pub use btree::*; pub use ilog2::*; pub use int_specs::*; +#[cfg(target_arch = "x86_64")] +pub use io_port::*; pub use nonnull::*; pub use ptr::*; pub use range::*; From 6750b28f84b2d529dc078c0e37ba054d2a840e29 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 15:31:01 +0800 Subject: [PATCH 05/30] refactor: replace state machine macros with ghost tokens --- Cargo.lock | 1 - Cargo.toml | 1 - ostd/Cargo.toml | 1 - ostd/src/io/io_mem/allocator.rs | 38 +++++++- ostd/src/io/io_mem/mod.rs | 1 + ostd/src/io/io_port/allocator.rs | 152 +++++++++++++++---------------- 6 files changed, 109 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdc0fc1ae..b2b577219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,7 +331,6 @@ dependencies = [ "spin", "tdx-guest", "unwinding", - "verus_state_machines_macros", "volatile 0.6.1", "vstd", "vstd_extra", diff --git a/Cargo.toml b/Cargo.toml index b7836d68d..0963ca334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,4 +51,3 @@ codegen-units = 1 [workspace.dependencies] # Verus vstd = { path = "tools/verus/source/vstd", default-features = false, features = ["alloc"] } -verus_state_machines_macros = { path = "tools/verus/source/state_machines_macros" } diff --git a/ostd/Cargo.toml b/ostd/Cargo.toml index e1e1e8591..830b3a55d 100644 --- a/ostd/Cargo.toml +++ b/ostd/Cargo.toml @@ -18,7 +18,6 @@ targets = ["x86_64-unknown-none"] [dependencies] vstd = { workspace = true } vstd_extra = { path = "../verified_libs/vstd_extra" } -verus_state_machines_macros = { workspace = true } bitflags = { path = "../verified_libs/bitflags" } align_ext = { path = "libs/align_ext", version = "0.1.0" } bit_field = "0.10.1" diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index cfa98c8aa..4e5cff9c2 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -3,6 +3,7 @@ use crate::specs::arch::PAGE_SIZE; use crate::sync::{OnceImpl, TrivialPred}; use vstd::prelude::*; +use vstd_extra::resource::flags::OneShotSet; use alloc::vec::Vec; use core::ops::Range; @@ -32,15 +33,26 @@ impl IoMemAllocator { vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), range.start < range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), + io_mem_range_registered(range), ensures result is Some ==> result->Some_0.paddr_spec() == range.start, result is Some ==> result->Some_0.length_spec() == vstd_extra::external::range::range_usize_len_spec(&range), )] pub fn acquire(&self, range: Range) -> Option { - find_allocator(&self.allocators, &range)? - .alloc_specific(&range) - .ok()?; + let allocator = find_allocator(&self.allocators, &range)?; + proof! { + // Trusted boot fact (`io_mem_range_registered`): `range` was registered inside a + // single builder window, and `find_allocator` returns its first overlapping window, + // which is the containing window because the registered windows are disjoint. + assume(allocator@.start <= range.start && range.end <= allocator@.end); + } + proof_decl! { + let tracked initialized: OneShotSet; + } + let result = #[verus_spec(with => Tracked(initialized))] + allocator.alloc_specific(&range); + result.ok()?; /* debug!("Acquiring MMIO range:{:x?}..{:x?}", range.start, range.end); */ @@ -111,13 +123,23 @@ impl IoMemAllocatorBuilder { requires range.start < range.end, vstd_extra::panic::may_panic(), + io_mem_range_registered(range), )] pub(crate) fn remove(&self, range: Range) { // Formatting machinery used by the original panic is not modeled by Verus. // Original Rust used two formatted `panic!` branches here. let allocator = find_allocator(&self.allocators, &range); vstd_extra::assert!(allocator.is_some()); - let result = allocator.unwrap().alloc_specific(&range); + let allocator = allocator.unwrap(); + proof! { + // Trusted boot fact, same reasoning as in `acquire`. + assume(allocator@.start <= range.start && range.end <= allocator@.end); + } + proof_decl! { + let tracked initialized: OneShotSet; + } + let result = #[verus_spec(with => Tracked(initialized))] + allocator.alloc_specific(&range); vstd_extra::assert!(result.is_ok()); } } @@ -125,6 +147,14 @@ impl IoMemAllocatorBuilder { /// The I/O Memory allocator of the system. verus! { +/// Trusted boot-state fact required before allocating or removing a `range`. +/// +/// Verus cannot mention an exec static in a specification, so this predicate is the explicit +/// specification boundary for the boot-time guarantee that `range` was registered inside a +/// single MMIO window of the builder (and the windows are pairwise disjoint), which is what +/// `RangeAllocator::alloc_specific` requires. +pub uninterp spec fn io_mem_range_registered(range: Range) -> bool; + pub exec static IO_MEM_ALLOCATOR: OnceImpl ensures IO_MEM_ALLOCATOR.wf(), diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index c92c2a947..8c7d91153 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -74,6 +74,7 @@ impl IoMem { vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), range.start < range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), + allocator::io_mem_range_registered(range), ensures result is Ok ==> result->Ok_0.paddr_spec() == range.start, result is Ok ==> result->Ok_0.length_spec() diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index ff4cad016..1917aa128 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port allocator. -use verus_state_machines_macros::tokenized_state_machine; use vstd::prelude::*; -use vstd::tokens::{InstanceId, SetToken, ValueToken}; +use vstd::resource::set::{GhostSetAuth, GhostSubset}; +use vstd::tokens::InstanceId; use core::ops::Range; @@ -40,12 +40,10 @@ pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; pub uninterp spec fn io_port_allocator_instance_id() -> InstanceId; closed spec fn io_port_inner_inv_values( - instance_id: InstanceId, allocated_instance_id: InstanceId, allocated: Set, allocator: &IdAlloc, ) -> bool { - &&& instance_id == io_port_allocator_instance_id() &&& allocated_instance_id == io_port_allocator_instance_id() &&& allocated.subset_of(id_alloc_view(allocator)) &&& id_alloc_capacity(allocator) == crate::arch::io::MAX_IO_PORT as usize @@ -104,24 +102,17 @@ struct ModeledIdAlloc { impl ModeledIdAlloc { #[verus_spec(result => with - Ghost(instance_id): Ghost, Ghost(allocated_instance_id): Ghost, Ghost(preserved): Ghost>, requires id < id_alloc_capacity(&old(self).inner), - io_port_inner_inv_values( - instance_id, - allocated_instance_id, - preserved, - &old(self).inner, - ), + io_port_inner_inv_values(allocated_instance_id, preserved, &old(self).inner), ensures id_alloc_capacity(&final(self).inner) == id_alloc_capacity(&old(self).inner), id_alloc_view(&final(self).inner) == id_alloc_view(&old(self).inner).insert(id), id_alloc_view(&old(self).inner).subset_of(id_alloc_view(&final(self).inner)), preserved.subset_of(id_alloc_view(&final(self).inner)), io_port_inner_inv_values( - instance_id, allocated_instance_id, preserved, &final(self).inner, @@ -137,66 +128,87 @@ impl ModeledIdAlloc { verus! { -tokenized_state_machine! { - IoPortAllocationState { - fields { - #[sharding(variable)] - pub allocated: Set, - #[sharding(set)] - pub claims: Set, - } +/// Authority over the set of PIO ids currently allocated by the global allocator. +/// +/// The `Loc` of `auth` identifies the protocol instance and `auth@` is the set of allocated +/// ids. [`IoPortClaim`] fragments minted by [`IoPortAllocation::allocate`] transfer the +/// ownership of an acquired PIO range to the caller. +pub(super) tracked struct IoPortAllocation { + auth: GhostSetAuth, +} - #[invariant] - pub fn allocated_matches_claims(&self) -> bool { - self.allocated =~= self.claims - } +/// Fragment claiming ownership of a PIO id range handed out by [`IoPortAllocator::acquire`]. +/// +/// A claim asserts that its ids are allocated by the [`IoPortAllocation`] with the same +/// instance identity, and is consumed when the ids are released by +/// [`IoPortAllocator::recycle`]. +pub(super) tracked struct IoPortClaim { + subset: GhostSubset, +} - init! { - initialize() { - init allocated = Set::empty(); - init claims = Set::empty(); - } - } +impl IoPortAllocation { + /// Instance identity of the protocol. + pub closed spec fn instance_id(self) -> InstanceId { + self.auth.id() + } - transition! { - allocate(ids: Set) { - require pre.allocated.disjoint(ids); - update allocated = pre.allocated.union(ids); - add claims += (ids) by { - assert(pre.claims =~= pre.allocated); - }; - } - } + /// Ids currently allocated. + pub closed spec fn value(self) -> Set { + self.auth@ + } - transition! { - release(ids: Set) { - update allocated = pre.allocated.difference(ids); - remove claims -= (ids); - } - } + /// Creates a fresh protocol instance with an empty allocated set. + pub proof fn initialize() -> (tracked result: Self) { + let tracked (auth, _empty_claims) = GhostSetAuth::new(Set::empty()); + Self { auth } + } - #[inductive(initialize)] - fn initialize_inductive(post: Self) {} + /// Allocates `ids`, requiring them to be currently free, and mints the matching claim + /// fragment. + pub proof fn allocate(tracked &mut self, ids: Set) -> (tracked claim: IoPortClaim) + requires + old(self).value().disjoint(ids), + ensures + final(self).instance_id() == old(self).instance_id(), + final(self).value() == old(self).value().union(ids), + claim.instance_id() == final(self).instance_id(), + claim.set() == ids, + { + let tracked subset = self.auth.insert_set(ids); + IoPortClaim { subset } + } - #[inductive(allocate)] - fn allocate_inductive(pre: Self, post: Self, ids: Set) {} + /// Consumes `claim`, removing its ids from the allocated set. + pub proof fn release(tracked &mut self, tracked claim: IoPortClaim) + requires + claim.instance_id() == old(self).instance_id(), + ensures + final(self).instance_id() == old(self).instance_id(), + final(self).value() == old(self).value().difference(claim.set()), + { + self.auth.delete(claim.subset); + } +} - #[inductive(release)] - fn release_inductive(pre: Self, post: Self, ids: Set) {} +impl IoPortClaim { + /// Instance identity of the protocol that issued this claim. + pub closed spec fn instance_id(self) -> InstanceId { + self.subset.id() + } + + /// Ids claimed by this fragment. + pub closed spec fn set(self) -> Set { + self.subset@ } } } // verus! -pub(super) type IoPortClaim = IoPortAllocationState::claims_set; - -/// Lock-protected executable bitmap and the state-machine token that models it. +/// Lock-protected executable bitmap and the ghost allocation token. #[verus_verify] struct IoPortAllocatorInner { allocator: ModeledIdAlloc, #[cfg(verus_keep_ghost_body)] - tracked_instance: Tracked, - #[cfg(verus_keep_ghost_body)] - tracked_allocated: Tracked, + tracked_allocated: Tracked, } verus! { @@ -205,7 +217,6 @@ impl IoPortAllocatorInner { #[verifier::type_invariant] pub closed spec fn type_inv(self) -> bool { io_port_inner_inv_values( - self.tracked_instance@.id(), self.tracked_allocated@.instance_id(), self.tracked_allocated@.value(), &self.allocator.inner, @@ -312,7 +323,6 @@ impl IoPortAllocator { #[verus_spec(allocation_iter => invariant range.end as usize <= id_alloc_capacity(&allocator_inner.allocator.inner), - allocator_inner.tracked_instance@.id() == io_port_allocator_instance_id(), allocator_inner.tracked_allocated@.instance_id() == io_port_allocator_instance_id(), allocator_inner.tracked_allocated@.value().subset_of(allocation_start_view), @@ -351,14 +361,12 @@ impl IoPortAllocator { } } assert(io_port_inner_inv_values( - allocator_inner.tracked_instance@.id(), allocator_inner.tracked_allocated@.instance_id(), allocator_inner.tracked_allocated@.value(), &allocator_inner.allocator.inner, )); } #[verus_spec(with - Ghost(allocator_inner.tracked_instance@.id()), Ghost(allocator_inner.tracked_allocated@.instance_id()), Ghost(allocator_inner.tracked_allocated@.value()), )] @@ -375,10 +383,7 @@ impl IoPortAllocator { !allocator_inner.tracked_allocated@.value().contains(id) by { } } - range_claim = allocator_inner.tracked_instance.borrow().allocate( - ids, - allocator_inner.tracked_allocated.borrow_mut(), - ); + range_claim = allocator_inner.tracked_allocated.borrow_mut().allocate(ids); } // SAFETY: The created IoPort is guaranteed not to access system device I/O @@ -406,17 +411,11 @@ impl IoPortAllocator { let mut allocator = self.allocator.lock(); let allocator_inner = &mut *allocator; - proof_decl! { - let ghost ids = port_id_set(range.start as usize, range.end as usize); - } proof! { use_type_invariant(&*allocator_inner); assert(range.start as usize <= range.end as usize); - allocator_inner.tracked_instance.borrow().release( - ids, - allocator_inner.tracked_allocated.borrow_mut(), - claim, - ); + assert(claim.instance_id() == allocator_inner.tracked_allocated@.instance_id()); + allocator_inner.tracked_allocated.borrow_mut().release(claim); assert forall|id: usize| #[trigger] allocator_inner.tracked_allocated@.value().contains(id) implies { &&& id_alloc_view(&allocator_inner.allocator.inner).contains(id) @@ -489,19 +488,16 @@ pub(crate) unsafe fn init() { IO_PORT_ALLOCATOR.call_once(|| { proof_decl! { - let tracked (Tracked(instance), Tracked(allocated), Tracked(_empty_claims)) = - IoPortAllocationState::Instance::initialize(); + let tracked allocated = IoPortAllocation::initialize(); } let inner = IoPortAllocatorInner { allocator: ModeledIdAlloc { inner: allocator }, #[cfg(verus_keep_ghost_body)] - tracked_instance: Tracked::new(instance), - #[cfg(verus_keep_ghost_body)] tracked_allocated: Tracked::new(allocated), }; // Original Rust: `IoPortAllocator { allocator: SpinLock::new(allocator) }`. IoPortAllocator { - allocator: SpinLock::new(inner), + allocator: SpinLock::new(inner, Ghost::new(()), Tracked::new(())), } }); } From 0eb7a36a31adb42b31422692cddf704d8e6dcd81 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 16:59:22 +0800 Subject: [PATCH 06/30] prove: derive io_mem window ordering from the boot builder --- ostd/src/arch/x86/io.rs | 3 +- ostd/src/boot/memory_region.rs | 3 +- ostd/src/boot/mod.rs | 8 +- ostd/src/io/io_mem/allocator.rs | 146 ++++++++++++++++++++++++++++--- ostd/src/io/io_port/allocator.rs | 2 +- 5 files changed, 142 insertions(+), 20 deletions(-) diff --git a/ostd/src/arch/x86/io.rs b/ostd/src/arch/x86/io.rs index 35913c17f..36fe128a4 100644 --- a/ostd/src/arch/x86/io.rs +++ b/ostd/src/arch/x86/io.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 use vstd::prelude::*; -/*use alloc::vec::Vec; +use alloc::vec::Vec; use align_ext::AlignExt; @@ -57,7 +57,6 @@ pub(super) fn construct_io_mem_allocator_builder() -> IoMemAllocatorBuilder { // SAFETY: The range is guaranteed not to access physical memory. unsafe { IoMemAllocatorBuilder::new(ranges) } } -*/ /// Port I/O definition reference: . #[verus_verify] pub const MAX_IO_PORT: u16 = u16::MAX; diff --git a/ostd/src/boot/memory_region.rs b/ostd/src/boot/memory_region.rs index 8b36d1a02..9806d5972 100644 --- a/ostd/src/boot/memory_region.rs +++ b/ostd/src/boot/memory_region.rs @@ -261,14 +261,13 @@ impl Default for MemoryRegionArray { Self::new() } } -/* impl Deref for MemoryRegionArray { type Target = [MemoryRegion]; fn deref(&self) -> &Self::Target { &self.regions[..self.count] } -}*/ +} #[verus_verify] impl MemoryRegionArray { /// Constructs an empty set. diff --git a/ostd/src/boot/mod.rs b/ostd/src/boot/mod.rs index bdcffdb7f..8eb8cc4ce 100644 --- a/ostd/src/boot/mod.rs +++ b/ostd/src/boot/mod.rs @@ -12,10 +12,10 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; - -use memory_region::{MemoryRegion, MemoryRegionArray}; +*/ +use memory_region::{/* MemoryRegion, */ MemoryRegionArray}; use spin::Once; - +/* /// The boot information provided by the bootloader. pub struct BootInfo { /// The name of the bootloader. @@ -38,6 +38,7 @@ pub fn boot_info() -> &'static BootInfo { } static INFO: Once = Once::new(); +*/ /// ACPI information from the bootloader. /// @@ -91,6 +92,7 @@ pub(crate) struct EarlyBootInfo { /// The boot-time information. pub(crate) static EARLY_INFO: Once = Once::new(); +/* /// Initializes the boot information. /// /// This function copies the boot-time accessible information to the heap to diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 4e5cff9c2..2d6a3bc9e 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -42,10 +42,8 @@ impl IoMemAllocator { pub fn acquire(&self, range: Range) -> Option { let allocator = find_allocator(&self.allocators, &range)?; proof! { - // Trusted boot fact (`io_mem_range_registered`): `range` was registered inside a - // single builder window, and `find_allocator` returns its first overlapping window, - // which is the containing window because the registered windows are disjoint. - assume(allocator@.start <= range.start && range.end <= allocator@.end); + use_type_invariant(self); + lemma_found_window_contains(&self.allocators, &range, allocator); } proof_decl! { let tracked initialized: OneShotSet; @@ -81,7 +79,10 @@ impl IoMemAllocator { /// # Safety /// /// User must ensure the range doesn't belong to physical memory or system device I/O. - #[verus_verify] + #[verus_spec(ret => + requires + windows_ordered(allocators@), + )] unsafe fn new(allocators: Vec) -> Self { Self { allocators } } @@ -103,15 +104,43 @@ impl IoMemAllocatorBuilder { /// # Safety /// /// User must ensure the range doesn't belong to physical memory. - #[verus_verify] + #[verus_spec(ret => + requires + usize_ranges_ordered(ranges@), + ensures + ret.type_inv(), + )] pub(crate) unsafe fn new(ranges: Vec>) -> Self { /* info!( "Creating new I/O memory allocator builder, ranges: {:#x?}", ranges ); */ - let mut allocators = Vec::with_capacity(ranges.len()); + let mut allocators: Vec = Vec::with_capacity(ranges.len()); + #[verus_spec(it => + invariant + allocators@.len() == it.index(), + forall|j: int| 0 <= j < it.index() ==> { + &&& allocators@[j]@.start == it.seq()[j].start + &&& allocators@[j]@.end == it.seq()[j].end + }, + usize_ranges_ordered(it.seq()), + windows_ordered(allocators@), + )] for range in ranges { + proof! { + assert(range == it.seq()[it.index()]); + } allocators.push(RangeAllocator::new(range)); + proof! { + assert(allocators@[allocators@.len() - 1]@.start == range.start); + assert forall|i: int| + 0 <= i < allocators@.len() - 1 + implies allocators@[i]@.end <= range.start by { + assert(allocators@[i]@.end == it.seq()[i].end); + assert(it.seq()[i].end <= it.seq()[i as int + 1].start); + } + assert(windows_ordered(allocators@)); + } } Self { allocators } } @@ -132,8 +161,8 @@ impl IoMemAllocatorBuilder { vstd_extra::assert!(allocator.is_some()); let allocator = allocator.unwrap(); proof! { - // Trusted boot fact, same reasoning as in `acquire`. - assume(allocator@.start <= range.start && range.end <= allocator@.end); + use_type_invariant(self); + lemma_found_window_contains(&self.allocators, &range, allocator); } proof_decl! { let tracked initialized: OneShotSet; @@ -147,12 +176,94 @@ impl IoMemAllocatorBuilder { /// The I/O Memory allocator of the system. verus! { +broadcast use vstd::std_specs::vec::group_vec_axioms; + +/// The registered MMIO windows are pairwise ordered: every window ends at or before the start +/// of the next one, so no window can partially cover a range that is contained in another one. +pub open spec fn windows_ordered(allocators: Seq) -> bool { + forall|i: int, j: int| + 0 <= i < j < allocators.len() ==> allocators[i]@.end <= allocators[j]@.start +} + +/// The format of the windows handed to [`IoMemAllocatorBuilder::new`]. +pub open spec fn usize_ranges_ordered(ranges: Seq>) -> bool { + forall|i: int, j: int| 0 <= i < j < ranges.len() ==> ranges[i].end <= ranges[j].start +} + +impl IoMemAllocatorBuilder { + /// The builder always holds the ordered windows handed to [`IoMemAllocatorBuilder::new`]. + #[verifier::type_invariant] + pub closed spec fn type_inv(self) -> bool { + windows_ordered(self.allocators@) + } +} + +impl IoMemAllocator { + /// The allocator inherits the ordered windows of the builder it was built from. + #[verifier::type_invariant] + pub closed spec fn type_inv(self) -> bool { + windows_ordered(self.allocators@) + } +} + +/// The trusted boot fact [`io_mem_range_registered`], made concrete: the index of the +/// registered window containing `range`. +pub proof fn lemma_registered_window(windows: &Vec, range: Range) -> (idx: + int) + requires + windows_ordered(windows@), + io_mem_range_registered(range), + ensures + 0 <= idx < windows@.len(), + windows@[idx]@.start <= range.start && range.end <= windows@[idx]@.end, +{ + assume(exists|m: int| + 0 <= m < windows@.len() && windows@[m]@.start <= range.start && range.end + <= windows@[m]@.end); + let idx = choose|m: int| + 0 <= m < windows@.len() && windows@[m]@.start <= range.start && range.end + <= windows@[m]@.end; + idx +} + +/// The window overlapping `range` found by [`find_allocator`] is exactly the registered +/// window containing it: ordered windows cannot partially cover a range contained in +/// another window. +pub proof fn lemma_found_window_contains( + windows: &Vec, + range: &Range, + found: &RangeAllocator, +) + requires + windows_ordered(windows@), + io_mem_range_registered(*range), + found@.start < range.end && found@.end > range.start, + exists|k: int| 0 <= k < windows@.len() && windows@[k]@ == found@, + ensures + found@.start <= range.start && range.end <= found@.end, +{ + let container_idx = lemma_registered_window(windows, *range); + let found_idx = choose|k: int| 0 <= k < windows@.len() && windows@[k]@ == found@; + if found_idx < container_idx { + assert(windows@[found_idx]@.end <= windows@[container_idx]@.start); + assert(windows@[container_idx]@.start <= range.start); + assert(false); + } else if found_idx == container_idx { + assert(windows@[found_idx]@.start <= range.start && range.end <= windows@[found_idx]@.end); + } else { + assert(range.end <= windows@[container_idx]@.end); + assert(windows@[container_idx]@.end <= windows@[found_idx]@.start); + assert(false); + } +} + /// Trusted boot-state fact required before allocating or removing a `range`. /// /// Verus cannot mention an exec static in a specification, so this predicate is the explicit -/// specification boundary for the boot-time guarantee that `range` was registered inside a -/// single MMIO window of the builder (and the windows are pairwise disjoint), which is what -/// `RangeAllocator::alloc_specific` requires. +/// specification boundary for the boot-time guarantee that `range` lies within a single window +/// registered by the boot builder. Combined with the ordered-window invariant +/// ([`IoMemAllocator::type_inv`]) it yields exactly what [`RangeAllocator::alloc_specific`] +/// requires; see [`IoMemAllocator::lemma_found_window_contains`]. pub uninterp spec fn io_mem_range_registered(range: Range) -> bool; pub exec static IO_MEM_ALLOCATOR: OnceImpl @@ -171,11 +282,22 @@ pub exec static IO_MEM_ALLOCATOR: OnceImpl /// `remove` function. #[verus_verify] pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { + proof! { + use_type_invariant(&io_mem_builder); + } // SAFETY: The safety is upheld by the caller. IO_MEM_ALLOCATOR.init(unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); } #[verus_verify] +#[verus_spec(ret => + ensures + ret matches Some(res) ==> { + &&& res@.start < range.end + &&& res@.end > range.start + &&& exists|k: int| 0 <= k < allocators@.len() && allocators@[k]@ == res@ + } +)] fn find_allocator<'a>( allocators: &'a [RangeAllocator], range: &Range, diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 1917aa128..3883c663d 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -203,7 +203,7 @@ impl IoPortClaim { } } // verus! -/// Lock-protected executable bitmap and the ghost allocation token. +/// Lock-protected executable bitmap and the state-machine token that models it. #[verus_verify] struct IoPortAllocatorInner { allocator: ModeledIdAlloc, From bc3bf00da70f2b828abf13ff347601d523393b00 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 19:40:07 +0800 Subject: [PATCH 07/30] refactor: add specifications for bitmap-backed ID allocator and update dependencies --- ostd/src/io/io_port/allocator.rs | 53 +--------------- verified_libs/vstd_extra/Cargo.toml | 1 + .../vstd_extra/src/external/id_alloc.rs | 61 +++++++++++++++++++ verified_libs/vstd_extra/src/external/mod.rs | 2 + 4 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 verified_libs/vstd_extra/src/external/id_alloc.rs diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 3883c663d..db36c8314 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -3,6 +3,7 @@ use vstd::prelude::*; use vstd::resource::set::{GhostSetAuth, GhostSubset}; use vstd::tokens::InstanceId; +use vstd_extra::external::{id_alloc_capacity, id_alloc_view}; use core::ops::Range; @@ -18,11 +19,6 @@ use crate::{ verus! { -/// Verus model for the external bitmap-backed ID allocator. -#[verifier::external_type_specification] -#[verifier::external_body] -pub struct ExIdAlloc(IdAlloc); - /// Opaque specification for the third-party one-time initialization primitive. #[verifier::external_type_specification] #[verifier::external_body] @@ -30,12 +26,6 @@ pub struct ExIdAlloc(IdAlloc); #[verifier::reject_recursive_types(R)] pub struct ExOnce(spin::once::Once); -/// IDs currently allocated by an external `IdAlloc`. -pub uninterp spec fn id_alloc_view(allocator: &IdAlloc) -> Set; - -/// Capacity configured for an external `IdAlloc`. -pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; - /// Identity assigned to the single global PIO allocator during trusted boot initialization. pub uninterp spec fn io_port_allocator_instance_id() -> InstanceId; @@ -49,47 +39,6 @@ closed spec fn io_port_inner_inv_values( &&& id_alloc_capacity(allocator) == crate::arch::io::MAX_IO_PORT as usize } -pub assume_specification[ IdAlloc::with_capacity ](capacity: usize) -> (allocator: IdAlloc) - ensures - id_alloc_capacity(&allocator) == capacity, - id_alloc_view(&allocator) == Set::::empty(), -; - -pub assume_specification[ IdAlloc::is_allocated ](allocator: &IdAlloc, id: usize) -> (allocated: - bool) - ensures - id < id_alloc_capacity(allocator) ==> allocated == id_alloc_view(allocator).contains(id), -; - -pub assume_specification[ IdAlloc::alloc_specific ](allocator: &mut IdAlloc, id: usize) -> (result: - Option) - requires - id < id_alloc_capacity(old(allocator)), - ensures - id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id), - id_alloc_view(old(allocator)).subset_of(id_alloc_view(final(allocator))), - id_alloc_view(old(allocator)).contains(id) ==> { - &&& result is None - }, - !id_alloc_view(old(allocator)).contains(id) ==> { - &&& result == Some(id) - }, - no_unwind -; - -pub assume_specification[ IdAlloc::free_consecutive ](allocator: &mut IdAlloc, range: Range) - requires - range.end <= id_alloc_capacity(old(allocator)), - ensures - id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - forall|id: usize| #[trigger] - id_alloc_view(final(allocator)).contains(id) <==> id_alloc_view( - old(allocator), - ).contains(id) && !(range.start <= id < range.end), - no_unwind -; - } // verus! /// Transparent facade used only to pass ghost frame facts to the third-party mutation. #[repr(transparent)] diff --git a/verified_libs/vstd_extra/Cargo.toml b/verified_libs/vstd_extra/Cargo.toml index 1455796e1..6050c4ff1 100644 --- a/verified_libs/vstd_extra/Cargo.toml +++ b/verified_libs/vstd_extra/Cargo.toml @@ -15,6 +15,7 @@ std = ["vstd/std"] [dependencies] vstd = { workspace = true } +id-alloc = { path = "../../ostd/libs/id-alloc" } [target.'cfg(target_arch = "x86_64")'.dependencies] x86_64 = "0.14.13" diff --git a/verified_libs/vstd_extra/src/external/id_alloc.rs b/verified_libs/vstd_extra/src/external/id_alloc.rs new file mode 100644 index 000000000..8e9904339 --- /dev/null +++ b/verified_libs/vstd_extra/src/external/id_alloc.rs @@ -0,0 +1,61 @@ +//! Specifications for the bitmap-backed ID allocator. +use core::ops::Range; + +use id_alloc::IdAlloc; +use vstd::prelude::*; + +verus! { + +/// Verus model for the external bitmap-backed ID allocator. +#[verifier::external_type_specification] +#[verifier::external_body] +pub struct ExIdAlloc(IdAlloc); + +/// IDs currently allocated by an external `IdAlloc`. +pub uninterp spec fn id_alloc_view(allocator: &IdAlloc) -> Set; + +/// Capacity configured for an external `IdAlloc`. +pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; + +pub assume_specification[ IdAlloc::with_capacity ](capacity: usize) -> (allocator: IdAlloc) + ensures + id_alloc_capacity(&allocator) == capacity, + id_alloc_view(&allocator) == Set::::empty(), +; + +pub assume_specification[ IdAlloc::is_allocated ](allocator: &IdAlloc, id: usize) -> (allocated: + bool) + ensures + id < id_alloc_capacity(allocator) ==> allocated == id_alloc_view(allocator).contains(id), +; + +pub assume_specification[ IdAlloc::alloc_specific ](allocator: &mut IdAlloc, id: usize) -> (result: + Option) + requires + id < id_alloc_capacity(old(allocator)), + ensures + id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), + id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id), + id_alloc_view(old(allocator)).subset_of(id_alloc_view(final(allocator))), + id_alloc_view(old(allocator)).contains(id) ==> { + &&& result is None + }, + !id_alloc_view(old(allocator)).contains(id) ==> { + &&& result == Some(id) + }, + no_unwind +; + +pub assume_specification[ IdAlloc::free_consecutive ](allocator: &mut IdAlloc, range: Range) + requires + range.end <= id_alloc_capacity(old(allocator)), + ensures + id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), + forall|id: usize| #[trigger] + id_alloc_view(final(allocator)).contains(id) <==> id_alloc_view( + old(allocator), + ).contains(id) && !(range.start <= id < range.end), + no_unwind +; + +} // verus! diff --git a/verified_libs/vstd_extra/src/external/mod.rs b/verified_libs/vstd_extra/src/external/mod.rs index b399bf1e0..137517a9b 100644 --- a/verified_libs/vstd_extra/src/external/mod.rs +++ b/verified_libs/vstd_extra/src/external/mod.rs @@ -5,6 +5,7 @@ pub mod btree; pub mod convert; pub mod deref; +pub mod id_alloc; pub mod ilog2; pub mod int_specs; #[cfg(target_arch = "x86_64")] @@ -17,6 +18,7 @@ pub mod smart_ptr; pub mod time; pub use btree::*; +pub use id_alloc::*; pub use ilog2::*; pub use int_specs::*; #[cfg(target_arch = "x86_64")] From 2dc5398fede31cb6c5b41fc7654fbbeb9eea90a5 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 20:18:10 +0800 Subject: [PATCH 08/30] refine --- Cargo.lock | 1 + ostd/src/arch/x86/device/io_port.rs | 1 + ostd/src/arch/x86/pci.rs | 6 ------ verified_libs/vstd_extra/src/external/int_specs.rs | 9 +++++++++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2b577219..91f60854b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -670,6 +670,7 @@ dependencies = [ name = "vstd_extra" version = "0.1.0" dependencies = [ + "id-alloc", "vstd", "x86_64", ] diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index 99c6fade2..11b10c0a8 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port access. pub use vstd_extra::external::{valid_io_port_access, valid_io_port_number}; + pub use x86_64::{ instructions::port::{ PortReadAccess as IoPortReadAccess, PortWriteAccess as IoPortWriteAccess, ReadOnlyAccess, diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index 889bbb5ee..ee4cdfcf5 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -7,12 +7,6 @@ use crate::{bus::pci::PciDeviceLocation, io::IoPort, prelude::*}; verus! { -/// x86 is little-endian, so converting a native-endian `u32` to little endian is the identity. -pub assume_specification[ u32::to_le ](value: u32) -> (result: u32) - ensures - result == value, -; - exec static PCI_ADDRESS_PORT: IoPort ensures PCI_ADDRESS_PORT.well_formed(), diff --git a/verified_libs/vstd_extra/src/external/int_specs.rs b/verified_libs/vstd_extra/src/external/int_specs.rs index 1998925a5..a840d2cb5 100644 --- a/verified_libs/vstd_extra/src/external/int_specs.rs +++ b/verified_libs/vstd_extra/src/external/int_specs.rs @@ -27,6 +27,15 @@ pub assume_specification[ u32::is_power_of_two ](self_: u32) -> (r: bool) no_unwind ; +/// On a little-endian target, converting a native-endian `u32` to little endian is the identity. +#[cfg(target_endian = "little")] +pub assume_specification[ u32::to_le ](value: u32) -> (result: u32) + returns + value, + opens_invariants none + no_unwind +; + pub assume_specification[ u64::is_power_of_two ](self_: u64) -> (r: bool) returns is_pow2(self_ as int), From 787495567e23e035ea2b9cf784933542614867d8 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 20:30:38 +0800 Subject: [PATCH 09/30] prove: assume --- ostd/src/io/io_mem/allocator.rs | 59 ++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 2d6a3bc9e..a83b71e9e 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -82,6 +82,7 @@ impl IoMemAllocator { #[verus_spec(ret => requires windows_ordered(allocators@), + windows_match_registered(allocators@), )] unsafe fn new(allocators: Vec) -> Self { Self { allocators } @@ -107,6 +108,7 @@ impl IoMemAllocatorBuilder { #[verus_spec(ret => requires usize_ranges_ordered(ranges@), + usize_ranges_match_registered(ranges@), ensures ret.type_inv(), )] @@ -124,6 +126,9 @@ impl IoMemAllocatorBuilder { &&& allocators@[j]@.end == it.seq()[j].end }, usize_ranges_ordered(it.seq()), + usize_ranges_match_registered(it.seq()), + forall|j: int| 0 <= j < it.index() ==> + allocators@[j]@ == registered_io_mem_windows()[j], windows_ordered(allocators@), )] for range in ranges { @@ -133,6 +138,8 @@ impl IoMemAllocatorBuilder { allocators.push(RangeAllocator::new(range)); proof! { assert(allocators@[allocators@.len() - 1]@.start == range.start); + assert(range.start == registered_io_mem_windows()[it.index()].start); + assert(range.end == registered_io_mem_windows()[it.index()].end); assert forall|i: int| 0 <= i < allocators@.len() - 1 implies allocators@[i]@.end <= range.start by { @@ -142,6 +149,10 @@ impl IoMemAllocatorBuilder { assert(windows_ordered(allocators@)); } } + proof! { + assert(allocators@.len() == registered_io_mem_windows().len()); + assert(windows_match_registered(allocators@)); + } Self { allocators } } @@ -190,11 +201,31 @@ pub open spec fn usize_ranges_ordered(ranges: Seq>) -> bool { forall|i: int, j: int| 0 <= i < j < ranges.len() ==> ranges[i].end <= ranges[j].start } +/// The abstract MMIO windows registered by platform boot code. +pub uninterp spec fn registered_io_mem_windows() -> Seq>; + +/// The concrete range allocators represent the abstract boot-time windows exactly. +pub open spec fn windows_match_registered(allocators: Seq) -> bool { + &&& allocators.len() == registered_io_mem_windows().len() + &&& forall|i: int| + 0 <= i < allocators.len() ==> allocators[i]@ == registered_io_mem_windows()[i] +} + +/// The ranges passed across the unsafe builder boundary represent the abstract windows. +pub open spec fn usize_ranges_match_registered(ranges: Seq>) -> bool { + &&& ranges.len() == registered_io_mem_windows().len() + &&& forall|i: int| + 0 <= i < ranges.len() ==> { + &&& ranges[i].start == registered_io_mem_windows()[i].start + &&& ranges[i].end == registered_io_mem_windows()[i].end + } +} + impl IoMemAllocatorBuilder { /// The builder always holds the ordered windows handed to [`IoMemAllocatorBuilder::new`]. #[verifier::type_invariant] pub closed spec fn type_inv(self) -> bool { - windows_ordered(self.allocators@) + windows_ordered(self.allocators@) && windows_match_registered(self.allocators@) } } @@ -202,7 +233,7 @@ impl IoMemAllocator { /// The allocator inherits the ordered windows of the builder it was built from. #[verifier::type_invariant] pub closed spec fn type_inv(self) -> bool { - windows_ordered(self.allocators@) + windows_ordered(self.allocators@) && windows_match_registered(self.allocators@) } } @@ -212,17 +243,16 @@ pub proof fn lemma_registered_window(windows: &Vec, range: Range int) requires windows_ordered(windows@), + windows_match_registered(windows@), io_mem_range_registered(range), ensures 0 <= idx < windows@.len(), windows@[idx]@.start <= range.start && range.end <= windows@[idx]@.end, { - assume(exists|m: int| - 0 <= m < windows@.len() && windows@[m]@.start <= range.start && range.end - <= windows@[m]@.end); let idx = choose|m: int| - 0 <= m < windows@.len() && windows@[m]@.start <= range.start && range.end - <= windows@[m]@.end; + 0 <= m < registered_io_mem_windows().len() && registered_io_mem_windows()[m].start + <= range.start && range.end <= registered_io_mem_windows()[m].end; + assert(windows@[idx]@ == registered_io_mem_windows()[idx]); idx } @@ -236,6 +266,7 @@ pub proof fn lemma_found_window_contains( ) requires windows_ordered(windows@), + windows_match_registered(windows@), io_mem_range_registered(*range), found@.start < range.end && found@.end > range.start, exists|k: int| 0 <= k < windows@.len() && windows@[k]@ == found@, @@ -257,14 +288,12 @@ pub proof fn lemma_found_window_contains( } } -/// Trusted boot-state fact required before allocating or removing a `range`. -/// -/// Verus cannot mention an exec static in a specification, so this predicate is the explicit -/// specification boundary for the boot-time guarantee that `range` lies within a single window -/// registered by the boot builder. Combined with the ordered-window invariant -/// ([`IoMemAllocator::type_inv`]) it yields exactly what [`RangeAllocator::alloc_specific`] -/// requires; see [`IoMemAllocator::lemma_found_window_contains`]. -pub uninterp spec fn io_mem_range_registered(range: Range) -> bool; +/// A range is registered when one abstract boot-time MMIO window contains it. +pub open spec fn io_mem_range_registered(range: Range) -> bool { + exists|m: int| + 0 <= m < registered_io_mem_windows().len() && registered_io_mem_windows()[m].start + <= range.start && range.end <= registered_io_mem_windows()[m].end +} pub exec static IO_MEM_ALLOCATOR: OnceImpl ensures From d3b93b18abc6729086329100c2cb32c8d7a0de80 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 21:36:37 +0800 Subject: [PATCH 10/30] refine --- ostd/src/io/io_mem/allocator.rs | 24 +++++++++++++++++------- ostd/src/io/io_port/allocator.rs | 4 ++-- ostd/src/io/io_port/mod.rs | 2 +- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index a83b71e9e..f0c58bbff 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -121,13 +121,13 @@ impl IoMemAllocatorBuilder { #[verus_spec(it => invariant allocators@.len() == it.index(), - forall|j: int| 0 <= j < it.index() ==> { + forall|j: int| #![trigger it.seq()[j]] 0 <= j < it.index() ==> { &&& allocators@[j]@.start == it.seq()[j].start &&& allocators@[j]@.end == it.seq()[j].end }, usize_ranges_ordered(it.seq()), usize_ranges_match_registered(it.seq()), - forall|j: int| 0 <= j < it.index() ==> + forall|j: int| #![trigger registered_io_mem_windows()[j]] 0 <= j < it.index() ==> allocators@[j]@ == registered_io_mem_windows()[j], windows_ordered(allocators@), )] @@ -140,7 +140,7 @@ impl IoMemAllocatorBuilder { assert(allocators@[allocators@.len() - 1]@.start == range.start); assert(range.start == registered_io_mem_windows()[it.index()].start); assert(range.end == registered_io_mem_windows()[it.index()].end); - assert forall|i: int| + assert forall|i: int| #![trigger allocators@[i]] 0 <= i < allocators@.len() - 1 implies allocators@[i]@.end <= range.start by { assert(allocators@[i]@.end == it.seq()[i].end); @@ -193,12 +193,15 @@ broadcast use vstd::std_specs::vec::group_vec_axioms; /// of the next one, so no window can partially cover a range that is contained in another one. pub open spec fn windows_ordered(allocators: Seq) -> bool { forall|i: int, j: int| + #![trigger allocators[i], allocators[j]] 0 <= i < j < allocators.len() ==> allocators[i]@.end <= allocators[j]@.start } /// The format of the windows handed to [`IoMemAllocatorBuilder::new`]. pub open spec fn usize_ranges_ordered(ranges: Seq>) -> bool { - forall|i: int, j: int| 0 <= i < j < ranges.len() ==> ranges[i].end <= ranges[j].start + forall|i: int, j: int| + #![trigger ranges[i], ranges[j]] + 0 <= i < j < ranges.len() ==> ranges[i].end <= ranges[j].start } /// The abstract MMIO windows registered by platform boot code. @@ -208,6 +211,7 @@ pub uninterp spec fn registered_io_mem_windows() -> Seq>; pub open spec fn windows_match_registered(allocators: Seq) -> bool { &&& allocators.len() == registered_io_mem_windows().len() &&& forall|i: int| + #![trigger registered_io_mem_windows()[i]] 0 <= i < allocators.len() ==> allocators[i]@ == registered_io_mem_windows()[i] } @@ -215,6 +219,7 @@ pub open spec fn windows_match_registered(allocators: Seq) -> bo pub open spec fn usize_ranges_match_registered(ranges: Seq>) -> bool { &&& ranges.len() == registered_io_mem_windows().len() &&& forall|i: int| + #![trigger ranges[i]] 0 <= i < ranges.len() ==> { &&& ranges[i].start == registered_io_mem_windows()[i].start &&& ranges[i].end == registered_io_mem_windows()[i].end @@ -250,6 +255,7 @@ pub proof fn lemma_registered_window(windows: &Vec, range: Range windows@[idx]@.start <= range.start && range.end <= windows@[idx]@.end, { let idx = choose|m: int| + #![trigger registered_io_mem_windows()[m]] 0 <= m < registered_io_mem_windows().len() && registered_io_mem_windows()[m].start <= range.start && range.end <= registered_io_mem_windows()[m].end; assert(windows@[idx]@ == registered_io_mem_windows()[idx]); @@ -269,12 +275,14 @@ pub proof fn lemma_found_window_contains( windows_match_registered(windows@), io_mem_range_registered(*range), found@.start < range.end && found@.end > range.start, - exists|k: int| 0 <= k < windows@.len() && windows@[k]@ == found@, + exists|k: int| #![trigger windows@[k]] 0 <= k < windows@.len() && windows@[k]@ == found@, ensures found@.start <= range.start && range.end <= found@.end, { let container_idx = lemma_registered_window(windows, *range); - let found_idx = choose|k: int| 0 <= k < windows@.len() && windows@[k]@ == found@; + let found_idx = choose|k: int| + #![trigger windows@[k]] + 0 <= k < windows@.len() && windows@[k]@ == found@; if found_idx < container_idx { assert(windows@[found_idx]@.end <= windows@[container_idx]@.start); assert(windows@[container_idx]@.start <= range.start); @@ -291,6 +299,7 @@ pub proof fn lemma_found_window_contains( /// A range is registered when one abstract boot-time MMIO window contains it. pub open spec fn io_mem_range_registered(range: Range) -> bool { exists|m: int| + #![trigger registered_io_mem_windows()[m]] 0 <= m < registered_io_mem_windows().len() && registered_io_mem_windows()[m].start <= range.start && range.end <= registered_io_mem_windows()[m].end } @@ -324,7 +333,8 @@ pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { ret matches Some(res) ==> { &&& res@.start < range.end &&& res@.end > range.start - &&& exists|k: int| 0 <= k < allocators@.len() && allocators@[k]@ == res@ + &&& exists|k: int| #![trigger allocators@[k]] + 0 <= k < allocators@.len() && allocators@[k]@ == res@ } )] fn find_allocator<'a>( diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index db36c8314..36903e181 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -280,7 +280,7 @@ impl IoPortAllocator { ), id_alloc_capacity(&allocator_inner.allocator.inner) == crate::arch::io::MAX_IO_PORT as usize, - id_alloc_view(&allocator_inner.allocator.inner) =~= + id_alloc_view(&allocator_inner.allocator.inner) == allocation_start_view.union( port_id_set( range.start as usize, @@ -322,7 +322,7 @@ impl IoPortAllocator { let _ = allocator_inner.allocator.alloc_specific(i as usize); proof! { lemma_port_id_set_insert(range.start as usize, i as usize); - assert(id_alloc_view(&allocator_inner.allocator.inner) =~= + assert(id_alloc_view(&allocator_inner.allocator.inner) == old_view.insert(i as usize)); } } diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index c541cb1b5..46d5cbedc 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -50,7 +50,7 @@ impl IoPort { /// Whether `claim` is the allocator-issued ownership token for this complete typed range. pub open spec fn claim_matches_set(&self, claim: Set) -> bool { - claim =~= port_id_set(self@ as usize, (self@ as usize + size_of::()) as usize) + claim == port_id_set(self@ as usize, (self@ as usize + size_of::()) as usize) } } From 3cd89551983f9599139590956c0d3a8fe7e79eaf Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 2 Sep 2026 21:57:02 +0800 Subject: [PATCH 11/30] chore: add comments for modified exec code --- ostd/src/arch/x86/pci.rs | 2 ++ ostd/src/io/io_mem/allocator.rs | 31 ++++++++++++++++++++++++++----- ostd/src/io/io_mem/mod.rs | 12 ++++++++---- ostd/src/io/io_port/allocator.rs | 17 ++++++++++++++--- ostd/src/io/io_port/mod.rs | 1 + 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index ee4cdfcf5..c1c142c04 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -7,6 +7,7 @@ use crate::{bus::pci::PciDeviceLocation, io::IoPort, prelude::*}; verus! { +// Original Rust: static PCI_ADDRESS_PORT: IoPort = unsafe { IoPort::new(0x0CF8) }; exec static PCI_ADDRESS_PORT: IoPort ensures PCI_ADDRESS_PORT.well_formed(), @@ -14,6 +15,7 @@ exec static PCI_ADDRESS_PORT: IoPort unsafe { IoPort::new(0x0CF8) } } +// Original Rust: static PCI_DATA_PORT: IoPort = unsafe { IoPort::new(0x0CFC) }; exec static PCI_DATA_PORT: IoPort ensures PCI_DATA_PORT.well_formed(), diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index f0c58bbff..ba4ef4850 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -40,6 +40,11 @@ impl IoMemAllocator { == vstd_extra::external::range::range_usize_len_spec(&range), )] pub fn acquire(&self, range: Range) -> Option { + /* Original Rust: + find_allocator(&self.allocators, &range)? + .alloc_specific(&range) + .ok()?; + */ let allocator = find_allocator(&self.allocators, &range)?; proof! { use_type_invariant(self); @@ -55,7 +60,7 @@ impl IoMemAllocator { /* debug!("Acquiring MMIO range:{:x?}..{:x?}", range.start, range.end); */ // SAFETY: The created `IoMem` is guaranteed not to access physical memory or system device I/O. - // Original Rust used the upstream bitflags-style associated constant `PageFlags::RW`. + /* Original Rust: PageFlags::RW */ unsafe { Some(IoMem::new(range, PageFlags::RW(), CachePolicy::Uncacheable)) } } @@ -166,8 +171,22 @@ impl IoMemAllocatorBuilder { io_mem_range_registered(range), )] pub(crate) fn remove(&self, range: Range) { - // Formatting machinery used by the original panic is not modeled by Verus. - // Original Rust used two formatted `panic!` branches here. + /* Formatting machinery used by the original panic is not modeled by Verus. + Original Rust: + let Some(allocator) = find_allocator(&self.allocators, &range) else { + panic!( + "Allocator for the system device's MMIO was not found. Range: {:x?}", + range + ); + }; + + if let Err(err) = allocator.alloc_specific(&range) { + panic!( + "An error occurred while trying to remove access to the system device's MMIO. Range: {:x?}. Error: {:?}", + range, err + ); + } + */ let allocator = find_allocator(&self.allocators, &range); vstd_extra::assert!(allocator.is_some()); let allocator = allocator.unwrap(); @@ -185,6 +204,7 @@ impl IoMemAllocatorBuilder { } /// The I/O Memory allocator of the system. +// Original Rust: pub static IO_MEM_ALLOCATOR: Once = Once::new(); verus! { broadcast use vstd::std_specs::vec::group_vec_axioms; @@ -324,6 +344,7 @@ pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { use_type_invariant(&io_mem_builder); } // SAFETY: The safety is upheld by the caller. + // Original Rust: IO_MEM_ALLOCATOR.call_once(|| unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); IO_MEM_ALLOCATOR.init(unsafe { IoMemAllocator::new(io_mem_builder.allocators) }); } @@ -343,8 +364,8 @@ fn find_allocator<'a>( ) -> Option<&'a RangeAllocator> { for allocator in allocators.iter() { let allocator_range = allocator.fullrange(); - // Verus does not yet support `continue` in `for` loops. Original Rust: - /* + /* Verus does not yet support `continue` in `for` loops. + Original Rust: if allocator_range.start >= range.end || allocator_range.end <= range.start { continue; } diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index 8c7d91153..a0d6cd1e8 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -83,6 +83,7 @@ impl IoMem { pub fn acquire(range: Range) -> Result { allocator::IO_MEM_ALLOCATOR .get() + /* .unwrap() */ .ok_or(Error::AccessDenied)? .acquire(range) .ok_or(Error::AccessDenied) @@ -224,9 +225,9 @@ impl IoMem { // SAFETY: The constructor of the `IoMem` structure has already ensured the // safety of reading from the mapped physical address, and the mapping is valid. unsafe { - // `from_kernel_space` in ostd/src/mm/io.rs is changed - // (self.kvirt_area.deref().start() + self.offset) as *mut u8 VmReader::from_kernel_space( + /* Original Rust: + (self.kvirt_area.deref().start() + self.offset) as *mut u8, */ VirtPtr::from_vaddr(self.kvirt_area.deref().start() + self.offset, self.limit), self.limit, ) @@ -237,9 +238,9 @@ impl IoMem { // SAFETY: The constructor of the `IoMem` structure has already ensured the // safety of writing to the mapped physical address, and the mapping is valid. unsafe { - // Original Rust passed the raw pointer - // `(self.kvirt_area.deref().start() + self.offset) as *mut u8`. VmWriter::from_kernel_space( + /* Original Rust: + (self.kvirt_area.deref().start() + self.offset) as *mut u8, */ VirtPtr::from_vaddr(self.kvirt_area.deref().start() + self.offset, self.limit), self.limit, ) @@ -249,6 +250,7 @@ impl IoMem { verus! { +/* Original Rust: impl VmIo for IoMem { */ impl VmIo<()> for IoMem { closed spec fn obeys_vmio_spec() -> bool { false @@ -293,6 +295,7 @@ impl VmIo<()> for IoMem { /// Device reads are a trusted hardware boundary; the range checks and cursor updates remain /// identical to the original implementation. #[verifier::external_body] + /* Original Rust: fn read(&self, offset: usize, writer: &mut VmWriter) -> Result<()> { */ fn read( &self, offset: usize, @@ -312,6 +315,7 @@ impl VmIo<()> for IoMem { /// Device writes are a trusted hardware boundary for the same reason as [`Self::read`]. #[verifier::external_body] + /* Original Rust: fn write(&self, offset: usize, reader: &mut VmReader) -> Result<()> { */ fn write( &self, offset: usize, diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 36903e181..c2ef4f11d 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -181,7 +181,7 @@ pub struct IoPortAllocator { /// /// Instead of using `RangeAllocator` like `IoMemAllocator` does, it is more reasonable to use `IdAlloc`, /// as PIO space includes only a small region; for example, x86 module in OSTD allows just 65536 I/O ports. - allocator: SpinLock, + allocator: SpinLock, } #[verus_verify] @@ -209,7 +209,8 @@ impl IoPortAllocator { use_type_invariant(&*allocator_inner); } let mut range = port..(port + size_of::() as u16); - // `Iterator::any` with a capturing closure is not supported by Verus. Original Rust: + // `Iterator::any` with a capturing closure is not supported by Verus. + // Original Rust: // if range.any(|i| allocator.is_allocated(i as usize)) { return None; } let mut already_allocated = false; #[verus_spec(scan_iter => @@ -319,6 +320,7 @@ impl IoPortAllocator { Ghost(allocator_inner.tracked_allocated@.instance_id()), Ghost(allocator_inner.tracked_allocated@.value()), )] + /* Original Rust: allocator.alloc_specific(i as usize); */ let _ = allocator_inner.allocator.alloc_specific(i as usize); proof! { lemma_port_id_set_insert(range.start as usize, i as usize); @@ -336,6 +338,7 @@ impl IoPortAllocator { } // SAFETY: The created IoPort is guaranteed not to access system device I/O + /* Original Rust: unsafe { Some(IoPort::new(port)) } */ let result = unsafe { Some(IoPort::new(port)) }; allocator.drop(); proof_with!(|= Tracked(Some(range_claim))); @@ -357,6 +360,11 @@ impl IoPortAllocator { )] pub(in crate::io) unsafe fn recycle(&self, range: Range) { /* debug!("Recycling MMIO range: {:#x?}", range); */ + /* Original Rust: + self.allocator + .lock() + .free_consecutive(range.start as usize..range.end as usize); + */ let mut allocator = self.allocator.lock(); let allocator_inner = &mut *allocator; @@ -435,6 +443,10 @@ pub(crate) unsafe fn init() { } } + /* Original Rust: + IO_PORT_ALLOCATOR.call_once(|| IoPortAllocator { + allocator: SpinLock::new(allocator), + }); */ IO_PORT_ALLOCATOR.call_once(|| { proof_decl! { let tracked allocated = IoPortAllocation::initialize(); @@ -444,7 +456,6 @@ pub(crate) unsafe fn init() { #[cfg(verus_keep_ghost_body)] tracked_allocated: Tracked::new(allocated), }; - // Original Rust: `IoPortAllocator { allocator: SpinLock::new(allocator) }`. IoPortAllocator { allocator: SpinLock::new(inner, Ghost::new(()), Tracked::new(())), } diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index 46d5cbedc..b89797e1b 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -126,6 +126,7 @@ impl IoPort { let tracked claim: Option; } #[verus_spec(with => Tracked(claim))] + /* Original Rust: allocator::IO_PORT_ALLOCATOR.get().unwrap() */ let port = initialized_allocator().acquire(port); let result = port.ok_or(Error::AccessDenied); proof_with!(|= Tracked(claim)); From 87875fed29a0d610bf8d1192dfda83ea5e7467d6 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Thu, 3 Sep 2026 09:47:43 +0800 Subject: [PATCH 12/30] fmt --- ostd/src/arch/x86/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ostd/src/arch/x86/mod.rs b/ostd/src/arch/x86/mod.rs index 302df01ae..db3d91a44 100644 --- a/ostd/src/arch/x86/mod.rs +++ b/ostd/src/arch/x86/mod.rs @@ -3,7 +3,6 @@ /*pub mod boot; pub(crate) mod cpu;*/ pub mod device; - /*pub(crate) mod ex_table;*/ pub(crate) mod io; /*pub(crate) mod iommu;*/ From b1201628c5a5862b0d4b2f6b8889f0d3eb0f81e6 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Sat, 12 Sep 2026 10:50:41 +0800 Subject: [PATCH 13/30] prove: drop id-alloc assume_specifications, wire io_port to the proven IdAlloc --- ostd/src/io/io_mem/allocator.rs | 3 +- ostd/src/io/io_mem/mod.rs | 7 +- ostd/src/io/io_port/allocator.rs | 259 +++++++++++++++++- verified_libs/vstd_extra/Cargo.toml | 1 - .../vstd_extra/src/external/id_alloc.rs | 61 ----- verified_libs/vstd_extra/src/external/mod.rs | 6 +- 6 files changed, 257 insertions(+), 80 deletions(-) delete mode 100644 verified_libs/vstd_extra/src/external/id_alloc.rs diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index ba4ef4850..f74301f12 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -36,8 +36,7 @@ impl IoMemAllocator { io_mem_range_registered(range), ensures result is Some ==> result->Some_0.paddr_spec() == range.start, - result is Some ==> result->Some_0.length_spec() - == vstd_extra::external::range::range_usize_len_spec(&range), + result is Some ==> result->Some_0.length_spec() == range.end - range.start, )] pub fn acquire(&self, range: Range) -> Option { /* Original Rust: diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index a0d6cd1e8..8bd0e1da1 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -78,7 +78,7 @@ impl IoMem { ensures result is Ok ==> result->Ok_0.paddr_spec() == range.start, result is Ok ==> result->Ok_0.length_spec() - == vstd_extra::external::range::range_usize_len_spec(&range), + == range.end - range.start, )] pub fn acquire(range: Range) -> Result { allocator::IO_MEM_ALLOCATOR @@ -131,8 +131,7 @@ impl IoMem { Self { kvirt_area: self.kvirt_area.clone(), offset: self.offset + range.start, - /* limit: range.len(), */ - limit: vstd_extra::external::range::range_usize_len(&range), + limit: range.len(), pa: self.pa + range.start, } } @@ -153,7 +152,7 @@ impl IoMem { ensures result.paddr_spec() == range.start, result.length_spec() - == vstd_extra::external::range::range_usize_len_spec(&range), + == range.end - range.start, )] pub(crate) unsafe fn new(range: Range, flags: PageFlags, cache: CachePolicy) -> Self { let first_page_start = range.start.align_down(PAGE_SIZE); diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index c2ef4f11d..b818ebbb5 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -3,7 +3,7 @@ use vstd::prelude::*; use vstd::resource::set::{GhostSetAuth, GhostSubset}; use vstd::tokens::InstanceId; -use vstd_extra::external::{id_alloc_capacity, id_alloc_view}; +use vstd_extra::ownership::Inv; use core::ops::Range; @@ -29,12 +29,162 @@ pub struct ExOnce(spin::once::Once); /// Identity assigned to the single global PIO allocator during trusted boot initialization. pub uninterp spec fn io_port_allocator_instance_id() -> InstanceId; -closed spec fn io_port_inner_inv_values( +/// Allocated ids of a bitmap prefix `[0, end)`: `i` is in iff `s[i]`. +pub(crate) closed spec fn id_alloc_bits(s: Seq, end: int) -> Set + decreases end, +{ + if end <= 0 { + Set::empty() + } else { + let rest = id_alloc_bits(s, end - 1); + if s[end - 1] { + rest.insert((end - 1) as usize) + } else { + rest + } + } +} + +/// Set of ids currently allocated by `allocator`. +pub(crate) open spec fn id_alloc_view(allocator: &IdAlloc) -> Set { + id_alloc_bits(allocator@, allocator@.len() as int) +} + +/// Number of ids `allocator` can hold. +pub(crate) open spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize { + allocator@.len() as usize +} + +/// Characterizes `id_alloc_bits`: `id_alloc_bits(s, end).contains(j)` holds exactly when +/// `0 <= j < end` and `s[j]` is `true`. +pub(crate) proof fn lemma_id_alloc_bits_char(s: Seq, end: int, j: usize) + requires + 0 <= end, + end <= usize::MAX as int, + s.len() >= end, + 0 <= (j as int), + ensures + id_alloc_bits(s, end).contains(j) == ((j as int) < end && s[j as int]), + decreases end, +{ + reveal(id_alloc_bits); + if end <= 0 { + assert(id_alloc_bits(s, end) =~= Set::empty()); + } else { + lemma_id_alloc_bits_char(s, end - 1, j); + if (j as int) == end - 1 { + assert(id_alloc_bits(s, end - 1).contains(j) == (((j as int) < end - 1) + && s[j as int])); + assert(id_alloc_bits(s, end).contains(j) == s[end - 1]); + assert(s[j as int] == s[end - 1]); + } else { + // j != end-1: the bit folded in at (end-1) is distinct from j. + assert(id_alloc_bits(s, end - 1).contains(j) == (((j as int) < end - 1) + && s[j as int])); + assert(((end - 1) as usize) as int == end - 1); + assert(j != (end - 1) as usize); + if s[end - 1] { + assert(id_alloc_bits(s, end - 1).insert((end - 1) as usize).contains(j) + == id_alloc_bits(s, end - 1).contains(j)); + } + assert(id_alloc_bits(s, end).contains(j) == id_alloc_bits(s, end - 1).contains(j)); + assert(((j as int) < end - 1 && s[j as int]) == ((j as int) < end && s[j as int])); + } + } +} + +/// For an in-bounds id, `id_alloc_view` membership coincides with the allocated bitmap bit. +pub(crate) proof fn lemma_id_alloc_view_contains(allocator: &IdAlloc, id: usize) + requires + allocator.inv(), + (id as int) < allocator@.len(), + allocator@.len() <= usize::MAX as int, + ensures + id_alloc_view(allocator).contains(id) == allocator@[id as int], +{ + lemma_id_alloc_bits_char(allocator@, allocator@.len() as int, id); +} + +/// Derives the set-level postcondition of `alloc_specific` from its bitmap postcondition. +pub(crate) proof fn lemma_alloc_specific_view( + old_a: &IdAlloc, + final_a: &IdAlloc, + id: usize, + res: Option, +) + requires + old_a.inv(), + final_a.inv(), + (id as int) < old_a@.len(), + old_a@.len() <= usize::MAX as int, + final_a@.len() == old_a@.len(), + res is None ==> old_a@[id as int] && final_a@ == old_a@, + res is Some ==> final_a@ == old_a@.update(id as int, true) && !old_a@[id as int], + res is Some ==> res == Some(id), + ensures + id_alloc_view(final_a) == id_alloc_view(old_a).insert(id), + id_alloc_view(old_a).subset_of(id_alloc_view(final_a)), + id_alloc_view(old_a).contains(id) ==> res is None, + !id_alloc_view(old_a).contains(id) ==> res == Some(id), +{ + let old_len: int = old_a@.len() as int; + assert forall|j: usize| + id_alloc_view(final_a).contains(j) == (id_alloc_view(old_a).insert(id)).contains(j) by { + lemma_id_alloc_bits_char(old_a@, old_len, j); + lemma_id_alloc_bits_char(final_a@, final_a@.len() as int, j); + if res is Some { + assert forall|jj: int| + #![trigger final_a@[jj]] + final_a@[jj] == (if jj == id as int { + true + } else { + old_a@[jj] + }) by { + assert(final_a@ == old_a@.update(id as int, true)); + } + } else { + assert(final_a@ == old_a@); + assert(old_a@[id as int]); + } + } + assert forall|j: usize| + #![trigger id_alloc_view(old_a).contains(j)] + id_alloc_view(old_a).contains(j) implies id_alloc_view(final_a).contains(j) by { + lemma_id_alloc_bits_char(old_a@, old_len, j); + lemma_id_alloc_bits_char(final_a@, final_a@.len() as int, j); + if res is Some { + assert forall|jj: int| + #![trigger final_a@[jj]] + final_a@[jj] == (if jj == id as int { + true + } else { + old_a@[jj] + }) by { + assert(final_a@ == old_a@.update(id as int, true)); + } + } else { + assert(final_a@ == old_a@); + } + } + lemma_id_alloc_bits_char(old_a@, old_len, id); + assert(id_alloc_view(old_a).contains(id) == old_a@[id as int]); + if res is Some { + assert(!old_a@[id as int]); + assert(res == Some(id)); + } else { + assert(old_a@[id as int]); + } +} + +/// Representation invariant of `IoPortAllocatorInner`. +pub(crate) open spec fn io_port_inner_inv_values( allocated_instance_id: InstanceId, allocated: Set, allocator: &IdAlloc, ) -> bool { &&& allocated_instance_id == io_port_allocator_instance_id() + &&& allocator.inv() + &&& allocator@.len() == crate::arch::io::MAX_IO_PORT as int &&& allocated.subset_of(id_alloc_view(allocator)) &&& id_alloc_capacity(allocator) == crate::arch::io::MAX_IO_PORT as usize } @@ -57,6 +207,8 @@ impl ModeledIdAlloc { id < id_alloc_capacity(&old(self).inner), io_port_inner_inv_values(allocated_instance_id, preserved, &old(self).inner), ensures + final(self).inner.inv(), + final(self).inner@.len() == crate::arch::io::MAX_IO_PORT as int, id_alloc_capacity(&final(self).inner) == id_alloc_capacity(&old(self).inner), id_alloc_view(&final(self).inner) == id_alloc_view(&old(self).inner).insert(id), id_alloc_view(&old(self).inner).subset_of(id_alloc_view(&final(self).inner)), @@ -68,10 +220,27 @@ impl ModeledIdAlloc { ), id_alloc_view(&old(self).inner).contains(id) ==> result is None, !id_alloc_view(&old(self).inner).contains(id) ==> result == Some(id), - no_unwind )] fn alloc_specific(&mut self, id: usize) -> Option { - self.inner.alloc_specific(id) + proof! { + assert(id < id_alloc_capacity(&old(self).inner)); + assert(id_alloc_capacity(&old(self).inner) + == crate::arch::io::MAX_IO_PORT as usize); + assert(old(self).inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert((id as int) < old(self).inner@.len()); + } + let res = self.inner.alloc_specific(id); + proof! { + assert(self.inner@.len() == old(self).inner@.len()); + assert(self.inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert(self.inner.inv()); + assert(old(self).inner@.len() <= usize::MAX as int); + lemma_alloc_specific_view(&old(self).inner, &self.inner, id, res); + assert(id_alloc_capacity(&self.inner) == id_alloc_capacity(&old(self).inner)); + assert(preserved.subset_of(id_alloc_view(&self.inner))); + assert(io_port_inner_inv_values(allocated_instance_id, preserved, &self.inner)); + } + res } } @@ -137,6 +306,16 @@ impl IoPortAllocation { { self.auth.delete(claim.subset); } + + /// Certifies `claim.set() <= self.value()` (the claim's ids are currently allocated). + pub proof fn claim_includes(tracked &self, tracked claim: &IoPortClaim) + requires + claim.instance_id() == self.instance_id(), + ensures + claim.set() <= self.value(), + { + claim.subset.agree(&self.auth); + } } impl IoPortClaim { @@ -163,7 +342,6 @@ struct IoPortAllocatorInner { verus! { impl IoPortAllocatorInner { - #[verifier::type_invariant] pub closed spec fn type_inv(self) -> bool { io_port_inner_inv_values( self.tracked_allocated@.instance_id(), @@ -194,6 +372,7 @@ impl IoPortAllocator { vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, + io_port_allocator_initialized(), ensures result is Some ==> result->Some_0@ == port, result is Some ==> result->Some_0.well_formed(), @@ -206,7 +385,7 @@ impl IoPortAllocator { let mut allocator = self.allocator.lock(); let allocator_inner = &mut *allocator; proof! { - use_type_invariant(&*allocator_inner); + lemma_io_port_alloc_init(&*allocator_inner); } let mut range = port..(port + size_of::() as u16); // `Iterator::any` with a capturing closure is not supported by Verus. @@ -215,16 +394,30 @@ impl IoPortAllocator { let mut already_allocated = false; #[verus_spec(scan_iter => invariant - allocator_inner.type_inv(), + allocator_inner.allocator.inner.inv(), + allocator_inner.allocator.inner@.len() + == crate::arch::io::MAX_IO_PORT as int, !already_allocated ==> forall|id: usize| range.start as usize <= id < (range.start as int + scan_iter.index()) as usize ==> !id_alloc_view(&allocator_inner.allocator.inner).contains(id), )] for i in range.clone() { + proof! { + assert((i as usize) < allocator_inner.allocator.inner@.len()) by { + assert((i as usize) < range.end as usize); + assert((range.end as usize) <= u16::MAX as usize); + assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); + assert(allocator_inner.allocator.inner@.len() + == crate::arch::io::MAX_IO_PORT as int); + } + } if allocator_inner.allocator.inner.is_allocated(i as usize) { already_allocated = true; } + proof! { + lemma_id_alloc_view_contains(&allocator_inner.allocator.inner, i as usize); + } } proof_decl! { let tracked range_claim: IoPortClaim; @@ -272,6 +465,9 @@ impl IoPortAllocator { } #[verus_spec(allocation_iter => invariant + allocator_inner.allocator.inner.inv(), + allocator_inner.allocator.inner@.len() + == crate::arch::io::MAX_IO_PORT as int, range.end as usize <= id_alloc_capacity(&allocator_inner.allocator.inner), allocator_inner.tracked_allocated@.instance_id() == io_port_allocator_instance_id(), @@ -357,6 +553,7 @@ impl IoPortAllocator { claim.instance_id() == io_port_allocator_instance_id(), claim.set() =~= port_id_set(range.start as usize, range.end as usize), range.start <= range.end, + io_port_allocator_initialized(), )] pub(in crate::io) unsafe fn recycle(&self, range: Range) { /* debug!("Recycling MMIO range: {:#x?}", range); */ @@ -369,9 +566,20 @@ impl IoPortAllocator { let mut allocator = self.allocator.lock(); let allocator_inner = &mut *allocator; proof! { - use_type_invariant(&*allocator_inner); + lemma_io_port_alloc_init(&*allocator_inner); assert(range.start as usize <= range.end as usize); assert(claim.instance_id() == allocator_inner.tracked_allocated@.instance_id()); + allocator_inner.tracked_allocated.borrow().claim_includes(&claim); + assert(port_id_set(range.start as usize, range.end as usize) + <= allocator_inner.tracked_allocated@.value()); + assert(port_id_set(range.start as usize, range.end as usize) + <= id_alloc_view(&allocator_inner.allocator.inner)); + assert((range.end as usize) <= allocator_inner.allocator.inner@.len()) by { + assert((range.end as usize) <= u16::MAX as usize); + assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); + assert(allocator_inner.allocator.inner@.len() + == crate::arch::io::MAX_IO_PORT as int); + } allocator_inner.tracked_allocated.borrow_mut().release(claim); assert forall|id: usize| #[trigger] allocator_inner.tracked_allocated@.value().contains(id) implies { @@ -384,6 +592,27 @@ impl IoPortAllocator { id, ); } + assert forall|i: int| + range.start as usize <= i + && i < allocator_inner.allocator.inner@.len() + && i < range.end as usize implies + allocator_inner.allocator.inner@[i] + by { + assert(0 <= i); + assert(i < allocator_inner.allocator.inner@.len()); + assert(allocator_inner.allocator.inner@.len() <= usize::MAX as int); + assert(i <= usize::MAX as int); + assert((i as usize) as int == i); + lemma_port_id_set_contains( + range.start as usize, + range.end as usize, + i as usize, + ); + assert(port_id_set(range.start as usize, range.end as usize).contains(i as usize)); + assert(id_alloc_view(&allocator_inner.allocator.inner).contains(i as usize)); + lemma_id_alloc_view_contains(&allocator_inner.allocator.inner, i as usize); + assert(allocator_inner.allocator.inner@[i]); + } } allocator_inner .allocator @@ -401,6 +630,20 @@ verus! { /// explicit specification boundary for the architecture's guarantee that [`init`] ran first. pub uninterp spec fn io_port_allocator_initialized() -> bool; +/// Trusted: once `init` has run, the global allocator's inner satisfies its invariant. +#[verifier::external_body] +proof fn lemma_io_port_alloc_init(inner: &IoPortAllocatorInner) + requires + io_port_allocator_initialized(), + ensures + io_port_inner_inv_values( + inner.tracked_allocated@.instance_id(), + inner.tracked_allocated@.value(), + &inner.allocator.inner, + ), +{ +} + } // verus! pub(super) static IO_PORT_ALLOCATOR: Once = Once::new(); diff --git a/verified_libs/vstd_extra/Cargo.toml b/verified_libs/vstd_extra/Cargo.toml index 9c87a296d..b95c61f43 100644 --- a/verified_libs/vstd_extra/Cargo.toml +++ b/verified_libs/vstd_extra/Cargo.toml @@ -16,7 +16,6 @@ std = ["vstd/std"] [dependencies] vstd = { workspace = true } bitvec.workspace = true -id-alloc = { path = "../../ostd/libs/id-alloc" } [target.'cfg(target_arch = "x86_64")'.dependencies] x86_64 = "0.14.13" diff --git a/verified_libs/vstd_extra/src/external/id_alloc.rs b/verified_libs/vstd_extra/src/external/id_alloc.rs deleted file mode 100644 index 8e9904339..000000000 --- a/verified_libs/vstd_extra/src/external/id_alloc.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Specifications for the bitmap-backed ID allocator. -use core::ops::Range; - -use id_alloc::IdAlloc; -use vstd::prelude::*; - -verus! { - -/// Verus model for the external bitmap-backed ID allocator. -#[verifier::external_type_specification] -#[verifier::external_body] -pub struct ExIdAlloc(IdAlloc); - -/// IDs currently allocated by an external `IdAlloc`. -pub uninterp spec fn id_alloc_view(allocator: &IdAlloc) -> Set; - -/// Capacity configured for an external `IdAlloc`. -pub uninterp spec fn id_alloc_capacity(allocator: &IdAlloc) -> usize; - -pub assume_specification[ IdAlloc::with_capacity ](capacity: usize) -> (allocator: IdAlloc) - ensures - id_alloc_capacity(&allocator) == capacity, - id_alloc_view(&allocator) == Set::::empty(), -; - -pub assume_specification[ IdAlloc::is_allocated ](allocator: &IdAlloc, id: usize) -> (allocated: - bool) - ensures - id < id_alloc_capacity(allocator) ==> allocated == id_alloc_view(allocator).contains(id), -; - -pub assume_specification[ IdAlloc::alloc_specific ](allocator: &mut IdAlloc, id: usize) -> (result: - Option) - requires - id < id_alloc_capacity(old(allocator)), - ensures - id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - id_alloc_view(final(allocator)) == id_alloc_view(old(allocator)).insert(id), - id_alloc_view(old(allocator)).subset_of(id_alloc_view(final(allocator))), - id_alloc_view(old(allocator)).contains(id) ==> { - &&& result is None - }, - !id_alloc_view(old(allocator)).contains(id) ==> { - &&& result == Some(id) - }, - no_unwind -; - -pub assume_specification[ IdAlloc::free_consecutive ](allocator: &mut IdAlloc, range: Range) - requires - range.end <= id_alloc_capacity(old(allocator)), - ensures - id_alloc_capacity(final(allocator)) == id_alloc_capacity(old(allocator)), - forall|id: usize| #[trigger] - id_alloc_view(final(allocator)).contains(id) <==> id_alloc_view( - old(allocator), - ).contains(id) && !(range.start <= id < range.end), - no_unwind -; - -} // verus! diff --git a/verified_libs/vstd_extra/src/external/mod.rs b/verified_libs/vstd_extra/src/external/mod.rs index d7aad6178..386b2e3b6 100644 --- a/verified_libs/vstd_extra/src/external/mod.rs +++ b/verified_libs/vstd_extra/src/external/mod.rs @@ -7,12 +7,11 @@ pub mod btree; pub mod cmp; pub mod convert; pub mod deref; -pub mod id_alloc; pub mod ilog2; pub mod int_specs; -pub mod iter; #[cfg(target_arch = "x86_64")] pub mod io_port; +pub mod iter; pub mod nonnull; pub mod ptr; pub mod range; @@ -23,12 +22,11 @@ pub mod time; pub use bitvec::*; pub use btree::*; pub use cmp::*; -pub use id_alloc::*; pub use ilog2::*; pub use int_specs::*; -pub use iter::*; #[cfg(target_arch = "x86_64")] pub use io_port::*; +pub use iter::*; pub use nonnull::*; pub use ptr::*; pub use range::*; From ea38da80c7ba74906513e9ea633030fe88cf262b Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Sat, 12 Sep 2026 19:56:24 +0800 Subject: [PATCH 14/30] refine: according to the review skill --- ostd/src/io/io_mem/allocator.rs | 10 +++-- ostd/src/io/io_mem/mod.rs | 25 ++++++------ ostd/src/io/io_port/allocator.rs | 39 ++++++------------- ostd/src/io/io_port/mod.rs | 11 +++--- .../vstd_extra/src/external/io_port.rs | 4 +- 5 files changed, 39 insertions(+), 50 deletions(-) diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index f74301f12..3767c31ca 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -2,7 +2,7 @@ //! I/O Memory allocator. use crate::specs::arch::PAGE_SIZE; use crate::sync::{OnceImpl, TrivialPred}; -use vstd::prelude::*; +use vstd::{arithmetic::power2::is_pow2, prelude::*}; use vstd_extra::resource::flags::OneShotSet; use alloc::vec::Vec; @@ -30,13 +30,15 @@ impl IoMemAllocator { /// If the range is not available, then the return value will be `None`. #[verus_spec(result => requires - vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + is_pow2(PAGE_SIZE as int), range.start < range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), io_mem_range_registered(range), ensures - result is Some ==> result->Some_0.paddr_spec() == range.start, - result is Some ==> result->Some_0.length_spec() == range.end - range.start, + result matches Some(io_mem) ==> { + &&& io_mem.paddr_spec() == range.start + &&& io_mem.length_spec() == range.end - range.start + }, )] pub fn acquire(&self, range: Range) -> Option { /* Original Rust: diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index 8bd0e1da1..fa64c5e96 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O memory and its allocator that allocates memory I/O (MMIO) to device drivers. -use crate::specs::arch::PAGE_SIZE; use crate::specs::{ + arch::PAGE_SIZE, mm::{io::VmIoOwner, virt_mem::VirtPtr}, task::AnyAtomicGuard, }; -use vstd::prelude::*; +use vstd::{arithmetic::power2::is_pow2, prelude::*}; +use vstd_extra::panic::UnwrapOrPanic; mod allocator; @@ -60,7 +61,7 @@ impl IoMem { } // verus! #[verus_verify] impl HasPaddr for IoMem { - #[verus_spec(result => ensures result == self.paddr_spec())] + #[verus_spec(returns self.paddr_spec())] fn paddr(&self) -> Paddr { self.pa } @@ -71,34 +72,36 @@ impl IoMem { /// Acquires an `IoMem` instance for the given range. #[verus_spec(result => requires - vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + is_pow2(PAGE_SIZE as int), range.start < range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), allocator::io_mem_range_registered(range), + vstd_extra::panic::may_panic(), ensures - result is Ok ==> result->Ok_0.paddr_spec() == range.start, - result is Ok ==> result->Ok_0.length_spec() - == range.end - range.start, + result matches Ok(io_mem) ==> { + &&& io_mem.paddr_spec() == range.start + &&& io_mem.length_spec() == range.end - range.start + }, )] pub fn acquire(range: Range) -> Result { allocator::IO_MEM_ALLOCATOR .get() /* .unwrap() */ - .ok_or(Error::AccessDenied)? + .unwrap_or_panic() .acquire(range) .ok_or(Error::AccessDenied) } /// Returns the physical address of the I/O memory. #[verus_verify] - #[verus_spec(result => ensures result == self.paddr_spec())] + #[verus_spec(returns self.paddr_spec())] pub fn paddr(&self) -> Paddr { self.pa } /// Returns the length of the I/O memory region. #[verus_verify] - #[verus_spec(result => ensures result == self.length_spec())] + #[verus_spec(returns self.length_spec())] pub fn length(&self) -> usize { self.limit } @@ -146,7 +149,7 @@ impl IoMem { #[verifier::external_body] #[verus_spec(result => requires - vstd::arithmetic::power2::is_pow2(PAGE_SIZE as int), + is_pow2(PAGE_SIZE as int), range.start <= range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), ensures diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index b818ebbb5..0c9766054 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port allocator. -use vstd::prelude::*; -use vstd::resource::set::{GhostSetAuth, GhostSubset}; -use vstd::tokens::InstanceId; +use vstd::{ + prelude::*, + resource::set::{GhostSetAuth, GhostSubset}, + tokens::InstanceId, +}; use vstd_extra::ownership::Inv; use core::ops::Range; @@ -19,13 +21,6 @@ use crate::{ verus! { -/// Opaque specification for the third-party one-time initialization primitive. -#[verifier::external_type_specification] -#[verifier::external_body] -#[verifier::reject_recursive_types(T)] -#[verifier::reject_recursive_types(R)] -pub struct ExOnce(spin::once::Once); - /// Identity assigned to the single global PIO allocator during trusted boot initialization. pub uninterp spec fn io_port_allocator_instance_id() -> InstanceId; @@ -339,19 +334,6 @@ struct IoPortAllocatorInner { tracked_allocated: Tracked, } -verus! { - -impl IoPortAllocatorInner { - pub closed spec fn type_inv(self) -> bool { - io_port_inner_inv_values( - self.tracked_allocated@.instance_id(), - self.tracked_allocated@.value(), - &self.allocator.inner, - ) - } -} - -} // verus! /// I/O port allocator that allocates port I/O access to device drivers. #[verus_verify] pub struct IoPortAllocator { @@ -374,12 +356,13 @@ impl IoPortAllocator { port as usize + size_of::() <= u16::MAX, io_port_allocator_initialized(), ensures - result is Some ==> result->Some_0@ == port, - result is Some ==> result->Some_0.well_formed(), result is Some <==> claim@ is Some, - result is Some ==> claim@->Some_0.instance_id() == - io_port_allocator_instance_id(), - result is Some ==> result->Some_0.claim_matches_set(claim@->Some_0.set()), + result matches Some(io_port) ==> { + &&& io_port@ == port + &&& io_port.well_formed() + &&& io_port.claim_matches_set(claim@->Some_0.set()) + &&& claim@->Some_0.instance_id() == io_port_allocator_instance_id() + }, )] pub fn acquire(&self, port: u16) -> Option> { let mut allocator = self.allocator.lock(); diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index b89797e1b..e844945f2 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -114,12 +114,13 @@ impl IoPort { port as usize + size_of::() <= u16::MAX, allocator::io_port_allocator_initialized(), ensures - result is Ok ==> result->Ok_0@ == port, - result is Ok ==> result->Ok_0.well_formed(), result is Ok <==> claim@ is Some, - result is Ok ==> claim@->Some_0.instance_id() == - allocator::io_port_allocator_instance_id(), - result is Ok ==> result->Ok_0.claim_matches_set(claim@->Some_0.set()), + result matches Ok(io_port) ==> { + &&& io_port@ == port + &&& io_port.well_formed() + &&& io_port.claim_matches_set(claim@->Some_0.set()) + &&& claim@->Some_0.instance_id() == allocator::io_port_allocator_instance_id() + }, )] pub fn acquire(port: u16) -> Result> { proof_decl! { diff --git a/verified_libs/vstd_extra/src/external/io_port.rs b/verified_libs/vstd_extra/src/external/io_port.rs index 96b810b22..1a0a6e41a 100644 --- a/verified_libs/vstd_extra/src/external/io_port.rs +++ b/verified_libs/vstd_extra/src/external/io_port.rs @@ -17,10 +17,10 @@ pub open spec fn valid_io_port_number(port: int) -> bool { 0 <= port <= u16::MAX as int } -/// Whether an access of type `T` is fully contained in the x86 I/O-port address space. +/// Whether an access of type `T` fits in the PIO byte range `0..u16::MAX`. pub open spec fn valid_io_port_access(port: int) -> bool { &&& valid_io_port_number(port) - &&& port + size_of::() <= u16::MAX as int + 1 + &&& port + size_of::() <= u16::MAX as int } /// Opaque specification boundary for the third-party read/write access marker. From e3b0444ac4ef64723fba9e21c9a41c72a3937d94 Mon Sep 17 00:00:00 2001 From: Yuwei LIU <22045841+Marsman1996@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:41:53 +0800 Subject: [PATCH 15/30] ci: refactor ci workflow (#766) --- .github/actions/setup-verus/action.yml | 143 ++++++ .github/workflows/ci.yml | 599 +++++++++++++++++++++---- .github/workflows/doc.yml | 225 ---------- .github/workflows/verify-perf.yml | 325 -------------- 4 files changed, 645 insertions(+), 647 deletions(-) create mode 100644 .github/actions/setup-verus/action.yml delete mode 100644 .github/workflows/doc.yml delete mode 100644 .github/workflows/verify-perf.yml diff --git a/.github/actions/setup-verus/action.yml b/.github/actions/setup-verus/action.yml new file mode 100644 index 000000000..81ceeb367 --- /dev/null +++ b/.github/actions/setup-verus/action.yml @@ -0,0 +1,143 @@ +name: Set up Verus toolchain and caches +description: >- + Shared CI setup: system dependencies, the Rust toolchain and cargo caches, + the dv build cache, and (optionally) the tools/verus cache with a + bootstrap-if-miss step. Expects a repository checkout (with submodules) in + the working directory. + +inputs: + verus: + description: Cache and bootstrap tools/verus ('true' or 'false') + default: 'true' + install-verusfmt: + description: Install verusfmt via its installer if missing ('true' or 'false') + default: 'false' + dv-cache: + description: "'save' (restore + save) or 'restore' (restore only) for dv/target" + default: 'save' + +outputs: + rust-version: + description: Rust toolchain channel from rust-toolchain.toml + value: ${{ steps.rust.outputs.rust-version }} + dv-commit: + description: dv submodule commit + value: ${{ steps.commits.outputs.dv-commit }} + verus-commit: + description: Upstream Verus commit used for the tools/verus cache key + value: ${{ steps.commits.outputs.verus-commit }} + verus-cache-hit: + description: Exact-match hit for the tools/verus cache ('true' or empty) + value: ${{ steps.verus-cache.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt update -qq + sudo apt install -y build-essential unzip pkg-config libssl-dev llvm + + - name: Get Rust toolchain version + id: rust + shell: bash + run: | + RUST_VERSION=$(sed -nE 's/^channel = "([^"]+)"/\1/p' rust-toolchain.toml) + if [[ -z "$RUST_VERSION" ]]; then + echo "Failed to extract the Rust version from rust-toolchain.toml" >&2 + exit 1 + fi + echo "rust-version=$RUST_VERSION" >> "$GITHUB_OUTPUT" + echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" + echo "Rust version: $RUST_VERSION" + + - name: Cache Rust toolchain + uses: actions/cache@v6 + with: + path: | + ~/.rustup/toolchains + ~/.rustup/update-hashes + ~/.rustup/tmp + key: ${{ runner.os }}-rust-toolchain-${{ steps.rust.outputs.rust-version }} + + # Must run before tools/verus exists, or hashFiles sees it and the key + # becomes nondeterministic. + - name: Cache Cargo dependencies + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Resolve dv and Verus commits + id: commits + shell: bash + run: | + DV_COMMIT=$(git rev-parse HEAD:dv) + echo "dv-commit=$DV_COMMIT" >> "$GITHUB_OUTPUT" + echo "DV_COMMIT=$DV_COMMIT" >> "$GITHUB_ENV" + echo "Using dv commit: $DV_COMMIT" + if [[ "${{ inputs.verus }}" == "true" ]]; then + VERUS_COMMIT=$(git ls-remote https://github.com/asterinas/verus HEAD | cut -f1) + echo "verus-commit=$VERUS_COMMIT" >> "$GITHUB_OUTPUT" + echo "VERUS_COMMIT=$VERUS_COMMIT" >> "$GITHUB_ENV" + echo "Using Verus commit: $VERUS_COMMIT" + fi + + - name: Cache dv build artifacts + if: inputs.dv-cache == 'save' + uses: actions/cache@v6 + with: + path: dv/target + key: ${{ runner.os }}-dv-${{ steps.commits.outputs.dv-commit }} + + - name: Restore dv build artifacts + if: inputs.dv-cache == 'restore' + uses: actions/cache/restore@v6 + with: + path: dv/target + key: ${{ runner.os }}-dv-${{ steps.commits.outputs.dv-commit }} + + - name: Cache Verus + id: verus-cache + if: inputs.verus == 'true' + uses: actions/cache@v6 + with: + # Only needed when rebuilding from scratch, which re-clones anyway. + # Excluding them roughly halves the entry size. + path: | + tools/verus + !tools/verus/.git + !tools/verus/.git/** + !tools/verus/source/target + !tools/verus/source/target/** + key: ${{ runner.os }}-verus-${{ steps.commits.outputs.verus-commit }} + + - name: Bootstrap Verus (if needed) + if: inputs.verus == 'true' + shell: bash + run: | + if [[ "${{ steps.verus-cache.outputs.cache-hit }}" == "true" ]] && + [[ -x tools/verus/source/target-verus/release/cargo-verus ]] && + [[ -f tools/verus/source/z3 ]]; then + echo "Using cached Verus (${{ steps.commits.outputs.verus-commit }})" + else + echo "Cache miss - bootstrapping Verus..." + rm -rf tools/verus + cargo dv bootstrap + fi + + - name: Install verusfmt (if needed) + if: inputs.install-verusfmt == 'true' + shell: bash + run: | + if ! command -v verusfmt >/dev/null 2>&1; then + echo "verusfmt not found, installing via its installer..." + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/verus-lang/verusfmt/releases/latest/download/verusfmt-installer.sh | sh + fi + verusfmt --version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6579110ce..3f11969f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,9 @@ name: Format and Verify VOSTD (Main) +# One verification run feeds everything: a verify leg runs it with +# --time-expanded and uploads the log; `perf` parses that log (never +# re-verifies), `doc` builds the docs, `format` needs no Verus. + on: push: branches: @@ -7,122 +11,58 @@ on: pull_request: branches: - main + issue_comment: + types: [created] workflow_dispatch: - inputs: - branch: - description: "Branch to run the workflow on" - required: true - default: "main" permissions: contents: read +concurrency: + # For comment events github.ref is the default branch, so the issue number + # keeps different PRs' runs (and a PR's own runs) from cancelling each other. + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.issue.number || 'push' }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +defaults: + run: + shell: bash + jobs: format-and-verify: + if: github.event_name != 'issue_comment' runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash + permissions: + contents: read strategy: fail-fast: false matrix: os: - ubuntu-24.04 - macos-14 - env: - CARGO_TERM_COLOR: always - + timeout-minutes: 240 steps: - name: Checkout repository uses: actions/checkout@v7 with: + # push: the pushed commit; pull_request: the merge commit. + ref: ${{ github.sha }} submodules: recursive - - name: Install dependencies (Linux) - if: runner.os == 'Linux' - run: | - sudo apt update -qq - sudo apt install -y build-essential unzip pkg-config libssl-dev llvm - - - name: Get Rust toolchain version - run: | - RUST_VERSION=$(grep 'channel' rust-toolchain.toml | sed -E 's/.*= *"(.*)"/\1/') - if [ -z "$RUST_VERSION" ]; then - echo "Failed to extract the Rust version from rust-toolchain.toml" >&2 - exit 1 - fi - echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" - echo "Rust version: $RUST_VERSION" - - - name: Cache Rust toolchain - uses: actions/cache@v6 + - name: Set up toolchain and caches + uses: ./.github/actions/setup-verus with: - path: | - ~/.rustup/toolchains - ~/.rustup/update-hashes - ~/.rustup/tmp - key: ${{ runner.os }}-rust-toolchain-${{ env.RUST_VERSION }} - - - name: Cache Cargo dependencies - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Get Verus commit - id: verus - run: | - VERUS_COMMIT=$(git ls-remote https://github.com/asterinas/verus HEAD | cut -f1) - echo "VERUS_COMMIT=$VERUS_COMMIT" >> "$GITHUB_ENV" - echo "Using Verus commit: $VERUS_COMMIT" - DV_COMMIT=$(git rev-parse HEAD:dv) - echo "DV_COMMIT=$DV_COMMIT" >> "$GITHUB_ENV" - echo "Using dv commit: $DV_COMMIT" - - - name: Cache dv build artifacts - uses: actions/cache@v6 - with: - path: dv/target - key: ${{ runner.os }}-dv-${{ env.DV_COMMIT }} - - - name: Cache Verus - id: cache-verus - uses: actions/cache@v6 - with: - path: tools/verus - key: ${{ runner.os }}-verus-${{ env.VERUS_COMMIT }} - - - name: Cache verusfmt - id: cache-verusfmt - uses: actions/cache@v6 - with: - path: ~/.cargo/bin/verusfmt - key: ${{ runner.os }}-verusfmt-${{ env.VERUS_COMMIT }} - - - name: Bootstrap Verus (if needed) - run: | - if [ "${{ steps.cache-verus.outputs.cache-hit }}" = "true" ]; then - echo "Using cached Verus" - else - echo "Cache miss, bootstrapping Verus..." - rm -rf tools/verus - cargo dv bootstrap - fi - - if ! command -v verusfmt >/dev/null 2>&1; then - echo "verusfmt not found, installing via cargo dv bootstrap..." - cargo dv bootstrap - fi - verusfmt --version + verus: 'true' + install-verusfmt: 'false' + # `make` plus --time-expanded for the perf job's log. - name: Run verification run: | set -o pipefail - if ! make 2>&1 | tee verify_output.txt; then + if ! cargo dv verify --targets ostd -- --time-expanded 2>&1 | tee verify_output.txt; then echo "❌ Verification failed" echo "VERIFY_FAILED=1" >> "$GITHUB_ENV" exit 1 @@ -157,12 +97,64 @@ jobs: exit 1 fi + - name: Upload verification log + if: always() + uses: actions/upload-artifact@v7 + with: + name: verify-log-${{ matrix.os }} + path: verify_output.txt + retention-days: 7 + if-no-files-found: warn + + - name: Publish summary + if: always() + run: | + { + echo "# CI Summary (${{ matrix.os }})" + if [[ "${VERIFY_FAILED:-0}" == "1" ]]; then + echo "- Verification: ❌ failed" + else + echo "- Verification: ✅ passed" + fi + if [[ "${VERIFY_WARNINGS_FOUND:-0}" == "1" ]]; then + echo "- Verification warnings: ❌ found" + else + echo "- Verification warnings: ✅ none" + fi + } >> "$GITHUB_STEP_SUMMARY" + + format: + # Needs no Verus build: only rustfmt/verusfmt and the Verus SOURCES for + # the vstd path dependency (fetched below). + if: github.event_name != 'issue_comment' + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Set up toolchain and caches + uses: ./.github/actions/setup-verus + with: + verus: 'false' + install-verusfmt: 'true' + + - name: Fetch Verus sources for the vstd path dependency + run: | + VERUS_COMMIT=$(git ls-remote https://github.com/asterinas/verus HEAD | cut -f1) + # --strip-components=1 drops the tarball's root directory name. + mkdir -p tools/verus + curl -Ls "https://github.com/asterinas/verus/archive/${VERUS_COMMIT}.tar.gz" | tar xz -C tools/verus --strip-components=1 + test -f tools/verus/source/vstd/Cargo.toml + - name: Run format - if: always() && runner.os == 'Linux' run: make fmt - name: Check for formatting changes - if: always() && runner.os == 'Linux' run: | if [[ -n $(git status --porcelain) ]]; then echo "Code is not properly formatted. Run 'make fmt'." @@ -175,20 +167,433 @@ jobs: if: always() run: | { - echo "# CI Summary (${{ matrix.os }})" + echo "# Format Summary" if [[ "${FMT_FAILED:-0}" == "1" ]]; then echo "- Formatting: ❌ failed" else echo "- Formatting: ✅ passed" fi + } >> "$GITHUB_STEP_SUMMARY" + + pr-comment-verify: + # Linux-only /verify-perf leg: verifies the merge ref and uploads the log + # `perf` consumes. Separate from the matrix job because a job-level `if` + # cannot reference `matrix`. + if: > + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/verify-perf') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + runs-on: ubuntu-24.04 + permissions: + contents: read + statuses: write # pending ci/verify-perf status on the PR head commit + timeout-minutes: 240 + steps: + - name: Get PR head commit + id: pr + uses: actions/github-script@v9 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.issue.number; + const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number }); + + core.setOutput('head_sha', pull.head.sha); + + - name: Mark verify-perf as pending + continue-on-error: true + uses: actions/github-script@v9 + with: + retries: 3 + script: | + const { owner, repo } = context.repo; + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: '${{ steps.pr.outputs.head_sha }}', + state: 'pending', + context: 'ci/verify-perf', + description: 'verify-perf comparison is running', + target_url: runUrl, + }); + + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: refs/pull/${{ github.event.issue.number }}/merge + submodules: recursive + + - name: Set up toolchain and caches + uses: ./.github/actions/setup-verus + with: + verus: 'true' + install-verusfmt: 'false' + + - name: Run verification + run: | + set -o pipefail + if ! cargo dv verify --targets ostd -- --time-expanded 2>&1 | tee verify_output.txt; then + echo "VERIFY_FAILED=1" >> "$GITHUB_ENV" + exit 1 + else + echo "Verification passed!" + fi + + - name: Upload verification log + if: always() + uses: actions/upload-artifact@v7 + with: + name: verify-log-ubuntu-24.04 + path: verify_output.txt + retention-days: 7 + if-no-files-found: warn + + - name: Publish summary + if: always() + run: | + { + echo "# PR-comment verification summary (merge ref)" if [[ "${VERIFY_FAILED:-0}" == "1" ]]; then echo "- Verification: ❌ failed" else echo "- Verification: ✅ passed" fi - if [[ "${VERIFY_WARNINGS_FOUND:-0}" == "1" ]]; then - echo "- Verification warnings: ❌ found" + } >> "$GITHUB_STEP_SUMMARY" + + perf: + name: verify-perf + needs: [format-and-verify, pr-comment-verify] + # Runs on every event, parsing a verify leg's log. Push/dispatch: record + # to the gh-pages benchmark history and save the baseline. PRs and + # comment runs: compare against the last main measurement; only comment + # runs post the comparison as a PR comment. `always()` so a failed + # verification still reports the status. + if: > + always() && + (github.event_name != 'issue_comment' || + (github.event.issue.pull_request && + contains(github.event.comment.body, '/verify-perf') && + contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))) + runs-on: ubuntu-24.04 + permissions: + contents: write # push benchmark history to the gh-pages branch + pull-requests: write # post /verify-perf comparison as a PR comment + deployments: write # github-action-benchmark gh-pages deployment + statuses: write # ci/verify-perf commit status on PR head commits + timeout-minutes: 30 + steps: + - name: Get PR head commit + id: pr + if: github.event_name == 'issue_comment' + uses: actions/github-script@v9 + with: + script: | + const { owner, repo } = context.repo; + const pull_number = context.payload.issue.number; + const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number }); + + core.setOutput('head_sha', pull.head.sha); + + - name: Checkout repository + uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/merge', github.event.issue.number) || github.sha }} + + - name: Record which commit was checked out + run: echo "CUR_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" + + # Comment runs use a throwaway perf-prtmp- key so PR metrics never + # become the baseline; restore-keys finds the last main measurement. + - name: Restore prior measurement (baseline) + id: baseline + uses: actions/cache/restore@v6 + with: + path: perf-store + key: ${{ github.event_name == 'issue_comment' && format('perf-prtmp-{0}', github.event.comment.id) || format('perf-metrics-{0}', github.sha) }} + restore-keys: | + perf-metrics- + + - name: Save aside the baseline (if any) + run: | + if [[ -f perf-store/metrics.json ]]; then + BASELINE_SHA=$(cat perf-store/source-sha.txt 2>/dev/null || echo unknown) + echo "BASELINE_SHA=$BASELINE_SHA" >> "$GITHUB_ENV" + mv perf-store/metrics.json perf-baseline.json + echo "Baseline: $BASELINE_SHA" + else + echo "No prior measurement cached; nothing to compare against yet." + fi + + - name: Download verification log + uses: actions/download-artifact@v8 + with: + name: verify-log-ubuntu-24.04 + + - name: Parse cost metrics + run: | + mkdir -p perf-store + python3 tools/verus_perf.py parse verify_output.txt > perf-store/metrics.json + echo "$CUR_SHA" > perf-store/source-sha.txt + echo "recorded run for: $CUR_SHA" + cat perf-store/metrics.json + + - name: Compare against the prior measurement + run: | + mkdir -p out + if [[ -f perf-baseline.json ]]; then + if [[ "${{ github.event_name }}" == "issue_comment" ]]; then + HEADER="PR \`/verify-perf\`: this PR's merge ref (\`$CUR_SHA\`) vs last recorded main (\`${BASELINE_SHA:-unknown}\`)." else - echo "- Verification warnings: ✅ none" + HEADER="Comparing this run (\`$CUR_SHA\`) against the last main measurement (\`${BASELINE_SHA:-unknown}\`)." fi + { + echo "## Verification cost" + echo + echo "$HEADER" + echo + python3 tools/verus_perf.py compare perf-baseline.json perf-store/metrics.json + } > out/perf-report.md + else + { + echo "## Verification cost: first recorded baseline" + echo + if [[ "${{ github.event_name }}" == "issue_comment" ]]; then + echo "No prior main measurement is cached yet (run \`verify-perf\` on main once first), so nothing to compare against. Recording this PR's cost (\`$CUR_SHA\`):" + else + echo "No prior measurement was cached; nothing to compare against yet. This run's cost (\`$CUR_SHA\`):" + fi + echo + echo '```' + cat perf-store/metrics.json + echo '```' + } > out/perf-report.md + fi + cat out/perf-report.md + { + cat out/perf-report.md } >> "$GITHUB_STEP_SUMMARY" + + # A failed verification's log has no results and would parse as zeros. + - name: Convert metrics to benchmark format + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.format-and-verify.result == 'success' + run: | + mkdir -p out + python3 tools/verus_perf.py to-bm perf-store/metrics.json -o out/benchmark.json + cat out/benchmark.json + + - name: Record benchmark on gh-pages + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.format-and-verify.result == 'success' + uses: benchmark-action/github-action-benchmark@v1 + with: + name: verify-perf + tool: customSmallerIsBetter + output-file-path: out/benchmark.json + github-token: ${{ secrets.GITHUB_TOKEN }} + auto-push: true + gh-pages-branch: gh-pages + benchmark-data-dir-path: dev/bench + alert-threshold: '120%' + comment-on-alert: true + summary-always: true + + - name: Save measurement as the next baseline + # Miss-guard: re-running the same sha would collide with its entry. + if: > + (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && + needs.format-and-verify.result == 'success' && + steps.baseline.outputs.cache-hit != 'true' + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: perf-store + key: ${{ format('perf-metrics-{0}', github.sha) }} + + - name: Comment the report on the PR + if: github.event_name == 'issue_comment' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr comment "${{ github.event.issue.number }}" --body-file out/perf-report.md + + - name: Upload report artifact + if: always() + uses: actions/upload-artifact@v7 + with: + name: verify-perf-${{ github.run_id }} + path: | + out/perf-report.md + out/benchmark.json + perf-store/metrics.json + perf-baseline.json + if-no-files-found: ignore + + - name: Report verify-perf status + if: always() && github.event_name == 'issue_comment' + continue-on-error: true + uses: actions/github-script@v9 + with: + retries: 3 + script: | + const { owner, repo } = context.repo; + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + const ok = '${{ job.status }}' === 'success'; + const state = ok ? 'success' : 'failure'; + const description = ok + ? 'verify-perf comparison posted' + : 'verify-perf run failed'; + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: '${{ steps.pr.outputs.head_sha }}', + state, + context: 'ci/verify-perf', + description, + target_url: runUrl, + }); + + doc: + # PRs build the docs; pushes also upload them for `deploy-gh-pages`. + # Needs `perf`: the benchmark embedded below must be on gh-pages first. + needs: [format-and-verify, perf] + if: > + always() && + github.event_name != 'issue_comment' && + (github.event_name == 'pull_request' || + needs.perf.result == 'success' || + needs.perf.result == 'skipped') && + (github.event_name == 'pull_request' || + needs.format-and-verify.result == 'success') + runs-on: ubuntu-24.04 + permissions: + contents: read + pages: write # deploy to GitHub Pages + id-token: write # deploy to GitHub Pages + statuses: write # ci/doc commit status + timeout-minutes: 90 + steps: + - name: Mark doc CI as pending + if: github.event_name == 'push' + continue-on-error: true + uses: actions/github-script@v9 + env: + STATUS_SHA: ${{ github.sha }} + with: + retries: 3 + script: | + const { owner, repo } = context.repo; + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: process.env.STATUS_SHA, + state: 'pending', + context: 'ci/doc', + description: 'Docs build is running', + target_url: runUrl, + }); + + - name: Checkout repository + uses: actions/checkout@v7 + with: + submodules: recursive + + - name: Set up toolchain and caches + uses: ./.github/actions/setup-verus + with: + verus: 'true' + dv-cache: 'restore' + + - name: Build docs + run: make doc + + # Serve the benchmark chart at /dev/bench/. + - name: Embed verify-perf benchmark into the doc artifact + run: | + set -e + if ! git fetch --depth=1 origin gh-pages 2>/dev/null; then + echo "no gh-pages branch yet; benchmark not embedded" + exit 0 + fi + git checkout FETCH_HEAD -- dev/bench || { echo "no dev/bench on gh-pages; skipping"; exit 0; } + mkdir -p doc/dev + mv dev/bench doc/dev/bench + rmdir dev 2>/dev/null || true + echo "embedded dev/bench into the doc artifact:" + ls doc/dev/bench + + - name: Upload artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v5 + with: + path: doc + + - name: Publish summary + if: always() + run: | + { + echo "# Doc Summary" + echo "- Docs build: ${{ job.status == 'success' && '✅ passed' || '❌ failed' }}" + } >> "$GITHUB_STEP_SUMMARY" + + deploy-gh-pages: + needs: [doc] + # The status function is required: without one, the implicit success() + # also counts doc's skipped-by-design transitive needs. + if: > + !cancelled() && + github.event_name != 'pull_request' && + needs.doc.result == 'success' + runs-on: ubuntu-latest + permissions: + pages: write # deploy to GitHub Pages + id-token: write # deploy to GitHub Pages + timeout-minutes: 15 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + report: + # `ci/doc` status for push runs; `always()` flips pending -> failure. + needs: [doc, deploy-gh-pages] + if: always() && github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + timeout-minutes: 10 + steps: + - name: Report doc status on the commit + continue-on-error: true + uses: actions/github-script@v9 + env: + STATUS_SHA: ${{ github.sha }} + with: + retries: 3 + script: | + const { owner, repo } = context.repo; + const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + + const buildResult = '${{ needs.doc.result }}'; + const deployResult = '${{ needs.deploy-gh-pages.result }}'; + // deploy is skipped when docs aren't published -- not a failure. + const ok = buildResult === 'success' && + (deployResult === 'success' || deployResult === 'skipped'); + + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: process.env.STATUS_SHA, + state: ok ? 'success' : 'failure', + context: 'ci/doc', + description: ok ? 'Docs CI succeeded' : 'Docs CI failed', + target_url: runUrl, + }); diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml deleted file mode 100644 index ec26a77da..000000000 --- a/.github/workflows/doc.yml +++ /dev/null @@ -1,225 +0,0 @@ -name: Deploy Rust docs to GitHub Pages - -# Deploys rustdoc to GitHub Pages. To keep the verify-perf benchmark chart -# fresh, this runs AFTER `verify-perf` completes (workflow_run), so the -# benchmark's `dev/bench/` is already on the gh-pages branch when we embed it -# into the artifact — no stale-chart race. PRs build only; manual deploys via -# workflow_dispatch. - -on: - pull_request: - branches: - - main - workflow_dispatch: - workflow_run: - workflows: ["verify-perf"] - types: [completed] - -permissions: - contents: read - pages: write - id-token: write - statuses: write # post ci/doc commit status for workflow_run runs - -concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha || github.ref }} - cancel-in-progress: true - -jobs: - build: - # Only build on PR/dispatch, or on a workflow_run triggered by a *push* to - # main verify-perf (skip PR-comment `/verify-perf` runs). - if: github.event_name != 'workflow_run' || github.event.workflow_run.event == 'push' - runs-on: ubuntu-latest - steps: - # workflow_run runs get no native commit badge; post one manually. - - name: Mark doc CI as pending - if: github.event_name == 'workflow_run' - continue-on-error: true - uses: actions/github-script@v9 - env: - STATUS_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - with: - retries: 3 - script: | - const { owner, repo } = context.repo; - const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; - - await github.rest.repos.createCommitStatus({ - owner, - repo, - sha: process.env.STATUS_SHA, - state: 'pending', - context: 'ci/doc', - description: 'Docs build is running', - target_url: runUrl, - }); - - - name: Checkout repository - uses: actions/checkout@v7 - with: - # workflow_run runs on the default branch's workflow file; build the - # exact commit verify-perf ran on. - ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.ref }} - submodules: recursive - - - name: Install dependencies - run: | - sudo apt update -qq - sudo apt install -y build-essential unzip pkg-config libssl-dev llvm - - - name: Get Rust toolchain version - id: rust-toolchain - run: | - RUST_VERSION=$(grep 'channel' rust-toolchain.toml | sed -E 's/.*= *"(.*)"/\1/') - if [ -z "$RUST_VERSION" ]; then - echo "Failed to extract Rust version from rust-toolchain.toml" - exit 1 - fi - echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" - echo "Rust version: $RUST_VERSION" - - - name: Cache Rust toolchain - uses: actions/cache@v6 - with: - path: | - ~/.rustup/toolchains - ~/.rustup/update-hashes - ~/.rustup/tmp - key: ${{ runner.os }}-rust-toolchain-${{ env.RUST_VERSION }} - - - name: Cache Cargo dependencies - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Get Verus commit - id: verus - run: | - VERUS_COMMIT=$(git ls-remote https://github.com/asterinas/verus HEAD | cut -f1) - echo "VERUS_COMMIT=$VERUS_COMMIT" >> "$GITHUB_ENV" - echo "Using Verus commit: $VERUS_COMMIT" - DV_COMMIT=$(git rev-parse HEAD:dv) - echo "DV_COMMIT=$DV_COMMIT" >> "$GITHUB_ENV" - echo "Using dv commit: $DV_COMMIT" - - - name: Cache dv build artifacts - uses: actions/cache@v6 - with: - path: dv/target - key: ${{ runner.os }}-dv-${{ env.DV_COMMIT }} - - - name: Cache Verus - id: cache-verus - uses: actions/cache@v6 - with: - path: tools/verus - key: ${{ runner.os }}-verus-${{ env.VERUS_COMMIT }} - - - name: Cache verusfmt - id: cache-verusfmt - uses: actions/cache@v6 - with: - path: ~/.cargo/bin/verusfmt - key: ${{ runner.os }}-verusfmt-${{ env.VERUS_COMMIT }} - - - name: Bootstrap Verus (if needed) - run: | - if [ "${{ steps.cache-verus.outputs.cache-hit }}" = "true" ]; then - echo "Using cached Verus" - else - echo "Cache miss, bootstrapping Verus..." - rm -rf tools/verus - cargo dv bootstrap - fi - - if ! command -v verusfmt >/dev/null 2>&1; then - echo "verusfmt not found, installing via cargo dv bootstrap..." - cargo dv bootstrap - fi - verusfmt --version - - - name: Build docs - run: make doc - - # Pull this run's `dev/bench/` (just pushed by verify-perf on the - # gh-pages branch) into the rustdoc artifact so the benchmark chart is - # served at /dev/bench/. - - name: Embed verify-perf benchmark into the doc artifact - run: | - set -e - if ! git fetch --depth=1 origin gh-pages 2>/dev/null; then - echo "no gh-pages branch yet; benchmark not embedded" - exit 0 - fi - git checkout FETCH_HEAD -- dev/bench || { echo "no dev/bench on gh-pages; skipping"; exit 0; } - mkdir -p doc/dev - mv dev/bench doc/dev/bench - rmdir dev 2>/dev/null || true - echo "embedded dev/bench into the doc artifact:" - ls doc/dev/bench - - - name: Upload artifact - # Deploy on dispatch, and on a workflow_run only if verify-perf - # *succeeded* (don't publish docs/benchmark from a failed perf run). - if: > - github.event_name != 'pull_request' && - (github.event_name != 'workflow_run' || - github.event.workflow_run.conclusion == 'success') - uses: actions/upload-pages-artifact@v5 - with: - path: doc - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - needs: build - if: > - github.event_name != 'pull_request' && - (github.event_name != 'workflow_run' || - github.event.workflow_run.conclusion == 'success') - runs-on: ubuntu-latest - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 - - report: - # Post a `ci/doc` status for push-triggered workflow_run runs (no native badge). - # `if: always()` so a failed build still flips pending -> failure. - if: always() && github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push' - needs: [build, deploy] - runs-on: ubuntu-latest - steps: - - name: Report doc status on the commit - continue-on-error: true - uses: actions/github-script@v9 - env: - STATUS_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - with: - retries: 3 - script: | - const { owner, repo } = context.repo; - const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; - - const buildResult = '${{ needs.build.result }}'; - const deployResult = '${{ needs.deploy.result }}'; - // deploy is skipped when docs aren't published (e.g. failed upstream verify-perf) -- not a failure. - const ok = buildResult === 'success' && - (deployResult === 'success' || deployResult === 'skipped'); - - await github.rest.repos.createCommitStatus({ - owner, - repo, - sha: process.env.STATUS_SHA, - state: ok ? 'success' : 'failure', - context: 'ci/doc', - description: ok ? 'Docs CI succeeded' : 'Docs CI failed', - target_url: runUrl, - }); diff --git a/.github/workflows/verify-perf.yml b/.github/workflows/verify-perf.yml deleted file mode 100644 index ebfa48a09..000000000 --- a/.github/workflows/verify-perf.yml +++ /dev/null @@ -1,325 +0,0 @@ -name: verify-perf - -# Records Verus verification cost with a SINGLE verify per run: -# - push to main / workflow_dispatch: verify the pushed commit, compare it -# against the cached prior run for the job summary, AND append the run's -# rlimit to a gh-pages benchmark history (this repo, ONE gh-pages branch) for -# time-series charts + regression alerts (alert-threshold 120%, comment-only; -# rlimit is deterministic so timer jitter never raises a false alert). -# - PR comment `/verify-perf`: verify the PR's merge ref ONCE and compare -# against the last recorded main measurement (from the `perf-metrics-*` -# cache); post the diff as a PR comment. Never writes to gh-pages. -# -# One verify per run either way. Only the gh-pages branch is ever written. - -on: - push: - branches: - - main - paths: - - ".github/workflows/verify-perf.yml" - - "tools/verus_perf.py" - - "dv" - - "Cargo.toml" - - "Cargo.lock" - - "ostd/**" - - "verified_libs/vstd_extra/**" - - "rust-toolchain.toml" - workflow_dispatch: - issue_comment: - types: [created] - -permissions: - contents: write # push benchmark history to the gh-pages branch - pull-requests: write # post /verify-perf comparison as a PR comment - deployments: write # github-action-benchmark gh-pages deployment - statuses: write # mark /verify-perf as pending + report success/failure on the PR head commit - -concurrency: - # push: per-branch. issue_comment: per-PR (github.ref is the default branch - # for comment events, so the issue number keeps different PRs' runs from - # cancelling each other). - group: verify-perf-${{ github.workflow }}-${{ github.ref }}-${{ github.event.issue.number || 'push' }} - cancel-in-progress: true - -jobs: - record-and-compare: - if: > - github.event_name != 'issue_comment' || - (github.event.issue.pull_request && - contains(github.event.comment.body, '/verify-perf') && - contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) - runs-on: ubuntu-24.04 - timeout-minutes: 120 - env: - CARGO_TERM_COLOR: always - - steps: - - name: Get PR head commit - id: pr - if: github.event_name == 'issue_comment' - uses: actions/github-script@v9 - with: - script: | - const { owner, repo } = context.repo; - const pull_number = context.payload.issue.number; - const { data: pull } = await github.rest.pulls.get({ owner, repo, pull_number }); - - core.setOutput('head_sha', pull.head.sha); - - - name: Mark verify-perf as pending - if: github.event_name == 'issue_comment' - continue-on-error: true - uses: actions/github-script@v9 - with: - retries: 3 - script: | - const { owner, repo } = context.repo; - const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; - - await github.rest.repos.createCommitStatus({ - owner, - repo, - sha: '${{ steps.pr.outputs.head_sha }}', - state: 'pending', - context: 'ci/verify-perf', - description: 'verify-perf comparison is running', - target_url: runUrl, - }); - - - name: Checkout repository - uses: actions/checkout@v7 - with: - # push: the pushed commit. issue_comment: the PR's merge ref. - ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/merge', github.event.issue.number) || github.sha }} - submodules: recursive - - - name: Record which commit was checked out - run: echo "CUR_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV" - - - name: Install dependencies - run: | - sudo apt update -qq - sudo apt install -y build-essential unzip pkg-config libssl-dev llvm - - - name: Get Rust toolchain version - run: | - RUST_VERSION=$(grep 'channel' rust-toolchain.toml | sed -E 's/.*= *"(.*)"/\1/') - if [ -z "$RUST_VERSION" ]; then - echo "Failed to extract the Rust version from rust-toolchain.toml" >&2 - exit 1 - fi - echo "RUST_VERSION=$RUST_VERSION" >> "$GITHUB_ENV" - - - name: Cache Rust toolchain - uses: actions/cache@v6 - with: - path: | - ~/.rustup/toolchains - ~/.rustup/update-hashes - ~/.rustup/tmp - key: ${{ runner.os }}-rust-toolchain-${{ env.RUST_VERSION }} - - - name: Cache Cargo dependencies - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Get Verus and dv commits - run: | - VERUS_COMMIT=$(git ls-remote https://github.com/asterinas/verus HEAD | cut -f1) - echo "VERUS_COMMIT=$VERUS_COMMIT" >> "$GITHUB_ENV" - DV_COMMIT=$(git rev-parse HEAD:dv) - echo "DV_COMMIT=$DV_COMMIT" >> "$GITHUB_ENV" - - - name: Cache dv build artifacts - uses: actions/cache@v6 - with: - path: dv/target - key: ${{ runner.os }}-dv-${{ env.DV_COMMIT }} - - - name: Cache Verus - id: cache-verus - uses: actions/cache@v6 - with: - path: tools/verus - key: ${{ runner.os }}-verus-${{ env.VERUS_COMMIT }} - - - name: Cache verusfmt - id: cache-verusfmt - uses: actions/cache@v6 - with: - path: ~/.cargo/bin/verusfmt - key: ${{ runner.os }}-verusfmt-${{ env.VERUS_COMMIT }} - - - name: Bootstrap Verus (if needed) - run: | - if [ "${{ steps.cache-verus.outputs.cache-hit }}" = "true" ]; then - echo "Using cached Verus" - else - rm -rf tools/verus - cargo dv bootstrap - fi - if ! command -v verusfmt >/dev/null 2>&1; then - cargo dv bootstrap - fi - - # On push, the cache key is perf-metrics- (records the measurement - # for the next run). On a PR comment the key is a throwaway - # perf-prtmp- so this run's PR metrics are never read back as - # a baseline; the `restore-keys` prefix still finds the last recorded - # main measurement to compare against. - - name: Restore prior measurement (baseline) - id: baseline - uses: actions/cache@v6 - with: - path: perf-store - key: ${{ github.event_name == 'issue_comment' && format('perf-prtmp-{0}', github.event.comment.id) || format('perf-metrics-{0}', github.sha) }} - restore-keys: | - perf-metrics- - - - name: Save aside the baseline (if any) - run: | - if [[ -f perf-store/metrics.json ]]; then - BASELINE_SHA=$(cat perf-store/source-sha.txt 2>/dev/null || echo unknown) - echo "BASELINE_SHA=$BASELINE_SHA" >> "$GITHUB_ENV" - mv perf-store/metrics.json perf-baseline.json - echo "Baseline: $BASELINE_SHA" - else - echo "No prior measurement cached; nothing to compare against yet." - fi - - - name: Verify with --time-expanded - run: | - set +e - cargo dv verify --targets ostd -- --time-expanded \ - > "$RUNNER_TEMP/verify-time-expanded.log" 2>&1 - VERIFY_EXIT=$? - set -e - echo "::group::verification results" - grep -E "verification results::|^error:" "$RUNNER_TEMP/verify-time-expanded.log" | tail -20 || true - echo "::endgroup::" - if [[ "$VERIFY_EXIT" -ne 0 ]]; then - echo "::error::verification failed (exit $VERIFY_EXIT)" - exit 1 - fi - - - name: Parse cost metrics - run: | - mkdir -p perf-store - python3 tools/verus_perf.py parse "$RUNNER_TEMP/verify-time-expanded.log" > perf-store/metrics.json - echo "$CUR_SHA" > perf-store/source-sha.txt - cat perf-store/metrics.json - - - name: Compare against the prior measurement - run: | - mkdir -p out - if [[ -f perf-baseline.json ]]; then - if [[ "${{ github.event_name }}" == "issue_comment" ]]; then - HEADER="PR \`/verify-perf\`: this PR's merge ref (\`$CUR_SHA\`) vs last recorded main (\`${BASELINE_SHA:-unknown}\`)." - else - HEADER="Comparing this push (\`$CUR_SHA\`) against the prior measurement (\`${BASELINE_SHA:-unknown}\`)." - fi - { - echo "## Verification cost" - echo - echo "$HEADER" - echo - python3 tools/verus_perf.py compare perf-baseline.json perf-store/metrics.json - } > out/perf-report.md - else - { - echo "## Verification cost: first recorded baseline" - echo - if [[ "${{ github.event_name }}" == "issue_comment" ]]; then - echo "No prior main measurement is cached yet (run \`verify-perf\` on main once first), so nothing to compare against. Recording this PR's cost (\`$CUR_SHA\`):" - else - echo "No prior measurement was cached. This run (\`$CUR_SHA\`) records the baseline; the next push will compare against it." - fi - echo - echo '```' - cat perf-store/metrics.json - echo '```' - } > out/perf-report.md - fi - cat out/perf-report.md - { - cat out/perf-report.md - } >> "$GITHUB_STEP_SUMMARY" - - - name: Convert metrics to benchmark format - if: github.event_name != 'issue_comment' - run: | - mkdir -p out - python3 tools/verus_perf.py to-bm perf-store/metrics.json -o out/benchmark.json - cat out/benchmark.json - - - name: Record benchmark on gh-pages - if: github.event_name != 'issue_comment' - uses: benchmark-action/github-action-benchmark@v1 - with: - name: verify-perf - tool: customSmallerIsBetter - output-file-path: out/benchmark.json - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: true - gh-pages-branch: gh-pages - benchmark-data-dir-path: dev/bench - alert-threshold: '120%' - comment-on-alert: true - summary-always: true - - - name: Comment the report on the PR - if: github.event_name == 'issue_comment' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "${{ github.event.issue.number }}" --body-file out/perf-report.md - - - name: Upload report artifact - if: always() - uses: actions/upload-artifact@v4 - with: - name: verify-perf-${{ github.run_id }} - path: | - out/perf-report.md - out/benchmark.json - perf-store/metrics.json - perf-baseline.json - if-no-files-found: ignore - - - name: Report verify-perf status - if: always() && github.event_name == 'issue_comment' - continue-on-error: true - uses: actions/github-script@v9 - with: - retries: 3 - script: | - const { owner, repo } = context.repo; - const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; - const ok = '${{ job.status }}' === 'success'; - const state = ok ? 'success' : 'failure'; - const description = ok - ? 'verify-perf comparison posted' - : 'verify-perf run failed'; - - await github.rest.repos.createCommitStatus({ - owner, - repo, - sha: '${{ steps.pr.outputs.head_sha }}', - state, - context: 'ci/verify-perf', - description, - target_url: runUrl, - }); - - # The "Restore prior measurement (baseline)" cache step's post-action - # saves perf-store/ (this run's metrics.json + source-sha): - # - push -> key perf-metrics- (the next run's baseline) - # - PR comment -> key perf-prtmp- (throwaway; restore-keys - # only reads perf-metrics-*, so it never pollutes main's baseline) From c7e0549310263adf5622ce3f3eaade1c28037689 Mon Sep 17 00:00:00 2001 From: Xinyi Wan <64517311+rikosellic@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:55:35 +0800 Subject: [PATCH 16/30] chore: remove unused lemmas and remove `level` in `NodeOwner` (#767) * chore: remove `level` in `NodeOwner` * remove a unnecessary condition * minor * remove `PageMetaModel` * Remove unused lemmas * remove more * clean more * remove more --- ostd/specs/mm/frame/segment.rs | 42 --- ostd/specs/mm/frame/untyped.rs | 0 .../mm/page_table/cursor/cursor_fn_lemmas.rs | 111 +----- .../mm/page_table/cursor/cursor_steps.rs | 125 +------ ostd/specs/mm/page_table/cursor/owners.rs | 50 +-- .../mm/page_table/cursor/page_size_lemmas.rs | 18 +- .../cursor/page_table_cursor_specs.rs | 73 ---- .../cursor/split_while_huge_lemmas.rs | 235 ------------- .../specs/mm/page_table/cursor/tree_lemmas.rs | 47 +-- ostd/specs/mm/page_table/node/entry.rs | 8 +- ostd/specs/mm/page_table/node/entry_owners.rs | 6 +- ostd/specs/mm/page_table/node/owners.rs | 41 +-- ostd/specs/mm/page_table/owners.rs | 318 +----------------- ostd/src/mm/page_table/cursor/mod.rs | 16 +- ostd/src/mm/page_table/mod.rs | 14 +- ostd/src/mm/page_table/node/entry.rs | 65 ++-- ostd/src/mm/page_table/node/mod.rs | 7 +- 17 files changed, 99 insertions(+), 1077 deletions(-) delete mode 100644 ostd/specs/mm/frame/untyped.rs diff --git a/ostd/specs/mm/frame/segment.rs b/ostd/specs/mm/frame/segment.rs index 0559692a0..e29b97fb9 100644 --- a/ostd/specs/mm/frame/segment.rs +++ b/ostd/specs/mm/frame/segment.rs @@ -60,48 +60,6 @@ impl Segment { ) != frame_to_index((self.range().start + j * PAGE_SIZE) as usize) } - /// Manually instantiates the [`relate_regions`] forall at a specific index. - /// Use this to extract per-frame facts without fighting trigger inference. - pub proof fn relate_regions_at(&self, regions: MetaRegionOwners, i: int) - requires - self.relate_regions(regions), - 0 <= i < seg_nframes(self.range()), - ensures - ({ - let idx = frame_to_index((self.range().start + i * PAGE_SIZE) as usize); - &&& self.slot_perms()[i] == regions.slots[idx] - &&& self.permissions()[i].frac() == 1 - &&& self.permissions()[i].id() == regions.slot_owners[idx].metadata_perm.id() - &&& MetaSlot::perms_related(*self.slot_perms()[i], self.permissions()[i].resource()) - &&& regions.contains(idx) - &&& regions.slot_owners[idx].slot_vaddr == index_to_meta(idx) - &&& 0 < regions.slot_owners[idx].ref_count() - <= crate::mm::frame::meta::REF_COUNT_MAX - &&& regions.slot_owners[idx].paths_in_pt.is_empty() - &&& regions.slot_owners[idx].usage is Frame - }), - { - // Trigger the forall at index `i`. - let _ = frame_to_index((self.range().start + i * PAGE_SIZE) as usize); - } - - /// Manually instantiates the [`relate_regions`] distinctness forall at a - /// specific index pair: distinct in-range frames map to distinct slot - /// indices. Reusable lever for `from_unused`/`split`/`slice` proofs. - pub proof fn relate_regions_distinct(&self, regions: MetaRegionOwners, i: int, j: int) - requires - self.relate_regions(regions), - 0 <= i < j < seg_nframes(self.range()), - ensures - frame_to_index((self.range().start + i * PAGE_SIZE) as usize) != frame_to_index( - (self.range().start + j * PAGE_SIZE) as usize, - ), - { - // Trigger the distinctness forall at `(i, j)`. - let _ = frame_to_index((self.range().start + i * PAGE_SIZE) as usize); - let _ = frame_to_index((self.range().start + j * PAGE_SIZE) as usize); - } - /// The bundled invariant for [`Segment`] operations that thread the global /// `regions`: the segment's own invariant, the region invariant, and the /// cross-object relation tying this segment's range to `regions`. diff --git a/ostd/specs/mm/frame/untyped.rs b/ostd/specs/mm/frame/untyped.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/ostd/specs/mm/page_table/cursor/cursor_fn_lemmas.rs b/ostd/specs/mm/page_table/cursor/cursor_fn_lemmas.rs index 94b04f376..0619dd78e 100644 --- a/ostd/specs/mm/page_table/cursor/cursor_fn_lemmas.rs +++ b/ostd/specs/mm/page_table/cursor/cursor_fn_lemmas.rs @@ -1,11 +1,4 @@ -/// Cursor function-specific lemmas for `CursorOwner`. -/// -/// Themes moved here from `owners.rs`: -/// - **Theme 7**: PTE & entry modification invariant preservation -/// (`protect_preserves_cursor_inv_metaregion`, `map_branch_none_*`) -/// - **Theme 14**: Cursor path structure & jump utilities -/// (`cursor_path_nesting`, `jump_above_locked_range_va_in_node`, -/// `jump_not_in_node_level_lt_guard_minus_one`, `lemma_page_size_spec_5_eq_pow2_48`) +//! Cursor function-specific lemmas for `CursorOwner`. use core::ops::Range; use vstd::prelude::*; @@ -308,108 +301,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { }; }; } - - /// After `map_branch_none` (alloc_if_none + push_level), the current entry is absent. - /// - /// Proof: `alloc_if_none` creates an empty PT node where all children are absent - /// (`allocated_empty_node_owner` line 172). `push_level` enters one of these children, - /// so `cur_entry_owner().is_absent()` holds. - pub proof fn map_branch_none_cur_entry_absent(self) - requires - self.inv(), - // All children of the current continuation are absent (from the empty node) - forall|i: int| - 0 <= i < NR_ENTRIES ==> #[trigger] self.continuations[self.level - - 1].children[i] is Some && self.continuations[self.level - - 1].children[i]->0.value().is_absent(), - ensures - self.cur_entry_owner().is_absent(), - { - } - - pub proof fn cursor_path_nesting(self, i: int, j: int) - requires - self.inv(), - self.level - 1 <= j < i, - i < NR_LEVELS, - ensures - self.continuations[j].path().len() as int > self.continuations[i].path().len(), - self.continuations[j].path()[self.continuations[i].path().len() as int] - == self.continuations[i].idx, - { - if i == 3 && j == 2 { - } else if i == 3 && j == 1 { - let p3 = self.continuations[3].path(); - let p2 = self.continuations[2].path(); - let idx3 = self.continuations[3].idx as int; - let idx2 = self.continuations[2].idx as int; - assert(p3.len() < p2.len()); - assert(self.continuations[1].path() == p2.push_tail(idx2)); - assert(p2.push_tail(idx2)[p3.len() as int] == p2[p3.len() as int]); - } else if i == 3 && j == 0 { - let p3 = self.continuations[3].path(); - let p2 = self.continuations[2].path(); - let p1 = self.continuations[1].path(); - let idx3 = self.continuations[3].idx as int; - let idx2 = self.continuations[2].idx as int; - let idx1 = self.continuations[1].idx as int; - assert(p3.len() < p2.len()); - assert(p3.len() < p1.len()); - assert(p1.push_tail(idx1)[p3.len() as int] == p1[p3.len() as int]); - assert(p2.push_tail(idx2)[p3.len() as int] == p2[p3.len() as int]); - } else if i == 2 && j == 1 { - } else if i == 2 && j == 0 { - let p2 = self.continuations[2].path(); - let p1 = self.continuations[1].path(); - let idx2 = self.continuations[2].idx as int; - let idx1 = self.continuations[1].idx as int; - assert(p2.len() < p1.len()); - assert(self.continuations[0].path() == p1.push_tail(idx1)); - assert(p1.push_tail(idx1)[p2.len() as int] == p1[p2.len() as int]); - assert(p1 == p2.push_tail(idx2)); - assert(p2.push_tail(idx2)[p2.len() as int] == idx2); - } else if i == 1 && j == 0 { - } - } - - pub proof fn lemma_page_size_spec_5_eq_pow2_48() - ensures - page_size(5) == pow2(48nat) as usize, - { - crate::arch::mm::lemma_nr_subpage_per_huge_eq_nr_entries(); - vstd_extra::external::ilog2::lemma_usize_ilog2_to32(); - vstd::arithmetic::power2::lemma2_to64(); - vstd::arithmetic::power2::lemma2_to64_rest(); - vstd::arithmetic::power2::lemma_pow2_adds(12nat, 36nat); - } - - pub proof fn jump_not_in_node_level_lt_guard_minus_one( - self, - level: PagingLevel, - va: Vaddr, - node_start: Vaddr, - ) - requires - self.inv(), - self.locked_range().start <= va < self.locked_range().end, - 1 <= level, - level + 1 <= self.guard_level, - self.locked_range().start <= node_start, - node_start + page_size((level + 1) as PagingLevel) <= self.locked_range().end, - !(node_start <= va && va < node_start + page_size((level + 1) as PagingLevel)), - ensures - level + 1 < self.guard_level, - { - if level + 1 == self.guard_level { - let pv = self.prefix.to_vaddr() as nat; - let ps = page_size(self.guard_level as PagingLevel) as nat; - self.prefix.align_down_concrete(self.guard_level as int); - self.lemma_prefix_aligned_to_guard_level(); - self.lemma_prefix_plus_ps_no_overflow(); - self.prefix.aligned_align_up_advances(self.guard_level as int); - AbstractVaddr::from_vaddr_to_vaddr_roundtrip(nat_align_down(pv, ps) as Vaddr); - } - } } } // verus! diff --git a/ostd/specs/mm/page_table/cursor/cursor_steps.rs b/ostd/specs/mm/page_table/cursor/cursor_steps.rs index f765762e8..744f2ba15 100644 --- a/ostd/specs/mm/page_table/cursor/cursor_steps.rs +++ b/ostd/specs/mm/page_table/cursor/cursor_steps.rs @@ -30,40 +30,6 @@ verus! { broadcast use group_ghost_tree_lemmas; -/// Paths obtained by push_tail with different indices are different -pub proof fn push_tail_different_indices_different_paths(path: TreePath, i: int, j: int) - requires - path.inv(), - 0 <= i < NR_ENTRIES, - 0 <= j < NR_ENTRIES, - i != j, - ensures - path.push_tail(i) != path.push_tail(j), -{ -} - -/// Paths with different lengths are different -pub proof fn different_length_different_paths( - path1: TreePath, - path2: TreePath, -) - requires - path1.len() != path2.len(), - ensures - path1 != path2, -{ -} - -/// A path obtained by push_tail has greater length than the original -pub proof fn push_tail_increases_length(path: TreePath, i: int) - requires - path.inv(), - 0 <= i < NR_ENTRIES, - ensures - path.push_tail(i).len() > path.len(), -{ -} - /// Upgrade `node_unlocked_except` to `node_unlocked` on a subtree where the excepted /// entry cannot appear. The precondition `path == subtree.value.path` ties structural /// positions to entry paths. `excepted_path` must differ from all descendant paths, @@ -185,13 +151,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { )) as nat } - pub proof fn max_steps_subtree_positive(level: usize) - ensures - Self::max_steps_subtree(level) > 0, - decreases level, - { - } - /// Two owners with the same idx values from `start` upward have the same max_steps_partial. pub proof fn max_steps_partial_eq(self, other: Self, start: usize) requires @@ -275,18 +234,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { } - pub proof fn push_level_owner_preserves_va(self, guard: PageTableGuard<'rcu, C>) - requires - self.inv(), - self.level > 1, - ensures - self.push_level_owner(guard).va == self.va, - self.push_level_owner(guard).continuations[self.level - 2].idx - == self.va.index[self.level - 2], - { - assert(self.va.index.contains_key(self.level - 2)); - } - pub proof fn push_level_owner_preserves_mappings(self, guard: PageTableGuard<'rcu, C>) requires self.inv(), @@ -374,7 +321,7 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { == child.entry_own.node().tree_level + 1 &&& child.children[j].unwrap().value().match_pte( child.entry_own.node().children_perm.value()[j], - child.entry_own.node().level, + child.entry_own.node().level(), ) &&& as TreeNodeValue>::rel_children( child.entry_own, @@ -649,64 +596,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { reveal(CursorOwner::path_metaregion_sound); } - /// Update va to a new value that shares the same indices at levels >= self.level. - /// This preserves invariants because: - /// 1. The new va satisfies va.inv() - /// 2. The indices at levels >= level match the continuation indices - /// 3. in_locked_range/above_locked_range depend on va but the preconditions ensure consistency - pub proof fn set_va_preserves_inv(self, new_va: AbstractVaddr) - requires - self.inv(), - self.in_locked_range(), - !self.popped_too_high, - self.level <= self.guard_level, - new_va.inv(), - new_va.offset == 0, - new_va.leading_bits == self.prefix.leading_bits, - forall|i: int| - #![auto] - self.level - 1 <= i < NR_LEVELS ==> new_va.index[i] == self.va.index[i], - forall|i: int| - #![auto] - self.guard_level - 1 <= i < NR_LEVELS ==> new_va.index[i] == self.prefix.index[i], - ensures - self.set_va(new_va).inv(), - { - let r = self.set_va(new_va); - - assert(r.in_locked_range()) by { - let gl = self.guard_level; - if gl >= 1 && gl <= NR_LEVELS { - r.va.align_down_to_vaddr_eq_if_upper_indices_eq(r.prefix, gl as int); - r.va.align_down_concrete(gl as int); - r.prefix.align_down_concrete(gl as int); - // Use cursor inv helpers on self (r.prefix == self.prefix). - self.lemma_prefix_aligned_to_guard_level(); - self.lemma_prefix_plus_ps_no_overflow(); - r.prefix.aligned_align_up_advances(gl as int); - AbstractVaddr::from_vaddr_to_vaddr_roundtrip( - nat_align_down( - r.va.to_vaddr() as nat, - page_size(gl as PagingLevel) as nat, - ) as Vaddr, - ); - AbstractVaddr::from_vaddr_to_vaddr_roundtrip( - nat_align_down( - r.prefix.to_vaddr() as nat, - page_size(gl as PagingLevel) as nat, - ) as Vaddr, - ); - - lemma_nat_align_down_sound( - r.va.to_vaddr() as nat, - page_size(gl as PagingLevel) as nat, - ); - - } - }; - - } - pub open spec fn move_forward_owner_spec(self) -> Self decreases NR_LEVELS - self.level, when self.level <= NR_LEVELS @@ -906,18 +795,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { } } - /// Trivial: zero_below_level is defined as Self { va: self.va.align_down(level), ..self }. - pub proof fn zero_below_level_eq_align_down(self) - requires - self.va.inv(), - self.va.offset == 0, - 1 <= self.level <= NR_LEVELS, - ensures - self.zero_below_level().va == self.va.align_down(self.level as int), - decreases self.level, - { - } - #[verifier::spinoff_prover] pub proof fn move_forward_va_is_align_up(self) requires diff --git a/ostd/specs/mm/page_table/cursor/owners.rs b/ostd/specs/mm/page_table/cursor/owners.rs index 407ed501f..88a24ca79 100644 --- a/ostd/specs/mm/page_table/cursor/owners.rs +++ b/ostd/specs/mm/page_table/cursor/owners.rs @@ -279,7 +279,7 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { // map_children_lift, map_children_lift_skip_idx, as_subtree_restore // have been moved to tree_lemmas.rs. pub open spec fn level(self) -> PagingLevel { - self.entry_own.node().level + self.entry_own.node().level() } pub open spec fn inv_children(self) -> bool { @@ -324,7 +324,7 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { &&& child->0.value().path.len() == self.entry_own.node().tree_level + 1 &&& child->0.value().match_pte( self.entry_own.node().children_perm.value()[i], - self.entry_own.node().level, + self.entry_own.node().level(), ) &&& child->0.value().path == self.path().push_tail(i) } @@ -364,7 +364,7 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { self.children[i]->0.value().path.len() == self.entry_own.node().tree_level + 1, self.children[i]->0.value().match_pte( self.entry_own.node().children_perm.value()[i], - self.entry_own.node().level, + self.entry_own.node().level(), ), self.children[i]->0.value().path == self.path().push_tail(i), { @@ -525,10 +525,10 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { child_value.path.len() == parent_owner.tree_level + 1, child_value.match_pte( parent_owner.children_perm.value()[idx as int], - parent_owner.level, + parent_owner.level(), ), child_value.path == entry_own.path.push_tail(idx as int), - child_value.parent_level == parent_owner.level, + child_value.parent_level == parent_owner.level(), { } @@ -567,7 +567,7 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { self.entry_own.is_node(), self.entry_own.inv(), self.entry_own.node().relate_guard(self.guard), - self.entry_own.node().level == parent_old.level, + self.entry_own.node().level() == parent_old.level(), self.entry_own.node().tree_level == parent_old.tree_level, // Other PTEs preserved (operation only touched the entry at idx) forall|j: int| @@ -597,7 +597,7 @@ impl<'rcu, C: PageTableConfig> CursorContinuation<'rcu, C> { + 1, self.children[self.idx as int]->0.value().match_pte( self.entry_own.node().children_perm.value()[self.idx as int], - self.entry_own.node().level, + self.entry_own.node().level(), ), // The new child satisfies the PT-specific tree invariant. This is // operation-specific (alloc_if_none/protect/split_if_mapped_huge/ @@ -794,10 +794,10 @@ impl<'rcu, C: PageTableConfig> Inv for CursorOwner<'rcu, C> { == self.continuations[3].entry_own.node().tree_level + 1 &&& self.continuations[2].entry_own.match_pte( self.continuations[3].entry_own.node().children_perm.value()[self.continuations[3].idx as int], - self.continuations[3].entry_own.node().level, + self.continuations[3].entry_own.node().level(), ) &&& self.continuations[2].entry_own.parent_level - == self.continuations[3].entry_own.node().level + == self.continuations[3].entry_own.node().level() } &&& self.level <= 2 ==> { &&& self.continuations.contains_key(1) @@ -818,10 +818,10 @@ impl<'rcu, C: PageTableConfig> Inv for CursorOwner<'rcu, C> { == self.continuations[2].entry_own.node().tree_level + 1 &&& self.continuations[1].entry_own.match_pte( self.continuations[2].entry_own.node().children_perm.value()[self.continuations[2].idx as int], - self.continuations[2].entry_own.node().level, + self.continuations[2].entry_own.node().level(), ) &&& self.continuations[1].entry_own.parent_level - == self.continuations[2].entry_own.node().level + == self.continuations[2].entry_own.node().level() } &&& self.level == 1 ==> { &&& self.continuations.contains_key(0) @@ -844,10 +844,10 @@ impl<'rcu, C: PageTableConfig> Inv for CursorOwner<'rcu, C> { == self.continuations[1].entry_own.node().tree_level + 1 &&& self.continuations[0].entry_own.match_pte( self.continuations[1].entry_own.node().children_perm.value()[self.continuations[1].idx as int], - self.continuations[1].entry_own.node().level, + self.continuations[1].entry_own.node().level(), ) &&& self.continuations[0].entry_own.parent_level - == self.continuations[1].entry_own.node().level + == self.continuations[1].entry_own.node().level() } } } @@ -935,30 +935,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { self.map_children_implies(f, g); } - /// After dropping the guard for the popped level, `nodes_locked` is preserved - /// for the new (higher-level) owner, because the dropped guard's address is not - /// among those checked by `nodes_locked` (which covers levels >= self.level - 1). - pub proof fn lemma_never_drop_restores_nodes_locked( - self, - guard: PageTableGuard<'rcu, C>, - guards0: Guards, - guards1: Guards, - ) - requires - self.inv(), - self.nodes_locked(guards0), - guards0.lock_held(guard.inner.inner@.ptr.addr()), - guards1.guards == guards0.guards.remove(guard.inner.inner@.ptr.addr()), - forall|i: int| - #![trigger self.continuations[i]] - self.level - 1 <= i < NR_LEVELS - ==> self.continuations[i].guard.inner.inner@.ptr.addr() - != guard.inner.inner@.ptr.addr(), - ensures - self.nodes_locked(guards1), - { - } - /// After a `protect` operation that only modifies `frame.prop` of the current entry, /// `CursorOwner::inv()` and `metaregion_sound` are preserved. /// diff --git a/ostd/specs/mm/page_table/cursor/page_size_lemmas.rs b/ostd/specs/mm/page_table/cursor/page_size_lemmas.rs index f98f8aaab..2aff8f7c0 100644 --- a/ostd/specs/mm/page_table/cursor/page_size_lemmas.rs +++ b/ostd/specs/mm/page_table/cursor/page_size_lemmas.rs @@ -218,8 +218,7 @@ pub proof fn lemma_page_size_divides(l1: PagingLevel, l2: PagingLevel) } /// For any valid physical address `pa < MAX_PADDR` and page level, pa + page_size(level) -/// does not overflow usize. This holds because MAX_PADDR = 2^31 and page sizes are at -/// most 2^39 (NR_LEVELS = 4), so pa + size < 2^40 << usize::MAX = 2^64. +/// does not overflow usize. pub proof fn lemma_pa_plus_page_size_no_overflow(pa: Paddr, level: PagingLevel) requires 1 <= level <= NR_LEVELS, @@ -232,12 +231,6 @@ pub proof fn lemma_pa_plus_page_size_no_overflow(pa: Paddr, level: PagingLevel) /// For any VA within the kernel virtual address range and any page level, /// va + page_size(level) does not overflow usize. -/// KERNEL_VADDR_RANGE.end = 0xffff_ffff_ffff_0000 and max page_size (level 4) = 512GB = 0x80_0000_0000. -/// The sum is at most 0x1_0000_7fff_ffff_0000 which overflows 64-bit usize. -/// However, at the levels actually used (1-3), page_size <= 1GB = 0x4000_0000, and -/// 0xffff_ffff_ffff_0000 + 0x4000_0000 = 0x1_0000_0000_3fff_0000 — still overflows. -/// So this lemma requires va + page_size(level) <= barrier_va.end <= KERNEL_VADDR_RANGE.end, -/// which is guaranteed by !map_panic_conditions / !find_next_panic_condition. pub proof fn lemma_va_plus_page_size_no_overflow(va: Vaddr, len: usize) requires va + len <= KERNEL_VADDR_RANGE.end, @@ -247,13 +240,4 @@ pub proof fn lemma_va_plus_page_size_no_overflow(va: Vaddr, len: usize) assert(KERNEL_VADDR_RANGE.end == 0xffff_ffff_ffff_0000usize) by (compute_only); } -/// The number of base pages in the address space fits in usize. -/// max pages = MAX_PADDR / PAGE_SIZE = 0x8000_0000 / 0x1000 = 0x8_0000 = 524288. -pub proof fn lemma_max_mappings_fit_usize() - ensures - MAX_PADDR / PAGE_SIZE < usize::MAX, -{ - assert(MAX_PADDR / PAGE_SIZE < usize::MAX) by (compute_only); -} - } // verus! diff --git a/ostd/specs/mm/page_table/cursor/page_table_cursor_specs.rs b/ostd/specs/mm/page_table/cursor/page_table_cursor_specs.rs index 49e17c370..26973212b 100644 --- a/ostd/specs/mm/page_table/cursor/page_table_cursor_specs.rs +++ b/ostd/specs/mm/page_table/cursor/page_table_cursor_specs.rs @@ -85,44 +85,6 @@ impl CursorView { } } - /// The specification for the internal function, `find_next_impl`. It finds the next mapped virtual address - /// that is at most `len` bytes away from the current virtual address. TODO: add the specifications for - /// `find_unmap_subtree` and `split_huge`, which are used by other functions that call this one. - /// This returns a mapping rather than the address because that is useful when it's called as a subroutine. - pub open spec fn find_next_impl_spec( - self, - len: usize, - find_unmap_subtree: bool, - split_huge: bool, - ) -> (Self, Option) { - let mappings_in_range = self.mappings.filter( - |m: Mapping| self.cur_va <= m.va_range.start < self.cur_va + len, - ); - - if mappings_in_range.len() > 0 { - let mapping = mappings_in_range.find_unique_minimal( - |m: Mapping, n: Mapping| m.va_range.start < n.va_range.start, - ); - let view = CursorView { cur_va: mapping.va_range.end as Vaddr, ..self }; - (view, Some(mapping)) - } else { - let view = CursorView { cur_va: (self.cur_va + len) as Vaddr, ..self }; - (view, None) - } - } - - /// Actual specification for `find_next`. The cursor finds the next mapped virtual address - /// that is at most `len` bytes away from the current virtual address, returns it, and then - /// moves the cursor forward to the next end of its range. - pub open spec fn find_next_spec(self, len: usize) -> (Self, Option) { - let (cursor, mapping) = self.find_next_impl_spec(len, false, false); - if mapping is Some { - (cursor, Some(mapping->0.va_range.start as Vaddr)) - } else { - (cursor, None) - } - } - /// Jump just sets the current virtual address to the given address. pub open spec fn jump_spec(self, va: usize) -> Self { CursorView { cur_va: va as Vaddr, ..self } @@ -274,41 +236,6 @@ impl CursorView { parent.pa_range.start + (m.va_range.start - parent.va_range.start)) as Paddr && m.property == parent.property } - - /// Models `protect_next`: find the next mapping in range, split it to - /// `target_page_size` if it is a huge page, then update its property via `op`. - /// - /// `target_page_size` corresponds to the cursor level after `find_next_impl` - /// with `split_huge = true` — this is determined by the page table structure - /// and cannot be derived from the abstract view alone. - pub open spec fn protect_spec( - self, - len: usize, - op: spec_fn(PageProperty) -> PageProperty, - target_page_size: usize, - ) -> (Self, Option>) { - let (find_cursor, next) = self.find_next_impl_spec(len, false, true); - if next is Some { - let found = next->0; - // Position cursor at the found mapping and split to target size - let at_found = CursorView { cur_va: found.va_range.start as Vaddr, ..self }; - let split_view = at_found.split_while_huge(target_page_size); - // The mapping at cur_va in the split view is the one to protect - let split_mapping = split_view.query_mapping(); - let new_mapping = Mapping { property: op(split_mapping.property), ..split_mapping }; - let new_cursor = CursorView { - cur_va: split_mapping.va_range.end as Vaddr, - mappings: split_view.mappings - set![split_mapping] + set![new_mapping], - ..self - }; - ( - new_cursor, - Some(split_mapping.va_range.start as Vaddr..split_mapping.va_range.end as Vaddr), - ) - } else { - (find_cursor, None) - } - } } } // verus! diff --git a/ostd/specs/mm/page_table/cursor/split_while_huge_lemmas.rs b/ostd/specs/mm/page_table/cursor/split_while_huge_lemmas.rs index 658b17d50..7e3ed6fa6 100644 --- a/ostd/specs/mm/page_table/cursor/split_while_huge_lemmas.rs +++ b/ostd/specs/mm/page_table/cursor/split_while_huge_lemmas.rs @@ -352,18 +352,6 @@ impl CursorView { self.split_if_mapped_huge_spec(new_size).split_while_huge_compose(s1, s2); } - /// When the current entry is absent or maps at `page_size <= size`, `split_while_huge(size)` - /// is a no-op. Applying a second call with the same `size` therefore returns the same value. - pub proof fn split_while_huge_idempotent(self, size: usize) - requires - self.inv(), - size >= PAGE_SIZE, - ensures - self.split_while_huge(size).split_while_huge(size) == self.split_while_huge(size), - { - self.split_while_huge_compose(size, size); - } - /// When `split_while_huge(size)` is a no-op and the view is `present()`, /// the mapping at `cur_va` already has `page_size <= size`. pub proof fn split_while_huge_noop_implies_page_size_le(self, size: usize) @@ -459,61 +447,8 @@ impl CursorView { assert(new_self.split_while_huge(size) == new_self); } - /// Locality of `split_if_mapped_huge_spec`: a mapping `m2` whose VA range - /// is disjoint from the mapping at `cur_va` is preserved. - pub proof fn split_if_mapped_huge_spec_locality(self, new_size: usize, m2: Mapping) - requires - self.inv(), - self.present(), - new_size > 0, - self.query_mapping().page_size % new_size == 0, - Mapping::disjoint_vaddrs(m2, self.query_mapping()), - ensures - self.split_if_mapped_huge_spec(new_size).mappings.contains(m2) - == self.mappings.contains(m2), - { - let m = self.query_mapping(); - let size = m.page_size; - let new_mappings = Set::::range(0int, (size / new_size) as int).map( - |n: int| Self::split_index(m, new_size, n as usize), - ); - - // Establish m covers cur_va (from present() + choose semantics). - let f = self.mappings.filter( - |m2: Mapping| m2.va_range.start <= self.cur_va < m2.va_range.end, - ); - vstd::set::lemma_set_choose_len(f); - assert(m.inv()); - - assert(!new_mappings.contains(m2)) by { - if new_mappings.contains(m2) { - let k = choose|k: int| - 0 <= k < size as int / new_size as int && #[trigger] Self::split_index( - m, - new_size, - k as usize, - ) == m2; - vstd::arithmetic::div_mod::lemma_fundamental_div_mod(size as int, new_size as int); - vstd::arithmetic::mul::lemma_mul_inequality( - (k + 1) as int, - size as int / new_size as int, - new_size as int, - ); - vstd::arithmetic::mul::lemma_mul_is_distributive_add_other_way( - new_size as int, - k, - 1int, - ); - } - }; - } - /// Locality of `split_while_huge`: a mapping `m2` that is in `self.mappings` /// and whose VA range does not contain `cur_va` is preserved. - /// - /// This is stronger than `split_if_mapped_huge_spec_locality` because it - /// handles the recursive case: each step only splits the mapping at `cur_va`, - /// and `m2` is disjoint from that mapping (by non-overlap invariant). #[verifier::rlimit(80)] pub proof fn split_while_huge_locality(self, size: usize, m2: Mapping) requires @@ -554,79 +489,6 @@ impl CursorView { } } - /// Converse locality: a mapping NOT in `self.mappings` and whose VA range - /// does not overlap any mapping in `self.mappings` that contains `cur_va` - /// is also NOT in `self.split_while_huge(size).mappings`. - /// - /// More precisely: if `m2 ∉ self.mappings` and `m2.va_range` is disjoint - /// from the range `[start, end)` of the mapping at `cur_va` (if present), - /// then `m2 ∉ self.split_while_huge(size).mappings`. - #[verifier::rlimit(120)] - pub proof fn split_while_huge_locality_absent(self, size: usize, m2: Mapping) - requires - self.inv(), - size >= PAGE_SIZE, - !self.mappings.contains(m2), - self.present() ==> Mapping::disjoint_vaddrs(m2, self.query_mapping()), - ensures - !self.split_while_huge(size).mappings.contains(m2), - decreases - if self.present() { - self.query_mapping().page_size as int - } else { - 0 - }, - { - if self.present() { - let m = self.query_mapping(); - if m.page_size > size { - let new_size = m.page_size / NR_ENTRIES; - // Establish m covers cur_va and m.inv(). - let f = self.mappings.filter( - |m3: Mapping| m3.va_range.start <= self.cur_va < m3.va_range.end, - ); - vstd::set::lemma_set_choose_len(f); - // page_size % new_size == 0 - assert(m.inv()); - assert(m.page_size % new_size == 0) by { - assert(2097152usize % (2097152usize / 512usize) == 0) by (compute_only); - assert(1073741824usize % (1073741824usize / 512usize) == 0) by (compute_only); - }; - assert(set![4096usize, 2097152, 1073741824].contains(new_size)) by { - if m.page_size != 2097152 && m.page_size != 1073741824 { - assert(false); - } - }; - let new_self = self.split_if_mapped_huge_spec(new_size); - Self::split_if_mapped_huge_spec_preserves_inv(self, new_size); - Self::split_if_mapped_huge_spec_decreases_page_size(self, new_size); - assert(new_self.present() ==> Mapping::disjoint_vaddrs( - m2, - new_self.query_mapping(), - )) by { - if new_self.present() { - let new_m = new_self.query_mapping(); - let nf = new_self.mappings.filter( - |m3: Mapping| m3.va_range.start <= new_self.cur_va < m3.va_range.end, - ); - vstd::set::lemma_set_choose_len(nf); - if self.mappings.contains(new_m) && new_m != m { - assert(false); - } - let new_mappings = Set::::range( - 0int, - m.page_size as int / new_size as int, - ).map(|n: int| Self::split_index(m, new_size, n as usize)); - let k = choose|k: int| - 0 <= k < m.page_size as int / new_size as int - && #[trigger] Self::split_index(m, new_size, k as usize) == new_m; - } - }; - new_self.split_while_huge_locality_absent(size, m2); - } - } - } - /// Refinement: every mapping in `split_while_huge(size).mappings` is either /// from `self.mappings` or a sub-mapping of an entry in `self.mappings`. /// Base lemma: every mapping in `split_if_mapped_huge_spec(new_size).mappings` @@ -729,27 +591,6 @@ impl CursorView { } } - // To speed up `take_next` verification. - pub proof fn split_while_huge_preserves_empty_prefix( - self, - split_view: CursorView, - size: usize, - m: Mapping, - ) - requires - self.inv(), - size >= PAGE_SIZE, - self.cur_va <= split_view.cur_va, - self.cur_va < split_view.cur_va ==> !self.present(), - self.mappings.filter(|m2: Mapping| self.cur_va <= m2.va_range.start < split_view.cur_va) - == Set::::empty(), - self.split_while_huge(size).mappings.contains(m), - self.cur_va <= m.va_range.start < split_view.cur_va, - ensures - self.mappings.contains(m), - { - } - /// `split_while_huge` produces a set disjoint from any set that is /// pairwise VA-disjoint from `self.mappings`. /// @@ -919,47 +760,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { } } - /// After `map_branch_none` splits a huge frame at level `level_before_frame` and descends, - /// the cursor view equals `owner0@.split_while_huge(page_size(level_before_frame - 1))`. - /// - /// Chain: - /// owner@ = owner_before_frame@.split_if_mapped_huge_spec(page_size(level_before_frame - 1)) - /// = owner0@.split_while_huge(page_size(level_before_frame)).split_if_mapped_huge_spec(...) - /// = owner0@.split_while_huge(page_size(level_before_frame - 1)) - /// The last equality uses the fact that split_while_huge(L) on a frame of size page_size(L) - /// takes exactly one split step to page_size(L-1), matching split_if_mapped_huge_spec. - pub proof fn map_branch_frame_split_while_huge( - self, - owner0: Self, - owner_before_frame: Self, - level_before_frame: int, - ) - requires - self.inv(), - owner0.inv(), - owner_before_frame.inv(), - 1 <= level_before_frame - 1, - level_before_frame <= NR_LEVELS, - self.level == (level_before_frame - 1) as u8, - owner_before_frame@ == owner0@.split_while_huge( - page_size(level_before_frame as PagingLevel), - ), - self@ == owner_before_frame@.split_if_mapped_huge_spec( - page_size((level_before_frame - 1) as PagingLevel), - ), - // The mapping at cur_va in owner_before_frame is exactly the - // frame at the level being split: present, with page_size equal - // to page_size(level_before_frame). Both follow from being in - // the ChildRef::Frame branch at level `level_before_frame`. - owner_before_frame@.present(), - owner_before_frame@.query_mapping().page_size == page_size( - level_before_frame as PagingLevel, - ), - { - owner0.view_preserves_inv(); - owner_before_frame.view_preserves_inv(); - } - /// After split_if_mapped_huge + push_level, the mappings equal /// `old_view.split_while_huge(page_size(current_level))`. pub proof fn find_next_split_push_equals_split_while_huge(self, old_view: CursorView) @@ -988,41 +788,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { crate::specs::mm::page_table::cursor::page_size_lemmas::lemma_page_size_spec_values(); old_view.split_while_huge_one_step(ps); } - - /// `split_while_huge` gives the same mappings for two `cur_va` values - /// when no mapping starts between them and the `!present` case is a no-op. - /// - /// The `v1.cur_va < v2.cur_va ==> !v1.present()` precondition rules out - /// the genuinely hard case where v1's query mapping spans v2.cur_va but - /// gets split inconsistently — at the call site this is supplied by - /// `find_next_impl`'s ensures (`final.va > old.va ==> !old(owner)@.present()`). - pub proof fn split_while_huge_cur_va_independent( - v1: CursorView, - v2: CursorView, - size: usize, - ) - requires - v1.inv(), - v2.inv(), - v1.mappings =~= v2.mappings, - v1.cur_va <= v2.cur_va, - // No mapping starts in [v1.cur_va, v2.cur_va). - v1.mappings.filter( - |m: Mapping| v1.cur_va <= m.va_range.start && m.va_range.start < v2.cur_va, - ) =~= Set::::empty(), - // When v1 has no mapping at cur_va, any mapping at v2.cur_va is - // already small enough that split_while_huge is a no-op on it too. - // (At the call site this follows from: split_while_huge(v1) was a - // no-op, so find_next found the mapping without splitting, meaning - // its page_size <= size.) - !v1.present() && v2.present() ==> v2.query_mapping().page_size <= size, - // When the cursor advances strictly forward, the original cur_va - // had no mapping. Supplied by `find_next_impl`'s ensures. - v1.cur_va < v2.cur_va ==> !v1.present(), - ensures - v1.split_while_huge(size).mappings == v2.split_while_huge(size).mappings, - { - } } } // verus! diff --git a/ostd/specs/mm/page_table/cursor/tree_lemmas.rs b/ostd/specs/mm/page_table/cursor/tree_lemmas.rs index 7730f34c4..336083cba 100644 --- a/ostd/specs/mm/page_table/cursor/tree_lemmas.rs +++ b/ostd/specs/mm/page_table/cursor/tree_lemmas.rs @@ -1,10 +1,5 @@ -/// Tree-predicate lifting, tree entry level constraints, and tree membership -/// lemmas for `CursorContinuation` and `CursorOwner`. -/// -/// Themes moved here from `owners.rs`: -/// - **Theme 5**: Tree predicate lifting (`map_children_lift`, `map_children_implies`, etc.) -/// - **Theme 11**: Tree entry level constraints (`cur_entry_node_implies_level_gt_1`, etc.) -/// - **Theme 12**: Tree membership & tracking (`absent_not_in_tree`) +//! Tree-predicate lifting, tree entry level constraints, and tree membership +//! lemmas for `CursorContinuation` and `CursorOwner`. use core::ops::Range; use vstd::prelude::*; @@ -222,44 +217,6 @@ impl<'rcu, C: PageTableConfig> CursorOwner<'rcu, C> { |owner0: EntryOwner, path: TreePath| owner0.meta_slot_paddr_neq(owner), ) } - - pub proof fn absent_not_in_tree(self, owner: EntryOwner) - requires - self.inv(), - owner.inv(), - owner.is_absent(), - ensures - self.not_in_tree(owner), - { - let g = |e: EntryOwner, p: TreePath| e.meta_slot_paddr_neq(owner); - let nsp = PageTableOwner::::not_in_scope_pred(); - assert(OwnerSubtree::implies(nsp, g)) by { - assert forall|entry: EntryOwner, path: TreePath| - entry.inv() && nsp(entry, path) implies #[trigger] g(entry, path) by {}; - }; - assert forall|i: int| - #![trigger self.continuations[i]] - self.level - 1 <= i < NR_LEVELS implies self.continuations[i].map_children(g) by { - reveal(CursorContinuation::map_children); - let cont = self.continuations[i]; - reveal(CursorContinuation::inv_children); - assert forall|j: int| - 0 <= j < NR_ENTRIES - && #[trigger] cont.children[j] is Some implies cont.children[j].unwrap().subtree_satisfies( - cont.path().push_tail(j), g) by { - cont.lemma_inv_children_unroll(j); - PageTableOwner::tree_not_in_scope( - cont.children[j].unwrap(), - cont.path().push_tail(j), - ); - cont.children[j].unwrap().lemma_subtree_satisfies_implies( - cont.path().push_tail(j), - nsp, - g, - ); - }; - }; - } } } // verus! diff --git a/ostd/specs/mm/page_table/node/entry.rs b/ostd/specs/mm/page_table/node/entry.rs index b850ed83c..8807d0d3e 100644 --- a/ostd/specs/mm/page_table/node/entry.rs +++ b/ostd/specs/mm/page_table/node/entry.rs @@ -28,7 +28,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { parent_owner: NodeOwner, guard: PageTableGuard<'rcu, C>, ) -> bool { - &&& parent_owner.level == owner.parent_level + &&& parent_owner.level() == owner.parent_level &&& parent_owner.inv() &&& parent_owner.relate_guard(guard) &&& owner.match_pte(parent_owner.children_perm.value()[self.idx as int], owner.parent_level) @@ -49,9 +49,9 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { new_owner: EntryOwner, ) -> bool { if new_owner.is_node() { - parent_owner.level - 1 == new_owner.node().level + parent_owner.level() - 1 == new_owner.node().level() } else if new_owner.is_frame() { - parent_owner.level == new_owner.parent_level + parent_owner.level() == new_owner.parent_level } else { true } @@ -170,7 +170,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { 0 <= i < NR_ENTRIES ==> i != self.idx ==> parent_owner0.children_perm.value()[i] == parent_owner1.children_perm.value()[i] &&& parent_owner1.slot_index == parent_owner0.slot_index - &&& parent_owner1.level == parent_owner0.level + &&& parent_owner1.level() == parent_owner0.level() &&& parent_owner1.tree_level == parent_owner0.tree_level &&& parent_owner1.meta_own.nr_children.id() == parent_owner0.meta_own.nr_children.id() &&& parent_owner1.meta_own.stray == parent_owner0.meta_own.stray diff --git a/ostd/specs/mm/page_table/node/entry_owners.rs b/ostd/specs/mm/page_table/node/entry_owners.rs index 419414d9b..340854826 100644 --- a/ostd/specs/mm/page_table/node/entry_owners.rs +++ b/ostd/specs/mm/page_table/node/entry_owners.rs @@ -131,7 +131,7 @@ impl EntryOwner { EntryOwner { kind: EntryOwnerKind::Node(node), path, - parent_level: (node.level + 1) as PagingLevel, + parent_level: (node.level() + 1) as PagingLevel, } } @@ -326,7 +326,7 @@ impl EntryOwner { Self::new_node(node, path), { Self { - parent_level: (node.level + 1) as PagingLevel, + parent_level: (node.level() + 1) as PagingLevel, kind: EntryOwnerKind::Node(node), path, } @@ -1030,7 +1030,7 @@ impl EntryOwner { pub open spec fn inv_base(self) -> bool { &&& self.is_node() ==> { &&& self.node().inv() - &&& self.parent_level == self.node().level + 1 + &&& self.parent_level == self.node().level() + 1 } &&& self.is_frame() ==> { // Architectural constraint: frames only exist at PT levels that the diff --git a/ostd/specs/mm/page_table/node/owners.rs b/ostd/specs/mm/page_table/node/owners.rs index 3e86659cd..2ca1e26ea 100644 --- a/ostd/specs/mm/page_table/node/owners.rs +++ b/ostd/specs/mm/page_table/node/owners.rs @@ -178,30 +178,6 @@ impl Inv for PageMetaOwner { } } -pub ghost struct PageMetaModel { - pub nr_children: u16, - pub stray: bool, -} - -impl Inv for PageMetaModel { - open spec fn inv(self) -> bool { - true - } -} - -impl View for PageMetaOwner { - type V = PageMetaModel; - - open spec fn view(&self) -> ::V { - PageMetaModel { nr_children: self.nr_children.value(), stray: self.stray.value() } - } -} - -impl InvView for PageMetaOwner { - proof fn view_preserves_inv(self) { - } -} - impl OwnerOf for PageTablePageMeta { type Owner = PageMetaOwner; @@ -227,7 +203,6 @@ pub tracked struct NodeOwner { pub meta_own: PageMetaOwner, pub frame_permission: FracMetadataPerm, pub children_perm: array_ptr::PointsTo, - pub ghost level: PagingLevel, pub ghost tree_level: int, pub ghost slot_index: int, } @@ -237,13 +212,13 @@ impl Inv for NodeOwner { &&& self.meta_own.inv() &&& self.frame_permission.frac() == 1 &&& 0 <= self.meta_own.nr_children.value() <= NR_ENTRIES - &&& 1 <= self.level <= NR_LEVELS + &&& 1 <= self.level() <= NR_LEVELS &&& self.children_perm.wf() &&& self.children_perm.is_init_all() &&& self.children_perm.addr() == paddr_to_vaddr( meta_to_frame(index_to_meta(self.slot_index)), ) - &&& self.tree_level == INC_LEVELS - self.level - 1 + &&& self.tree_level == INC_LEVELS - self.level() - 1 &&& 0 <= self.slot_index < max_meta_slots() &&& FRAME_METADATA_RANGE.start <= index_to_meta(self.slot_index) < FRAME_METADATA_RANGE.end &&& index_to_meta(self.slot_index) % META_SLOT_SIZE == 0 @@ -251,7 +226,6 @@ impl Inv for NodeOwner { - LINEAR_MAPPING_BASE_VADDR &&& meta_to_frame(index_to_meta(self.slot_index)) < MAX_PADDR &&& meta_to_frame(index_to_meta(self.slot_index)) == self.children_perm.addr() - &&& self.slot_index == meta_to_index(index_to_meta(self.slot_index)) } } @@ -265,9 +239,9 @@ impl NodeOwner { permission.tracked_borrow() } - pub proof fn tracked_borrow_metadata_perm(tracked &self) -> (tracked res: &MetadataPerm) - ensures - *res == self.frame_permission.resource(), + pub proof fn tracked_borrow_metadata_perm(tracked &self) -> tracked &MetadataPerm + returns + self.frame_permission.resource(), { self.frame_permission.tracked_borrow() } @@ -289,6 +263,10 @@ impl NodeOwner { typed_meta_value::>(self.frame_permission.resource(), ()) } + pub open spec fn level(self) -> PagingLevel { + self.meta_value().level + } + /// Regions-tied invariants that used to live in `NodeOwner::inv()` via /// the now-removed `meta_perm` field. Establishes the bridge between /// the NodeOwner and the slot perm parked in regions. @@ -298,7 +276,6 @@ impl NodeOwner { &&& self.frame_permission.id() == regions.slot_owners[idx].metadata_perm.id() &&& self.meta_wf(regions) &&& self.meta_value().wf(self.meta_own) - &&& self.level == self.meta_value().level &&& self.meta_own.nr_children.id() == self.meta_value().nr_children.id() // A page-table node's slot is tracked with `PageTable` usage (set at diff --git a/ostd/specs/mm/page_table/owners.rs b/ostd/specs/mm/page_table/owners.rs index 3161c3e2b..cedd49834 100644 --- a/ostd/specs/mm/page_table/owners.rs +++ b/ostd/specs/mm/page_table/owners.rs @@ -356,7 +356,7 @@ pub open spec fn allocated_empty_node_owner( &&& owner.value().is_node() &&& owner.value().path == TreePath::::new(Seq::empty()) &&& owner.value().parent_level == (level + 1) as PagingLevel - &&& owner.value().node().level + &&& owner.value().node().level() == level // The fresh subtree's ghost-tree depth. Lets `alloc_if_none` discharge // `final(owner).inv()`'s `child.level == self.level + 1`: the grafted @@ -381,7 +381,7 @@ pub open spec fn allocated_empty_node_owner( &&& forall|i: int| #![auto] 0 <= i < NR_ENTRIES ==> owner.child(i).value().parent_level - == owner.value().node().level + == owner.value().node().level() // The freshly-allocated PT node is zero-filled, so every PTE in // `children_perm` is the absent PTE. (Stronger than the existing // "not all are present" clause; needed by `split_if_mapped_huge`'s @@ -537,14 +537,14 @@ impl PageTableOwner { // `match_pte`, so borrowing never appears below the root. &&& (parent.child(i).value().match_pte( parent.value().node().children_perm.value()[i], - parent.value().node().level, - ) || (parent.value().node().level == NR_LEVELS && C::LEADING_BITS_spec() == 0 + parent.value().node().level(), + ) || (parent.value().node().level() == NR_LEVELS && C::LEADING_BITS_spec() == 0 && parent.child(i).value().borrowed_match_pte( parent.value().node().children_perm.value()[i], - parent.value().node().level, + parent.value().node().level(), ))) &&& parent.child(i).value().path == parent.value().path.push_tail(i) - &&& parent.child(i).value().parent_level == parent.value().node().level + &&& parent.child(i).value().parent_level == parent.value().node().level() } /// Depth-indexed PT-specific per-edge invariant. `depth` is a manifest @@ -643,10 +643,10 @@ impl PageTableOwner { &&& owner.child(i).value().path.len() == owner.value().node().tree_level + 1 &&& owner.child(i).value().match_pte( owner.value().node().children_perm.value()[i], - owner.value().node().level, + owner.value().node().level(), ) &&& owner.child(i).value().path == owner.value().path.push_tail(i) - &&& owner.child(i).value().parent_level == owner.value().node().level + &&& owner.child(i).value().parent_level == owner.value().node().level() }, allocated_empty_node_grandchildren_none(owner), ensures @@ -1661,18 +1661,6 @@ impl PageTableOwner { } } - pub open spec fn relate_region_tracked_pred(regions: MetaRegionOwners) -> spec_fn( - EntryOwner, - TreePath, - ) -> bool { - |entry: EntryOwner, path: TreePath| - { - &&& entry.meta_slot_paddr() is Some - &&& regions.slot_owners.contains_key(frame_to_index(entry.meta_slot_paddr()->0)) - &&& regions.slot_owner(entry.meta_slot_paddr()->0).paths_in_pt == set![path] - } - } - pub open spec fn path_correct_pred() -> spec_fn(EntryOwner, TreePath) -> bool { |entry: EntryOwner, path: TreePath| { entry.path == path } } @@ -1775,147 +1763,6 @@ impl PageTableOwner { { } - pub proof fn prefix_push_different_indices( - prefix: TreePath, - path: TreePath, - i: int, - j: int, - ) - requires - prefix.inv(), - path.inv(), - i != j, - Self::is_prefix_of(prefix.push_tail(i), path), - ensures - !Self::is_prefix_of(prefix.push_tail(j), path), - { - assert(path[prefix.len() as int] == i); - } - - pub proof fn prefix_push_tail_implies_prefix( - prefix: TreePath, - path: TreePath, - i: int, - ) - requires - prefix.inv(), - path.inv(), - 0 <= i < N, - Self::is_prefix_of(prefix.push_tail(i), path), - ensures - Self::is_prefix_of(prefix, path), - { - } - - pub open spec fn is_at_pred(entry: EntryOwner, path: TreePath) -> spec_fn( - EntryOwner, - TreePath, - ) -> bool { - |entry0: EntryOwner, path0: TreePath| { path0 == path ==> entry0 == entry } - } - - pub open spec fn path_in_tree_pred(path: TreePath) -> spec_fn( - EntryOwner, - TreePath, - ) -> bool { - |entry: EntryOwner, path0: TreePath| - Self::is_prefix_of(path0, path) ==> !entry.is_node() ==> path == path0 - } - - pub proof fn is_at_pred_eq( - path: TreePath, - entry1: EntryOwner, - entry2: EntryOwner, - ) - requires - entry1.inv(), - OwnerSubtree::implies(Self::is_at_pred(entry1, path), Self::is_at_pred(entry2, path)), - ensures - entry1 == entry2, - { - assert(Self::is_at_pred(entry1, path)(entry1, path) ==> Self::is_at_pred(entry2, path)( - entry1, - path, - )); - } - - pub proof fn is_at_holds_when_on_wrong_path( - subtree: OwnerSubtree, - root_path: TreePath, - dest_path: TreePath, - entry: EntryOwner, - ) - requires - subtree.inv(), - PageTableOwner(subtree).pt_inv(), - dest_path.inv(), - !Self::is_prefix_of(root_path, dest_path), - root_path.len() <= INC_LEVELS - 1, - root_path.len() == subtree.level(), - ensures - subtree.subtree_satisfies(root_path, Self::is_at_pred(entry, dest_path)), - decreases INC_LEVELS - root_path.len(), - { - reveal(PageTableOwner::pt_inv_at_depth); - if subtree.level() < INC_LEVELS - 1 { - if subtree.value().is_node() { - assert forall|i: int| 0 <= i < NR_ENTRIES implies ( - #[trigger] subtree.children()[i as int]).unwrap().subtree_satisfies( - root_path.push_tail(i), - Self::is_at_pred(entry, dest_path), - ) by { - PageTableOwner(subtree).pt_inv_unroll(i); - Self::is_at_holds_when_on_wrong_path( - subtree.children()[i as int].unwrap(), - root_path.push_tail(i), - dest_path, - entry, - ); - }; - } else { - } - } - } - - /// Counterintuitive: the predicate is vacuously true when the path is not a prefix of the target path, - /// because it is actually a liveness property: if we keep following the path, we will eventually reach it. - /// This covers when we are not following it. - pub proof fn path_in_tree_holds_when_on_wrong_path( - subtree: OwnerSubtree, - root_path: TreePath, - dest_path: TreePath, - ) - requires - subtree.inv(), - PageTableOwner(subtree).pt_inv(), - dest_path.inv(), - !Self::is_prefix_of(root_path, dest_path), - root_path.len() <= INC_LEVELS - 1, - root_path.len() == subtree.level(), - ensures - subtree.subtree_satisfies(root_path, Self::path_in_tree_pred(dest_path)), - decreases INC_LEVELS - root_path.len(), - { - reveal(PageTableOwner::pt_inv_at_depth); - if subtree.level() < INC_LEVELS - 1 { - if subtree.value().is_node() { - assert forall|i: int| 0 <= i < NR_ENTRIES implies ( - #[trigger] subtree.children()[i as int]).unwrap().subtree_satisfies( - root_path.push_tail(i), - Self::path_in_tree_pred(dest_path), - ) by { - PageTableOwner(subtree).pt_inv_unroll(i); - Self::path_in_tree_holds_when_on_wrong_path( - subtree.children()[i as int].unwrap(), - root_path.push_tail(i), - dest_path, - ); - }; - } else { - } - } - } - /// Entries in a subtree whose structural path is disjoint from `old_entry.path` /// have different physical addresses from `old_entry`. pub proof fn neq_old_from_path_disjoint( @@ -1983,155 +1830,6 @@ impl PageTableOwner { } } - pub proof fn is_at_eq_rec( - subtree: OwnerSubtree, - root_path: TreePath, - dest_path: TreePath, - entry1: EntryOwner, - entry2: EntryOwner, - ) - requires - subtree.inv(), - PageTableOwner(subtree).pt_inv(), - dest_path.inv(), - root_path.inv(), - Self::is_prefix_of(root_path, dest_path), - root_path.len() <= INC_LEVELS - 1, - root_path.len() == subtree.level(), - subtree.subtree_satisfies(root_path, Self::path_in_tree_pred(dest_path)), - subtree.subtree_satisfies(root_path, Self::is_at_pred(entry1, dest_path)), - subtree.subtree_satisfies(root_path, Self::is_at_pred(entry2, dest_path)), - ensures - entry1 == entry2, - decreases INC_LEVELS - root_path.len(), - { - if root_path == dest_path { - } else if subtree.level() == INC_LEVELS - 1 || !subtree.value().is_node() { - proof_from_false() - } else { - if root_path.len() == dest_path.len() { - assert forall|i: int| 0 <= i < root_path.0.len() implies #[trigger] root_path.0[i] - == dest_path.0[i] by { - assert(root_path[i] == dest_path[i]); - }; - assert(root_path == dest_path); - assert(false); - } - let i = dest_path[root_path.len() as int]; - PageTableOwner(subtree).pt_inv_unroll(i as int); - Self::is_at_eq_rec( - subtree.children()[i as int].unwrap(), - root_path.push_tail(i), - dest_path, - entry1, - entry2, - ); - } - } - - pub proof fn view_rec_inversion( - self, - path: TreePath, - regions: MetaRegionOwners, - m: Mapping, - ) -> (entry: EntryOwner) - requires - self.pt_inv(), - path.len() == self.0.level(), - self.view_rec(path).contains(m), - self.0.subtree_satisfies(path, Self::path_correct_pred()), - self.0.subtree_satisfies(path, Self::relate_region_tracked_pred(regions)), - ensures - Self::is_prefix_of(path, entry.path), - regions.slot_owner(m.pa_range.start).paths_in_pt == set![entry.path], - m.va_range.start == vaddr_of::(entry.path), - m.page_size == page_size((INC_LEVELS - entry.path.len()) as PagingLevel), - entry.is_frame(), - m.property == entry.frame().prop, - self.0.subtree_satisfies(path, Self::is_at_pred(entry, entry.path)), - self.0.subtree_satisfies(path, Self::path_in_tree_pred(entry.path)), - entry.inv(), - decreases INC_LEVELS - path.len(), - { - broadcast use PageTableOwner::group_lemmas; - - reveal(PageTableOwner::pt_inv_at_depth); - - if self.0.value().is_frame() { - self.0.value() - } else if self.0.value().is_node() { - let i = self.view_rec_contains_choose(path, m); - self.pt_inv_unroll(i); - let entry = PageTableOwner(self.0.children()[i].unwrap()).view_rec_inversion( - path.push_tail(i), - regions, - m, - ); - assert forall|j: int| - 0 <= j < NR_ENTRIES - && #[trigger] self.0.children()[j] is Some implies self.0.children()[j].unwrap().subtree_satisfies( - path.push_tail(j), Self::is_at_pred(entry, entry.path)) by { - if j != i { - self.pt_inv_unroll(j); - Self::is_at_holds_when_on_wrong_path( - self.0.children()[j].unwrap(), - path.push_tail(j), - entry.path, - entry, - ); - } - }; - - assert forall|j: int| - 0 <= j < NR_ENTRIES && #[trigger] self.0.has_child(j) implies self.0.child( - j, - ).subtree_satisfies(path.push_tail(j), Self::path_in_tree_pred(entry.path)) by { - if j != i { - Self::path_in_tree_holds_when_on_wrong_path( - self.0.child(j), - path.push_tail(j), - entry.path, - ); - } - }; - entry - } else { - proof_from_false() - } - } - - pub proof fn view_rec_inversion_unique( - self, - path: TreePath, - regions: MetaRegionOwners, - m1: Mapping, - m2: Mapping, - ) - requires - self.pt_inv(), - path.len() <= INC_LEVELS - 1, - path.len() == self.0.level(), - self.view_rec(path).contains(m1), - self.view_rec(path).contains(m2), - m1.pa_range.start == m2.pa_range.start, - m1.inv(), - m2.inv(), - self.0.subtree_satisfies(path, Self::path_tracked_pred(regions)), - self.0.subtree_satisfies(path, Self::path_correct_pred()), - self.0.subtree_satisfies(path, Self::relate_region_tracked_pred(regions)), - ensures - m1 == m2, - { - let entry1 = self.view_rec_inversion(path, regions, m1); - let entry2 = self.view_rec_inversion(path, regions, m2); - - // Same paddr ⇒ same slot ⇒ same singleton paths_in_pt ⇒ same entry path. - let idx = frame_to_index(m1.pa_range.start); - assert(set![entry1.path].contains(entry2.path)); - - Self::is_at_eq_rec(self.0, path, entry1.path, entry1, entry2); - } - pub broadcast group group_lemmas { PageTableOwner::lemma_view_rec_contains, PageTableOwner::lemma_view_rec_contains_intro, diff --git a/ostd/src/mm/page_table/cursor/mod.rs b/ostd/src/mm/page_table/cursor/mod.rs index d53c0d289..5fd2e9164 100644 --- a/ostd/src/mm/page_table/cursor/mod.rs +++ b/ostd/src/mm/page_table/cursor/mod.rs @@ -592,6 +592,8 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { proof { let idx = frame_to_index(pa); + old(regions).lemma_contains_valid_frame_paddr(pa); + assert(old(regions).contains(idx)); if C::item_into_raw(item).3@ is Some && regions.ref_count(idx) >= REF_COUNT_MAX { EntryOwner::::axiom_frame_is_tracked_iff_not_mmio( @@ -612,6 +614,17 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { #[verus_spec(with Tracked(regions), Ghost(pa))] let cloned = Self::clone_item(&item); + proof { + let idx = frame_to_index(pa); + if C::item_into_raw(item).3@ is Some { + broadcast use crate::specs::mm::frame::meta_owners::axiom_mmio_usage_iff_mmio_paddr; + + EntryOwner::::axiom_frame_is_tracked_iff_not_mmio( + owner_before_permission_take.cur_entry_owner(), + ); + } + } + let (_pa, _level, _prop, Tracked(restored_permission)) = C::item_into_raw(item); proof { let tracked child_value = child_owner.tracked_borrow_mut_value(); @@ -672,6 +685,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { ); } }; + } return Ok( @@ -1295,7 +1309,7 @@ impl<'rcu, C: PageTableConfig, A: InAtomicMode> Cursor<'rcu, C, A> { let tracked mut child_owner = continuation.tracked_take_child(); proof { - assert(continuation.entry_own.node().level > 1) by { + assert(continuation.entry_own.node().level() > 1) by { owner0.cur_va_range().start.reflect_prop(cur_va_range.start); owner0.cur_va_range().end.reflect_prop(cur_va_range.end); assert(cur_entry_fits_range == (cur_va diff --git a/ostd/src/mm/page_table/mod.rs b/ostd/src/mm/page_table/mod.rs index ac46ae25c..fc75fdb77 100644 --- a/ostd/src/mm/page_table/mod.rs +++ b/ostd/src/mm/page_table/mod.rs @@ -958,7 +958,7 @@ impl PageTable { < KernelPtConfig::TOP_LEVEL_INDEX_RANGE().end && { let pte = root_owner.children_perm.value()[i as int]; ||| !pte.is_present() - ||| pte.is_last(root_owner.level) + ||| pte.is_last(root_owner.level()) } } @@ -1168,10 +1168,10 @@ impl PageTable { KernelPtConfig::TOP_LEVEL_INDEX_RANGE().start <= j < KernelPtConfig::TOP_LEVEL_INDEX_RANGE().end implies { let pte = kern_node.children_perm.value()[j as int]; - pte.is_present() && !pte.is_last(kern_node.level) + pte.is_present() && !pte.is_last(kern_node.level()) } by { let pte = kern_node.children_perm.value()[j as int]; - if !pte.is_present() || pte.is_last(kern_node.level) { + if !pte.is_present() || pte.is_last(kern_node.level()) { assert(Self::create_user_pt_panic_condition(kern_node)); } } @@ -1185,7 +1185,7 @@ impl PageTable { kern_node.children_perm.value()[i as int], entry_owner.parent_level, )); - assert(entry_owner.parent_level == kern_node.level); + assert(entry_owner.parent_level == kern_node.level()); assert(child_subtree.inv()); assert(entry_owner.inv()); assert(root_owner.relate_guard(root_node)); @@ -1212,8 +1212,8 @@ impl PageTable { let kern_node = kernel_owner.0.value().node(); let pte = kern_node.children_perm.value()[i as int]; - assert(pte.is_present() && !pte.is_last(kern_node.level)) by { - if !pte.is_present() || pte.is_last(kern_node.level) { + assert(pte.is_present() && !pte.is_last(kern_node.level())) by { + if !pte.is_present() || pte.is_last(kern_node.level()) { assert(KernelPtConfig::TOP_LEVEL_INDEX_RANGE().start <= i < KernelPtConfig::TOP_LEVEL_INDEX_RANGE().end); assert(exists|j: usize| @@ -1221,7 +1221,7 @@ impl PageTable { < KernelPtConfig::TOP_LEVEL_INDEX_RANGE().end && { let p = #[trigger] kern_node.children_perm.value()[j as int]; ||| !p.is_present() - ||| p.is_last(kern_node.level) + ||| p.is_last(kern_node.level()) }); assert(Self::create_user_pt_panic_condition(kern_node)); } diff --git a/ostd/src/mm/page_table/node/entry.rs b/ostd/src/mm/page_table/node/entry.rs index 411b96d63..6cdaa93f2 100644 --- a/ostd/src/mm/page_table/node/entry.rs +++ b/ostd/src/mm/page_table/node/entry.rs @@ -122,10 +122,9 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { requires owner.inv(), self.wf(owner), - parent_owner.level == parent_owner.meta_value().level, parent_owner.relate_guard(*self.node), parent_owner.inv(), - parent_owner.level == owner.parent_level, + parent_owner.level() == owner.parent_level, returns owner.is_node(), )] @@ -538,7 +537,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { // (`INC_LEVELS - parent_PT_level`), tying it to the freshly- // allocated node's depth (`new_node_owner.level`) so we can prove // `final(owner).inv()`'s `child.level == self.level + 1`. - old(owner).level() + old(parent_owner).level == INC_LEVELS, + old(owner).level() + old(parent_owner).level() == INC_LEVELS, old(parent_owner).metaregion_sound_node(*old(regions)), ensures final(self).invariants(final(owner).value(), *final(regions)), @@ -546,11 +545,11 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { final(self).idx == old(self).idx, final(parent_owner).count_consistent(), *final(self).node == *old(self).node, - old(owner).value().is_absent() && old(parent_owner).level > 1 ==> { + old(owner).value().is_absent() && old(parent_owner).level() > 1 ==> { &&& final(self).node_matching(final(owner).value(), *final(parent_owner), *final(self).node) &&& final(owner).inv() }, - old(owner).value().is_absent() && old(parent_owner).level > 1 ==> { + old(owner).value().is_absent() && old(parent_owner).level() > 1 ==> { &&& res is Some &&& final(owner).value().is_node() &&& final(owner).level() == old(owner).level() @@ -589,7 +588,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { // Other child fields preserved from `allocated_empty_node_owner`. &&& forall|i: int| 0 <= i < NR_ENTRIES ==> (#[trigger] final(owner).child(i)).value().parent_level - == final(owner).value().node().level + == final(owner).value().node().level() &&& forall|i: int| 0 <= i < NR_ENTRIES ==> (#[trigger] final(owner).child(i)).value().match_pte( final(owner).value().node().children_perm.value()[i], @@ -807,18 +806,18 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { old(self).wf(old(owner).value()), old(parent_owner).relate_guard(*old(self).node), old(parent_owner).inv(), - old(parent_owner).level == old(owner).value().parent_level, - old(parent_owner).level < NR_LEVELS, + old(parent_owner).level() == old(owner).value().parent_level, + old(parent_owner).level() < NR_LEVELS, old(parent_owner).metaregion_sound_node(*old(regions)), // Frame entries being split must have `metaregion_sound` for // their slot — provides `regions.slots.contains_key(pa_idx)` and // ref_count facts at the parent slot itself (j = 0 case in the // split loop's invariant). Without this, those facts can't be // re-established after alloc. - old(owner).value().is_frame() && old(parent_owner).level > 1 ==> + old(owner).value().is_frame() && old(parent_owner).level() > 1 ==> old(owner).value().metaregion_sound(*old(regions)), ensures - old(owner).value().is_frame() && old(parent_owner).level > 1 ==> { + old(owner).value().is_frame() && old(parent_owner).level() > 1 ==> { &&& res is Some &&& final(owner).value().is_node() &&& final(owner).level() == old(owner).level() @@ -845,23 +844,23 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { &&& final(owner).subtree_satisfies(final(owner).value().path, PageTableOwner::::metaregion_sound_pred(*final(regions))) }, - !old(owner).value().is_frame() || old(parent_owner).level <= 1 ==> { + !old(owner).value().is_frame() || old(parent_owner).level() <= 1 ==> { &&& res is None &&& *final(owner) == *old(owner) }, final(owner).inv(), final(owner).value().parent_level == old(owner).value().parent_level, final(self).idx == old(self).idx, - old(owner).value().is_frame() && old(parent_owner).level > 1 ==> + old(owner).value().is_frame() && old(parent_owner).level() > 1 ==> final(self).node_matching(final(owner).value(), *final(parent_owner), *final(self).node), final(regions).inv(), final(parent_owner).inv(), - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(self).node.inner.inner@.ptr.addr() == old(self).node.inner.inner@.ptr.addr(), forall |i: usize| old(guards).lock_held(i) ==> final(guards).lock_held(i), forall |i: usize| old(guards).unlocked(i) ==> final(guards).unlocked(i), // slot_owners unchanged for all indices except the new PT node's index. - old(owner).value().is_frame() && old(parent_owner).level > 1 ==> { + old(owner).value().is_frame() && old(parent_owner).level() > 1 ==> { &&& forall|i: int| i != meta_to_index(final(owner).value().node().meta_vaddr()) ==> (#[trigger] final(regions).slot_owners[i]) == old(regions).slot_owners[i] // slots keys preserved (alloc removes then borrow re-inserts). @@ -878,7 +877,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { // is overwritten (with the new PT pointer). Lets callers re-derive // `inv_children_rel` for the unchanged children when restoring the // parent NodeOwner into the cursor's continuation. - old(owner).value().is_frame() && old(parent_owner).level > 1 ==> + old(owner).value().is_frame() && old(parent_owner).level() > 1 ==> forall|j: int| 0 <= j < NR_ENTRIES && j != old(self).idx ==> #[trigger] final(parent_owner).children_perm.value()[j] == old(parent_owner).children_perm.value()[j], @@ -1017,7 +1016,7 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { !owner.value().frame_is_tracked(), owner.value().frame_permission() is None, pa == old(owner).value().frame().mapped_pa, - level == old(parent_owner).level, + level == old(parent_owner).level(), pa % page_size(level) == 0, pa + page_size(level) <= MAX_PADDR, regions.inv(), @@ -1032,17 +1031,17 @@ impl<'a, 'rcu, C: PageTableConfig> Entry<'a, 'rcu, C> { new_owner.value().node().meta_vaddr() == new_owner_meta_addr, new_owner.value().node().relate_guard(pt_lock_guard), guards.lock_held(new_owner_meta_addr), - new_owner.value().node().level == (level - 1) as PagingLevel, + new_owner.value().node().level() == (level - 1) as PagingLevel, forall|j: int| 0 <= j < NR_ENTRIES ==> (#[trigger] new_owner.children()[j]) is Some, forall|j: int| 0 <= j < NR_ENTRIES ==> { &&& (#[trigger] new_owner.children()[j]) is Some &&& new_owner.children()[j].unwrap().value().match_pte( new_owner.value().node().children_perm.value()[j], - new_owner.value().node().level, + new_owner.value().node().level(), ) &&& new_owner.children()[j].unwrap().value().parent_level - == new_owner.value().node().level + == new_owner.value().node().level() &&& new_owner.children()[j].unwrap().value().inv() &&& new_owner.children()[j].unwrap().value().path == new_owner_path.push_tail(j) @@ -1388,7 +1387,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { ), old(parent_owner).inv(), old(parent_owner).relate_guard(*old(self)), - old(parent_owner).level == old(owner).parent_level, + old(parent_owner).level() == old(owner).parent_level, old(parent_owner).metaregion_sound_node(*regions), idx < NR_ENTRIES, op.requires((old(owner).frame().prop,)), @@ -1408,15 +1407,15 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { ensures final(owner).inv(), final(owner).is_frame(), - final(owner).match_pte(res, final(parent_owner).level), + final(owner).match_pte(res, final(parent_owner).level()), final(owner).match_pte( final(parent_owner).children_perm.value()[idx as int], - final(parent_owner).level, + final(parent_owner).level(), ), res == final(parent_owner).children_perm.value()[idx as int], final(parent_owner).inv(), final(parent_owner).slot_index == old(parent_owner).slot_index, - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(parent_owner).tree_level == old(parent_owner).tree_level, final(parent_owner).meta_own.nr_children.id() == old(parent_owner).meta_own.nr_children.id(), final(parent_owner).meta_own.stray == old(parent_owner).meta_own.stray, @@ -1503,7 +1502,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { ), old(parent_owner).inv(), old(parent_owner).relate_guard(*old(self)), - old(parent_owner).level == old(owner).parent_level, + old(parent_owner).level() == old(owner).parent_level, idx < NR_ENTRIES, old(regions).inv(), old(regions).slots.contains_key(old(parent_owner).slot_index), @@ -1521,7 +1520,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { final(new_owner).metaregion_sound(*final(regions)), final(new_owner).match_pte( final(parent_owner).children_perm.value()[idx as int], - final(parent_owner).level, + final(parent_owner).level(), ), final(new_owner).path == old(new_owner).path, final(new_owner).parent_level == old(new_owner).parent_level, @@ -1544,7 +1543,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { C, >::path_tracked_pred(*final(regions))(*final(new_owner), final(new_owner).path), final(parent_owner).inv(), - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(parent_owner).relate_guard(*final(self)), final(parent_owner).metaregion_sound_node(*final(regions)), forall|j: int| 0 <= j < NR_ENTRIES && j != idx ==> @@ -1750,8 +1749,8 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { old(owner).value().metaregion_sound(*old(regions)), old(parent_owner).inv(), old(parent_owner).relate_guard(*old(self)), - old(parent_owner).level == old(owner).value().parent_level, - old(parent_owner).level > 1, + old(parent_owner).level() == old(owner).value().parent_level, + old(parent_owner).level() > 1, old(parent_owner).metaregion_sound_node(*old(regions)), idx < NR_ENTRIES, old(owner).value().match_pte( @@ -1771,7 +1770,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { final(owner).value().node().meta_vaddr() == res.inner.inner@.ptr.addr(), final(owner).value().match_pte( final(parent_owner).children_perm.value()[idx as int], - final(parent_owner).level, + final(parent_owner).level(), ), final(guards).lock_held(final(owner).value().node().meta_vaddr()), OwnerSubtree::implies( @@ -1798,7 +1797,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { crate::specs::mm::page_table::allocated_empty_node_grandchildren_none(*final(owner)), forall|i: int| 0 <= i < NR_ENTRIES ==> (#[trigger] final(owner).children()[i])->0.value().parent_level - == final(owner).value().node().level, + == final(owner).value().node().level(), forall|i: int| 0 <= i < NR_ENTRIES ==> (#[trigger] final(owner).children()[i])->0.value().match_pte( final(owner).value().node().children_perm.value()[i], @@ -1819,7 +1818,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { final(owner).value().meta_slot_paddr().unwrap()), final(regions).inv(), final(parent_owner).inv(), - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(parent_owner).relate_guard(*final(self)), final(parent_owner).metaregion_sound_node(*final(regions)), forall|j: int| 0 <= j < NR_ENTRIES && j != idx ==> @@ -1998,7 +1997,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { old(owner).metaregion_sound(*old(regions)), old(parent_owner).inv(), old(parent_owner).relate_guard(*old(self)), - old(parent_owner).level == old(owner).parent_level, + old(parent_owner).level() == old(owner).parent_level, idx < NR_ENTRIES, old(owner).match_pte( old(parent_owner).children_perm.value()[idx as int], @@ -2023,7 +2022,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { 0 <= i < NR_ENTRIES && i != idx ==> #[trigger] old(parent_owner).children_perm.value()[i] == final(parent_owner).children_perm.value()[i], final(parent_owner).slot_index == old(parent_owner).slot_index, - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(parent_owner).tree_level == old(parent_owner).tree_level, final(parent_owner).meta_own.nr_children.id() == old(parent_owner).meta_own.nr_children.id(), final(parent_owner).meta_own.stray == old(parent_owner).meta_own.stray, diff --git a/ostd/src/mm/page_table/node/mod.rs b/ostd/src/mm/page_table/node/mod.rs index 402c281cd..9f4905f59 100644 --- a/ostd/src/mm/page_table/node/mod.rs +++ b/ostd/src/mm/page_table/node/mod.rs @@ -227,9 +227,8 @@ impl PageTableNode { pub(super) fn level(&self) -> PagingLevel requires self.external_meta_wf(owner.frame_permission.resource(), ()), - owner.level == owner.meta_value().level, returns - owner.level, + owner.level(), { #[verus_spec(with Tracked(Some(owner.tracked_borrow_metadata_perm())), @@ -275,7 +274,7 @@ impl PageTableNode { final(parent_owner).meta_own == old(parent_owner).meta_own, final(parent_owner).frame_permission == old(parent_owner).frame_permission, final(parent_owner).slot_index == old(parent_owner).slot_index, - final(parent_owner).level == old(parent_owner).level, + final(parent_owner).level() == old(parent_owner).level(), final(parent_owner).tree_level == old(parent_owner).tree_level, final(parent_owner).children_perm.addr() == old(parent_owner).children_perm.addr(), final(parent_owner).children_perm.value() == old(parent_owner).children_perm.value().update( @@ -585,7 +584,7 @@ impl<'rcu, C: PageTableConfig> PageTableGuard<'rcu, C> { idx < NR_ENTRIES, ensures final(owner).inv(), - final(owner).level == old(owner).level, + final(owner).level() == old(owner).level(), final(owner).meta_own == old(owner).meta_own, final(owner).frame_permission == old(owner).frame_permission, final(owner).slot_index == old(owner).slot_index, From 987e810cc30fc89eb4f1931694bf0baf4162b15a Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Tue, 15 Sep 2026 15:35:35 +0800 Subject: [PATCH 17/30] sync: ostd/src/io/io_port to 0.18.1 --- README.md | 2 +- ostd/src/arch/x86/device/io_port.rs | 2 +- ostd/src/arch/x86/pci.rs | 2 + ostd/src/io/io_port/allocator.rs | 68 ++++--- ostd/src/io/io_port/mod.rs | 168 ++++++++++++++---- ostd/src/io/mod.rs | 9 +- .../vstd_extra/src/external/io_port.rs | 54 ++++-- 7 files changed, 229 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 52e1f12da..f45609b82 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![verify (verus-lang/verus)](https://img.shields.io/github/actions/workflow/status/asterinas/vostd/ci-upstream-verus.yml?branch=main&label=verify%20(verus-lang%2Fverus))](https://github.com/asterinas/vostd/actions/workflows/ci-upstream-verus.yml) > [!NOTE] -> This repository is currently in a transitional state: the components under `ostd/src/sync` and `ostd/libs/id-alloc` already track the upcoming Asterinas release [v0.18.1](https://github.com/asterinas/asterinas/releases/tag/v0.18.1), while every other OSTD component is still based on [v0.16.0](https://github.com/asterinas/asterinas/releases/tag/v0.16.0). +> This repository is currently in a transitional state: the components under `ostd/src/sync`, `ostd/libs/id-alloc` and `ostd/src/io/io_port` already track the upcoming Asterinas release [v0.18.1](https://github.com/asterinas/asterinas/releases/tag/v0.18.1), while every other OSTD component is still based on [v0.16.0](https://github.com/asterinas/asterinas/releases/tag/v0.16.0). ## Overview diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index 11b10c0a8..4ab813494 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port access. -pub use vstd_extra::external::{valid_io_port_access, valid_io_port_number}; +pub use vstd_extra::external::{group_io_port_models, obeys_pio_model, valid_io_port_access}; pub use x86_64::{ instructions::port::{ diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index c1c142c04..d8292ac4e 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -7,7 +7,9 @@ use crate::{bus::pci::PciDeviceLocation, io::IoPort, prelude::*}; verus! { +broadcast use super::device::io_port::group_io_port_models; // Original Rust: static PCI_ADDRESS_PORT: IoPort = unsafe { IoPort::new(0x0CF8) }; + exec static PCI_ADDRESS_PORT: IoPort ensures PCI_ADDRESS_PORT.well_formed(), diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 0c9766054..bf1aa787b 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -14,6 +14,7 @@ use log::debug; use spin::Once; use super::{IoPort, lemma_port_id_set_contains, lemma_port_id_set_insert, port_id_set}; +use crate::arch::device::io_port::obeys_pio_model; use crate::{ io::RawIoPortRange, sync::{LocalIrqDisabled, SpinLock}, @@ -124,6 +125,7 @@ pub(crate) proof fn lemma_alloc_specific_view( { let old_len: int = old_a@.len() as int; assert forall|j: usize| + #![trigger id_alloc_view(final_a).contains(j)] id_alloc_view(final_a).contains(j) == (id_alloc_view(old_a).insert(id)).contains(j) by { lemma_id_alloc_bits_char(old_a@, old_len, j); lemma_id_alloc_bits_char(final_a@, final_a@.len() as int, j); @@ -336,7 +338,7 @@ struct IoPortAllocatorInner { /// I/O port allocator that allocates port I/O access to device drivers. #[verus_verify] -pub struct IoPortAllocator { +pub(super) struct IoPortAllocator { /// Each ID indicates whether a Port I/O (1B) is allocated. /// /// Instead of using `RangeAllocator` like `IoMemAllocator` does, it is more reasonable to use `IdAlloc`, @@ -346,31 +348,42 @@ pub struct IoPortAllocator { #[verus_verify] impl IoPortAllocator { - /// Acquires the `IoPort`. Return None if any region in `port` cannot be allocated. + /// Acquires an `IoPort`. Returns `None` if the PIO range is unavailable. + /// + /// `is_overlapping` indicates whether another `IoPort` can have a PIO range that overlaps with + /// this one. If it is true, only the first port in the PIO range will be marked as occupied; + /// otherwise, all ports in the PIO range will be marked as occupied. #[verus_spec(result => with - -> claim: Tracked>, + Tracked(claim_out): Tracked<&mut Tracked>>, requires - vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, - port as usize + size_of::() <= u16::MAX, + is_overlapping ==> port as usize + size_of::() <= u16::MAX, + obeys_pio_model::(), io_port_allocator_initialized(), + (*old(claim_out))@ is None, ensures - result is Some <==> claim@ is Some, + result is Some <==> (*final(claim_out))@ is Some, result matches Some(io_port) ==> { &&& io_port@ == port + &&& io_port.is_overlapping() == is_overlapping &&& io_port.well_formed() - &&& io_port.claim_matches_set(claim@->Some_0.set()) - &&& claim@->Some_0.instance_id() == io_port_allocator_instance_id() + &&& io_port.claim_matches_set((*final(claim_out))@->Some_0.set()) + &&& (*final(claim_out))@->Some_0.instance_id() == io_port_allocator_instance_id() }, )] - pub fn acquire(&self, port: u16) -> Option> { + pub(super) fn acquire(&self, port: u16, is_overlapping: bool) -> Option> { + let range = if !is_overlapping { + port..port.checked_add(size_of::().try_into().ok()?)? + } else { + port..port.checked_add(1)? + }; + /* debug!("Try to acquire PIO range: {:#x?}", range); */ let mut allocator = self.allocator.lock(); let allocator_inner = &mut *allocator; proof! { lemma_io_port_alloc_init(&*allocator_inner); } - let mut range = port..(port + size_of::() as u16); // `Iterator::any` with a capturing closure is not supported by Verus. // Original Rust: // if range.any(|i| allocator.is_allocated(i as usize)) { return None; } @@ -407,10 +420,10 @@ impl IoPortAllocator { } if already_allocated { allocator.drop(); - return { - proof_with!(|= Tracked(None)); - None - }; + proof! { + *claim_out = Tracked(None); + } + return None; } proof_decl! { @@ -516,11 +529,13 @@ impl IoPortAllocator { range_claim = allocator_inner.tracked_allocated.borrow_mut().allocate(ids); } - // SAFETY: The created IoPort is guaranteed not to access system device I/O - /* Original Rust: unsafe { Some(IoPort::new(port)) } */ - let result = unsafe { Some(IoPort::new(port)) }; + // SAFETY: The created `IoPort` is guaranteed not to access system device I/O. + /* Original Rust: unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) } */ + let result = unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) }; allocator.drop(); - proof_with!(|= Tracked(Some(range_claim))); + proof! { + *claim_out = Tracked(Some(range_claim)); + } result } @@ -538,8 +553,8 @@ impl IoPortAllocator { range.start <= range.end, io_port_allocator_initialized(), )] - pub(in crate::io) unsafe fn recycle(&self, range: Range) { - /* debug!("Recycling MMIO range: {:#x?}", range); */ + pub(super) unsafe fn recycle(&self, range: Range) { + /* debug!("Recycling PIO range: {:#x?}", range); */ /* Original Rust: self.allocator .lock() @@ -642,22 +657,23 @@ pub(super) static IO_PORT_ALLOCATOR: Once = Once::new(); /// 2. `MAX_IO_PORT` defined in `crate::arch::io` is guaranteed not to exceed the maximum /// value specified by architecture. #[verifier::external_body] -pub(crate) unsafe fn init() { +pub(in crate::io) unsafe fn init() { // SAFETY: `MAX_IO_PORT` is guaranteed not to exceed the maximum value specified by architecture. let mut allocator = IdAlloc::with_capacity(crate::arch::io::MAX_IO_PORT as usize); - extern "C" { + unsafe extern "C" { fn __sensitive_io_ports_start(); fn __sensitive_io_ports_end(); } - let start = __sensitive_io_ports_start as usize; - let end = __sensitive_io_ports_end as usize; - assert!((end - start) % size_of::() == 0); + let start = __sensitive_io_ports_start as *const () as usize; + let end = __sensitive_io_ports_end as *const () as usize; + assert!((end - start).is_multiple_of(size_of::())); // Iterate through the sensitive I/O port ranges and remove them from the allocator. let io_port_range_count = (end - start) / size_of::(); for i in 0..io_port_range_count { - let range_base_addr = __sensitive_io_ports_start as usize + i * size_of::(); + let range_base_addr = + __sensitive_io_ports_start as *const () as usize + i * size_of::(); // SAFETY: The range is guaranteed to be valid as it is defined in the `.sensitive_io_ports` section. let port_range = unsafe { *(range_base_addr as *const RawIoPortRange) }; diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index e844945f2..fddbed460 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -3,7 +3,7 @@ use vstd::prelude::*; use crate::arch::device::io_port::{ - IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite, valid_io_port_access, + IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite, obeys_pio_model, valid_io_port_access, }; mod allocator; @@ -12,6 +12,11 @@ use core::{marker::PhantomData, mem::size_of}; pub(super) use self::allocator::init; use crate::{Error, prelude::*}; +verus! { + +broadcast use crate::arch::device::io_port::group_io_port_models; + +} // verus! /// An I/O port, representing a specific address in the I/O address of x86. /// /// The following code shows and example to read and write u32 value to an I/O port: @@ -24,9 +29,11 @@ use crate::{Error, prelude::*}; /// } /// ``` /// +#[derive(Debug)] #[verus_verify] pub struct IoPort { port: u16, + is_overlapping: bool, value_marker: PhantomData, access_marker: PhantomData, } @@ -48,9 +55,26 @@ impl IoPort { valid_io_port_access::(self@ as int) } - /// Whether `claim` is the allocator-issued ownership token for this complete typed range. + /// Whether the port was acquired as overlapping: it occupies only its first port. + /// + /// Marks the occupied range of [`Self::claim_matches_set`] and the released range of + /// [`Self::drop`]. + pub closed spec fn is_overlapping(&self) -> bool { + self.is_overlapping + } + + /// Whether `claim` is the allocator-issued ownership token for this port's occupied range: + /// the complete typed range, or only the first port if the port was acquired as + /// overlapping. pub open spec fn claim_matches_set(&self, claim: Set) -> bool { - claim == port_id_set(self@ as usize, (self@ as usize + size_of::()) as usize) + claim == port_id_set( + self@ as usize, + if self.is_overlapping() { + (self@ as usize + 1) as usize + } else { + (self@ as usize + size_of::()) as usize + }, + ) } } @@ -105,18 +129,21 @@ fn initialized_allocator() -> &'static allocator::IoPortAllocator { #[verus_verify] impl IoPort { /// Acquires an `IoPort` instance for the given range. + /// + /// This method will mark all ports in the PIO range as occupied. #[verus_spec(result => with -> claim: Tracked>, requires - vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, + obeys_pio_model::(), allocator::io_port_allocator_initialized(), ensures - result is Ok <==> claim@ is Some, + result is Ok <== claim@ is Some, result matches Ok(io_port) ==> { &&& io_port@ == port + &&& !io_port.is_overlapping() &&& io_port.well_formed() &&& io_port.claim_matches_set(claim@->Some_0.set()) &&& claim@->Some_0.instance_id() == allocator::io_port_allocator_instance_id() @@ -124,13 +151,56 @@ impl IoPort { )] pub fn acquire(port: u16) -> Result> { proof_decl! { - let tracked claim: Option; + let tracked mut claim: Tracked> = Tracked(None); + } + let port = { + /* Original Rust: allocator::IO_PORT_ALLOCATOR.get().unwrap() */ + #[verus_spec(with Tracked(&mut claim))] + initialized_allocator().acquire(port, false) + }; + let result = port.ok_or(Error::AccessDenied); + proof_decl! { + let tracked claim_val: Option = claim.get(); + } + proof_with!(|= Tracked(claim_val)); + result + } + + /// Acquires an `IoPort` instance that may overlap with other `IoPort`s. + /// + /// This method will only mark the first port in the PIO range as occupied. + #[verus_spec(result => + with + -> claim: Tracked>, + requires + size_of::() <= u16::MAX, + port as usize + size_of::() <= u16::MAX, + obeys_pio_model::(), + allocator::io_port_allocator_initialized(), + ensures + result is Ok <== claim@ is Some, + result matches Ok(io_port) ==> { + &&& io_port@ == port + &&& io_port.is_overlapping() + &&& io_port.well_formed() + &&& io_port.claim_matches_set(claim@->Some_0.set()) + &&& claim@->Some_0.instance_id() == allocator::io_port_allocator_instance_id() + }, + )] + pub fn acquire_overlapping(port: u16) -> Result> { + proof_decl! { + let tracked mut claim: Tracked> = Tracked(None); } - #[verus_spec(with => Tracked(claim))] - /* Original Rust: allocator::IO_PORT_ALLOCATOR.get().unwrap() */ - let port = initialized_allocator().acquire(port); + let port = { + /* Original Rust: allocator::IO_PORT_ALLOCATOR.get().unwrap() */ + #[verus_spec(with Tracked(&mut claim))] + initialized_allocator().acquire(port, true) + }; let result = port.ok_or(Error::AccessDenied); - proof_with!(|= Tracked(claim)); + proof_decl! { + let tracked claim_val: Option = claim.get(); + } + proof_with!(|= Tracked(claim_val)); result } @@ -145,24 +215,50 @@ impl IoPort { size_of::() as u16 } - /// Create an I/O port. + /// Creates an I/O port. + /// + /// # Safety + /// + /// Reading from or writing to the I/O port may have side effects. Those side effects must + /// not cause soundness problems (e.g., they must not corrupt the kernel memory). + #[verus_spec(ret => + requires + size_of::() <= u16::MAX, + port as usize + size_of::() <= u16::MAX, + obeys_pio_model::(), + ensures + ret@ == port, + !ret.is_overlapping(), + ret.well_formed(), + )] + pub(crate) const unsafe fn new(port: u16) -> Self { + // SAFETY: The safety is upheld by the caller. + unsafe { Self::new_overlapping(port, false) } + } + + /// Creates an I/O port. + /// + /// See [`allocator::IoPortAllocator::acquire`] for an explanation of the `is_overlapping` + /// argument. /// /// # Safety /// - /// This function is marked unsafe as creating an I/O port is considered - /// a privileged operation. + /// Reading from or writing to the I/O port may have side effects. Those side effects must + /// not cause soundness problems (e.g., they must not corrupt the kernel memory). #[verus_spec(ret => requires - vstd::layout::size_of::() <= u16::MAX, size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, + obeys_pio_model::(), ensures ret@ == port, + ret.is_overlapping() == is_overlapping, ret.well_formed(), )] - pub const unsafe fn new(port: u16) -> Self { + const unsafe fn new_overlapping(port: u16, is_overlapping: bool) -> Self { Self { port, + is_overlapping, value_marker: PhantomData, access_marker: PhantomData, } @@ -170,8 +266,8 @@ impl IoPort { /// Releases the allocator claim for this port. /// - /// VERUS LIMITATION: this is called explicitly because Verus does not yet support proving the - /// standard `Drop` implementation below. + /// VERUS LIMITATION: this is called explicitly because Verus does not yet support proving + /// the standard `Drop` implementation below. #[verus_spec( with Tracked(claim): Tracked, @@ -179,10 +275,15 @@ impl IoPort { allocator::io_port_allocator_initialized(), claim.instance_id() == allocator::io_port_allocator_instance_id(), self.claim_matches_set(claim.set()), - self@ as usize + size_of::() <= u16::MAX, + self@ as usize + + (if self.is_overlapping() { 1 } else { size_of::() }) <= u16::MAX, )] pub fn drop(self) { - let range = self.port..(self.port + size_of::() as u16); + let range = if self.is_overlapping { + self.port..(self.port + 1) + } else { + self.port..(self.port + size_of::() as u16) + }; unsafe { #[verus_spec(with Tracked(claim))] initialized_allocator().recycle(range); @@ -194,7 +295,6 @@ impl IoPort { #[verifier::allow(undeclared_external_trait)] impl IoPort { /// Reads from the I/O port - #[inline] #[verus_spec(requires self.well_formed())] pub fn read(&self) -> T { unsafe { PortRead::read_from_port(self.port) } @@ -205,7 +305,6 @@ impl IoPort { #[verifier::allow(undeclared_external_trait)] impl IoPort { /// Writes to the I/O port - #[inline] #[verus_spec(requires self.well_formed())] pub fn write(&self, value: T) { unsafe { PortWrite::write_to_port(self.port, value) } @@ -214,13 +313,14 @@ impl IoPort { /* impl Drop for IoPort { fn drop(&mut self) { - // SAFETY: The caller have ownership of the PIO region. - unsafe { - allocator::IO_PORT_ALLOCATOR - .get() - .unwrap() - .recycle(self.port..(self.port + size_of::() as u16)); - } + let range = if !self.is_overlapping { + self.port..(self.port + size_of::() as u16) + } else { + self.port..(self.port + 1) + }; + + // SAFETY: We have ownership of the PIO region. + unsafe { allocator::IO_PORT_ALLOCATOR.get().unwrap().recycle(range) }; } } */ @@ -240,7 +340,8 @@ macro_rules! reserve_io_port_range { const _: () = { #[used] - #[link_section = ".sensitive_io_ports"] + // SAFETY: This is properly handled in the linker script. + #[unsafe(link_section = ".sensitive_io_ports")] static _RANGE: crate::io::RawIoPortRange = crate::io::RawIoPortRange { begin: $range.start, end: $range.end, @@ -277,15 +378,14 @@ macro_rules! sensitive_io_port { $(#[$meta])* $vis static $name: IoPort<$size, $access> = { #[used] - #[link_section = ".sensitive_io_ports"] + // SAFETY: This is properly handled in the linker script. + #[unsafe(link_section = ".sensitive_io_ports")] static _RESERVED_IO_PORT_RANGE: crate::io::RawIoPortRange = crate::io::RawIoPortRange { begin: $name.port(), end: $name.port() + $name.size(), }; - unsafe { - IoPort::new($port) - } + unsafe { IoPort::new($port) } }; )* }; @@ -295,8 +395,8 @@ pub(crate) use reserve_io_port_range; pub(crate) use sensitive_io_port; #[doc(hidden)] -#[derive(Debug, Clone, Copy)] #[repr(C)] +#[derive(Clone, Copy, Debug)] #[verus_verify] pub(crate) struct RawIoPortRange { pub(crate) begin: u16, diff --git a/ostd/src/io/mod.rs b/ostd/src/io/mod.rs index b27636383..e868aa4b3 100644 --- a/ostd/src/io/mod.rs +++ b/ostd/src/io/mod.rs @@ -7,7 +7,7 @@ //! - `IoPort` for port I/O (PIO). use vstd::prelude::*; -mod io_mem; +pub(crate) mod io_mem; use cfg_if::cfg_if; @@ -17,7 +17,8 @@ pub(crate) use self::io_mem::IoMemAllocatorBuilder; cfg_if!( if #[cfg(target_arch = "x86_64")] { mod io_port; - pub use io_port::IoPort; + + pub use self::io_port::IoPort; pub(crate) use self::io_port::{reserve_io_port_range, sensitive_io_port, RawIoPortRange}; } ); @@ -39,11 +40,11 @@ cfg_if!( #[verus_verify] pub(crate) unsafe fn init(io_mem_builder: IoMemAllocatorBuilder) { // SAFETY: The safety is upheld by the caller. - unsafe { self::io_mem::init(io_mem_builder) }; + unsafe { io_mem::init(io_mem_builder) }; // SAFETY: The safety is upheld by the caller. #[cfg(target_arch = "x86_64")] unsafe { - self::io_port::init() + io_port::init() }; } diff --git a/verified_libs/vstd_extra/src/external/io_port.rs b/verified_libs/vstd_extra/src/external/io_port.rs index 1a0a6e41a..4180a4d31 100644 --- a/verified_libs/vstd_extra/src/external/io_port.rs +++ b/verified_libs/vstd_extra/src/external/io_port.rs @@ -9,18 +9,52 @@ use x86_64::{ verus! { -/// Whether `port` is representable in the 16-bit x86 I/O-port address space. +/// Whether a `T`-typed access at `port` fits in the PIO byte range `0..=u16::MAX`. /// -/// This is only the ISA-level validity condition. It does not claim that a device decodes the -/// port, that the current CPU context may access it, or that the caller owns it. -pub open spec fn valid_io_port_number(port: int) -> bool { - 0 <= port <= u16::MAX as int -} +/// Uninterpreted: `PortRead`/`PortWrite` are user-implementable, so meaning comes only from +/// the trusted widths in [`group_io_port_models`]. ISA-level fact only — no claim about +/// device decoding, access permission, or ownership. +pub uninterp spec fn valid_io_port_access(port: int) -> bool; + +/// Whether `T` is one of the trusted port widths; uninterpreted, admitted only by the axioms +/// below. +pub uninterp spec fn obeys_pio_model() -> bool; + +/// Trusted: `u8` ports are written and read via `outb`/`inb`. +pub broadcast axiom fn axiom_u8_pio_model() + ensures + #[trigger] obeys_pio_model::(), +; + +/// Trusted: `u16` ports are written and read via `outw`/`inw`. +pub broadcast axiom fn axiom_u16_pio_model() + ensures + #[trigger] obeys_pio_model::(), +; + +/// Trusted: `u32` ports are written and read via `outl`/`inl`. +pub broadcast axiom fn axiom_u32_pio_model() + ensures + #[trigger] obeys_pio_model::(), +; + +/// Under the model, a `T`-access is ISA-valid iff its `size_of::()` bytes fit in +/// `0..(u16::MAX + 1)`. +pub broadcast axiom fn axiom_pio_model_access() + requires + obeys_pio_model::(), + ensures + forall|port: int| #[trigger] + valid_io_port_access::(port) <==> (0 <= port && port + size_of::() <= u16::MAX + + 1), +; -/// Whether an access of type `T` fits in the PIO byte range `0..u16::MAX`. -pub open spec fn valid_io_port_access(port: int) -> bool { - &&& valid_io_port_number(port) - &&& port + size_of::() <= u16::MAX as int +/// The trusted instances of the PIO model. +pub broadcast group group_io_port_models { + axiom_u8_pio_model, + axiom_u16_pio_model, + axiom_u32_pio_model, + axiom_pio_model_access, } /// Opaque specification boundary for the third-party read/write access marker. From 15e0bb714754eff837723b53e777ac61f611d4c1 Mon Sep 17 00:00:00 2001 From: Xinyi Wan <64517311+rikosellic@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:49:51 +0800 Subject: [PATCH 18/30] chore: fix `PartialSpec` for `Frame` (#768) * chore: fix `PartialSpec` for `Frame` * fmt --- ostd/src/mm/frame/mod.rs | 48 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/ostd/src/mm/frame/mod.rs b/ostd/src/mm/frame/mod.rs index 9d335c282..377e934f7 100644 --- a/ostd/src/mm/frame/mod.rs +++ b/ostd/src/mm/frame/mod.rs @@ -33,6 +33,7 @@ use vstd::atomic::PermissionU64; use vstd::map::assert_maps_equal_internal; use vstd::prelude::*; use vstd::simple_pptr::{self, PPtr}; +use vstd::std_specs::cmp::PartialEqSpecImpl; use vstd::{assert_maps_equal, assert_sets_equal}; use vstd_extra::cast_ptr::*; use vstd_extra::ownership::*; @@ -146,43 +147,36 @@ impl core::fmt::Debug for Frame { write!(f, "Frame({:#x})", self.start_paddr()) } } +*/ -impl PartialEq for Frame { - fn eq(&self, other: &Self) -> bool { - self.start_paddr() == other.start_paddr() +verus!{ +impl + ?Sized> PartialEqSpecImpl for Frame{ + open spec fn obeys_eq_spec() -> bool { true } + + open spec fn eq_spec(&self, other: &Self) -> bool { + self.start_paddr_spec() == other.start_paddr_spec() } } -impl Eq for Frame {} -*/ +} #[verus_verify] -impl + ?Sized> Frame { - /// Compares two frames by their start physical address. - /// - /// # Verified Properties - /// ## Preconditions - /// - **Safety Invariant**: the frames and metadata regions must satisfy the global invariants. - /// ## Postconditions - /// - **Correctness**: the function returns true if the frames have - /// the same physical addresses and false otherwise. - /// ## Safety - /// Everything is immutable, so the safety invariant is preserved implicitly. - /// ## Verification Design - /// This is an inherent impl equivalent to `PartialEq::eq` for `Frame`: freed from the - /// trait signature so that this version can thread the tracked `MetaRegionOwners` via `verus_spec`. - #[verus_spec( - requires - self.ptr_inv(), - other.ptr_inv(), - returns - self.start_paddr_spec() == other.start_paddr_spec(), - )] - pub fn eq(&self, other: &Self) -> bool { +impl + ?Sized> PartialEq for Frame { + fn eq(&self, other: &Self) -> bool { + proof!{ + //FIXME: Add `ptr_inv` as type invariant when we fix visibility. + assume(self.ptr_inv()); + assume(other.ptr_inv()); + } self.start_paddr() == other.start_paddr() } } +#[verus_verify] +impl + ?Sized> Eq for Frame { + +} + #[verus_verify] impl + OwnerOf> Frame { /// Gets a [`Frame`] with a specific usage from a raw, unused page. From 16058e2563f0937d77878a1dff8af450009a2081 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Tue, 15 Sep 2026 16:53:29 +0800 Subject: [PATCH 19/30] refine: replace `obeys_pio_model` with `valid_io_port_access` in I/O port handling --- ostd/src/arch/x86/device/io_port.rs | 2 +- ostd/src/io/io_port/allocator.rs | 4 +-- ostd/src/io/io_port/mod.rs | 12 +++---- .../vstd_extra/src/external/io_port.rs | 34 +++++-------------- 4 files changed, 18 insertions(+), 34 deletions(-) diff --git a/ostd/src/arch/x86/device/io_port.rs b/ostd/src/arch/x86/device/io_port.rs index 4ab813494..c2d0399b6 100644 --- a/ostd/src/arch/x86/device/io_port.rs +++ b/ostd/src/arch/x86/device/io_port.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port access. -pub use vstd_extra::external::{group_io_port_models, obeys_pio_model, valid_io_port_access}; +pub use vstd_extra::external::{group_io_port_models, valid_io_port_access}; pub use x86_64::{ instructions::port::{ diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index bf1aa787b..e7f859c67 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O port allocator. +use crate::arch::device::io_port::valid_io_port_access; use vstd::{ prelude::*, resource::set::{GhostSetAuth, GhostSubset}, @@ -14,7 +15,6 @@ use log::debug; use spin::Once; use super::{IoPort, lemma_port_id_set_contains, lemma_port_id_set_insert, port_id_set}; -use crate::arch::device::io_port::obeys_pio_model; use crate::{ io::RawIoPortRange, sync::{LocalIrqDisabled, SpinLock}, @@ -359,7 +359,7 @@ impl IoPortAllocator { requires size_of::() <= u16::MAX, is_overlapping ==> port as usize + size_of::() <= u16::MAX, - obeys_pio_model::(), + valid_io_port_access::(port), io_port_allocator_initialized(), (*old(claim_out))@ is None, ensures diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index fddbed460..71f753c97 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -3,7 +3,7 @@ use vstd::prelude::*; use crate::arch::device::io_port::{ - IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite, obeys_pio_model, valid_io_port_access, + IoPortReadAccess, IoPortWriteAccess, PortRead, PortWrite, valid_io_port_access, }; mod allocator; @@ -52,7 +52,7 @@ impl IoPort { /// The complete byte range occupied by this typed port lies in the x86 PIO address space. #[verifier::type_invariant] pub open spec fn well_formed(&self) -> bool { - valid_io_port_access::(self@ as int) + valid_io_port_access::(self@) } /// Whether the port was acquired as overlapping: it occupies only its first port. @@ -137,7 +137,7 @@ impl IoPort { requires size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, - obeys_pio_model::(), + valid_io_port_access::(port), allocator::io_port_allocator_initialized(), ensures result is Ok <== claim@ is Some, @@ -175,7 +175,7 @@ impl IoPort { requires size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, - obeys_pio_model::(), + valid_io_port_access::(port), allocator::io_port_allocator_initialized(), ensures result is Ok <== claim@ is Some, @@ -225,7 +225,7 @@ impl IoPort { requires size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, - obeys_pio_model::(), + valid_io_port_access::(port), ensures ret@ == port, !ret.is_overlapping(), @@ -249,7 +249,7 @@ impl IoPort { requires size_of::() <= u16::MAX, port as usize + size_of::() <= u16::MAX, - obeys_pio_model::(), + valid_io_port_access::(port), ensures ret@ == port, ret.is_overlapping() == is_overlapping, diff --git a/verified_libs/vstd_extra/src/external/io_port.rs b/verified_libs/vstd_extra/src/external/io_port.rs index 4180a4d31..d4ce66be8 100644 --- a/verified_libs/vstd_extra/src/external/io_port.rs +++ b/verified_libs/vstd_extra/src/external/io_port.rs @@ -14,39 +14,24 @@ verus! { /// Uninterpreted: `PortRead`/`PortWrite` are user-implementable, so meaning comes only from /// the trusted widths in [`group_io_port_models`]. ISA-level fact only — no claim about /// device decoding, access permission, or ownership. -pub uninterp spec fn valid_io_port_access(port: int) -> bool; - -/// Whether `T` is one of the trusted port widths; uninterpreted, admitted only by the axioms -/// below. -pub uninterp spec fn obeys_pio_model() -> bool; +pub uninterp spec fn valid_io_port_access(port: u16) -> bool; /// Trusted: `u8` ports are written and read via `outb`/`inb`. -pub broadcast axiom fn axiom_u8_pio_model() +pub broadcast axiom fn axiom_u8_pio_model(port: u16) ensures - #[trigger] obeys_pio_model::(), + #[trigger] valid_io_port_access::(port) <==> port + size_of::() <= u16::MAX + 1, ; /// Trusted: `u16` ports are written and read via `outw`/`inw`. -pub broadcast axiom fn axiom_u16_pio_model() +pub broadcast axiom fn axiom_u16_pio_model(port: u16) ensures - #[trigger] obeys_pio_model::(), + #[trigger] valid_io_port_access::(port) <==> port + size_of::() <= u16::MAX + 1, ; /// Trusted: `u32` ports are written and read via `outl`/`inl`. -pub broadcast axiom fn axiom_u32_pio_model() - ensures - #[trigger] obeys_pio_model::(), -; - -/// Under the model, a `T`-access is ISA-valid iff its `size_of::()` bytes fit in -/// `0..(u16::MAX + 1)`. -pub broadcast axiom fn axiom_pio_model_access() - requires - obeys_pio_model::(), +pub broadcast axiom fn axiom_u32_pio_model(port: u16) ensures - forall|port: int| #[trigger] - valid_io_port_access::(port) <==> (0 <= port && port + size_of::() <= u16::MAX - + 1), + #[trigger] valid_io_port_access::(port) <==> port + size_of::() <= u16::MAX + 1, ; /// The trusted instances of the PIO model. @@ -54,7 +39,6 @@ pub broadcast group group_io_port_models { axiom_u8_pio_model, axiom_u16_pio_model, axiom_u32_pio_model, - axiom_pio_model_access, } /// Opaque specification boundary for the third-party read/write access marker. @@ -75,7 +59,7 @@ pub trait ExPortRead { /// A port read can produce any value supplied by the device. unsafe fn read_from_port(port: u16) -> Self where Self: Sized requires - valid_io_port_access::(port as int), + valid_io_port_access::(port), ; } @@ -87,7 +71,7 @@ pub trait ExPortWrite { /// A port write has no modeled logical effect on kernel memory. unsafe fn write_to_port(port: u16, value: Self) where Self: Sized requires - valid_io_port_access::(port as int), + valid_io_port_access::(port), ; } From 56c7f3f06757e2f9a64e1f433d4fc1f8f1150e8a Mon Sep 17 00:00:00 2001 From: Xinyi Wan <64517311+rikosellic@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:30:09 +0800 Subject: [PATCH 20/30] refactor: rebase `Atomicdatawithowner` and `OnceImpl` on `ResourceInvariant` (#769) * refactor `AtomicDataWithOwner` * refactor: rebase `AtomicDataWithOwner` with `ResourceInvariant`` * rebase `OnceImpl` * fix comment --- ostd/src/mm/dma/dma_coherent.rs | 34 +++++++---- ostd/src/mm/dma/dma_stream.rs | 34 +++++++---- ostd/src/mm/dma/mod.rs | 35 +++++++---- ostd/src/mm/kspace/mod.rs | 7 +-- ostd/src/sync/mod.rs | 4 -- ostd/src/sync/rcu/mod.rs | 6 +- ostd/src/sync/rcu/monitor.rs | 30 ++++++---- .../vstd_extra/src}/atomic_data.rs | 60 ++++++++----------- verified_libs/vstd_extra/src/lib.rs | 2 + .../vstd_extra/src}/once.rs | 38 ++++-------- .../vstd_extra/src/resource_invariant.rs | 51 +++++++++++++--- 11 files changed, 175 insertions(+), 126 deletions(-) rename {ostd/src/sync => verified_libs/vstd_extra/src}/atomic_data.rs (52%) rename {ostd/src/sync => verified_libs/vstd_extra/src}/once.rs (85%) diff --git a/ostd/src/mm/dma/dma_coherent.rs b/ostd/src/mm/dma/dma_coherent.rs index 2998fee7f..860e51a2d 100644 --- a/ostd/src/mm/dma/dma_coherent.rs +++ b/ostd/src/mm/dma/dma_coherent.rs @@ -1,9 +1,13 @@ // SPDX-License-Identifier: MPL-2.0 use core::marker::PhantomData; -use vstd::{predicate::Predicate, prelude::*}; +use vstd::prelude::*; -use vstd_extra::ownership::{Inv, OwnerOf}; +use vstd_extra::{ + atomic_data::AtomicDataWithOwner, + ownership::{Inv, OwnerOf}, + resource_invariant::SimpleResourceInvariant, +}; use crate::{ error::Error, @@ -22,7 +26,7 @@ use crate::{ arch::{PAGE_SIZE, lemma_max_paddr_range, lemma_paddr_to_vaddr_properties}, mm::virt_mem::VirtPtr, }, - sync::{AtomicDataWithOwner, PreemptDisabled, RwArc, RwLockReadGuard}, + sync::{PreemptDisabled, RwArc, RwLockReadGuard}, }; use super::{DmaError, HasDaddr, check_and_insert_dma_mapping, is_valid_daddr}; @@ -62,9 +66,11 @@ pub tracked struct DmaCoherentInnerOwner { pub _marker: PhantomData, } +pub ghost struct DmaCoherentInnerInvariant; + pub type DmaCoherentInnerAtomic = AtomicDataWithOwner< DmaCoherentInner, - DmaCoherentInnerOwner, + DmaCoherentInnerInvariant, >; impl Inv for DmaCoherent { @@ -86,6 +92,17 @@ impl Inv for DmaCoherentInnerOwner { } } +impl SimpleResourceInvariant< + DmaCoherentInner, +> for DmaCoherentInnerInvariant { + type Resource = DmaCoherentInnerOwner; + + open spec fn inv(value: DmaCoherentInner, resource: DmaCoherentInnerOwner) -> bool { + &&& resource.inv() + &&& value.inv() + } +} + #[verus_verify] impl DmaCoherent { /// Creates a coherent DMA mapping backed by `segment`. @@ -165,6 +182,7 @@ impl DmaCoherent { AtomicDataWithOwner::new( DmaCoherentInner { segment, start_daddr, is_cache_coherent }, Tracked(inner_owner), + Ghost(DmaCoherentInnerInvariant), ), ); @@ -450,14 +468,6 @@ impl DmaCoherent { } } -impl Predicate> for DmaCoherentInnerOwner { - #[verifier::inline] - open spec fn predicate(&self, v: DmaCoherentInner) -> bool { - &&& self.inv() - &&& v.inv() - } -} - #[verus_verify] impl Clone for DmaCoherent { #[verus_spec(r => diff --git a/ostd/src/mm/dma/dma_stream.rs b/ostd/src/mm/dma/dma_stream.rs index 658a9907d..3e574f867 100644 --- a/ostd/src/mm/dma/dma_stream.rs +++ b/ostd/src/mm/dma/dma_stream.rs @@ -1,10 +1,14 @@ use core::{marker::PhantomData, ops::Deref, ops::Range}; // SPDX-License-Identifier: MPL-2.0 -use vstd::{predicate::Predicate, prelude::*}; +use vstd::prelude::*; use vstd_extra::external::convert::AsRefSpec; -use vstd_extra::ownership::{Inv, OwnerOf}; +use vstd_extra::{ + atomic_data::AtomicDataWithOwner, + ownership::{Inv, OwnerOf}, + resource_invariant::SimpleResourceInvariant, +}; use crate::mm::vm_space::vm_space_specs::VmSpaceOwner; use crate::{ @@ -25,7 +29,7 @@ use crate::{ mm::io::{VmIoMemView, VmIoOwner}, mm::virt_mem::{MemView, VirtPtr}, }, - sync::{AtomicDataWithOwner, PreemptDisabled, RoArc, RwArc, RwLockReadGuard}, + sync::{PreemptDisabled, RoArc, RwArc, RwLockReadGuard}, }; use super::{DmaError, HasDaddr, check_and_insert_dma_mapping, is_valid_daddr}; @@ -378,7 +382,9 @@ pub tracked struct DmaStreamInnerOwner { pub _marker: core::marker::PhantomData, } -pub type DmaStreanInnerAtomic = AtomicDataWithOwner, DmaStreamInnerOwner>; +pub ghost struct DmaStreamInnerInvariant; + +pub type DmaStreanInnerAtomic = AtomicDataWithOwner, DmaStreamInnerInvariant>; #[verus_verify] impl DmaStream { @@ -451,6 +457,7 @@ impl DmaStream { AtomicDataWithOwner::new( DmaStreamInner { segment, start_daddr, is_cache_coherent, direction }, Tracked(inner_owner), + Ghost(DmaStreamInnerInvariant), ), ); @@ -685,6 +692,17 @@ impl Inv for DmaStreamInnerOwner { } } +impl SimpleResourceInvariant< + DmaStreamInner, +> for DmaStreamInnerInvariant { + type Resource = DmaStreamInnerOwner; + + open spec fn inv(value: DmaStreamInner, resource: DmaStreamInnerOwner) -> bool { + &&& resource.inv() + &&& value.inv() + } +} + #[verus_verify] impl DmaStream { /// Acquires a read guard for the inner DMA stream state. @@ -1002,14 +1020,6 @@ impl DmaStream { } } -impl Predicate> for DmaStreamInnerOwner { - #[verifier::inline] - open spec fn predicate(&self, v: DmaStreamInner) -> bool { - &&& self.inv() - &&& v.inv() - } -} - /* // Original Drop impl for DmaStreamInner (removed during Verus migration): impl Drop for DmaStreamInner { diff --git a/ostd/src/mm/dma/mod.rs b/ostd/src/mm/dma/mod.rs index 16c2a4762..cf8d63d42 100644 --- a/ostd/src/mm/dma/mod.rs +++ b/ostd/src/mm/dma/mod.rs @@ -6,34 +6,46 @@ mod dma_stream; mod test; use alloc::collections::BTreeSet; -use vstd::{predicate::Predicate as DataPredicate, prelude::*}; - -use crate::sync::{ - AtomicDataWithOwner, Once, PreemptDisabled, SpinLock, SpinLockGuard, TrivialPred, +use vstd::prelude::*; +use vstd_extra::{ + atomic_data::AtomicDataWithOwner, + once::Once, + resource_invariant::{SimpleResourceInvariant, TrivialResourceInvariant}, }; +use crate::sync::{PreemptDisabled, SpinLock, SpinLockGuard}; + use super::Paddr; verus! { pub tracked struct DmaMappingSetOwner {} -impl DataPredicate, PreemptDisabled>> for DmaMappingSetOwner { - open spec fn predicate(&self, v: SpinLock, PreemptDisabled>) -> bool { - v.wf() +pub ghost struct DmaMappingSetInvariant; + +impl SimpleResourceInvariant< + SpinLock, PreemptDisabled>, +> for DmaMappingSetInvariant { + type Resource = DmaMappingSetOwner; + + open spec fn inv( + v: SpinLock, PreemptDisabled>, + _resource: DmaMappingSetOwner, + ) -> bool { + v.type_inv() } } /// Set of all physical addresses with dma mapping. exec static DMA_MAPPING_SET: Once< SpinLock, PreemptDisabled>, - DmaMappingSetOwner, - TrivialPred, + DmaMappingSetInvariant, + TrivialResourceInvariant, > ensures DMA_MAPPING_SET.wf(), { - Once::new(Ghost(TrivialPred)) + Once::new(Ghost(TrivialResourceInvariant)) } #[inline(always)] @@ -44,7 +56,8 @@ pub fn init() { use_type_invariant(&lock); } - let data = AtomicDataWithOwner::new(lock, Tracked(DmaMappingSetOwner { })); + let tracked owner = DmaMappingSetOwner { }; + let data = AtomicDataWithOwner::new(lock, Tracked(owner), Ghost(DmaMappingSetInvariant)); DMA_MAPPING_SET.init(data); } diff --git a/ostd/src/mm/kspace/mod.rs b/ostd/src/mm/kspace/mod.rs index 5a5eaa585..2c8514790 100644 --- a/ostd/src/mm/kspace/mod.rs +++ b/ostd/src/mm/kspace/mod.rs @@ -36,9 +36,9 @@ use core::{marker::PhantomData, ops::Range}; use vstd::atomic::PermissionU64; use vstd::prelude::*; use vstd::simple_pptr::PointsTo; +use vstd_extra::{once::OnceImpl, resource_invariant::TrivialResourceInvariant}; //use log::info; -use crate::sync::{OnceImpl, TrivialPred}; pub(crate) mod kvirt_area; #[cfg(ktest)] mod test; @@ -155,9 +155,8 @@ pub fn paddr_to_vaddr(pa: Paddr) -> usize /// It manages the kernel mapping of all address spaces by sharing the kernel part. And it /// is unlikely to be activated. #[allow(private_interfaces)] -pub exec static KERNEL_PAGE_TABLE: OnceImpl, TrivialPred> = OnceImpl::new( - Ghost(TrivialPred), -); +pub exec static KERNEL_PAGE_TABLE: OnceImpl, TrivialResourceInvariant> = + OnceImpl::new(Ghost(TrivialResourceInvariant)); #[verifier::allow(autoderive_clone_without_spec)] #[derive(Clone, Debug)] diff --git a/ostd/src/sync/mod.rs b/ostd/src/sync/mod.rs index 4908e75f4..1d3f5e951 100644 --- a/ostd/src/sync/mod.rs +++ b/ostd/src/sync/mod.rs @@ -1,9 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 //! Useful synchronization primitives. -mod atomic_data; mod guard; mod mutex; -mod once; mod rcu; mod rwarc; mod rwlock; @@ -12,10 +10,8 @@ mod spin; mod wait; //pub(crate) use self::rcu::finish_grace_period; pub use self::{ - atomic_data::*, guard::{GuardTransfer, LocalIrqDisabled, PreemptDisabled, SpinGuardian, /*WriteIrqDisabled*/}, mutex::{Mutex, MutexGuard}, - once::{Once, OnceImpl, TrivialPred}, rcu::{non_null /*, Rcu, RcuDrop, RcuOption, RcuOptionReadGuard, RcuReadGuard*/}, rwarc::{RoArc, RwArc}, rwlock::{RwLock, RwLockReadGuard, RwLockUpgradeableGuard, RwLockWriteGuard}, diff --git a/ostd/src/sync/rcu/mod.rs b/ostd/src/sync/rcu/mod.rs index f1ec84f45..4144c2bd7 100644 --- a/ostd/src/sync/rcu/mod.rs +++ b/ostd/src/sync/rcu/mod.rs @@ -13,6 +13,7 @@ use vstd::{ }; use vstd_extra::{ + once::Once, prelude::*, resource::ghost_resource::count_auth::{Count, CountResource}, }; @@ -30,9 +31,8 @@ use core::{ use non_null::{NonNullPtr, NonNullPtrRef}; // use spin::once::Once; -use super::Once; -use self::monitor::{RcuMonitor, RcuMonitorOwner, RcuMonitorPred}; +use self::monitor::{RcuMonitor, RcuMonitorInvariant, RcuMonitorPred}; use crate::task::{ DisabledPreemptGuard, //atomic_mode::{AsAtomicModeGuard, InAtomicMode}, @@ -1062,7 +1062,7 @@ pub unsafe fn finish_grace_period() { */ -exec static RCU_MONITOR: Once +exec static RCU_MONITOR: Once ensures RCU_MONITOR.wf(), RCU_MONITOR.inv() == RcuMonitorPred, diff --git a/ostd/src/sync/rcu/monitor.rs b/ostd/src/sync/rcu/monitor.rs index bedcd3c6d..6224e04fb 100644 --- a/ostd/src/sync/rcu/monitor.rs +++ b/ostd/src/sync/rcu/monitor.rs @@ -1,12 +1,14 @@ // SPDX-License-Identifier: MPL-2.0 -use vstd::{ - atomic_ghost::AtomicBool, atomic_with_ghost, predicate::Predicate as DataPredicate, prelude::*, +use vstd::{atomic_ghost::AtomicBool, atomic_with_ghost, prelude::*}; +use vstd_extra::{ + atomic_data::AtomicDataWithOwner, + ownership::Inv, + resource_invariant::{SimpleResourceInvariant, ValueInvariant}, }; -use vstd_extra::ownership::Inv; use crate::{ specs::mm::cpu::{AtomicCpuSet, CpuSet}, - sync::{AtomicDataWithOwner, LocalIrqDisabled, SpinLock, once::Predicate as OncePredicate}, + sync::{LocalIrqDisabled, SpinLock}, }; verus! { @@ -27,6 +29,8 @@ pub(super) struct State { /// Owner of this [`RcuMonitor`]. pub(super) tracked struct RcuMonitorOwner {} +pub(super) ghost struct RcuMonitorInvariant; + struct_with_invariants! { /// A RCU monitor ensures the completion of _grace periods_ by keeping track /// of each CPU's passing _quiescent states_. @@ -43,8 +47,10 @@ closed spec fn wf(self) -> bool { } } -impl DataPredicate for RcuMonitorOwner { - closed spec fn predicate(&self, v: RcuMonitor) -> bool { +impl SimpleResourceInvariant for RcuMonitorInvariant { + type Resource = RcuMonitorOwner; + + closed spec fn inv(_value: RcuMonitor, _resource: RcuMonitorOwner) -> bool { true } } @@ -58,9 +64,9 @@ impl RcuMonitor { pub(super) struct RcuMonitorPred; -impl OncePredicate> for RcuMonitorPred { - closed spec fn inv(self, v: AtomicDataWithOwner) -> bool { - &&& v.permission@.predicate(v.data) +impl ValueInvariant> for RcuMonitorPred { + closed spec fn inv(v: AtomicDataWithOwner) -> bool { + &&& v.inv() &&& v.data.inv() } } @@ -107,9 +113,11 @@ impl RcuMonitor { ensures r.inv(), r.data.inv(), - RcuMonitorPred.inv(r), + , + >>::inv(r), )] - pub(super) fn new_data() -> AtomicDataWithOwner { + pub(super) fn new_data() -> AtomicDataWithOwner { let data = Self::new(); proof { use_type_invariant(&data); diff --git a/ostd/src/sync/atomic_data.rs b/verified_libs/vstd_extra/src/atomic_data.rs similarity index 52% rename from ostd/src/sync/atomic_data.rs rename to verified_libs/vstd_extra/src/atomic_data.rs index 46b80edc3..ba9ef016f 100644 --- a/ostd/src/sync/atomic_data.rs +++ b/verified_libs/vstd_extra/src/atomic_data.rs @@ -1,9 +1,7 @@ use core::ops::Deref; -use vstd::{predicate::Predicate, prelude::*}; -use vstd_extra::ownership::Inv; - -use super::{RwLockReadGuard, SpinGuardian}; +use crate::{ownership::Inv, resource_invariant::SimpleResourceInvariant}; +use vstd::prelude::*; verus! { @@ -34,41 +32,30 @@ verus! { /// pub quz: Seq, /// } /// -/// impl Inv for MyData { -/// // inv... -/// } +/// ghost struct MyDataInvariant; +/// +/// impl SimpleResourceInvariant for MyDataInvariant { /// -/// impl Predicate for MyDataWithOwner { -/// #[verifier::inline] -/// open spec fn inv_with(self, v: MyData) -> bool { -/// &&& self.baz == v.foo as nat -/// &&& self.quz.len() == v.bar as nat -/// } +/// type Resource = MyDataWithOwner; +/// +/// open spec fn inv(value: MyData, resource: MyDataWithOwner) -> bool { +/// &&& resource.baz == value.foo +/// &&& resource.quz.len() == value.bar +/// } /// } /// -/// type Data = AtomicDataWithOwner; +/// type Data = AtomicDataWithOwner; /// ``` -pub struct AtomicDataWithOwner { +pub struct AtomicDataWithOwner> { /// The underlying data. pub data: V, /// The permission to access the data. - pub permission: Tracked, -} - -impl<'a, V, Own, G: SpinGuardian> RwLockReadGuard<'a, crate::sync::AtomicDataWithOwner, G> { - /// Borrows the tracked permission stored in an [`AtomicDataWithOwner`]. - #[verifier::external_body] - pub proof fn atomic_permission(tracked &self) -> (tracked permission: &'a Own) - returns - self@.permission@, - { - unimplemented!() - } + pub permission: Tracked, } } // verus! #[verus_verify] -impl Deref for AtomicDataWithOwner { +impl> Deref for AtomicDataWithOwner { type Target = V; #[inline] @@ -80,29 +67,32 @@ impl Deref for AtomicDataWithOwner { verus! { -impl AtomicDataWithOwner { +impl> AtomicDataWithOwner { #[inline] - pub fn new(data: V, permission: Tracked) -> Self { + pub fn new(data: V, permission: Tracked, Ghost(_pred): Ghost) -> Self + requires + I::inv(data, permission@), + { Self { data, permission } } } -impl !Copy for AtomicDataWithOwner { +impl> !Copy for AtomicDataWithOwner { } -impl !Clone for AtomicDataWithOwner { +impl> !Clone for AtomicDataWithOwner { } -impl> Inv for AtomicDataWithOwner { +impl> Inv for AtomicDataWithOwner { #[verifier::inline] open spec fn inv(self) -> bool { - &&& self.permission.predicate(self.data) + I::inv(self.data, self.permission@) } } -impl View for AtomicDataWithOwner { +impl> View for AtomicDataWithOwner { type V = T; #[verifier::inline] diff --git a/verified_libs/vstd_extra/src/lib.rs b/verified_libs/vstd_extra/src/lib.rs index 9fa472683..285c3446d 100644 --- a/verified_libs/vstd_extra/src/lib.rs +++ b/verified_libs/vstd_extra/src/lib.rs @@ -18,6 +18,7 @@ extern crate alloc; pub mod arithmetic; pub mod array_ptr; +pub mod atomic_data; #[cfg(feature = "irc11")] pub mod atomic_irc11; pub mod auxiliary; @@ -26,6 +27,7 @@ pub mod drop_tracking; pub mod external; pub mod function_properties; pub mod ghost_tree; +pub mod once; pub mod ownership; pub mod panic; pub mod resource; diff --git a/ostd/src/sync/once.rs b/verified_libs/vstd_extra/src/once.rs similarity index 85% rename from ostd/src/sync/once.rs rename to verified_libs/vstd_extra/src/once.rs index 2b07be415..741d31b66 100644 --- a/ostd/src/sync/once.rs +++ b/verified_libs/vstd_extra/src/once.rs @@ -7,7 +7,7 @@ use vstd::{ prelude::*, }; -use super::AtomicDataWithOwner; +use crate::{atomic_data::AtomicDataWithOwner, resource_invariant::ValueInvariant}; verus! { @@ -34,22 +34,6 @@ unsafe impl Objective for OnceState { } -/// A [`Predicate`] is something you're gonna preserve during the lifetime -/// of any synchronization primitives like [`Once`]. -pub trait Predicate { - spec fn inv(self, v: V) -> bool; -} - -/// A trivial predicate that holds for any value. -/// Use with [`OnceImpl`] when no invariant is needed. -pub struct TrivialPred; - -impl Predicate for TrivialPred { - open spec fn inv(self, v: V) -> bool { - true - } -} - struct_with_invariants! { /// A synchronization primitive which can nominally be written to only once. /// @@ -71,7 +55,7 @@ struct_with_invariants! { /// assert(value.is_some()); // unsatisfied precondition, as MY_ONCE is uninitialized. /// ``` #[verifier::reject_recursive_types(V)] -pub struct OnceImpl> { +pub struct OnceImpl> { cell: PCell>, state: vstd::atomic_ghost::AtomicU64<_, OnceState, _>, f: Ghost, @@ -93,7 +77,7 @@ pub closed spec fn wf(&self) -> bool { &&& v == INITED &&& points_to.id() == cell.id() &&& points_to.value() is Some - &&& f@.inv(points_to.value()->0) + &&& F::inv(points_to.value()->0) } } } @@ -102,16 +86,16 @@ pub closed spec fn wf(&self) -> bool { } #[verifier::external] -unsafe impl> Send for OnceImpl { +unsafe impl> Send for OnceImpl { } #[verifier::external] -unsafe impl> Sync for OnceImpl { +unsafe impl> Sync for OnceImpl { } -impl> OnceImpl { +impl> OnceImpl { pub closed spec fn inv(&self) -> F { self.f@ } @@ -136,7 +120,7 @@ impl> OnceImpl { /// Initializes the [`Once`] with the given value `v`. pub fn init(&self, v: V) requires - self.inv().inv(v), + F::inv(v), self.wf(), { let cur_state = @@ -194,7 +178,7 @@ impl> OnceImpl { self.wf(), ensures self.wf(), - r matches Some(res) ==> self.inv().inv(*res), + r matches Some(res) ==> F::inv(*res), { let tracked mut points_to = None; let res = @@ -223,9 +207,9 @@ impl> OnceImpl { /// A `Once` that combines some data with a permission to access it. /// /// This type alias automatically lifts the target value `V` into -/// a wrapper [`AtomicDataWithOwner`] where `Own` is the -/// permission type so that we can reason about non-trivial runtime +/// a wrapper [`AtomicDataWithOwner`] where `I` relates the value to +/// its tracked resource so that we can reason about non-trivial runtime /// properties in verification. -pub type Once = OnceImpl, F>; +pub type Once = OnceImpl, F>; } // verus! diff --git a/verified_libs/vstd_extra/src/resource_invariant.rs b/verified_libs/vstd_extra/src/resource_invariant.rs index 12ec12be5..fcb3ab30f 100644 --- a/verified_libs/vstd_extra/src/resource_invariant.rs +++ b/verified_libs/vstd_extra/src/resource_invariant.rs @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 -use vstd::prelude::*; #[cfg(feature = "irc11")] use vstd::thread_view::Objective; +use vstd::{prelude::*, resource}; verus! { -/// An invariant that relates a value to a tracked resource. -pub trait ResourceInvariant: Sized { +/// An invariant that relates a value to a tracked resource with a constant. +pub trait ResourceInvariant { /// Immutable ghost configuration fixed at creation time. type Constant; @@ -25,15 +25,52 @@ pub trait ResourceInvariant: Sized { spec fn inv(constant: Self::Constant, value: V, resource: Self::Resource) -> bool; } -/// A resource invariant that imposes no condition on the value. -pub struct TrivialResourceInvariant; +/// A resource invariant that does not need a constant. +pub trait SimpleResourceInvariant { + /// A tracked resource associated with the value and transferred linearly between owners. + #[cfg(not(feature = "irc11"))] + type Resource; + + /// The tracked resource stored in an IRC11 atomic invariant. + /// + /// It must be objective so moving the resource through a lock does not + /// implicitly transfer a thread's subjective weak-memory observations. + #[cfg(feature = "irc11")] + type Resource: Objective; + + // The relation that must hold between the value and tracked resource. + spec fn inv(value: V, resource: Self::Resource) -> bool; +} -impl ResourceInvariant for TrivialResourceInvariant { +impl> ResourceInvariant for T { type Constant = (); + type Resource = >::Resource; + + open spec fn inv(_constant: (), value: V, resource: Self::Resource) -> bool { + >::inv(value, resource) + } +} + +/// A resource invariant that only considers the value. +pub trait ValueInvariant { + // The relation that must hold on the value. + spec fn inv(value: V) -> bool; +} + +impl> SimpleResourceInvariant for T { type Resource = (); - open spec fn inv(_constant: (), _value: V, _resource: ()) -> bool { + open spec fn inv(value: V, _resource: ()) -> bool { + >::inv(value) + } +} + +/// A resource invariant that imposes no condition on the value. +pub struct TrivialResourceInvariant; + +impl ValueInvariant for TrivialResourceInvariant { + open spec fn inv(value: V) -> bool { true } } From 04aaba355673148e73ab0cc0329a20ad36291e8f Mon Sep 17 00:00:00 2001 From: Yuwei LIU <22045841+Marsman1996@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:00:46 +0800 Subject: [PATCH 21/30] ci: skip jobs on cancelled runs (#771) --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f11969f4..67bffcdf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,10 +269,14 @@ jobs: # Runs on every event, parsing a verify leg's log. Push/dispatch: record # to the gh-pages benchmark history and save the baseline. PRs and # comment runs: compare against the last main measurement; only comment - # runs post the comparison as a PR comment. `always()` so a failed - # verification still reports the status. + # runs post the comparison as a PR comment. `always()` keeps a failed + # verification reporting, but skips this job when the verify leg was + # cancelled (superseded by a newer commit that re-runs everything). if: > always() && + !cancelled() && + needs.format-and-verify.result != 'cancelled' && + needs.pr-comment-verify.result != 'cancelled' && (github.event_name != 'issue_comment' || (github.event.issue.pull_request && contains(github.event.comment.body, '/verify-perf') && @@ -461,6 +465,8 @@ jobs: needs: [format-and-verify, perf] if: > always() && + !cancelled() && + needs.format-and-verify.result != 'cancelled' && github.event_name != 'issue_comment' && (github.event_name == 'pull_request' || needs.perf.result == 'success' || From 662ee6f9e48fe1b46d535606a7e565739ed75637 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 16 Sep 2026 13:01:19 +0800 Subject: [PATCH 22/30] chore: update with `OnceImpl` update --- ostd/src/io/io_mem/allocator.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 3767c31ca..49222a054 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MPL-2.0 //! I/O Memory allocator. use crate::specs::arch::PAGE_SIZE; -use crate::sync::{OnceImpl, TrivialPred}; use vstd::{arithmetic::power2::is_pow2, prelude::*}; -use vstd_extra::resource::flags::OneShotSet; +use vstd_extra::{ + once::OnceImpl, resource::flags::OneShotSet, resource_invariant::TrivialResourceInvariant, +}; use alloc::vec::Vec; use core::ops::Range; @@ -325,11 +326,11 @@ pub open spec fn io_mem_range_registered(range: Range) -> bool { <= range.start && range.end <= registered_io_mem_windows()[m].end } -pub exec static IO_MEM_ALLOCATOR: OnceImpl +pub exec static IO_MEM_ALLOCATOR: OnceImpl ensures IO_MEM_ALLOCATOR.wf(), { - OnceImpl::new(Ghost(TrivialPred)) + OnceImpl::new(Ghost(TrivialResourceInvariant)) } } // verus! From 21346a50eed0fca06358d77da012c8c09c5ce9d4 Mon Sep 17 00:00:00 2001 From: Xinyi Wan <64517311+rikosellic@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:37:34 +0800 Subject: [PATCH 23/30] fix: simplify `Range::clone` spec (#772) --- ostd/src/util/ops.rs | 6 +----- verified_libs/vstd_extra/src/external/range.rs | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/ostd/src/util/ops.rs b/ostd/src/util/ops.rs index d37f30114..3e556935c 100644 --- a/ostd/src/util/ops.rs +++ b/ostd/src/util/ops.rs @@ -87,12 +87,8 @@ pub proof fn lemma_range_difference_set(a: Range, b: Ra #[verus_spec(ret => requires obeys_cmp::(), - T::clone.requires((&a.start,)), - T::clone.requires((&a.end,)), - T::clone.requires((&b.start,)), - T::clone.requires((&b.end,)), - forall|x: T, cloned: T| #[trigger] T::clone.ensures((&x,), cloned) ==> cloned == x, finite_range_matches_ord::(), + forall |x: T, y: T| #[trigger] cloned::(x,y) ==> x == y, ensures ret.obeys_prophetic_iter_laws() && ret.will_return_none() ==> { &&& ret.remaining() == range_difference_spec(*a, *b) diff --git a/verified_libs/vstd_extra/src/external/range.rs b/verified_libs/vstd_extra/src/external/range.rs index 4bc1a2f37..c42a6c12e 100644 --- a/verified_libs/vstd_extra/src/external/range.rs +++ b/verified_libs/vstd_extra/src/external/range.rs @@ -7,14 +7,11 @@ use core::ops::{Range, RangeInclusive}; verus! { -/// `Range::clone` clones each field via `Idx::clone`; each field's clone -/// `ensures` (guarded by its `requires`) applies to `res.start`/`res.end`. +/// `Range::clone` clones each field via `Idx::clone`. pub assume_specification[ Range::::clone ](range: &Range) -> (res: Range) ensures - Idx::clone.requires((&range.start,)) && Idx::clone.requires((&range.end,)) ==> { - &&& Idx::clone.ensures((&range.start,), res.start) - &&& Idx::clone.ensures((&range.end,), res.end) - }, + cloned::(range.start, res.start), + cloned::(range.end, res.end), ; /// See [`Range::is_empty`](https://doc.rust-lang.org/std/ops/struct.Range.html#method.is_empty). From 038fe48760666df618f467b90917c1fe7b5fc87a Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 16 Sep 2026 16:54:26 +0800 Subject: [PATCH 24/30] refine: carry PIO allocator's ghost token via the lock's ResourceInvariant --- ostd/src/io/io_port/allocator.rs | 273 +++++++++++++++++++------------ 1 file changed, 169 insertions(+), 104 deletions(-) diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index e7f859c67..3455d44a4 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -7,6 +7,7 @@ use vstd::{ tokens::InstanceId, }; use vstd_extra::ownership::Inv; +use vstd_extra::resource_invariant::ResourceInvariant; use core::ops::Range; @@ -173,7 +174,10 @@ pub(crate) proof fn lemma_alloc_specific_view( } } -/// Representation invariant of `IoPortAllocatorInner`. +/// Relation tying the ghost allocation authority (`IoPortAllocation`) to the executable bitmap. +/// +/// Used as the `ResourceInvariant` of the allocator's lock, so the lock maintains it across +/// lock/unlock instead of a separate bundling struct. pub(crate) open spec fn io_port_inner_inv_values( allocated_instance_id: InstanceId, allocated: Set, @@ -328,14 +332,21 @@ impl IoPortClaim { } } // verus! -/// Lock-protected executable bitmap and the state-machine token that models it. -#[verus_verify] -struct IoPortAllocatorInner { - allocator: ModeledIdAlloc, - #[cfg(verus_keep_ghost_body)] - tracked_allocated: Tracked, +verus! { + +ghost struct IoPortAllocInvariant; + +impl ResourceInvariant for IoPortAllocInvariant { + type Constant = (); + + type Resource = IoPortAllocation; + + closed spec fn inv(_constant: (), alloc: ModeledIdAlloc, r: IoPortAllocation) -> bool { + io_port_inner_inv_values(r.instance_id(), r.value(), &alloc.inner) + } } +} // verus! /// I/O port allocator that allocates port I/O access to device drivers. #[verus_verify] pub(super) struct IoPortAllocator { @@ -343,7 +354,11 @@ pub(super) struct IoPortAllocator { /// /// Instead of using `RangeAllocator` like `IoMemAllocator` does, it is more reasonable to use `IdAlloc`, /// as PIO space includes only a small region; for example, x86 module in OSTD allows just 65536 I/O ports. - allocator: SpinLock, + allocator: SpinLock< + /* Original Rust: IdAlloc */ ModeledIdAlloc, + LocalIrqDisabled, + IoPortAllocInvariant, + >, } #[verus_verify] @@ -380,39 +395,43 @@ impl IoPortAllocator { }; /* debug!("Try to acquire PIO range: {:#x?}", range); */ let mut allocator = self.allocator.lock(); - let allocator_inner = &mut *allocator; + proof_decl! { + let ghost instance_id = allocator.resource().instance_id(); + let ghost preserved = allocator.resource().value(); + } proof! { - lemma_io_port_alloc_init(&*allocator_inner); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value().inner)); } + let allocator_inner = &mut *allocator; // `Iterator::any` with a capturing closure is not supported by Verus. // Original Rust: // if range.any(|i| allocator.is_allocated(i as usize)) { return None; } let mut already_allocated = false; #[verus_spec(scan_iter => invariant - allocator_inner.allocator.inner.inv(), - allocator_inner.allocator.inner@.len() + allocator_inner.inner.inv(), + allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int, !already_allocated ==> forall|id: usize| range.start as usize <= id < (range.start as int + scan_iter.index()) as usize ==> - !id_alloc_view(&allocator_inner.allocator.inner).contains(id), + !id_alloc_view(&allocator_inner.inner).contains(id), )] for i in range.clone() { proof! { - assert((i as usize) < allocator_inner.allocator.inner@.len()) by { + assert((i as usize) < allocator_inner.inner@.len()) by { assert((i as usize) < range.end as usize); assert((range.end as usize) <= u16::MAX as usize); assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); - assert(allocator_inner.allocator.inner@.len() + assert(allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int); } } - if allocator_inner.allocator.inner.is_allocated(i as usize) { + if allocator_inner.inner.is_allocated(i as usize) { already_allocated = true; } proof! { - lemma_id_alloc_view_contains(&allocator_inner.allocator.inner, i as usize); + lemma_id_alloc_view_contains(&allocator_inner.inner, i as usize); } } proof_decl! { @@ -428,12 +447,12 @@ impl IoPortAllocator { proof_decl! { let ghost ids = port_id_set(range.start as usize, range.end as usize); - let ghost allocation_start_view = id_alloc_view(&allocator_inner.allocator.inner); + let ghost allocation_start_view = id_alloc_view(&allocator_inner.inner); } proof! { - assert(ids.disjoint(id_alloc_view(&allocator_inner.allocator.inner))) by { + assert(ids.disjoint(id_alloc_view(&allocator_inner.inner))) by { assert forall|id: usize| #[trigger] ids.contains(id) implies - !id_alloc_view(&allocator_inner.allocator.inner).contains(id) by { + !id_alloc_view(&allocator_inner.inner).contains(id) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -442,7 +461,7 @@ impl IoPortAllocator { } } assert forall|id: usize| ids.contains(id) implies - id < id_alloc_capacity(&allocator_inner.allocator.inner) by { + id < id_alloc_capacity(&allocator_inner.inner) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -451,7 +470,7 @@ impl IoPortAllocator { } assert forall|id: usize| range.start as usize <= id < range.end as usize implies - !id_alloc_view(&allocator_inner.allocator.inner).contains(id) by { + !id_alloc_view(&allocator_inner.inner).contains(id) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -461,19 +480,16 @@ impl IoPortAllocator { } #[verus_spec(allocation_iter => invariant - allocator_inner.allocator.inner.inv(), - allocator_inner.allocator.inner@.len() + allocator_inner.inner.inv(), + allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int, - range.end as usize <= id_alloc_capacity(&allocator_inner.allocator.inner), - allocator_inner.tracked_allocated@.instance_id() == - io_port_allocator_instance_id(), - allocator_inner.tracked_allocated@.value().subset_of(allocation_start_view), - allocator_inner.tracked_allocated@.value().subset_of( - id_alloc_view(&allocator_inner.allocator.inner), - ), - id_alloc_capacity(&allocator_inner.allocator.inner) == + range.end as usize <= id_alloc_capacity(&allocator_inner.inner), + instance_id == io_port_allocator_instance_id(), + preserved.subset_of(allocation_start_view), + preserved.subset_of(id_alloc_view(&allocator_inner.inner)), + id_alloc_capacity(&allocator_inner.inner) == crate::arch::io::MAX_IO_PORT as usize, - id_alloc_view(&allocator_inner.allocator.inner) == + id_alloc_view(&allocator_inner.inner) == allocation_start_view.union( port_id_set( range.start as usize, @@ -483,55 +499,60 @@ impl IoPortAllocator { forall|id: usize| (range.start as int + allocation_iter.index()) as usize <= id < range.end as usize ==> - !id_alloc_view(&allocator_inner.allocator.inner).contains(id), + !id_alloc_view(&allocator_inner.inner).contains(id), )] for i in range.clone() { proof_decl! { - let ghost old_view = id_alloc_view(&allocator_inner.allocator.inner); + let ghost old_view = id_alloc_view(&allocator_inner.inner); } proof! { assert((i as usize) == (range.start as int + allocation_iter.index()) as usize); - assert((i as usize) < id_alloc_capacity(&allocator_inner.allocator.inner)); - assert(!id_alloc_view(&allocator_inner.allocator.inner).contains(i as usize)); - assert(allocator_inner.tracked_allocated@.value().subset_of( - old_view.insert(i as usize), - )) by { + assert((i as usize) < id_alloc_capacity(&allocator_inner.inner)); + assert(!id_alloc_view(&allocator_inner.inner).contains(i as usize)); + assert(preserved.subset_of(old_view.insert(i as usize))) by { assert forall|id: usize| - allocator_inner.tracked_allocated@.value().contains(id) implies + preserved.contains(id) implies old_view.insert(i as usize).contains(id) by { } } - assert(io_port_inner_inv_values( - allocator_inner.tracked_allocated@.instance_id(), - allocator_inner.tracked_allocated@.value(), - &allocator_inner.allocator.inner, - )); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner.inner)); } #[verus_spec(with - Ghost(allocator_inner.tracked_allocated@.instance_id()), - Ghost(allocator_inner.tracked_allocated@.value()), + Ghost(instance_id), + Ghost(preserved), )] /* Original Rust: allocator.alloc_specific(i as usize); */ - let _ = allocator_inner.allocator.alloc_specific(i as usize); + let _ = allocator_inner.alloc_specific(i as usize); proof! { lemma_port_id_set_insert(range.start as usize, i as usize); - assert(id_alloc_view(&allocator_inner.allocator.inner) == + assert(id_alloc_view(&allocator_inner.inner) == old_view.insert(i as usize)); } } proof! { - assert(ids.disjoint(allocator_inner.tracked_allocated@.value())) by { + assert(ids.disjoint(preserved)) by { assert forall|id: usize| #[trigger] ids.contains(id) implies - !allocator_inner.tracked_allocated@.value().contains(id) by { + !preserved.contains(id) by { } } - range_claim = allocator_inner.tracked_allocated.borrow_mut().allocate(ids); + assert(preserved.disjoint(ids)); + range_claim = allocator.tracked_borrow_mut_resource().allocate(ids); } // SAFETY: The created `IoPort` is guaranteed not to access system device I/O. /* Original Rust: unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) } */ let result = unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) }; + proof! { + assert(id_alloc_view(&allocator.value().inner) == allocation_start_view.union(ids)); + assert(preserved.subset_of(allocation_start_view)); + assert(allocator.resource().value() == preserved.union(ids)); + assert(io_port_inner_inv_values( + allocator.resource().instance_id(), + allocator.resource().value(), + &allocator.value().inner, + )); + } allocator.drop(); proof! { *claim_out = Tracked(Some(range_claim)); @@ -562,26 +583,49 @@ impl IoPortAllocator { */ let mut allocator = self.allocator.lock(); - let allocator_inner = &mut *allocator; + proof_decl! { + let ghost instance_id: InstanceId; + let ghost preserved: Set; + let ghost bitmap_view: Set; + let ghost token_released: Set; + let ghost pre_seq: Seq; + let ghost pre_len: int; + } proof! { - lemma_io_port_alloc_init(&*allocator_inner); assert(range.start as usize <= range.end as usize); - assert(claim.instance_id() == allocator_inner.tracked_allocated@.instance_id()); - allocator_inner.tracked_allocated.borrow().claim_includes(&claim); - assert(port_id_set(range.start as usize, range.end as usize) - <= allocator_inner.tracked_allocated@.value()); - assert(port_id_set(range.start as usize, range.end as usize) - <= id_alloc_view(&allocator_inner.allocator.inner)); - assert((range.end as usize) <= allocator_inner.allocator.inner@.len()) by { + instance_id = allocator.resource().instance_id(); + preserved = allocator.resource().value(); + pre_seq = allocator.value().inner@; + pre_len = allocator.value().inner@.len() as int; + bitmap_view = id_alloc_view(&allocator.value().inner); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value().inner)); + assert(preserved.subset_of(bitmap_view)); + assert(bitmap_view == id_alloc_bits(pre_seq, pre_len)); + assert(pre_len == crate::arch::io::MAX_IO_PORT as int); + assert(crate::arch::io::MAX_IO_PORT as int <= usize::MAX as int); + assert(pre_len <= usize::MAX as int); + assert(claim.instance_id() == instance_id); + assert((range.end as usize) <= allocator.value().inner@.len()) by { assert((range.end as usize) <= u16::MAX as usize); assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); - assert(allocator_inner.allocator.inner@.len() + assert(allocator.value().inner@.len() == crate::arch::io::MAX_IO_PORT as int); } - allocator_inner.tracked_allocated.borrow_mut().release(claim); + } + proof! { + let tracked r = allocator.tracked_borrow_mut_resource(); + r.claim_includes(&claim); + assert(port_id_set(range.start as usize, range.end as usize) <= r.value()); + assert(r.value() == preserved); + assert(port_id_set(range.start as usize, range.end as usize) <= preserved); + r.release(claim); + token_released = r.value(); + assert(token_released + == preserved.difference(port_id_set(range.start as usize, range.end as usize))); assert forall|id: usize| - #[trigger] allocator_inner.tracked_allocated@.value().contains(id) implies { - &&& id_alloc_view(&allocator_inner.allocator.inner).contains(id) + #![trigger token_released.contains(id)] + token_released.contains(id) implies { + &&& bitmap_view.contains(id) &&& !(range.start as usize <= id < range.end as usize) } by { lemma_port_id_set_contains( @@ -590,32 +634,73 @@ impl IoPortAllocator { id, ); } + } + let allocator_inner = &mut *allocator; + /* Original Rust: + self.allocator + .lock() + .free_consecutive(range.start as usize..range.end as usize); + */ + proof! { + assert(allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert(id_alloc_capacity(&allocator_inner.inner) + == crate::arch::io::MAX_IO_PORT as usize); + assert(allocator_inner.inner@ == pre_seq); + assert((range.end as usize) <= id_alloc_capacity(&allocator_inner.inner)); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner.inner)); + assert(id_alloc_view(&allocator_inner.inner) == bitmap_view); assert forall|i: int| range.start as usize <= i - && i < allocator_inner.allocator.inner@.len() + && i < allocator_inner.inner@.len() && i < range.end as usize implies - allocator_inner.allocator.inner@[i] + allocator_inner.inner@[i] by { - assert(0 <= i); - assert(i < allocator_inner.allocator.inner@.len()); - assert(allocator_inner.allocator.inner@.len() <= usize::MAX as int); - assert(i <= usize::MAX as int); - assert((i as usize) as int == i); + assert(port_id_set(range.start as usize, range.end as usize) <= preserved); + assert(preserved.subset_of(bitmap_view)); + assert(bitmap_view == id_alloc_view(&allocator_inner.inner)); lemma_port_id_set_contains( range.start as usize, range.end as usize, i as usize, ); - assert(port_id_set(range.start as usize, range.end as usize).contains(i as usize)); - assert(id_alloc_view(&allocator_inner.allocator.inner).contains(i as usize)); - lemma_id_alloc_view_contains(&allocator_inner.allocator.inner, i as usize); - assert(allocator_inner.allocator.inner@[i]); + lemma_id_alloc_view_contains(&allocator_inner.inner, i as usize); } } allocator_inner - .allocator .inner .free_consecutive(range.start as usize..range.end as usize); + proof! { + assert(allocator.value().inner == allocator_inner.inner); + let final_llen: int = allocator_inner.inner@.len() as int; + assert(final_llen == pre_len); + assert(final_llen <= usize::MAX as int); + assert forall|id: usize| + #![trigger allocator.resource().value().contains(id)] + allocator.resource().value().contains(id) implies + id_alloc_view(&allocator_inner.inner).contains(id) by { + lemma_port_id_set_contains(range.start as usize, range.end as usize, id); + lemma_id_alloc_bits_char(pre_seq, pre_len, id); + lemma_id_alloc_bits_char(allocator_inner.inner@, final_llen, id); + assert(allocator.resource().value().contains(id) + == (preserved.contains(id) + && !port_id_set(range.start as usize, range.end as usize).contains(id))); + assert(port_id_set(range.start as usize, range.end as usize) <= preserved); + assert(preserved.subset_of(bitmap_view)); + assert(bitmap_view == id_alloc_bits(pre_seq, pre_len)); + assert(id_alloc_view(&allocator_inner.inner) + == id_alloc_bits(allocator_inner.inner@, final_llen)); + } + assert(allocator_inner.inner.inv()); + assert(allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert(id_alloc_capacity(&allocator_inner.inner) + == crate::arch::io::MAX_IO_PORT as usize); + assert(allocator.resource().instance_id() == instance_id); + assert(io_port_inner_inv_values( + allocator.resource().instance_id(), + allocator.resource().value(), + &allocator_inner.inner, + )); + } allocator.drop(); } } @@ -628,20 +713,6 @@ verus! { /// explicit specification boundary for the architecture's guarantee that [`init`] ran first. pub uninterp spec fn io_port_allocator_initialized() -> bool; -/// Trusted: once `init` has run, the global allocator's inner satisfies its invariant. -#[verifier::external_body] -proof fn lemma_io_port_alloc_init(inner: &IoPortAllocatorInner) - requires - io_port_allocator_initialized(), - ensures - io_port_inner_inv_values( - inner.tracked_allocated@.instance_id(), - inner.tracked_allocated@.value(), - &inner.allocator.inner, - ), -{ -} - } // verus! pub(super) static IO_PORT_ALLOCATOR: Once = Once::new(); @@ -689,17 +760,11 @@ pub(in crate::io) unsafe fn init() { IO_PORT_ALLOCATOR.call_once(|| IoPortAllocator { allocator: SpinLock::new(allocator), }); */ - IO_PORT_ALLOCATOR.call_once(|| { - proof_decl! { - let tracked allocated = IoPortAllocation::initialize(); - } - let inner = IoPortAllocatorInner { - allocator: ModeledIdAlloc { inner: allocator }, - #[cfg(verus_keep_ghost_body)] - tracked_allocated: Tracked::new(allocated), - }; - IoPortAllocator { - allocator: SpinLock::new(inner, Ghost::new(()), Tracked::new(())), - } + IO_PORT_ALLOCATOR.call_once(|| IoPortAllocator { + allocator: SpinLock::new( + ModeledIdAlloc { inner: allocator }, + Ghost::new(()), + Tracked::new(IoPortAllocation::initialize()), + ), }); } From afdbf4462206f4eba236a88eaf947ca612298b82 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 16 Sep 2026 17:55:55 +0800 Subject: [PATCH 25/30] refine: remove modeled id alloc wrapper --- ostd/src/io/io_port/allocator.rs | 287 +++++++++---------------------- 1 file changed, 85 insertions(+), 202 deletions(-) diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index 3455d44a4..c73d67ace 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -102,82 +102,6 @@ pub(crate) proof fn lemma_id_alloc_view_contains(allocator: &IdAlloc, id: usize) lemma_id_alloc_bits_char(allocator@, allocator@.len() as int, id); } -/// Derives the set-level postcondition of `alloc_specific` from its bitmap postcondition. -pub(crate) proof fn lemma_alloc_specific_view( - old_a: &IdAlloc, - final_a: &IdAlloc, - id: usize, - res: Option, -) - requires - old_a.inv(), - final_a.inv(), - (id as int) < old_a@.len(), - old_a@.len() <= usize::MAX as int, - final_a@.len() == old_a@.len(), - res is None ==> old_a@[id as int] && final_a@ == old_a@, - res is Some ==> final_a@ == old_a@.update(id as int, true) && !old_a@[id as int], - res is Some ==> res == Some(id), - ensures - id_alloc_view(final_a) == id_alloc_view(old_a).insert(id), - id_alloc_view(old_a).subset_of(id_alloc_view(final_a)), - id_alloc_view(old_a).contains(id) ==> res is None, - !id_alloc_view(old_a).contains(id) ==> res == Some(id), -{ - let old_len: int = old_a@.len() as int; - assert forall|j: usize| - #![trigger id_alloc_view(final_a).contains(j)] - id_alloc_view(final_a).contains(j) == (id_alloc_view(old_a).insert(id)).contains(j) by { - lemma_id_alloc_bits_char(old_a@, old_len, j); - lemma_id_alloc_bits_char(final_a@, final_a@.len() as int, j); - if res is Some { - assert forall|jj: int| - #![trigger final_a@[jj]] - final_a@[jj] == (if jj == id as int { - true - } else { - old_a@[jj] - }) by { - assert(final_a@ == old_a@.update(id as int, true)); - } - } else { - assert(final_a@ == old_a@); - assert(old_a@[id as int]); - } - } - assert forall|j: usize| - #![trigger id_alloc_view(old_a).contains(j)] - id_alloc_view(old_a).contains(j) implies id_alloc_view(final_a).contains(j) by { - lemma_id_alloc_bits_char(old_a@, old_len, j); - lemma_id_alloc_bits_char(final_a@, final_a@.len() as int, j); - if res is Some { - assert forall|jj: int| - #![trigger final_a@[jj]] - final_a@[jj] == (if jj == id as int { - true - } else { - old_a@[jj] - }) by { - assert(final_a@ == old_a@.update(id as int, true)); - } - } else { - assert(final_a@ == old_a@); - } - } - lemma_id_alloc_bits_char(old_a@, old_len, id); - assert(id_alloc_view(old_a).contains(id) == old_a@[id as int]); - if res is Some { - assert(!old_a@[id as int]); - assert(res == Some(id)); - } else { - assert(old_a@[id as int]); - } -} - -/// Relation tying the ghost allocation authority (`IoPortAllocation`) to the executable bitmap. -/// -/// Used as the `ResourceInvariant` of the allocator's lock, so the lock maintains it across -/// lock/unlock instead of a separate bundling struct. pub(crate) open spec fn io_port_inner_inv_values( allocated_instance_id: InstanceId, allocated: Set, @@ -191,60 +115,6 @@ pub(crate) open spec fn io_port_inner_inv_values( } } // verus! -/// Transparent facade used only to pass ghost frame facts to the third-party mutation. -#[repr(transparent)] -#[verus_verify] -struct ModeledIdAlloc { - inner: IdAlloc, -} - -#[verus_verify] -impl ModeledIdAlloc { - #[verus_spec(result => - with - Ghost(allocated_instance_id): Ghost, - Ghost(preserved): Ghost>, - requires - id < id_alloc_capacity(&old(self).inner), - io_port_inner_inv_values(allocated_instance_id, preserved, &old(self).inner), - ensures - final(self).inner.inv(), - final(self).inner@.len() == crate::arch::io::MAX_IO_PORT as int, - id_alloc_capacity(&final(self).inner) == id_alloc_capacity(&old(self).inner), - id_alloc_view(&final(self).inner) == id_alloc_view(&old(self).inner).insert(id), - id_alloc_view(&old(self).inner).subset_of(id_alloc_view(&final(self).inner)), - preserved.subset_of(id_alloc_view(&final(self).inner)), - io_port_inner_inv_values( - allocated_instance_id, - preserved, - &final(self).inner, - ), - id_alloc_view(&old(self).inner).contains(id) ==> result is None, - !id_alloc_view(&old(self).inner).contains(id) ==> result == Some(id), - )] - fn alloc_specific(&mut self, id: usize) -> Option { - proof! { - assert(id < id_alloc_capacity(&old(self).inner)); - assert(id_alloc_capacity(&old(self).inner) - == crate::arch::io::MAX_IO_PORT as usize); - assert(old(self).inner@.len() == crate::arch::io::MAX_IO_PORT as int); - assert((id as int) < old(self).inner@.len()); - } - let res = self.inner.alloc_specific(id); - proof! { - assert(self.inner@.len() == old(self).inner@.len()); - assert(self.inner@.len() == crate::arch::io::MAX_IO_PORT as int); - assert(self.inner.inv()); - assert(old(self).inner@.len() <= usize::MAX as int); - lemma_alloc_specific_view(&old(self).inner, &self.inner, id, res); - assert(id_alloc_capacity(&self.inner) == id_alloc_capacity(&old(self).inner)); - assert(preserved.subset_of(id_alloc_view(&self.inner))); - assert(io_port_inner_inv_values(allocated_instance_id, preserved, &self.inner)); - } - res - } -} - verus! { /// Authority over the set of PIO ids currently allocated by the global allocator. @@ -336,13 +206,13 @@ verus! { ghost struct IoPortAllocInvariant; -impl ResourceInvariant for IoPortAllocInvariant { +impl ResourceInvariant for IoPortAllocInvariant { type Constant = (); type Resource = IoPortAllocation; - closed spec fn inv(_constant: (), alloc: ModeledIdAlloc, r: IoPortAllocation) -> bool { - io_port_inner_inv_values(r.instance_id(), r.value(), &alloc.inner) + closed spec fn inv(_constant: (), alloc: IdAlloc, r: IoPortAllocation) -> bool { + io_port_inner_inv_values(r.instance_id(), r.value(), &alloc) } } @@ -354,11 +224,7 @@ pub(super) struct IoPortAllocator { /// /// Instead of using `RangeAllocator` like `IoMemAllocator` does, it is more reasonable to use `IdAlloc`, /// as PIO space includes only a small region; for example, x86 module in OSTD allows just 65536 I/O ports. - allocator: SpinLock< - /* Original Rust: IdAlloc */ ModeledIdAlloc, - LocalIrqDisabled, - IoPortAllocInvariant, - >, + allocator: SpinLock, } #[verus_verify] @@ -400,7 +266,7 @@ impl IoPortAllocator { let ghost preserved = allocator.resource().value(); } proof! { - assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value().inner)); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value())); } let allocator_inner = &mut *allocator; // `Iterator::any` with a capturing closure is not supported by Verus. @@ -409,29 +275,29 @@ impl IoPortAllocator { let mut already_allocated = false; #[verus_spec(scan_iter => invariant - allocator_inner.inner.inv(), - allocator_inner.inner@.len() + allocator_inner.inv(), + allocator_inner@.len() == crate::arch::io::MAX_IO_PORT as int, !already_allocated ==> forall|id: usize| range.start as usize <= id < (range.start as int + scan_iter.index()) as usize ==> - !id_alloc_view(&allocator_inner.inner).contains(id), + !id_alloc_view(&allocator_inner).contains(id), )] for i in range.clone() { proof! { - assert((i as usize) < allocator_inner.inner@.len()) by { + assert((i as usize) < allocator_inner@.len()) by { assert((i as usize) < range.end as usize); assert((range.end as usize) <= u16::MAX as usize); assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); - assert(allocator_inner.inner@.len() + assert(allocator_inner@.len() == crate::arch::io::MAX_IO_PORT as int); } } - if allocator_inner.inner.is_allocated(i as usize) { + if allocator_inner.is_allocated(i as usize) { already_allocated = true; } proof! { - lemma_id_alloc_view_contains(&allocator_inner.inner, i as usize); + lemma_id_alloc_view_contains(&allocator_inner, i as usize); } } proof_decl! { @@ -447,12 +313,12 @@ impl IoPortAllocator { proof_decl! { let ghost ids = port_id_set(range.start as usize, range.end as usize); - let ghost allocation_start_view = id_alloc_view(&allocator_inner.inner); + let ghost allocation_start_view = id_alloc_view(&allocator_inner); } proof! { - assert(ids.disjoint(id_alloc_view(&allocator_inner.inner))) by { + assert(ids.disjoint(id_alloc_view(&allocator_inner))) by { assert forall|id: usize| #[trigger] ids.contains(id) implies - !id_alloc_view(&allocator_inner.inner).contains(id) by { + !id_alloc_view(&allocator_inner).contains(id) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -461,7 +327,7 @@ impl IoPortAllocator { } } assert forall|id: usize| ids.contains(id) implies - id < id_alloc_capacity(&allocator_inner.inner) by { + id < id_alloc_capacity(&allocator_inner) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -470,7 +336,7 @@ impl IoPortAllocator { } assert forall|id: usize| range.start as usize <= id < range.end as usize implies - !id_alloc_view(&allocator_inner.inner).contains(id) by { + !id_alloc_view(&allocator_inner).contains(id) by { lemma_port_id_set_contains( range.start as usize, range.end as usize, @@ -480,16 +346,16 @@ impl IoPortAllocator { } #[verus_spec(allocation_iter => invariant - allocator_inner.inner.inv(), - allocator_inner.inner@.len() + allocator_inner.inv(), + allocator_inner@.len() == crate::arch::io::MAX_IO_PORT as int, - range.end as usize <= id_alloc_capacity(&allocator_inner.inner), + range.end as usize <= id_alloc_capacity(&allocator_inner), instance_id == io_port_allocator_instance_id(), preserved.subset_of(allocation_start_view), - preserved.subset_of(id_alloc_view(&allocator_inner.inner)), - id_alloc_capacity(&allocator_inner.inner) == + preserved.subset_of(id_alloc_view(&allocator_inner)), + id_alloc_capacity(&allocator_inner) == crate::arch::io::MAX_IO_PORT as usize, - id_alloc_view(&allocator_inner.inner) == + id_alloc_view(&allocator_inner) == allocation_start_view.union( port_id_set( range.start as usize, @@ -499,35 +365,54 @@ impl IoPortAllocator { forall|id: usize| (range.start as int + allocation_iter.index()) as usize <= id < range.end as usize ==> - !id_alloc_view(&allocator_inner.inner).contains(id), + !id_alloc_view(&allocator_inner).contains(id), )] for i in range.clone() { proof_decl! { - let ghost old_view = id_alloc_view(&allocator_inner.inner); + let ghost old_view = id_alloc_view(&allocator_inner); + let ghost old_seq = allocator_inner@; + let ghost old_len = (allocator_inner@.len()) as int; } proof! { assert((i as usize) == (range.start as int + allocation_iter.index()) as usize); - assert((i as usize) < id_alloc_capacity(&allocator_inner.inner)); - assert(!id_alloc_view(&allocator_inner.inner).contains(i as usize)); + assert((i as usize) < id_alloc_capacity(&allocator_inner)); + assert(allocator_inner.inv()); + assert(!id_alloc_view(&allocator_inner).contains(i as usize)); assert(preserved.subset_of(old_view.insert(i as usize))) by { assert forall|id: usize| preserved.contains(id) implies old_view.insert(i as usize).contains(id) by { } } - assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner.inner)); } - #[verus_spec(with - Ghost(instance_id), - Ghost(preserved), - )] /* Original Rust: allocator.alloc_specific(i as usize); */ - let _ = allocator_inner.alloc_specific(i as usize); + let _res = allocator_inner.alloc_specific(i as usize); proof! { - lemma_port_id_set_insert(range.start as usize, i as usize); - assert(id_alloc_view(&allocator_inner.inner) == - old_view.insert(i as usize)); + assert((allocator_inner@.len()) as int == old_len); + assert(id_alloc_view(&allocator_inner) == old_view.insert(i as usize)) by { + lemma_port_id_set_insert(range.start as usize, i as usize); + assert forall|j: usize| + #![trigger id_alloc_view(&allocator_inner).contains(j)] + id_alloc_view(&allocator_inner).contains(j) + == old_view.insert(i as usize).contains(j) by { + lemma_id_alloc_bits_char(old_seq, old_len, j); + lemma_id_alloc_bits_char(allocator_inner@, old_len, j); + if _res is Some { + assert(allocator_inner@ == old_seq.update(i as int, true)); + assert(allocator_inner@[j as int] == (if (j as int) == (i as int) { + true + } else { + old_seq[j as int] + })); + } else { + assert(old_seq[i as int]); + assert(allocator_inner@ == old_seq); + } + } + } + assert(preserved.subset_of(id_alloc_view(&allocator_inner))); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner)); } } proof! { @@ -544,13 +429,13 @@ impl IoPortAllocator { /* Original Rust: unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) } */ let result = unsafe { Some(IoPort::new_overlapping(port, is_overlapping)) }; proof! { - assert(id_alloc_view(&allocator.value().inner) == allocation_start_view.union(ids)); + assert(id_alloc_view(&allocator.value()) == allocation_start_view.union(ids)); assert(preserved.subset_of(allocation_start_view)); assert(allocator.resource().value() == preserved.union(ids)); assert(io_port_inner_inv_values( allocator.resource().instance_id(), allocator.resource().value(), - &allocator.value().inner, + &allocator.value(), )); } allocator.drop(); @@ -595,20 +480,20 @@ impl IoPortAllocator { assert(range.start as usize <= range.end as usize); instance_id = allocator.resource().instance_id(); preserved = allocator.resource().value(); - pre_seq = allocator.value().inner@; - pre_len = allocator.value().inner@.len() as int; - bitmap_view = id_alloc_view(&allocator.value().inner); - assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value().inner)); + pre_seq = allocator.value()@; + pre_len = allocator.value()@.len() as int; + bitmap_view = id_alloc_view(&allocator.value()); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator.value())); assert(preserved.subset_of(bitmap_view)); assert(bitmap_view == id_alloc_bits(pre_seq, pre_len)); assert(pre_len == crate::arch::io::MAX_IO_PORT as int); assert(crate::arch::io::MAX_IO_PORT as int <= usize::MAX as int); assert(pre_len <= usize::MAX as int); assert(claim.instance_id() == instance_id); - assert((range.end as usize) <= allocator.value().inner@.len()) by { + assert((range.end as usize) <= allocator.value()@.len()) by { assert((range.end as usize) <= u16::MAX as usize); assert((u16::MAX as usize) <= crate::arch::io::MAX_IO_PORT as usize); - assert(allocator.value().inner@.len() + assert(allocator.value()@.len() == crate::arch::io::MAX_IO_PORT as int); } } @@ -642,63 +527,61 @@ impl IoPortAllocator { .free_consecutive(range.start as usize..range.end as usize); */ proof! { - assert(allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int); - assert(id_alloc_capacity(&allocator_inner.inner) + assert(allocator_inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert(id_alloc_capacity(&allocator_inner) == crate::arch::io::MAX_IO_PORT as usize); - assert(allocator_inner.inner@ == pre_seq); - assert((range.end as usize) <= id_alloc_capacity(&allocator_inner.inner)); - assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner.inner)); - assert(id_alloc_view(&allocator_inner.inner) == bitmap_view); + assert(allocator_inner@ == pre_seq); + assert((range.end as usize) <= id_alloc_capacity(&allocator_inner)); + assert(io_port_inner_inv_values(instance_id, preserved, &allocator_inner)); + assert(id_alloc_view(&allocator_inner) == bitmap_view); assert forall|i: int| range.start as usize <= i - && i < allocator_inner.inner@.len() + && i < allocator_inner@.len() && i < range.end as usize implies - allocator_inner.inner@[i] + allocator_inner@[i] by { assert(port_id_set(range.start as usize, range.end as usize) <= preserved); assert(preserved.subset_of(bitmap_view)); - assert(bitmap_view == id_alloc_view(&allocator_inner.inner)); + assert(bitmap_view == id_alloc_view(&allocator_inner)); lemma_port_id_set_contains( range.start as usize, range.end as usize, i as usize, ); - lemma_id_alloc_view_contains(&allocator_inner.inner, i as usize); + lemma_id_alloc_view_contains(&allocator_inner, i as usize); } } - allocator_inner - .inner - .free_consecutive(range.start as usize..range.end as usize); + allocator_inner.free_consecutive(range.start as usize..range.end as usize); proof! { - assert(allocator.value().inner == allocator_inner.inner); - let final_llen: int = allocator_inner.inner@.len() as int; + assert(allocator.value() == *allocator_inner); + let final_llen: int = allocator_inner@.len() as int; assert(final_llen == pre_len); assert(final_llen <= usize::MAX as int); assert forall|id: usize| #![trigger allocator.resource().value().contains(id)] allocator.resource().value().contains(id) implies - id_alloc_view(&allocator_inner.inner).contains(id) by { + id_alloc_view(&allocator_inner).contains(id) by { lemma_port_id_set_contains(range.start as usize, range.end as usize, id); lemma_id_alloc_bits_char(pre_seq, pre_len, id); - lemma_id_alloc_bits_char(allocator_inner.inner@, final_llen, id); + lemma_id_alloc_bits_char(allocator_inner@, final_llen, id); assert(allocator.resource().value().contains(id) == (preserved.contains(id) && !port_id_set(range.start as usize, range.end as usize).contains(id))); assert(port_id_set(range.start as usize, range.end as usize) <= preserved); assert(preserved.subset_of(bitmap_view)); assert(bitmap_view == id_alloc_bits(pre_seq, pre_len)); - assert(id_alloc_view(&allocator_inner.inner) - == id_alloc_bits(allocator_inner.inner@, final_llen)); + assert(id_alloc_view(&allocator_inner) + == id_alloc_bits(allocator_inner@, final_llen)); } - assert(allocator_inner.inner.inv()); - assert(allocator_inner.inner@.len() == crate::arch::io::MAX_IO_PORT as int); - assert(id_alloc_capacity(&allocator_inner.inner) + assert(allocator_inner.inv()); + assert(allocator_inner@.len() == crate::arch::io::MAX_IO_PORT as int); + assert(id_alloc_capacity(&allocator_inner) == crate::arch::io::MAX_IO_PORT as usize); assert(allocator.resource().instance_id() == instance_id); assert(io_port_inner_inv_values( allocator.resource().instance_id(), allocator.resource().value(), - &allocator_inner.inner, + &allocator_inner, )); } allocator.drop(); @@ -762,7 +645,7 @@ pub(in crate::io) unsafe fn init() { }); */ IO_PORT_ALLOCATOR.call_once(|| IoPortAllocator { allocator: SpinLock::new( - ModeledIdAlloc { inner: allocator }, + allocator, Ghost::new(()), Tracked::new(IoPortAllocation::initialize()), ), From 9cec4bee109bcda2971f8f3b9e1d6c321892a49e Mon Sep 17 00:00:00 2001 From: Yuwei LIU <22045841+Marsman1996@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:18:49 +0800 Subject: [PATCH 26/30] docs(coding-guidelines): add avoid-redundant-as-int-casts guideline (#773) * docs(coding-guidelines): add avoid-redundant-as-int-casts guideline * refine --- docs/coding-guidelines/README.md | 1 + docs/coding-guidelines/maintainability.md | 49 +++++++++++++++++++-- docs/coding-guidelines/proof-engineering.md | 32 ++++++++++++-- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/docs/coding-guidelines/README.md b/docs/coding-guidelines/README.md index 58059ed1c..64f106162 100644 --- a/docs/coding-guidelines/README.md +++ b/docs/coding-guidelines/README.md @@ -25,6 +25,7 @@ Reference guidelines in reviews by their stable kebab-case names. - [`separate-verus-modes`](maintainability.md#separate-verus-modes) — keep executable code, specifications, and proofs visually distinct. - [`use-chained-comparisons`](maintainability.md#use-chained-comparisons) — express contiguous bounds as one logically equivalent chained comparison. - [`use-returns-for-exact-results`](maintainability.md#use-returns-for-exact-results) — express exact return values with `returns` and remove unused return binders. +- [`avoid-redundant-as-int-casts`](maintainability.md#avoid-redundant-as-int-casts) — drop `as int` where Verus auto-coerces comparisons and arithmetic, but keep it at spec `int` parameters and standalone `/` divisors. - [`organize-proof-imports`](maintainability.md#organize-proof-imports) — import proof symbols concisely while keeping proof-only dependencies visible and `reveal` calls minimal. - [`group-imports-by-crate`](maintainability.md#group-imports-by-crate) — combine definitions imported from the same crate into one `use` group. - [`bind-option-payloads`](maintainability.md#bind-option-payloads) — bind a shared `Some` payload once instead of repeating implications and projections. diff --git a/docs/coding-guidelines/maintainability.md b/docs/coding-guidelines/maintainability.md index 9e75bb846..b135b22d7 100644 --- a/docs/coding-guidelines/maintainability.md +++ b/docs/coding-guidelines/maintainability.md @@ -35,8 +35,8 @@ form a chain. Use `returns expr` for an exact return value and `ensures` for other result or -state properties. Omit unused named return binders and unit return declarations -such as `-> (ret: ())`. +state properties. This also applies to `assume_specification`. Omit unused named +return binders and unit return declarations such as `-> (ret: ())`. The expression must match the return type. Keep required casts, such as `as usize` for a sequence's `nat` length, and justify that the value fits. @@ -44,8 +44,49 @@ The expression must match the return type. Keep required casts, such as See also: PR [#742](https://github.com/asterinas/vostd/pull/742#discussion_r3940933539), [#742](https://github.com/asterinas/vostd/pull/742#discussion_r3946290469), [#742](https://github.com/asterinas/vostd/pull/742#discussion_r3946316792), -[#742](https://github.com/asterinas/vostd/pull/742#discussion_r3946322891), and -[#742](https://github.com/asterinas/vostd/pull/742#discussion_r3947067359). +[#742](https://github.com/asterinas/vostd/pull/742#discussion_r3946322891), +[#742](https://github.com/asterinas/vostd/pull/742#discussion_r3947067359), and +[#770](https://github.com/asterinas/vostd/pull/770#discussion_r4023879416). + +### Avoid redundant `as int` casts + + + +Drop `as int` where Verus auto-coerces integer types in ghost code, but keep it +where a coercion is genuinely required. Rely on Verus, then keep exactly the +casts the compiler asks for, rather than casting defensively everywhere or +stripping blindly — both create review friction. + +Verus compares values of different integer types in ghost code directly, and +the same auto-coercion reaches `*`, `+`, `-` once any operand or literal is +`int`/`nat` (see the [Verus integer guide](../../tools/verus/source/docs/guide/src/integers.md) +on comparisons and `as` coercion). So these casts are redundant: + +```rust +// Prefer this: +len == smallvec_view(v).len(), +2 * new_len * size_of::() <= isize::MAX, +r == (self_ + rhs - 1) / (rhs as int), + +// Over this: +(len as int) == smallvec_view(v).len(), +2 * (new_len as int) * (size_of::() as int) <= isize::MAX as int, +(r as int) == ((self_ as int) + (rhs as int) - 1) / (rhs as int), +``` + +Keep `as int` where Verus does not auto-coerce; `E0308` names the operand that +still needs it: + +- A `usize`/`nat` argument to a spec function's `int` parameter. The call site + inserts no coercion, so `Seq::subrange(0, new_len)` and + `Seq::subrange(0, seq.len())` need `new_len as int` and `seq.len() as int` + (`Seq::len` returns `nat`; `subrange`'s bounds are `int`). +- A standalone `/` divisor: once the dividend is `int`, `/` expects an `int` + divisor, so write `(self_ + rhs - 1) / (rhs as int)`, not `/ rhs`. +- A narrowing cast whose target may not hold the value (e.g. `as usize` from a + sequence's `nat` length), where the cast is the point of the expression. + +See also: PR [#770](https://github.com/asterinas/vostd/pull/770#discussion_r4023112584). ### Organize proof imports diff --git a/docs/coding-guidelines/proof-engineering.md b/docs/coding-guidelines/proof-engineering.md index 46babf699..3fb773afc 100644 --- a/docs/coding-guidelines/proof-engineering.md +++ b/docs/coding-guidelines/proof-engineering.md @@ -16,8 +16,33 @@ For example, a `BTreeMap::get_mut` model must preserve entries other than the selected key and must express the documented compatibility between the stored key ordering and borrowed-key ordering. Exclude documented panic conditions with explicit preconditions, such as capacity or index bounds, rather than merely -marking the operation `may_panic`. Do not claim `no_unwind` while a panic remains -possible under the preconditions. +marking the operation `may_panic`. `no_unwind` asserts that the modeled function +never unwinds — an unconditional claim, not one gated by the `requires` clause +(see the [Verus unwinding-signature reference](../../tools/verus/source/docs/guide/src/reference-unwind-sig.md)). +A function with a real panic path, such as `usize::div_ceil` on a zero divisor, +must not carry bare `no_unwind`, even when a `requires` clause excludes the +panicking input: the precondition restricts callers, it does not make the external +function panic-free. Model the no-panic regime faithfully with +`no_unwind when ` — giving callers the guarantee exactly where +`requires` holds — or omit `no_unwind` when no caller needs it. + +```rust +// Never panics: bare `no_unwind` is faithful. +pub assume_specification[ usize::is_power_of_two ](self_: usize) -> (r: bool) + returns is_pow2(self_ as int) + opens_invariants none + no_unwind; + +/// `usize::div_ceil` panics if `rhs` is zero; the precondition excludes that case. +pub assume_specification[ usize::div_ceil ](self_: usize, rhs: usize) -> usize + requires + rhs > 0, + returns + ((self_ + rhs - 1) / (rhs as int)) as usize, + opens_invariants none + no_unwind when rhs > 0 +; +``` Mirror the standard library's evaluation semantics in adapter contracts. For closure-driven adapters such as `Iterator::filter` and `map`, the closure runs @@ -40,7 +65,8 @@ See also: PR [#699](https://github.com/asterinas/vostd/pull/699#discussion_r3747 [#742](https://github.com/asterinas/vostd/pull/742#discussion_r3946394200), [#718](https://github.com/asterinas/vostd/pull/718#discussion_r3831831025), [#718](https://github.com/asterinas/vostd/pull/718#discussion_r3831836346), -and [#718](https://github.com/asterinas/vostd/pull/718#issuecomment-5390400295). +[#718](https://github.com/asterinas/vostd/pull/718#issuecomment-5390400295), +and [#770](https://github.com/asterinas/vostd/pull/770#discussion_r4023000538). ### Centralize trusted boundaries From 4f8df107b80f2bad608c9be03c50ed539ccfa8c4 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Wed, 16 Sep 2026 22:04:49 +0800 Subject: [PATCH 27/30] refine --- verified_libs/vstd_extra/src/external/int_specs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verified_libs/vstd_extra/src/external/int_specs.rs b/verified_libs/vstd_extra/src/external/int_specs.rs index a840d2cb5..239a7919a 100644 --- a/verified_libs/vstd_extra/src/external/int_specs.rs +++ b/verified_libs/vstd_extra/src/external/int_specs.rs @@ -29,7 +29,7 @@ pub assume_specification[ u32::is_power_of_two ](self_: u32) -> (r: bool) /// On a little-endian target, converting a native-endian `u32` to little endian is the identity. #[cfg(target_endian = "little")] -pub assume_specification[ u32::to_le ](value: u32) -> (result: u32) +pub assume_specification[ u32::to_le ](value: u32) -> u32 returns value, opens_invariants none From a7a57fabff88ca077f7303cdecbcab7a2639feb0 Mon Sep 17 00:00:00 2001 From: Hiroki Chen Date: Wed, 16 Sep 2026 16:47:48 -0400 Subject: [PATCH 28/30] Fix irc11 verus upgrade (#774) * Refresh IRC11 patches for the updated Verus toolchain Pin IRC11 to Asterinas Verus fb2386ecea5a45810420577de3e312ded4f84b12, matching the current default CI toolchain. Refresh the patch context for the btree_cursors and no_trait_conflicts additions upstream; all added and removed implementation lines in the IRC11 patch remain unchanged. This fixes bootstrap failures when applying the patch to vstd.rs and vstd_build/src/main.rs. The existing vstd compatibility patch still applies without changes, and the mainline Cargo.lock remains current. Validation: clean patch application, IRC11 bootstrap (2041 proofs), SC/IRC11 atomic smoke test, and vstd_extra verification (604 proofs). * Mark DMA and RCU monitor owners as objective The ResourceInvariant refactor requires the resource of each SimpleResourceInvariant to implement Objective in IRC11 builds. Add explicit implementations for the DMA and RCU monitor owners that otherwise fail the Verus trait conflict checker. The two DMA inner owners contain only PhantomData; the mapping set and RCU monitor owners are empty. These types carry no subjective memory permissions. Keep the implementations behind the irc11 feature. Validation: full IRC11 OSTD verification (1493 proofs), default make (1480 OSTD proofs), targeted verusfmt checks, and git diff --check. --- .github/workflows/ci-irc11.yml | 2 +- ostd/src/mm/dma/dma_coherent.rs | 8 ++++++++ ostd/src/mm/dma/dma_stream.rs | 8 ++++++++ ostd/src/mm/dma/mod.rs | 8 ++++++++ ostd/src/sync/rcu/monitor.rs | 8 ++++++++ tools/patches/verus-irc11.patch | 20 ++++++++++---------- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-irc11.yml b/.github/workflows/ci-irc11.yml index 5f3541ec8..48a151dda 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: 38f659b0e97a003d4fed73e083c71e786c6a3523 + VERUS_BASE_COMMIT: fb2386ecea5a45810420577de3e312ded4f84b12 VERUS_IRC11_PATCH: tools/patches/verus-irc11.patch VERUS_COMPAT_PATCH: tools/patches/verus-irc11-vstd.patch diff --git a/ostd/src/mm/dma/dma_coherent.rs b/ostd/src/mm/dma/dma_coherent.rs index 860e51a2d..cf679be74 100644 --- a/ostd/src/mm/dma/dma_coherent.rs +++ b/ostd/src/mm/dma/dma_coherent.rs @@ -2,6 +2,8 @@ use core::marker::PhantomData; use vstd::prelude::*; +#[cfg(feature = "irc11")] +use vstd::thread_view::Objective; use vstd_extra::{ atomic_data::AtomicDataWithOwner, @@ -66,6 +68,12 @@ pub tracked struct DmaCoherentInnerOwner { pub _marker: PhantomData, } +// This owner contains only PhantomData and carries no subjective memory permissions. +#[cfg(feature = "irc11")] +unsafe impl Objective for DmaCoherentInnerOwner { + +} + pub ghost struct DmaCoherentInnerInvariant; pub type DmaCoherentInnerAtomic = AtomicDataWithOwner< diff --git a/ostd/src/mm/dma/dma_stream.rs b/ostd/src/mm/dma/dma_stream.rs index 3e574f867..4d3d78491 100644 --- a/ostd/src/mm/dma/dma_stream.rs +++ b/ostd/src/mm/dma/dma_stream.rs @@ -2,6 +2,8 @@ use core::{marker::PhantomData, ops::Deref, ops::Range}; // SPDX-License-Identifier: MPL-2.0 use vstd::prelude::*; +#[cfg(feature = "irc11")] +use vstd::thread_view::Objective; use vstd_extra::external::convert::AsRefSpec; use vstd_extra::{ @@ -382,6 +384,12 @@ pub tracked struct DmaStreamInnerOwner { pub _marker: core::marker::PhantomData, } +// This owner contains only PhantomData and carries no subjective memory permissions. +#[cfg(feature = "irc11")] +unsafe impl Objective for DmaStreamInnerOwner { + +} + pub ghost struct DmaStreamInnerInvariant; pub type DmaStreanInnerAtomic = AtomicDataWithOwner, DmaStreamInnerInvariant>; diff --git a/ostd/src/mm/dma/mod.rs b/ostd/src/mm/dma/mod.rs index cf8d63d42..43cbe1045 100644 --- a/ostd/src/mm/dma/mod.rs +++ b/ostd/src/mm/dma/mod.rs @@ -7,6 +7,8 @@ mod test; use alloc::collections::BTreeSet; use vstd::prelude::*; +#[cfg(feature = "irc11")] +use vstd::thread_view::Objective; use vstd_extra::{ atomic_data::AtomicDataWithOwner, once::Once, @@ -21,6 +23,12 @@ verus! { pub tracked struct DmaMappingSetOwner {} +// This empty owner carries no subjective weak-memory observations. +#[cfg(feature = "irc11")] +unsafe impl Objective for DmaMappingSetOwner { + +} + pub ghost struct DmaMappingSetInvariant; impl SimpleResourceInvariant< diff --git a/ostd/src/sync/rcu/monitor.rs b/ostd/src/sync/rcu/monitor.rs index 6224e04fb..72132060b 100644 --- a/ostd/src/sync/rcu/monitor.rs +++ b/ostd/src/sync/rcu/monitor.rs @@ -1,4 +1,6 @@ // SPDX-License-Identifier: MPL-2.0 +#[cfg(feature = "irc11")] +use vstd::thread_view::Objective; use vstd::{atomic_ghost::AtomicBool, atomic_with_ghost, prelude::*}; use vstd_extra::{ atomic_data::AtomicDataWithOwner, @@ -29,6 +31,12 @@ pub(super) struct State { /// Owner of this [`RcuMonitor`]. pub(super) tracked struct RcuMonitorOwner {} +// This empty owner carries no subjective weak-memory observations. +#[cfg(feature = "irc11")] +unsafe impl Objective for RcuMonitorOwner { + +} + pub(super) ghost struct RcuMonitorInvariant; struct_with_invariants! { diff --git a/tools/patches/verus-irc11.patch b/tools/patches/verus-irc11.patch index 5cc9707ab..c4c2406a9 100644 --- a/tools/patches/verus-irc11.patch +++ b/tools/patches/verus-irc11.patch @@ -2963,18 +2963,18 @@ index 00000000..bfb1c293 + +} // verus! diff --git a/source/vstd/vstd.rs b/source/vstd/vstd.rs -index eb06f180..41016a35 100644 +index 0674e2ea..3fb12455 100644 --- a/source/vstd/vstd.rs +++ b/source/vstd/vstd.rs -@@ -26,6 +26,7 @@ - #![cfg_attr(verus_keep_ghost, feature(fmt_internals))] +@@ -27,6 +27,7 @@ #![cfg_attr(verus_keep_ghost, feature(fmt_arguments_from_str))] + #![cfg_attr(all(feature = "alloc", verus_keep_ghost), feature(btree_cursors))] #![cfg_attr(verus_keep_ghost, feature(panic_internals))] +#![cfg_attr(verus_keep_ghost, feature(auto_traits))] #[cfg(feature = "alloc")] extern crate alloc; -@@ -34,6 +35,7 @@ pub mod arithmetic; +@@ -35,6 +36,7 @@ pub mod arithmetic; pub mod array; pub mod atomic; pub mod atomic_ghost; @@ -2982,7 +2982,7 @@ index eb06f180..41016a35 100644 pub mod bits; pub mod bytes; pub mod calc_macro; -@@ -85,6 +87,7 @@ pub mod state_machine_internal; +@@ -88,6 +90,7 @@ pub mod state_machine_internal; pub mod string; #[cfg(feature = "std")] pub mod thread; @@ -2991,18 +2991,18 @@ index eb06f180..41016a35 100644 pub mod utf8; pub mod view; diff --git a/source/vstd_build/src/main.rs b/source/vstd_build/src/main.rs -index 6b849bf2..2250d553 100644 +index 5662268c..fa6cd837 100644 --- a/source/vstd_build/src/main.rs +++ b/source/vstd_build/src/main.rs -@@ -37,6 +37,7 @@ fn main() { - let mut no_lifetime = false; +@@ -38,6 +38,7 @@ fn main() { + let mut no_trait_conflicts = false; let mut expand_errors = false; let mut no_solver_version_check = false; + let mut weak_memory = false; for arg in args { if arg == "--release" { release = true; -@@ -58,6 +59,8 @@ fn main() { +@@ -61,6 +62,8 @@ fn main() { expand_errors = true; } else if arg == "--no-solver-version-check" { no_solver_version_check = true; @@ -3011,7 +3011,7 @@ index 6b849bf2..2250d553 100644 } else { panic!("unexpected argument: {:}", arg) } -@@ -142,6 +145,10 @@ fn main() { +@@ -149,6 +152,10 @@ fn main() { child_args.push("--cfg".to_string()); child_args.push("feature=\"alloc\"".to_string()); } From c032fc63ae0ba279acf75340cc1aa14b3b0b14b9 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Thu, 17 Sep 2026 10:25:30 +0800 Subject: [PATCH 29/30] refine: according to the review skill --- ostd/src/arch/x86/pci.rs | 20 +++++++++----------- ostd/src/io/io_mem/mod.rs | 3 +-- ostd/src/io/io_port/allocator.rs | 9 +-------- ostd/src/io/io_port/mod.rs | 4 ++-- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/ostd/src/arch/x86/pci.rs b/ostd/src/arch/x86/pci.rs index d8292ac4e..1a01d7f8e 100644 --- a/ostd/src/arch/x86/pci.rs +++ b/ostd/src/arch/x86/pci.rs @@ -54,11 +54,10 @@ pub(crate) fn has_pci_bus() -> bool { pub(crate) const MSIX_DEFAULT_MSG_ADDR: u32 = 0xFEE0_0000; #[verus_verify] -#[verus_spec(address => - ensures - address == MSIX_DEFAULT_MSG_ADDR | 0b1_1000 - | ((remapping_index & 0x7FFF) << 5) - | ((remapping_index & 0x8000) >> 13), +#[verus_spec(returns + MSIX_DEFAULT_MSG_ADDR | 0b1_1000 + | ((remapping_index & 0x7FFF) << 5) + | ((remapping_index & 0x8000) >> 13), )] pub(crate) fn construct_remappable_msix_address(remapping_index: u32) -> u32 { // Use remappable format. The bits[4:3] should be always set to 1 according to the manual. @@ -73,12 +72,11 @@ pub(crate) fn construct_remappable_msix_address(remapping_index: u32) -> u32 { /// Encodes the bus, device, and function into a port address for use with the PCI I/O port. #[verus_verify] -#[verus_spec(port => - ensures - port == (1u32 << 31) - | ((location.bus as u32) << 16) - | (((location.device as u32) & 0b11111) << 11) - | (((location.function as u32) & 0b111) << 8), +#[verus_spec(returns + (1u32 << 31) + | ((location.bus as u32) << 16) + | (((location.device as u32) & 0b11111) << 11) + | (((location.function as u32) & 0b111) << 8), )] fn encode_as_port(location: &PciDeviceLocation) -> u32 { // 1 << 31: Configuration enable diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index fa64c5e96..306fc0e8d 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -114,8 +114,7 @@ impl IoMem { #[verus_verify] #[verus_spec(result => requires - range.start < range.end, - range.end <= self.length_spec(), + range.start < range.end <= self.length_spec(), self.offset_spec() + range.start <= usize::MAX, self.paddr_spec() + range.start <= usize::MAX, ensures diff --git a/ostd/src/io/io_port/allocator.rs b/ostd/src/io/io_port/allocator.rs index c73d67ace..f33c54ac2 100644 --- a/ostd/src/io/io_port/allocator.rs +++ b/ostd/src/io/io_port/allocator.rs @@ -6,8 +6,7 @@ use vstd::{ resource::set::{GhostSetAuth, GhostSubset}, tokens::InstanceId, }; -use vstd_extra::ownership::Inv; -use vstd_extra::resource_invariant::ResourceInvariant; +use vstd_extra::{ownership::Inv, resource_invariant::ResourceInvariant}; use core::ops::Range; @@ -114,9 +113,6 @@ pub(crate) open spec fn io_port_inner_inv_values( &&& id_alloc_capacity(allocator) == crate::arch::io::MAX_IO_PORT as usize } -} // verus! -verus! { - /// Authority over the set of PIO ids currently allocated by the global allocator. /// /// The `Loc` of `auth` identifies the protocol instance and `auth@` is the set of allocated @@ -201,9 +197,6 @@ impl IoPortClaim { } } -} // verus! -verus! { - ghost struct IoPortAllocInvariant; impl ResourceInvariant for IoPortAllocInvariant { diff --git a/ostd/src/io/io_port/mod.rs b/ostd/src/io/io_port/mod.rs index 71f753c97..6f4c5671a 100644 --- a/ostd/src/io/io_port/mod.rs +++ b/ostd/src/io/io_port/mod.rs @@ -140,7 +140,7 @@ impl IoPort { valid_io_port_access::(port), allocator::io_port_allocator_initialized(), ensures - result is Ok <== claim@ is Some, + result is Ok <==> claim@ is Some, result matches Ok(io_port) ==> { &&& io_port@ == port &&& !io_port.is_overlapping() @@ -178,7 +178,7 @@ impl IoPort { valid_io_port_access::(port), allocator::io_port_allocator_initialized(), ensures - result is Ok <== claim@ is Some, + result is Ok <==> claim@ is Some, result matches Ok(io_port) ==> { &&& io_port@ == port &&& io_port.is_overlapping() From e13a92b87986440218fe26770898f15ca2af2305 Mon Sep 17 00:00:00 2001 From: Marsman1996 Date: Thu, 17 Sep 2026 11:18:22 +0800 Subject: [PATCH 30/30] refine: xinyi --- ostd/src/io/io_mem/allocator.rs | 4 ++-- ostd/src/io/io_mem/mod.rs | 34 +++++++++++---------------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/ostd/src/io/io_mem/allocator.rs b/ostd/src/io/io_mem/allocator.rs index 49222a054..9e60f79c6 100644 --- a/ostd/src/io/io_mem/allocator.rs +++ b/ostd/src/io/io_mem/allocator.rs @@ -37,8 +37,8 @@ impl IoMemAllocator { io_mem_range_registered(range), ensures result matches Some(io_mem) ==> { - &&& io_mem.paddr_spec() == range.start - &&& io_mem.length_spec() == range.end - range.start + &&& io_mem.paddr() == range.start + &&& io_mem.length() == range.end - range.start }, )] pub fn acquire(&self, range: Range) -> Option { diff --git a/ostd/src/io/io_mem/mod.rs b/ostd/src/io/io_mem/mod.rs index 306fc0e8d..b546c7483 100644 --- a/ostd/src/io/io_mem/mod.rs +++ b/ostd/src/io/io_mem/mod.rs @@ -42,16 +42,6 @@ verus! { #[verus_verify] impl IoMem { - /// Logical physical-address projection used by verified callers. - pub closed spec fn paddr_spec(&self) -> Paddr { - self.pa - } - - /// Logical byte length used by verified callers. - pub closed spec fn length_spec(&self) -> usize { - self.limit - } - /// Logical offset into the page-aligned mapping. pub closed spec fn offset_spec(&self) -> usize { self.offset @@ -61,7 +51,7 @@ impl IoMem { } // verus! #[verus_verify] impl HasPaddr for IoMem { - #[verus_spec(returns self.paddr_spec())] + #[verus_spec(returns IoMem::paddr(self))] fn paddr(&self) -> Paddr { self.pa } @@ -79,8 +69,8 @@ impl IoMem { vstd_extra::panic::may_panic(), ensures result matches Ok(io_mem) ==> { - &&& io_mem.paddr_spec() == range.start - &&& io_mem.length_spec() == range.end - range.start + &&& io_mem.paddr() == range.start + &&& io_mem.length() == range.end - range.start }, )] pub fn acquire(range: Range) -> Result { @@ -93,15 +83,13 @@ impl IoMem { } /// Returns the physical address of the I/O memory. - #[verus_verify] - #[verus_spec(returns self.paddr_spec())] + #[verus_verify(dual_spec)] pub fn paddr(&self) -> Paddr { self.pa } /// Returns the length of the I/O memory region. - #[verus_verify] - #[verus_spec(returns self.length_spec())] + #[verus_verify(dual_spec)] pub fn length(&self) -> usize { self.limit } @@ -114,13 +102,13 @@ impl IoMem { #[verus_verify] #[verus_spec(result => requires - range.start < range.end <= self.length_spec(), + range.start < range.end <= self.length(), self.offset_spec() + range.start <= usize::MAX, - self.paddr_spec() + range.start <= usize::MAX, + self.paddr() + range.start <= usize::MAX, ensures result.offset_spec() == self.offset_spec() + range.start, - result.length_spec() == range.end - range.start, - result.paddr_spec() == self.paddr_spec() + range.start, + result.length() == range.end - range.start, + result.paddr() == self.paddr() + range.start, )] pub fn slice(&self, range: Range) -> Self { // This ensures `range.start < range.end` and `range.end <= limit`. @@ -152,8 +140,8 @@ impl IoMem { range.start <= range.end, range.end <= usize::MAX - (PAGE_SIZE - 1), ensures - result.paddr_spec() == range.start, - result.length_spec() + result.paddr() == range.start, + result.length() == range.end - range.start, )] pub(crate) unsafe fn new(range: Range, flags: PageFlags, cache: CachePolicy) -> Self {