diff --git a/src/arch/aarch64/security/bti.rs b/src/arch/aarch64/security/bti.rs
index 3e4a390fd9..42fad82b80 100644
--- a/src/arch/aarch64/security/bti.rs
+++ b/src/arch/aarch64/security/bti.rs
@@ -17,7 +17,9 @@
mod control;
mod guard;
mod landing;
+mod pad;
pub use control::{bti_enabled, bti_supported, disable_bti, enable_bti, init_bti};
pub use guard::BtiGuard;
pub use landing::check_bti_landing_pad;
+pub use pad::is_bti_landing_pad;
diff --git a/src/arch/aarch64/security/bti/landing.rs b/src/arch/aarch64/security/bti/landing.rs
index 1c73dd006c..b01644defd 100644
--- a/src/arch/aarch64/security/bti/landing.rs
+++ b/src/arch/aarch64/security/bti/landing.rs
@@ -14,7 +14,9 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+use super::pad::is_bti_landing_pad;
+
pub fn check_bti_landing_pad(addr: u64) -> bool {
let instruction = unsafe { *(addr as *const u32) };
- matches!(instruction, 0xD503201F | 0xD503245F | 0xD503249F | 0xD50324DF)
+ is_bti_landing_pad(instruction)
}
diff --git a/src/arch/aarch64/security/bti/pad.rs b/src/arch/aarch64/security/bti/pad.rs
new file mode 100644
index 0000000000..9ecfe9a704
--- /dev/null
+++ b/src/arch/aarch64/security/bti/pad.rs
@@ -0,0 +1,29 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pub const BTI_C: u32 = 0xD503245F;
+pub const BTI_J: u32 = 0xD503249F;
+pub const BTI_JC: u32 = 0xD50324DF;
+pub const PACIASP: u32 = 0xD503233F;
+pub const PACIBSP: u32 = 0xD503237F;
+
+/* Whether an indirect branch may land on this instruction in a guarded page.
+BTI c, j and jc are landing pads, and PACIASP and PACIBSP act as BTI c. A NOP
+or a bare BTI accepts no branch type, so on a core with FEAT_BTI a branch to
+either raises a Branch Target exception. */
+pub const fn is_bti_landing_pad(instruction: u32) -> bool {
+ matches!(instruction, BTI_C | BTI_J | BTI_JC | PACIASP | PACIBSP)
+}
diff --git a/src/arch/x86_64/acpi/data/ioapic.rs b/src/arch/x86_64/acpi/data/ioapic.rs
index 0dc38596d5..1178f05852 100644
--- a/src/arch/x86_64/acpi/data/ioapic.rs
+++ b/src/arch/x86_64/acpi/data/ioapic.rs
@@ -22,7 +22,8 @@ pub struct IoApicInfo {
}
impl IoApicInfo {
+ /* gsi_base comes from the MADT unchecked; saturate rather than abort. */
pub fn gsi_max(&self) -> u32 {
- self.gsi_base + 23
+ self.gsi_base.saturating_add(23)
}
}
diff --git a/src/arch/x86_64/iommu/regs/mod.rs b/src/arch/x86_64/iommu/regs/mod.rs
index 6f5e29c8ea..d70c53fe18 100644
--- a/src/arch/x86_64/iommu/regs/mod.rs
+++ b/src/arch/x86_64/iommu/regs/mod.rs
@@ -16,3 +16,4 @@
pub mod cap;
pub mod offsets;
+pub mod window;
diff --git a/src/arch/x86_64/iommu/regs/window.rs b/src/arch/x86_64/iommu/regs/window.rs
new file mode 100644
index 0000000000..ee5111c374
--- /dev/null
+++ b/src/arch/x86_64/iommu/regs/window.rs
@@ -0,0 +1,27 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::cap::{fault_recording_count, fault_recording_offset};
+use super::offsets::iotlb_offset;
+
+/* Whether every register the kernel touches lies inside a mapped window of
+`window` bytes. The IOTLB register and the fault-recording registers sit
+where CAP and ECAP say, up to about 16 KiB into the unit, so a unit that
+places them past the window must be refused rather than accessed. */
+pub const fn registers_fit(cap: u64, ecap: u64, window: usize) -> bool {
+ let faults_end = fault_recording_offset(cap) + fault_recording_count(cap) as usize * 16;
+ iotlb_offset(ecap) + 8 <= window && faults_end <= window
+}
diff --git a/src/arch/x86_64/iommu/unit/access.rs b/src/arch/x86_64/iommu/unit/access.rs
index 1ce75e3fb8..bfb0d3b160 100644
--- a/src/arch/x86_64/iommu/unit/access.rs
+++ b/src/arch/x86_64/iommu/unit/access.rs
@@ -27,6 +27,9 @@ pub struct RemapUnit {
/// Register window of a unit, per the spec.
pub const UNIT_WINDOW: usize = 4096;
+/* Each accessor bounds the offset by subtracting from the window rather than
+adding to the offset, so no offset, however large, can wrap past the check. */
+
impl RemapUnit {
/// # Safety
/// `base_va` must be a live mapping of `UNIT_WINDOW` uncached bytes over
@@ -40,14 +43,14 @@ impl RemapUnit {
}
pub fn read32(&self, offset: usize) -> u32 {
- debug_assert!(offset + 4 <= UNIT_WINDOW);
+ assert!(offset <= UNIT_WINDOW - 4);
// SAFETY: offset is inside the mapped register window this value
// promises, and the registers are uncached device memory.
unsafe { core::ptr::read_volatile((self.base_va as usize + offset) as *const u32) }
}
pub fn read64(&self, offset: usize) -> u64 {
- debug_assert!(offset + 8 <= UNIT_WINDOW);
+ assert!(offset <= UNIT_WINDOW - 8);
// SAFETY: as read32.
unsafe { core::ptr::read_volatile((self.base_va as usize + offset) as *const u64) }
}
@@ -56,7 +59,7 @@ impl RemapUnit {
/// Writing a remapping register changes how devices reach memory. The
/// caller owns the sequencing the spec requires around the register.
pub unsafe fn write32(&self, offset: usize, value: u32) {
- debug_assert!(offset + 4 <= UNIT_WINDOW);
+ assert!(offset <= UNIT_WINDOW - 4);
// SAFETY: offset is inside the mapped window; the caller owns meaning.
unsafe { core::ptr::write_volatile((self.base_va as usize + offset) as *mut u32, value) }
}
@@ -64,7 +67,7 @@ impl RemapUnit {
/// # Safety
/// As `write32`.
pub unsafe fn write64(&self, offset: usize, value: u64) {
- debug_assert!(offset + 8 <= UNIT_WINDOW);
+ assert!(offset <= UNIT_WINDOW - 8);
// SAFETY: offset is inside the mapped window; the caller owns meaning.
unsafe { core::ptr::write_volatile((self.base_va as usize + offset) as *mut u64, value) }
}
diff --git a/src/arch/x86_64/iommu/unit/probe.rs b/src/arch/x86_64/iommu/unit/probe.rs
index d5b6c2c90d..4e8befef2e 100644
--- a/src/arch/x86_64/iommu/unit/probe.rs
+++ b/src/arch/x86_64/iommu/unit/probe.rs
@@ -19,7 +19,7 @@
use super::access::{RemapUnit, UNIT_WINDOW};
use crate::arch::x86_64::acpi::parser::other::remap_unit_bases;
-use crate::arch::x86_64::iommu::regs::{cap, offsets};
+use crate::arch::x86_64::iommu::regs::{cap, offsets, window};
use crate::memory::addr::PhysAddr;
/// What one unit supports, as read from its Capability register.
@@ -44,6 +44,8 @@ pub enum ProbeError {
MapFailed,
/// The unit reports no supported paging depth, so it cannot translate.
NoUsableAgaw,
+ /// CAP or ECAP places a register the kernel uses beyond the mapped window.
+ RegistersOutsideWindow,
}
/// Map the first unit DMAR reported and read its capabilities.
@@ -77,6 +79,9 @@ pub fn probe_at(base_pa: u64) -> Result {
let status = unit.read32(offsets::GSTS);
let levels = cap::preferred_levels(capability).ok_or(ProbeError::NoUsableAgaw)?;
+ if !window::registers_fit(capability, ecap, UNIT_WINDOW) {
+ return Err(ProbeError::RegistersOutsideWindow);
+ }
Ok(UnitInfo {
unit,
diff --git a/src/arch/x86_64/iommu/unit/report/failure.rs b/src/arch/x86_64/iommu/unit/report/failure.rs
index 9e8ac724fd..59521dd358 100644
--- a/src/arch/x86_64/iommu/unit/report/failure.rs
+++ b/src/arch/x86_64/iommu/unit/report/failure.rs
@@ -23,5 +23,6 @@ pub(super) fn reason(e: ProbeError) -> &'static [u8] {
ProbeError::NoUnits => b"no units",
ProbeError::MapFailed => b"register window not mappable",
ProbeError::NoUsableAgaw => b"no supported paging depth",
+ ProbeError::RegistersOutsideWindow => b"registers beyond the mapped window",
}
}
diff --git a/src/arch/x86_64/multiboot/modules_acpi.rs b/src/arch/x86_64/multiboot/modules_acpi.rs
index 82d5f6700d..326e5907c8 100644
--- a/src/arch/x86_64/multiboot/modules_acpi.rs
+++ b/src/arch/x86_64/multiboot/modules_acpi.rs
@@ -24,6 +24,7 @@ pub struct AcpiRsdp {
pub length: Option,
pub xsdt_address: Option,
pub extended_checksum: Option,
+ pub reserved: Option<[u8; 3]>,
}
impl AcpiRsdp {
@@ -60,10 +61,23 @@ impl AcpiRsdp {
sum == 0
}
+ /* An ACPI 2.0 RSDP passes only with all its extended fields present, a
+ length of exactly 36, and those 36 bytes summing to zero, reserved bytes
+ included. The extended checksum covers the length the table declares, and
+ only 36 bytes are kept, so a longer declaration could hide unchecked bytes.
+ Below revision 2 there is no extended checksum to check. */
pub fn verify_extended_checksum(&self) -> bool {
if !self.is_acpi2() {
return true;
}
+ let (Some(len), Some(xsdt), Some(ext), Some(reserved)) =
+ (self.length, self.xsdt_address, self.extended_checksum, self.reserved)
+ else {
+ return false;
+ };
+ if len != 36 {
+ return false;
+ }
let mut sum: u8 = 0;
for &b in &self.signature {
sum = sum.wrapping_add(b);
@@ -76,18 +90,15 @@ impl AcpiRsdp {
for &b in &self.rsdt_address.to_le_bytes() {
sum = sum.wrapping_add(b);
}
- if let Some(len) = self.length {
- for &b in &len.to_le_bytes() {
- sum = sum.wrapping_add(b);
- }
+ for &b in &len.to_le_bytes() {
+ sum = sum.wrapping_add(b);
}
- if let Some(xsdt) = self.xsdt_address {
- for &b in &xsdt.to_le_bytes() {
- sum = sum.wrapping_add(b);
- }
+ for &b in &xsdt.to_le_bytes() {
+ sum = sum.wrapping_add(b);
}
- if let Some(ext) = self.extended_checksum {
- sum = sum.wrapping_add(ext);
+ sum = sum.wrapping_add(ext);
+ for &b in &reserved {
+ sum = sum.wrapping_add(b);
}
sum == 0
}
diff --git a/src/arch/x86_64/multiboot/state/parse/firmware.rs b/src/arch/x86_64/multiboot/state/parse/firmware.rs
index e8320a131f..53460f5a4c 100644
--- a/src/arch/x86_64/multiboot/state/parse/firmware.rs
+++ b/src/arch/x86_64/multiboot/state/parse/firmware.rs
@@ -87,13 +87,15 @@ impl MultibootManager {
let revision = *rsdp_ptr.add(15);
let rsdt_address = core::ptr::read_unaligned(rsdp_ptr.add(16) as *const u32);
- let (length, xsdt_address, extended_checksum) = if is_new && rsdp_size >= 36 {
+ let (length, xsdt_address, extended_checksum, reserved) = if is_new && rsdp_size >= 36 {
let length = core::ptr::read_unaligned(rsdp_ptr.add(20) as *const u32);
let xsdt_address = core::ptr::read_unaligned(rsdp_ptr.add(24) as *const u64);
let extended_checksum = *rsdp_ptr.add(32);
- (Some(length), Some(xsdt_address), Some(extended_checksum))
+ let mut reserved = [0u8; 3];
+ reserved.copy_from_slice(slice::from_raw_parts(rsdp_ptr.add(33), 3));
+ (Some(length), Some(xsdt_address), Some(extended_checksum), Some(reserved))
} else {
- (None, None, None)
+ (None, None, None, None)
};
Ok(AcpiRsdp {
@@ -105,6 +107,7 @@ impl MultibootManager {
length,
xsdt_address,
extended_checksum,
+ reserved,
})
}
}
diff --git a/src/arch/x86_64/port/stats_snapshot.rs b/src/arch/x86_64/port/stats_snapshot.rs
index 8b1c2a07f0..de01e25e14 100644
--- a/src/arch/x86_64/port/stats_snapshot.rs
+++ b/src/arch/x86_64/port/stats_snapshot.rs
@@ -26,10 +26,14 @@ pub struct PortStatsSnapshot {
}
impl PortStatsSnapshot {
+ /* The counters wrap on their own; their sums saturate rather than abort. */
pub const fn total_ops(&self) -> u64 {
- self.read_ops + self.write_ops + self.string_read_ops + self.string_write_ops
+ self.read_ops
+ .saturating_add(self.write_ops)
+ .saturating_add(self.string_read_ops)
+ .saturating_add(self.string_write_ops)
}
pub const fn total_bytes(&self) -> u64 {
- self.bytes_read + self.bytes_written
+ self.bytes_read.saturating_add(self.bytes_written)
}
}
diff --git a/src/arch/x86_64/port/stats_types.rs b/src/arch/x86_64/port/stats_types.rs
index 327fb45560..fbe461cb09 100644
--- a/src/arch/x86_64/port/stats_types.rs
+++ b/src/arch/x86_64/port/stats_types.rs
@@ -63,10 +63,7 @@ impl PortStats {
}
pub fn total_ops(&self) -> u64 {
- self.read_ops.load(Ordering::Relaxed)
- + self.write_ops.load(Ordering::Relaxed)
- + self.string_read_ops.load(Ordering::Relaxed)
- + self.string_write_ops.load(Ordering::Relaxed)
+ self.snapshot().total_ops()
}
}
diff --git a/src/arch/x86_64/uefi/constants/revisions.rs b/src/arch/x86_64/uefi/constants/revisions.rs
index f88202fd8f..2fee856bd7 100644
--- a/src/arch/x86_64/uefi/constants/revisions.rs
+++ b/src/arch/x86_64/uefi/constants/revisions.rs
@@ -14,27 +14,34 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-pub const UEFI_REVISION_2_0: u32 = 0x00020000;
+/* The UEFI specification packs a revision as the major version in the upper
+sixteen bits and, in the lower sixteen, the minor version times ten plus the
+patch digit: 2.3.1 is (2 << 16) | 31 and 2.10 is (2 << 16) | 100. */
+pub const fn uefi_revision(major: u16, minor: u16, patch: u16) -> u32 {
+ ((major as u32) << 16) | (minor as u32 * 10 + patch as u32)
+}
-pub const UEFI_REVISION_2_1: u32 = 0x00020100;
+pub const UEFI_REVISION_2_0: u32 = uefi_revision(2, 0, 0);
-pub const UEFI_REVISION_2_3: u32 = 0x00020300;
+pub const UEFI_REVISION_2_1: u32 = uefi_revision(2, 1, 0);
-pub const UEFI_REVISION_2_3_1: u32 = 0x0002001F;
+pub const UEFI_REVISION_2_3: u32 = uefi_revision(2, 3, 0);
-pub const UEFI_REVISION_2_4: u32 = 0x00020400;
+pub const UEFI_REVISION_2_3_1: u32 = uefi_revision(2, 3, 1);
-pub const UEFI_REVISION_2_5: u32 = 0x00020500;
+pub const UEFI_REVISION_2_4: u32 = uefi_revision(2, 4, 0);
-pub const UEFI_REVISION_2_6: u32 = 0x00020600;
+pub const UEFI_REVISION_2_5: u32 = uefi_revision(2, 5, 0);
-pub const UEFI_REVISION_2_7: u32 = 0x00020700;
+pub const UEFI_REVISION_2_6: u32 = uefi_revision(2, 6, 0);
-pub const UEFI_REVISION_2_8: u32 = 0x00020800;
+pub const UEFI_REVISION_2_7: u32 = uefi_revision(2, 7, 0);
-pub const UEFI_REVISION_2_9: u32 = 0x00020900;
+pub const UEFI_REVISION_2_8: u32 = uefi_revision(2, 8, 0);
-pub const UEFI_REVISION_2_10: u32 = 0x00020A00;
+pub const UEFI_REVISION_2_9: u32 = uefi_revision(2, 9, 0);
+
+pub const UEFI_REVISION_2_10: u32 = uefi_revision(2, 10, 0);
pub const RESET_TYPE_COLD: u32 = 0;
diff --git a/src/arch/x86_64/uefi/manager/init.rs b/src/arch/x86_64/uefi/manager/init.rs
index b50f2baf4f..423751c57d 100644
--- a/src/arch/x86_64/uefi/manager/init.rs
+++ b/src/arch/x86_64/uefi/manager/init.rs
@@ -21,6 +21,7 @@ use core::sync::atomic::Ordering;
use super::core::UefiManager;
use super::state::INITIALIZED;
+use crate::arch::x86_64::uefi::constants::UEFI_REVISION_2_8;
use crate::arch::x86_64::uefi::error::UefiError;
use crate::arch::x86_64::uefi::tables::RuntimeServices;
use crate::arch::x86_64::uefi::types::Guid;
@@ -70,7 +71,7 @@ impl UefiManager {
info.vendor = String::from("NONOS UEFI");
info.version = String::from("2.8");
- info.revision = 0x00020008;
+ info.revision = UEFI_REVISION_2_8;
info.firmware_revision = 0x00010000;
*self.firmware_info.write() = info;
diff --git a/src/arch/x86_64/uefi/tables/memory_desc.rs b/src/arch/x86_64/uefi/tables/memory_desc.rs
index dda156d915..2584c6413f 100644
--- a/src/arch/x86_64/uefi/tables/memory_desc.rs
+++ b/src/arch/x86_64/uefi/tables/memory_desc.rs
@@ -40,11 +40,16 @@ impl MemoryDescriptor {
pub const EFI_MEMORY_CPU_CRYPTO: u64 = 0x0000000000080000;
pub const EFI_MEMORY_RUNTIME: u64 = 0x8000000000000000;
+ /* Saturating: the page count and start come from firmware, and an
+ overflowing descriptor must not abort the kernel. */
pub fn size_bytes(&self) -> u64 {
+ if self.number_of_pages > u64::MAX / 4096 {
+ return u64::MAX;
+ }
self.number_of_pages * 4096
}
pub fn end_address(&self) -> u64 {
- self.physical_start + self.size_bytes()
+ self.physical_start.saturating_add(self.size_bytes())
}
pub fn is_runtime(&self) -> bool {
self.attribute & Self::EFI_MEMORY_RUNTIME != 0
diff --git a/src/arch/x86_64/uefi/tables/time.rs b/src/arch/x86_64/uefi/tables/time.rs
index 2c62a3cdb5..3a906d2955 100644
--- a/src/arch/x86_64/uefi/tables/time.rs
+++ b/src/arch/x86_64/uefi/tables/time.rs
@@ -41,7 +41,7 @@ impl EfiTime {
&& self.month >= 1
&& self.month <= 12
&& self.day >= 1
- && self.day <= 31
+ && self.day <= Self::days_in_month(self.year, self.month)
&& self.hour <= 23
&& self.minute <= 59
&& self.second <= 59
@@ -63,6 +63,10 @@ impl EfiTime {
days += if Self::is_leap_year(y as u16) { 366 } else { 365 };
}
+ for y in year..1970 {
+ days -= if Self::is_leap_year(y as u16) { 366 } else { 365 };
+ }
+
for m in 1..month {
days += days_per_month[(m - 1) as usize];
if m == 2 && Self::is_leap_year(year as u16) {
@@ -82,6 +86,21 @@ impl EfiTime {
}
}
+ /* Only asked for months 1 to 12, which is_valid checks first. */
+ fn days_in_month(year: u16, month: u8) -> u8 {
+ match month {
+ 2 => {
+ if Self::is_leap_year(year) {
+ 29
+ } else {
+ 28
+ }
+ }
+ 4 | 6 | 9 | 11 => 30,
+ _ => 31,
+ }
+ }
+
fn is_leap_year(year: u16) -> bool {
(year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
}
diff --git a/src/arch/x86_64/uefi/types/attributes.rs b/src/arch/x86_64/uefi/types/attributes.rs
index 321d43ff1c..e4942acf52 100644
--- a/src/arch/x86_64/uefi/types/attributes.rs
+++ b/src/arch/x86_64/uefi/types/attributes.rs
@@ -73,8 +73,11 @@ impl VariableAttributes {
}
#[inline]
+ /* Every authenticated-write flag counts, including the deprecated
+ count-based one, which firmware still reports on old variables. */
pub const fn requires_authentication(&self) -> bool {
- self.contains(Self::TIME_BASED_AUTHENTICATED_WRITE_ACCESS)
+ self.contains(Self::AUTHENTICATED_WRITE_ACCESS)
+ || self.contains(Self::TIME_BASED_AUTHENTICATED_WRITE_ACCESS)
|| self.contains(Self::ENHANCED_AUTHENTICATED_ACCESS)
}
diff --git a/src/arch/x86_64/vga/constants.rs b/src/arch/x86_64/vga/constants.rs
index 0cd3bee9b0..de75b88c2c 100644
--- a/src/arch/x86_64/vga/constants.rs
+++ b/src/arch/x86_64/vga/constants.rs
@@ -82,12 +82,15 @@ impl Color {
pub struct ColorCode(u8);
impl ColorCode {
+ /* Bit 7 is blink, which the attribute controller enables by default, so
+ the background keeps three bits: a bright background is drawn dark rather
+ than blinking. */
pub const fn new(foreground: Color, background: Color) -> Self {
- Self((background as u8) << 4 | (foreground as u8))
+ Self(((background as u8) & 0x07) << 4 | (foreground as u8))
}
pub const fn with_blink(foreground: Color, background: Color) -> Self {
- Self(0x80 | (background as u8) << 4 | (foreground as u8))
+ Self(0x80 | ((background as u8) & 0x07) << 4 | (foreground as u8))
}
pub const fn foreground(self) -> u8 {
diff --git a/src/boot/vga/colors.rs b/src/boot/vga/colors.rs
index 1716c38ad9..016cdbde01 100644
--- a/src/boot/vga/colors.rs
+++ b/src/boot/vga/colors.rs
@@ -31,8 +31,10 @@ pub const PINK: u8 = 0x0D;
pub const YELLOW: u8 = 0x0E;
pub const WHITE: u8 = 0x0F;
+/* Bit 7 is blink with the attribute controller's default setting, so the
+background is three bits, as ColorCode reads it. */
pub const fn make_attr(fg: u8, bg: u8) -> u8 {
- (bg << 4) | (fg & 0x0F)
+ ((bg & 0x07) << 4) | (fg & 0x0F)
}
pub const fn fg_color(attr: u8) -> u8 {
@@ -40,5 +42,5 @@ pub const fn fg_color(attr: u8) -> u8 {
}
pub const fn bg_color(attr: u8) -> u8 {
- (attr >> 4) & 0x0F
+ (attr >> 4) & 0x07
}
diff --git a/src/drivers/pci/constants/address_packing.rs b/src/drivers/pci/constants/address_packing.rs
index 7ac4296a95..0195b71711 100644
--- a/src/drivers/pci/constants/address_packing.rs
+++ b/src/drivers/pci/constants/address_packing.rs
@@ -18,7 +18,7 @@
pub const fn pci_config_address(bus: u8, device: u8, function: u8, offset: u8) -> u32 {
(1u32 << 31)
| ((bus as u32) << 16)
- | ((device as u32) << 11)
- | ((function as u32) << 8)
+ | (((device & 0x1F) as u32) << 11)
+ | (((function & 0x7) as u32) << 8)
| ((offset as u32) & 0xFC)
}
diff --git a/src/elf/loader/core/section.rs b/src/elf/loader/core/section.rs
index b58c3d3f2a..b7a82a4675 100644
--- a/src/elf/loader/core/section.rs
+++ b/src/elf/loader/core/section.rs
@@ -32,8 +32,11 @@ impl ParsedSection {
pub fn is_alloc(&self) -> bool {
self.flags & 0x2 != 0
}
+ /* SHT_SYMTAB only. The dynamic symbol table, SHT_DYNSYM, has its own
+ query, and matching it here made get_symbol_table return .dynsym on an
+ image that places it first. */
pub fn is_symtab(&self) -> bool {
- self.section_type == 2 || self.section_type == 11
+ self.section_type == 2
}
pub fn is_strtab(&self) -> bool {
self.section_type == 3
diff --git a/src/fs/procfs/pid/entry.rs b/src/fs/procfs/pid/entry.rs
index 9a83f072c5..6d054e7327 100644
--- a/src/fs/procfs/pid/entry.rs
+++ b/src/fs/procfs/pid/entry.rs
@@ -16,11 +16,12 @@
extern crate alloc;
+use crate::fs::procfs::pid_inode::PID_INODE_SHIFT;
use crate::fs::procfs::types::ProcEntry;
use alloc::vec::Vec;
pub fn pid_entries(pid: i32) -> Vec {
- let base = (pid as u64) << 20;
+ let base = (pid as u64) << PID_INODE_SHIFT;
alloc::vec![
ProcEntry::file("status", base | 1),
ProcEntry::file("stat", base | 2),
diff --git a/src/fs/procfs/pid_inode.rs b/src/fs/procfs/pid_inode.rs
index 6e91148275..fd5739b92c 100644
--- a/src/fs/procfs/pid_inode.rs
+++ b/src/fs/procfs/pid_inode.rs
@@ -14,11 +14,15 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-/* A pid directory's inode is pid * 1000 + 100. A negative pid has none: cast
-to u64 it would overflow the product. */
+/* Every inode under a pid directory is (pid << PID_INODE_SHIFT) | k: the
+directory itself is k = 0 and its entries k = 1 to 103. Root entries sit below
+1 << PID_INODE_SHIFT. Only pids from 1 have a directory, so none of these
+numbers is the root's, a root entry's or another pid's. */
+pub(crate) const PID_INODE_SHIFT: u32 = 20;
+
pub(crate) fn pid_dir_inode(pid: i32) -> Option {
- if pid < 0 {
+ if pid <= 0 {
return None;
}
- Some(pid as u64 * 1000 + 100)
+ Some((pid as u64) << PID_INODE_SHIFT)
}
diff --git a/src/fs/utils/scan_config.rs b/src/fs/utils/scan_config.rs
index 7641d7a05a..96407945d7 100644
--- a/src/fs/utils/scan_config.rs
+++ b/src/fs/utils/scan_config.rs
@@ -67,8 +67,7 @@ pub fn scan_with_config(dir_path: &str, config: &ScanConfig) -> UtilsResult,
@@ -97,6 +98,7 @@ impl Default for ScanConfig {
fn default() -> Self {
Self {
include_hidden: true,
+ only_hidden: false,
max_depth: MAX_SCAN_DEPTH,
max_files: MAX_SCAN_FILES,
extensions: Vec::new(),
@@ -111,6 +113,7 @@ impl ScanConfig {
pub const fn new() -> Self {
Self {
include_hidden: true,
+ only_hidden: false,
max_depth: MAX_SCAN_DEPTH,
max_files: MAX_SCAN_FILES,
extensions: Vec::new(),
@@ -132,9 +135,21 @@ impl ScanConfig {
pub fn hidden_only(mut self) -> Self {
self.include_hidden = true;
+ self.only_hidden = true;
self
}
+ /* Whether a file survives the hidden-file settings: a hidden file needs
+ include_hidden, and a visible one is dropped when only hidden files are
+ asked for. */
+ pub const fn admits_hidden(&self, hidden: bool) -> bool {
+ if hidden {
+ self.include_hidden
+ } else {
+ !self.only_hidden
+ }
+ }
+
pub fn sensitive_only(mut self, level: SensitivityLevel) -> Self {
self.sensitivity_threshold = level;
self
diff --git a/src/memory/addr/phys.rs b/src/memory/addr/phys.rs
index 183cb06b7c..fb5f682526 100644
--- a/src/memory/addr/phys.rs
+++ b/src/memory/addr/phys.rs
@@ -40,15 +40,31 @@ impl PhysAddr {
}
pub const fn is_aligned(self, align: u64) -> bool {
- align != 0 && self.0 % align == 0
+ align != 0 && self.0.is_multiple_of(align)
}
+ /* Rounds to a multiple of any non-zero alignment, not only a power of
+ two, so the result always passes is_aligned. An alignment of zero leaves
+ the address unchanged. */
pub const fn align_down(self, align: u64) -> Self {
- Self(self.0 & !(align - 1))
+ if align == 0 {
+ return self;
+ }
+ Self(self.0 - self.0 % align)
}
+ /* As align_down, rounding up. It overflows only when the rounded address
+ is past u64::MAX. */
pub const fn align_up(self, align: u64) -> Self {
- Self((self.0 + align - 1) & !(align - 1))
+ if align == 0 {
+ return self;
+ }
+ let rem = self.0 % align;
+ if rem == 0 {
+ self
+ } else {
+ Self(self.0 + (align - rem))
+ }
}
}
diff --git a/src/memory/addr/virt.rs b/src/memory/addr/virt.rs
index 03c771afb8..bca99611ad 100644
--- a/src/memory/addr/virt.rs
+++ b/src/memory/addr/virt.rs
@@ -48,15 +48,31 @@ impl VirtAddr {
}
pub const fn is_aligned(self, align: u64) -> bool {
- align != 0 && self.0 % align == 0
+ align != 0 && self.0.is_multiple_of(align)
}
+ /* Rounds to a multiple of any non-zero alignment, not only a power of
+ two, so the result always passes is_aligned. An alignment of zero leaves
+ the address unchanged. */
pub const fn align_down(self, align: u64) -> Self {
- Self(self.0 & !(align - 1))
+ if align == 0 {
+ return self;
+ }
+ Self(self.0 - self.0 % align)
}
+ /* As align_down, rounding up. It overflows only when the rounded address
+ is past u64::MAX. */
pub const fn align_up(self, align: u64) -> Self {
- Self((self.0 + align - 1) & !(align - 1))
+ if align == 0 {
+ return self;
+ }
+ let rem = self.0 % align;
+ if rem == 0 {
+ self
+ } else {
+ Self(self.0 + (align - rem))
+ }
}
}
diff --git a/src/memory/layout/manager/mod.rs b/src/memory/layout/manager/mod.rs
index bf687b3a53..f39e6ef5a7 100644
--- a/src/memory/layout/manager/mod.rs
+++ b/src/memory/layout/manager/mod.rs
@@ -19,6 +19,7 @@ mod align;
mod kaslr_ops;
mod percpu;
mod regions;
+mod stack_slots;
mod state;
pub use address::{in_kernel_space, in_user_space, is_canonical, range, selfref_l4_va};
diff --git a/src/memory/layout/manager/percpu.rs b/src/memory/layout/manager/percpu.rs
index 2a5cd62f12..17c8dd348f 100644
--- a/src/memory/layout/manager/percpu.rs
+++ b/src/memory/layout/manager/percpu.rs
@@ -16,6 +16,7 @@
use super::super::constants::*;
use super::super::types::*;
+use super::stack_slots::stack_slot_offset;
use super::state::kernel_sections;
use alloc::vec::Vec;
@@ -24,17 +25,15 @@ pub fn get_all_stack_regions() -> Vec {
for cpu_id in 0..MAX_CPUS {
let stack_base = PERCPU_BASE.saturating_add((cpu_id as u64).saturating_mul(PERCPU_STRIDE));
regions.push(StackRegion {
- base: stack_base,
+ base: stack_base.saturating_add(stack_slot_offset(0)),
size: KSTACK_SIZE,
guard_size: GUARD_PAGES * PAGE_SIZE,
cpu_id: Some(cpu_id),
thread_id: None,
});
for ist_num in 0..IST_STACKS_PER_CPU {
- let ist_offset = (KSTACK_SIZE as u64)
- .saturating_add((ist_num as u64).saturating_mul(IST_STACK_SIZE as u64));
regions.push(StackRegion {
- base: stack_base.saturating_add(ist_offset),
+ base: stack_base.saturating_add(stack_slot_offset(ist_num + 1)),
size: IST_STACK_SIZE,
guard_size: GUARD_PAGES * PAGE_SIZE,
cpu_id: Some(cpu_id),
diff --git a/src/memory/layout/manager/stack_slots.rs b/src/memory/layout/manager/stack_slots.rs
new file mode 100644
index 0000000000..6e3d90205e
--- /dev/null
+++ b/src/memory/layout/manager/stack_slots.rs
@@ -0,0 +1,30 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::super::constants::{GUARD_PAGES, IST_STACK_SIZE, KSTACK_SIZE, PAGE_SIZE};
+
+/* Offset of stack slot `slot` from the start of a CPU's stack area. Slot 0 is
+the kernel stack and slot i + 1 is IST stack i. A guard sits below the first
+stack, between every two stacks and above the last, so no stack's guard is a
+page of another stack. */
+pub(crate) const fn stack_slot_offset(slot: usize) -> u64 {
+ let guard = (GUARD_PAGES * PAGE_SIZE) as u64;
+ if slot == 0 {
+ return guard;
+ }
+ let ist = slot as u64 - 1;
+ guard + KSTACK_SIZE as u64 + guard + ist * (IST_STACK_SIZE as u64 + guard)
+}
diff --git a/src/memory/layout/types/percpu.rs b/src/memory/layout/types/percpu.rs
index 499077cbb1..dec7b5fe59 100644
--- a/src/memory/layout/types/percpu.rs
+++ b/src/memory/layout/types/percpu.rs
@@ -28,11 +28,13 @@ impl PercpuRegion {
#[inline]
pub const fn end(&self) -> u64 {
- self.base + self.size as u64
+ self.base.saturating_add(self.size as u64)
}
#[inline]
+ /* Offset from base, so a region flush against the top of the address
+ space neither overflows nor loses its last byte. */
pub const fn contains(&self, addr: u64) -> bool {
- addr >= self.base && addr < self.end()
+ addr >= self.base && addr - self.base < self.size as u64
}
}
diff --git a/src/memory/mmio/types/stats_snapshot.rs b/src/memory/mmio/types/stats_snapshot.rs
index 81822acf7f..42579eed14 100644
--- a/src/memory/mmio/types/stats_snapshot.rs
+++ b/src/memory/mmio/types/stats_snapshot.rs
@@ -27,7 +27,8 @@ impl MmioStatsSnapshot {
Self { total_regions: 0, total_mapped_size: 0, read_operations: 0, write_operations: 0 }
}
+ /* The counters wrap on their own; their sum saturates rather than abort. */
pub const fn total_operations(&self) -> u64 {
- self.read_operations + self.write_operations
+ self.read_operations.saturating_add(self.write_operations)
}
}
diff --git a/tools/ratchets/proven_functions.py b/tools/ratchets/proven_functions.py
index d55499096d..1731eb6918 100755
--- a/tools/ratchets/proven_functions.py
+++ b/tools/ratchets/proven_functions.py
@@ -25,7 +25,9 @@
This walks the refinement modules with comments stripped, so a function named
only in a file header does not count as proven, and fails when the proven count
falls below the floor or when the unproven gap grows past its ceiling. A name
-counts only in the modules that import its own crate's generated code.
+counts only in the modules that import its own crate's generated code, and a
+function counts as substantive only when a theorem statement names it or a name
+the file gives it.
"""
import argparse
@@ -35,13 +37,13 @@
from pathlib import Path
# Raise these when the numbers improve. They may never be lowered.
-FLOOR = 981
+FLOOR = 987
GAP_CEILING = 0
# Functions carrying a property beyond "this wrapper is its method". That
# wrapper theorem is real, and it is what ties a manifest entry to the method a
# theorem talks about, but on its own it says nothing about behaviour. Counting
# the two together would be the inflation this file exists to stop.
-SUBSTANTIVE_FLOOR = 336
+SUBSTANTIVE_FLOOR = 494
PROOF_MODULES = ('CapsComplete.lean', 'Closure.lean')
PROOF_DIRS = (
@@ -97,6 +99,34 @@ def mirrored_sources(root):
return {k: v for k, v in out.items() if len(v) > 1}
+THEOREM = re.compile(r'^(?:private\s+)?theorem\s+(\S+)(.*?):=', re.S | re.M)
+RENAMING = re.compile(r'\b(\w+)\s*→\s*(\w+)')
+ALIAS = re.compile(
+ r'^(?:private\s+|noncomputable\s+)*(?:abbrev|def)\s+(\w+)([^\n]*(?:\n[ \t]+[^\n]*)*)',
+ re.M)
+
+
+def statements(text):
+ """The statements of a file's theorems, wrappers left out.
+
+ A name in a proof, an `open ... renaming` line, an `attribute` list or a
+ `#print axioms` line is not a property of the function. Six functions once
+ counted as substantive from those lines alone.
+ """
+ return [sig for name, sig in THEOREM.findall(text)
+ if not re.match(r'the_\w+_wrapper_is_its_method$', name)]
+
+
+def aliases(texts, leaf):
+ """Names a file gives the function: `open ... renaming` and `abbrev`."""
+ names = {leaf}
+ word = re.compile(r'\b%s\b' % re.escape(leaf))
+ for t in texts:
+ names.update(b for a, b in RENAMING.findall(t) if a == leaf)
+ names.update(m.group(1) for m in ALIAS.finditer(t) if word.search(m.group(2)))
+ return names
+
+
def classify(root):
# Read the crate manifest, not EVIDENCE.json. The evidence script calls this
# to fill its own counts field, so reading its output here would make the
@@ -105,33 +135,25 @@ def classify(root):
(root / 'verification/extraction/crates.json').read_text())
names = sorted({s for c in manifest['crates'] for s in c['starts']})
files = proof_files(root)
- # A wrapper theorem names the function only on its own line; a substantive
- # theorem names it somewhere else. Strip the generated wrapper block and see
- # what still mentions it.
- # Only continuation lines, never blank ones: `\s+` matches a newline, so a
- # greedy version of this ate everything after the first wrapper theorem and
- # reported every crate as wrapper-only.
- wrapper = re.compile(
- r'theorem the_\w+_wrapper_is_its_method[^\n]*\n(?:[ \t]+[^\n]*\n)*')
# A name counts only in files that import its own crate's module. Leaf
# names repeat across crates (`new`, `is_present`, `leaf`), and matching
# them anywhere once counted functions no theorem mentions.
own = {}
for c in manifest['crates']:
mine = [t for t in files if imports_module(t, c['lean'])]
- text = '\n'.join(mine)
for st in c['starts']:
- prev = own.get(st, ('', ''))
- own[st] = (prev[0] + '\n' + text, prev[1] + '\n' + wrapper.sub('', text))
+ prev = own.setdefault(st, [])
+ prev.extend(mine)
proven, bare, substantive = [], [], []
for name in names:
leaf = name.split('::')[-1]
- pat = r'\b%s\b' % re.escape(leaf)
- code, without_wrappers = own.get(name, ('', ''))
- if re.search(pat, code):
+ mine = own.get(name, [])
+ if re.search(r'\b%s\b' % re.escape(leaf), '\n'.join(mine)):
proven.append(name)
- if re.search(pat, without_wrappers):
+ pat = re.compile(r'\b(%s)\b' % '|'.join(
+ map(re.escape, sorted(aliases(mine, leaf)))))
+ if any(pat.search(sig) for t in mine for sig in statements(t)):
substantive.append(name)
else:
bare.append(name)
diff --git a/userland/kernel_proofs/src/addr_align/mod.rs b/userland/kernel_proofs/src/addr_align/mod.rs
new file mode 100644
index 0000000000..d977c8b15e
--- /dev/null
+++ b/userland/kernel_proofs/src/addr_align/mod.rs
@@ -0,0 +1,30 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * Address rounding lands on a multiple of the alignment.
+ *
+ * The kernel's physical and virtual address types are included by path.
+ * align_down and align_up cleared low bits with !(align - 1), which rounds
+ * only for a power of two, so ten aligned down to three gave eight, and an
+ * alignment of zero underflowed. The checks below fail against that code.
+ */
+
+#[path = "../../../../src/memory/addr/phys.rs"]
+pub mod phys;
+#[path = "../../../../src/memory/addr/virt.rs"]
+pub mod virt;
+mod tests;
diff --git a/userland/kernel_proofs/src/addr_align/tests.rs b/userland/kernel_proofs/src/addr_align/tests.rs
new file mode 100644
index 0000000000..30a01cf637
--- /dev/null
+++ b/userland/kernel_proofs/src/addr_align/tests.rs
@@ -0,0 +1,50 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::phys::PhysAddr;
+use super::virt::VirtAddr;
+
+#[test]
+fn physical_rounding_lands_on_a_multiple() {
+ for align in [1u64, 3, 4, 7, 12, 4096, 4097] {
+ for addr in [0u64, 1, 10, 4095, 4096, 12_345, 1 << 40] {
+ let down = PhysAddr::new(addr).align_down(align);
+ let up = PhysAddr::new(addr).align_up(align);
+ assert!(down.is_aligned(align), "{addr} down to {align}");
+ assert!(up.is_aligned(align), "{addr} up to {align}");
+ assert!(down.as_u64() <= addr && addr - down.as_u64() < align);
+ assert!(up.as_u64() >= addr && up.as_u64() - addr < align);
+ }
+ }
+ assert_eq!(PhysAddr::new(10).align_down(3).as_u64(), 9);
+ assert_eq!(PhysAddr::new(10).align_up(3).as_u64(), 12);
+}
+
+#[test]
+fn virtual_rounding_lands_on_a_multiple() {
+ assert_eq!(VirtAddr::new(10).align_down(3).as_u64(), 9);
+ assert_eq!(VirtAddr::new(10).align_up(3).as_u64(), 12);
+ assert_eq!(VirtAddr::new(0x1234).align_down(4096).as_u64(), 0x1000);
+ assert_eq!(VirtAddr::new(0x1234).align_up(4096).as_u64(), 0x2000);
+}
+
+#[test]
+fn an_alignment_of_zero_leaves_the_address() {
+ assert_eq!(PhysAddr::new(4097).align_down(0).as_u64(), 4097);
+ assert_eq!(PhysAddr::new(4097).align_up(0).as_u64(), 4097);
+ assert_eq!(VirtAddr::new(4097).align_down(0).as_u64(), 4097);
+ assert_eq!(VirtAddr::new(4097).align_up(0).as_u64(), 4097);
+}
diff --git a/userland/kernel_proofs/src/bti_pad/mod.rs b/userland/kernel_proofs/src/bti_pad/mod.rs
new file mode 100644
index 0000000000..1af0ac29ca
--- /dev/null
+++ b/userland/kernel_proofs/src/bti_pad/mod.rs
@@ -0,0 +1,30 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * A NOP is not a BTI landing pad, and PACIASP and PACIBSP are.
+ *
+ * The kernel's landing-pad check and its decoder are included by path. The
+ * check accepted the NOP, which faults as a branch target on a core with
+ * FEAT_BTI, and rejected PACIASP and PACIBSP, which act as BTI c. The check
+ * below fails against that code.
+ */
+
+#[path = "../../../../src/arch/aarch64/security/bti/landing.rs"]
+pub mod landing;
+#[path = "../../../../src/arch/aarch64/security/bti/pad.rs"]
+pub mod pad;
+mod tests;
diff --git a/userland/kernel_proofs/src/bti_pad/tests.rs b/userland/kernel_proofs/src/bti_pad/tests.rs
new file mode 100644
index 0000000000..1d4f2d0812
--- /dev/null
+++ b/userland/kernel_proofs/src/bti_pad/tests.rs
@@ -0,0 +1,35 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::landing::check_bti_landing_pad;
+use super::pad::{BTI_C, BTI_J, BTI_JC, PACIASP, PACIBSP};
+
+const NOP: u32 = 0xD503201F;
+const BARE_BTI: u32 = 0xD503241F;
+
+fn check(word: u32) -> bool {
+ check_bti_landing_pad(&word as *const u32 as u64)
+}
+
+#[test]
+fn only_real_landing_pads_pass() {
+ for pad in [BTI_C, BTI_J, BTI_JC, PACIASP, PACIBSP] {
+ assert!(check(pad), "{pad:#x}");
+ }
+ assert!(!check(NOP));
+ assert!(!check(BARE_BTI));
+ assert!(!check(0));
+}
diff --git a/userland/kernel_proofs/src/efi_time/mod.rs b/userland/kernel_proofs/src/efi_time/mod.rs
new file mode 100644
index 0000000000..f9fe6e67b2
--- /dev/null
+++ b/userland/kernel_proofs/src/efi_time/mod.rs
@@ -0,0 +1,27 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * EFI times follow the calendar and count back before 1970.
+ *
+ * The kernel's EfiTime comes from the UEFI modules the crate already includes
+ * by path. is_valid bounded the day by 31 in every month, so February 31
+ * passed and converted as March 3, and to_unix_timestamp only counted years
+ * forward from 1970, so every date from 1900 to 1969 converted as a date in
+ * 1970. The checks below fail against that code.
+ */
+
+mod tests;
diff --git a/userland/kernel_proofs/src/efi_time/tests.rs b/userland/kernel_proofs/src/efi_time/tests.rs
new file mode 100644
index 0000000000..d9513783cb
--- /dev/null
+++ b/userland/kernel_proofs/src/efi_time/tests.rs
@@ -0,0 +1,41 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use crate::arch::x86_64::uefi::tables::time::EfiTime;
+
+fn midnight(year: u16, month: u8, day: u8) -> EfiTime {
+ EfiTime { year, month, day, timezone: EfiTime::TIMEZONE_UNSPECIFIED, ..EfiTime::default() }
+}
+
+#[test]
+fn days_past_the_end_of_the_month_are_invalid() {
+ assert!(!midnight(2021, 2, 31).is_valid());
+ assert!(!midnight(2023, 2, 29).is_valid());
+ assert!(!midnight(2100, 2, 29).is_valid());
+ assert!(!midnight(2024, 4, 31).is_valid());
+ assert!(midnight(2024, 2, 29).is_valid());
+ assert!(midnight(2000, 2, 29).is_valid());
+ assert!(midnight(2024, 12, 31).is_valid());
+}
+
+#[test]
+fn dates_before_1970_count_back_from_the_epoch() {
+ assert_eq!(midnight(1970, 1, 1).to_unix_timestamp(), 0);
+ assert_eq!(midnight(1969, 12, 31).to_unix_timestamp(), -86_400);
+ assert_eq!(midnight(1900, 1, 1).to_unix_timestamp(), -2_208_988_800);
+ assert_eq!(midnight(1970, 12, 31).to_unix_timestamp(), 31_449_600);
+ assert_eq!(midnight(2000, 3, 1).to_unix_timestamp(), 951_868_800);
+}
diff --git a/userland/kernel_proofs/src/elf/loader/core/mod.rs b/userland/kernel_proofs/src/elf/loader/core/mod.rs
index 32f6f5fdb6..9910c7b85e 100644
--- a/userland/kernel_proofs/src/elf/loader/core/mod.rs
+++ b/userland/kernel_proofs/src/elf/loader/core/mod.rs
@@ -1,2 +1,4 @@
// NONOS Operating System (AGPL-3.0-or-later)
pub mod parse_header;
+#[path = "../../../../../../src/elf/loader/core/section.rs"]
+pub mod section;
diff --git a/userland/kernel_proofs/src/elf_section_tests.rs b/userland/kernel_proofs/src/elf_section_tests.rs
new file mode 100644
index 0000000000..317226dbdf
--- /dev/null
+++ b/userland/kernel_proofs/src/elf_section_tests.rs
@@ -0,0 +1,48 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * is_symtab must name the full symbol table only.
+ *
+ * It accepted SHT_DYNSYM as well, so get_symbol_table returned .dynsym on an
+ * image that places it first. The check below fails against that code.
+ */
+
+use crate::elf::loader::core::section::ParsedSection;
+
+fn section(section_type: u32) -> ParsedSection {
+ ParsedSection {
+ name: "s".into(),
+ section_type,
+ flags: 0,
+ addr: 0,
+ offset: 0,
+ size: 0,
+ link: 0,
+ info: 0,
+ alignment: 0,
+ entry_size: 0,
+ }
+}
+
+#[test]
+fn only_sht_symtab_is_a_symbol_table() {
+ assert!(section(2).is_symtab());
+ assert!(!section(11).is_symtab());
+ for t in (0..64).filter(|&t| t != 2) {
+ assert!(!section(t).is_symtab(), "type {t}");
+ }
+}
diff --git a/userland/kernel_proofs/src/firmware_arith/mod.rs b/userland/kernel_proofs/src/firmware_arith/mod.rs
new file mode 100644
index 0000000000..443301c589
--- /dev/null
+++ b/userland/kernel_proofs/src/firmware_arith/mod.rs
@@ -0,0 +1,37 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * Arithmetic on firmware-supplied values must not overflow.
+ *
+ * The kernel's UEFI memory descriptor, per-CPU region, I/O APIC entry, and
+ * MMIO and port statistics snapshots are included by path. Each added or
+ * multiplied values that firmware or a wrapping counter controls with an
+ * overflow-checked operator, which aborts the kernel, or in this release build
+ * wraps. The checks below fail against that code.
+ */
+
+#[path = "../../../../src/arch/x86_64/acpi/data/ioapic.rs"]
+pub mod ioapic;
+#[path = "../../../../src/arch/x86_64/uefi/tables/memory_desc.rs"]
+pub mod memory_desc;
+#[path = "../../../../src/memory/layout/types/percpu.rs"]
+pub mod percpu;
+#[path = "../../../../src/memory/mmio/types/stats_snapshot.rs"]
+pub mod stats_snapshot;
+#[path = "../../../../src/arch/x86_64/port/stats_snapshot.rs"]
+pub mod port_snapshot;
+mod tests;
diff --git a/userland/kernel_proofs/src/firmware_arith/tests.rs b/userland/kernel_proofs/src/firmware_arith/tests.rs
new file mode 100644
index 0000000000..04564a78d7
--- /dev/null
+++ b/userland/kernel_proofs/src/firmware_arith/tests.rs
@@ -0,0 +1,77 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::ioapic::IoApicInfo;
+use super::memory_desc::MemoryDescriptor;
+use super::percpu::PercpuRegion;
+use super::port_snapshot::PortStatsSnapshot;
+use super::stats_snapshot::MmioStatsSnapshot;
+
+const TOP_PAGE: u64 = 0xFFFF_FFFF_FFFF_F000;
+
+fn descriptor(start: u64, pages: u64) -> MemoryDescriptor {
+ MemoryDescriptor {
+ memory_type: 7,
+ physical_start: start,
+ virtual_start: 0,
+ number_of_pages: pages,
+ attribute: 0,
+ }
+}
+
+#[test]
+fn memory_descriptors_saturate_instead_of_overflowing() {
+ assert_eq!(descriptor(0, (1 << 52) - 1).size_bytes(), TOP_PAGE);
+ assert_eq!(descriptor(0, 1 << 52).size_bytes(), u64::MAX);
+ assert_eq!(descriptor(TOP_PAGE, 1).end_address(), u64::MAX);
+ assert_eq!(descriptor(0x1000, 2).end_address(), 0x3000);
+}
+
+#[test]
+fn the_last_percpu_page_holds_its_own_bytes() {
+ let region = PercpuRegion::new(TOP_PAGE, 0x1000, 0);
+ assert!(region.contains(TOP_PAGE));
+ assert!(region.contains(u64::MAX));
+ assert!(!region.contains(TOP_PAGE - 1));
+ assert_eq!(region.end(), u64::MAX);
+}
+
+#[test]
+fn counts_from_firmware_and_counters_saturate() {
+ let chip = IoApicInfo { id: 0, address: 0, gsi_base: u32::MAX - 1 };
+ assert_eq!(chip.gsi_max(), u32::MAX);
+ let mut stats = MmioStatsSnapshot::new();
+ stats.read_operations = u64::MAX;
+ stats.write_operations = 1;
+ assert_eq!(stats.total_operations(), u64::MAX);
+}
+
+#[test]
+fn port_statistics_totals_saturate() {
+ let bytes = PortStatsSnapshot { bytes_read: u64::MAX, bytes_written: 1, ..Default::default() };
+ assert_eq!(bytes.total_bytes(), u64::MAX);
+ let ops = PortStatsSnapshot { read_ops: u64::MAX, string_write_ops: 1, ..Default::default() };
+ assert_eq!(ops.total_ops(), u64::MAX);
+ let small = PortStatsSnapshot {
+ read_ops: 1,
+ write_ops: 2,
+ string_read_ops: 3,
+ string_write_ops: 4,
+ io_delays: 100,
+ ..Default::default()
+ };
+ assert_eq!(small.total_ops(), 10);
+}
diff --git a/userland/kernel_proofs/src/iommu_access/mod.rs b/userland/kernel_proofs/src/iommu_access/mod.rs
new file mode 100644
index 0000000000..2c499f60da
--- /dev/null
+++ b/userland/kernel_proofs/src/iommu_access/mod.rs
@@ -0,0 +1,29 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * A remapping unit's register accessors refuse every offset past the window.
+ *
+ * The kernel's accessors are included by path and run against a heap buffer
+ * standing in for the mapped page. They checked offset + width against the
+ * window, which wraps for an offset near usize::MAX where overflow checks are
+ * off, so such an offset passed the check and was read. The check below fails
+ * against that code.
+ */
+
+#[path = "../../../../src/arch/x86_64/iommu/unit/access.rs"]
+pub mod access;
+mod tests;
diff --git a/userland/kernel_proofs/src/iommu_access/tests.rs b/userland/kernel_proofs/src/iommu_access/tests.rs
new file mode 100644
index 0000000000..d6173b8e3c
--- /dev/null
+++ b/userland/kernel_proofs/src/iommu_access/tests.rs
@@ -0,0 +1,44 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::access::{RemapUnit, UNIT_WINDOW};
+use std::panic::{catch_unwind, AssertUnwindSafe};
+
+fn unit(page: &[u64]) -> RemapUnit {
+ // SAFETY: page is a live buffer of UNIT_WINDOW bytes for this test.
+ unsafe { RemapUnit::from_mapped(page.as_ptr() as u64, 0xFED9_0000) }
+}
+
+#[test]
+fn the_last_register_in_the_window_is_read() {
+ let mut page = vec![0u64; UNIT_WINDOW / 8];
+ *page.last_mut().unwrap() = 0x1122_3344_5566_7788;
+ let u = unit(&page);
+ assert_eq!(u.read64(UNIT_WINDOW - 8), 0x1122_3344_5566_7788);
+ assert_eq!(u.read32(UNIT_WINDOW - 4), 0x1122_3344);
+}
+
+#[test]
+fn offsets_past_the_window_are_refused_even_when_they_would_wrap() {
+ let page = vec![0u64; UNIT_WINDOW / 8];
+ let u = unit(&page);
+ for offset in [UNIT_WINDOW - 3, UNIT_WINDOW, usize::MAX - 3, usize::MAX] {
+ assert!(catch_unwind(AssertUnwindSafe(|| u.read32(offset))).is_err(), "{offset:#x}");
+ }
+ for offset in [UNIT_WINDOW - 7, usize::MAX - 7, usize::MAX] {
+ assert!(catch_unwind(AssertUnwindSafe(|| u.read64(offset))).is_err(), "{offset:#x}");
+ }
+}
diff --git a/userland/kernel_proofs/src/iommu_window/mod.rs b/userland/kernel_proofs/src/iommu_window/mod.rs
new file mode 100644
index 0000000000..7823f7cf4b
--- /dev/null
+++ b/userland/kernel_proofs/src/iommu_window/mod.rs
@@ -0,0 +1,32 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * A remapping unit whose registers lie past the mapped page must be refused.
+ *
+ * The kernel's register decoders and registers_fit are included by path. The
+ * probe mapped 4 KiB and then used IOTLB and fault-record offsets that CAP and
+ * ECAP place up to 16 KiB in, with only a debug_assert on the window. The
+ * checks below do not build against that code, which had no registers_fit.
+ */
+
+#[path = "../../../../src/arch/x86_64/iommu/regs/cap/fault.rs"]
+pub mod cap;
+#[path = "../../../../src/arch/x86_64/iommu/regs/offsets/invalidate.rs"]
+pub mod offsets;
+#[path = "../../../../src/arch/x86_64/iommu/regs/window.rs"]
+pub mod window;
+mod tests;
diff --git a/userland/kernel_proofs/src/iommu_window/tests.rs b/userland/kernel_proofs/src/iommu_window/tests.rs
new file mode 100644
index 0000000000..e04c0ff659
--- /dev/null
+++ b/userland/kernel_proofs/src/iommu_window/tests.rs
@@ -0,0 +1,42 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::window::registers_fit;
+
+const PAGE: usize = 4096;
+
+#[test]
+fn registers_past_the_page_are_refused() {
+ assert!(registers_fit(0, 0xFF << 8, PAGE));
+ assert!(!registers_fit(0, 0x100 << 8, PAGE));
+ assert!(registers_fit(0xFF << 24, 0, PAGE));
+ assert!(!registers_fit(0x100 << 24, 0, PAGE));
+ assert!(!registers_fit((0xF0 << 24) | (0x10 << 40), 0, PAGE));
+}
+
+#[test]
+fn every_field_value_agrees_with_the_byte_bounds() {
+ for iro in 0..1024u64 {
+ let fits = iro * 16 + 16 <= PAGE as u64;
+ assert_eq!(registers_fit(0, iro << 8, PAGE), fits, "iro {iro}");
+ }
+ for fro in (0..1024u64).step_by(7) {
+ for nfr in 0..256u64 {
+ let fits = fro * 16 + (nfr + 1) * 16 <= PAGE as u64;
+ assert_eq!(registers_fit((fro << 24) | (nfr << 40), 0, PAGE), fits);
+ }
+ }
+}
diff --git a/userland/kernel_proofs/src/layout_slots/constants/mod.rs b/userland/kernel_proofs/src/layout_slots/constants/mod.rs
new file mode 100644
index 0000000000..62583fa2d7
--- /dev/null
+++ b/userland/kernel_proofs/src/layout_slots/constants/mod.rs
@@ -0,0 +1,23 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#[path = "../../../../../src/memory/layout/constants/page.rs"]
+pub mod page;
+#[path = "../../../../../src/memory/layout/constants/percpu.rs"]
+pub mod percpu;
+
+pub use page::PAGE_SIZE;
+pub use percpu::{GUARD_PAGES, IST_STACKS_PER_CPU, IST_STACK_SIZE, KSTACK_SIZE, PERCPU_STRIDE};
diff --git a/userland/kernel_proofs/src/layout_slots/manager/mod.rs b/userland/kernel_proofs/src/layout_slots/manager/mod.rs
new file mode 100644
index 0000000000..2e7dd31d62
--- /dev/null
+++ b/userland/kernel_proofs/src/layout_slots/manager/mod.rs
@@ -0,0 +1,18 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+#[path = "../../../../../src/memory/layout/manager/stack_slots.rs"]
+pub mod stack_slots;
diff --git a/userland/kernel_proofs/src/layout_slots/mod.rs b/userland/kernel_proofs/src/layout_slots/mod.rs
new file mode 100644
index 0000000000..3d79d5c115
--- /dev/null
+++ b/userland/kernel_proofs/src/layout_slots/mod.rs
@@ -0,0 +1,28 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * Every stack in a CPU's area must keep a guard page of its own.
+ *
+ * The kernel's stack slot arithmetic and the constants it reads are included
+ * by path. The stacks were packed back to back, so each stack's upper guard
+ * was the next stack's first page. The checks below do not build against that
+ * code, which had no stack_slot_offset.
+ */
+
+pub mod constants;
+pub mod manager;
+mod tests;
diff --git a/userland/kernel_proofs/src/layout_slots/tests.rs b/userland/kernel_proofs/src/layout_slots/tests.rs
new file mode 100644
index 0000000000..8f7516847a
--- /dev/null
+++ b/userland/kernel_proofs/src/layout_slots/tests.rs
@@ -0,0 +1,45 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::constants::{
+ GUARD_PAGES, IST_STACKS_PER_CPU, IST_STACK_SIZE, KSTACK_SIZE, PAGE_SIZE, PERCPU_STRIDE,
+};
+use super::manager::stack_slots::stack_slot_offset;
+
+fn slot(i: usize) -> (u64, u64) {
+ let size = if i == 0 { KSTACK_SIZE } else { IST_STACK_SIZE } as u64;
+ let base = stack_slot_offset(i);
+ (base, base + size)
+}
+
+#[test]
+fn no_guard_page_is_a_page_of_another_stack() {
+ let guard = (GUARD_PAGES * PAGE_SIZE) as u64;
+ let slots: Vec<_> = (0..=IST_STACKS_PER_CPU).map(slot).collect();
+ for (i, &(base, top)) in slots.iter().enumerate() {
+ let guards = [(base - guard, base), (top, top + guard)];
+ for (j, &(b, t)) in slots.iter().enumerate() {
+ if i == j {
+ continue;
+ }
+ for &(gs, ge) in &guards {
+ assert!(ge <= b || t <= gs, "guard of slot {i} overlaps slot {j}");
+ }
+ }
+ }
+ assert!(slots[0].0 >= guard);
+ assert!(slots[IST_STACKS_PER_CPU].1 + guard <= PERCPU_STRIDE);
+}
diff --git a/userland/kernel_proofs/src/lib.rs b/userland/kernel_proofs/src/lib.rs
index 60f5d95fcb..e8e2226d0f 100644
--- a/userland/kernel_proofs/src/lib.rs
+++ b/userland/kernel_proofs/src/lib.rs
@@ -21,14 +21,28 @@
extern crate alloc;
+#[cfg(test)]
+pub mod addr_align;
pub mod arch;
+#[cfg(test)]
+pub mod bti_pad;
pub mod bus;
pub mod capabilities;
+#[cfg(test)]
+pub mod efi_time;
pub mod elf;
#[cfg(test)]
pub mod fd_fork;
#[cfg(test)]
+pub mod firmware_arith;
+#[cfg(test)]
pub mod idt_vectors;
+#[cfg(test)]
+pub mod iommu_access;
+#[cfg(test)]
+pub mod iommu_window;
+#[cfg(test)]
+pub mod layout_slots;
pub mod memory;
#[cfg(test)]
pub mod pipe_counts;
@@ -41,21 +55,31 @@ pub mod procfs_inode;
#[cfg(test)]
pub mod riscv_mmu;
#[cfg(test)]
+pub mod scan_hidden;
+#[cfg(test)]
pub mod range_ends;
#[cfg(test)]
pub mod rsdp_address;
pub mod syscall;
pub mod time;
+#[cfg(test)]
+pub mod uefi_attrs;
+#[cfg(test)]
+pub mod uefi_revision;
pub mod security;
pub mod spec;
pub mod sys;
pub mod usercopy;
+#[cfg(test)]
+pub mod vga_attr;
#[cfg(test)]
mod align_tests;
#[cfg(test)]
mod authorization_tests;
#[cfg(test)]
+mod elf_section_tests;
+#[cfg(test)]
mod elf_tests;
#[cfg(test)]
mod inbox_name_tests;
diff --git a/userland/kernel_proofs/src/pci_address/mod.rs b/userland/kernel_proofs/src/pci_address/mod.rs
index 74fc0ec13a..d27d010481 100644
--- a/userland/kernel_proofs/src/pci_address/mod.rs
+++ b/userland/kernel_proofs/src/pci_address/mod.rs
@@ -17,12 +17,14 @@
/*
* PCI requester ids must keep device and function inside their fields.
*
- * Both kernel encoders are included by path: the IOMMU DeviceAddress and the
- * ACPI PciDevice. Neither masked the device to five bits, so device 32 on bus
+ * The kernel encoders are included by path: the IOMMU DeviceAddress, the
+ * ACPI PciDevice and the 0xCF8 configuration address. Neither masked the device to five bits, so device 32 on bus
* 0 packed as device 0 on bus 1 and named another device's DMA context; bdf
* did not mask the function either. The checks below fail against that code.
*/
+#[path = "../../../../src/drivers/pci/constants/address_packing.rs"]
+pub mod address_packing;
#[path = "../../../../src/memory/iommu/device.rs"]
pub mod device;
#[path = "../../../../src/arch/x86_64/acpi/devices/pci/types.rs"]
diff --git a/userland/kernel_proofs/src/pci_address/tests.rs b/userland/kernel_proofs/src/pci_address/tests.rs
index 3d61ca6d70..ea57057647 100644
--- a/userland/kernel_proofs/src/pci_address/tests.rs
+++ b/userland/kernel_proofs/src/pci_address/tests.rs
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+use super::address_packing::pci_config_address;
use super::device::DeviceAddress;
use super::types::PciDevice;
@@ -55,3 +56,16 @@ fn bdf_high_byte_is_the_bus() {
}
assert_ne!(pci(0, 0, 8).bdf(), pci(0, 1, 0).bdf());
}
+
+#[test]
+fn config_address_bus_field_is_the_bus() {
+ for device in 0..=u8::MAX {
+ for function in 0..=u8::MAX {
+ let word = pci_config_address(3, device, function, 0x40);
+ assert_eq!((word >> 16) & 0xFF, 3, "device {device} function {function}");
+ assert_eq!((word >> 11) & 0x1F, u32::from(device & 0x1F));
+ assert_eq!((word >> 8) & 0x7, u32::from(function & 0x7));
+ }
+ }
+ assert_ne!(pci_config_address(0, 32, 0, 0), pci_config_address(1, 0, 0, 0));
+}
diff --git a/userland/kernel_proofs/src/procfs_inode/mod.rs b/userland/kernel_proofs/src/procfs_inode/mod.rs
index ed4278a8a2..2892bc1897 100644
--- a/userland/kernel_proofs/src/procfs_inode/mod.rs
+++ b/userland/kernel_proofs/src/procfs_inode/mod.rs
@@ -15,11 +15,12 @@
// along with this program. If not, see .
/*
- * A procfs pid directory must not take its inode from a negative pid.
+ * A procfs pid directory's inode must be no other inode.
*
- * The kernel's pid_dir_inode is included by path. lookup_root used to compute
- * pid as u64 * 1000 + 100 on any parsed i32, so the name -1 overflowed. The
- * check below does not build against that code, which had no such function.
+ * The kernel's pid_dir_inode is included by path. The directory inode was
+ * pid * 1000 + 100: the name -1 overflowed it, pid 0 got the root sys entry's
+ * inode, and pid 131072 got pid 125's task entry inode. The checks below fail
+ * against that numbering.
*/
#[path = "../../../../src/fs/procfs/pid_inode.rs"]
diff --git a/userland/kernel_proofs/src/procfs_inode/tests.rs b/userland/kernel_proofs/src/procfs_inode/tests.rs
index 0deb249450..58f6b48894 100644
--- a/userland/kernel_proofs/src/procfs_inode/tests.rs
+++ b/userland/kernel_proofs/src/procfs_inode/tests.rs
@@ -17,12 +17,20 @@
use super::pid_inode::pid_dir_inode;
#[test]
-fn negative_pids_have_no_directory_inode() {
+fn only_pids_from_one_have_a_directory_inode() {
assert_eq!(pid_dir_inode(-1), None);
assert_eq!(pid_dir_inode(i32::MIN), None);
- assert_eq!(pid_dir_inode(0), Some(100));
- assert_eq!(pid_dir_inode(i32::MAX), Some(2_147_483_647_100));
- for pid in 0..2048 {
- assert_eq!(pid_dir_inode(pid), Some(pid as u64 * 1000 + 100));
+ assert_eq!(pid_dir_inode(0), None);
+ assert_eq!(pid_dir_inode(1), Some(1 << 20));
+ assert_eq!(pid_dir_inode(i32::MAX), Some((i32::MAX as u64) << 20));
+}
+
+#[test]
+fn directory_inodes_miss_root_and_entry_inodes() {
+ for pid in 1..4096 {
+ let ino = pid_dir_inode(pid).expect("inode");
+ assert!(ino >= 1 << 20, "pid {pid} reaches the root entries");
+ assert_eq!(ino & 0xF_FFFF, 0, "pid {pid} shares an entry's low bits");
}
+ assert_ne!(pid_dir_inode(131072), Some((125 << 20) | 100));
}
diff --git a/userland/kernel_proofs/src/rsdp_address/mod.rs b/userland/kernel_proofs/src/rsdp_address/mod.rs
index ce260af5fe..7a1f50a81e 100644
--- a/userland/kernel_proofs/src/rsdp_address/mod.rs
+++ b/userland/kernel_proofs/src/rsdp_address/mod.rs
@@ -15,12 +15,14 @@
// along with this program. If not, see .
/*
- * An RSDP below revision 2 must not hand out its XSDT field.
+ * An RSDP below revision 2 must not hand out its XSDT field, and a revision 2
+ * one must pass the full 36-byte checksum.
*
* The kernel's multiboot AcpiRsdp is included by path. table_address returned
* a nonzero XSDT pointer whatever the revision, although below revision 2 the
- * extended checksum that would cover it is never computed. The check below
- * fails against that code.
+ * extended checksum that would cover it is never computed, and the extended
+ * check passed a revision 2 RSDP with no extended fields and never summed the
+ * reserved bytes. The checks below fail against that code.
*/
#[path = "../../../../src/arch/x86_64/multiboot/modules_acpi.rs"]
diff --git a/userland/kernel_proofs/src/rsdp_address/tests.rs b/userland/kernel_proofs/src/rsdp_address/tests.rs
index 7df8436736..44e406a007 100644
--- a/userland/kernel_proofs/src/rsdp_address/tests.rs
+++ b/userland/kernel_proofs/src/rsdp_address/tests.rs
@@ -26,6 +26,7 @@ fn rsdp(revision: u8, xsdt: Option) -> AcpiRsdp {
length: None,
xsdt_address: xsdt,
extended_checksum: None,
+ reserved: None,
}
}
@@ -37,3 +38,43 @@ fn xsdt_is_used_only_from_revision_two() {
assert_eq!(rsdp(2, Some(0)).table_address(), 0x000E_0000);
assert_eq!(rsdp(2, None).table_address(), 0x000E_0000);
}
+
+fn byte_sum(bytes: &[u8]) -> u8 {
+ bytes.iter().fold(0u8, |a, &b| a.wrapping_add(b))
+}
+
+fn with_extension(mut r: AcpiRsdp, length: u32, reserved: [u8; 3]) -> AcpiRsdp {
+ let xsdt = r.xsdt_address.unwrap_or(0);
+ let sum = byte_sum(&r.signature)
+ .wrapping_add(r.checksum)
+ .wrapping_add(byte_sum(&r.oem_id))
+ .wrapping_add(r.revision)
+ .wrapping_add(byte_sum(&r.rsdt_address.to_le_bytes()))
+ .wrapping_add(byte_sum(&length.to_le_bytes()))
+ .wrapping_add(byte_sum(&xsdt.to_le_bytes()))
+ .wrapping_add(byte_sum(&reserved));
+ r.length = Some(length);
+ r.reserved = Some(reserved);
+ r.extended_checksum = Some(0u8.wrapping_sub(sum));
+ r
+}
+
+#[test]
+fn extended_checksum_needs_every_acpi2_field_and_the_reserved_bytes() {
+ assert!(!rsdp(2, None).verify_extended_checksum());
+ let good = with_extension(rsdp(2, Some(0xDEAD_0000)), 36, [1, 2, 3]);
+ assert!(good.verify_extended_checksum());
+ let mut tampered = good.clone();
+ tampered.reserved = Some([1, 2, 4]);
+ assert!(!tampered.verify_extended_checksum());
+ assert!(!with_extension(rsdp(2, Some(0xDEAD_0000)), 20, [0; 3]).verify_extended_checksum());
+ assert!(rsdp(1, None).verify_extended_checksum());
+}
+
+#[test]
+fn a_declared_length_other_than_36_is_refused() {
+ for length in [37u32, 40, 4096, u32::MAX] {
+ let longer = with_extension(rsdp(2, Some(0xDEAD_0000)), length, [0; 3]);
+ assert!(!longer.verify_extended_checksum(), "length {length}");
+ }
+}
diff --git a/userland/kernel_proofs/src/scan_hidden/mod.rs b/userland/kernel_proofs/src/scan_hidden/mod.rs
new file mode 100644
index 0000000000..15500306a1
--- /dev/null
+++ b/userland/kernel_proofs/src/scan_hidden/mod.rs
@@ -0,0 +1,28 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * A hidden-only scan configuration admits only hidden files.
+ *
+ * The kernel's scan configuration is included by path. hidden_only set
+ * include_hidden, which every fresh configuration already has, so
+ * ScanConfig::new().hidden_only() still admitted every visible file. The
+ * check below fails against that code.
+ */
+
+#[path = "../../../../src/fs/utils/types.rs"]
+pub mod types;
+mod tests;
diff --git a/userland/kernel_proofs/src/scan_hidden/tests.rs b/userland/kernel_proofs/src/scan_hidden/tests.rs
new file mode 100644
index 0000000000..1d4550855c
--- /dev/null
+++ b/userland/kernel_proofs/src/scan_hidden/tests.rs
@@ -0,0 +1,38 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::types::ScanConfig;
+
+#[test]
+fn a_fresh_configuration_admits_every_file() {
+ let config = ScanConfig::new();
+ assert!(config.admits_hidden(true));
+ assert!(config.admits_hidden(false));
+}
+
+#[test]
+fn hidden_only_admits_exactly_the_hidden_files() {
+ let config = ScanConfig::new().hidden_only();
+ assert!(config.admits_hidden(true));
+ assert!(!config.admits_hidden(false));
+}
+
+#[test]
+fn excluding_hidden_files_keeps_the_visible_ones() {
+ let config = ScanConfig { include_hidden: false, ..ScanConfig::new() };
+ assert!(!config.admits_hidden(true));
+ assert!(config.admits_hidden(false));
+}
diff --git a/userland/kernel_proofs/src/uefi_attrs/mod.rs b/userland/kernel_proofs/src/uefi_attrs/mod.rs
new file mode 100644
index 0000000000..5e40363ee9
--- /dev/null
+++ b/userland/kernel_proofs/src/uefi_attrs/mod.rs
@@ -0,0 +1,28 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * Every authenticated-write flag makes a variable need authentication.
+ *
+ * The kernel's variable attribute type comes from the UEFI modules the crate
+ * already includes by path. requires_authentication counted the time-based and
+ * enhanced flags but not the older count-based AUTHENTICATED_WRITE_ACCESS,
+ * which firmware still reports on variables written before UEFI 2.3.1, so a
+ * set carrying only that flag was answered as needing none. The check below
+ * fails against that code.
+ */
+
+mod tests;
diff --git a/userland/kernel_proofs/src/uefi_attrs/tests.rs b/userland/kernel_proofs/src/uefi_attrs/tests.rs
new file mode 100644
index 0000000000..5383c60209
--- /dev/null
+++ b/userland/kernel_proofs/src/uefi_attrs/tests.rs
@@ -0,0 +1,34 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use crate::arch::x86_64::uefi::types::VariableAttributes;
+
+#[test]
+fn each_authenticated_write_flag_needs_authentication() {
+ for flag in [
+ VariableAttributes::AUTHENTICATED_WRITE_ACCESS,
+ VariableAttributes::TIME_BASED_AUTHENTICATED_WRITE_ACCESS,
+ VariableAttributes::ENHANCED_AUTHENTICATED_ACCESS,
+ ] {
+ assert!(flag.requires_authentication(), "{:#x}", flag.bits());
+ let word = VariableAttributes::from_bits(
+ VariableAttributes::DEFAULT_NV_BS_RT.bits() | flag.bits(),
+ );
+ assert!(word.requires_authentication(), "{:#x}", word.bits());
+ }
+ assert!(!VariableAttributes::DEFAULT_NV_BS_RT.requires_authentication());
+ assert!(!VariableAttributes::from_bits(0x4F).requires_authentication());
+}
diff --git a/userland/kernel_proofs/src/uefi_revision/mod.rs b/userland/kernel_proofs/src/uefi_revision/mod.rs
new file mode 100644
index 0000000000..36f49e8b58
--- /dev/null
+++ b/userland/kernel_proofs/src/uefi_revision/mod.rs
@@ -0,0 +1,27 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * UEFI revisions use the specification's packing.
+ *
+ * The kernel's revision constants and firmware record come from the UEFI
+ * modules the crate already includes by path. The constants packed the minor
+ * version into the upper byte of the lower half, so 2.8 read back as minor
+ * 2048, while 2.3.1 alone used the specification's tens-and-units form and so
+ * sorted below 2.1. The checks below fail against that code.
+ */
+
+mod tests;
diff --git a/userland/kernel_proofs/src/uefi_revision/tests.rs b/userland/kernel_proofs/src/uefi_revision/tests.rs
new file mode 100644
index 0000000000..09aedecb3d
--- /dev/null
+++ b/userland/kernel_proofs/src/uefi_revision/tests.rs
@@ -0,0 +1,60 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use crate::arch::x86_64::uefi::constants::*;
+use crate::arch::x86_64::uefi::variable::FirmwareInfo;
+
+#[test]
+fn revisions_are_the_specification_values() {
+ assert_eq!(UEFI_REVISION_2_0, 0x0002_0000);
+ assert_eq!(UEFI_REVISION_2_1, 0x0002_000A);
+ assert_eq!(UEFI_REVISION_2_3, 0x0002_001E);
+ assert_eq!(UEFI_REVISION_2_3_1, 0x0002_001F);
+ assert_eq!(UEFI_REVISION_2_4, 0x0002_0028);
+ assert_eq!(UEFI_REVISION_2_5, 0x0002_0032);
+ assert_eq!(UEFI_REVISION_2_6, 0x0002_003C);
+ assert_eq!(UEFI_REVISION_2_7, 0x0002_0046);
+ assert_eq!(UEFI_REVISION_2_8, 0x0002_0050);
+ assert_eq!(UEFI_REVISION_2_9, 0x0002_005A);
+ assert_eq!(UEFI_REVISION_2_10, 0x0002_0064);
+}
+
+#[test]
+fn revisions_order_as_versions() {
+ let ordered = [
+ UEFI_REVISION_2_0,
+ UEFI_REVISION_2_1,
+ UEFI_REVISION_2_3,
+ UEFI_REVISION_2_3_1,
+ UEFI_REVISION_2_4,
+ UEFI_REVISION_2_5,
+ UEFI_REVISION_2_6,
+ UEFI_REVISION_2_7,
+ UEFI_REVISION_2_8,
+ UEFI_REVISION_2_9,
+ UEFI_REVISION_2_10,
+ ];
+ for pair in ordered.windows(2) {
+ assert!(pair[0] < pair[1], "{:#x} {:#x}", pair[0], pair[1]);
+ }
+}
+
+#[test]
+fn the_firmware_record_reads_2_8_back() {
+ let info = FirmwareInfo { revision: UEFI_REVISION_2_8, ..FirmwareInfo::default() };
+ assert_eq!(info.uefi_major_version(), 2);
+ assert_eq!(info.uefi_minor_version(), 80);
+}
diff --git a/userland/kernel_proofs/src/vga_attr/mod.rs b/userland/kernel_proofs/src/vga_attr/mod.rs
new file mode 100644
index 0000000000..5183598a84
--- /dev/null
+++ b/userland/kernel_proofs/src/vga_attr/mod.rs
@@ -0,0 +1,29 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+/*
+ * A bright background must not make VGA text blink.
+ *
+ * The boot attribute helpers are included by path. make_attr put the whole
+ * background in bits 4 to 7, and bit 7 is blink with the attribute
+ * controller's default setting, so a bright background blinked on the dark
+ * one; bg_color read bit 7 as a background bit. The checks below fail against
+ * that code. The arch ColorCode is covered for all colour pairs in Lean.
+ */
+
+#[path = "../../../../src/boot/vga/colors.rs"]
+pub mod colors;
+mod tests;
diff --git a/userland/kernel_proofs/src/vga_attr/tests.rs b/userland/kernel_proofs/src/vga_attr/tests.rs
new file mode 100644
index 0000000000..8fe3412bad
--- /dev/null
+++ b/userland/kernel_proofs/src/vga_attr/tests.rs
@@ -0,0 +1,26 @@
+// NONOS Operating System
+// Copyright (C) 2026 NONOS Contributors
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use super::colors::{bg_color, make_attr};
+
+#[test]
+fn a_bright_background_never_sets_the_blink_bit() {
+ for bg in 0..=u8::MAX {
+ assert_eq!(make_attr(15, bg) & 0x80, 0, "background {bg}");
+ }
+ assert_eq!(bg_color(0x8F), 0);
+ assert_eq!(bg_color(make_attr(15, 5)), 5);
+}
diff --git a/verification/evidence/EVIDENCE.json b/verification/evidence/EVIDENCE.json
index be25928fc8..cedf794a59 100644
--- a/verification/evidence/EVIDENCE.json
+++ b/verification/evidence/EVIDENCE.json
@@ -22,13 +22,13 @@
"harnesses": 100
},
"lean_extraction": {
- "bv_decide_calls": 12,
+ "bv_decide_calls": 10,
"bv_decide_theorems": 11,
"counts": {
- "extracted": 981,
- "proven": 981,
- "substantive": 336,
- "trivial": 645,
+ "extracted": 987,
+ "proven": 987,
+ "substantive": 494,
+ "trivial": 493,
"unproven": 0
},
"description": "Functions lowered from real Rust MIR by Charon and translated to Lean by Aeneas. Counted three ways, because they are not equally proven: substantive carries a property about behaviour, trivial carries only the theorem that its generated wrapper is the method it forwards to, and unproven carries nothing.",
@@ -309,6 +309,7 @@
"nonos_x_boot_types_context::exceptioncontext_is_user_mode",
"nonos_x_boot_types_context::exceptioncontext_stack_pointer",
"nonos_x_bti_guard::btiguard_instruction",
+ "nonos_x_bti_pad::is_bti_landing_pad",
"nonos_x_buddy_types_stats::allocstats_free_memory",
"nonos_x_buddy_types_stats::allocstats_new",
"nonos_x_cache_types::cachestatistics_new",
@@ -499,6 +500,7 @@
"nonos_x_iommu_domain_id::domainid_new",
"nonos_x_iommu_protection::iommuprotection_readable",
"nonos_x_iommu_protection::iommuprotection_writable",
+ "nonos_x_iommu_regs_window::registers_fit",
"nonos_x_irq_reserved::is_reserved",
"nonos_x_irq_reserved::reserve",
"nonos_x_key_management_types::keytype_key_length",
@@ -508,6 +510,7 @@
"nonos_x_key_management_types::keyusage_signing",
"nonos_x_key_management_types::keyusage_verification",
"nonos_x_keyring_capsule_types::keytype_to_u8",
+ "nonos_x_layout_stack_slots::stack_slot_offset",
"nonos_x_main_mode::is_microkernel",
"nonos_x_memory_boot_memory_types_handoff::boothandoff_has_capsule",
"nonos_x_memory_boot_memory_types_memory_region::memoryregion_has_flag",
@@ -953,12 +956,14 @@
"nonos_x_uefi_secure_boot_status::securebootstatus_can_modify_keys",
"nonos_x_uefi_secure_boot_status::securebootstatus_is_fully_configured",
"nonos_x_usercopy_walk_leaf::userleaf_bytes_remaining_in_page",
+ "nonos_x_utils_types::scanconfig_admits_hidden",
"nonos_x_utils_types::scanconfig_hidden_only",
"nonos_x_utils_types::scanconfig_new",
"nonos_x_utils_types::scanconfig_with_max_depth",
"nonos_x_validation_simd_level::simdlevel_register_width",
"nonos_x_variable_firmware::firmwareinfo_uefi_major_version",
"nonos_x_variable_firmware::firmwareinfo_uefi_minor_version",
+ "nonos_x_variable_firmware::uefi_revision",
"nonos_x_verify_caps_bits::grant_within_manifest",
"nonos_x_verify_caps_bits::install_caps",
"nonos_x_verify_caps_bits::within_ceiling",
@@ -968,6 +973,7 @@
"nonos_x_vga_constants::colorcode_background",
"nonos_x_vga_constants::colorcode_foreground",
"nonos_x_vga_constants::colorcode_is_blinking",
+ "nonos_x_vga_constants::colorcode_new",
"nonos_x_vga_constants::colorcode_value",
"nonos_x_vga_constants::screenchar_as_u16",
"nonos_x_walker_align::align4",
@@ -1078,11 +1084,11 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/AddrPhys.lean",
- "sha256": "191637030d078ee42ce20368f85ee4a0820c84dc5bda30555f743114074c9a35"
+ "sha256": "69ff673d65e46d6e83e9e7c6ff3a2a47c98faf05ef77309955d1826c510cdd71"
},
{
"path": "verification/extraction/lean/NonosExtraction/AddrVirt.lean",
- "sha256": "82394c4eb9ce6385932a13232d7d369e615b388871bdb4bd27ba0b369e0aa7c7"
+ "sha256": "8ac98e783cbe35ec4767210bde3cf32d1a1b9f92b1fea37f11d3021aa9e6947b"
},
{
"path": "verification/extraction/lean/NonosExtraction/AlgIdTypes.lean",
@@ -1192,6 +1198,10 @@
"path": "verification/extraction/lean/NonosExtraction/BtiGuard.lean",
"sha256": "52d94784f24c431893ac9cdcab8a491c486b3c60ea2abb983ca2ec4deb446ab7"
},
+ {
+ "path": "verification/extraction/lean/NonosExtraction/BtiPad.lean",
+ "sha256": "b1f2127c0b52e374e65bc6c33266df11030f7961728eb465972b102d55526e28"
+ },
{
"path": "verification/extraction/lean/NonosExtraction/BuddyTypesStats.lean",
"sha256": "a40f6266ff05f6ee725d64b876a61725b31843a7467072de37b11f837d7ce79d"
@@ -1262,7 +1272,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/ConstantsAddressPacking.lean",
- "sha256": "8fdb7bff26de67cba505f04154bb27a3290cdc9d06b3df573c71b56fdd1a31a3"
+ "sha256": "dd509f54fa2d55c61dcc6c7bc4bb81799ed627abd67ffca4d42fb9acfd080fa6"
},
{
"path": "verification/extraction/lean/NonosExtraction/ConstantsHelpers.lean",
@@ -1286,7 +1296,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/CoreSection.lean",
- "sha256": "92da08439bcbfa604108b7846066a860cf9a62d65470b1fed1dd2c2f7fe4ba09"
+ "sha256": "a1e2015514cda8d342f4667c68c9d846bfb1ca9fa8c627b8493fd4107b459aee"
},
{
"path": "verification/extraction/lean/NonosExtraction/CpuCacheAssoc.lean",
@@ -1322,7 +1332,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/DataIoapic.lean",
- "sha256": "a372b5840dc6c6bbf71f9cbdefbf7050c5c35f4f00a79551b622438f33df354c"
+ "sha256": "eb49e459e9b9c104eda3f3b72161ecc4bd4c62e6248382295df076ffadf81fa3"
},
{
"path": "verification/extraction/lean/NonosExtraction/DataNuma.lean",
@@ -1370,7 +1380,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/DriversPciTypesAddress.lean",
- "sha256": "f4565651c4310b386ba6455cb3d88bba07272d7d02871ac5c2abb2c9f89bb251"
+ "sha256": "c6bdc627e9dddb07b641acc59d1ecd6bdde8990da87e75542f2e3b5765459977"
},
{
"path": "verification/extraction/lean/NonosExtraction/DriversPciTypesBar.lean",
@@ -1504,6 +1514,10 @@
"path": "verification/extraction/lean/NonosExtraction/IommuProtection.lean",
"sha256": "02f1f92895022c6e63def5698b27f9bd1215f1f2bfee983b33d3112cdc11cfa3"
},
+ {
+ "path": "verification/extraction/lean/NonosExtraction/IommuRegsWindow.lean",
+ "sha256": "0d4f52beac664c63dbd7aef5cd4c0299edf35e4c075991a4f097deb5b267bfb3"
+ },
{
"path": "verification/extraction/lean/NonosExtraction/Irq.lean",
"sha256": "9bda548e235ff0e615d704ef376481377371b395429b846c18b8fb9ecca5e2d3"
@@ -1520,6 +1534,10 @@
"path": "verification/extraction/lean/NonosExtraction/KeyringCapsuleTypes.lean",
"sha256": "761220c82c5b047a9b214245d45531143fa85f25d5bc64d100eafead799dada9"
},
+ {
+ "path": "verification/extraction/lean/NonosExtraction/LayoutStackSlots.lean",
+ "sha256": "814b39fc0db41b43ab789a6d4002c92878b784331406cdb1cf93b4158b5b892a"
+ },
{
"path": "verification/extraction/lean/NonosExtraction/MainMode.lean",
"sha256": "b8dcb271861e4ef6cd03912a68ae72e11810ea86d635c5695486bb0f1da350de"
@@ -1558,7 +1576,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/MemoryFrameAllocTypesRange.lean",
- "sha256": "09fb2a54448b7d8f042ff8099b7ce9b5de45e2b053064a02556d43d877651ffd"
+ "sha256": "e5572454abcc09ae8ddeebb6fb540dbda0f85f414967bc50c53caf25e26fcd85"
},
{
"path": "verification/extraction/lean/NonosExtraction/MemoryHardeningStatsQuery.lean",
@@ -1682,7 +1700,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/MultibootModulesAcpi.lean",
- "sha256": "18c5b33209799e9cda2aa61d7aa16ecba1add5b9e08064d75056d2393292aab2"
+ "sha256": "a0e69803064e24a2cc7b03eb8efb633fd202cac12dab784cefe52031695dcafb"
},
{
"path": "verification/extraction/lean/NonosExtraction/MultibootPlatformTypes.lean",
@@ -1762,7 +1780,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/PortStatsSnapshot.lean",
- "sha256": "649250a12caa92d2d71b5625147c90b4047b04c2afd7a604fdc7b9fd524c0a22"
+ "sha256": "0e25c2d2140672606ada02104bed8aa7297a5bad5f3b46d52827c01923653751"
},
{
"path": "verification/extraction/lean/NonosExtraction/ProcessFdTypes.lean",
@@ -1782,7 +1800,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/ProcfsPidInode.lean",
- "sha256": "1b18d3829f705cbfa486457ec1265632285ab7f63a553f23d5f13869635417d4"
+ "sha256": "b872c0503448db4f3e44748d25714235f6b7f89d623580ba3c539a22d8f0370d"
},
{
"path": "verification/extraction/lean/NonosExtraction/ProcfsTypes.lean",
@@ -1934,7 +1952,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/TablesMemoryDesc.lean",
- "sha256": "ba52b4ded8009cdc12dc736439a9dd75c303dbc99585847b32de3073f4d2417e"
+ "sha256": "1cf7075b0e35ffe6e637e9c8a413ce11b0bc3714d1915b4d4d41fd06f8864829"
},
{
"path": "verification/extraction/lean/NonosExtraction/TablesMemoryType.lean",
@@ -1958,7 +1976,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/TablesTime.lean",
- "sha256": "0c74834df2271ea9a25fd60f1647b65a4277c22feb3aa773f2a33804889baae7"
+ "sha256": "3bed7773eaa0b16ecb336f8723f782690e797f52affcb9373d8884e17c42dd7d"
},
{
"path": "verification/extraction/lean/NonosExtraction/TimeRtcTypesAlarm.lean",
@@ -2002,7 +2020,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/TypesPercpu.lean",
- "sha256": "79f5f9ccd39b66536eedd0271048924b34ed077d60b6d045b5f38c2a7cf9e54f"
+ "sha256": "c676a1852430c05067b0bcfcc5b158e36642c394dbc27518c54ed913368dc8cc"
},
{
"path": "verification/extraction/lean/NonosExtraction/TypesPermissions.lean",
@@ -2046,7 +2064,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/TypesStatsSnapshot.lean",
- "sha256": "720c6a5db6a94fd1a9ee806d3652189b5b9ad1a9334f96999ef681438fcca1ee"
+ "sha256": "cd103ef20447825347b40fcbfe2139bbfbd399c966e5e06fbdf785b9f4624cf6"
},
{
"path": "verification/extraction/lean/NonosExtraction/TypesZoneStats.lean",
@@ -2054,7 +2072,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/Uefi.lean",
- "sha256": "3c4d4e3bd07ce97e4736a45a8345af678b95d6af894f17c194dc9631986f5976"
+ "sha256": "8ddb4d5a32899cf1611e54199040f527efda60caf94a03109587c6f8a94c361c"
},
{
"path": "verification/extraction/lean/NonosExtraction/UefiCrc.lean",
@@ -2070,7 +2088,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/UtilsTypes.lean",
- "sha256": "82b1f58650ccf6854d84b2690bc9bb1520c63ed67df90b71ffe93ce8c1549f59"
+ "sha256": "5e1dc7a46a080d7949e29b3164f40e389ac4afb4e6fb57c9b79e90bc909f7ca9"
},
{
"path": "verification/extraction/lean/NonosExtraction/ValidationSimdLevel.lean",
@@ -2078,7 +2096,7 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/VariableFirmware.lean",
- "sha256": "d2d6a16662fa8e19a6390fc4279c4271a5f43a6fc3742675b6599d1f874efe4d"
+ "sha256": "dc73f6d2b67752567b83468863e53443866041ff09f745bef59932adafc7825c"
},
{
"path": "verification/extraction/lean/NonosExtraction/Vectors.lean",
@@ -2090,11 +2108,11 @@
},
{
"path": "verification/extraction/lean/NonosExtraction/VgaColors.lean",
- "sha256": "b6caeeedba8da390db96240f80d637e1f88a9a1938410d191059b88d0b4fb3d5"
+ "sha256": "64ee712b4328f6f3366634e6827056373e6158a5e8792808edb81e6002fa76c5"
},
{
"path": "verification/extraction/lean/NonosExtraction/VgaConstants.lean",
- "sha256": "a502d776fec101fc7b5350c915f7ad9464f37c810fdf0dac174d3353ab72496a"
+ "sha256": "cc45dffa5fea2c817b952096b041f8980cd126755998232e7f9d9e9f7b2a85b2"
},
{
"path": "verification/extraction/lean/NonosExtraction/WalkerAlign.lean",
diff --git a/verification/extraction/ASSUMPTIONS.md b/verification/extraction/ASSUMPTIONS.md
index 5b856eada6..cdf2a1ce3d 100644
--- a/verification/extraction/ASSUMPTIONS.md
+++ b/verification/extraction/ASSUMPTIONS.md
@@ -77,6 +77,10 @@ cannot say that a given part sets a given bit.
opaque because the allocator is not in the extracted set.
- **`core.num.U16.wrapping_neg`**, 2 uses. Wrapping negation of a 16-bit word.
+- **`core.num.U64.wrapping_neg`**, 1 use. Wrapping negation of a 64-bit
+ word, in `ct_is_zero_u64`. The theorem that reads it,
+ `ct_is_zero_u64_is_one_exactly_at_zero`, takes the model `-x` modulo `2^64`
+ as a stated hypothesis rather than relying on the axiom.
- **`core.num.U64.count_ones`**, 1 use. Population count.
- **`core.num.Usize.div_ceil`**, 1 use. Division rounding up.
- **`core.option.Option.map`**, 1 use, **`core.result.Result.is_err`**, 1 use,
diff --git a/verification/extraction/align/Cargo.lock b/verification/extraction/align/Cargo.lock
new file mode 100644
index 0000000000..c42d2850cb
--- /dev/null
+++ b/verification/extraction/align/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_align"
+version = "0.1.0"
diff --git a/verification/extraction/crates.json b/verification/extraction/crates.json
index 041907253f..364d753ee8 100644
--- a/verification/extraction/crates.json
+++ b/verification/extraction/crates.json
@@ -293,7 +293,8 @@
"nonos_x_vga_constants::colorcode_background",
"nonos_x_vga_constants::colorcode_is_blinking",
"nonos_x_vga_constants::colorcode_value",
- "nonos_x_vga_constants::screenchar_as_u16"
+ "nonos_x_vga_constants::screenchar_as_u16",
+ "nonos_x_vga_constants::colorcode_new"
]
},
{
@@ -305,7 +306,8 @@
"starts": [
"nonos_x_utils_types::scanconfig_new",
"nonos_x_utils_types::scanconfig_with_max_depth",
- "nonos_x_utils_types::scanconfig_hidden_only"
+ "nonos_x_utils_types::scanconfig_hidden_only",
+ "nonos_x_utils_types::scanconfig_admits_hidden"
]
},
{
@@ -1081,7 +1083,8 @@
"generated": "VariableFirmware.lean",
"starts": [
"nonos_x_variable_firmware::firmwareinfo_uefi_major_version",
- "nonos_x_variable_firmware::firmwareinfo_uefi_minor_version"
+ "nonos_x_variable_firmware::firmwareinfo_uefi_minor_version",
+ "nonos_x_variable_firmware::uefi_revision"
]
},
{
@@ -1325,6 +1328,36 @@
"nonos_x_procfs_pid_inode::pid_dir_inode"
]
},
+ {
+ "name": "iommu_regs_window",
+ "crate": "nonos_x_iommu_regs_window",
+ "dir": "verification/extraction/tree/iommu_regs_window",
+ "lean": "IommuRegsWindow",
+ "generated": "IommuRegsWindow.lean",
+ "starts": [
+ "nonos_x_iommu_regs_window::registers_fit"
+ ]
+ },
+ {
+ "name": "bti_pad",
+ "crate": "nonos_x_bti_pad",
+ "dir": "verification/extraction/sweep/bti_pad",
+ "lean": "BtiPad",
+ "generated": "BtiPad.lean",
+ "starts": [
+ "nonos_x_bti_pad::is_bti_landing_pad"
+ ]
+ },
+ {
+ "name": "layout_stack_slots",
+ "crate": "nonos_x_layout_stack_slots",
+ "dir": "verification/extraction/tree/layout_stack_slots",
+ "lean": "LayoutStackSlots",
+ "generated": "LayoutStackSlots.lean",
+ "starts": [
+ "nonos_x_layout_stack_slots::stack_slot_offset"
+ ]
+ },
{
"name": "keyring_capsule_types",
"crate": "nonos_x_keyring_capsule_types",
diff --git a/verification/extraction/ct/Cargo.lock b/verification/extraction/ct/Cargo.lock
new file mode 100644
index 0000000000..b5936f3b0b
--- /dev/null
+++ b/verification/extraction/ct/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_ct"
+version = "0.2.0"
diff --git a/verification/extraction/ed_field/Cargo.lock b/verification/extraction/ed_field/Cargo.lock
new file mode 100644
index 0000000000..00e19f9389
--- /dev/null
+++ b/verification/extraction/ed_field/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_ed_field"
+version = "0.1.0"
diff --git a/verification/extraction/elf/Cargo.lock b/verification/extraction/elf/Cargo.lock
new file mode 100644
index 0000000000..852b1bbdc2
--- /dev/null
+++ b/verification/extraction/elf/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_elf"
+version = "0.2.0"
diff --git a/verification/extraction/iommu/Cargo.lock b/verification/extraction/iommu/Cargo.lock
new file mode 100644
index 0000000000..a67b8b2210
--- /dev/null
+++ b/verification/extraction/iommu/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_iommu"
+version = "0.2.0"
diff --git a/verification/extraction/lean/NonosExtraction/Aarch64ContextTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/Aarch64ContextTypesRefinement.lean
index fbe92e9b10..59f9135cdc 100644
--- a/verification/extraction/lean/NonosExtraction/Aarch64ContextTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/Aarch64ContextTypesRefinement.lean
@@ -38,9 +38,48 @@ theorem the_userentry_zeroed_wrapper_is_its_method :
theorem the_saveduser_zeroed_wrapper_is_its_method :
saveduser_zeroed = types.SavedUser.zeroed := rfl
+/-! ### A fresh context carries nothing from the kernel
+
+ `save_user_frame` starts from `SavedUser::zeroed` and then overwrites the
+ registers it knows about: `gprs::copy` fills `x0` through `x30`, and the
+ four special registers are assigned from the exception frame. The theorems
+ below say the starting record is zero in every slot, so a slot the copy
+ missed would hold zero rather than stale kernel data, and that the register
+ array has exactly the 31 slots `x0..x30` the copy writes.
+
+ For `UserEntry`, the zeroed record is the one `enter_user` must refuse: its
+ entry point, user stack and kernel stack are all zero, and each of those is
+ one of the checks in `enter_user`. Its `spsr` is zero, which is the value
+ the kernel names `SPSR_EL0T_INITIAL`. `enter_user`, the assembly it jumps
+ to and the process table are not extracted, so these theorems cannot show
+ that the refusal happens, only that the zeroed record is the input it is
+ written to refuse.
+-/
+
+/-- Every slot of a zeroed saved context is zero, and the register array is
+ exactly 31 words long, one for each of `x0` through `x30`. -/
+theorem saveduser_zeroed_holds_zero_in_every_register :
+ ∃ s, saveduser_zeroed = ok s ∧
+ s.gprs.val = List.replicate 31 0#u64 ∧
+ s.sp_el0.val = 0 ∧ s.elr_el1.val = 0 ∧
+ s.spsr_el1.val = 0 ∧ s.kernel_sp.val = 0 := by
+ exact ⟨_, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- A zeroed entry record names no entry point, no user stack and no kernel
+ stack, so it fails the first check of `enter_user`. Its `spsr` is the
+ kernel's `SPSR_EL0T_INITIAL` (zero: EL0t with no exception masked) and all
+ eight argument registers are zero. -/
+theorem userentry_zeroed_is_an_entry_enter_user_refuses :
+ ∃ e, userentry_zeroed = ok e ∧
+ e.entry.val = 0 ∧ e.user_sp.val = 0 ∧ e.kernel_sp.val = 0 ∧
+ e.spsr.val = 0 ∧ e.args.val = List.replicate 8 0#u64 := by
+ exact ⟨_, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.Aarch64ContextTypes.the_userentry_zeroed_wrapper_is_its_method
#print axioms NonosExtraction.Aarch64ContextTypes.the_saveduser_zeroed_wrapper_is_its_method
+#print axioms NonosExtraction.Aarch64ContextTypes.saveduser_zeroed_holds_zero_in_every_register
+#print axioms NonosExtraction.Aarch64ContextTypes.userentry_zeroed_is_an_entry_enter_user_refuses
end NonosExtraction.Aarch64ContextTypes
diff --git a/verification/extraction/lean/NonosExtraction/Aarch64FpuContextRefinement.lean b/verification/extraction/lean/NonosExtraction/Aarch64FpuContextRefinement.lean
index d3736d076e..afa0edef2f 100644
--- a/verification/extraction/lean/NonosExtraction/Aarch64FpuContextRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/Aarch64FpuContextRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.Aarch64FpuContext
+import Nonos.FpuState
open Aeneas Aeneas.Std Result
open nonos_x_aarch64_fpu_context
@@ -35,8 +36,104 @@ namespace NonosExtraction.Aarch64FpuContext
theorem the_fpsimdcontext_zeroed_wrapper_is_its_method :
fpsimdcontext_zeroed = context.FpSimdContext.zeroed := rfl
+/-! ### A zeroed save area is a safe first state on aarch64
+
+A task's first FP/SIMD state is whatever `aarch64_fpu_restore` loads from
+`FpSimdContext::zeroed`, so the value that function returns is the register file
+every fresh task begins with. The theorems below establish three things about
+it. No vector register and no padding word carries a value, so nothing left by
+an earlier task can reach a new one through this area. `FPSR` holds no
+cumulative exception flag and no saturation flag, so a task does not start with
+conditions it never raised. And `FPCR` enables no exception trap, rounds to
+nearest, and keeps flush-to-zero, default-NaN and alternative half precision
+off.
+
+The third point is the one that went wrong on x86_64, and the comparison is made
+against the tier-one model `Nonos.FpuState`. `MXCSR` masks exceptions with set
+bits, so a zeroed word traps on all six conditions. `FPCR` enables traps with
+set bits (`IOE`, `DZE`, `OFE`, `UFE`, `IXE` at bits 8 to 12, `IDE` at bit 15),
+so the same zero traps on none of them, and on aarch64 the zeroed area needs no
+correction.
+
+These are statements about the value the kernel builds, not about the machine.
+The restore itself is assembly outside the extraction, the byte offsets it uses
+(`0x200` for `FPSR`, `0x204` for `FPCR`) depend on the `repr(C)` layout that
+Aeneas does not model, and `FpSimdSlot::zeroed` and the lazy-enable path that
+call this function are not extracted.
+-/
+
+/-- Where `FPCR` keeps the trap enable for each condition `Nonos.FpuState`
+ names. The input-denormal enable `IDE` is bit 15, apart from the other five
+ at bits 8 to 12. -/
+def fpcrTrapEnableIndex : Nonos.FpuState.Exc → Nat
+ | .invalid => 8
+ | .divideByZero => 9
+ | .overflow => 10
+ | .underflow => 11
+ | .precision => 12
+ | .denormal => 15
+
+/-- The zeroed area leaves every vector register and both padding words at
+ zero, so a fresh task observes nothing of the task that used the FPU
+ before it. -/
+theorem fpsimdcontext_zeroed_leaves_no_register_value :
+ ∃ c, fpsimdcontext_zeroed = ok c ∧
+ (∀ x ∈ c.q.val, x = 0#u128) ∧ (∀ w ∈ c._pad.val, w = 0#u32) := by
+ refine ⟨_, rfl, ?_, ?_⟩
+ · intro x hx
+ rw [Array.repeat_val] at hx
+ exact List.eq_of_mem_replicate hx
+ · intro w hw
+ rw [Array.repeat_val] at hw
+ exact List.eq_of_mem_replicate hw
+
+/-- For every condition the x86_64 model tracks, a zeroed `MXCSR` traps and the
+ zeroed `FPCR` this function returns does not. The polarity of the two
+ control registers is opposite, so the zero that was the x86_64 defect is
+ the correct aarch64 reset. -/
+theorem fpsimdcontext_zeroed_enables_no_trap_where_a_zero_mxcsr_traps :
+ ∃ c, fpsimdcontext_zeroed = ok c ∧
+ ∀ e ∈ Nonos.FpuState.every,
+ Nonos.FpuState.Traps Nonos.FpuState.mxcsrZero e ∧
+ ¬ Nonos.FpuState.bitSet c.fpcr.val (fpcrTrapEnableIndex e) := by
+ refine ⟨_, rfl, ?_⟩
+ intro e he
+ refine ⟨?_, ?_⟩
+ · unfold Nonos.FpuState.Traps
+ exact Nonos.FpuState.zero_unmasks_everything e he
+ · cases e <;> (unfold Nonos.FpuState.bitSet fpcrTrapEnableIndex; decide)
+
+/-- The zeroed `FPCR` selects round to nearest (`RMode`, bits 22 and 23, reads
+ zero) and leaves flush-to-zero (bit 24), default NaN (bit 25), alternative
+ half precision (bit 26) and half-precision flush-to-zero (bit 19) off, so
+ arithmetic in a fresh task gives IEEE 754 default results. -/
+theorem fpsimdcontext_zeroed_rounds_to_nearest_with_ieee_defaults :
+ ∃ c, fpsimdcontext_zeroed = ok c ∧
+ c.fpcr.val / 2 ^ 22 % 4 = 0 ∧
+ ¬ Nonos.FpuState.bitSet c.fpcr.val 24 ∧
+ ¬ Nonos.FpuState.bitSet c.fpcr.val 25 ∧
+ ¬ Nonos.FpuState.bitSet c.fpcr.val 26 ∧
+ ¬ Nonos.FpuState.bitSet c.fpcr.val 19 := by
+ refine ⟨_, rfl, ?_⟩
+ unfold Nonos.FpuState.bitSet
+ decide
+
+/-- The zeroed `FPSR` has no cumulative exception flag set (`IOC`, `DZC`,
+ `OFC`, `UFC`, `IXC` at bits 0 to 4, `IDC` at bit 7) and no saturation flag
+ (`QC`, bit 27), so a fresh task does not see a condition it never raised. -/
+theorem fpsimdcontext_zeroed_starts_with_no_status_flag :
+ ∃ c, fpsimdcontext_zeroed = ok c ∧
+ ∀ i ∈ [0, 1, 2, 3, 4, 7, 27], ¬ Nonos.FpuState.bitSet c.fpsr.val i := by
+ refine ⟨_, rfl, ?_⟩
+ unfold Nonos.FpuState.bitSet
+ decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.Aarch64FpuContext.the_fpsimdcontext_zeroed_wrapper_is_its_method
+#print axioms NonosExtraction.Aarch64FpuContext.fpsimdcontext_zeroed_leaves_no_register_value
+#print axioms NonosExtraction.Aarch64FpuContext.fpsimdcontext_zeroed_enables_no_trap_where_a_zero_mxcsr_traps
+#print axioms NonosExtraction.Aarch64FpuContext.fpsimdcontext_zeroed_rounds_to_nearest_with_ieee_defaults
+#print axioms NonosExtraction.Aarch64FpuContext.fpsimdcontext_zeroed_starts_with_no_status_flag
end NonosExtraction.Aarch64FpuContext
diff --git a/verification/extraction/lean/NonosExtraction/AddrPhys.lean b/verification/extraction/lean/NonosExtraction/AddrPhys.lean
index b1f9130d8e..679f903a7a 100644
--- a/verification/extraction/lean/NonosExtraction/AddrPhys.lean
+++ b/verification/extraction/lean/NonosExtraction/AddrPhys.lean
@@ -56,31 +56,34 @@ def phys.PhysAddr.is_null (self : phys.PhysAddr) : Result Bool := do
def phys.PhysAddr.is_aligned
(self : phys.PhysAddr) (align : Std.U64) : Result Bool := do
if align != 0#u64
- then let i ← self % align
- ok (i = 0#u64)
+ then core.num.U64.is_multiple_of self align
else ok false
/-- [nonos_x_addr_phys::phys::{nonos_x_addr_phys::phys::PhysAddr}::align_down]:
- Source: 'src/../../../../../src/memory/addr/phys.rs', lines 46:4-48:5
+ Source: 'src/../../../../../src/memory/addr/phys.rs', lines 49:4-54:5
Visibility: public -/
def phys.PhysAddr.align_down
(self : phys.PhysAddr) (align : Std.U64) : Result phys.PhysAddr := do
- let i ← align - 1#u64
- let i1 ← lift (~~~ i)
- let i2 ← lift (self &&& i1)
- ok i2
+ if align = 0#u64
+ then ok self
+ else let i ← self % align
+ let i1 ← self - i
+ ok i1
/-- [nonos_x_addr_phys::phys::{nonos_x_addr_phys::phys::PhysAddr}::align_up]:
- Source: 'src/../../../../../src/memory/addr/phys.rs', lines 50:4-52:5
+ Source: 'src/../../../../../src/memory/addr/phys.rs', lines 58:4-68:5
Visibility: public -/
def phys.PhysAddr.align_up
(self : phys.PhysAddr) (align : Std.U64) : Result phys.PhysAddr := do
- let i ← self + align
- let i1 ← i - 1#u64
- let i2 ← align - 1#u64
- let i3 ← lift (~~~ i2)
- let i4 ← lift (i1 &&& i3)
- ok i4
+ if align = 0#u64
+ then ok self
+ else
+ let rem ← self % align
+ if rem = 0#u64
+ then ok self
+ else let i ← align - rem
+ let i1 ← self + i
+ ok i1
/-- [nonos_x_addr_phys::physaddr_new]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/AddrPhysRefinement.lean b/verification/extraction/lean/NonosExtraction/AddrPhysRefinement.lean
index 6680575fb3..279591b4b4 100644
--- a/verification/extraction/lean/NonosExtraction/AddrPhysRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/AddrPhysRefinement.lean
@@ -59,74 +59,119 @@ theorem the_physaddr_align_up_wrapper_is_its_method (a : phys.PhysAddr) (b : Std
/-! ### Alignment
- Three functions decide alignment for every physical address in the kernel,
- and they do not agree with each other.
-
- `is_aligned` asks `addr % align == 0`, which is the right question for any
- alignment. `align_down` and `align_up` clear low bits with `!(align - 1)`,
- which answers a different question unless `align` is a power of two. Nothing
- in the type says it has to be.
-
- The kernel builds with `overflow-checks = true` and `panic = "abort"`, so
- the checked arithmetic below is faithful: where the extracted function fails,
- the shipping kernel halts. That is a controlled halt rather than silent
- corruption, which is the trade the profile comment argues for, but it is
- still a halt reachable from an address and an alignment.
+ Three functions decide alignment for every physical address in the kernel.
+ `is_aligned` asks `addr % align == 0`. `align_down` and `align_up` round to
+ the nearest multiple of `align` below and above, computed with `%` so that
+ they are right for every non-zero alignment, and the theorems below show the
+ three agree: whatever the two rounders return passes `is_aligned`, lies on
+ the right side of the address, and is less than one alignment away from it.
+ An alignment of zero leaves the address unchanged. `align_up` fails exactly
+ when the rounded address is past `u64::MAX`; the kernel builds with
+ `overflow-checks = true` and `panic = "abort"`, so that failure is a halt.
+
+ The rounders used to clear low bits with `!(align - 1)`, which is only a
+ rounding when `align` is a power of two: aligning ten down to three gave
+ eight, which `is_aligned` refused, and an alignment of zero underflowed and
+ halted. Every caller in the kernel passes a page size, where the two
+ methods agree.
-/
-/-- The disagreement, at the smallest witness. Aligning ten down to a multiple
- of three gives eight, and `is_aligned` then says eight is not aligned to
- three. Both functions are reachable from the same call site with the same
- argument, and a caller that aligns and then checks gets told no. -/
-theorem aligning_down_can_produce_an_unaligned_address :
- physaddr_align_down 10#u64 3#u64 = ok 8#u64 ∧
- physaddr_is_aligned 8#u64 3#u64 = ok false := by
- refine ⟨?_, ?_⟩ <;> rfl
-
-/-- On a power of two the two do agree, which is the case every caller in the
- kernel actually passes and the reason this has never been noticed. -/
-theorem on_a_power_of_two_they_agree :
- physaddr_align_down 10#u64 4#u64 = ok 8#u64 ∧
- physaddr_is_aligned 8#u64 4#u64 = ok true := by
- refine ⟨?_, ?_⟩ <;> rfl
-
-/-- An alignment of zero is not refused, it halts the machine. `align - 1`
- underflows, and under this profile an underflow aborts. `is_aligned` handles
- the same argument by answering false, so the three functions disagree about
- zero as well. -/
-theorem an_alignment_of_zero_halts_rather_than_refusing :
- physaddr_is_aligned 4096#u64 0#u64 = ok false ∧
- physaddr_align_down 4096#u64 0#u64 = fail Error.integerOverflow := by
- refine ⟨rfl, rfl⟩
-
-/-- Aligning up near the top of the address space halts too, because the sum
- overflows before the mask is applied. The witness is an address one page
- below the top, aligned to a page, which is an ordinary thing to ask for at
- the end of a memory map. -/
-theorem aligning_up_near_the_top_halts :
+/-- For a non-zero alignment, `is_aligned` is the remainder test. -/
+theorem physaddr_is_aligned_is_the_remainder_test (a al : Std.U64) (h : al.val ≠ 0) :
+ physaddr_is_aligned a al = ok (decide (a.val % al.val = 0)) := by
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ have hb : (al != 0#u64) = true := by simp [hne]
+ unfold physaddr_is_aligned phys.PhysAddr.is_aligned
+ simp only [hb, if_true, core.num.U64.is_multiple_of, UScalar.is_multiple_of, ok.injEq]
+ cases a.val % al.val <;> rfl
+
+/-- Rounding down gives the largest multiple of a non-zero alignment at or below
+ the address, never fails, and the result passes `is_aligned`. -/
+theorem physaddr_align_down_is_the_largest_multiple_below (a al : Std.U64) (h : al.val ≠ 0) :
+ ∃ r : Std.U64, physaddr_align_down a al = ok r ∧ r.val = a.val - a.val % al.val ∧
+ r.val ≤ a.val ∧ a.val - r.val < al.val ∧ physaddr_is_aligned r al = ok true := by
+ unfold physaddr_align_down phys.PhysAddr.align_down
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists (UScalar.rem_spec a (y := al) h)
+ have hlt : a.val % al.val < al.val := Nat.mod_lt _ (Nat.pos_of_ne_zero h)
+ have hle : a.val % al.val ≤ a.val := Nat.mod_le _ _
+ obtain ⟨r, hr, hrv, -⟩ := WP.spec_imp_exists (UScalar.sub_spec (x := a) (y := m) (by omega))
+ simp only [hne, if_false, hm, hr, bind_tc_ok]
+ refine ⟨r, rfl, by omega, by omega, by omega, ?_⟩
+ have : r.val % al.val = 0 := by
+ rw [hrv, hmv]; exact Nat.sub_mod_eq_zero_of_mod_eq (by simp)
+ rw [physaddr_is_aligned_is_the_remainder_test r al h]
+ simp [this]
+
+/-- Rounding up gives the smallest multiple of a non-zero alignment at or above
+ the address whenever that multiple fits in 64 bits, and the result passes
+ `is_aligned`. -/
+theorem physaddr_align_up_is_the_smallest_multiple_above (a al : Std.U64) (h : al.val ≠ 0)
+ (hfit : a.val + (al.val - a.val % al.val) % al.val ≤ U64.max) :
+ ∃ r : Std.U64, physaddr_align_up a al = ok r ∧
+ r.val = a.val + (al.val - a.val % al.val) % al.val ∧
+ a.val ≤ r.val ∧ r.val - a.val < al.val ∧ physaddr_is_aligned r al = ok true := by
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ have hpos : 0 < al.val := Nat.pos_of_ne_zero h
+ have hlt : a.val % al.val < al.val := Nat.mod_lt _ hpos
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists (UScalar.rem_spec a (y := al) h)
+ have aligned : ∀ r : Std.U64, r.val % al.val = 0 → physaddr_is_aligned r al = ok true := by
+ intro r hr
+ rw [physaddr_is_aligned_is_the_remainder_test r al h]
+ simp [hr]
+ unfold physaddr_align_up phys.PhysAddr.align_up
+ simp only [hne, if_false, hm, bind_tc_ok]
+ by_cases h0 : a.val % al.val = 0
+ · have hm0 : m = 0#u64 := UScalar.eq_of_val_eq (by rw [hmv, h0]; rfl)
+ simp only [hm0, if_true]
+ refine ⟨a, rfl, by rw [h0, Nat.sub_zero, Nat.mod_self]; rfl, le_rfl, by omega, aligned a h0⟩
+ · have hm0 : m ≠ 0#u64 := fun e => h0 (by rw [← hmv, e]; rfl)
+ have hmod : (al.val - a.val % al.val) % al.val = al.val - a.val % al.val :=
+ Nat.mod_eq_of_lt (by omega)
+ rw [hmod] at hfit
+ obtain ⟨d, hd, hdv, -⟩ := WP.spec_imp_exists (UScalar.sub_spec (x := al) (y := m) (by omega))
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (UScalar.add_spec (x := a) (y := d)
+ (by rw [hdv, hmv]; scalar_tac))
+ simp only [hm0, if_false, hd, hr, bind_tc_ok]
+ refine ⟨r, rfl, by rw [hmod, hrv, hdv, hmv], by omega, by omega, aligned r ?_⟩
+ rw [hrv, hdv, hmv]
+ have e : a.val + (al.val - a.val % al.val) = (a.val / al.val + 1) * al.val := by
+ have := Nat.div_add_mod a.val al.val
+ rw [Nat.add_mul, Nat.one_mul]
+ rw [Nat.mul_comm] at this
+ omega
+ rw [e, Nat.mul_mod_left]
+
+/-- Rounding up fails exactly when the smallest multiple at or above the
+ address is past `u64::MAX`: one page below the top, rounded to a page, is
+ `2^64`. -/
+theorem physaddr_align_up_fails_past_the_top :
physaddr_align_up 0xFFFFFFFFFFFFF001#u64 4096#u64 = fail Error.integerOverflow := by
rfl
-/-- Away from the top it behaves, so the halt is a boundary case and not a
- broken function. -/
-theorem aligning_up_in_the_ordinary_range :
- physaddr_align_up 4097#u64 4096#u64 = ok 8192#u64 ∧
- physaddr_align_up 4096#u64 4096#u64 = ok 4096#u64 := by
- refine ⟨rfl, rfl⟩
-
-/-- Aligning down never moves an address upward, for every address, on a page
- alignment. This is the property callers rely on when they align a base down
- to find the page containing it. -/
-theorem aligning_down_is_the_mask (a : Std.U64) :
- physaddr_align_down a 4096#u64 = ok (a &&& 0xFFFFFFFFFFFFF000#u64) := by
- rfl
-
-/-- And clearing those bits never moves an address upward, for every word. This
- is the property a caller relies on when it aligns a base down to find the
- page containing it. -/
-theorem masking_down_never_moves_up (a : BitVec 64) :
- (a &&& 0xFFFFFFFFFFFFF000#64) ≤ a := by
- bv_decide
+/-- An alignment of zero leaves the address unchanged in both directions, where
+ the old mask underflowed and halted. `is_aligned` still answers false. -/
+theorem physaddr_an_alignment_of_zero_leaves_the_address :
+ physaddr_align_down 4096#u64 0#u64 = ok 4096#u64 ∧
+ physaddr_align_up 4097#u64 0#u64 = ok 4097#u64 ∧
+ physaddr_is_aligned 4096#u64 0#u64 = ok false := ⟨rfl, rfl, rfl⟩
+
+/-- The witness the old mask got wrong: ten rounds down to nine and up to twelve
+ on an alignment of three, both multiples of three. -/
+theorem physaddr_rounds_to_multiples_of_three :
+ physaddr_align_down 10#u64 3#u64 = ok 9#u64 ∧ physaddr_align_up 10#u64 3#u64 = ok 12#u64 ∧
+ physaddr_is_aligned 9#u64 3#u64 = ok true ∧ physaddr_is_aligned 12#u64 3#u64 = ok true :=
+ ⟨rfl, rfl, rfl, rfl⟩
+
+/-- On a page alignment rounding down clears the twelve low bits, which is what
+ the frame allocator relies on when it takes the frame holding an address. -/
+theorem physaddr_align_down_to_a_page_clears_the_low_twelve_bits (a : Std.U64) :
+ ∃ r : Std.U64, physaddr_align_down a 4096#u64 = ok r ∧ r.val = a.val / 4096 * 4096 := by
+ obtain ⟨r, hr, hv, -⟩ := physaddr_align_down_is_the_largest_multiple_below a 4096#u64 (by decide)
+ refine ⟨r, hr, ?_⟩
+ rw [hv, show (4096#u64 : Std.U64).val = 4096 from rfl]
+ have := Nat.div_add_mod a.val 4096
+ omega
/-! ### The trivial readers
@@ -142,15 +187,51 @@ theorem zero_is_null_and_nothing_else :
physaddr_is_null 1#u64 = ok false := by
refine ⟨rfl, rfl⟩
+/-! ### Converting to a machine word
+
+ `as_usize` is `self.0 as usize`. The theorems below say it never fails,
+ that it keeps exactly the low `System.Platform.numBits` bits of the
+ address, and that on a 64-bit target it loses nothing, so it agrees with
+ `as_u64`. An implementation that masked to the 48-bit canonical range, or
+ cleared the page offset, would disagree on a higher-half address.
+
+ The theorems cannot say that a 32-bit build never passes an address above
+ four gigabytes: on such a target the cast silently truncates, and that is
+ exactly what the first theorem records.
+-/
+
+/-- The conversion never fails and keeps exactly the low platform-width bits. -/
+theorem physaddr_as_usize_keeps_the_low_platform_bits (a : Std.U64) :
+ ∃ r, physaddr_as_usize a = ok r ∧ r.val = a.val % 2 ^ System.Platform.numBits := by
+ refine ⟨_, rfl, ?_⟩
+ simp [UScalar.cast_val_eq]
+
+/-- On a 64-bit target the conversion is lossless, so it reads the same number
+ as `as_u64` for every address, including the higher half. -/
+theorem on_a_64_bit_target_physaddr_as_usize_agrees_with_as_u64
+ (h : System.Platform.numBits = 64) (a : Std.U64) :
+ ∃ r w, physaddr_as_usize a = ok r ∧ physaddr_as_u64 a = ok w ∧ r.val = w.val := by
+ refine ⟨_, _, rfl, rfl, ?_⟩
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, h]
+ exact Nat.mod_eq_of_lt (by scalar_tac)
+
+/-- The higher-half kernel base survives the conversion unchanged on a 64-bit
+ target, the witness a canonical-range mask would get wrong. -/
+theorem physaddr_as_usize_keeps_the_higher_half (h : System.Platform.numBits = 64) :
+ ∃ r, physaddr_as_usize 0xFFFF800000000000#u64 = ok r ∧ r.val = 0xFFFF800000000000 := by
+ refine ⟨_, rfl, ?_⟩
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, h]
+ decide
+
/-! ### Axiom profile -/
-#print axioms NonosExtraction.AddrPhys.aligning_down_can_produce_an_unaligned_address
-#print axioms NonosExtraction.AddrPhys.on_a_power_of_two_they_agree
-#print axioms NonosExtraction.AddrPhys.an_alignment_of_zero_halts_rather_than_refusing
-#print axioms NonosExtraction.AddrPhys.aligning_up_near_the_top_halts
-#print axioms NonosExtraction.AddrPhys.aligning_up_in_the_ordinary_range
-#print axioms NonosExtraction.AddrPhys.aligning_down_is_the_mask
-#print axioms NonosExtraction.AddrPhys.masking_down_never_moves_up
+#print axioms NonosExtraction.AddrPhys.physaddr_is_aligned_is_the_remainder_test
+#print axioms NonosExtraction.AddrPhys.physaddr_align_down_is_the_largest_multiple_below
+#print axioms NonosExtraction.AddrPhys.physaddr_align_up_is_the_smallest_multiple_above
+#print axioms NonosExtraction.AddrPhys.physaddr_align_up_fails_past_the_top
+#print axioms NonosExtraction.AddrPhys.physaddr_an_alignment_of_zero_leaves_the_address
+#print axioms NonosExtraction.AddrPhys.physaddr_rounds_to_multiples_of_three
+#print axioms NonosExtraction.AddrPhys.physaddr_align_down_to_a_page_clears_the_low_twelve_bits
#print axioms NonosExtraction.AddrPhys.the_newtype_round_trips
#print axioms NonosExtraction.AddrPhys.zero_is_null_and_nothing_else
#print axioms NonosExtraction.AddrPhys.the_physaddr_new_wrapper_is_its_method
@@ -161,5 +242,8 @@ theorem zero_is_null_and_nothing_else :
#print axioms NonosExtraction.AddrPhys.the_physaddr_is_aligned_wrapper_is_its_method
#print axioms NonosExtraction.AddrPhys.the_physaddr_align_down_wrapper_is_its_method
#print axioms NonosExtraction.AddrPhys.the_physaddr_align_up_wrapper_is_its_method
+#print axioms NonosExtraction.AddrPhys.physaddr_as_usize_keeps_the_low_platform_bits
+#print axioms NonosExtraction.AddrPhys.on_a_64_bit_target_physaddr_as_usize_agrees_with_as_u64
+#print axioms NonosExtraction.AddrPhys.physaddr_as_usize_keeps_the_higher_half
end NonosExtraction.AddrPhys
diff --git a/verification/extraction/lean/NonosExtraction/AddrVirt.lean b/verification/extraction/lean/NonosExtraction/AddrVirt.lean
index 84e2c1af5e..5712ee363b 100644
--- a/verification/extraction/lean/NonosExtraction/AddrVirt.lean
+++ b/verification/extraction/lean/NonosExtraction/AddrVirt.lean
@@ -56,31 +56,34 @@ def virt.VirtAddr.is_null (self : virt.VirtAddr) : Result Bool := do
def virt.VirtAddr.is_aligned
(self : virt.VirtAddr) (align : Std.U64) : Result Bool := do
if align != 0#u64
- then let i ← self % align
- ok (i = 0#u64)
+ then core.num.U64.is_multiple_of self align
else ok false
/-- [nonos_x_addr_virt::virt::{nonos_x_addr_virt::virt::VirtAddr}::align_down]:
- Source: 'src/../../../../../src/memory/addr/virt.rs', lines 54:4-56:5
+ Source: 'src/../../../../../src/memory/addr/virt.rs', lines 57:4-62:5
Visibility: public -/
def virt.VirtAddr.align_down
(self : virt.VirtAddr) (align : Std.U64) : Result virt.VirtAddr := do
- let i ← align - 1#u64
- let i1 ← lift (~~~ i)
- let i2 ← lift (self &&& i1)
- ok i2
+ if align = 0#u64
+ then ok self
+ else let i ← self % align
+ let i1 ← self - i
+ ok i1
/-- [nonos_x_addr_virt::virt::{nonos_x_addr_virt::virt::VirtAddr}::align_up]:
- Source: 'src/../../../../../src/memory/addr/virt.rs', lines 58:4-60:5
+ Source: 'src/../../../../../src/memory/addr/virt.rs', lines 66:4-76:5
Visibility: public -/
def virt.VirtAddr.align_up
(self : virt.VirtAddr) (align : Std.U64) : Result virt.VirtAddr := do
- let i ← self + align
- let i1 ← i - 1#u64
- let i2 ← align - 1#u64
- let i3 ← lift (~~~ i2)
- let i4 ← lift (i1 &&& i3)
- ok i4
+ if align = 0#u64
+ then ok self
+ else
+ let rem ← self % align
+ if rem = 0#u64
+ then ok self
+ else let i ← align - rem
+ let i1 ← self + i
+ ok i1
/-- [nonos_x_addr_virt::virtaddr_new]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/AddrVirtRefinement.lean b/verification/extraction/lean/NonosExtraction/AddrVirtRefinement.lean
index c4cdc914f2..ee968f4398 100644
--- a/verification/extraction/lean/NonosExtraction/AddrVirtRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/AddrVirtRefinement.lean
@@ -58,79 +58,119 @@ theorem the_virtaddr_align_up_wrapper_is_its_method (a : virt.VirtAddr) (b : Std
/-! ### Alignment
- Three functions decide alignment for every virtual address in the kernel,
- and they do not agree with each other.
-
- This is the same code as `PhysAddr` carries, written out a second time
- rather than shared, so the same three findings hold here. That duplication
- is itself worth noticing: two copies of an alignment rule drift, and a fix
- applied to one of them leaves the other wrong.
-
- `is_aligned` asks `addr % align == 0`, which is the right question for any
- alignment. `align_down` and `align_up` clear low bits with `!(align - 1)`,
- which answers a different question unless `align` is a power of two. Nothing
- in the type says it has to be.
-
- The kernel builds with `overflow-checks = true` and `panic = "abort"`, so
- the checked arithmetic below is faithful: where the extracted function fails,
- the shipping kernel halts. That is a controlled halt rather than silent
- corruption, which is the trade the profile comment argues for, but it is
- still a halt reachable from an address and an alignment.
+ Three functions decide alignment for every virtual address in the kernel.
+ `is_aligned` asks `addr % align == 0`. `align_down` and `align_up` round to
+ the nearest multiple of `align` below and above, computed with `%` so that
+ they are right for every non-zero alignment, and the theorems below show the
+ three agree: whatever the two rounders return passes `is_aligned`, lies on
+ the right side of the address, and is less than one alignment away from it.
+ An alignment of zero leaves the address unchanged. `align_up` fails exactly
+ when the rounded address is past `u64::MAX`; the kernel builds with
+ `overflow-checks = true` and `panic = "abort"`, so that failure is a halt.
+
+ The rounders used to clear low bits with `!(align - 1)`, which is only a
+ rounding when `align` is a power of two: aligning ten down to three gave
+ eight, which `is_aligned` refused, and an alignment of zero underflowed and
+ halted. Every caller in the kernel passes a page size, where the two
+ methods agree.
-/
-/-- The disagreement, at the smallest witness. Aligning ten down to a multiple
- of three gives eight, and `is_aligned` then says eight is not aligned to
- three. Both functions are reachable from the same call site with the same
- argument, and a caller that aligns and then checks gets told no. -/
-theorem aligning_down_can_produce_an_unaligned_address :
- virtaddr_align_down 10#u64 3#u64 = ok 8#u64 ∧
- virtaddr_is_aligned 8#u64 3#u64 = ok false := by
- refine ⟨?_, ?_⟩ <;> rfl
-
-/-- On a power of two the two do agree, which is the case every caller in the
- kernel actually passes and the reason this has never been noticed. -/
-theorem on_a_power_of_two_they_agree :
- virtaddr_align_down 10#u64 4#u64 = ok 8#u64 ∧
- virtaddr_is_aligned 8#u64 4#u64 = ok true := by
- refine ⟨?_, ?_⟩ <;> rfl
-
-/-- An alignment of zero is not refused, it halts the machine. `align - 1`
- underflows, and under this profile an underflow aborts. `is_aligned` handles
- the same argument by answering false, so the three functions disagree about
- zero as well. -/
-theorem an_alignment_of_zero_halts_rather_than_refusing :
- virtaddr_is_aligned 4096#u64 0#u64 = ok false ∧
- virtaddr_align_down 4096#u64 0#u64 = fail Error.integerOverflow := by
- refine ⟨rfl, rfl⟩
-
-/-- Aligning up near the top of the address space halts too, because the sum
- overflows before the mask is applied. The witness is an address one page
- below the top, aligned to a page, which is an ordinary thing to ask for at
- the end of a memory map. -/
-theorem aligning_up_near_the_top_halts :
+/-- For a non-zero alignment, `is_aligned` is the remainder test. -/
+theorem virtaddr_is_aligned_is_the_remainder_test (a al : Std.U64) (h : al.val ≠ 0) :
+ virtaddr_is_aligned a al = ok (decide (a.val % al.val = 0)) := by
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ have hb : (al != 0#u64) = true := by simp [hne]
+ unfold virtaddr_is_aligned virt.VirtAddr.is_aligned
+ simp only [hb, if_true, core.num.U64.is_multiple_of, UScalar.is_multiple_of, ok.injEq]
+ cases a.val % al.val <;> rfl
+
+/-- Rounding down gives the largest multiple of a non-zero alignment at or below
+ the address, never fails, and the result passes `is_aligned`. -/
+theorem virtaddr_align_down_is_the_largest_multiple_below (a al : Std.U64) (h : al.val ≠ 0) :
+ ∃ r : Std.U64, virtaddr_align_down a al = ok r ∧ r.val = a.val - a.val % al.val ∧
+ r.val ≤ a.val ∧ a.val - r.val < al.val ∧ virtaddr_is_aligned r al = ok true := by
+ unfold virtaddr_align_down virt.VirtAddr.align_down
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists (UScalar.rem_spec a (y := al) h)
+ have hlt : a.val % al.val < al.val := Nat.mod_lt _ (Nat.pos_of_ne_zero h)
+ have hle : a.val % al.val ≤ a.val := Nat.mod_le _ _
+ obtain ⟨r, hr, hrv, -⟩ := WP.spec_imp_exists (UScalar.sub_spec (x := a) (y := m) (by omega))
+ simp only [hne, if_false, hm, hr, bind_tc_ok]
+ refine ⟨r, rfl, by omega, by omega, by omega, ?_⟩
+ have : r.val % al.val = 0 := by
+ rw [hrv, hmv]; exact Nat.sub_mod_eq_zero_of_mod_eq (by simp)
+ rw [virtaddr_is_aligned_is_the_remainder_test r al h]
+ simp [this]
+
+/-- Rounding up gives the smallest multiple of a non-zero alignment at or above
+ the address whenever that multiple fits in 64 bits, and the result passes
+ `is_aligned`. -/
+theorem virtaddr_align_up_is_the_smallest_multiple_above (a al : Std.U64) (h : al.val ≠ 0)
+ (hfit : a.val + (al.val - a.val % al.val) % al.val ≤ U64.max) :
+ ∃ r : Std.U64, virtaddr_align_up a al = ok r ∧
+ r.val = a.val + (al.val - a.val % al.val) % al.val ∧
+ a.val ≤ r.val ∧ r.val - a.val < al.val ∧ virtaddr_is_aligned r al = ok true := by
+ have hne : al ≠ 0#u64 := fun e => h (by rw [e]; rfl)
+ have hpos : 0 < al.val := Nat.pos_of_ne_zero h
+ have hlt : a.val % al.val < al.val := Nat.mod_lt _ hpos
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists (UScalar.rem_spec a (y := al) h)
+ have aligned : ∀ r : Std.U64, r.val % al.val = 0 → virtaddr_is_aligned r al = ok true := by
+ intro r hr
+ rw [virtaddr_is_aligned_is_the_remainder_test r al h]
+ simp [hr]
+ unfold virtaddr_align_up virt.VirtAddr.align_up
+ simp only [hne, if_false, hm, bind_tc_ok]
+ by_cases h0 : a.val % al.val = 0
+ · have hm0 : m = 0#u64 := UScalar.eq_of_val_eq (by rw [hmv, h0]; rfl)
+ simp only [hm0, if_true]
+ refine ⟨a, rfl, by rw [h0, Nat.sub_zero, Nat.mod_self]; rfl, le_rfl, by omega, aligned a h0⟩
+ · have hm0 : m ≠ 0#u64 := fun e => h0 (by rw [← hmv, e]; rfl)
+ have hmod : (al.val - a.val % al.val) % al.val = al.val - a.val % al.val :=
+ Nat.mod_eq_of_lt (by omega)
+ rw [hmod] at hfit
+ obtain ⟨d, hd, hdv, -⟩ := WP.spec_imp_exists (UScalar.sub_spec (x := al) (y := m) (by omega))
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (UScalar.add_spec (x := a) (y := d)
+ (by rw [hdv, hmv]; scalar_tac))
+ simp only [hm0, if_false, hd, hr, bind_tc_ok]
+ refine ⟨r, rfl, by rw [hmod, hrv, hdv, hmv], by omega, by omega, aligned r ?_⟩
+ rw [hrv, hdv, hmv]
+ have e : a.val + (al.val - a.val % al.val) = (a.val / al.val + 1) * al.val := by
+ have := Nat.div_add_mod a.val al.val
+ rw [Nat.add_mul, Nat.one_mul]
+ rw [Nat.mul_comm] at this
+ omega
+ rw [e, Nat.mul_mod_left]
+
+/-- Rounding up fails exactly when the smallest multiple at or above the
+ address is past `u64::MAX`: one page below the top, rounded to a page, is
+ `2^64`. -/
+theorem virtaddr_align_up_fails_past_the_top :
virtaddr_align_up 0xFFFFFFFFFFFFF001#u64 4096#u64 = fail Error.integerOverflow := by
rfl
-/-- Away from the top it behaves, so the halt is a boundary case and not a
- broken function. -/
-theorem aligning_up_in_the_ordinary_range :
- virtaddr_align_up 4097#u64 4096#u64 = ok 8192#u64 ∧
- virtaddr_align_up 4096#u64 4096#u64 = ok 4096#u64 := by
- refine ⟨rfl, rfl⟩
-
-/-- Aligning down never moves an address upward, for every address, on a page
- alignment. This is the property callers rely on when they align a base down
- to find the page containing it. -/
-theorem aligning_down_is_the_mask (a : Std.U64) :
- virtaddr_align_down a 4096#u64 = ok (a &&& 0xFFFFFFFFFFFFF000#u64) := by
- rfl
-
-/-- And clearing those bits never moves an address upward, for every word. This
- is the property a caller relies on when it aligns a base down to find the
- page containing it. -/
-theorem masking_down_never_moves_up (a : BitVec 64) :
- (a &&& 0xFFFFFFFFFFFFF000#64) ≤ a := by
- bv_decide
+/-- An alignment of zero leaves the address unchanged in both directions, where
+ the old mask underflowed and halted. `is_aligned` still answers false. -/
+theorem virtaddr_an_alignment_of_zero_leaves_the_address :
+ virtaddr_align_down 4096#u64 0#u64 = ok 4096#u64 ∧
+ virtaddr_align_up 4097#u64 0#u64 = ok 4097#u64 ∧
+ virtaddr_is_aligned 4096#u64 0#u64 = ok false := ⟨rfl, rfl, rfl⟩
+
+/-- The witness the old mask got wrong: ten rounds down to nine and up to twelve
+ on an alignment of three, both multiples of three. -/
+theorem virtaddr_rounds_to_multiples_of_three :
+ virtaddr_align_down 10#u64 3#u64 = ok 9#u64 ∧ virtaddr_align_up 10#u64 3#u64 = ok 12#u64 ∧
+ virtaddr_is_aligned 9#u64 3#u64 = ok true ∧ virtaddr_is_aligned 12#u64 3#u64 = ok true :=
+ ⟨rfl, rfl, rfl, rfl⟩
+
+/-- On a page alignment rounding down clears the twelve low bits, which is what
+ a caller relies on when it takes the page holding an address. -/
+theorem virtaddr_align_down_to_a_page_clears_the_low_twelve_bits (a : Std.U64) :
+ ∃ r : Std.U64, virtaddr_align_down a 4096#u64 = ok r ∧ r.val = a.val / 4096 * 4096 := by
+ obtain ⟨r, hr, hv, -⟩ := virtaddr_align_down_is_the_largest_multiple_below a 4096#u64 (by decide)
+ refine ⟨r, hr, ?_⟩
+ rw [hv, show (4096#u64 : Std.U64).val = 4096 from rfl]
+ have := Nat.div_add_mod a.val 4096
+ omega
/-! ### The trivial readers
@@ -146,15 +186,51 @@ theorem zero_is_null_and_nothing_else :
virtaddr_is_null 1#u64 = ok false := by
refine ⟨rfl, rfl⟩
+/-! ### Converting to a machine word
+
+ `as_usize` is `self.0 as usize`. The theorems below say it never fails,
+ that it keeps exactly the low `System.Platform.numBits` bits of the
+ address, and that on a 64-bit target it loses nothing, so it agrees with
+ `as_u64`. An implementation that masked to the 48-bit canonical range, or
+ cleared the page offset, would disagree on a higher-half address.
+
+ The theorems cannot say that a 32-bit build never passes an address above
+ four gigabytes: on such a target the cast silently truncates, and that is
+ exactly what the first theorem records.
+-/
+
+/-- The conversion never fails and keeps exactly the low platform-width bits. -/
+theorem virtaddr_as_usize_keeps_the_low_platform_bits (a : Std.U64) :
+ ∃ r, virtaddr_as_usize a = ok r ∧ r.val = a.val % 2 ^ System.Platform.numBits := by
+ refine ⟨_, rfl, ?_⟩
+ simp [UScalar.cast_val_eq]
+
+/-- On a 64-bit target the conversion is lossless, so it reads the same number
+ as `as_u64` for every address, including the higher half. -/
+theorem on_a_64_bit_target_virtaddr_as_usize_agrees_with_as_u64
+ (h : System.Platform.numBits = 64) (a : Std.U64) :
+ ∃ r w, virtaddr_as_usize a = ok r ∧ virtaddr_as_u64 a = ok w ∧ r.val = w.val := by
+ refine ⟨_, _, rfl, rfl, ?_⟩
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, h]
+ exact Nat.mod_eq_of_lt (by scalar_tac)
+
+/-- The higher-half kernel base survives the conversion unchanged on a 64-bit
+ target, the witness a canonical-range mask would get wrong. -/
+theorem virtaddr_as_usize_keeps_the_higher_half (h : System.Platform.numBits = 64) :
+ ∃ r, virtaddr_as_usize 0xFFFF800000000000#u64 = ok r ∧ r.val = 0xFFFF800000000000 := by
+ refine ⟨_, rfl, ?_⟩
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, h]
+ decide
+
/-! ### Axiom profile -/
-#print axioms NonosExtraction.AddrVirt.aligning_down_can_produce_an_unaligned_address
-#print axioms NonosExtraction.AddrVirt.on_a_power_of_two_they_agree
-#print axioms NonosExtraction.AddrVirt.an_alignment_of_zero_halts_rather_than_refusing
-#print axioms NonosExtraction.AddrVirt.aligning_up_near_the_top_halts
-#print axioms NonosExtraction.AddrVirt.aligning_up_in_the_ordinary_range
-#print axioms NonosExtraction.AddrVirt.aligning_down_is_the_mask
-#print axioms NonosExtraction.AddrVirt.masking_down_never_moves_up
+#print axioms NonosExtraction.AddrVirt.virtaddr_is_aligned_is_the_remainder_test
+#print axioms NonosExtraction.AddrVirt.virtaddr_align_down_is_the_largest_multiple_below
+#print axioms NonosExtraction.AddrVirt.virtaddr_align_up_is_the_smallest_multiple_above
+#print axioms NonosExtraction.AddrVirt.virtaddr_align_up_fails_past_the_top
+#print axioms NonosExtraction.AddrVirt.virtaddr_an_alignment_of_zero_leaves_the_address
+#print axioms NonosExtraction.AddrVirt.virtaddr_rounds_to_multiples_of_three
+#print axioms NonosExtraction.AddrVirt.virtaddr_align_down_to_a_page_clears_the_low_twelve_bits
#print axioms NonosExtraction.AddrVirt.the_newtype_round_trips
#print axioms NonosExtraction.AddrVirt.zero_is_null_and_nothing_else
#print axioms NonosExtraction.AddrVirt.the_virtaddr_new_wrapper_is_its_method
@@ -165,5 +241,8 @@ theorem zero_is_null_and_nothing_else :
#print axioms NonosExtraction.AddrVirt.the_virtaddr_is_aligned_wrapper_is_its_method
#print axioms NonosExtraction.AddrVirt.the_virtaddr_align_down_wrapper_is_its_method
#print axioms NonosExtraction.AddrVirt.the_virtaddr_align_up_wrapper_is_its_method
+#print axioms NonosExtraction.AddrVirt.virtaddr_as_usize_keeps_the_low_platform_bits
+#print axioms NonosExtraction.AddrVirt.on_a_64_bit_target_virtaddr_as_usize_agrees_with_as_u64
+#print axioms NonosExtraction.AddrVirt.virtaddr_as_usize_keeps_the_higher_half
end NonosExtraction.AddrVirt
diff --git a/verification/extraction/lean/NonosExtraction/AlgIdTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/AlgIdTypesRefinement.lean
index aed329c18c..e1f7a1e7c1 100644
--- a/verification/extraction/lean/NonosExtraction/AlgIdTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/AlgIdTypesRefinement.lean
@@ -35,8 +35,36 @@ namespace NonosExtraction.AlgIdTypes
theorem the_algid_as_u8_wrapper_is_its_method (a : types.AlgId) :
algid_as_u8 a = types.AlgId.as_u8 a := rfl
+/-! ### Algorithm identifiers are their wire bytes
+
+`as_u8` is the byte that names a signature algorithm on the wire. The theorems
+below fix it to 0x01 through 0x04 for Ed25519, ML-DSA-44, ML-DSA-65 and ML-DSA-87,
+the same bytes `from_u8` matches in the kernel source (`from_u8` is not
+extracted, so that agreement is with its source), show that zero is never
+produced, and show that two algorithms never share a byte, so a signature
+tagged by one can never be read as another. -/
+
+/-- Each algorithm encodes as the byte `from_u8` decodes back to it. -/
+theorem algid_as_u8_is_the_wire_byte :
+ algid_as_u8 .Ed25519 = ok 1#u8 ∧ algid_as_u8 .MlDsa44 = ok 2#u8 ∧
+ algid_as_u8 .MlDsa65 = ok 3#u8 ∧ algid_as_u8 .MlDsa87 = ok 4#u8 :=
+ ⟨rfl, rfl, rfl, rfl⟩
+
+/-- Every encoding lies in 1 to 4, so a zeroed tag is never a valid algorithm. -/
+theorem algid_as_u8_lies_between_one_and_four (a : types.AlgId) :
+ ∃ r, algid_as_u8 a = ok r ∧ 1 ≤ r.val ∧ r.val ≤ 4 := by
+ cases a <;> exact ⟨_, rfl, by decide, by decide⟩
+
+/-- Distinct algorithms have distinct encodings. -/
+theorem algid_as_u8_is_injective (a b : types.AlgId)
+ (h : algid_as_u8 a = algid_as_u8 b) : a = b := by
+ cases a <;> cases b <;> first | rfl | (simp [algid_as_u8, types.AlgId.as_u8, types.AlgId.read_discriminant] at h)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.AlgIdTypes.the_algid_as_u8_wrapper_is_its_method
+#print axioms NonosExtraction.AlgIdTypes.algid_as_u8_is_the_wire_byte
+#print axioms NonosExtraction.AlgIdTypes.algid_as_u8_lies_between_one_and_four
+#print axioms NonosExtraction.AlgIdTypes.algid_as_u8_is_injective
end NonosExtraction.AlgIdTypes
diff --git a/verification/extraction/lean/NonosExtraction/AmlTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/AmlTypesRefinement.lean
index 3d9df553fc..52b02c068e 100644
--- a/verification/extraction/lean/NonosExtraction/AmlTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/AmlTypesRefinement.lean
@@ -38,9 +38,106 @@ theorem the_lpsscontroller_is_valid_wrapper_is_its_method (a : types.LpssControl
theorem the_gpiocontroller_is_valid_wrapper_is_its_method (a : types.GpioController) :
gpiocontroller_is_valid a = types.GpioController.is_valid a := rfl
+/-! ### A controller is kept only with a usable MMIO window
+
+ `enumerate_i2c_controllers` and `enumerate_gpio_controllers` push a
+ controller onto their result exactly when `is_valid` holds, and the drivers
+ that follow map the window it names. The theorems below establish that
+ `is_valid` holds exactly when both the MMIO base and the MMIO size are
+ nonzero, that a zero base or a zero size is refused whatever the other
+ field says, that the smallest nonzero window is accepted, and that the
+ interrupt and identity fields play no part. The GPIO size is a 64 bit field,
+ and a window whose size has only bits above the low 32 set is still
+ accepted, so the check does not truncate. They cannot establish that the
+ window is backed by real device memory, nor anything about the AML parsing
+ that fills these fields, which is not extracted.
+-/
+
+/-- The I2C controller check is the conjunction of a nonzero base and a nonzero
+size. -/
+theorem lpsscontroller_is_valid_iff_base_and_size_are_nonzero (c : types.LpssController) :
+ lpsscontroller_is_valid c = ok (decide (c.mmio_base.val ≠ 0 ∧ c.mmio_size.val ≠ 0)) := by
+ unfold lpsscontroller_is_valid types.LpssController.is_valid
+ by_cases hb : c.mmio_base = 0#u64
+ · simp [hb]
+ · have hbv : c.mmio_base.val ≠ 0 := fun h => hb (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ by_cases hs : c.mmio_size = 0#u32
+ · simp [hb, hs]
+ · have hsv : c.mmio_size.val ≠ 0 := fun h => hs (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ simp [hb, hs, hbv, hsv]
+
+/-- The GPIO controller check is the conjunction of a nonzero base and a nonzero
+size. -/
+theorem gpiocontroller_is_valid_iff_base_and_size_are_nonzero (c : types.GpioController) :
+ gpiocontroller_is_valid c = ok (decide (c.mmio_base.val ≠ 0 ∧ c.mmio_size.val ≠ 0)) := by
+ unfold gpiocontroller_is_valid types.GpioController.is_valid
+ by_cases hb : c.mmio_base = 0#u64
+ · simp [hb]
+ · have hbv : c.mmio_base.val ≠ 0 := fun h => hb (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ by_cases hs : c.mmio_size = 0#u64
+ · simp [hb, hs]
+ · have hsv : c.mmio_size.val ≠ 0 := fun h => hs (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ simp [hb, hs, hbv, hsv]
+
+/-- A zero-length I2C window is refused even at a nonzero base. -/
+theorem lpsscontroller_is_valid_refuses_a_zero_size (c : types.LpssController)
+ (hs : c.mmio_size = 0#u32) : lpsscontroller_is_valid c = ok false := by
+ rw [lpsscontroller_is_valid_iff_base_and_size_are_nonzero, hs]
+ simp
+
+/-- An I2C window at base zero is refused whatever its size. -/
+theorem lpsscontroller_is_valid_refuses_a_zero_base (c : types.LpssController)
+ (hb : c.mmio_base = 0#u64) : lpsscontroller_is_valid c = ok false := by
+ rw [lpsscontroller_is_valid_iff_base_and_size_are_nonzero, hb]
+ simp
+
+/-- A zero-length GPIO window is refused even at a nonzero base. -/
+theorem gpiocontroller_is_valid_refuses_a_zero_size (c : types.GpioController)
+ (hs : c.mmio_size = 0#u64) : gpiocontroller_is_valid c = ok false := by
+ rw [gpiocontroller_is_valid_iff_base_and_size_are_nonzero, hs]
+ simp
+
+/-- A GPIO window at base zero is refused whatever its size. -/
+theorem gpiocontroller_is_valid_refuses_a_zero_base (c : types.GpioController)
+ (hb : c.mmio_base = 0#u64) : gpiocontroller_is_valid c = ok false := by
+ rw [gpiocontroller_is_valid_iff_base_and_size_are_nonzero, hb]
+ simp
+
+/-- The smallest nonzero I2C window, one byte at address one, is accepted. -/
+theorem lpsscontroller_is_valid_accepts_the_smallest_window
+ (c : types.LpssController) (hb : c.mmio_base = 1#u64) (hs : c.mmio_size = 1#u32) :
+ lpsscontroller_is_valid c = ok true := by
+ rw [lpsscontroller_is_valid_iff_base_and_size_are_nonzero, hb, hs]
+ simp
+
+/-- The interrupt fields do not enter into the I2C check: a controller without
+an interrupt is kept or dropped exactly as the same window with one. -/
+theorem lpsscontroller_is_valid_ignores_the_interrupt (c : types.LpssController)
+ (b : Bool) (q : Std.U32) :
+ lpsscontroller_is_valid { c with has_irq := b, irq := q } = lpsscontroller_is_valid c := by
+ rw [lpsscontroller_is_valid_iff_base_and_size_are_nonzero,
+ lpsscontroller_is_valid_iff_base_and_size_are_nonzero]
+
+/-- A GPIO window whose size is exactly 2^32 has a zero low word and is still
+accepted: the 64 bit size is compared whole, not truncated. -/
+theorem gpiocontroller_is_valid_does_not_truncate_the_size (c : types.GpioController)
+ (hb : c.mmio_base = 1#u64) (hs : c.mmio_size.val = 2 ^ 32) :
+ gpiocontroller_is_valid c = ok true := by
+ rw [gpiocontroller_is_valid_iff_base_and_size_are_nonzero, hb, hs]
+ simp
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.AmlTypes.the_lpsscontroller_is_valid_wrapper_is_its_method
#print axioms NonosExtraction.AmlTypes.the_gpiocontroller_is_valid_wrapper_is_its_method
+#print axioms NonosExtraction.AmlTypes.lpsscontroller_is_valid_iff_base_and_size_are_nonzero
+#print axioms NonosExtraction.AmlTypes.gpiocontroller_is_valid_iff_base_and_size_are_nonzero
+#print axioms NonosExtraction.AmlTypes.lpsscontroller_is_valid_refuses_a_zero_size
+#print axioms NonosExtraction.AmlTypes.lpsscontroller_is_valid_refuses_a_zero_base
+#print axioms NonosExtraction.AmlTypes.gpiocontroller_is_valid_refuses_a_zero_size
+#print axioms NonosExtraction.AmlTypes.gpiocontroller_is_valid_refuses_a_zero_base
+#print axioms NonosExtraction.AmlTypes.lpsscontroller_is_valid_accepts_the_smallest_window
+#print axioms NonosExtraction.AmlTypes.lpsscontroller_is_valid_ignores_the_interrupt
+#print axioms NonosExtraction.AmlTypes.gpiocontroller_is_valid_does_not_truncate_the_size
end NonosExtraction.AmlTypes
diff --git a/verification/extraction/lean/NonosExtraction/ApicTimerModeRefinement.lean b/verification/extraction/lean/NonosExtraction/ApicTimerModeRefinement.lean
index b9abcd67f5..4d070b9754 100644
--- a/verification/extraction/lean/NonosExtraction/ApicTimerModeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ApicTimerModeRefinement.lean
@@ -38,9 +38,174 @@ theorem the_divider_to_code_wrapper_is_its_method (a : Std.U8) :
theorem the_calibrate_timer_wrapper_is_its_method (a : Std.U32) :
calibrate_timer a = timer_mode.calibrate_timer a := rfl
+/-! ### The divide code and the initial count
+
+`divider_to_code` produces the value `timer_enable` and `timer_oneshot` write to
+the local APIC Divide Configuration Register. The Intel SDM (Vol. 3A, 11.5.4)
+defines that register by bits 0, 1 and 3, with bit 2 reserved: reading those
+three bits as `b3 b1 b0`, the timer divides the bus clock by `2^((b3b1b0 + 1)
+mod 8)`. `sdmDivisor` restates that decoding independently of the match table,
+and the theorems below show every supported divisor encodes to a code that the
+hardware decodes back to the same divisor, that no code sets a reserved bit,
+and that an unsupported divisor falls back to divide by 16.
+
+`calibrate_timer` gives the initial count `timer_enable` programs for a
+requested rate. The theorems pin it exactly, then draw out what the caller
+relies on: it never fails (the divisor is clamped to at least 1), the count is
+never below 50000, so the timer is never programmed with 0 (which stops it) or
+with a period short enough to storm the core, a faster rate never gets a longer
+count, and the floor takes over from exactly 200 kHz upward. Whether 10000000
+ticks is one second on a given machine depends on the bus clock and divider,
+which nothing here models; the MMIO and MSR writes are not extracted.
+-/
+
+/-- The divisor the local APIC applies for a Divide Configuration Register value,
+ read from bits 0, 1 and 3 as the SDM specifies. -/
+def sdmDivisor (c : Nat) : Nat := 2 ^ ((c % 4 + c / 8 % 2 * 4 + 1) % 8)
+
+/-- Every divisor the hardware supports encodes to a code the hardware decodes
+ back to that divisor. -/
+theorem divider_to_code_encodes_each_supported_divisor_as_the_sdm_reads_it (d : Std.U8)
+ (h : d.val ∈ [1, 2, 4, 8, 16, 32, 64, 128]) :
+ ∃ c, divider_to_code d = ok c ∧ sdmDivisor c.val = d.val := by
+ unfold divider_to_code timer_mode.divider_to_code
+ split
+ all_goals first
+ | exact ⟨_, rfl, by decide⟩
+ | skip
+ rename_i h1 h2 h3 h4 h5 h6 h7 h8
+ exfalso
+ simp at h
+ rcases h with h | h | h | h | h | h | h | h
+ all_goals first
+ | exact h1 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h2 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h3 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h4 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h5 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h6 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h7 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ | exact h8 (UScalar.eq_of_val_eq (by rw [h]; rfl))
+
+/-- An unsupported divisor is not refused: it is programmed as divide by 16,
+ the same code the table gives for 16 itself. -/
+theorem divider_to_code_falls_back_to_divide_by_16 (d : Std.U8)
+ (h : d.val ∉ [1, 2, 4, 8, 16, 32, 64, 128]) :
+ divider_to_code d = ok 3#u32 ∧ divider_to_code 16#u8 = ok 3#u32 := by
+ refine ⟨?_, rfl⟩
+ unfold divider_to_code timer_mode.divider_to_code
+ split
+ all_goals first
+ | (exfalso; apply h; decide)
+ | rfl
+
+/-- No code sets bit 2 or anything above bit 3, the reserved bits of the Divide
+ Configuration Register, whatever divisor is asked for. -/
+theorem divider_to_code_never_sets_a_reserved_bit (d : Std.U8) :
+ ∃ c, divider_to_code d = ok c ∧ c.val < 16 ∧ c.val.testBit 2 = false := by
+ unfold divider_to_code timer_mode.divider_to_code
+ split <;> exact ⟨_, rfl, by decide, by decide⟩
+
+/-- The initial count as a function of the requested rate, over the naturals. -/
+def calibrateModel (hz : Nat) : Nat :=
+ if hz < 1000 then 10000000 else max (10000000 / (hz / 1000)) 50000
+
+private theorem max_val (x y : Std.U32) :
+ (core.cmp.impls.OrdU32.max x y).val = max x.val y.val := by
+ simp only [core.cmp.impls.OrdU32.max]
+ split <;> rename_i h <;> simp only [UScalar.lt_equiv] at h <;> omega
+
+/-- `calibrate_timer` never fails and computes `calibrateModel` exactly: below
+ 1 kHz the count is 10000000, and from 1 kHz up it is 10000000 divided by
+ the rate in whole kilohertz, but never below 50000. -/
+theorem calibrate_timer_is_ten_million_over_the_rate_in_khz (hz : Std.U32) :
+ ∃ c, calibrate_timer hz = ok c ∧ c.val = calibrateModel hz.val := by
+ unfold calibrate_timer timer_mode.calibrate_timer calibrateModel
+ split
+ · rename_i hge
+ have hge' : 1000 ≤ hz.val := by simpa using hge
+ obtain ⟨i, hi, hiv⟩ := UScalar.div_spec hz (y := 1000#u32) (by simp)
+ rw [hi, bind_tc_ok]
+ simp only [lift, bind_tc_ok]
+ have hj : (core.cmp.impls.OrdU32.max i 1#u32).val = hz.val / 1000 := by
+ rw [max_val, hiv]
+ have : 1 ≤ hz.val / 1000 := by omega
+ simp; omega
+ obtain ⟨q, hq, hqv⟩ :=
+ UScalar.div_spec 10000000#u32 (y := core.cmp.impls.OrdU32.max i 1#u32) (by omega)
+ rw [hq, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ rw [max_val, hqv, hj]
+ simp [Nat.not_lt.mpr hge']
+ · rename_i hlt
+ have hlt' : hz.val < 1000 := by simp at hlt; omega
+ refine ⟨_, rfl, ?_⟩
+ rw [max_val]
+ simp [hlt']
+
+/-- The count `timer_enable` writes is never 0 and never below 50000, and never
+ above 10000000, for every 32-bit rate including 0. -/
+theorem calibrate_timer_stays_between_50000_and_10000000 (hz : Std.U32) :
+ ∃ c, calibrate_timer hz = ok c ∧ 50000 ≤ c.val ∧ c.val ≤ 10000000 := by
+ obtain ⟨c, hc, hv⟩ := calibrate_timer_is_ten_million_over_the_rate_in_khz hz
+ refine ⟨c, hc, ?_⟩
+ rw [hv]
+ unfold calibrateModel
+ split
+ · omega
+ · have := Nat.div_le_self 10000000 (hz.val / 1000)
+ omega
+
+/-- A faster requested rate never gets a longer initial count. -/
+theorem calibrate_timer_does_not_lengthen_the_period_as_the_rate_rises
+ (hz₁ hz₂ : Std.U32) (h : hz₁.val ≤ hz₂.val) :
+ ∃ c₁ c₂, calibrate_timer hz₁ = ok c₁ ∧ calibrate_timer hz₂ = ok c₂ ∧ c₂.val ≤ c₁.val := by
+ obtain ⟨c₁, h₁, v₁⟩ := calibrate_timer_is_ten_million_over_the_rate_in_khz hz₁
+ obtain ⟨c₂, h₂, v₂⟩ := calibrate_timer_is_ten_million_over_the_rate_in_khz hz₂
+ refine ⟨c₁, c₂, h₁, h₂, ?_⟩
+ rw [v₁, v₂]
+ unfold calibrateModel
+ have hd := Nat.div_le_self 10000000 (hz₂.val / 1000)
+ split <;> split
+ · omega
+ · omega
+ · omega
+ · have hk : hz₁.val / 1000 ≤ hz₂.val / 1000 := Nat.div_le_div_right h
+ have hpos : 0 < hz₁.val / 1000 := by omega
+ have := Nat.div_le_div_left hk hpos (a := 10000000)
+ omega
+
+/-- The 50000 floor is reached exactly at 200 kHz: at 200000 Hz and above the
+ count is exactly 50000, and at every rate below it the count is larger. -/
+theorem calibrate_timer_hits_its_floor_exactly_from_200_khz (hz : Std.U32) :
+ ∃ c, calibrate_timer hz = ok c ∧ (c.val = 50000 ↔ 200000 ≤ hz.val) := by
+ obtain ⟨c, hc, hv⟩ := calibrate_timer_is_ten_million_over_the_rate_in_khz hz
+ refine ⟨c, hc, ?_⟩
+ rw [hv]
+ unfold calibrateModel
+ split
+ · omega
+ · rename_i hge
+ by_cases hk : 200 ≤ hz.val / 1000
+ · have := Nat.div_le_div_left hk (by decide : 0 < 200) (a := 10000000)
+ have h2 : (10000000 : Nat) / 200 = 50000 := by decide
+ omega
+ · have hk' : hz.val / 1000 ≤ 199 := by omega
+ have hpos : 0 < hz.val / 1000 := by omega
+ have := Nat.div_le_div_left hk' hpos (a := 10000000)
+ have h2 : (10000000 : Nat) / 199 = 50251 := by decide
+ omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ApicTimerMode.the_divider_to_code_wrapper_is_its_method
#print axioms NonosExtraction.ApicTimerMode.the_calibrate_timer_wrapper_is_its_method
+#print axioms NonosExtraction.ApicTimerMode.divider_to_code_encodes_each_supported_divisor_as_the_sdm_reads_it
+#print axioms NonosExtraction.ApicTimerMode.divider_to_code_falls_back_to_divide_by_16
+#print axioms NonosExtraction.ApicTimerMode.divider_to_code_never_sets_a_reserved_bit
+#print axioms NonosExtraction.ApicTimerMode.calibrate_timer_is_ten_million_over_the_rate_in_khz
+#print axioms NonosExtraction.ApicTimerMode.calibrate_timer_stays_between_50000_and_10000000
+#print axioms NonosExtraction.ApicTimerMode.calibrate_timer_does_not_lengthen_the_period_as_the_rate_rises
+#print axioms NonosExtraction.ApicTimerMode.calibrate_timer_hits_its_floor_exactly_from_200_khz
end NonosExtraction.ApicTimerMode
diff --git a/verification/extraction/lean/NonosExtraction/AttributesKindRefinement.lean b/verification/extraction/lean/NonosExtraction/AttributesKindRefinement.lean
index 6e66ce440b..34e2dd954d 100644
--- a/verification/extraction/lean/NonosExtraction/AttributesKindRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/AttributesKindRefinement.lean
@@ -38,9 +38,102 @@ theorem the_memorytype_attr_index_wrapper_is_its_method (a : kind.MemoryType) :
theorem the_memorytype_mair_attr_wrapper_is_its_method (a : kind.MemoryType) :
memorytype_mair_attr a = kind.MemoryType.mair_attr a := rfl
+/-! ### Descriptor slots and MAIR_EL1 bytes agree
+
+ A page descriptor names its memory type by the slot `attr_index` returns, and
+ `control::mair` builds MAIR_EL1 by placing each type's `mair_attr` byte at
+ that slot. `mairFold` below is that loop over `MemoryType::ALL`, computed on
+ unbounded naturals. The theorems show that every slot fits the three bit
+ `AttrIndx` field and one of the eight register bytes, that no two types
+ share a slot, that reading the assembled register back at a type's slot
+ gives exactly that type's byte, that the one unclaimed slot reads as
+ Device-nGnRnE, and that each byte is a well formed MAIR encoding of the kind
+ its name says.
+
+ `mair_value`, `MemoryType::ALL` and the `msr` that writes the register are
+ not in the extracted crate, so `mairFold` is a transcription of the loop
+ rather than the loop itself, and its list is copied from `ALL` by hand.
+-/
+
+/-- The memory types in the order `MemoryType::ALL` lists them. -/
+def allTypes : List kind.MemoryType :=
+ [.DeviceNGnRnE, .DeviceNGnRE, .DeviceNGRE, .DeviceGRE, .NormalNC, .NormalWT, .NormalWB]
+
+/-- One type's contribution to MAIR_EL1: its attribute byte shifted to its slot. -/
+def slotByte (t : kind.MemoryType) : Nat :=
+ match memorytype_attr_index t, memorytype_mair_attr t with
+ | ok i, ok m => m.val <<< (i.val * 8)
+ | _, _ => 0
+
+/-- The register `control::mair` assembles, as an OR over every type. -/
+def mairFold : Nat := allTypes.foldl (fun acc t => acc ||| slotByte t) 0
+
+/-- Which types the kernel means as Device memory. -/
+def isDevice : kind.MemoryType → Bool
+ | .DeviceNGnRnE | .DeviceNGnRE | .DeviceNGRE | .DeviceGRE => true
+ | _ => false
+
+/-- Every slot is below eight, so `attr_index << 2` stays inside `AttrIndx[2:0]`
+ (descriptor bits 2 to 4) and `attr_index * 8` names a byte of the 64 bit
+ register. A slot of eight would spill into the NS bit of every descriptor. -/
+theorem memorytype_attr_index_fits_attrindx (t : kind.MemoryType) :
+ ∃ i, memorytype_attr_index t = ok i ∧ i.val < 8 := by
+ cases t <;> exact ⟨_, rfl, by decide⟩
+
+/-- No two memory types share a slot. Two types in one slot would leave one of
+ them mapped with the other's caching rules. -/
+theorem memorytype_attr_index_is_injective (a b : kind.MemoryType) (i : Std.U64)
+ (ha : memorytype_attr_index a = ok i) (hb : memorytype_attr_index b = ok i) :
+ a = b := by
+ cases a <;> cases b <;>
+ simp only [memorytype_attr_index, kind.MemoryType.attr_index, ok.injEq] at ha hb <;>
+ subst ha <;> first | rfl |
+ (have := congrArg (fun x : Std.U64 => x.val) hb; simp at this)
+
+/-- Reading the assembled register at a type's slot gives that type's byte, so
+ the index a descriptor carries and the meaning MAIR_EL1 gives it agree. The
+ assembled value also fits in 64 bits, so the unbounded fold is the same
+ number the kernel's `u64` loop produces. -/
+theorem memorytype_mair_attr_is_the_byte_at_memorytype_attr_index
+ (t : kind.MemoryType) (i : Std.U64) (m : Std.U8)
+ (hi : memorytype_attr_index t = ok i) (hm : memorytype_mair_attr t = ok m) :
+ (mairFold >>> (i.val * 8)) % 256 = m.val ∧ mairFold < 2 ^ 64 := by
+ cases t <;>
+ simp only [memorytype_attr_index, kind.MemoryType.attr_index,
+ memorytype_mair_attr, kind.MemoryType.mair_attr, ok.injEq] at hi hm <;>
+ subst hi hm <;> decide
+
+/-- The slot no type claims holds zero, which MAIR reads as Device-nGnRnE, the
+ strictest type, as the comment on `mair_value` intends. -/
+theorem the_slot_no_memorytype_attr_index_names_reads_as_device_ngnrne :
+ (∀ t i, memorytype_attr_index t = ok i → i.val ≠ 7) ∧ (mairFold >>> 56) % 256 = 0 := by
+ refine ⟨?_, by decide⟩
+ intro t i h
+ cases t <;> simp only [memorytype_attr_index, kind.MemoryType.attr_index, ok.injEq] at h <;>
+ subst h <;> decide
+
+/-- Each byte is a legal MAIR encoding of the kind its name says. A Device byte
+ has a zero high nibble and the form `0b0000dd00`, since a non-zero low pair
+ is UNPREDICTABLE. A Normal byte has a non-zero inner nibble (an inner nibble
+ of zero under a non-zero outer one is UNPREDICTABLE) and the same policy
+ inside and out. -/
+theorem memorytype_mair_attr_encodes_device_and_normal_memory
+ (t : kind.MemoryType) (m : Std.U8) (h : memorytype_mair_attr t = ok m) :
+ (isDevice t = true ↔ m.val / 16 = 0) ∧
+ (isDevice t = true → m.val % 4 = 0) ∧
+ (isDevice t = false → m.val % 16 ≠ 0 ∧ m.val / 16 = m.val % 16) := by
+ cases t <;>
+ simp only [memorytype_mair_attr, kind.MemoryType.mair_attr, ok.injEq] at h <;>
+ subst h <;> decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.AttributesKind.the_memorytype_attr_index_wrapper_is_its_method
#print axioms NonosExtraction.AttributesKind.the_memorytype_mair_attr_wrapper_is_its_method
+#print axioms NonosExtraction.AttributesKind.memorytype_attr_index_fits_attrindx
+#print axioms NonosExtraction.AttributesKind.memorytype_attr_index_is_injective
+#print axioms NonosExtraction.AttributesKind.memorytype_mair_attr_is_the_byte_at_memorytype_attr_index
+#print axioms NonosExtraction.AttributesKind.the_slot_no_memorytype_attr_index_names_reads_as_device_ngnrne
+#print axioms NonosExtraction.AttributesKind.memorytype_mair_attr_encodes_device_and_normal_memory
end NonosExtraction.AttributesKind
diff --git a/verification/extraction/lean/NonosExtraction/BtiGuardRefinement.lean b/verification/extraction/lean/NonosExtraction/BtiGuardRefinement.lean
index 56a5113650..454efaa10f 100644
--- a/verification/extraction/lean/NonosExtraction/BtiGuardRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/BtiGuardRefinement.lean
@@ -35,8 +35,62 @@ namespace NonosExtraction.BtiGuard
theorem the_btiguard_instruction_wrapper_is_its_method (a : guard.BtiGuard) :
btiguard_instruction a = guard.BtiGuard.instruction a := rfl
+/-! ### The encodings of the landing pads
+
+ Each guard is emitted as an A64 hint instruction: the word
+ `0xD503201F ||| (imm << 5)` for a seven-bit immediate. The hint space is
+ architecturally a NOP on a core without FEAT_BTI, which is what lets the same
+ image run on cores with and without branch target identification. `C`, `J`
+ and `Jc` are BTI, hint 32, with the two target bits 6..7 equal to the
+ `repr(u8)` discriminant (1 accepts calls, 2 accepts jumps, 3 accepts both),
+ and `None` is hint 0, the plain NOP rather than a bare BTI (`0xD503241F`).
+ `BtiPadRefinement` relates these words to the landing-pad check: every guard
+ but `None` is a landing pad. These theorems cannot say whether the kernel places these words at
+ the targets of its indirect branches, or anything about the guarded page
+ attribute; neither is extracted.
+-/
+
+/-- The `repr(u8)` discriminant of each guard, as written in `guard.rs`. -/
+def btiGuardDiscriminant : guard.BtiGuard → Nat
+ | .None => 0
+ | .C => 1
+ | .J => 2
+ | .Jc => 3
+
+/-- Every guard lies in the hint space, so it executes as a NOP on a core
+ without branch target identification. -/
+theorem every_btiguard_instruction_is_a_hint (g : guard.BtiGuard) :
+ ∃ w, btiguard_instruction g = ok w ∧ w.val &&& 0xFFFFF01F = 0xD503201F := by
+ cases g <;> exact ⟨_, rfl, by decide⟩
+
+/-- A guarding variant is BTI with its discriminant in the target field; `None`
+ is the NOP, hint 0. -/
+theorem btiguard_instruction_is_bti_with_the_discriminant_as_targets (g : guard.BtiGuard) :
+ ∃ w, btiguard_instruction g = ok w ∧
+ w.val = (if btiGuardDiscriminant g = 0 then 0xD503201F
+ else 0xD503241F ||| (btiGuardDiscriminant g <<< 6)) := by
+ cases g <;> exact ⟨_, rfl, by decide⟩
+
+/-- `Jc` accepts both kinds of branch: its word is the union of the `C` and `J`
+ words. -/
+theorem btiguard_instruction_jc_is_c_and_j_together :
+ ∃ c j jc, btiguard_instruction .C = ok c ∧ btiguard_instruction .J = ok j ∧
+ btiguard_instruction .Jc = ok jc ∧ jc.val = c.val ||| j.val :=
+ ⟨_, _, _, rfl, rfl, rfl, by decide⟩
+
+/-- Distinct guards are distinct words, so no guard is emitted as another's
+ landing pad. -/
+theorem btiguard_instruction_tells_guards_apart (a b : guard.BtiGuard)
+ (h : btiguard_instruction a = btiguard_instruction b) : a = b := by
+ cases a <;> cases b <;> simp_all [btiguard_instruction, guard.BtiGuard.instruction]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.BtiGuard.the_btiguard_instruction_wrapper_is_its_method
+#print axioms NonosExtraction.BtiGuard.every_btiguard_instruction_is_a_hint
+#print axioms NonosExtraction.BtiGuard.btiguard_instruction_is_bti_with_the_discriminant_as_targets
+#print axioms NonosExtraction.BtiGuard.btiguard_instruction_jc_is_c_and_j_together
+#print axioms NonosExtraction.BtiGuard.btiguard_instruction_tells_guards_apart
+
end NonosExtraction.BtiGuard
diff --git a/verification/extraction/lean/NonosExtraction/BtiPad.lean b/verification/extraction/lean/NonosExtraction/BtiPad.lean
new file mode 100644
index 0000000000..6ebbc986f2
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/BtiPad.lean
@@ -0,0 +1,35 @@
+-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
+-- [nonos_x_bti_pad]
+import Aeneas
+open Aeneas Aeneas.Std Result ControlFlow Error
+set_option linter.dupNamespace false
+set_option linter.hashCommand false
+set_option linter.unusedVariables false
+
+/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
+set_option maxHeartbeats 1000000
+
+/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
+set_option maxRecDepth 2048
+
+namespace nonos_x_bti_pad
+
+/-- [nonos_x_bti_pad::pad::is_bti_landing_pad]:
+ Source: 'src/../../../../../src/arch/aarch64/security/bti/pad.rs', lines 27:0-29:1
+ Visibility: public -/
+def pad.is_bti_landing_pad (instruction : Std.U32) : Result Bool := do
+ match instruction with
+ | 3573752927#uscalar => ok true
+ | 3573752991#uscalar => ok true
+ | 3573753055#uscalar => ok true
+ | 3573752639#uscalar => ok true
+ | 3573752703#uscalar => ok true
+ | _ => ok false
+
+/-- [nonos_x_bti_pad::is_bti_landing_pad]:
+ Source: 'src/lib.rs', lines 10:0-12:1
+ Visibility: public -/
+def is_bti_landing_pad (instruction : Std.U32) : Result Bool := do
+ pad.is_bti_landing_pad instruction
+
+end nonos_x_bti_pad
diff --git a/verification/extraction/lean/NonosExtraction/BtiPadRefinement.lean b/verification/extraction/lean/NonosExtraction/BtiPadRefinement.lean
new file mode 100644
index 0000000000..674d3845e0
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/BtiPadRefinement.lean
@@ -0,0 +1,104 @@
+/-
+NONOS Operating System
+Copyright (C) 2026 NONOS Contributors
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version. See .
+
+bti_pad, on the extracted code.
+
+Charon cannot take an inherent method as an entry point, so the crate root
+carries a free function per method and those are the names the manifest counts.
+A wrapper that quietly did something other than call through would make every
+theorem about the method a statement about code nobody runs, so each one is
+proven to be its method below.
+
+`is_bti_landing_pad` decides whether an indirect branch may land on an
+instruction in a guarded page. The Armv8.5 rules: `BTI c`, `BTI j` and `BTI jc`
+are landing pads, and `PACIASP` and `PACIBSP` act as `BTI c`. A NOP and a bare
+`BTI`, which accepts no branch type, are not: a branch to either raises a
+Branch Target exception on a core with FEAT_BTI. `check_bti_landing_pad` used
+to accept the NOP and reject `PACIASP` and `PACIBSP`; it now reads the word and
+asks this function.
+
+The theorems below give the accepted set exactly and relate it to the words
+`BtiGuard::instruction` emits: every guard but `None` emits a landing pad, and
+`None`, which emits the NOP, marks code that must not be a branch target.
+
+What these cannot establish: that the word read at an address is the
+instruction the core fetches there. `check_bti_landing_pad` reads memory
+through a raw pointer, which is not extracted.
+-/
+
+import NonosExtraction.BtiPad
+import NonosExtraction.BtiGuard
+
+open Aeneas Aeneas.Std Result
+open nonos_x_bti_pad
+
+set_option linter.hashCommand false
+set_option maxRecDepth 100000
+
+namespace NonosExtraction.BtiPad
+
+/-! ### The forwarding functions add nothing -/
+
+theorem the_is_bti_landing_pad_wrapper_is_its_method (a : Std.U32) :
+ is_bti_landing_pad a = pad.is_bti_landing_pad a := rfl
+
+/-! ### The landing pads -/
+
+/-- The accepted words are exactly `BTI c` (`0xD503245F`), `BTI j`
+ (`0xD503249F`), `BTI jc` (`0xD50324DF`), `PACIASP` (`0xD503233F`) and
+ `PACIBSP` (`0xD503237F`). -/
+theorem is_bti_landing_pad_is_exactly_the_five_landing_pads (w : Std.U32) :
+ is_bti_landing_pad w =
+ ok (decide (w.val ∈ [0xD503245F, 0xD503249F, 0xD50324DF, 0xD503233F, 0xD503237F])) := by
+ unfold is_bti_landing_pad pad.is_bti_landing_pad
+ split
+ all_goals first
+ | rfl
+ | (rename_i h1 h2 h3 h4 h5
+ congr 1
+ symm
+ simp only [decide_eq_false_iff_not, List.mem_cons, List.not_mem_nil, or_false]
+ intro h
+ rcases h with h | h | h | h | h
+ · exact h1 (UScalar.eq_of_val_eq h)
+ · exact h2 (UScalar.eq_of_val_eq h)
+ · exact h3 (UScalar.eq_of_val_eq h)
+ · exact h4 (UScalar.eq_of_val_eq h)
+ · exact h5 (UScalar.eq_of_val_eq h))
+
+/-- The NOP (`0xD503201F`), hint 0, and the bare `BTI` (`0xD503241F`) are not
+ landing pads. The check used to accept the NOP. -/
+theorem nop_and_bare_bti_are_not_landing_pads :
+ is_bti_landing_pad 0xD503201F#u32 = ok false ∧
+ is_bti_landing_pad 0xD503241F#u32 = ok false := ⟨rfl, rfl⟩
+
+/-- `PACIASP` and `PACIBSP` are landing pads, as the architecture treats them
+ as `BTI c`. The check used to reject both. -/
+theorem paciasp_and_pacibsp_are_landing_pads :
+ is_bti_landing_pad 0xD503233F#u32 = ok true ∧
+ is_bti_landing_pad 0xD503237F#u32 = ok true := ⟨rfl, rfl⟩
+
+/-- Every guard but `None` emits a landing pad, and `None` emits a word that is
+ not one, so code marked `None` cannot be reached by an indirect branch in a
+ guarded page. -/
+theorem a_guard_emits_a_landing_pad_exactly_unless_it_is_none
+ (g : nonos_x_bti_guard.guard.BtiGuard) :
+ ∃ w, nonos_x_bti_guard.btiguard_instruction g = ok w ∧
+ is_bti_landing_pad w = ok (match g with | .None => false | _ => true) := by
+ cases g <;> exact ⟨_, rfl, rfl⟩
+
+/-! ### Axiom profile -/
+
+#print axioms NonosExtraction.BtiPad.the_is_bti_landing_pad_wrapper_is_its_method
+#print axioms NonosExtraction.BtiPad.is_bti_landing_pad_is_exactly_the_five_landing_pads
+#print axioms NonosExtraction.BtiPad.nop_and_bare_bti_are_not_landing_pads
+#print axioms NonosExtraction.BtiPad.paciasp_and_pacibsp_are_landing_pads
+#print axioms NonosExtraction.BtiPad.a_guard_emits_a_landing_pad_exactly_unless_it_is_none
+
+end NonosExtraction.BtiPad
diff --git a/verification/extraction/lean/NonosExtraction/BuddyTypesStatsRefinement.lean b/verification/extraction/lean/NonosExtraction/BuddyTypesStatsRefinement.lean
index ead035916c..ab61467f10 100644
--- a/verification/extraction/lean/NonosExtraction/BuddyTypesStatsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/BuddyTypesStatsRefinement.lean
@@ -38,9 +38,89 @@ theorem the_allocstats_new_wrapper_is_its_method :
theorem the_allocstats_free_memory_wrapper_is_its_method (a : stats.AllocStats) (b : Std.U64) :
allocstats_free_memory a b = stats.AllocStats.free_memory a b := rfl
+/-! ### Free memory is a saturating difference that never fails
+
+`free_memory` takes the total the caller supplies and subtracts what the
+allocator has handed out, clamping at zero when the books show more
+allocated than the total (the same saturating reading `phys::free_memory`
+uses for physical frames). The theorems below establish that it never
+fails for any record and any total, that the result is exactly the natural
+number difference truncated at zero, that it is zero at and below the
+allocated amount, and that a fresh record, which `new` starts with every
+counter at zero, reports the whole total as free. They cannot establish that
+`total_allocated` tracks the ranges the allocator actually holds: the buddy
+allocator state and its lock are not extracted. -/
+
+/-- Every counter of a fresh allocator statistics record is zero. -/
+theorem allocstats_new_counts_nothing :
+ allocstats_new = ok
+ { total_allocated := 0#u64, peak_allocated := 0#u64,
+ allocation_count := 0#usize, free_count := 0#usize,
+ active_ranges := 0#usize } := rfl
+
+/-- `free_memory` always returns, and what it returns is `total` minus
+`total_allocated` truncated at zero: the saturating subtraction, with no
+underflow and no wrap. -/
+theorem allocstats_free_memory_is_the_saturating_difference
+ (s : stats.AllocStats) (total : Std.U64) :
+ ∃ r, allocstats_free_memory s total = ok r ∧
+ r.val = total.val - s.total_allocated.val := by
+ unfold allocstats_free_memory stats.AllocStats.free_memory
+ by_cases hgt : total > s.total_allocated
+ · simp only [hgt, if_true]
+ have hle : s.total_allocated.val ≤ total.val := by
+ have : s.total_allocated.val < total.val := hgt
+ omega
+ have hs := UScalar.sub_equiv total s.total_allocated
+ cases hr : (total - s.total_allocated : Result Std.U64) with
+ | ok z => rw [hr] at hs; exact ⟨z, rfl, by omega⟩
+ | fail e => rw [hr] at hs; omega
+ | div => rw [hr] at hs; exact hs.elim
+ · simp only [hgt, if_false]
+ have : ¬ s.total_allocated.val < total.val := hgt
+ exact ⟨0#u64, rfl, by simp; omega⟩
+
+/-- With nothing allocated beyond the total, the free amount plus the
+allocated amount is the total, so no byte is lost or invented. -/
+theorem allocstats_free_memory_plus_allocated_is_the_total
+ (s : stats.AllocStats) (total : Std.U64)
+ (h : s.total_allocated.val ≤ total.val) :
+ ∃ r, allocstats_free_memory s total = ok r ∧
+ r.val + s.total_allocated.val = total.val := by
+ obtain ⟨r, hr, hv⟩ := allocstats_free_memory_is_the_saturating_difference s total
+ exact ⟨r, hr, by omega⟩
+
+/-- When the books show at least the whole total allocated (including exactly
+the total), `free_memory` reports zero rather than failing or wrapping to a
+huge value. -/
+theorem allocstats_free_memory_is_zero_when_allocated_reaches_the_total
+ (s : stats.AllocStats) (total : Std.U64)
+ (h : total.val ≤ s.total_allocated.val) :
+ allocstats_free_memory s total = ok 0#u64 := by
+ obtain ⟨r, hr, hv⟩ := allocstats_free_memory_is_the_saturating_difference s total
+ have : r = 0#u64 := UScalar.eq_of_val_eq (by simp; omega)
+ rw [hr, this]
+
+/-- A fresh record reports the entire total as free. -/
+theorem allocstats_free_memory_of_new_is_the_total (total : Std.U64) :
+ (do let s ← allocstats_new; allocstats_free_memory s total) = ok total := by
+ rw [allocstats_new_counts_nothing]
+ simp only [bind_tc_ok]
+ obtain ⟨r, hr, hv⟩ := allocstats_free_memory_is_the_saturating_difference
+ { total_allocated := 0#u64, peak_allocated := 0#u64,
+ allocation_count := 0#usize, free_count := 0#usize,
+ active_ranges := 0#usize } total
+ have : r = total := UScalar.eq_of_val_eq (by simp at hv; omega)
+ rw [hr, this]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.BuddyTypesStats.the_allocstats_new_wrapper_is_its_method
#print axioms NonosExtraction.BuddyTypesStats.the_allocstats_free_memory_wrapper_is_its_method
+#print axioms NonosExtraction.BuddyTypesStats.allocstats_new_counts_nothing
+#print axioms NonosExtraction.BuddyTypesStats.allocstats_free_memory_is_the_saturating_difference
+#print axioms NonosExtraction.BuddyTypesStats.allocstats_free_memory_plus_allocated_is_the_total
+#print axioms NonosExtraction.BuddyTypesStats.allocstats_free_memory_is_zero_when_allocated_reaches_the_total
+#print axioms NonosExtraction.BuddyTypesStats.allocstats_free_memory_of_new_is_the_total
end NonosExtraction.BuddyTypesStats
diff --git a/verification/extraction/lean/NonosExtraction/CacheTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/CacheTypesRefinement.lean
index 7b251557a7..2034c861b9 100644
--- a/verification/extraction/lean/NonosExtraction/CacheTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CacheTypesRefinement.lean
@@ -38,9 +38,60 @@ theorem the_cachestatistics_new_wrapper_is_its_method :
theorem the_cachestatistics_reset_wrapper_is_its_method (a : types.CacheStatistics) :
cachestatistics_reset a = types.CacheStatistics.reset a := rfl
+/-! ### Counters start at zero and reset to zero
+
+`CACHE_STATS` is built by `CacheStatistics::new`, and the cache code reads
+`hits` and `misses` back to compute a hit ratio, so every counter has to start
+from zero and `reset` has to return every counter there. The theorems below
+establish that each of the four fields `cachestatistics_new` returns is an
+atomic built from zero (no field is built from another value or left out), and
+that `cachestatistics_reset` stores zero with relaxed ordering into `hits`,
+`misses`, `evictions` and `writebacks`, each exactly once and in that order,
+and does nothing else.
+
+What they cannot establish is what those atomics hold. Aeneas leaves
+`AtomicU64::new` and `AtomicU64::store` opaque, so nothing here says that a
+load after the store returns zero, and the concurrency of a relaxed store
+racing a `fetch_add` from another core is outside the model. The static
+`CACHE_STATS` itself is not extracted.
+-/
+
+/-- Every counter `cachestatistics_new` returns is the atomic that
+ `AtomicU64::new(0)` returns. -/
+theorem every_counter_of_cachestatistics_new_is_built_from_zero
+ (s : types.CacheStatistics) (h : cachestatistics_new = ok s) :
+ core.sync.atomic.AtomicU64Align8U64.new 0#u64 = ok s.hits ∧
+ core.sync.atomic.AtomicU64Align8U64.new 0#u64 = ok s.misses ∧
+ core.sync.atomic.AtomicU64Align8U64.new 0#u64 = ok s.evictions ∧
+ core.sync.atomic.AtomicU64Align8U64.new 0#u64 = ok s.writebacks := by
+ unfold cachestatistics_new types.CacheStatistics.new at h
+ cases hn : core.sync.atomic.AtomicU64Align8U64.new 0#u64 with
+ | ok a =>
+ rw [hn] at h
+ simp only [bind_tc_ok, ok.injEq] at h
+ subst h
+ exact ⟨rfl, rfl, rfl, rfl⟩
+ | fail e => rw [hn] at h; cases h
+ | div => rw [hn] at h; cases h
+
+/-- `cachestatistics_reset` is the relaxed store of zero into each of the four
+ counters in turn, one store per counter, and nothing more. -/
+theorem cachestatistics_reset_stores_zero_relaxed_into_each_counter_once
+ (s : types.CacheStatistics) :
+ cachestatistics_reset s =
+ [s.hits, s.misses, s.evictions, s.writebacks].forM
+ (fun a => core.sync.atomic.AtomicU64Align8U64.store a 0#u64
+ core.sync.atomic.Ordering.Relaxed) := by
+ unfold cachestatistics_reset types.CacheStatistics.reset
+ simp only [List.forM]
+ cases core.sync.atomic.AtomicU64Align8U64.store s.writebacks 0#u64
+ core.sync.atomic.Ordering.Relaxed <;> rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CacheTypes.the_cachestatistics_new_wrapper_is_its_method
#print axioms NonosExtraction.CacheTypes.the_cachestatistics_reset_wrapper_is_its_method
+#print axioms NonosExtraction.CacheTypes.every_counter_of_cachestatistics_new_is_built_from_zero
+#print axioms NonosExtraction.CacheTypes.cachestatistics_reset_stores_zero_relaxed_into_each_counter_once
end NonosExtraction.CacheTypes
diff --git a/verification/extraction/lean/NonosExtraction/CapBehaviourRefinement.lean b/verification/extraction/lean/NonosExtraction/CapBehaviourRefinement.lean
index 9a8ccf8946..dc1b5a82db 100644
--- a/verification/extraction/lean/NonosExtraction/CapBehaviourRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CapBehaviourRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.CapBehaviour
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_cap_behaviour
@@ -38,9 +39,54 @@ theorem the_requires_write_buffer_flush_wrapper_is_its_method (a : Std.U64) :
theorem the_caching_mode_wrapper_is_its_method (a : Std.U64) :
caching_mode a = behaviour.caching_mode a := rfl
+/-! ### Which capability bit each behaviour reader returns
+
+`probe_at` stores both readers in `UnitInfo` from the Capability register.
+The theorems below say that `requires_write_buffer_flush` is exactly bit 4 of
+the word (RWBF) and `caching_mode` is exactly bit 7 (CM), as the VT-d
+specification places them, and that neither fails. They then state that the
+two are independent readers: a word carrying only the CM bit reports caching
+mode without asking for a write buffer flush, and a word carrying only RWBF
+does the reverse. A reader that tested the wrong bit, or both readers that
+tested the same one, would have a unit that caches not-present entries go
+without the invalidation it needs.
+
+Whether the kernel acts on either flag after a table write is decided in
+`probe_at` and its callers, which are not in this crate, so that obligation is
+not established here.
+-/
+
+/-- The write buffer flush requirement is bit 4 of the capability word. -/
+theorem requires_write_buffer_flush_reads_bit_four (cap : Std.U64) :
+ requires_write_buffer_flush cap = ok (cap.val.testBit 4) := by
+ unfold requires_write_buffer_flush behaviour.requires_write_buffer_flush
+ have hs : (1#u64 <<< 4#i32 : Result Std.U64) = ok 16#u64 := by rfl
+ simp only [hs, lift, bind_tc_ok]
+ rw [Bits.reads_bit cap 16#u64 0#u64 4 rfl rfl]
+
+/-- Caching mode is bit 7 of the capability word. -/
+theorem caching_mode_reads_bit_seven (cap : Std.U64) :
+ caching_mode cap = ok (cap.val.testBit 7) := by
+ unfold caching_mode behaviour.caching_mode
+ have hs : (1#u64 <<< 7#i32 : Result Std.U64) = ok 128#u64 := by rfl
+ simp only [hs, lift, bind_tc_ok]
+ rw [Bits.reads_bit cap 128#u64 0#u64 7 rfl rfl]
+
+/-- The two flags are separate bits: CM alone (`0x80`) is caching mode without
+ a flush requirement, and RWBF alone (`0x10`) is the reverse. -/
+theorem caching_mode_and_requires_write_buffer_flush_read_different_bits :
+ caching_mode 0x80#u64 = ok true ∧ requires_write_buffer_flush 0x80#u64 = ok false ∧
+ caching_mode 0x10#u64 = ok false ∧ requires_write_buffer_flush 0x10#u64 = ok true := by
+ rw [caching_mode_reads_bit_seven, caching_mode_reads_bit_seven,
+ requires_write_buffer_flush_reads_bit_four, requires_write_buffer_flush_reads_bit_four]
+ exact ⟨rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CapBehaviour.the_requires_write_buffer_flush_wrapper_is_its_method
#print axioms NonosExtraction.CapBehaviour.the_caching_mode_wrapper_is_its_method
+#print axioms NonosExtraction.CapBehaviour.requires_write_buffer_flush_reads_bit_four
+#print axioms NonosExtraction.CapBehaviour.caching_mode_reads_bit_seven
+#print axioms NonosExtraction.CapBehaviour.caching_mode_and_requires_write_buffer_flush_read_different_bits
end NonosExtraction.CapBehaviour
diff --git a/verification/extraction/lean/NonosExtraction/CapFaultRefinement.lean b/verification/extraction/lean/NonosExtraction/CapFaultRefinement.lean
index 26e93d7ccf..775653e9d5 100644
--- a/verification/extraction/lean/NonosExtraction/CapFaultRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CapFaultRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.CapFault
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_cap_fault
@@ -38,9 +39,126 @@ theorem the_fault_recording_offset_wrapper_is_its_method (a : Std.U64) :
theorem the_fault_recording_count_wrapper_is_its_method (a : Std.U64) :
fault_recording_count a = fault.fault_recording_count a := rfl
+/-! ### Where the fault records are and how many there are
+
+`drain_faults` walks `index` over `0..fault_recording_count(cap)`, and
+`take_fault` reads the record at `fault_recording_offset(cap) + index * 16`
+(its high half eight bytes further on). The theorems below say that
+`fault_recording_offset` is exactly the ten-bit FRO field (bits 24 to 33) times
+sixteen, that `fault_recording_count` is exactly the eight-bit NFR field (bits 40
+to 47) plus one, and that neither ever fails: the multiply cannot overflow a
+`usize` and the increment cannot overflow a `u16`, so no capability word the
+hardware reports makes either reader panic. The count is never zero, so a unit
+with records is always drained, and it never exceeds 256.
+
+They also say where that leaves the reads. `RemapUnit` maps a 4096 byte
+register window, and an FRO field of 256 or more already places the first
+record at or past its end, with the last record a unit may describe well
+beyond it. The readers do not bound the fields, so the bound has to come from
+a caller. It used to come only from a `debug_assert!` in the accessors, which
+release builds do not carry; `probe_at` now refuses a unit whose records do
+not fit through `registers_fit`, proven in `NonosExtraction.IommuRegsWindow`,
+and the accessors assert the bound in every build.
+
+`take_fault`, `drain_faults` and the MMIO accessors are not in this crate, so
+the loop bound and the address arithmetic of the callers are not established
+here; the window size is the kernel constant `UNIT_WINDOW`, restated as 4096.
+-/
+
+/-- A right shift of a 64-bit word by a constant below 64 succeeds and divides
+ by that power of two. -/
+private theorem shr_u64 (x : Std.U64) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 64) :
+ ∃ z : Std.U64, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+/-- The register offset is the FRO field, bits 24 to 33, counted in sixteen
+ byte units, and computing it never overflows. -/
+theorem fault_recording_offset_is_sixteen_times_bits_twenty_four_to_thirty_three
+ (cap : Std.U64) :
+ ∃ o : Std.Usize, fault_recording_offset cap = ok o ∧
+ o.val = cap.val / 2 ^ 24 % 1024 * 16 := by
+ unfold fault_recording_offset fault.fault_recording_offset
+ obtain ⟨z, hz, hv⟩ := shr_u64 cap 24#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ have hf : (UScalar.cast .Usize (z &&& 1023#u64)).val = cap.val / 2 ^ 24 % 1024 := by
+ rw [UScalar.cast_val_eq, Bits.land_low_mask z 1023#u64 10 rfl, hv]
+ simp only [show (24#i32 : Std.I32).toNat = 24 from rfl]
+ have h1 : cap.val / 2 ^ 24 % 2 ^ 10 < 2 ^ 10 := Nat.mod_lt _ (by decide)
+ have h2 : (2:Nat) ^ 10 ≤ 2 ^ UScalarTy.Usize.numBits := by
+ simp only [UScalarTy.numBits]
+ cases System.Platform.numBits_eq <;> simp [*]
+ rw [Nat.mod_eq_of_lt (by omega)]
+ rfl
+ obtain ⟨o, ho, hov⟩ := WP.spec_imp_exists
+ (Usize.mul_spec (x := UScalar.cast .Usize (z &&& 1023#u64)) (y := 16#usize)
+ (by rw [hf]; scalar_tac))
+ exact ⟨o, ho, by rw [hov, hf]; rfl⟩
+
+/-- The record count is the NFR field, bits 40 to 47, plus one, and the
+ increment never overflows. -/
+theorem fault_recording_count_is_bits_forty_to_forty_seven_plus_one (cap : Std.U64) :
+ ∃ n : Std.U16, fault_recording_count cap = ok n ∧
+ n.val = cap.val / 2 ^ 40 % 256 + 1 := by
+ unfold fault_recording_count fault.fault_recording_count
+ obtain ⟨z, hz, hv⟩ := shr_u64 cap 40#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ have hf : (UScalar.cast .U16 (z &&& 255#u64)).val = cap.val / 2 ^ 40 % 256 := by
+ rw [UScalar.cast_val_eq, Bits.land_low_mask z 255#u64 8 rfl, hv]
+ simp only [show (40#i32 : Std.I32).toNat = 40 from rfl]
+ simp [UScalarTy.numBits]
+ omega
+ obtain ⟨n, hn, hnv⟩ := WP.spec_imp_exists
+ (U16.add_spec (x := UScalar.cast .U16 (z &&& 255#u64)) (y := 1#u16)
+ (by rw [hf]; scalar_tac))
+ exact ⟨n, hn, by rw [hnv, hf]; rfl⟩
+
+/-- `drain_faults` always visits at least one record and never more than 256,
+ whatever capability word the unit reports. -/
+theorem fault_recording_count_is_between_one_and_two_hundred_fifty_six (cap : Std.U64) :
+ ∃ n : Std.U16, fault_recording_count cap = ok n ∧ 1 ≤ n.val ∧ n.val ≤ 256 := by
+ obtain ⟨n, hn, hv⟩ := fault_recording_count_is_bits_forty_to_forty_seven_plus_one cap
+ exact ⟨n, hn, by omega, by omega⟩
+
+/-- The first fault record lies at or past the end of the 4096 byte register
+ window that `probe_at` maps (`UNIT_WINDOW`) exactly when the FRO field is
+ 256 or more. `fault_recording_offset` does not refuse such a field;
+ `registers_fit` in `probe_at` does, and before it did the only bound was a
+ `debug_assert!` in `RemapUnit::read64`. -/
+theorem fault_recording_offset_leaves_the_register_window_once_fro_reaches_256
+ (cap : Std.U64) (o : Std.Usize) (h : fault_recording_offset cap = ok o) :
+ 4096 ≤ o.val ↔ 256 ≤ cap.val / 2 ^ 24 % 1024 := by
+ obtain ⟨o', ho', hv⟩ :=
+ fault_recording_offset_is_sixteen_times_bits_twenty_four_to_thirty_three cap
+ rw [h] at ho'
+ cases ho'
+ omega
+
+/-- A concrete word. A capability word with every FRO and NFR bit set
+ (`0xFF03FF000000`) puts the first record at byte 16368 and describes 256 of
+ them, so the last high half `take_fault` would read is at byte
+ 16368 + 255 * 16 + 8 = 20456, about four pages past the end of the mapped
+ window. `registers_fit` refuses this word. -/
+theorem fault_recording_offset_and_count_of_a_full_field_word :
+ fault_recording_offset 0xFF03FF000000#u64 = ok 16368#usize ∧
+ fault_recording_count 0xFF03FF000000#u64 = ok 256#u16 := by
+ obtain ⟨o, ho, hov⟩ :=
+ fault_recording_offset_is_sixteen_times_bits_twenty_four_to_thirty_three 0xFF03FF000000#u64
+ obtain ⟨n, hn, hnv⟩ :=
+ fault_recording_count_is_bits_forty_to_forty_seven_plus_one 0xFF03FF000000#u64
+ refine ⟨?_, ?_⟩
+ · rw [ho]; congr 1; apply UScalar.eq_of_val_eq; rw [hov]; decide
+ · rw [hn]; congr 1; apply UScalar.eq_of_val_eq; rw [hnv]; decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CapFault.the_fault_recording_offset_wrapper_is_its_method
#print axioms NonosExtraction.CapFault.the_fault_recording_count_wrapper_is_its_method
+#print axioms NonosExtraction.CapFault.fault_recording_offset_is_sixteen_times_bits_twenty_four_to_thirty_three
+#print axioms NonosExtraction.CapFault.fault_recording_count_is_bits_forty_to_forty_seven_plus_one
+#print axioms NonosExtraction.CapFault.fault_recording_count_is_between_one_and_two_hundred_fifty_six
+#print axioms NonosExtraction.CapFault.fault_recording_offset_leaves_the_register_window_once_fro_reaches_256
+#print axioms NonosExtraction.CapFault.fault_recording_offset_and_count_of_a_full_field_word
end NonosExtraction.CapFault
diff --git a/verification/extraction/lean/NonosExtraction/CapLimitsRefinement.lean b/verification/extraction/lean/NonosExtraction/CapLimitsRefinement.lean
index ad475103fc..f09e7c5fd8 100644
--- a/verification/extraction/lean/NonosExtraction/CapLimitsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CapLimitsRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.CapLimits
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_cap_limits
@@ -38,9 +39,103 @@ theorem the_domain_count_wrapper_is_its_method (a : Std.U64) :
theorem the_max_address_width_wrapper_is_its_method (a : Std.U64) :
max_address_width a = limits.max_address_width a := rfl
+/-! ### What the limit readers decode from the Capability register
+
+`probe_at` stores `max_address_width` and `domain_count` in `UnitInfo`. The
+theorems below say that `domain_count` is exactly `2^(4 + 2 * ND)` for the ND
+field in bits 0 to 2 and 0 for the reserved ND of 7, that the largest count it
+can report is 65536, so every domain id below it fits the kernel's sixteen-bit
+`DomainId`, and that it is zero only for the reserved encoding. They say that
+`max_address_width` is exactly the MGAW field in bits 16 to 21 plus one, since
+the field stores the width less one, and that it lies between 1 and 64. None of
+the readers ever fails: the shift, the multiply and the increment stay in range
+for every capability word.
+
+How the kernel uses the count and the width after probing (the domain table's
+size, the paging depth it picks) is decided outside this crate, so those
+contracts are not established here.
+-/
+
+/-- A right shift of a 64-bit word by a constant below 64 succeeds and divides
+ by that power of two. -/
+private theorem shr_u64 (x : Std.U64) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 64) :
+ ∃ z : Std.U64, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+/-- The domain count as the kernel promises it: `2^(4 + 2 * ND)` for the ND
+ field in bits 0 to 2, and 0 for the reserved ND of 7. It never fails. -/
+theorem domain_count_is_two_to_the_four_plus_twice_nd_and_zero_when_reserved
+ (cap : Std.U64) :
+ ∃ d : Std.U32, domain_count cap = ok d ∧
+ d.val = if cap.val % 8 = 7 then 0 else 2 ^ (4 + 2 * (cap.val % 8)) := by
+ unfold domain_count limits.domain_count
+ simp only [bind_tc_ok, lift]
+ have hf : (UScalar.cast .U32 (cap &&& 7#u64)).val = cap.val % 8 := by
+ rw [UScalar.cast_val_eq, Bits.land_low_mask cap 7#u64 3 rfl]
+ simp [UScalarTy.numBits]
+ omega
+ generalize UScalar.cast .U32 (cap &&& 7#u64) = nd at hf
+ have hlt : cap.val % 8 < 8 := Nat.mod_lt _ (by decide)
+ rw [← hf]
+ obtain ⟨bv⟩ := nd
+ have e : bv = BitVec.ofNat 32 bv.toNat := by simp
+ have hv : bv.toNat < 8 := by
+ have : (⟨bv⟩ : Std.U32).val = bv.toNat := rfl
+ omega
+ rcases (by omega : bv.toNat = 0 ∨ bv.toNat = 1 ∨ bv.toNat = 2 ∨ bv.toNat = 3 ∨
+ bv.toNat = 4 ∨ bv.toNat = 5 ∨ bv.toNat = 6 ∨ bv.toNat = 7)
+ with h | h | h | h | h | h | h | h <;> rw [h] at e <;> subst e <;> exact ⟨_, rfl, rfl⟩
+
+/-- Every count the unit can advertise is at most 65536, reached at ND = 6, so
+ each domain id below it fits the sixteen-bit domain id the context entries
+ carry, and a count is zero only for the reserved ND. -/
+theorem domain_count_is_at_most_sixty_five_thousand_five_hundred_thirty_six
+ (cap : Std.U64) :
+ ∃ d : Std.U32, domain_count cap = ok d ∧ d.val ≤ 65536 ∧
+ (d.val = 0 ↔ cap.val % 8 = 7) := by
+ obtain ⟨d, hd, hv⟩ :=
+ domain_count_is_two_to_the_four_plus_twice_nd_and_zero_when_reserved cap
+ refine ⟨d, hd, ?_⟩
+ rw [hv]
+ have hlt : cap.val % 8 < 8 := Nat.mod_lt _ (by decide)
+ rcases (by omega : cap.val % 8 = 0 ∨ cap.val % 8 = 1 ∨ cap.val % 8 = 2 ∨
+ cap.val % 8 = 3 ∨ cap.val % 8 = 4 ∨ cap.val % 8 = 5 ∨ cap.val % 8 = 6 ∨
+ cap.val % 8 = 7) with h | h | h | h | h | h | h | h <;> rw [h] <;> decide
+
+/-- The address width is the MGAW field, bits 16 to 21, plus one: the field
+ stores the width less one. The increment never overflows a `u8`. -/
+theorem max_address_width_is_bits_sixteen_to_twenty_one_plus_one (cap : Std.U64) :
+ ∃ w : Std.U8, max_address_width cap = ok w ∧
+ w.val = cap.val / 2 ^ 16 % 64 + 1 := by
+ unfold max_address_width limits.max_address_width
+ obtain ⟨z, hz, hv⟩ := shr_u64 cap 16#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ have hf : (UScalar.cast .U8 (z &&& 63#u64)).val = cap.val / 2 ^ 16 % 64 := by
+ rw [UScalar.cast_val_eq, Bits.land_low_mask z 63#u64 6 rfl, hv]
+ simp only [show (16#i32 : Std.I32).toNat = 16 from rfl]
+ simp [UScalarTy.numBits]
+ omega
+ obtain ⟨w, hw, hwv⟩ := WP.spec_imp_exists
+ (U8.add_spec (x := UScalar.cast .U8 (z &&& 63#u64)) (y := 1#u8)
+ (by rw [hf]; scalar_tac))
+ exact ⟨w, hw, by rw [hwv, hf]; rfl⟩
+
+/-- The reported width is never zero and never above 64 bits, whatever the
+ capability word says. -/
+theorem max_address_width_is_between_one_and_sixty_four (cap : Std.U64) :
+ ∃ w : Std.U8, max_address_width cap = ok w ∧ 1 ≤ w.val ∧ w.val ≤ 64 := by
+ obtain ⟨w, hw, hv⟩ := max_address_width_is_bits_sixteen_to_twenty_one_plus_one cap
+ exact ⟨w, hw, by omega, by omega⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CapLimits.the_domain_count_wrapper_is_its_method
#print axioms NonosExtraction.CapLimits.the_max_address_width_wrapper_is_its_method
+#print axioms NonosExtraction.CapLimits.domain_count_is_two_to_the_four_plus_twice_nd_and_zero_when_reserved
+#print axioms NonosExtraction.CapLimits.domain_count_is_at_most_sixty_five_thousand_five_hundred_thirty_six
+#print axioms NonosExtraction.CapLimits.max_address_width_is_bits_sixteen_to_twenty_one_plus_one
+#print axioms NonosExtraction.CapLimits.max_address_width_is_between_one_and_sixty_four
end NonosExtraction.CapLimits
diff --git a/verification/extraction/lean/NonosExtraction/CapPagesRefinement.lean b/verification/extraction/lean/NonosExtraction/CapPagesRefinement.lean
index 23d6efa656..6cbfa1160e 100644
--- a/verification/extraction/lean/NonosExtraction/CapPagesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CapPagesRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.CapPages
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_cap_pages
@@ -35,8 +36,58 @@ namespace NonosExtraction.CapPages
theorem the_best_leaf_level_wrapper_is_its_method (a : Std.U64) :
best_leaf_level a = pages.best_leaf_level a := rfl
+/-! ### Which leaf size the capability word selects
+
+ `best_leaf_level` reads two bits of the VT-d capability register: SLLPS bit 1
+ (register bit 35, 1 GiB leaves) and SLLPS bit 0 (register bit 34, 2 MiB
+ leaves). The theorems below say exactly which bit decides each answer, that
+ the 1 GiB bit wins when both are set, that no other bit of the word matters,
+ and that the answer always lies in the range `map_identity` accepts for every
+ depth `preferred_levels` can pick (three, four or five levels), so the
+ identity domain's `leaf_level == 0 || leaf_level > levels` refusal never fires
+ on this value. They cannot establish that the hardware reports SLLPS
+ truthfully, nor anything about `map_identity` itself, which is not extracted
+ here.
+-/
+
+/-- The level is 3 exactly when bit 35 is set, 2 when only bit 34 is set, and 1
+ otherwise. -/
+theorem best_leaf_level_reads_bits_thirty_five_then_thirty_four (cap : Std.U64) :
+ best_leaf_level cap =
+ ok (if cap.val.testBit 35 then 3#u8 else if cap.val.testBit 34 then 2#u8 else 1#u8) := by
+ unfold best_leaf_level pages.best_leaf_level
+ have h1 : pages.SLLPS_1GB = ok 34359738368#u64 := by unfold pages.SLLPS_1GB; rfl
+ have h2 : pages.SLLPS_2MB = ok 17179869184#u64 := by unfold pages.SLLPS_2MB; rfl
+ simp only [h1, h2, lift, bind_tc_ok]
+ rw [Bits.reads_bit cap 34359738368#u64 0#u64 35 rfl rfl,
+ Bits.reads_bit cap 17179869184#u64 0#u64 34 rfl rfl]
+ cases cap.val.testBit 35 <;> cases cap.val.testBit 34 <;> rfl
+
+/-- The level is never 0 and never above 3, the shallowest depth a unit can be
+ driven at, so it always passes the leaf level check in `map_identity`. -/
+theorem best_leaf_level_is_between_one_and_three (cap : Std.U64) :
+ ∃ l : Std.U8, best_leaf_level cap = ok l ∧ 1 ≤ l.val ∧ l.val ≤ 3 := by
+ rw [best_leaf_level_reads_bits_thirty_five_then_thirty_four]
+ refine ⟨_, rfl, ?_⟩
+ cases cap.val.testBit 35 <;> cases cap.val.testBit 34 <;> decide
+
+/-- A unit advertising both leaf sizes gets 1 GiB leaves, and one advertising
+ neither gets 4 KiB leaves; bits outside 34 and 35 are ignored. -/
+theorem best_leaf_level_on_the_two_capability_bits :
+ best_leaf_level 0#u64 = ok 1#u8 ∧
+ best_leaf_level 17179869184#u64 = ok 2#u8 ∧
+ best_leaf_level 34359738368#u64 = ok 3#u8 ∧
+ best_leaf_level 51539607552#u64 = ok 3#u8 ∧
+ best_leaf_level 17179869183#u64 = ok 1#u8 ∧
+ best_leaf_level 68719476736#u64 = ok 1#u8 := by
+ simp only [best_leaf_level_reads_bits_thirty_five_then_thirty_four]
+ exact ⟨rfl, rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CapPages.the_best_leaf_level_wrapper_is_its_method
+#print axioms NonosExtraction.CapPages.best_leaf_level_reads_bits_thirty_five_then_thirty_four
+#print axioms NonosExtraction.CapPages.best_leaf_level_is_between_one_and_three
+#print axioms NonosExtraction.CapPages.best_leaf_level_on_the_two_capability_bits
end NonosExtraction.CapPages
diff --git a/verification/extraction/lean/NonosExtraction/CensusBufRefinement.lean b/verification/extraction/lean/NonosExtraction/CensusBufRefinement.lean
index 5fa5f7555d..18d7ef5982 100644
--- a/verification/extraction/lean/NonosExtraction/CensusBufRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CensusBufRefinement.lean
@@ -35,8 +35,28 @@ namespace NonosExtraction.CensusBuf
theorem the_linebuf_new_wrapper_is_its_method :
linebuf_new = buf.LineBuf.new := rfl
+/-! ### A new line buffer is empty and in bounds
+
+ `put`, `hex` and `dec` write `data[len]` only while `len < 100`, and
+ `as_str` slices `data[..len]`. Both rely on `len` never exceeding the
+ storage, and a fresh buffer is expected to start with nothing in it. The
+ theorem below says `new` establishes that: the length is zero, strictly
+ below the storage length of 100, and every byte is zero. The writers and
+ `as_str` are not extracted, so the invariant is shown to hold at birth, not
+ to be preserved.
+-/
+
+/-- A new buffer has length zero, room for all 100 bytes, and zeroed storage,
+ so its first `put` writes `data[0]` and its `as_str` is the empty string. -/
+theorem linebuf_new_is_empty_with_zeroed_storage :
+ ∃ b, linebuf_new = ok b ∧ b.len.val = 0 ∧
+ b.data.val = List.replicate 100 0#u8 ∧ b.len.val < b.data.val.length := by
+ refine ⟨_, rfl, rfl, rfl, ?_⟩
+ decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CensusBuf.the_linebuf_new_wrapper_is_its_method
+#print axioms NonosExtraction.CensusBuf.linebuf_new_is_empty_with_zeroed_storage
end NonosExtraction.CensusBuf
diff --git a/verification/extraction/lean/NonosExtraction/ChainErrorRefinement.lean b/verification/extraction/lean/NonosExtraction/ChainErrorRefinement.lean
index efaf3c37da..34fefa0736 100644
--- a/verification/extraction/lean/NonosExtraction/ChainErrorRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ChainErrorRefinement.lean
@@ -35,8 +35,41 @@ namespace NonosExtraction.ChainError
theorem the_chainerror_is_recoverable_wrapper_is_its_method (a : error.ChainError) :
chainerror_is_recoverable a = error.ChainError.is_recoverable a := rfl
+/-! ### Only expiry and absence are worth retrying
+
+A capability-chain check that fails with a recoverable error invites the caller
+to refresh and try again; one that fails with an unrecoverable error means the
+chain itself is bad. These theorems establish that `is_recoverable` answers for
+every error, answers `true` exactly for `ExpiredToken` and
+`CapabilityNotFound`, and so never reports a forged signature (`InvalidToken`),
+a broken delegation link, an over-deep chain or an empty chain as retryable.
+
+They cannot establish that the verifier raises the right variant for a given
+chain: the verifier is not in this crate.
+-/
+
+/-- `chainerror_is_recoverable` always answers, and answers `true` exactly for
+ an expired token or a missing capability. -/
+theorem chainerror_is_recoverable_exactly_for_expiry_and_absence (e : error.ChainError) :
+ ∃ b, chainerror_is_recoverable e = ok b ∧
+ (b = true ↔ ((∃ i, e = .ExpiredToken i) ∨ e = .CapabilityNotFound)) := by
+ unfold chainerror_is_recoverable error.ChainError.is_recoverable
+ cases e <;> simp
+
+/-- No integrity failure is ever reported as retryable by
+ `chainerror_is_recoverable`. -/
+theorem chainerror_is_recoverable_refuses_every_integrity_failure (i d m : Std.Usize) :
+ chainerror_is_recoverable (.InvalidToken i) = ok false ∧
+ chainerror_is_recoverable (.BrokenLink i) = ok false ∧
+ chainerror_is_recoverable (.TooDeep d m) = ok false ∧
+ chainerror_is_recoverable .EmptyChain = ok false :=
+ ⟨rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ChainError.the_chainerror_is_recoverable_wrapper_is_its_method
+#print axioms NonosExtraction.ChainError.chainerror_is_recoverable_exactly_for_expiry_and_absence
+#print axioms NonosExtraction.ChainError.chainerror_is_recoverable_refuses_every_integrity_failure
+
end NonosExtraction.ChainError
diff --git a/verification/extraction/lean/NonosExtraction/CivilDaysRefinement.lean b/verification/extraction/lean/NonosExtraction/CivilDaysRefinement.lean
index 214d3de799..b05a765c3b 100644
--- a/verification/extraction/lean/NonosExtraction/CivilDaysRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CivilDaysRefinement.lean
@@ -41,10 +41,109 @@ theorem the_days_in_month_wrapper_is_its_method (a : Std.U16) (b : Std.U8) :
theorem the_days_in_year_wrapper_is_its_method (a : Std.U16) :
days_in_year a = days.days_in_year a := rfl
+/-! ### The Gregorian calendar, and months that add up to the year
+
+ `is_leap_year` is the full Gregorian rule on every `u16` year: divisible by
+ four and not by one hundred, or divisible by four hundred. The century clause
+ is the one the kernel comment warns a naive rule drops, and 1900, 2000 and
+ 2100 are pinned below so that dropping either exception is visible.
+ `days_in_year` is 366 exactly in those years and 365 otherwise, and repeats
+ every four hundred years. `days_in_month` is zero exactly outside months one
+ to twelve, and the twelve months it returns add up to `days_in_year`, which is
+ what lets the month loop in civil::time::from_unix, run on a remainder smaller
+ than `days_in_year`, stop at or before December. These theorems cannot
+ establish anything about the loops themselves: civil::time and the RTC
+ calendar in arch/x86_64/time/rtc are not extracted here.
+-/
+
+theorem is_leap_year_is_the_gregorian_rule (y : Std.U16) :
+ is_leap_year y =
+ ok (decide ((y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0)) := by
+ unfold is_leap_year days.is_leap_year
+ simp only [core.num.U16.is_multiple_of, UScalar.is_multiple_of, bind_tc_ok]
+ have h4 : (4#u16 : Std.U16).val = 4 := rfl
+ have h100 : (100#u16 : Std.U16).val = 100 := rfl
+ have h400 : (400#u16 : Std.U16).val = 400 := rfl
+ rw [h4, h100, h400]
+ by_cases a : y.val % 4 = 0 <;> by_cases b : y.val % 100 = 0 <;>
+ by_cases c : y.val % 400 = 0 <;> simp [a, b, c]
+
+/-- The century years: 1900 and 2100 are common, 2000 is leap, and so is 2024. -/
+theorem days_in_year_at_the_century_boundaries :
+ days_in_year 1900#u16 = ok 365#u16 ∧ days_in_year 2000#u16 = ok 366#u16 ∧
+ days_in_year 2100#u16 = ok 365#u16 ∧ days_in_year 2024#u16 = ok 366#u16 ∧
+ days_in_year 2023#u16 = ok 365#u16 := by
+ unfold days_in_year days.days_in_year
+ simp only [← the_is_leap_year_wrapper_is_its_method, is_leap_year_is_the_gregorian_rule,
+ bind_tc_ok]
+ refine ⟨?_, ?_, ?_, ?_, ?_⟩ <;> rfl
+
+theorem days_in_year_is_366_exactly_in_leap_years (y : Std.U16) :
+ days_in_year y = ok (if (y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0
+ then 366#u16 else 365#u16) := by
+ unfold days_in_year days.days_in_year
+ simp only [← the_is_leap_year_wrapper_is_its_method, is_leap_year_is_the_gregorian_rule,
+ bind_tc_ok]
+ by_cases h : (y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0 <;> simp [h]
+
+/-- The calendar repeats every four hundred years (as far as `u16` reaches). -/
+theorem days_in_year_repeats_every_four_hundred_years (y z : Std.U16)
+ (h : z.val = y.val + 400) : days_in_year z = days_in_year y := by
+ rw [days_in_year_is_366_exactly_in_leap_years, days_in_year_is_366_exactly_in_leap_years]
+ have e : ((z.val % 4 = 0 ∧ z.val % 100 ≠ 0) ∨ z.val % 400 = 0) ↔
+ ((y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0) := by
+ rw [h]; omega
+ simp only [e]
+
+/-- A month number outside 1 to 12 gets zero days, and only such a number does:
+ a fallback of 31 would make month 13 look valid. -/
+theorem days_in_month_is_zero_exactly_outside_one_to_twelve (y : Std.U16) (m : Std.U8) :
+ days_in_month y m = ok 0#u8 ↔ ¬ (1 ≤ m.val ∧ m.val ≤ 12) := by
+ unfold days_in_month days.days_in_month
+ simp only [← the_is_leap_year_wrapper_is_its_method, is_leap_year_is_the_gregorian_rule,
+ bind_tc_ok]
+ split
+ case h_13 h1 h3 h5 h7 h8 h10 h12 h4 h6 h9 h11 h2 =>
+ have ne : ∀ (c : Std.U8) (k : Nat), c.val = k → (m = c → False) → m.val ≠ k :=
+ fun c k hc hne he => hne (UScalar.eq_of_val_eq (he.trans hc.symm))
+ have := ne _ 1 rfl h1; have := ne _ 2 rfl h2; have := ne _ 3 rfl h3
+ have := ne _ 4 rfl h4; have := ne _ 5 rfl h5; have := ne _ 6 rfl h6
+ have := ne _ 7 rfl h7; have := ne _ 8 rfl h8; have := ne _ 9 rfl h9
+ have := ne _ 10 rfl h10; have := ne _ 11 rfl h11; have := ne _ 12 rfl h12
+ simp only [true_iff]
+ omega
+ case h_12 =>
+ split <;> simp only [ok.injEq] <;> decide
+ all_goals simp only [ok.injEq]; decide
+
+/-- The twelve months add up to the year: 366 days in a Gregorian leap year and
+ 365 otherwise, so February carries the whole leap rule. -/
+theorem days_in_month_twelve_months_add_up_to_days_in_year (y : Std.U16) :
+ ∃ ds n, [1#u8, 2#u8, 3#u8, 4#u8, 5#u8, 6#u8, 7#u8, 8#u8, 9#u8, 10#u8, 11#u8, 12#u8].mapM
+ (days_in_month y) = ok ds ∧
+ days_in_year y = ok n ∧ (ds.map (·.val)).sum = n.val := by
+ have hl := is_leap_year_is_the_gregorian_rule y
+ rw [days_in_year_is_366_exactly_in_leap_years]
+ unfold days_in_month days.days_in_month
+ simp only [← the_is_leap_year_wrapper_is_its_method]
+ by_cases h : (y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0
+ · rw [decide_eq_true h] at hl
+ simp only [hl, h, if_true]
+ exact ⟨_, _, rfl, rfl, rfl⟩
+ · rw [decide_eq_false h] at hl
+ simp only [hl, h, if_false]
+ exact ⟨_, _, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CivilDays.the_is_leap_year_wrapper_is_its_method
#print axioms NonosExtraction.CivilDays.the_days_in_month_wrapper_is_its_method
#print axioms NonosExtraction.CivilDays.the_days_in_year_wrapper_is_its_method
+#print axioms NonosExtraction.CivilDays.is_leap_year_is_the_gregorian_rule
+#print axioms NonosExtraction.CivilDays.days_in_year_at_the_century_boundaries
+#print axioms NonosExtraction.CivilDays.days_in_year_is_366_exactly_in_leap_years
+#print axioms NonosExtraction.CivilDays.days_in_year_repeats_every_four_hundred_years
+#print axioms NonosExtraction.CivilDays.days_in_month_is_zero_exactly_outside_one_to_twelve
+#print axioms NonosExtraction.CivilDays.days_in_month_twelve_months_add_up_to_days_in_year
end NonosExtraction.CivilDays
diff --git a/verification/extraction/lean/NonosExtraction/CoherencyModeRefinement.lean b/verification/extraction/lean/NonosExtraction/CoherencyModeRefinement.lean
index f47e1d36db..22afb47e78 100644
--- a/verification/extraction/lean/NonosExtraction/CoherencyModeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CoherencyModeRefinement.lean
@@ -41,10 +41,62 @@ theorem the_coherency_is_coherent_wrapper_is_its_method (a : mode.Coherency) :
theorem the_coherency_requires_cache_maintenance_wrapper_is_its_method (a : mode.Coherency) :
coherency_requires_cache_maintenance a = mode.Coherency.requires_cache_maintenance a := rfl
+/-! ### Coherency is one bit, and maintenance is its negation
+
+`from_bool` and `is_coherent` are inverse to each other: the flag a DMA region
+records is the flag read back, and a mode is coherent exactly when it is
+`Coherent`. `requires_cache_maintenance` is the negation of `is_coherent` on
+both modes, so exactly one of the two answers holds and there is no mode that
+is neither coherent nor maintained.
+
+The contract the aarch64 backend relies on follows. `sync_for_device` and
+`sync_for_cpu` in `coherency/backend_aarch64/sync.rs` clean or invalidate the
+cache only when `requires_cache_maintenance` holds, and the allocators in
+`dma/allocator/buffer.rs` and `buffer_iommu.rs` build the mode with
+`from_bool(region.coherent)`. A region not marked coherent therefore always gets
+cache maintenance, and a coherent one never does. The backends, the allocators
+and the barriers they issue are not extracted, so these theorems cannot show
+that the maintenance itself is correct, only which regions ask for it.
+-/
+
+/-- Reading back the mode built from a flag returns the flag. -/
+theorem coherency_is_coherent_reads_back_coherency_from_bool (b : Bool) :
+ (coherency_from_bool b >>= coherency_is_coherent) = ok b := by
+ cases b <;> rfl
+
+/-- A mode is coherent exactly when it is `Coherent`. -/
+theorem coherency_is_coherent_holds_only_for_coherent (c : mode.Coherency) :
+ coherency_is_coherent c = ok true ↔ c = .Coherent := by
+ cases c <;> simp [coherency_is_coherent, mode.Coherency.is_coherent]
+
+/-- Building from the flag of a mode rebuilds that mode, so `from_bool` reaches
+both modes and loses nothing. -/
+theorem coherency_from_bool_rebuilds_the_mode_it_is_read_from (c : mode.Coherency) :
+ (coherency_is_coherent c >>= coherency_from_bool) = ok c := by
+ cases c <;> rfl
+
+/-- Cache maintenance is required exactly when the mode is not coherent. -/
+theorem coherency_requires_cache_maintenance_is_not_coherency_is_coherent
+ (c : mode.Coherency) :
+ coherency_requires_cache_maintenance c = (coherency_is_coherent c >>= fun b => ok (!b)) := by
+ cases c <;> rfl
+
+/-- The caller contract: a DMA region whose `coherent` flag is `b` requires cache
+maintenance exactly when `b` is false. -/
+theorem coherency_requires_cache_maintenance_of_coherency_from_bool_is_its_negation
+ (b : Bool) :
+ (coherency_from_bool b >>= coherency_requires_cache_maintenance) = ok (!b) := by
+ cases b <;> rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CoherencyMode.the_coherency_from_bool_wrapper_is_its_method
#print axioms NonosExtraction.CoherencyMode.the_coherency_is_coherent_wrapper_is_its_method
#print axioms NonosExtraction.CoherencyMode.the_coherency_requires_cache_maintenance_wrapper_is_its_method
+#print axioms NonosExtraction.CoherencyMode.coherency_is_coherent_reads_back_coherency_from_bool
+#print axioms NonosExtraction.CoherencyMode.coherency_is_coherent_holds_only_for_coherent
+#print axioms NonosExtraction.CoherencyMode.coherency_from_bool_rebuilds_the_mode_it_is_read_from
+#print axioms NonosExtraction.CoherencyMode.coherency_requires_cache_maintenance_is_not_coherency_is_coherent
+#print axioms NonosExtraction.CoherencyMode.coherency_requires_cache_maintenance_of_coherency_from_bool_is_its_negation
end NonosExtraction.CoherencyMode
diff --git a/verification/extraction/lean/NonosExtraction/ConstantsAddressPacking.lean b/verification/extraction/lean/NonosExtraction/ConstantsAddressPacking.lean
index 08c37be359..e2ec82d6f6 100644
--- a/verification/extraction/lean/NonosExtraction/ConstantsAddressPacking.lean
+++ b/verification/extraction/lean/NonosExtraction/ConstantsAddressPacking.lean
@@ -25,15 +25,17 @@ def address_packing.pci_config_address
let i1 ← lift (UScalar.cast .U32 bus)
let i2 ← i1 <<< 16#i32
let i3 ← lift (i ||| i2)
- let i4 ← lift (UScalar.cast .U32 device)
- let i5 ← i4 <<< 11#i32
- let i6 ← lift (i3 ||| i5)
- let i7 ← lift (UScalar.cast .U32 function)
- let i8 ← i7 <<< 8#i32
- let i9 ← lift (i6 ||| i8)
- let i10 ← lift (UScalar.cast .U32 offset)
- let i11 ← lift (i10 &&& 252#u32)
- ok (i9 ||| i11)
+ let i4 ← lift (device &&& 31#u8)
+ let i5 ← lift (UScalar.cast .U32 i4)
+ let i6 ← i5 <<< 11#i32
+ let i7 ← lift (i3 ||| i6)
+ let i8 ← lift (function &&& 7#u8)
+ let i9 ← lift (UScalar.cast .U32 i8)
+ let i10 ← i9 <<< 8#i32
+ let i11 ← lift (i7 ||| i10)
+ let i12 ← lift (UScalar.cast .U32 offset)
+ let i13 ← lift (i12 &&& 252#u32)
+ ok (i11 ||| i13)
/-- [nonos_x_constants_address_packing::pci_config_address]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/ConstantsAddressPackingRefinement.lean b/verification/extraction/lean/NonosExtraction/ConstantsAddressPackingRefinement.lean
index e0c9cc2212..b802595798 100644
--- a/verification/extraction/lean/NonosExtraction/ConstantsAddressPackingRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ConstantsAddressPackingRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.ConstantsAddressPacking
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_constants_address_packing
@@ -35,8 +36,169 @@ namespace NonosExtraction.ConstantsAddressPacking
theorem the_pci_config_address_wrapper_is_its_method (a : Std.U8) (b : Std.U8) (c : Std.U8) (d : Std.U8) :
pci_config_address a b c d = address_packing.pci_config_address a b c d := rfl
+/-! ### The configuration mechanism #1 address word
+
+ `PciAddress::config_address` passes its bus, device and function to
+ `pci_config_address`, and the result is written to port 0xCF8. The theorems
+ below say what word comes out: the enable bit 31 is always set, the offset is
+ rounded down to a dword, and the word is `0x80000000 + (bus * 256 +
+ device % 32 * 8 + function % 8) * 256 + offset / 4 * 4`, from which each
+ field reads back. The device is masked to five bits and the function to
+ three, so the bus field holds the bus for every input. Before the masks, a device of 32 or more spilled into the bus
+ field and a function of 8 or more into the device field, so device 32 on bus
+ 0 read or wrote the configuration space of device 0 on bus 1. They cannot
+ establish anything about the port I/O that follows, which is not extracted
+ here.
+-/
+
+private theorem shl_u32 (x : Std.U32) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 32) :
+ ∃ z : Std.U32, x <<< k = ok z ∧ z.val = x.val * 2 ^ k.toNat % 2 ^ 32 := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftLeft_IScalar_spec x k (UScalar.size .U32) h0
+ (by simpa using h1) rfl)
+ exact ⟨z, hz, by rw [hv, Nat.shiftLeft_eq]; simp [U32.size, U32.numBits]⟩
+
+private theorem low_byte_dword_aligned (n : Nat) (h : n < 256) : n &&& 252 = n / 4 * 4 := by
+ apply Nat.eq_of_testBit_eq
+ intro i
+ rw [Nat.testBit_and, show n / 4 * 4 = n / 2 ^ 2 * 2 ^ 2 from rfl, Nat.testBit_mul_two_pow,
+ Nat.testBit_div_two_pow]
+ by_cases hi : i < 8
+ · have : i = 0 ∨ i = 1 ∨ i = 2 ∨ i = 3 ∨ i = 4 ∨ i = 5 ∨ i = 6 ∨ i = 7 := by omega
+ rcases this with h | h | h | h | h | h | h | h <;> subst h <;>
+ first | (cases n.testBit _ <;> decide) | (rw [show Nat.testBit 252 _ = false from rfl]; simp)
+ · have hp : 256 ≤ 2 ^ i := Nat.pow_le_pow_right (n := 2) (by decide) (by omega : 8 ≤ i)
+ have h1 : n.testBit i = false := Nat.testBit_eq_false_of_lt (by omega)
+ have h2 : Nat.testBit 252 i = false := Nat.testBit_eq_false_of_lt (by omega)
+ have e : i - 2 + 2 = i := by omega
+ rw [e, h1, h2]
+ simp
+
+/-- Without any assumption on the fields, the word is the enable bit ORed with
+ the shifted bus, the device reduced to five bits, the function reduced to
+ three, and the offset with its low two bits cleared. -/
+theorem pci_config_address_ors_the_shifted_fields (b d f o : Std.U8) :
+ ∃ v : Std.U32, pci_config_address b d f o = ok v ∧
+ v.val = 2 ^ 31 ||| b.val * 2 ^ 16 ||| d.val % 32 * 2 ^ 11 ||| f.val % 8 * 2 ^ 8 |||
+ o.val / 4 * 4 := by
+ unfold pci_config_address address_packing.pci_config_address
+ have hb := b.hBounds
+ have hd := d.hBounds
+ have hf := f.hBounds
+ have ho := o.hBounds
+ simp [UScalarTy.numBits] at hb hd hf ho
+ have h31 : (1#u32 <<< 31#i32 : Result Std.U32) = ok 2147483648#u32 := by rfl
+ obtain ⟨z1, hz1, hv1⟩ := shl_u32 (UScalar.cast .U32 b) 16#i32 (by decide) (by decide)
+ obtain ⟨z2, hz2, hv2⟩ :=
+ shl_u32 (UScalar.cast .U32 (d &&& 31#u8)) 11#i32 (by decide) (by decide)
+ obtain ⟨z3, hz3, hv3⟩ :=
+ shl_u32 (UScalar.cast .U32 (f &&& 7#u8)) 8#i32 (by decide) (by decide)
+ have hdm : (d &&& 31#u8).val = d.val % 32 := Bits.land_low_mask d 31#u8 5 rfl
+ have hfm : (f &&& 7#u8).val = f.val % 8 := Bits.land_low_mask f 7#u8 3 rfl
+ simp only [h31, lift, bind_tc_ok, hz1, hz2, hz3]
+ refine ⟨_, rfl, ?_⟩
+ simp only [show (16#i32 : Std.I32).toNat = 16 from rfl, show (11#i32 : Std.I32).toNat = 11 from rfl,
+ show (8#i32 : Std.I32).toNat = 8 from rfl, UScalar.cast_val_eq, UScalarTy.numBits] at hv1 hv2 hv3
+ rw [hdm] at hv2
+ rw [hfm] at hv3
+ rw [Nat.mod_eq_of_lt (by omega : b.val < 2 ^ 32)] at hv1
+ rw [Nat.mod_eq_of_lt (by omega : d.val % 32 < 2 ^ 32)] at hv2
+ rw [Nat.mod_eq_of_lt (by omega : f.val % 8 < 2 ^ 32)] at hv3
+ rw [Nat.mod_eq_of_lt (by omega)] at hv1 hv2 hv3
+ simp only [UScalar.val_or, UScalar.val_and, hv1, hv2, hv3, UScalar.cast_val_eq, UScalarTy.numBits]
+ rw [Nat.mod_eq_of_lt (by omega : o.val < 2 ^ 32), show (252#u32 : Std.U32).val = 252 from rfl,
+ low_byte_dword_aligned o.val ho]
+ rfl
+
+/-- For every input the word is the enable bit, the routing id
+ `bus * 256 + device % 32 * 8 + function % 8` in bits 8 to 23, and the
+ dword-aligned offset. A device shift of 12, a bus shift of 15 or an offset
+ mask of 0xFF would each break this. -/
+theorem pci_config_address_is_enable_routing_id_and_dword_offset (b d f o : Std.U8) :
+ ∃ v : Std.U32, pci_config_address b d f o = ok v ∧
+ v.val = 2 ^ 31 + (b.val * 256 + d.val % 32 * 8 + f.val % 8) * 256 + o.val / 4 * 4 := by
+ obtain ⟨v, hv, hval⟩ := pci_config_address_ors_the_shifted_fields b d f o
+ refine ⟨v, hv, ?_⟩
+ have hb := b.hBounds
+ have ho := o.hBounds
+ simp [UScalarTy.numBits] at hb ho
+ rw [hval, Nat.lor_assoc, Nat.lor_assoc, Nat.lor_assoc]
+ have e1 : f.val % 8 * 2 ^ 8 ||| o.val / 4 * 4 = f.val % 8 * 2 ^ 8 + o.val / 4 * 4 := by
+ rw [← Nat.shiftLeft_eq, Nat.shiftLeft_add_eq_or_of_lt (by omega)]
+ have e2 : d.val % 32 * 2 ^ 11 ||| (f.val % 8 * 2 ^ 8 + o.val / 4 * 4) =
+ d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4) := by
+ rw [← Nat.shiftLeft_eq, Nat.shiftLeft_add_eq_or_of_lt (by omega)]
+ have e3 : b.val * 2 ^ 16 ||| (d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4)) =
+ b.val * 2 ^ 16 + (d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4)) := by
+ rw [← Nat.shiftLeft_eq, Nat.shiftLeft_add_eq_or_of_lt (by omega)]
+ have e4 : 2 ^ 31 ||| (b.val * 2 ^ 16 + (d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4))) =
+ 2 ^ 31 + (b.val * 2 ^ 16 + (d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4))) := by
+ have := Nat.shiftLeft_add_eq_or_of_lt (a := 1) (i := 31)
+ (b := b.val * 2 ^ 16 + (d.val % 32 * 2 ^ 11 + (f.val % 8 * 2 ^ 8 + o.val / 4 * 4))) (by omega)
+ rw [Nat.shiftLeft_eq, Nat.one_mul] at this
+ exact this.symm
+ rw [e1, e2, e3, e4]
+ omega
+
+/-- The fields read back out of the word for every input: bit 31 is set, the
+ bus is bits 16 to 23, the device reduced to five bits is bits 11 to 15, the
+ function reduced to three is bits 8 to 10, and bits 0 and 1 are clear, as
+ the 0xCF8 register requires. The bus field is the bus whatever device and
+ function are passed. -/
+theorem pci_config_address_decodes_to_its_fields (b d f o : Std.U8) :
+ ∃ v : Std.U32, pci_config_address b d f o = ok v ∧
+ v.val / 2 ^ 31 = 1 ∧ v.val / 2 ^ 16 % 256 = b.val ∧ v.val / 2 ^ 11 % 32 = d.val % 32 ∧
+ v.val / 2 ^ 8 % 8 = f.val % 8 ∧ v.val % 256 = o.val / 4 * 4 ∧ v.val % 4 = 0 := by
+ obtain ⟨v, hv, hval⟩ := pci_config_address_is_enable_routing_id_and_dword_offset b d f o
+ have hb := b.hBounds
+ have ho := o.hBounds
+ simp [UScalarTy.numBits] at hb ho
+ exact ⟨v, hv, by omega, by omega, by omega, by omega, by omega, by omega⟩
+
+/-- Offsets within one dword name the same register: the low two offset bits are
+ dropped rather than carried into the function field. -/
+theorem pci_config_address_ignores_the_low_offset_bits (b d f o : Std.U8) :
+ pci_config_address b d f o = pci_config_address b d f (o &&& 252#u8) := by
+ obtain ⟨v, hv, hval⟩ := pci_config_address_ors_the_shifted_fields b d f o
+ obtain ⟨w, hw, hwal⟩ := pci_config_address_ors_the_shifted_fields b d f (o &&& 252#u8)
+ rw [hv, hw]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hval, hwal]
+ have ho := o.hBounds
+ simp [UScalarTy.numBits] at ho
+ have : (o &&& 252#u8).val = o.val / 4 * 4 := by
+ rw [UScalar.val_and]; exact low_byte_dword_aligned o.val ho
+ rw [this]
+ congr 1
+ omega
+
+/-- Device 32 on bus 0 is reduced to device 0 on bus 0, not device 0 on bus 1,
+ and function 8 of device 0 to function 0 of device 0, not device 1. -/
+theorem pci_config_address_keeps_an_out_of_range_device_on_its_bus :
+ pci_config_address 0#u8 32#u8 0#u8 0#u8 = pci_config_address 0#u8 0#u8 0#u8 0#u8 ∧
+ pci_config_address 0#u8 0#u8 8#u8 0#u8 = pci_config_address 0#u8 0#u8 0#u8 0#u8 ∧
+ pci_config_address 0#u8 32#u8 0#u8 0#u8 ≠ pci_config_address 1#u8 0#u8 0#u8 0#u8 := by
+ obtain ⟨v1, h1, e1⟩ := pci_config_address_is_enable_routing_id_and_dword_offset 0#u8 32#u8 0#u8 0#u8
+ obtain ⟨v2, h2, e2⟩ := pci_config_address_is_enable_routing_id_and_dword_offset 0#u8 0#u8 0#u8 0#u8
+ obtain ⟨v3, h3, e3⟩ := pci_config_address_is_enable_routing_id_and_dword_offset 0#u8 0#u8 8#u8 0#u8
+ obtain ⟨v4, h4, e4⟩ := pci_config_address_is_enable_routing_id_and_dword_offset 1#u8 0#u8 0#u8 0#u8
+ rw [h1, h2, h3, h4]
+ refine ⟨?_, ?_, ?_⟩
+ · congr 1; apply UScalar.eq_of_val_eq; rw [e1, e2]; rfl
+ · congr 1; apply UScalar.eq_of_val_eq; rw [e3, e2]; rfl
+ · intro h
+ have := congrArg UScalar.val (ok.inj h)
+ rw [e1, e4] at this
+ exact absurd this (by decide)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ConstantsAddressPacking.the_pci_config_address_wrapper_is_its_method
+#print axioms NonosExtraction.ConstantsAddressPacking.pci_config_address_ors_the_shifted_fields
+#print axioms NonosExtraction.ConstantsAddressPacking.pci_config_address_is_enable_routing_id_and_dword_offset
+#print axioms NonosExtraction.ConstantsAddressPacking.pci_config_address_decodes_to_its_fields
+#print axioms NonosExtraction.ConstantsAddressPacking.pci_config_address_ignores_the_low_offset_bits
+#print axioms NonosExtraction.ConstantsAddressPacking.pci_config_address_keeps_an_out_of_range_device_on_its_bus
end NonosExtraction.ConstantsAddressPacking
diff --git a/verification/extraction/lean/NonosExtraction/ContextCpuContextRefinement.lean b/verification/extraction/lean/NonosExtraction/ContextCpuContextRefinement.lean
index c49181862e..49bc128956 100644
--- a/verification/extraction/lean/NonosExtraction/ContextCpuContextRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ContextCpuContextRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.ContextCpuContext
+import Nonos.Rflags
open Aeneas Aeneas.Std Result
open nonos_x_context_cpu_context
@@ -35,8 +36,43 @@ namespace NonosExtraction.ContextCpuContext
theorem the_cpucontext_new_wrapper_is_its_method :
cpucontext_new = cpu_context.CpuContext.new := rfl
+/-! ### A fresh context carries nothing and grants nothing
+
+`CpuContext::new` is the saved state a thread starts from before
+`prepare_kernel_entry` or `prepare_user_entry` fills in its entry point and
+stack. These theorems establish that every one of the eleven saved registers
+starts at zero, so nothing leaks from whatever occupied the memory before, and
+that the starting RFLAGS carries no bit the tier-one `Nonos.Rflags` model
+calls privileged (IOPL is 0; TF, NT, VM and AC are clear) and has IF off.
+
+They cannot establish what the entry preparation later writes, nor that the
+restore path sanitizes RFLAGS: those functions are not in this crate. The
+starting RFLAGS also lacks the reserved bit 1, which `sanitize` restores on the
+way out.
+-/
+
+/-- All eleven saved registers of a new context are zero. -/
+theorem cpucontext_new_zeroes_every_saved_register :
+ ∃ c, cpucontext_new = ok c ∧
+ [c.r15, c.r14, c.r13, c.r12, c.rbx, c.rbp, c.rip, c.rsp, c.rflags, c.cs, c.ss].all
+ (fun r => r.val == 0) = true := by
+ unfold cpucontext_new cpu_context.CpuContext.new
+ exact ⟨_, rfl, by decide⟩
+
+/-- The RFLAGS a new context starts from has no privileged bit and no IF, as
+ `Nonos.Rflags` defines them. -/
+theorem cpucontext_new_starts_with_no_privileged_flag_and_interrupts_off :
+ ∃ c, cpucontext_new = ok c ∧
+ c.rflags.val &&& Nonos.Rflags.privilegedMask = 0 ∧
+ Nonos.Rflags.bit c.rflags.val 9 = false := by
+ unfold cpucontext_new cpu_context.CpuContext.new
+ exact ⟨_, rfl, by decide, by decide⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ContextCpuContext.the_cpucontext_new_wrapper_is_its_method
+#print axioms NonosExtraction.ContextCpuContext.cpucontext_new_zeroes_every_saved_register
+#print axioms NonosExtraction.ContextCpuContext.cpucontext_new_starts_with_no_privileged_flag_and_interrupts_off
+
end NonosExtraction.ContextCpuContext
diff --git a/verification/extraction/lean/NonosExtraction/ContextRflagsRefinement.lean b/verification/extraction/lean/NonosExtraction/ContextRflagsRefinement.lean
index 3af6e75025..8c8154cee6 100644
--- a/verification/extraction/lean/NonosExtraction/ContextRflagsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ContextRflagsRefinement.lean
@@ -21,6 +21,8 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.ContextRflags
+import NonosExtraction.Bits
+import Nonos.Rflags
open Aeneas Aeneas.Std Result
open nonos_x_context_rflags
@@ -38,9 +40,116 @@ theorem the_sanitize_wrapper_is_its_method (a : Std.U64) :
theorem the_sanitize_user_wrapper_is_its_method (a : Std.U64) :
sanitize_user a = context_rflags.sanitize_user a := rfl
+/-! ### The sanitizer clears exactly the privileged positions
+
+`Nonos.Rflags` states, as a list of bit positions, which RFLAGS bits a
+restored context must not choose for itself, and proves its copy of the mask
+literal is exactly those positions. The theorems below close the loop on the
+extracted code: bit by bit, across all 64 bits, `sanitize` keeps a saved bit
+exactly when its position is not on that list, forces the reserved bit 1 on,
+and leaves IF as saved; `sanitize_user` is the same with IF forced on. So a
+typo in the mask literal of `rflags.rs`, a dropped `!`, or a sanitizer that
+touched IF would each break a theorem here, and the positions are checked
+against the independent list rather than against the literal itself.
+
+What this cannot establish is that every restore path calls these functions.
+`validate_resume.rs` and `sanitize_rflags.rs` do, but neither caller is
+extracted, and neither is the `iretq` or `sysretq` that consumes the result.
+-/
+
+private theorem the_extracted_constants_are_the_model_positions :
+ ∀ i < 64, Nat.testBit 2061568 i = Nonos.Rflags.privilegedBits.contains i ∧
+ Nat.testBit 2 i = (i == 1) ∧ Nat.testBit 512 i = (i == 9) := by
+ have h : (List.range 64).all (fun i =>
+ Nat.testBit 2061568 i == Nonos.Rflags.privilegedBits.contains i &&
+ Nat.testBit 2 i == (i == 1) && Nat.testBit 512 i == (i == 9)) = true := by
+ decide
+ intro i hi
+ simpa using List.all_eq_true.mp h i (List.mem_range.mpr hi)
+
+private theorem small_constant_has_no_high_bits (c i : Nat) (hc : c < 2 ^ 64) (hi : ¬ i < 64) :
+ Nat.testBit c i = false :=
+ Nat.testBit_eq_false_of_lt
+ (Nat.lt_of_lt_of_le hc (Nat.pow_le_pow_right (by decide) (by omega)))
+
+/-- Bit `i` of the sanitized flags is set exactly when `i` is the reserved bit
+ 1, or the saved flags had it set and `i` is not one of the privileged
+ positions TF, DF, IOPL, NT, RF, VM, AC, VIF, VIP that `Nonos.Rflags` lists.
+ Nothing above bit 63 is set, and the function never fails. -/
+theorem sanitize_keeps_exactly_the_bits_outside_the_privileged_positions (r : Std.U64) :
+ ∃ s, sanitize r = ok s ∧ ∀ i, s.val.testBit i =
+ (decide (i < 64) &&
+ ((i == 1) || (r.val.testBit i && !Nonos.Rflags.privilegedBits.contains i))) := by
+ unfold sanitize context_rflags.sanitize
+ simp only [lift, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ intro i
+ rw [UScalar.val_or, Nat.testBit_or, UScalar.val_and, Nat.testBit_and,
+ NonosExtraction.Bits.testBit_val_not]
+ unfold context_rflags.RFLAGS_PRIVILEGED_MASK context_rflags.RFLAGS_RESERVED_SET
+ have hm : (2061568#u64).val = 2061568 := rfl
+ have hr : (2#u64).val = 2 := rfl
+ have hw : UScalarTy.U64.numBits = 64 := rfl
+ rw [hm, hr, hw]
+ by_cases hi : i < 64
+ · obtain ⟨h1, h2, -⟩ := the_extracted_constants_are_the_model_positions i hi
+ rw [h1, h2]
+ cases r.val.testBit i <;> cases Nonos.Rflags.privilegedBits.contains i <;>
+ simp [hi, Bool.or_comm]
+ · have h1 : r.val.testBit i = false :=
+ NonosExtraction.Bits.testBit_val_high r i (by simp at hi ⊢; omega)
+ have h2 := small_constant_has_no_high_bits 2 i (by decide) hi
+ simp [h1, h2, hi]
+
+/-- `sanitize_user` is `sanitize` with IF (bit 9) also forced on: every other
+ bit is decided exactly as `sanitize` decides it. -/
+theorem sanitize_user_is_sanitize_with_interrupts_forced_on (r : Std.U64) :
+ ∃ s, sanitize_user r = ok s ∧ ∀ i, s.val.testBit i =
+ (decide (i < 64) && ((i == 1) || (i == 9) ||
+ (r.val.testBit i && !Nonos.Rflags.privilegedBits.contains i))) := by
+ obtain ⟨s, hs, hbits⟩ := sanitize_keeps_exactly_the_bits_outside_the_privileged_positions r
+ have hs' : context_rflags.sanitize r = ok s := hs
+ unfold sanitize_user context_rflags.sanitize_user
+ rw [hs']
+ simp only [bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ intro i
+ unfold context_rflags.RFLAGS_IF
+ have hf : (512#u64).val = 512 := rfl
+ rw [UScalar.val_or, Nat.testBit_or, hbits, hf]
+ by_cases hi : i < 64
+ · obtain ⟨-, -, h3⟩ := the_extracted_constants_are_the_model_positions i hi
+ rw [h3]
+ cases r.val.testBit i <;> cases Nonos.Rflags.privilegedBits.contains i <;>
+ cases (i == 1) <;> cases (i == 9) <;> simp [hi]
+ · have h3 := small_constant_has_no_high_bits 512 i (by decide) hi
+ simp [h3, hi]
+
+/-- Whatever a process left in its saved flags, the user-mode resume runs with
+ IOPL 0 (bits 12 and 13 clear), interrupts on, and the reserved bit set. An
+ IOPL of 3 here would give the process every I/O port. -/
+theorem sanitize_user_resumes_with_iopl_zero_and_interrupts_on (r : Std.U64) :
+ ∃ s, sanitize_user r = ok s ∧ s.val.testBit 12 = false ∧ s.val.testBit 13 = false ∧
+ s.val.testBit 9 = true ∧ s.val.testBit 1 = true := by
+ obtain ⟨s, hs, hbits⟩ := sanitize_user_is_sanitize_with_interrupts_forced_on r
+ refine ⟨s, hs, ?_, ?_, ?_, ?_⟩ <;> rw [hbits] <;> cases r.val.testBit _ <;> decide
+
+/-- The kernel-continuation path relies on `sanitize` leaving IF exactly as
+ saved: a yield taken with interrupts off must resume with them off, or the
+ timer can fire while a scheduler lock is held. IOPL is still cleared. -/
+theorem sanitize_leaves_if_as_saved_and_clears_iopl (r : Std.U64) :
+ ∃ s, sanitize r = ok s ∧ s.val.testBit 9 = r.val.testBit 9 ∧
+ s.val.testBit 12 = false ∧ s.val.testBit 13 = false := by
+ obtain ⟨s, hs, hbits⟩ := sanitize_keeps_exactly_the_bits_outside_the_privileged_positions r
+ refine ⟨s, hs, ?_, ?_, ?_⟩ <;> rw [hbits] <;> cases r.val.testBit _ <;> decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ContextRflags.the_sanitize_wrapper_is_its_method
#print axioms NonosExtraction.ContextRflags.the_sanitize_user_wrapper_is_its_method
+#print axioms NonosExtraction.ContextRflags.sanitize_keeps_exactly_the_bits_outside_the_privileged_positions
+#print axioms NonosExtraction.ContextRflags.sanitize_user_is_sanitize_with_interrupts_forced_on
+#print axioms NonosExtraction.ContextRflags.sanitize_user_resumes_with_iopl_zero_and_interrupts_on
+#print axioms NonosExtraction.ContextRflags.sanitize_leaves_if_as_saved_and_clears_iopl
end NonosExtraction.ContextRflags
diff --git a/verification/extraction/lean/NonosExtraction/ContractArgsRefinement.lean b/verification/extraction/lean/NonosExtraction/ContractArgsRefinement.lean
index 5f6fdccfae..75b9a172db 100644
--- a/verification/extraction/lean/NonosExtraction/ContractArgsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ContractArgsRefinement.lean
@@ -35,8 +35,46 @@ namespace NonosExtraction.ContractArgs
theorem the_syscallargs_arg_wrapper_is_its_method (a : args.SyscallArgs) (b : Std.Usize) :
syscallargs_arg a b = args.SyscallArgs.arg a b := rfl
+/-! ### An argument read is the register at that index, or a refusal
+
+`SyscallArgs` holds the six argument registers in the order the per-arch shim
+extracted them, and handlers read them by position. These theorems establish
+that `arg i` answers with exactly the `i`th stored register for every index
+below six, and that every index from six upward is refused with an
+out-of-bounds failure (the Rust bounds-check panic) rather than answered with
+some other register or a default.
+
+They cannot establish that the shim stored the registers in the calling
+convention's order, nor that no handler asks for an index past five: the shims
+and handlers are not extracted.
+-/
+
+/-- Below six, `syscallargs_arg` reads the register at exactly that position. -/
+theorem syscallargs_arg_reads_the_register_at_its_index
+ (a : args.SyscallArgs) (i : Std.Usize) (h : i.val < 6) :
+ syscallargs_arg a i = ok (a.val[i.val]!) := by
+ unfold syscallargs_arg args.SyscallArgs.arg
+ have hl : a.val.length = 6 := by simp [a.property]
+ obtain ⟨x, hx, hv⟩ := WP.spec_imp_exists (Array.index_usize_spec a i (by simp [hl, h]))
+ rw [hx, hv]
+ simp [List.getElem!_eq_getElem?_getD, List.getElem?_eq_getElem (by omega : i.val < a.val.length)]
+
+/-- From six upward, `syscallargs_arg` fails with an out-of-bounds error. -/
+theorem syscallargs_arg_refuses_every_index_from_six
+ (a : args.SyscallArgs) (i : Std.Usize) (h : 6 ≤ i.val) :
+ syscallargs_arg a i = fail .arrayOutOfBounds := by
+ unfold syscallargs_arg args.SyscallArgs.arg Array.index_usize
+ have hl : a.val.length = 6 := by simp [a.property]
+ have hn : a[i]? = none := by
+ simp [hl]
+ omega
+ rw [hn]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ContractArgs.the_syscallargs_arg_wrapper_is_its_method
+#print axioms NonosExtraction.ContractArgs.syscallargs_arg_reads_the_register_at_its_index
+#print axioms NonosExtraction.ContractArgs.syscallargs_arg_refuses_every_index_from_six
+
end NonosExtraction.ContractArgs
diff --git a/verification/extraction/lean/NonosExtraction/CoreSection.lean b/verification/extraction/lean/NonosExtraction/CoreSection.lean
index 21c5a8527e..7bab29025c 100644
--- a/verification/extraction/lean/NonosExtraction/CoreSection.lean
+++ b/verification/extraction/lean/NonosExtraction/CoreSection.lean
@@ -38,23 +38,21 @@ def section.ParsedSection.is_alloc
ok (i != 0#u64)
/-- [nonos_x_core_section::section::{nonos_x_core_section::section::ParsedSection}::is_symtab]:
- Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 35:4-37:5
+ Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 38:4-40:5
Visibility: public -/
def section.ParsedSection.is_symtab
(self : section.ParsedSection) : Result Bool := do
- if self.section_type = 2#u32
- then ok true
- else ok (self.section_type = 11#u32)
+ ok (self.section_type = 2#u32)
/-- [nonos_x_core_section::section::{nonos_x_core_section::section::ParsedSection}::is_strtab]:
- Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 38:4-40:5
+ Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 41:4-43:5
Visibility: public -/
def section.ParsedSection.is_strtab
(self : section.ParsedSection) : Result Bool := do
ok (self.section_type = 3#u32)
/-- [nonos_x_core_section::section::{nonos_x_core_section::section::ParsedSection}::is_rela]:
- Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 41:4-43:5
+ Source: 'src/../../../../../src/elf/loader/core/section.rs', lines 44:4-46:5
Visibility: public -/
def section.ParsedSection.is_rela
(self : section.ParsedSection) : Result Bool := do
diff --git a/verification/extraction/lean/NonosExtraction/CoreSectionRefinement.lean b/verification/extraction/lean/NonosExtraction/CoreSectionRefinement.lean
index 2968e3d56d..ed0814783e 100644
--- a/verification/extraction/lean/NonosExtraction/CoreSectionRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CoreSectionRefinement.lean
@@ -55,15 +55,15 @@ the theorems here pin each answer to the ELF gABI value the kernel names in
no other bit, so a section that is only writable or only executable is not
allocated. The three type classifiers read the type word alone, and hit exactly
their values: no two of them hold for one section, `SHT_REL` (whose entries have
-no addend) is never taken for `SHT_RELA`, and `is_symtab` holds for a dynamic
-symbol table as well as for the full one.
-
-That last fact is a property of the code as written, not of the gABI, and it is
-recorded as such: `ElfLoader::get_symbol_table` returns the first section for
-which `is_symtab` holds, so on an image that places `.dynsym` before `.symtab` it
-returns `.dynsym`. The loader methods that iterate over sections are not
-extracted, so the theorems below cannot state what `get_symbol_table` returns;
-they state only the predicate it filters by. The kernel literals are restated
+no addend) is never taken for `SHT_RELA`, and `is_symtab` holds for the full
+symbol table only, not for the dynamic one.
+
+`is_symtab` used to accept `SHT_DYNSYM` as well, so `ElfLoader::get_symbol_table`,
+which returns the first section for which it holds, returned `.dynsym` on an
+image that places it before `.symtab`; `get_dynsym` exists for that section. The
+loader methods that iterate over sections are not extracted, so the theorems
+below cannot state what `get_symbol_table` returns; they state only the
+predicate it filters by. The kernel literals are restated
here as numerals because the constants module is not extracted.
-/
@@ -88,31 +88,23 @@ theorem parsedsection_is_alloc_refuses_write_exec_and_accepts_alloc
have h2 : ((2 : Nat).testBit 1) = true := by decide
exact ⟨congrArg ok h5, congrArg ok h2⟩
-/-- A section counts as a symbol table exactly when its type is `SHT_SYMTAB` (2)
- or `SHT_DYNSYM` (11). -/
-theorem parsedsection_is_symtab_holds_exactly_for_types_two_and_eleven
+/-- A section counts as a symbol table exactly when its type is `SHT_SYMTAB`
+ (2). -/
+theorem parsedsection_is_symtab_holds_exactly_for_type_two
(s : section.ParsedSection) :
- parsedsection_is_symtab s = ok true ↔
- (s.section_type.val = 2 ∨ s.section_type.val = 11) := by
+ parsedsection_is_symtab s = ok true ↔ s.section_type.val = 2 := by
unfold parsedsection_is_symtab section.ParsedSection.is_symtab
have h2 : s.section_type = 2#u32 ↔ s.section_type.val = 2 :=
⟨fun h => by rw [h]; rfl, fun h => UScalar.eq_of_val_eq (by rw [h]; rfl)⟩
- have h11 : s.section_type = 11#u32 ↔ s.section_type.val = 11 :=
- ⟨fun h => by rw [h]; rfl, fun h => UScalar.eq_of_val_eq (by rw [h]; rfl)⟩
- by_cases a : s.section_type = 2#u32
- · simp [a]
- · have a' : ¬ s.section_type.val = 2 := fun h => a (h2.mpr h)
- simp only [a, if_false, a', false_or, ok.injEq, decide_eq_true_eq]
- exact h11
-
-/-- This records a property of the code rather than of the gABI: a dynamic symbol
- table (`SHT_DYNSYM`, 11) passes `is_symtab`, whatever its name, flags or
- placement. `ElfLoader::get_symbol_table` filters by this predicate and takes
- the first match, so it returns `.dynsym` whenever that section precedes
- `.symtab`, while the separate `get_dynsym` exists for exactly that section. -/
-theorem parsedsection_is_symtab_accepts_a_dynamic_symbol_table (s : section.ParsedSection) :
- parsedsection_is_symtab { s with section_type := 11#u32 } = ok true :=
- (parsedsection_is_symtab_holds_exactly_for_types_two_and_eleven _).mpr (Or.inr rfl)
+ simp only [ok.injEq, decide_eq_true_eq]
+ exact h2
+
+/-- A dynamic symbol table (`SHT_DYNSYM`, 11) does not pass `is_symtab`, whatever
+ its name, flags or placement. It used to. -/
+theorem parsedsection_is_symtab_refuses_a_dynamic_symbol_table (s : section.ParsedSection) :
+ parsedsection_is_symtab { s with section_type := 11#u32 } = ok false := by
+ unfold parsedsection_is_symtab section.ParsedSection.is_symtab
+ rfl
/-- A section is a string table exactly when its type is `SHT_STRTAB` (3). -/
theorem parsedsection_is_strtab_holds_exactly_for_type_three (s : section.ParsedSection) :
@@ -150,7 +142,7 @@ theorem parsedsection_is_symtab_is_strtab_and_is_rela_are_disjoint
(parsedsection_is_symtab s = ok true → parsedsection_is_strtab s = ok false ∧
parsedsection_is_rela s = ok false) ∧
(parsedsection_is_strtab s = ok true → parsedsection_is_rela s = ok false) := by
- have hs := parsedsection_is_symtab_holds_exactly_for_types_two_and_eleven s
+ have hs := parsedsection_is_symtab_holds_exactly_for_type_two s
have ht := parsedsection_is_strtab_holds_exactly_for_type_three s
have hr := parsedsection_is_rela_holds_exactly_for_type_four s
have bt : ∀ b : Bool, (ok b : Result Bool) = ok false ↔ ¬ (ok b : Result Bool) = ok true := by
@@ -173,8 +165,8 @@ theorem parsedsection_is_symtab_is_strtab_and_is_rela_are_disjoint
#print axioms NonosExtraction.CoreSection.the_parsedsection_is_rela_wrapper_is_its_method
#print axioms NonosExtraction.CoreSection.parsedsection_is_alloc_reads_exactly_the_shf_alloc_bit
#print axioms NonosExtraction.CoreSection.parsedsection_is_alloc_refuses_write_exec_and_accepts_alloc
-#print axioms NonosExtraction.CoreSection.parsedsection_is_symtab_holds_exactly_for_types_two_and_eleven
-#print axioms NonosExtraction.CoreSection.parsedsection_is_symtab_accepts_a_dynamic_symbol_table
+#print axioms NonosExtraction.CoreSection.parsedsection_is_symtab_holds_exactly_for_type_two
+#print axioms NonosExtraction.CoreSection.parsedsection_is_symtab_refuses_a_dynamic_symbol_table
#print axioms NonosExtraction.CoreSection.parsedsection_is_strtab_holds_exactly_for_type_three
#print axioms NonosExtraction.CoreSection.parsedsection_is_strtab_depends_only_on_the_type
#print axioms NonosExtraction.CoreSection.parsedsection_is_rela_holds_exactly_for_type_four
diff --git a/verification/extraction/lean/NonosExtraction/CpuCacheAssocRefinement.lean b/verification/extraction/lean/NonosExtraction/CpuCacheAssocRefinement.lean
index 360a0114eb..6862b199e0 100644
--- a/verification/extraction/lean/NonosExtraction/CpuCacheAssocRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CpuCacheAssocRefinement.lean
@@ -35,8 +35,76 @@ namespace NonosExtraction.CpuCacheAssoc
theorem the_decode_l2_assoc_wrapper_is_its_method (a : Std.U8) :
decode_l2_assoc a = cache_assoc.decode_l2_assoc a := rfl
+/-! ### Decoding the extended-leaf associativity field
+
+ `detect_extended` takes the four-bit field `(reg >> 12) & 0xF` from CPUID leaf
+ 0x80000006 ECX (L2) and EDX (L3) and hands it to `decode_l2_assoc`. The
+ theorems below say that every even encoding from 2 to 14 decodes to
+ `2^(e/2)` ways, that 11 and 13 decode to the non-power-of-two 48 and 96 ways,
+ and exactly which encodings read as zero: 0 (cache disabled), the encodings
+ 3, 5, 7 and 9, 15 (fully associative) and every byte above 15, which the
+ caller's mask never produces. A zero therefore does not tell a disabled cache
+ from a fully associative one, and newer processors that define 3 and 5 as
+ three-way and six-way, or 9 as "see leaf 0x8000001D", are also reported as
+ zero. They cannot establish what `cpuid` returns; the instruction and the
+ `CacheInfo` record are not extracted here.
+-/
+
+private theorem small_byte_cases (v : Std.U8) (h : v.val < 16) :
+ v = 0#u8 ∨ v = 1#u8 ∨ v = 2#u8 ∨ v = 3#u8 ∨ v = 4#u8 ∨ v = 5#u8 ∨ v = 6#u8 ∨
+ v = 7#u8 ∨ v = 8#u8 ∨ v = 9#u8 ∨ v = 10#u8 ∨ v = 11#u8 ∨ v = 12#u8 ∨
+ v = 13#u8 ∨ v = 14#u8 ∨ v = 15#u8 := by
+ have : v.val = 0 ∨ v.val = 1 ∨ v.val = 2 ∨ v.val = 3 ∨ v.val = 4 ∨ v.val = 5 ∨
+ v.val = 6 ∨ v.val = 7 ∨ v.val = 8 ∨ v.val = 9 ∨ v.val = 10 ∨ v.val = 11 ∨
+ v.val = 12 ∨ v.val = 13 ∨ v.val = 14 ∨ v.val = 15 := by omega
+ rcases this with h | h | h | h | h | h | h | h | h | h | h | h | h | h | h | h <;>
+ simp only [UScalar.eq_equiv, h] <;> decide
+
+/-- Every even encoding from 2 to 14 is a power-of-two way count, `2^(e/2)`. -/
+theorem decode_l2_assoc_reads_an_even_encoding_as_a_power_of_two (e : Std.U8)
+ (h2 : 2 ≤ e.val) (h14 : e.val ≤ 14) (heven : e.val % 2 = 0) :
+ ∃ w : Std.U16, decode_l2_assoc e = ok w ∧ w.val = 2 ^ (e.val / 2) := by
+ rcases small_byte_cases e (by omega) with
+ h | h | h | h | h | h | h | h | h | h | h | h | h | h | h | h <;> subst h <;>
+ first
+ | exact ⟨_, rfl, rfl⟩
+ | exact absurd heven (by decide)
+ | exact absurd h2 (by decide)
+
+/-- The two encodings that are not powers of two are 48 and 96 ways. -/
+theorem decode_l2_assoc_reads_eleven_and_thirteen_as_48_and_96 :
+ decode_l2_assoc 11#u8 = ok 48#u16 ∧ decode_l2_assoc 13#u8 = ok 96#u16 ∧
+ decode_l2_assoc 1#u8 = ok 1#u16 := by
+ exact ⟨rfl, rfl, rfl⟩
+
+/-- A byte above 15 cannot come from the caller's four-bit mask and decodes to
+ zero rather than failing. -/
+theorem decode_l2_assoc_reads_a_byte_above_fifteen_as_zero (e : Std.U8) (h : 15 < e.val) :
+ decode_l2_assoc e = ok 0#u16 := by
+ unfold decode_l2_assoc cache_assoc.decode_l2_assoc
+ split <;> first | rfl | exact absurd h (by decide)
+
+/-- Within the four-bit field, zero is reported exactly for 0, 3, 5, 7, 9 and 15:
+ a disabled cache, the encodings this table leaves undefined, and a fully
+ associative cache all read alike. -/
+theorem decode_l2_assoc_is_zero_exactly_on_these_encodings (e : Std.U8) (h : e.val < 16) :
+ decode_l2_assoc e = ok 0#u16 ↔
+ (e.val = 0 ∨ e.val = 3 ∨ e.val = 5 ∨ e.val = 7 ∨ e.val = 9 ∨ e.val = 15) := by
+ rcases small_byte_cases e h with
+ h | h | h | h | h | h | h | h | h | h | h | h | h | h | h | h <;> subst h <;>
+ first
+ | exact ⟨fun _ => by decide, fun _ => rfl⟩
+ | exact ⟨fun hh => by
+ have hv := congrArg (fun r : Result Std.U16 => match r with | ok w => w.val | _ => 0) hh
+ revert hv; decide,
+ fun hp => absurd hp (by decide)⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.CpuCacheAssoc.the_decode_l2_assoc_wrapper_is_its_method
+#print axioms NonosExtraction.CpuCacheAssoc.decode_l2_assoc_reads_an_even_encoding_as_a_power_of_two
+#print axioms NonosExtraction.CpuCacheAssoc.decode_l2_assoc_reads_eleven_and_thirteen_as_48_and_96
+#print axioms NonosExtraction.CpuCacheAssoc.decode_l2_assoc_reads_a_byte_above_fifteen_as_zero
+#print axioms NonosExtraction.CpuCacheAssoc.decode_l2_assoc_is_zero_exactly_on_these_encodings
end NonosExtraction.CpuCacheAssoc
diff --git a/verification/extraction/lean/NonosExtraction/CtPrimitivesRefinement.lean b/verification/extraction/lean/NonosExtraction/CtPrimitivesRefinement.lean
index b452061956..7d88e94187 100644
--- a/verification/extraction/lean/NonosExtraction/CtPrimitivesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/CtPrimitivesRefinement.lean
@@ -36,14 +36,12 @@ with masks rather than branches. `the_lookup_scans_a_fixed_range` says the first
of those about the extracted definition. It does not say the compiler kept it, and
nothing here does.
-Two things are absent and should not be read as unproven-because-false.
-`ct_clz_u64` is a chain of six nested selects over `ct_is_zero_u64`, and
-`ct_select_usize` goes through a cast the kernel will not reduce past here;
-neither closes by reduction or by `simp` in this encoding. Both were checked by
-sampling against the reference operation, `ct_clz_u64` on twenty thousand random
-words and every boundary, with no disagreement. That is a differential check of
-the kind `kernel_proofs` carries, not a proof, and it is recorded as such rather
-than dressed up.
+Two functions do not close here. `ct_clz_u64` is a chain of six nested selects
+over `ct_is_zero_u64`, and `ct_select_usize` goes through a cast the kernel will
+not reduce past; neither closes by reduction or by `simp` in this encoding.
+`CtSelectClzRefinement` proves both for every input: the selectors for every
+pair of operands, and the leading-zero count given the Rust meaning of
+`wrapping_neg`, which the extraction leaves opaque.
-/
import NonosExtraction.Ct
diff --git a/verification/extraction/lean/NonosExtraction/CtSelectClzRefinement.lean b/verification/extraction/lean/NonosExtraction/CtSelectClzRefinement.lean
new file mode 100644
index 0000000000..1a2ede49ba
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/CtSelectClzRefinement.lean
@@ -0,0 +1,356 @@
+/-
+NONOS Operating System
+Copyright (C) 2026 NONOS Contributors
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version. See .
+
+The constant-time selectors for every operand, and the leading-zero count.
+
+`CtPrimitivesRefinement` states the selectors on sample operands and records
+that `ct_select_usize` and `ct_clz_u64` did not close there. They close here.
+
+The three selectors turn a `bool` into an all-ones or all-zero mask and combine
+the operands with it. The theorems below say each returns its first operand on
+`true` and its second on `false` for every pair of operands, and for
+`ct_select_usize` on a 32-bit or a 64-bit `usize` alike, which is what the
+`cast` that stopped reduction hid.
+
+`ct_clz_u64` is six stages of `ct_is_zero_u64` and the selectors, halving the
+width each time, and a final one-bit step. `ct_is_zero_u64` calls
+`u64::wrapping_neg`, which the extraction leaves opaque, so no statement about
+either can be proven outright. Every theorem that needs it takes the Rust
+meaning of `wrapping_neg`, two's complement negation, as the hypothesis `hneg`,
+the way `ElfBoundsRefinement` takes `Option::ok_or`. Under it, `ct_is_zero_u64`
+is one exactly at zero, and `ct_clz_u64` returns 64 for zero and otherwise the
+shift that brings the leading one to bit 63. The proof keeps an invariant per
+stage: the word is the input times a power of two without overflow, and its
+leading one lies in the bits the next stage still has to inspect.
+
+These are value statements. That the compiler keeps the computation free of
+branches is a property of the generated code, and nothing here says it.
+-/
+
+import NonosExtraction.Ct
+
+open Aeneas Aeneas.Std Result
+open nonos_ct
+
+set_option linter.hashCommand false
+
+namespace NonosExtraction.CtSelectClz
+
+/-! ### The selectors -/
+
+private theorem u64_ext {x y : Std.U64} (h : x.bv = y.bv) : x = y := by
+ cases x; cases y; simp_all
+private theorem u32_ext {x y : Std.U32} (h : x.bv = y.bv) : x = y := by
+ cases x; cases y; simp_all
+private theorem usize_ext {x y : Std.Usize} (h : x.bv = y.bv) : x = y := by
+ cases x; cases y; simp_all
+
+/-- `ct_select_u64` returns its first operand on `true` and its second on
+ `false`, for every pair of operands. -/
+theorem ct_select_u64_selects (c : Bool) (a b : Std.U64) :
+ crypto.util.constant_time.select.ct_select_u64 c a b = ok (if c then a else b) := by
+ unfold crypto.util.constant_time.select.ct_select_u64
+ cases c
+ · have h : (-. (IScalar.cast_fromBool .I64 false) : Result Std.I64) = ok 0#i64 := by rfl
+ simp only [lift, bind_tc_ok, h]
+ congr 1; apply u64_ext
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, IScalar.hcast]
+ · have h : (-. (IScalar.cast_fromBool .I64 true) : Result Std.I64) = ok (-1)#i64 := by rfl
+ simp only [lift, bind_tc_ok, h]
+ congr 1; apply u64_ext
+ have : (IScalar.hcast .U64 ((-1)#i64 : Std.I64)).bv = BitVec.allOnes 64 := by rfl
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, this]
+
+/-- `ct_select_u32` returns its first operand on `true` and its second on
+ `false`, for every pair of operands. -/
+theorem ct_select_u32_selects (c : Bool) (a b : Std.U32) :
+ crypto.util.constant_time.select.ct_select_u32 c a b = ok (if c then a else b) := by
+ unfold crypto.util.constant_time.select.ct_select_u32
+ cases c
+ · have h : (-. (IScalar.cast_fromBool .I32 false) : Result Std.I32) = ok 0#i32 := by rfl
+ simp only [lift, bind_tc_ok, h]
+ congr 1; apply u32_ext
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, IScalar.hcast]
+ · have h : (-. (IScalar.cast_fromBool .I32 true) : Result Std.I32) = ok (-1)#i32 := by rfl
+ simp only [lift, bind_tc_ok, h]
+ congr 1; apply u32_ext
+ have : (IScalar.hcast .U32 ((-1)#i32 : Std.I32)).bv = BitVec.allOnes 32 := by rfl
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, this]
+
+private theorem cfb_true : (IScalar.cast_fromBool .Isize true).val = 1 := by
+ simp only [IScalar.cast_fromBool, ite_true, IScalar.val]
+ apply BitVec.toInt_one_of_lt
+ simp only [IScalarTy.numBits]
+ rcases System.Platform.numBits_eq with h | h <;> omega
+
+private theorem cfb_false : (IScalar.cast_fromBool .Isize false).val = 0 := by
+ simp only [IScalar.cast_fromBool, Bool.false_eq_true, ite_false, IScalar.val]
+ simp
+
+private theorem cfb_val (c : Bool) :
+ (IScalar.cast_fromBool .Isize c).val = if c then 1 else 0 := by
+ cases c
+ · exact cfb_false
+ · exact cfb_true
+
+/-- `ct_select_usize` returns its first operand on `true` and its second on
+ `false`, on a 32-bit or a 64-bit `usize`. -/
+theorem ct_select_usize_selects (c : Bool) (a b : Std.Usize) :
+ crypto.util.constant_time.select.ct_select_usize c a b = ok (if c then a else b) := by
+ unfold crypto.util.constant_time.select.ct_select_usize
+ simp only [lift, bind_tc_ok]
+ have hw := System.Platform.numBits_eq
+ have hm : IScalar.cast_fromBool .Isize c ≠ IScalar.min .Isize := by
+ intro e
+ rw [cfb_val] at e
+ simp only [IScalar.min, IScalarTy.numBits] at e
+ have : (2 : Int) ^ (System.Platform.numBits - 1) ≥ 2 ^ 31 := by
+ rcases hw with hn | hn <;> (rw [hn]; try decide)
+ split at e <;> omega
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (HNeg.hNeg.step _ hm)
+ rw [cfb_val] at hrv
+ simp only [hr, bind_tc_ok]
+ congr 1
+ apply usize_ext
+ have hext : (IScalar.hcast UScalarTy.Usize r).bv = r.bv := BitVec.signExtend_eq r.bv
+ have hrb : r.bv.toInt = r.val := rfl
+ cases c
+ · have hr0 : r.bv = 0#_ := by
+ apply BitVec.eq_of_toInt_eq
+ rw [hrb, hrv]; simp
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, hext, hr0]
+ · have hr1 : r.bv = BitVec.allOnes _ := by
+ apply BitVec.eq_of_toInt_eq
+ rw [hrb, hrv, BitVec.toInt_allOnes]
+ simp only [IScalarTy.numBits]
+ rcases hw with hn | hn <;> simp [hn]
+ simp [UScalar.bv_and, UScalar.bv_or, UScalar.bv_not, hext, hr1]
+
+/-! ### Zero test and leading-zero count -/
+
+/-- Given the Rust meaning of `wrapping_neg`, `ct_is_zero_u64` answers one
+ exactly at zero and zero everywhere else. The top bit of `y | -y` is set
+ exactly when `y` is not zero. -/
+theorem ct_is_zero_u64_is_one_exactly_at_zero
+ (hneg : ∀ x : Std.U64, core.num.U64.wrapping_neg x = ok ⟨-x.bv⟩) (y : Std.U64) :
+ crypto.util.constant_time.compare.ct_is_zero_u64 y =
+ ok (if y.val = 0 then 1#u64 else 0#u64) := by
+ unfold crypto.util.constant_time.compare.ct_is_zero_u64
+ simp only [hneg, lift, bind_tc_ok]
+ have hy := y.hBounds
+ simp only [UScalarTy.U64_numBits_eq] at hy
+ obtain ⟨z, hz, hzv, -⟩ := WP.spec_imp_exists
+ (UScalar.ShiftRight_IScalar_spec (y ||| (⟨-y.bv⟩ : Std.U64)) 63#i32 (by decide) (by decide))
+ rw [hz]; simp only [bind_tc_ok]
+ have hor : (y ||| (⟨-y.bv⟩ : Std.U64)).val = y.val ||| (2 ^ 64 - y.val) % 2 ^ 64 := by
+ rw [UScalar.val_or]
+ show _ ||| (-y.bv).toNat = _
+ rw [BitVec.toNat_neg]
+ rfl
+ have hlt : y.val ||| (2 ^ 64 - y.val) % 2 ^ 64 < 2 ^ 64 :=
+ Nat.or_lt_two_pow hy (Nat.mod_lt _ (by decide))
+ have h63 : (63#i32 : Std.I32).toNat = 63 := rfl
+ rw [h63, hor, Nat.shiftRight_eq_div_pow] at hzv
+ congr 1
+ apply u64_ext
+ by_cases h0 : y.val = 0
+ · have : z.val = 0 := by rw [hzv, h0]; decide
+ have hz0 : z = 0#u64 := UScalar.eq_of_val_eq this
+ simp [h0, hz0]
+ · have hge : 2 ^ 63 ≤ y.val ||| (2 ^ 64 - y.val) % 2 ^ 64 := by
+ by_cases hb : 2 ^ 63 ≤ y.val
+ · exact Nat.le_trans hb Nat.left_le_or
+ · rw [Nat.or_comm]
+ refine Nat.le_trans ?_ Nat.left_le_or
+ rw [Nat.mod_eq_of_lt (by omega)]
+ omega
+ have : z.val = 1 := by rw [hzv]; omega
+ have hz1 : z = 1#u64 := UScalar.eq_of_val_eq this
+ simp [h0, hz1]
+
+/-- One halving step of the count, on naturals. `v` is the word after the
+ stages so far, `x * 2 ^ m` without overflow, with its leading one in the top
+ `2 * k` bits. If the top `k` bits are clear the stage shifts by `k`; either
+ way the leading one ends in the top `k` bits. -/
+private theorem clz_stage (x v m k : Nat) (hk : 0 < k) (hk2 : 2 * k ≤ 64)
+ (hv : v = x * 2 ^ m) (hv64 : v < 2 ^ 64) (hlead : x = 0 ∨ 2 ^ (64 - 2 * k) ≤ v)
+ (c : Prop) [Decidable c] (hc : c ↔ v / 2 ^ (64 - k) = 0) (v' : Nat)
+ (hv' : v' = if c then v * 2 ^ k % 2 ^ 64 else v) :
+ v' = x * 2 ^ (m + if c then k else 0) ∧ v' < 2 ^ 64 ∧ (x = 0 ∨ 2 ^ (64 - k) ≤ v') := by
+ have hsplit : (2 : Nat) ^ 64 = 2 ^ (64 - k) * 2 ^ k := by
+ rw [← Nat.pow_add]; congr 1; omega
+ by_cases h : c
+ · have hlt : v < 2 ^ (64 - k) := by
+ have := hc.mp h
+ rcases Nat.eq_zero_or_pos (2 ^ (64 - k)) with h0 | h0
+ · exact absurd h0 (by positivity)
+ · exact (Nat.div_eq_zero_iff_lt h0).mp this
+ have hmul : v * 2 ^ k < 2 ^ 64 := by
+ rw [hsplit]; exact Nat.mul_lt_mul_of_pos_right hlt (by positivity)
+ simp only [h, ↓reduceIte] at hv' ⊢
+ rw [Nat.mod_eq_of_lt hmul] at hv'
+ refine ⟨by rw [hv', hv, Nat.pow_add, Nat.mul_assoc], by omega, ?_⟩
+ rcases hlead with h0 | hl
+ · exact Or.inl h0
+ · right
+ have e : (2 : Nat) ^ (64 - k) = 2 ^ (64 - 2 * k) * 2 ^ k := by
+ rw [← Nat.pow_add]; congr 1; omega
+ rw [hv', e]
+ exact Nat.mul_le_mul_right _ hl
+ · have hge : 2 ^ (64 - k) ≤ v := by
+ by_contra hn
+ exact h (hc.mpr ((Nat.div_eq_zero_iff_lt (by positivity)).mpr (by omega)))
+ simp only [h, ↓reduceIte, Nat.add_zero] at hv' ⊢
+ exact ⟨by rw [hv', hv], by omega, Or.inr (by omega)⟩
+
+private theorem small_sum (a b c d e f g : Nat) (ha : a ≤ 32) (hb : b ≤ 16) (hc : c ≤ 8)
+ (hd : d ≤ 4) (he : e ≤ 2) (hf : f ≤ 1) (hg : g ≤ 1) :
+ a + b + c + d + e + f + g ≤ 4294967295 := by omega
+
+private theorem small_sum6 (a b c d e f : Nat) (ha : a ≤ 32) (hb : b ≤ 16) (hc : c ≤ 8)
+ (hd : d ≤ 4) (he : e ≤ 2) (hf : f ≤ 1) :
+ a + b + c + d + e + f ≤ 4294967295 := by omega
+
+private theorem le32 {x : Std.U32} (h : x.val = 32 ∨ x.val = 0) : x.val ≤ 32 := by omega
+private theorem le16 {x : Std.U32} (h : x.val = 16 ∨ x.val = 0) : x.val ≤ 16 := by omega
+private theorem le8 {x : Std.U32} (h : x.val = 8 ∨ x.val = 0) : x.val ≤ 8 := by omega
+private theorem le4 {x : Std.U32} (h : x.val = 4 ∨ x.val = 0) : x.val ≤ 4 := by omega
+private theorem le2 {x : Std.U32} (h : x.val = 2 ∨ x.val = 0) : x.val ≤ 2 := by omega
+private theorem le1 {x : Std.U32} (h : x.val = 1 ∨ x.val = 0) : x.val ≤ 1 := by omega
+
+@[local step]
+private theorem sel32_step (c : Bool) (a b : Std.U32) :
+ crypto.util.constant_time.select.ct_select_u32 c a b ⦃ r =>
+ r = (if c then a else b) ∧ (r.val = a.val ∨ r.val = b.val) ⦄ := by
+ rw [ct_select_u32_selects]
+ simp only [WP.spec_ok]
+ split <;> simp
+
+@[local step]
+private theorem sel64_step (c : Bool) (a b : Std.U64) :
+ crypto.util.constant_time.select.ct_select_u64 c a b ⦃ r => r = if c then a else b ⦄ := by
+ rw [ct_select_u64_selects]; simp
+
+@[local step]
+private theorem is_zero_step
+ (hneg : ∀ x : Std.U64, core.num.U64.wrapping_neg x = ok ⟨-x.bv⟩)
+ (y : Std.U64) :
+ crypto.util.constant_time.compare.ct_is_zero_u64 y ⦃ r =>
+ ((r != 0#u64) = true ↔ y.val = 0) ⦄ := by
+ rw [ct_is_zero_u64_is_one_exactly_at_zero hneg]
+ by_cases h : y.val = 0 <;> simp [h]
+
+set_option maxHeartbeats 4000000 in
+/-- Given the Rust meaning of `wrapping_neg`, `ct_clz_u64` counts leading zeros:
+ it returns 64 for zero, and for any other word the shift `n` that brings its
+ leading one to bit 63, so `2 ^ 63 ≤ x * 2 ^ n < 2 ^ 64`. -/
+theorem ct_clz_u64_counts_leading_zeros
+ (hneg : ∀ x : Std.U64, core.num.U64.wrapping_neg x = ok ⟨-x.bv⟩) (x : Std.U64) :
+ crypto.util.constant_time.math.ct_clz_u64 x ⦃ n =>
+ (x.val = 0 ∧ n.val = 64) ∨
+ (x.val ≠ 0 ∧ 2 ^ 63 ≤ x.val * 2 ^ n.val ∧ x.val * 2 ^ n.val < 2 ^ 64) ⦄ := by
+ unfold crypto.util.constant_time.math.ct_clz_u64
+ step*
+ · (rw [show U32.max = 4294967295 by simp [U32.max, U32.numBits], n4_post, n3_post, n2_post,
+ n1_post]
+ exact small_sum6 _ _ _ _ _ _ (le32 n_post2) (le16 i3_post2) (le8 i6_post2) (le4 i9_post2)
+ (le2 i12_post2) (le1 i15_post2))
+ · (rw [show U32.max = 4294967295 by simp [U32.max, U32.numBits], n5_post, n4_post, n3_post,
+ n2_post, n1_post]
+ exact small_sum _ _ _ _ _ _ _ (le32 n_post2) (le16 i3_post2) (le8 i6_post2) (le4 i9_post2)
+ (le2 i12_post2) (le1 i15_post2) (le1 i18_post2))
+ · have hx := x.hBounds
+ simp only [UScalarTy.U64_numBits_eq] at hx
+ -- the selected words and counts, as naturals
+ have sel : ∀ (c : Prop) [Decidable c] (a b r : Std.U64), r = (if c then a else b) →
+ r.val = if c then a.val else b.val := by
+ intro c _ a b r h; subst h; split <;> rfl
+ have sel32v : ∀ (c : Prop) [Decidable c] (a b r : Std.U32), r = (if c then a else b) →
+ r.val = if c then a.val else b.val := by
+ intro c _ a b r h; subst h; split <;> rfl
+ have shl : ∀ (v r : Std.U64) (k : Nat), r.val = v.val <<< k % U64.size →
+ r.val = v.val * 2 ^ k % 2 ^ 64 := by
+ intro v r k h
+ have hs : U64.size = 2 ^ 64 := by simp [U64.size, U64.numBits]
+ rw [h, Nat.shiftLeft_eq, hs]
+ have shr : ∀ (v r : Std.U64) (k : Nat), r.val = v.val >>> k → r.val = v.val / 2 ^ k := by
+ intro v r k h; rw [h, Nat.shiftRight_eq_div_pow]
+ have c1 := upper_zero_post.trans (by rw [shr _ _ _ i_post1])
+ have c2 := upper_zero1_post.trans (by rw [shr _ _ _ i2_post1])
+ have c3 := upper_zero2_post.trans (by rw [shr _ _ _ i5_post1])
+ have c4 := upper_zero3_post.trans (by rw [shr _ _ _ i8_post1])
+ have c5 := upper_zero4_post.trans (by rw [shr _ _ _ i11_post1])
+ have c6 := upper_zero5_post.trans (by rw [shr _ _ _ i14_post1])
+ have v1 := sel _ _ _ _ val_post; rw [shl _ _ _ i1_post1] at v1
+ have v2 := sel _ _ _ _ val1_post; rw [shl _ _ _ i4_post1] at v2
+ have v3 := sel _ _ _ _ val2_post; rw [shl _ _ _ i7_post1] at v3
+ have v4 := sel _ _ _ _ val3_post; rw [shl _ _ _ i10_post1] at v4
+ have v5 := sel _ _ _ _ val4_post; rw [shl _ _ _ i13_post1] at v5
+ have v6 := sel _ _ _ _ val5_post; rw [shl _ _ _ i16_post1] at v6
+ obtain ⟨e1, l1, d1⟩ := clz_stage x.val x.val 0 32 (by decide) (by decide) (by simp) hx
+ (if h : x.val = 0 then Or.inl h else Or.inr (Nat.pos_of_ne_zero h)) _ c1 _ v1
+ obtain ⟨e2, l2, d2⟩ := clz_stage x.val _ _ 16 (by decide) (by decide) e1 l1 d1 _ c2 _ v2
+ obtain ⟨e3, l3, d3⟩ := clz_stage x.val _ _ 8 (by decide) (by decide) e2 l2 d2 _ c3 _ v3
+ obtain ⟨e4, l4, d4⟩ := clz_stage x.val _ _ 4 (by decide) (by decide) e3 l3 d3 _ c4 _ v4
+ obtain ⟨e5, l5, d5⟩ := clz_stage x.val _ _ 2 (by decide) (by decide) e4 l4 d4 _ c5 _ v5
+ obtain ⟨e6, l6, d6⟩ := clz_stage x.val _ _ 1 (by decide) (by decide) e5 l5 d5 _ c6 _ v6
+ have t0 := sel32v _ _ _ _ n_post1
+ have t1 := sel32v _ _ _ _ i3_post1
+ have t2 := sel32v _ _ _ _ i6_post1
+ have t3 := sel32v _ _ _ _ i9_post1
+ have t4 := sel32v _ _ _ _ i12_post1
+ have t5 := sel32v _ _ _ _ i15_post1
+ have t6 := sel32v _ _ _ _ i18_post1
+ have c7 := final_zero_post.trans (by rw [shr _ _ _ i17_post1])
+ simp only [show (32#u32 : Std.U32).val = 32 from rfl, show (16#u32 : Std.U32).val = 16 from rfl,
+ show (8#u32 : Std.U32).val = 8 from rfl, show (4#u32 : Std.U32).val = 4 from rfl,
+ show (2#u32 : Std.U32).val = 2 from rfl, show (1#u32 : Std.U32).val = 1 from rfl,
+ show (0#u32 : Std.U32).val = 0 from rfl] at t0 t1 t2 t3 t4 t5 t6
+ have hn : n.val = (0 + (if (upper_zero != 0#u64) = true then 32 else 0) +
+ (if (upper_zero1 != 0#u64) = true then 16 else 0) +
+ (if (upper_zero2 != 0#u64) = true then 8 else 0) +
+ (if (upper_zero3 != 0#u64) = true then 4 else 0) +
+ (if (upper_zero4 != 0#u64) = true then 2 else 0) +
+ (if (upper_zero5 != 0#u64) = true then 1 else 0)) +
+ (if (final_zero != 0#u64) = true then 1 else 0) := by
+ rw [n_post, t6, n5_post, t5, n4_post, t4, n3_post, t3, n2_post, t2, n1_post, t1, t0,
+ Nat.zero_add]
+ by_cases hx0 : x.val = 0
+ · left
+ refine ⟨hx0, ?_⟩
+ have z1 : val.val = 0 := by rw [e1, hx0, Nat.zero_mul]
+ have z2 : val1.val = 0 := by rw [e2, hx0, Nat.zero_mul]
+ have z3 : val2.val = 0 := by rw [e3, hx0, Nat.zero_mul]
+ have z4 : val3.val = 0 := by rw [e4, hx0, Nat.zero_mul]
+ have z5 : val4.val = 0 := by rw [e5, hx0, Nat.zero_mul]
+ have z6 : val5.val = 0 := by rw [e6, hx0, Nat.zero_mul]
+ rw [if_pos (c1.mpr (by rw [hx0, Nat.zero_div])), if_pos (c2.mpr (by rw [z1, Nat.zero_div])),
+ if_pos (c3.mpr (by rw [z2, Nat.zero_div])), if_pos (c4.mpr (by rw [z3, Nat.zero_div])),
+ if_pos (c5.mpr (by rw [z4, Nat.zero_div])), if_pos (c6.mpr (by rw [z5, Nat.zero_div])),
+ if_pos (c7.mpr (by rw [z6, Nat.zero_div]))] at hn
+ exact hn
+ · right
+ rcases d6 with h0 | h63
+ · exact absurd h0 hx0
+ · have hf : ¬ ((final_zero != 0#u64) = true) := by
+ rw [c7]; intro h
+ exact absurd ((Nat.div_eq_zero_iff_lt (by positivity)).mp h) (Nat.not_lt.mpr h63)
+ rw [if_neg hf, Nat.add_zero] at hn
+ rw [hn, ← e6]
+ exact ⟨hx0, h63, l6⟩
+
+/-! ### Axiom profile -/
+
+#print axioms NonosExtraction.CtSelectClz.ct_select_u64_selects
+#print axioms NonosExtraction.CtSelectClz.ct_select_u32_selects
+#print axioms NonosExtraction.CtSelectClz.ct_select_usize_selects
+#print axioms NonosExtraction.CtSelectClz.ct_is_zero_u64_is_one_exactly_at_zero
+#print axioms NonosExtraction.CtSelectClz.ct_clz_u64_counts_leading_zeros
+
+end NonosExtraction.CtSelectClz
diff --git a/verification/extraction/lean/NonosExtraction/DataInterruptRefinement.lean b/verification/extraction/lean/NonosExtraction/DataInterruptRefinement.lean
index e66578d173..e1755a0c40 100644
--- a/verification/extraction/lean/NonosExtraction/DataInterruptRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DataInterruptRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.DataInterrupt
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_data_interrupt
@@ -41,10 +42,136 @@ theorem the_interruptoverride_is_level_triggered_wrapper_is_its_method (a : inte
theorem the_nmiconfig_applies_to_all_wrapper_is_its_method (a : interrupt.NmiConfig) :
nmiconfig_applies_to_all a = interrupt.NmiConfig.applies_to_all a := rfl
+/-! ### The flag readers agree with the MADT encoding they are decoded from
+
+The parser fills `polarity` with `flags & 0x3` and `trigger_mode` with
+`(flags & 0xC) >> 2` of the raw MPS INTI flags word of an interrupt source
+override, and ACPI encodes active low as `11b` in bits 1:0 and level triggered
+as `11b` in bits 3:2. The theorems below establish that, fed a field decoded the
+parser's way, each reader answers exactly whether both bits of its field are set
+in the raw word, so the conforming (`00b`), the opposite (`01b`) and the reserved
+(`10b`) encodings all read as false. They also establish that `applies_to_all`
+agrees with both MADT sources of an NMI entry: the legacy 8-bit processor id,
+whose broadcast value `0xFF` the parser widens to `u32::MAX`, and the x2APIC
+32-bit uid, whose broadcast value is `0xFFFFFFFF`.
+
+The parser itself reads the entry through a volatile pointer read and is not
+extracted, so the decoding is stated as a hypothesis on the field's value rather
+than proven of the parser; what is proven is that the readers are right for the
+values that decoding produces. -/
+
+/-- The polarity field as the parser decodes it, `(flags & 0x3) as u8`, reads as
+ active low exactly when bits 0 and 1 of the raw flags word are both set. -/
+theorem interruptoverride_is_active_low_reads_both_polarity_bits
+ (flags : Std.U16) (src tm : Std.U8) (gsi : Std.U32) :
+ interruptoverride_is_active_low ⟨src, gsi, UScalar.cast .U8 (flags &&& 3#u16), tm⟩
+ = ok (flags.val.testBit 0 && flags.val.testBit 1) := by
+ unfold interruptoverride_is_active_low interrupt.InterruptOverride.is_active_low
+ have hv : (UScalar.cast .U8 (flags &&& 3#u16)).val = flags.val % 4 := by
+ rw [UScalar.cast_val_eq, Bits.land_low_mask flags 3#u16 2 rfl]
+ simp; omega
+ have h0 : flags.val.testBit 0 = decide (flags.val % 2 = 1) := by
+ simp [Nat.testBit, Nat.shiftRight_eq_div_pow, Nat.one_and_eq_mod_two]; rfl
+ have h1 : flags.val.testBit 1 = decide (flags.val / 2 % 2 = 1) := by
+ simp [Nat.testBit, Nat.shiftRight_eq_div_pow, Nat.one_and_eq_mod_two]; rfl
+ rw [h0, h1]
+ congr 1
+ by_cases h : flags.val % 4 = 3
+ · have : UScalar.cast .U8 (flags &&& 3#u16) = 3#u8 := UScalar.eq_of_val_eq (by rw [hv, h]; rfl)
+ rw [this]; simp; omega
+ · have : UScalar.cast .U8 (flags &&& 3#u16) ≠ 3#u8 := by
+ intro he; have := congrArg UScalar.val he; rw [hv] at this; exact h this
+ simp only [this, decide_false]
+ symm; simp; omega
+
+/-- A trigger field holding the value the parser decodes, `((flags & 0xC) >> 2)`,
+ reads as level triggered exactly when bits 2 and 3 of the raw flags word are
+ both set, whatever the polarity bits hold. -/
+theorem interruptoverride_is_level_triggered_reads_both_trigger_bits
+ (flags : Std.U16) (src pol tm : Std.U8) (gsi : Std.U32)
+ (htm : tm.val = flags.val / 4 % 4) :
+ interruptoverride_is_level_triggered ⟨src, gsi, pol, tm⟩
+ = ok (flags.val.testBit 2 && flags.val.testBit 3) := by
+ unfold interruptoverride_is_level_triggered interrupt.InterruptOverride.is_level_triggered
+ have h2 : flags.val.testBit 2 = decide (flags.val / 4 % 2 = 1) := by
+ simp [Nat.testBit, Nat.shiftRight_eq_div_pow, Nat.one_and_eq_mod_two]; rfl
+ have h3 : flags.val.testBit 3 = decide (flags.val / 8 % 2 = 1) := by
+ simp [Nat.testBit, Nat.shiftRight_eq_div_pow, Nat.one_and_eq_mod_two]; rfl
+ rw [h2, h3]
+ congr 1
+ by_cases h : tm.val = 3
+ · have : tm = 3#u8 := UScalar.eq_of_val_eq (by rw [h]; rfl)
+ rw [this]; simp; omega
+ · have : tm ≠ 3#u8 := by
+ intro he; exact h (by rw [he]; rfl)
+ simp only [this, decide_false]
+ symm; simp; omega
+
+/-- Only the `11b` encoding of either field is honoured: conforming (`0`), the
+ opposite sense (`1`) and reserved (`2`) all read as false, for polarity and
+ for trigger alike. -/
+theorem only_the_eleven_encoding_is_active_low_or_level
+ (src : Std.U8) (gsi : Std.U32) (v : Std.U8) (hv : v.val ≤ 2) :
+ interruptoverride_is_active_low ⟨src, gsi, v, v⟩ = ok false ∧
+ interruptoverride_is_level_triggered ⟨src, gsi, v, v⟩ = ok false ∧
+ interruptoverride_is_active_low ⟨src, gsi, 3#u8, v⟩ = ok true ∧
+ interruptoverride_is_level_triggered ⟨src, gsi, v, 3#u8⟩ = ok true := by
+ have hne : v ≠ 3#u8 := by
+ intro he; have := congrArg UScalar.val he; simp at this; omega
+ simp [interruptoverride_is_active_low, interrupt.InterruptOverride.is_active_low,
+ interruptoverride_is_level_triggered, interrupt.InterruptOverride.is_level_triggered, hne]
+
+/-- An NMI entry built the way `parse_local_apic_nmi` builds it from a legacy
+ 8-bit processor id (`0xFF` widened to `u32::MAX`, anything else zero
+ extended) applies to all processors exactly when the id is `0xFF`, which is
+ `MadtLocalApicNmi::ALL_PROCESSORS`. -/
+theorem nmiconfig_applies_to_all_agrees_with_the_legacy_broadcast_id
+ (id lint : Std.U8) (flags : Std.U16) :
+ nmiconfig_applies_to_all
+ ⟨if id = 255#u8 then core.num.U32.MAX else UScalar.cast .U32 id, lint, flags⟩
+ = ok (decide (id.val = 255)) := by
+ unfold nmiconfig_applies_to_all interrupt.NmiConfig.applies_to_all
+ have hmax : core.num.U32.MAX.val = 4294967295 := by rfl
+ by_cases h : id = 255#u8
+ · have : id.val = 255 := by rw [h]; rfl
+ simp [h]
+ · have hlt : id.val < 255 := by
+ have := id.hBounds
+ have : id.val ≠ 255 := fun hv => h (UScalar.eq_of_val_eq (by rw [hv]; rfl))
+ simp at *; omega
+ have hne : UScalar.cast .U32 id ≠ core.num.U32.MAX := by
+ intro he
+ have := congrArg UScalar.val he
+ rw [UScalar.cast_val_eq, hmax] at this
+ simp at this
+ omega
+ simp only [h, if_false, hne, decide_false]
+ congr 1; symm; simp; omega
+
+/-- An NMI entry carrying a 32-bit x2APIC uid applies to all processors exactly
+ when the uid is `0xFFFFFFFF`, which is `MadtLocalX2ApicNmi::ALL_PROCESSORS`;
+ the legacy value `0xFF` in particular names processor 255 and nothing more. -/
+theorem nmiconfig_applies_to_all_agrees_with_the_x2apic_broadcast_uid
+ (uid : Std.U32) (lint : Std.U8) (flags : Std.U16) :
+ nmiconfig_applies_to_all ⟨uid, lint, flags⟩ = ok (decide (uid.val = 4294967295)) := by
+ unfold nmiconfig_applies_to_all interrupt.NmiConfig.applies_to_all
+ congr 1
+ by_cases h : uid.val = 4294967295
+ · have : uid = core.num.U32.MAX := UScalar.eq_of_val_eq (by rw [h]; rfl)
+ rw [decide_eq_true this, decide_eq_true h]
+ · have : uid ≠ core.num.U32.MAX := by
+ intro he; exact h (by rw [he]; rfl)
+ simp [this, h]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DataInterrupt.the_interruptoverride_is_active_low_wrapper_is_its_method
#print axioms NonosExtraction.DataInterrupt.the_interruptoverride_is_level_triggered_wrapper_is_its_method
#print axioms NonosExtraction.DataInterrupt.the_nmiconfig_applies_to_all_wrapper_is_its_method
+#print axioms NonosExtraction.DataInterrupt.interruptoverride_is_active_low_reads_both_polarity_bits
+#print axioms NonosExtraction.DataInterrupt.interruptoverride_is_level_triggered_reads_both_trigger_bits
+#print axioms NonosExtraction.DataInterrupt.only_the_eleven_encoding_is_active_low_or_level
+#print axioms NonosExtraction.DataInterrupt.nmiconfig_applies_to_all_agrees_with_the_legacy_broadcast_id
+#print axioms NonosExtraction.DataInterrupt.nmiconfig_applies_to_all_agrees_with_the_x2apic_broadcast_uid
end NonosExtraction.DataInterrupt
diff --git a/verification/extraction/lean/NonosExtraction/DataIoapic.lean b/verification/extraction/lean/NonosExtraction/DataIoapic.lean
index 5aeff77c44..107ff89f95 100644
--- a/verification/extraction/lean/NonosExtraction/DataIoapic.lean
+++ b/verification/extraction/lean/NonosExtraction/DataIoapic.lean
@@ -23,10 +23,10 @@ structure ioapic.IoApicInfo where
gsi_base : Std.U32
/-- [nonos_x_data_ioapic::ioapic::{nonos_x_data_ioapic::ioapic::IoApicInfo}::gsi_max]:
- Source: 'src/../../../../../src/arch/x86_64/acpi/data/ioapic.rs', lines 25:4-27:5
+ Source: 'src/../../../../../src/arch/x86_64/acpi/data/ioapic.rs', lines 26:4-28:5
Visibility: public -/
def ioapic.IoApicInfo.gsi_max (self : ioapic.IoApicInfo) : Result Std.U32 := do
- self.gsi_base + 23#u32
+ ok (core.num.U32.saturating_add self.gsi_base 23#u32)
/-- [nonos_x_data_ioapic::ioapicinfo_gsi_max]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/DataIoapicRefinement.lean b/verification/extraction/lean/NonosExtraction/DataIoapicRefinement.lean
index 834fdf0795..cf46604b50 100644
--- a/verification/extraction/lean/NonosExtraction/DataIoapicRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DataIoapicRefinement.lean
@@ -35,8 +35,48 @@ namespace NonosExtraction.DataIoapic
theorem the_ioapicinfo_gsi_max_wrapper_is_its_method (a : ioapic.IoApicInfo) :
ioapicinfo_gsi_max a = ioapic.IoApicInfo.gsi_max a := rfl
+/-! ### The last interrupt line of an I/O APIC
+
+ `gsi_max` names the last global system interrupt of a chip as its base plus
+ twenty three, which assumes the twenty four redirection entries of the
+ common part. The base is copied from the firmware's MADT entry unchecked,
+ and the sum used to overflow for a base in the top twenty three values of a
+ `u32`, which halted the kernel with overflow checks on. It now saturates at
+ `u32::MAX`: these theorems say it never fails, returns the base plus twenty
+ three whenever that fits and `u32::MAX` otherwise. They cannot say that twenty four is the
+ chip's real entry count: the interrupt tier reads that from the version
+ register instead, and no caller of `gsi_max` exists under `src/` today. -/
+
+/-- `gsi_max` never fails and is the base plus twenty three, saturated at
+ `u32::MAX`. -/
+theorem ioapicinfo_gsi_max_is_the_base_plus_twenty_three_saturated (i : ioapic.IoApicInfo) :
+ ∃ r, ioapicinfo_gsi_max i = ok r ∧ r.val = min (2 ^ 32 - 1) (i.gsi_base.val + 23) := by
+ unfold ioapicinfo_gsi_max ioapic.IoApicInfo.gsi_max
+ refine ⟨_, rfl, ?_⟩
+ simp only [core.num.U32.saturating_add, UScalar.saturating_add, UScalar.val, UScalar.max]
+ rw [BitVec.toNat_ofNat]
+ show min (2 ^ 32 - 1) _ % 2 ^ 32 = _
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- A base up to `0xFFFFFFE8` gives the line twenty three above it. -/
+theorem ioapicinfo_gsi_max_is_the_base_plus_twenty_three (i : ioapic.IoApicInfo)
+ (h : i.gsi_base.val ≤ 0xFFFFFFE8) :
+ ∃ r, ioapicinfo_gsi_max i = ok r ∧ r.val = i.gsi_base.val + 23 := by
+ obtain ⟨r, hr, hv⟩ := ioapicinfo_gsi_max_is_the_base_plus_twenty_three_saturated i
+ exact ⟨r, hr, by omega⟩
+
+/-- A base above `0xFFFFFFE8` gives `u32::MAX` rather than halting. -/
+theorem ioapicinfo_gsi_max_saturates_on_a_base_in_the_top_twenty_three
+ (i : ioapic.IoApicInfo) (h : 0xFFFFFFE8 < i.gsi_base.val) :
+ ∃ r, ioapicinfo_gsi_max i = ok r ∧ r.val = 2 ^ 32 - 1 := by
+ obtain ⟨r, hr, hv⟩ := ioapicinfo_gsi_max_is_the_base_plus_twenty_three_saturated i
+ exact ⟨r, hr, by omega⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DataIoapic.the_ioapicinfo_gsi_max_wrapper_is_its_method
+#print axioms NonosExtraction.DataIoapic.ioapicinfo_gsi_max_is_the_base_plus_twenty_three_saturated
+#print axioms NonosExtraction.DataIoapic.ioapicinfo_gsi_max_is_the_base_plus_twenty_three
+#print axioms NonosExtraction.DataIoapic.ioapicinfo_gsi_max_saturates_on_a_base_in_the_top_twenty_three
end NonosExtraction.DataIoapic
diff --git a/verification/extraction/lean/NonosExtraction/DataProcessorRefinement.lean b/verification/extraction/lean/NonosExtraction/DataProcessorRefinement.lean
index 7f08c653ee..ed5f4f19b5 100644
--- a/verification/extraction/lean/NonosExtraction/DataProcessorRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DataProcessorRefinement.lean
@@ -35,8 +35,28 @@ namespace NonosExtraction.DataProcessor
theorem the_processorinfo_new_wrapper_is_its_method (a : Std.U32) (b : Std.U32) (c : Bool) (d : Bool) :
processorinfo_new a b c d = processor.ProcessorInfo.new a b c d := rfl
+/-! ### A processor record keeps its identifiers apart
+
+ The MADT parsers build one `ProcessorInfo` per usable local APIC entry,
+ passing the APIC id first and the ACPI processor UID second, and the SRAT
+ parser later finds the record by its APIC id to fill in the proximity
+ domain. This theorem says that each argument lands in its own field, that
+ the two identifiers are not swapped, and that the proximity domain starts at
+ zero until SRAT sets it. It cannot say anything about the parsers, which are
+ not extracted. -/
+
+/-- Each argument is stored in the field of the same name, and the proximity
+ domain is zero. -/
+theorem processorinfo_new_stores_each_identifier_in_its_own_field
+ (apic uid : Std.U32) (x2 en : Bool) :
+ ∃ p, processorinfo_new apic uid x2 en = ok p ∧
+ p.apic_id = apic ∧ p.processor_uid = uid ∧ p.proximity_domain.val = 0 ∧
+ p.is_x2apic = x2 ∧ p.enabled = en :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DataProcessor.the_processorinfo_new_wrapper_is_its_method
+#print axioms NonosExtraction.DataProcessor.processorinfo_new_stores_each_identifier_in_its_own_field
end NonosExtraction.DataProcessor
diff --git a/verification/extraction/lean/NonosExtraction/DataStatsRefinement.lean b/verification/extraction/lean/NonosExtraction/DataStatsRefinement.lean
index a74dfa5ff5..2e3cff4b3c 100644
--- a/verification/extraction/lean/NonosExtraction/DataStatsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DataStatsRefinement.lean
@@ -35,8 +35,25 @@ namespace NonosExtraction.DataStats
theorem the_acpistats_new_wrapper_is_its_method :
acpistats_new = stats.AcpiStats.new := rfl
+/-! ### The ACPI statistics start from zero
+
+ The parser's `STATS` static is initialised with `AcpiStats::new()` and every
+ field is then only incremented as tables are found. This theorem says that
+ each of the seven counters starts at zero, so each later reading is a count
+ of events rather than an offset from some initial value. It cannot say
+ anything about the static or the lock around it, which Aeneas leaves opaque. -/
+
+/-- Every counter of a new `AcpiStats` is zero. -/
+theorem acpistats_new_starts_every_counter_at_zero :
+ ∃ s, acpistats_new = ok s ∧
+ s.tables_found.val = 0 ∧ s.processors_found.val = 0 ∧ s.ioapics_found.val = 0 ∧
+ s.overrides_found.val = 0 ∧ s.numa_nodes.val = 0 ∧ s.pcie_segments.val = 0 ∧
+ s.parse_errors.val = 0 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DataStats.the_acpistats_new_wrapper_is_its_method
+#print axioms NonosExtraction.DataStats.acpistats_new_starts_every_counter_at_zero
end NonosExtraction.DataStats
diff --git a/verification/extraction/lean/NonosExtraction/DiagCplRefinement.lean b/verification/extraction/lean/NonosExtraction/DiagCplRefinement.lean
index 16797d8300..16a2abdc49 100644
--- a/verification/extraction/lean/NonosExtraction/DiagCplRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DiagCplRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.DiagCpl
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_diag_cpl
@@ -35,8 +36,47 @@ namespace NonosExtraction.DiagCpl
theorem the_cpl_from_cs_wrapper_is_its_method (a : Std.U64) :
cpl_from_cs a = cpl.cpl_from_cs a := rfl
+/-! ### The privilege level read from a code selector
+
+ `cpl_from_cs` keeps the requested privilege level, the low two bits of the
+ saved CS selector, and discards the descriptor index and table indicator
+ above them. The theorems below say it reads exactly those two bits, that it
+ never exceeds 3, and so that `dump_trap`'s `b'0' + cpl` is always one of the
+ digits `0` to `3` and cannot overflow a byte. They cannot establish that the
+ frame `dump_trap` receives carries the selector the processor pushed; that
+ function and the interrupt frame type are not extracted here.
+-/
+
+/-- The level is the selector modulo 4: the RPL field and nothing else. -/
+theorem cpl_from_cs_is_the_low_two_bits (cs : Std.U64) :
+ ∃ l : Std.U8, cpl_from_cs cs = ok l ∧ l.val = cs.val % 4 := by
+ unfold cpl_from_cs cpl.cpl_from_cs
+ simp only [lift, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ have h : (cs &&& 3#u64).val = cs.val % 4 := Bits.land_low_mask cs 3#u64 2 rfl
+ rw [UScalar.cast_val_eq, h]
+ simp only [UScalarTy.numBits]
+ omega
+
+/-- The digit `dump_trap` prints is `'0' + cpl`, which stays within `'0'..'3'`,
+ so the byte addition in the caller never overflows. -/
+theorem cpl_from_cs_prints_as_one_ascii_digit (cs : Std.U64) :
+ ∃ l : Std.U8, cpl_from_cs cs = ok l ∧ l.val ≤ 3 ∧ 48 + l.val < 256 := by
+ obtain ⟨l, hl, hv⟩ := cpl_from_cs_is_the_low_two_bits cs
+ exact ⟨l, hl, by omega, by omega⟩
+
+/-- A kernel selector (0x08) is ring 0 and the usual user code selector (0x23,
+ GDT index 4 with RPL 3) is ring 3: the index bits do not leak into the level. -/
+theorem cpl_from_cs_on_kernel_and_user_selectors :
+ cpl_from_cs 8#u64 = ok 0#u8 ∧ cpl_from_cs 35#u64 = ok 3#u8 ∧
+ cpl_from_cs 43#u64 = ok 3#u8 := by
+ exact ⟨rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DiagCpl.the_cpl_from_cs_wrapper_is_its_method
+#print axioms NonosExtraction.DiagCpl.cpl_from_cs_is_the_low_two_bits
+#print axioms NonosExtraction.DiagCpl.cpl_from_cs_prints_as_one_ascii_digit
+#print axioms NonosExtraction.DiagCpl.cpl_from_cs_on_kernel_and_user_selectors
end NonosExtraction.DiagCpl
diff --git a/verification/extraction/lean/NonosExtraction/DispatchArgsRefinement.lean b/verification/extraction/lean/NonosExtraction/DispatchArgsRefinement.lean
index b3f3d80153..d9f52db1e2 100644
--- a/verification/extraction/lean/NonosExtraction/DispatchArgsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DispatchArgsRefinement.lean
@@ -35,8 +35,41 @@ namespace NonosExtraction.DispatchArgs
theorem the_args_new_wrapper_is_its_method (a : Std.U64) (b : Std.U64) (c : Std.U64) (d : Std.U64) (e : Std.U64) (f : Std.U64) :
args_new a b c d e f = args.Args.new a b c d e f := rfl
+/-! ### Each syscall argument lands in its own slot
+
+`dispatch_microkernel_syscall` hands its six raw argument registers to
+`Args::new` in order and routes the result to every syscall handler, which
+reads `args.a0` through `args.a5` by position. The theorems below show that
+`args_new` never fails, puts the argument at position `i` in field `ai` (no
+two are swapped or duplicated), and so loses no argument: two calls that
+build the same `Args` were given the same six values. They say nothing about
+the register-to-argument mapping of the architecture entry paths, which are
+not extracted.
+-/
+
+/-- `args_new` puts each argument in the field of the same position. A version
+ that swapped two registers would hand a handler its length as its
+ pointer. -/
+theorem args_new_keeps_every_argument_in_its_slot
+ (a0 a1 a2 a3 a4 a5 : Std.U64) :
+ ∃ r, args_new a0 a1 a2 a3 a4 a5 = ok r ∧
+ r.a0 = a0 ∧ r.a1 = a1 ∧ r.a2 = a2 ∧ r.a3 = a3 ∧ r.a4 = a4 ∧ r.a5 = a5 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- `args_new` loses no argument: equal results come only from equal inputs.
+ A version that filled one field from a neighbouring argument would map two
+ different register sets to the same `Args`. -/
+theorem args_new_loses_no_argument
+ (a0 a1 a2 a3 a4 a5 b0 b1 b2 b3 b4 b5 : Std.U64)
+ (h : args_new a0 a1 a2 a3 a4 a5 = args_new b0 b1 b2 b3 b4 b5) :
+ a0 = b0 ∧ a1 = b1 ∧ a2 = b2 ∧ a3 = b3 ∧ a4 = b4 ∧ a5 = b5 := by
+ simp only [args_new, args.Args.new, ok.injEq, args.Args.mk.injEq] at h
+ exact h
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DispatchArgs.the_args_new_wrapper_is_its_method
+#print axioms NonosExtraction.DispatchArgs.args_new_keeps_every_argument_in_its_slot
+#print axioms NonosExtraction.DispatchArgs.args_new_loses_no_argument
end NonosExtraction.DispatchArgs
diff --git a/verification/extraction/lean/NonosExtraction/DistributorDeviceRefinement.lean b/verification/extraction/lean/NonosExtraction/DistributorDeviceRefinement.lean
index 799b9bc5b2..415908400a 100644
--- a/verification/extraction/lean/NonosExtraction/DistributorDeviceRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/DistributorDeviceRefinement.lean
@@ -35,8 +35,29 @@ namespace NonosExtraction.DistributorDevice
theorem the_gicdistributor_new_wrapper_is_its_method (a : Std.U64) :
gicdistributor_new a = device.GicDistributor.new a := rfl
+/-! ### The distributor frame is where the firmware put it
+
+ `new` records the base address it is given without masking, rounding or
+ offsetting it, so every register access the driver makes is relative to the
+ address the device tree or ACPI table reported. The theorems cannot say that
+ the address is a valid, mapped distributor frame; that comes from the firmware
+ tables and the MMIO mapping, neither of which is extracted.
+-/
+
+/-- The constructed distributor keeps the base exactly as given. -/
+theorem gicdistributor_new_keeps_the_firmware_base (b : Std.U64) :
+ ∃ d, gicdistributor_new b = ok d ∧ d.base = b := ⟨_, rfl, rfl⟩
+
+/-- Different bases give different devices, so two frames are never collapsed
+ onto one. -/
+theorem gicdistributor_new_tells_bases_apart (a b : Std.U64) (h : gicdistributor_new a = gicdistributor_new b) : a = b := by
+ simpa [gicdistributor_new, device.GicDistributor.new] using h
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.DistributorDevice.the_gicdistributor_new_wrapper_is_its_method
+#print axioms NonosExtraction.DistributorDevice.gicdistributor_new_keeps_the_firmware_base
+#print axioms NonosExtraction.DistributorDevice.gicdistributor_new_tells_bases_apart
+
end NonosExtraction.DistributorDevice
diff --git a/verification/extraction/lean/NonosExtraction/DriversPciTypesAddress.lean b/verification/extraction/lean/NonosExtraction/DriversPciTypesAddress.lean
index 4cfc35888e..0ceaec3960 100644
--- a/verification/extraction/lean/NonosExtraction/DriversPciTypesAddress.lean
+++ b/verification/extraction/lean/NonosExtraction/DriversPciTypesAddress.lean
@@ -25,15 +25,17 @@ def drivers.pci.constants.address_packing.pci_config_address
let i1 ← lift (UScalar.cast .U32 bus)
let i2 ← i1 <<< 16#i32
let i3 ← lift (i ||| i2)
- let i4 ← lift (UScalar.cast .U32 device)
- let i5 ← i4 <<< 11#i32
- let i6 ← lift (i3 ||| i5)
- let i7 ← lift (UScalar.cast .U32 function)
- let i8 ← i7 <<< 8#i32
- let i9 ← lift (i6 ||| i8)
- let i10 ← lift (UScalar.cast .U32 offset)
- let i11 ← lift (i10 &&& 252#u32)
- ok (i9 ||| i11)
+ let i4 ← lift (device &&& 31#u8)
+ let i5 ← lift (UScalar.cast .U32 i4)
+ let i6 ← i5 <<< 11#i32
+ let i7 ← lift (i3 ||| i6)
+ let i8 ← lift (function &&& 7#u8)
+ let i9 ← lift (UScalar.cast .U32 i8)
+ let i10 ← i9 <<< 8#i32
+ let i11 ← lift (i7 ||| i10)
+ let i12 ← lift (UScalar.cast .U32 offset)
+ let i13 ← lift (i12 &&& 252#u32)
+ ok (i11 ||| i13)
/-- [nonos_x_drivers_pci_types_address::drivers::pci::types::address::PciAddress]
Source: 'src/drivers/pci/types/../../../../../../../../src/drivers/pci/types/address.rs', lines 21:0-25:1
diff --git a/verification/extraction/lean/NonosExtraction/ErrorFaultInfoRefinement.lean b/verification/extraction/lean/NonosExtraction/ErrorFaultInfoRefinement.lean
index 8e23a1be22..3f0448e3d5 100644
--- a/verification/extraction/lean/NonosExtraction/ErrorFaultInfoRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ErrorFaultInfoRefinement.lean
@@ -21,6 +21,8 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.ErrorFaultInfo
+import NonosExtraction.Bits
+import Nonos.DemandPaging
open Aeneas Aeneas.Std Result
open nonos_x_error_fault_info
@@ -41,10 +43,90 @@ theorem the_pagefaultinfo_is_cow_fault_wrapper_is_its_method (a : fault_info.Pag
theorem the_pagefaultinfo_is_demand_fault_wrapper_is_its_method (a : fault_info.PageFaultInfo) :
pagefaultinfo_is_demand_fault a = fault_info.PageFaultInfo.is_demand_fault a := rfl
+/-! ### Decoding the hardware error code, and agreeing with the dispatch model
+
+ `from_fault` keeps the faulting address and the raw error code, and sets each
+ flag from exactly one bit of the code: present from bit 0 (PF_PRESENT), write
+ from bit 1 (PF_WRITE), user from bit 2 (PF_USER), and instruction fetch from
+ bit 4 (PF_INSTRUCTION); bit 3 (reserved write) is read by none of them. The
+ two classifiers then agree with `Nonos.DemandPaging.route`, the tier-one model
+ of the dispatch in faults/handler.rs: a decoded fault is a copy-on-write fault
+ exactly when the model routes it to copy-on-write, and a demand fault exactly
+ when the model routes it to the demand path, so the two are never both true.
+ These theorems cannot establish that the handler actually consults these
+ classifiers, nor that the error code it passes is the one the processor
+ pushed: the interrupt entry and the handler are not extracted.
+-/
+
+/-- Each flag is one bit of the error code, and the address and code are kept. -/
+theorem pagefaultinfo_from_fault_reads_one_bit_per_flag (a e : Std.U64) :
+ pagefaultinfo_from_fault a e = ok
+ { address := a, error_code := e,
+ is_write := e.val.testBit 1, is_user := e.val.testBit 2,
+ is_instruction_fetch := e.val.testBit 4,
+ page_was_present := e.val.testBit 0 } := by
+ unfold pagefaultinfo_from_fault fault_info.PageFaultInfo.from_fault
+ simp only [lift, bind_tc_ok]
+ rw [Bits.reads_bit e 2#u64 0#u64 1 rfl rfl, Bits.reads_bit e 4#u64 0#u64 2 rfl rfl,
+ Bits.reads_bit e 16#u64 0#u64 4 rfl rfl, Bits.reads_bit e 1#u64 0#u64 0 rfl rfl]
+
+/-- The fault the dispatch model sees for a decoded error code. -/
+def modelFault (a e : Std.U64) : Nonos.DemandPaging.Fault :=
+ ⟨a.val, e.val.testBit 0, e.val.testBit 1⟩
+
+/-- A decoded fault is a copy-on-write fault exactly when the dispatch model
+ routes it to copy-on-write: a write (bit 1) on a present page (bit 0). -/
+theorem pagefaultinfo_is_cow_fault_agrees_with_the_dispatch_model (a e : Std.U64) :
+ (do let i ← pagefaultinfo_from_fault a e; pagefaultinfo_is_cow_fault i) = ok true ↔
+ Nonos.DemandPaging.route (modelFault a e) = .cow := by
+ rw [pagefaultinfo_from_fault_reads_one_bit_per_flag]
+ simp only [bind_tc_ok, pagefaultinfo_is_cow_fault, fault_info.PageFaultInfo.is_cow_fault,
+ modelFault, Nonos.DemandPaging.route]
+ by_cases h0 : e.val.testBit 0 = true <;> by_cases h1 : e.val.testBit 1 = true <;> simp [h0, h1]
+
+/-- A decoded fault is a demand fault exactly when the dispatch model routes it
+ to the demand path: the page was not present (bit 0 clear), whatever the
+ other bits say. -/
+theorem pagefaultinfo_is_demand_fault_agrees_with_the_dispatch_model (a e : Std.U64) :
+ (do let i ← pagefaultinfo_from_fault a e; pagefaultinfo_is_demand_fault i) = ok true ↔
+ Nonos.DemandPaging.route (modelFault a e) = .demand := by
+ rw [pagefaultinfo_from_fault_reads_one_bit_per_flag]
+ simp only [bind_tc_ok, pagefaultinfo_is_demand_fault,
+ fault_info.PageFaultInfo.is_demand_fault, modelFault, Nonos.DemandPaging.route]
+ by_cases h0 : e.val.testBit 0 = true <;> by_cases h1 : e.val.testBit 1 = true <;> simp [h0, h1]
+
+/-- No fault record, decoded or not, is both a copy-on-write fault and a demand
+ fault, so the handler can never send one fault down both paths. -/
+theorem pagefaultinfo_is_cow_fault_and_pagefaultinfo_is_demand_fault_exclude
+ (i : fault_info.PageFaultInfo) :
+ ¬ (pagefaultinfo_is_cow_fault i = ok true ∧ pagefaultinfo_is_demand_fault i = ok true) := by
+ unfold pagefaultinfo_is_cow_fault fault_info.PageFaultInfo.is_cow_fault
+ pagefaultinfo_is_demand_fault fault_info.PageFaultInfo.is_demand_fault
+ cases i.page_was_present <;> simp
+
+/-- A write to a present page decoded from the real code (0x3, and 0x7 from
+ user mode) is copy-on-write, while a user write to a missing page (0x6) is a
+ demand fault; the instruction-fetch bit alone (0x10) is a demand fault. -/
+theorem pagefaultinfo_from_fault_classifies_typical_codes (a : Std.U64) :
+ (do let i ← pagefaultinfo_from_fault a 3#u64; pagefaultinfo_is_cow_fault i) = ok true ∧
+ (do let i ← pagefaultinfo_from_fault a 7#u64; pagefaultinfo_is_cow_fault i) = ok true ∧
+ (do let i ← pagefaultinfo_from_fault a 6#u64; pagefaultinfo_is_demand_fault i) = ok true ∧
+ (do let i ← pagefaultinfo_from_fault a 6#u64; pagefaultinfo_is_cow_fault i) = ok false ∧
+ (do let i ← pagefaultinfo_from_fault a 16#u64; pagefaultinfo_is_demand_fault i) = ok true := by
+ simp only [pagefaultinfo_from_fault_reads_one_bit_per_flag, bind_tc_ok,
+ pagefaultinfo_is_cow_fault, fault_info.PageFaultInfo.is_cow_fault,
+ pagefaultinfo_is_demand_fault, fault_info.PageFaultInfo.is_demand_fault]
+ refine ⟨?_, ?_, ?_, ?_, ?_⟩ <;> rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ErrorFaultInfo.the_pagefaultinfo_from_fault_wrapper_is_its_method
#print axioms NonosExtraction.ErrorFaultInfo.the_pagefaultinfo_is_cow_fault_wrapper_is_its_method
#print axioms NonosExtraction.ErrorFaultInfo.the_pagefaultinfo_is_demand_fault_wrapper_is_its_method
+#print axioms NonosExtraction.ErrorFaultInfo.pagefaultinfo_from_fault_reads_one_bit_per_flag
+#print axioms NonosExtraction.ErrorFaultInfo.pagefaultinfo_is_cow_fault_agrees_with_the_dispatch_model
+#print axioms NonosExtraction.ErrorFaultInfo.pagefaultinfo_is_demand_fault_agrees_with_the_dispatch_model
+#print axioms NonosExtraction.ErrorFaultInfo.pagefaultinfo_is_cow_fault_and_pagefaultinfo_is_demand_fault_exclude
+#print axioms NonosExtraction.ErrorFaultInfo.pagefaultinfo_from_fault_classifies_typical_codes
end NonosExtraction.ErrorFaultInfo
diff --git a/verification/extraction/lean/NonosExtraction/FadtProfileRefinement.lean b/verification/extraction/lean/NonosExtraction/FadtProfileRefinement.lean
index 187235c6e2..29fceb791b 100644
--- a/verification/extraction/lean/NonosExtraction/FadtProfileRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/FadtProfileRefinement.lean
@@ -41,10 +41,94 @@ theorem the_pmprofile_is_server_wrapper_is_its_method (a : profile.PmProfile) :
theorem the_pmprofile_is_mobile_wrapper_is_its_method (a : profile.PmProfile) :
pmprofile_is_mobile a = profile.PmProfile.is_mobile a := rfl
+/-! ### Decoding the FADT preferred power management profile
+
+`Fadt::pm_profile` hands the raw `preferred_pm_profile` byte to `from_u8`, and the
+kernel's `power_query::is_server` and `is_mobile` ask the decoded profile. The
+theorems below establish that `from_u8` inverts the `repr(u8)` discriminants the
+enum declares (the ACPI numbering), that every byte past 8 (reserved by ACPI) reads
+as `Unspecified`, that no profile is both a server and a mobile system, and, going
+from the raw byte, exactly which bytes make the kernel report a server (4, 5 and 7)
+and which a mobile system (2 and 8).
+
+They cannot establish that the firmware's byte is truthful, and they do not reach
+the parser's table lookup or the `unwrap_or(false)` in `power_query`, which are not
+extracted; a missing FADT and a reserved byte both end up reported as neither.
+-/
+
+/-- Every byte a profile's own discriminant can take decodes back to that profile:
+`from_u8` agrees with the numbering the `repr(u8)` enum declares. -/
+theorem pmprofile_from_u8_inverts_the_declared_discriminants (p : profile.PmProfile) :
+ pmprofile_from_u8 (read_discriminant p) = ok p := by
+ cases p <;> rfl
+
+/-- Bytes 9 to 255 are reserved by ACPI and decode as `Unspecified`, the same as 0. -/
+theorem pmprofile_from_u8_reads_a_reserved_byte_as_unspecified (v : Std.U8)
+ (h : 8 < v.val) : pmprofile_from_u8 v = ok profile.PmProfile.Unspecified := by
+ unfold pmprofile_from_u8 profile.PmProfile.from_u8
+ split <;> first | rfl | exact absurd h (by decide)
+
+private theorem small_byte_cases (v : Std.U8) (h : v.val ≤ 8) :
+ v = 0#u8 ∨ v = 1#u8 ∨ v = 2#u8 ∨ v = 3#u8 ∨ v = 4#u8 ∨ v = 5#u8 ∨ v = 6#u8 ∨
+ v = 7#u8 ∨ v = 8#u8 := by
+ have : v.val = 0 ∨ v.val = 1 ∨ v.val = 2 ∨ v.val = 3 ∨ v.val = 4 ∨ v.val = 5 ∨
+ v.val = 6 ∨ v.val = 7 ∨ v.val = 8 := by omega
+ rcases this with e | e | e | e | e | e | e | e | e
+ · exact Or.inl (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inl (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inl (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inl (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inl (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inl
+ (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inl
+ (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inl
+ (UScalar.eq_of_val_eq (by rw [e]; rfl))
+ · exact Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr <| Or.inr
+ (UScalar.eq_of_val_eq (by rw [e]; rfl))
+
+/-- No decoded profile is both a server and a mobile system, so the kernel never
+reports a machine as both. -/
+theorem pmprofile_is_server_and_pmprofile_is_mobile_are_exclusive (p : profile.PmProfile) :
+ ¬ (pmprofile_is_server p = ok true ∧ pmprofile_is_mobile p = ok true) := by
+ cases p <;> simp [pmprofile_is_server, pmprofile_is_mobile, profile.PmProfile.is_server,
+ profile.PmProfile.is_mobile]
+
+/-- Read from the raw FADT byte, the kernel reports a server for exactly the ACPI
+values 4 (enterprise server), 5 (SOHO server) and 7 (performance server). The
+appliance PC at 6 and every reserved byte are not servers. -/
+theorem a_fadt_byte_makes_pmprofile_is_server_true_exactly_at_4_5_and_7 (v : Std.U8) :
+ (do let p ← pmprofile_from_u8 v; pmprofile_is_server p) =
+ ok (decide (v.val = 4 ∨ v.val = 5 ∨ v.val = 7)) := by
+ by_cases h : 8 < v.val
+ · rw [pmprofile_from_u8_reads_a_reserved_byte_as_unspecified v h, bind_tc_ok]
+ have : ¬ (v.val = 4 ∨ v.val = 5 ∨ v.val = 7) := by omega
+ simp only [this, decide_false]; rfl
+ · rcases small_byte_cases v (by omega) with e | e | e | e | e | e | e | e | e <;>
+ (subst e; rfl)
+
+/-- Read from the raw FADT byte, the kernel reports a mobile system for exactly the
+ACPI values 2 (mobile) and 8 (tablet). -/
+theorem a_fadt_byte_makes_pmprofile_is_mobile_true_exactly_at_2_and_8 (v : Std.U8) :
+ (do let p ← pmprofile_from_u8 v; pmprofile_is_mobile p) =
+ ok (decide (v.val = 2 ∨ v.val = 8)) := by
+ by_cases h : 8 < v.val
+ · rw [pmprofile_from_u8_reads_a_reserved_byte_as_unspecified v h, bind_tc_ok]
+ have : ¬ (v.val = 2 ∨ v.val = 8) := by omega
+ simp only [this, decide_false]; rfl
+ · rcases small_byte_cases v (by omega) with e | e | e | e | e | e | e | e | e <;>
+ (subst e; rfl)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.FadtProfile.the_pmprofile_from_u8_wrapper_is_its_method
#print axioms NonosExtraction.FadtProfile.the_pmprofile_is_server_wrapper_is_its_method
#print axioms NonosExtraction.FadtProfile.the_pmprofile_is_mobile_wrapper_is_its_method
+#print axioms NonosExtraction.FadtProfile.pmprofile_from_u8_inverts_the_declared_discriminants
+#print axioms NonosExtraction.FadtProfile.pmprofile_from_u8_reads_a_reserved_byte_as_unspecified
+#print axioms NonosExtraction.FadtProfile.pmprofile_is_server_and_pmprofile_is_mobile_are_exclusive
+#print axioms NonosExtraction.FadtProfile.a_fadt_byte_makes_pmprofile_is_server_true_exactly_at_4_5_and_7
+#print axioms NonosExtraction.FadtProfile.a_fadt_byte_makes_pmprofile_is_mobile_true_exactly_at_2_and_8
end NonosExtraction.FadtProfile
diff --git a/verification/extraction/lean/NonosExtraction/FieldTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/FieldTypesRefinement.lean
index 2831c57d55..48bee626bf 100644
--- a/verification/extraction/lean/NonosExtraction/FieldTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/FieldTypesRefinement.lean
@@ -38,9 +38,78 @@ theorem the_fe_zero_wrapper_is_its_method :
theorem the_fe_one_wrapper_is_its_method :
fe_one = types.Fe.one := rfl
+/-! ### Zero and one are the field's zero and one
+
+A field element is ten signed limbs in radix 2^25.5: limb `i` carries weight
+`2^w` where `w` runs 0, 26, 51, 77, 102, 128, 153, 179, 204, 230, the alternation
+of twenty-six and twenty-five bit limbs that the carry chains in `fe_mul`,
+`fe_sq` and `fe_tobytes` shift by. `feValue` below is that reading of the limbs as
+an integer, written here from the carry chains rather than from `Fe::zero` or
+`Fe::one`. The theorems show that `fe_zero` and `fe_one` produce exactly the limbs
+expected, that under the radix reading they denote 0 and 1 (so the identity point
+built from them, `(0, 1, 1, 0)`, is the identity), and that neither can fail.
+
+They cannot establish anything about arithmetic on these elements: `fe_add`,
+`fe_mul` and the rest are not in this extraction, so that 0 and 1 are neutral for
+them is a consequence of the representation, not something proven here.
+-/
+
+/-- The bit position of each limb: twenty-six and twenty-five bits in turn. -/
+def limbWeights : List Nat := [0, 26, 51, 77, 102, 128, 153, 179, 204, 230]
+
+/-- The integer a field element denotes, before reduction modulo `2^255 - 19`. -/
+def feValue (f : types.Fe) : Int :=
+ ((f.val.zip limbWeights).map fun p => p.1.val * (2 : Int) ^ p.2).sum
+
+/-- `fe_zero` is ten zero limbs. -/
+theorem fe_zero_is_ten_zero_limbs :
+ ∃ a, fe_zero = ok a ∧ a.val = List.replicate 10 0#i32 := by
+ exact ⟨_, rfl, rfl⟩
+
+/-- `fe_one` is a one in the lowest limb and zero in the other nine. -/
+theorem fe_one_is_one_in_the_lowest_limb :
+ ∃ a, fe_one = ok a ∧ a.val = 1#i32 :: List.replicate 9 0#i32 := by
+ exact ⟨_, rfl, rfl⟩
+
+/-- Under the radix reading, `fe_zero` denotes zero. -/
+theorem fe_zero_denotes_zero : ∃ a, fe_zero = ok a ∧ feValue a = 0 := by
+ obtain ⟨a, ha, hv⟩ := fe_zero_is_ten_zero_limbs
+ refine ⟨a, ha, ?_⟩
+ unfold feValue
+ rw [hv]
+ decide
+
+/-- Under the radix reading, `fe_one` denotes one. A one in any other limb would
+ denote a power of two of at least 2^26. -/
+theorem fe_one_denotes_one : ∃ a, fe_one = ok a ∧ feValue a = 1 := by
+ obtain ⟨a, ha, hv⟩ := fe_one_is_one_in_the_lowest_limb
+ refine ⟨a, ha, ?_⟩
+ unfold feValue
+ rw [hv]
+ decide
+
+/-- `fe_zero` and `fe_one` are different elements. -/
+theorem fe_zero_and_fe_one_differ (z o : types.Fe) (hz : fe_zero = ok z) (ho : fe_one = ok o) :
+ z ≠ o := by
+ obtain ⟨a, ha, hv⟩ := fe_zero_denotes_zero
+ obtain ⟨b, hb, hw⟩ := fe_one_denotes_one
+ rw [hz] at ha
+ rw [ho] at hb
+ cases ha
+ cases hb
+ intro h
+ rw [h] at hv
+ rw [hv] at hw
+ exact absurd hw (by decide)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.FieldTypes.the_fe_zero_wrapper_is_its_method
#print axioms NonosExtraction.FieldTypes.the_fe_one_wrapper_is_its_method
+#print axioms NonosExtraction.FieldTypes.fe_zero_is_ten_zero_limbs
+#print axioms NonosExtraction.FieldTypes.fe_one_is_one_in_the_lowest_limb
+#print axioms NonosExtraction.FieldTypes.fe_zero_denotes_zero
+#print axioms NonosExtraction.FieldTypes.fe_one_denotes_one
+#print axioms NonosExtraction.FieldTypes.fe_zero_and_fe_one_differ
end NonosExtraction.FieldTypes
diff --git a/verification/extraction/lean/NonosExtraction/GicStateRefinement.lean b/verification/extraction/lean/NonosExtraction/GicStateRefinement.lean
index e8f6d49964..6d0149739d 100644
--- a/verification/extraction/lean/NonosExtraction/GicStateRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/GicStateRefinement.lean
@@ -41,10 +41,74 @@ theorem the_dist_base_wrapper_is_its_method :
theorem the_redist_base_wrapper_is_its_method :
redist_base = state.redist_base := rfl
+/-! ### Which cell each accessor touches, with which ordering
+
+`set_bases` is called once from `gic::api::init` with the distributor and
+redistributor bases from the firmware, and every later interrupt enable or
+disable builds a `Gic` from `dist_base()` and `redist_base()`. The theorems below
+establish the shape of that handoff on the extracted code: `set_bases` stores its
+first argument, unchanged, into the distributor cell and then its second argument
+into the redistributor cell, both with `Release`; each reader is a single
+`Acquire` load of its own cell; and both cells are created holding zero, the value
+a reader sees before `set_bases` has run.
+
+Aeneas leaves `AtomicU64::new`, `load` and `store` opaque and threads no memory
+through `Result`, so nothing here can establish that a load returns what an
+earlier store wrote, that the `Release`/`Acquire` pair orders the stores before a
+reader's later device accesses, or even that the two statics are distinct
+objects: in this model both are the value `AtomicU64::new(0)` and cannot be told
+apart. What the theorems pin down is the program text the memory model acts on:
+which argument goes to which cell, in which order, and with which orderings. -/
+
+/-- `set_bases` publishes the distributor base first and the redistributor base
+ second, each as given and each with a `Release` store. -/
+theorem set_bases_stores_each_base_into_its_own_cell_with_release (dist redist : Std.U64) :
+ set_bases dist redist = (do
+ let a ← state.DIST_BASE
+ core.sync.atomic.AtomicU64Align8U64.store a dist core.sync.atomic.Ordering.Release
+ let b ← state.REDIST_BASE
+ core.sync.atomic.AtomicU64Align8U64.store b redist core.sync.atomic.Ordering.Release) := by
+ unfold set_bases state.set_bases
+ rfl
+
+/-- `dist_base` is one `Acquire` load of the distributor cell, the ordering that
+ pairs with the `Release` store in `set_bases`. -/
+theorem dist_base_is_an_acquire_load_of_the_distributor_cell :
+ dist_base = (do
+ let a ← state.DIST_BASE
+ core.sync.atomic.AtomicU64Align8U64.load a core.sync.atomic.Ordering.Acquire) := by
+ unfold dist_base state.dist_base
+ rfl
+
+/-- `redist_base` is one `Acquire` load of the redistributor cell. -/
+theorem redist_base_is_an_acquire_load_of_the_redistributor_cell :
+ redist_base = (do
+ let a ← state.REDIST_BASE
+ core.sync.atomic.AtomicU64Align8U64.load a core.sync.atomic.Ordering.Acquire) := by
+ unfold redist_base state.redist_base
+ rfl
+
+/-- Before `set_bases` has run, `dist_base` and `redist_base` read cells that were
+ created holding zero, so an uninitialised controller is seen at base zero
+ rather than at some leftover address. -/
+theorem dist_base_and_redist_base_read_cells_created_at_zero :
+ dist_base = (do
+ let a ← core.sync.atomic.AtomicU64Align8U64.new 0#u64
+ core.sync.atomic.AtomicU64Align8U64.load a core.sync.atomic.Ordering.Acquire) ∧
+ redist_base = (do
+ let a ← core.sync.atomic.AtomicU64Align8U64.new 0#u64
+ core.sync.atomic.AtomicU64Align8U64.load a core.sync.atomic.Ordering.Acquire) := by
+ unfold dist_base state.dist_base redist_base state.redist_base state.DIST_BASE state.REDIST_BASE
+ exact ⟨rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.GicState.the_set_bases_wrapper_is_its_method
#print axioms NonosExtraction.GicState.the_dist_base_wrapper_is_its_method
#print axioms NonosExtraction.GicState.the_redist_base_wrapper_is_its_method
+#print axioms NonosExtraction.GicState.set_bases_stores_each_base_into_its_own_cell_with_release
+#print axioms NonosExtraction.GicState.dist_base_is_an_acquire_load_of_the_distributor_cell
+#print axioms NonosExtraction.GicState.redist_base_is_an_acquire_load_of_the_redistributor_cell
+#print axioms NonosExtraction.GicState.dist_base_and_redist_base_read_cells_created_at_zero
end NonosExtraction.GicState
diff --git a/verification/extraction/lean/NonosExtraction/HeapTypesStatsRefinement.lean b/verification/extraction/lean/NonosExtraction/HeapTypesStatsRefinement.lean
index 60f2d442bf..2724cae4f4 100644
--- a/verification/extraction/lean/NonosExtraction/HeapTypesStatsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/HeapTypesStatsRefinement.lean
@@ -35,8 +35,57 @@ namespace NonosExtraction.HeapTypesStats
theorem the_heapstats_free_memory_wrapper_is_its_method (a : stats.HeapStats) :
heapstats_free_memory a = stats.HeapStats.free_memory a := rfl
+/-! ### Free heap memory saturates at zero
+
+ `free_memory` is the truncated difference of the heap size and the current
+ usage: it never fails, it adds back to the heap size whenever usage fits, and
+ it reports zero rather than a wrapped count when usage has reached or passed
+ the size. That last case is reachable: `record_deallocation` in
+ `heap/types/statistics.rs` lowers `current_usage` with a wrapping
+ `fetch_sub`, so an unmatched free leaves a huge usage behind. The theorems
+ cannot say anything about how `get_stats` samples the atomic counters, which
+ are loaded one at a time and are not extracted.
+-/
+
+/-- The free count is the natural-number truncated difference, and the call
+ never takes the overflow-failing path. -/
+theorem heapstats_free_memory_is_the_truncated_difference (s : stats.HeapStats) :
+ ∃ r, heapstats_free_memory s = ok r ∧
+ r.val = s.total_size.val - s.current_usage.val := by
+ unfold heapstats_free_memory stats.HeapStats.free_memory
+ split
+ · next hlt =>
+ have hle : s.current_usage.val ≤ s.total_size.val := by scalar_tac
+ obtain ⟨z, hz, hv, -⟩ := WP.spec_imp_exists
+ (UScalar.sub_spec (x := s.total_size) (y := s.current_usage) hle)
+ exact ⟨z, hz, hv⟩
+ · next hge =>
+ exact ⟨0#usize, rfl, by simp; scalar_tac⟩
+
+/-- While usage fits in the heap, free memory and usage add up to the heap size. -/
+theorem heapstats_free_memory_and_usage_fill_the_heap (s : stats.HeapStats)
+ (h : s.current_usage.val ≤ s.total_size.val) :
+ ∃ r, heapstats_free_memory s = ok r ∧
+ r.val + s.current_usage.val = s.total_size.val := by
+ obtain ⟨r, hr, hv⟩ := heapstats_free_memory_is_the_truncated_difference s
+ exact ⟨r, hr, by omega⟩
+
+/-- Usage at or past the heap size, as after an unmatched free wraps the counter,
+ reports no free memory. -/
+theorem heapstats_free_memory_is_zero_once_usage_reaches_the_size (s : stats.HeapStats)
+ (h : s.total_size.val ≤ s.current_usage.val) :
+ heapstats_free_memory s = ok 0#usize := by
+ obtain ⟨r, hr, hv⟩ := heapstats_free_memory_is_the_truncated_difference s
+ rw [hr]
+ congr 1
+ exact UScalar.eq_of_val_eq (by simp; omega)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.HeapTypesStats.the_heapstats_free_memory_wrapper_is_its_method
+#print axioms NonosExtraction.HeapTypesStats.heapstats_free_memory_is_the_truncated_difference
+#print axioms NonosExtraction.HeapTypesStats.heapstats_free_memory_and_usage_fill_the_heap
+#print axioms NonosExtraction.HeapTypesStats.heapstats_free_memory_is_zero_once_usage_reaches_the_size
+
end NonosExtraction.HeapTypesStats
diff --git a/verification/extraction/lean/NonosExtraction/HpetProtectionRefinement.lean b/verification/extraction/lean/NonosExtraction/HpetProtectionRefinement.lean
index 26070f6221..d0f27ad3ed 100644
--- a/verification/extraction/lean/NonosExtraction/HpetProtectionRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/HpetProtectionRefinement.lean
@@ -35,8 +35,60 @@ namespace NonosExtraction.HpetProtection
theorem the_pageprotection_from_u8_wrapper_is_its_method (a : Std.U8) :
pageprotection_from_u8 a = protection.PageProtection.from_u8 a := rfl
+/-! ### Decoding the HPET page protection field
+
+ The HPET table's accessor passes the low four bits of the page protection
+ byte to `from_u8`. These theorems say that the three codes the ACPI
+ specification defines decode to their named variants, that every other value
+ is kept as `Unknown` carrying the value itself, and so that no two bytes
+ decode to the same variant. They cannot say anything about the masking in
+ the accessor, which is not extracted. -/
+
+/-- The numeric code each variant stands for: the specification's zero, one and
+ two, and the carried byte for `Unknown`. -/
+def protectionCode : protection.PageProtection → Nat
+ | .NoGuarantee => 0
+ | .Protected4K => 1
+ | .Protected64K => 2
+ | .Unknown u => u.val
+
+/-- Codes zero, one and two are no guarantee, 4 KiB and 64 KiB protection, in
+ that order, as the ACPI HPET description table defines them. -/
+theorem pageprotection_from_u8_decodes_the_three_specified_codes :
+ pageprotection_from_u8 0#u8 = ok .NoGuarantee ∧
+ pageprotection_from_u8 1#u8 = ok .Protected4K ∧
+ pageprotection_from_u8 2#u8 = ok .Protected64K := ⟨rfl, rfl, rfl⟩
+
+/-- Every value from three up is reported as unknown with the value intact,
+ rather than being folded into one of the named variants. -/
+theorem pageprotection_from_u8_keeps_every_other_value_as_unknown (v : Std.U8)
+ (h : 3 ≤ v.val) :
+ pageprotection_from_u8 v = ok (.Unknown v) := by
+ unfold pageprotection_from_u8 protection.PageProtection.from_u8
+ split
+ all_goals first | rfl | exact absurd h (by decide)
+
+/-- The decoded variant always gives back the byte it came from. -/
+theorem pageprotection_from_u8_can_be_read_back_to_its_code (v : Std.U8) :
+ ∃ p, pageprotection_from_u8 v = ok p ∧ protectionCode p = v.val := by
+ unfold pageprotection_from_u8 protection.PageProtection.from_u8
+ split <;> exact ⟨_, rfl, rfl⟩
+
+/-- Distinct bytes decode to distinct variants, so the decoding loses nothing. -/
+theorem pageprotection_from_u8_sends_distinct_bytes_to_distinct_variants (a b : Std.U8)
+ (h : pageprotection_from_u8 a = pageprotection_from_u8 b) : a = b := by
+ obtain ⟨p, hp, hpa⟩ := pageprotection_from_u8_can_be_read_back_to_its_code a
+ obtain ⟨q, hq, hqb⟩ := pageprotection_from_u8_can_be_read_back_to_its_code b
+ rw [hp, hq] at h
+ cases h
+ exact UScalar.eq_of_val_eq (by rw [← hpa, ← hqb])
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.HpetProtection.the_pageprotection_from_u8_wrapper_is_its_method
+#print axioms NonosExtraction.HpetProtection.pageprotection_from_u8_decodes_the_three_specified_codes
+#print axioms NonosExtraction.HpetProtection.pageprotection_from_u8_keeps_every_other_value_as_unknown
+#print axioms NonosExtraction.HpetProtection.pageprotection_from_u8_can_be_read_back_to_its_code
+#print axioms NonosExtraction.HpetProtection.pageprotection_from_u8_sends_distinct_bytes_to_distinct_variants
end NonosExtraction.HpetProtection
diff --git a/verification/extraction/lean/NonosExtraction/HpetRegistersRefinement.lean b/verification/extraction/lean/NonosExtraction/HpetRegistersRefinement.lean
index 88b4ae7148..bdd7593f56 100644
--- a/verification/extraction/lean/NonosExtraction/HpetRegistersRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/HpetRegistersRefinement.lean
@@ -41,10 +41,144 @@ theorem the_timer_comparator_wrapper_is_its_method (a : Std.U8) :
theorem the_timer_fsb_route_wrapper_is_its_method (a : Std.U8) :
timer_fsb_route a = registers.timer_fsb_route a := rfl
+/-! ### Where each timer's registers sit
+
+The three offset functions compute `base + 0x20 * n` in checked `u64` arithmetic
+from a `u8` timer index. The theorems below establish that the arithmetic never
+overflows for any of the 256 indices, give each function's exact value, pin timer 0
+to the literal offsets `configure_hpet` in `src/arch/x86_64/time/timer/hpet.rs`
+writes through, and show that the three register families interleave without
+aliasing: a comparator offset is always its configuration offset plus 8, no offset
+of one family equals an offset of another, distinct timers get distinct offsets,
+and every register of the 32 timers an HPET can describe lies below `0x500`, inside
+the first page of the block.
+
+They say nothing about whether the index a caller passes is below the comparator
+count the capability register reports: the functions accept every `u8`, and
+`timer_fsb_route 120` is `0x1010`, past the first page. They also cannot see the
+MMIO accesses themselves, which are not extracted; `configure_hpet` currently
+adds its literal offsets rather than calling these functions, so the agreement at
+timer 0 is agreement between two independent statements of the layout.
+-/
+
+/-- The shared shape of the three offset functions: a `u8` index widened to `u64`,
+scaled by `0x20` and added to a base, never overflows while the base is small. -/
+private theorem offset_is_exact (n : Std.U8) (base : Std.U64) (hb : base.val ≤ 0x1000) :
+ ∃ v : Std.U64, (do
+ let i ← lift (UScalar.cast .U64 n)
+ let i1 ← i * 32#u64
+ base + i1 : Result Std.U64) = ok v ∧ v.val = base.val + 0x20 * n.val := by
+ have hn : n.val < 256 := by scalar_tac
+ simp only [lift, bind_tc_ok]
+ have hc : (UScalar.cast .U64 n).val = n.val := by
+ rw [UScalar.cast_val_eq]; simp; omega
+ have ⟨m, hm, hmv⟩ := WP.spec_imp_exists
+ (U64.mul_spec (x := UScalar.cast .U64 n) (y := 32#u64) (by simp only [hc]; scalar_tac))
+ rw [hm]; simp only [bind_tc_ok]
+ have ⟨s, hs, hsv⟩ := WP.spec_imp_exists
+ (U64.add_spec (x := base) (y := m) (by simp only [hmv, hc]; scalar_tac))
+ exact ⟨s, hs, by simp only [hsv, hmv, hc]; simp; omega⟩
+
+theorem timer_config_is_0x100_plus_0x20_per_timer (n : Std.U8) :
+ ∃ v : Std.U64, timer_config n = ok v ∧ v.val = 0x100 + 0x20 * n.val :=
+ offset_is_exact n 256#u64 (by decide)
+
+theorem timer_comparator_is_0x108_plus_0x20_per_timer (n : Std.U8) :
+ ∃ v : Std.U64, timer_comparator n = ok v ∧ v.val = 0x108 + 0x20 * n.val :=
+ offset_is_exact n 264#u64 (by decide)
+
+theorem timer_fsb_route_is_0x110_plus_0x20_per_timer (n : Std.U8) :
+ ∃ v : Std.U64, timer_fsb_route n = ok v ∧ v.val = 0x110 + 0x20 * n.val :=
+ offset_is_exact n 272#u64 (by decide)
+
+/-- Timer 0's configuration and comparator registers are at `0x100` and `0x108`,
+the literal offsets `configure_hpet` adds to the HPET base when it programs the
+periodic tick. -/
+theorem timer_zero_config_and_comparator_are_where_configure_hpet_writes :
+ timer_config 0#u8 = ok 0x100#u64 ∧ timer_comparator 0#u8 = ok 0x108#u64 := by
+ constructor <;> rfl
+
+/-- The comparator register is the second quadword of its timer's block: eight
+bytes past that timer's configuration register, for every index. -/
+theorem timer_comparator_is_eight_past_timer_config (n : Std.U8) :
+ ∃ c v : Std.U64, timer_config n = ok c ∧ timer_comparator n = ok v ∧
+ v.val = c.val + 8 := by
+ obtain ⟨c, hc, hcv⟩ := timer_config_is_0x100_plus_0x20_per_timer n
+ obtain ⟨v, hv, hvv⟩ := timer_comparator_is_0x108_plus_0x20_per_timer n
+ exact ⟨c, v, hc, hv, by omega⟩
+
+/-- The FSB route register is the third quadword of its timer's block. -/
+theorem timer_fsb_route_is_sixteen_past_timer_config (n : Std.U8) :
+ ∃ c v : Std.U64, timer_config n = ok c ∧ timer_fsb_route n = ok v ∧
+ v.val = c.val + 0x10 := by
+ obtain ⟨c, hc, hcv⟩ := timer_config_is_0x100_plus_0x20_per_timer n
+ obtain ⟨v, hv, hvv⟩ := timer_fsb_route_is_0x110_plus_0x20_per_timer n
+ exact ⟨c, v, hc, hv, by omega⟩
+
+private theorem ok_ne_of_val_ne {a b : Std.U64} (h : a.val ≠ b.val) :
+ (ok a : Result Std.U64) ≠ ok b := by
+ intro he; injection he with he; exact h (congrArg UScalar.val he)
+
+/-- No register of one family is ever a register of another family, whatever the
+two timer indices: a write meant for one timer's comparator can never land on any
+timer's configuration or FSB route register, and so on. -/
+theorem timer_config_timer_comparator_and_timer_fsb_route_never_alias (n m : Std.U8) :
+ timer_comparator n ≠ timer_config m ∧ timer_fsb_route n ≠ timer_config m ∧
+ timer_fsb_route n ≠ timer_comparator m := by
+ obtain ⟨c, hc, hcv⟩ := timer_config_is_0x100_plus_0x20_per_timer m
+ obtain ⟨v, hv, hvv⟩ := timer_comparator_is_0x108_plus_0x20_per_timer n
+ obtain ⟨v', hv', hvv'⟩ := timer_comparator_is_0x108_plus_0x20_per_timer m
+ obtain ⟨f, hf, hfv⟩ := timer_fsb_route_is_0x110_plus_0x20_per_timer n
+ rw [hc, hv, hf, hv']
+ exact ⟨ok_ne_of_val_ne (by omega), ok_ne_of_val_ne (by omega),
+ ok_ne_of_val_ne (by omega)⟩
+
+/-- Distinct timers have distinct configuration registers. -/
+theorem timer_config_tells_timers_apart (n m : Std.U8)
+ (h : timer_config n = timer_config m) : n = m := by
+ obtain ⟨c, hc, hcv⟩ := timer_config_is_0x100_plus_0x20_per_timer n
+ obtain ⟨c', hc', hcv'⟩ := timer_config_is_0x100_plus_0x20_per_timer m
+ rw [hc, hc'] at h
+ injection h with h
+ have := congrArg UScalar.val h
+ exact UScalar.eq_of_val_eq (by omega)
+
+/-- Every timer register lies above the general registers (the main counter at
+`0x0F0` is the last of them and ends at `0x0F8`), and for the at most 32 timers the
+capability register can report, every timer register ends by `0x500`, inside the
+first 4 KiB page of the HPET block. Timer 31's FSB route register is the last one,
+at `0x4F0`. -/
+theorem timer_registers_of_the_32_reportable_timers_fit_below_0x500 (n : Std.U8)
+ (hn : n.val < 32) :
+ ∃ c v f : Std.U64, timer_config n = ok c ∧ timer_comparator n = ok v ∧
+ timer_fsb_route n = ok f ∧
+ 0x0F8 ≤ c.val ∧ c.val < v.val ∧ v.val < f.val ∧ f.val + 8 ≤ 0x500 := by
+ obtain ⟨c, hc, hcv⟩ := timer_config_is_0x100_plus_0x20_per_timer n
+ obtain ⟨v, hv, hvv⟩ := timer_comparator_is_0x108_plus_0x20_per_timer n
+ obtain ⟨f, hf, hfv⟩ := timer_fsb_route_is_0x110_plus_0x20_per_timer n
+ exact ⟨c, v, f, hc, hv, hf, by omega, by omega, by omega, by omega⟩
+
+/-- The bound on the index is the caller's: the functions take any `u8`, the
+largest offset (timer 255's FSB route register, `0x20F0`) is still computed without
+overflow, and timer 120's FSB route register is already past the first page. -/
+theorem timer_fsb_route_past_the_reportable_timers_leaves_the_first_page :
+ timer_fsb_route 120#u8 = ok 0x1010#u64 ∧ timer_fsb_route 255#u8 = ok 0x20F0#u64 := by
+ constructor <;> rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.HpetRegisters.the_timer_config_wrapper_is_its_method
#print axioms NonosExtraction.HpetRegisters.the_timer_comparator_wrapper_is_its_method
#print axioms NonosExtraction.HpetRegisters.the_timer_fsb_route_wrapper_is_its_method
+#print axioms NonosExtraction.HpetRegisters.timer_config_is_0x100_plus_0x20_per_timer
+#print axioms NonosExtraction.HpetRegisters.timer_comparator_is_0x108_plus_0x20_per_timer
+#print axioms NonosExtraction.HpetRegisters.timer_fsb_route_is_0x110_plus_0x20_per_timer
+#print axioms NonosExtraction.HpetRegisters.timer_zero_config_and_comparator_are_where_configure_hpet_writes
+#print axioms NonosExtraction.HpetRegisters.timer_comparator_is_eight_past_timer_config
+#print axioms NonosExtraction.HpetRegisters.timer_fsb_route_is_sixteen_past_timer_config
+#print axioms NonosExtraction.HpetRegisters.timer_config_timer_comparator_and_timer_fsb_route_never_alias
+#print axioms NonosExtraction.HpetRegisters.timer_config_tells_timers_apart
+#print axioms NonosExtraction.HpetRegisters.timer_registers_of_the_32_reportable_timers_fit_below_0x500
+#print axioms NonosExtraction.HpetRegisters.timer_fsb_route_past_the_reportable_timers_leaves_the_first_page
end NonosExtraction.HpetRegisters
diff --git a/verification/extraction/lean/NonosExtraction/I2cTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/I2cTypesRefinement.lean
index f00b6ab227..b45f94fef8 100644
--- a/verification/extraction/lean/NonosExtraction/I2cTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/I2cTypesRefinement.lean
@@ -38,9 +38,63 @@ theorem the_i2chiddevice_is_touchpad_wrapper_is_its_method (a : types.I2cHidDevi
theorem the_i2chiddevice_is_touchscreen_wrapper_is_its_method (a : types.I2cHidDevice) :
i2chiddevice_is_touchscreen a = types.I2cHidDevice.is_touchscreen a := rfl
+/-! ### Which device a predicate picks out
+
+`is_touchpad` and `is_touchscreen` are what the enumeration filters in
+`acpi/devices/i2c/enumerate.rs` keep a device by. The theorems below say each
+one answers true for exactly its own device type, never fails, and that no
+device is both, so the touchpad list and the touchscreen list the kernel builds
+from one enumeration cannot share an entry. The comparison goes through the
+derived `PartialEq`, which reads discriminants, so these also say the derived
+equality separates the two variants from each other and from the other five.
+They say nothing about how `device_type` was assigned in the first place: the
+classification from ACPI `_HID` and `_CID` strings is not extracted here.
+-/
+
+/-- A device is reported as a touchpad exactly when its type is `Touchpad`, and
+ the query always answers. -/
+theorem i2chiddevice_is_touchpad_holds_exactly_for_the_touchpad_type
+ (d : types.I2cHidDevice) :
+ ∃ b, i2chiddevice_is_touchpad d = ok b ∧ (b = true ↔ d.device_type = .Touchpad) := by
+ unfold i2chiddevice_is_touchpad types.I2cHidDevice.is_touchpad
+ types.I2cHidDeviceType.Insts.CoreCmpPartialEqI2cHidDeviceType.eq
+ refine ⟨_, rfl, ?_⟩
+ generalize d.device_type = t
+ cases t <;> simp [types.I2cHidDeviceType.read_discriminant]
+
+/-- A device is reported as a touchscreen exactly when its type is
+ `Touchscreen`, and the query always answers. -/
+theorem i2chiddevice_is_touchscreen_holds_exactly_for_the_touchscreen_type
+ (d : types.I2cHidDevice) :
+ ∃ b, i2chiddevice_is_touchscreen d = ok b ∧
+ (b = true ↔ d.device_type = .Touchscreen) := by
+ unfold i2chiddevice_is_touchscreen types.I2cHidDevice.is_touchscreen
+ types.I2cHidDeviceType.Insts.CoreCmpPartialEqI2cHidDeviceType.eq
+ refine ⟨_, rfl, ?_⟩
+ generalize d.device_type = t
+ cases t <;> simp [types.I2cHidDeviceType.read_discriminant]
+
+/-- No device passes both filters, so `enumerate_touchpads` and
+ `enumerate_touchscreens` never hand the same device to two drivers. -/
+theorem no_device_is_both_i2chiddevice_is_touchpad_and_i2chiddevice_is_touchscreen
+ (d : types.I2cHidDevice) :
+ ¬ (i2chiddevice_is_touchpad d = ok true ∧ i2chiddevice_is_touchscreen d = ok true) := by
+ obtain ⟨b, hb, hbi⟩ := i2chiddevice_is_touchpad_holds_exactly_for_the_touchpad_type d
+ obtain ⟨c, hc, hci⟩ := i2chiddevice_is_touchscreen_holds_exactly_for_the_touchscreen_type d
+ rintro ⟨h1, h2⟩
+ rw [hb] at h1
+ rw [hc] at h2
+ have e1 := hbi.mp (by cases h1; rfl)
+ have e2 := hci.mp (by cases h2; rfl)
+ rw [e1] at e2
+ cases e2
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.I2cTypes.the_i2chiddevice_is_touchpad_wrapper_is_its_method
#print axioms NonosExtraction.I2cTypes.the_i2chiddevice_is_touchscreen_wrapper_is_its_method
+#print axioms NonosExtraction.I2cTypes.i2chiddevice_is_touchpad_holds_exactly_for_the_touchpad_type
+#print axioms NonosExtraction.I2cTypes.i2chiddevice_is_touchscreen_holds_exactly_for_the_touchscreen_type
+#print axioms NonosExtraction.I2cTypes.no_device_is_both_i2chiddevice_is_touchpad_and_i2chiddevice_is_touchscreen
end NonosExtraction.I2cTypes
diff --git a/verification/extraction/lean/NonosExtraction/IommuDomainIdRefinement.lean b/verification/extraction/lean/NonosExtraction/IommuDomainIdRefinement.lean
index c504bf40f5..169169637c 100644
--- a/verification/extraction/lean/NonosExtraction/IommuDomainIdRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/IommuDomainIdRefinement.lean
@@ -38,9 +38,38 @@ theorem the_domainid_new_wrapper_is_its_method (a : Std.U16) :
theorem the_domainid_as_u16_wrapper_is_its_method (a : domain_id.DomainId) :
domainid_as_u16 a = domain_id.DomainId.as_u16 a := rfl
+/-! ### A domain carries the number it was made from
+
+ `DomainId` is a newtype over `u16`, and every IOMMU backend call rebuilds the
+ hardware domain from `as_u16`. These theorems establish that `as_u16` returns
+ exactly the number `new` was given, for all 65536 values including zero (the
+ kernel domain) and the largest, so the domain torn down or mapped is the one
+ allocated, and that distinct numbers give distinct domains. They cannot
+ establish anything about the `raw_id as u16` truncation in the backend that
+ produces the number, which is not extracted.
+-/
+
+theorem domainid_as_u16_returns_the_number_domainid_new_was_given (id : Std.U16) :
+ (do let d ← domainid_new id; domainid_as_u16 d) = ok id := rfl
+
+theorem domainid_as_u16_of_the_largest_and_the_kernel_domain_survive :
+ (do let d ← domainid_new 65535#u16; domainid_as_u16 d) = ok 65535#u16 ∧
+ (do let d ← domainid_new 0#u16; domainid_as_u16 d) = ok 0#u16 := ⟨rfl, rfl⟩
+
+/-- Two domains made from different numbers are different domains, and two
+ domains that report the same number are the same domain. -/
+theorem domainid_new_and_domainid_as_u16_are_injective (a b : Std.U16) (x y : domain_id.DomainId) :
+ (domainid_new a = domainid_new b → a = b) ∧
+ (domainid_as_u16 x = domainid_as_u16 y → x = y) := by
+ unfold domainid_new domain_id.DomainId.new domainid_as_u16 domain_id.DomainId.as_u16
+ exact ⟨fun h => ok.inj h, fun h => ok.inj h⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.IommuDomainId.the_domainid_new_wrapper_is_its_method
#print axioms NonosExtraction.IommuDomainId.the_domainid_as_u16_wrapper_is_its_method
+#print axioms NonosExtraction.IommuDomainId.domainid_as_u16_returns_the_number_domainid_new_was_given
+#print axioms NonosExtraction.IommuDomainId.domainid_as_u16_of_the_largest_and_the_kernel_domain_survive
+#print axioms NonosExtraction.IommuDomainId.domainid_new_and_domainid_as_u16_are_injective
end NonosExtraction.IommuDomainId
diff --git a/verification/extraction/lean/NonosExtraction/IommuProtectionRefinement.lean b/verification/extraction/lean/NonosExtraction/IommuProtectionRefinement.lean
index c24d1f9a66..d1424db7bb 100644
--- a/verification/extraction/lean/NonosExtraction/IommuProtectionRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/IommuProtectionRefinement.lean
@@ -38,9 +38,59 @@ theorem the_iommuprotection_readable_wrapper_is_its_method (a : protection.Iommu
theorem the_iommuprotection_writable_wrapper_is_its_method (a : protection.IommuProtection) :
iommuprotection_writable a = protection.IommuProtection.writable a := rfl
+/-! ### The accessors report the permissions the backend programs
+
+ The VT-d backend's `map` builds its page flags from `protection.read` and
+ `protection.write` directly, while policy code asks `readable()` and
+ `writable()`. The theorems below establish that the two accessors agree
+ with the fields the backend reads, that each reads its own field and not
+ the other, so the pair of answers determines the protection completely,
+ and that the read-only shape of `IommuProtection::READ` is readable and not
+ writable while a write-only protection is writable and not readable. They
+ cannot establish that the hardware honours those flags, and the `READ` and
+ `READ_WRITE` constants and the backend itself are not extracted.
+-/
+
+/-- Each accessor returns exactly the field the backend turns into a page flag. -/
+theorem iommuprotection_readable_and_writable_are_the_backend_flags
+ (p : protection.IommuProtection) :
+ iommuprotection_readable p = ok p.read ∧ iommuprotection_writable p = ok p.write :=
+ ⟨rfl, rfl⟩
+
+/-- Two protections that answer both accessors alike are the same protection:
+neither accessor reads the other's field. -/
+theorem iommuprotection_readable_and_writable_determine_the_protection
+ (p q : protection.IommuProtection)
+ (hr : iommuprotection_readable p = iommuprotection_readable q)
+ (hw : iommuprotection_writable p = iommuprotection_writable q) : p = q := by
+ obtain ⟨pr, pw⟩ := p
+ obtain ⟨qr, qw⟩ := q
+ simp only [iommuprotection_readable, protection.IommuProtection.readable,
+ iommuprotection_writable, protection.IommuProtection.writable, ok.injEq] at hr hw
+ subst hr hw
+ rfl
+
+/-- The read-only shape of `IommuProtection::READ` grants reads and refuses
+writes. -/
+theorem a_read_only_protection_is_iommuprotection_readable_but_not_iommuprotection_writable :
+ iommuprotection_readable { read := true, write := false } = ok true ∧
+ iommuprotection_writable { read := true, write := false } = ok false :=
+ ⟨rfl, rfl⟩
+
+/-- A write-only protection grants writes and refuses reads: writability does
+not imply readability in this type. -/
+theorem a_write_only_protection_is_iommuprotection_writable_but_not_iommuprotection_readable :
+ iommuprotection_writable { read := false, write := true } = ok true ∧
+ iommuprotection_readable { read := false, write := true } = ok false :=
+ ⟨rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.IommuProtection.the_iommuprotection_readable_wrapper_is_its_method
#print axioms NonosExtraction.IommuProtection.the_iommuprotection_writable_wrapper_is_its_method
+#print axioms NonosExtraction.IommuProtection.iommuprotection_readable_and_writable_are_the_backend_flags
+#print axioms NonosExtraction.IommuProtection.iommuprotection_readable_and_writable_determine_the_protection
+#print axioms NonosExtraction.IommuProtection.a_read_only_protection_is_iommuprotection_readable_but_not_iommuprotection_writable
+#print axioms NonosExtraction.IommuProtection.a_write_only_protection_is_iommuprotection_writable_but_not_iommuprotection_readable
end NonosExtraction.IommuProtection
diff --git a/verification/extraction/lean/NonosExtraction/IommuRefinement.lean b/verification/extraction/lean/NonosExtraction/IommuRefinement.lean
index b6972cb724..e0f8471de5 100644
--- a/verification/extraction/lean/NonosExtraction/IommuRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/IommuRefinement.lean
@@ -34,8 +34,11 @@ reaches through the scalar and `Result` encoding the extraction produces.
The last group is about the level arithmetic, which is partial. `index_for` and
`level_span` both compute `level - 1` and shift by it, so level zero underflows:
-`the_span_fails_at_level_zero` is that failure, as the overflow it really is. The
-shift also passes the word width at level seven, which is not proven here.
+`the_span_fails_at_level_zero` and `the_index_fails_at_level_zero` are that
+failure, as the overflow it really is. For levels one to six the index is the
+nine-bit slice of the address above that level's shift; at level seven the shift
+passes the word width, which is not stated here. `fits_address_width` is exact
+for every width, the top one included.
Rather than clamp, which would replace a loud failure with a wrong index,
`the_producer_keeps_the_indexing_total` proves the precondition holds. The only
@@ -45,6 +48,7 @@ because of what can reach it, and that is a theorem rather than an argument.
-/
import NonosExtraction.Iommu
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_iommu
@@ -276,6 +280,68 @@ theorem the_producer_keeps_the_indexing_total (l : Agaw) :
theorem the_span_fails_at_level_zero : slSpan 0#u8 = fail Error.integerOverflow := by
rfl
+/-- For levels one to six, `index_for` is the nine bits of the address above that
+ level's shift, `12 + 9 * (level - 1)`: bits 12 to 20 at level one, 21 to 29
+ at level two, and so on. The walk uses it to pick the entry at each level. -/
+theorem sl_index_for_is_the_nine_bit_slice (addr : Std.U64) (level : Std.U8)
+ (h1 : 1 ≤ level.val) (h6 : level.val ≤ 6) :
+ ∃ i : Std.Usize, arch.x86_64.iommu.tables.sl_pte.index_for addr level = ok i ∧
+ i.val = addr.val / 2 ^ (12 + 9 * (level.val - 1)) % 512 := by
+ unfold arch.x86_64.iommu.tables.sl_pte.index_for arch.x86_64.iommu.tables.sl_pte.LEVEL_SHIFT
+ arch.x86_64.iommu.tables.sl_pte.PAGE_SHIFT arch.x86_64.iommu.tables.sl_pte.ENTRIES
+ have hc : (UScalar.cast .U32 level).val = level.val := by
+ rw [UScalar.cast_val_eq]; have := level.hBounds; simp at this ⊢; omega
+ simp only [lift, bind_tc_ok]
+ have ⟨a, ha, hav⟩ := WP.spec_imp_exists
+ (U32.sub_spec (x := UScalar.cast .U32 level) (y := 1#u32) (by scalar_tac))
+ simp only [ha, bind_tc_ok]
+ have ⟨b, hb, hbv⟩ := WP.spec_imp_exists (U32.mul_spec (x := 9#u32) (y := a) (by scalar_tac))
+ simp only [hb, bind_tc_ok]
+ have ⟨s, hs, hsv⟩ := WP.spec_imp_exists (U32.add_spec (x := 12#u32) (y := b) (by scalar_tac))
+ simp only [hs, bind_tc_ok]
+ obtain ⟨r, hr, hrv, -⟩ := WP.spec_imp_exists
+ (UScalar.ShiftRight_spec addr s (by simp; scalar_tac))
+ simp only [hr, bind_tc_ok]
+ have ⟨m, hm, hmv⟩ := WP.spec_imp_exists (Usize.sub_spec (x := 512#usize) (y := 1#usize) (by scalar_tac))
+ simp only [hm]
+ refine ⟨_, rfl, ?_⟩
+ have hm511 : m.val = 2 ^ 9 - 1 := by scalar_tac
+ rw [Bits.land_low_mask _ m 9 hm511, UScalar.cast_val_eq, hrv, Nat.shiftRight_eq_div_pow]
+ have hsv' : s.val = 12 + 9 * (level.val - 1) := by scalar_tac
+ rw [hsv']
+ have hw : 9 ≤ UScalarTy.Usize.numBits := by
+ simp [UScalarTy.numBits]; have := System.Platform.numBits_eq; omega
+ rw [Nat.mod_mod_of_dvd _ (Nat.pow_dvd_pow 2 hw)]
+
+theorem the_index_fails_at_level_zero (addr : Std.U64) :
+ arch.x86_64.iommu.tables.sl_pte.index_for addr 0#u8 = fail Error.integerOverflow := by
+ rfl
+
+/-- The width check is exact: an address fits a width below 64 exactly when it is
+ below `2 ^ width`, and every address fits a width of 64 or more. -/
+theorem sl_fits_address_width_is_below_two_to_the_width (addr : Std.U64) (w : Std.U8) :
+ arch.x86_64.iommu.tables.sl_pte.fits_address_width addr w =
+ ok (decide (64 ≤ w.val ∨ addr.val < 2 ^ w.val)) := by
+ unfold arch.x86_64.iommu.tables.sl_pte.fits_address_width
+ by_cases h : 64 ≤ w.val
+ · have : w >= 64#u8 := by scalar_tac
+ simp [this, h]
+ · have : ¬ w >= 64#u8 := by scalar_tac
+ simp only [this, ↓reduceIte]
+ have ⟨z, hz, hv, _⟩ := (WP.spec_equiv_exists _ _).mp
+ (UScalar.ShiftLeft_spec (1#u64) w (2 ^ 64) (by simp; omega) (by simp [U64.size, U64.numBits]))
+ simp only [hz, bind_tc_ok]
+ have hzv : z.val = 2 ^ w.val := by
+ rw [hv]; simp only [UScalar.val, Nat.shiftLeft_eq]
+ have hw : w.val < 64 := by omega
+ have hp : 2 ^ w.val < 2 ^ 64 := Nat.pow_lt_pow_right (by decide) hw
+ simp only [show (1#u64 : Std.U64).bv.toNat = 1 from rfl, Nat.one_mul]
+ exact Nat.mod_eq_of_lt hp
+ congr 1
+ simp only [h, false_or, decide_eq_decide]
+ show addr.val < z.val ↔ _
+ rw [hzv]
+
theorem the_depth_is_three_four_or_five (l : Agaw) :
agaw_page_table_levels l = ok 3#u8 ∨ agaw_page_table_levels l = ok 4#u8 ∨
agaw_page_table_levels l = ok 5#u8 := by
@@ -311,6 +377,9 @@ theorem the_context_width_fits_its_field (l : Agaw) :
#print axioms NonosExtraction.the_domain_id_round_trips
#print axioms NonosExtraction.out_of_range_device_numbers_alias
#print axioms NonosExtraction.the_span_fails_at_level_zero
+#print axioms NonosExtraction.sl_index_for_is_the_nine_bit_slice
+#print axioms NonosExtraction.the_index_fails_at_level_zero
+#print axioms NonosExtraction.sl_fits_address_width_is_below_two_to_the_width
#print axioms NonosExtraction.a_snooped_leaf_sets_bit_eleven
#print axioms NonosExtraction.an_unsnooped_leaf_leaves_bit_eleven_clear
#print axioms NonosExtraction.snoop_is_the_only_difference
diff --git a/verification/extraction/lean/NonosExtraction/IommuRegsWindow.lean b/verification/extraction/lean/NonosExtraction/IommuRegsWindow.lean
new file mode 100644
index 0000000000..8776dd01c2
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/IommuRegsWindow.lean
@@ -0,0 +1,78 @@
+-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
+-- [nonos_x_iommu_regs_window]
+import Aeneas
+open Aeneas Aeneas.Std Result ControlFlow Error
+set_option linter.dupNamespace false
+set_option linter.hashCommand false
+set_option linter.unusedVariables false
+
+/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
+set_option maxHeartbeats 1000000
+
+/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
+set_option maxRecDepth 2048
+
+namespace nonos_x_iommu_regs_window
+
+/-- [nonos_x_iommu_regs_window::arch::x86_64::iommu::regs::cap::fault::fault_recording_offset]:
+ Source: 'src/arch/x86_64/iommu/regs/../../../../../../../../../src/arch/x86_64/iommu/regs/cap/fault.rs', lines 18:0-20:1
+ Visibility: public -/
+def arch.x86_64.iommu.regs.cap.fault.fault_recording_offset
+ (cap : Std.U64) : Result Std.Usize := do
+ let i ← cap >>> 24#i32
+ let i1 ← lift (i &&& 1023#u64)
+ let i2 ← lift (UScalar.cast .Usize i1)
+ i2 * 16#usize
+
+/-- [nonos_x_iommu_regs_window::arch::x86_64::iommu::regs::cap::fault::fault_recording_count]:
+ Source: 'src/arch/x86_64/iommu/regs/../../../../../../../../../src/arch/x86_64/iommu/regs/cap/fault.rs', lines 23:0-25:1
+ Visibility: public -/
+def arch.x86_64.iommu.regs.cap.fault.fault_recording_count
+ (cap : Std.U64) : Result Std.U16 := do
+ let i ← cap >>> 40#i32
+ let i1 ← lift (i &&& 255#u64)
+ let i2 ← lift (UScalar.cast .U16 i1)
+ i2 + 1#u16
+
+/-- [nonos_x_iommu_regs_window::arch::x86_64::iommu::regs::offsets::invalidate::iva_offset]:
+ Source: 'src/arch/x86_64/iommu/regs/offsets/../../../../../../../../../../src/arch/x86_64/iommu/regs/offsets/invalidate.rs', lines 23:0-25:1
+ Visibility: public -/
+def arch.x86_64.iommu.regs.offsets.invalidate.iva_offset
+ (ecap : Std.U64) : Result Std.Usize := do
+ let i ← ecap >>> 8#i32
+ let i1 ← lift (i &&& 1023#u64)
+ let i2 ← lift (UScalar.cast .Usize i1)
+ i2 * 16#usize
+
+/-- [nonos_x_iommu_regs_window::arch::x86_64::iommu::regs::offsets::invalidate::iotlb_offset]:
+ Source: 'src/arch/x86_64/iommu/regs/offsets/../../../../../../../../../../src/arch/x86_64/iommu/regs/offsets/invalidate.rs', lines 27:0-29:1
+ Visibility: public -/
+def arch.x86_64.iommu.regs.offsets.invalidate.iotlb_offset
+ (ecap : Std.U64) : Result Std.Usize := do
+ let i ← arch.x86_64.iommu.regs.offsets.invalidate.iva_offset ecap
+ i + 8#usize
+
+/-- [nonos_x_iommu_regs_window::arch::x86_64::iommu::regs::window::registers_fit]:
+ Source: 'src/arch/x86_64/iommu/regs/../../../../../../../../../src/arch/x86_64/iommu/regs/window.rs', lines 24:0-27:1
+ Visibility: public -/
+def arch.x86_64.iommu.regs.window.registers_fit
+ (cap : Std.U64) (ecap : Std.U64) (window : Std.Usize) : Result Bool := do
+ let i ← arch.x86_64.iommu.regs.cap.fault.fault_recording_offset cap
+ let i1 ← arch.x86_64.iommu.regs.cap.fault.fault_recording_count cap
+ let i2 ← lift (UScalar.cast .Usize i1)
+ let i3 ← i2 * 16#usize
+ let faults_end ← i + i3
+ let i4 ← arch.x86_64.iommu.regs.offsets.invalidate.iotlb_offset ecap
+ let i5 ← i4 + 8#usize
+ if i5 <= window
+ then ok (faults_end <= window)
+ else ok false
+
+/-- [nonos_x_iommu_regs_window::registers_fit]:
+ Source: 'src/lib.rs', lines 11:0-13:1
+ Visibility: public -/
+def registers_fit
+ (cap : Std.U64) (ecap : Std.U64) (window : Std.Usize) : Result Bool := do
+ arch.x86_64.iommu.regs.window.registers_fit cap ecap window
+
+end nonos_x_iommu_regs_window
diff --git a/verification/extraction/lean/NonosExtraction/IommuRegsWindowRefinement.lean b/verification/extraction/lean/NonosExtraction/IommuRegsWindowRefinement.lean
new file mode 100644
index 0000000000..5b5b08f748
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/IommuRegsWindowRefinement.lean
@@ -0,0 +1,190 @@
+/-
+NONOS Operating System
+Copyright (C) 2026 NONOS Contributors
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version. See .
+
+iommu_regs_window, on the extracted code.
+
+`probe_at` maps 4 KiB of a remapping unit's registers. Two register sets have
+no fixed place: the IOTLB register sits at `IRO * 16 + 8`, with IRO in ECAP
+bits 8 to 17, and the `NFR + 1` fault records start at `FRO * 16`, with FRO in
+CAP bits 24 to 33. Both fields reach past 16 KiB. `invalidate_iotlb_global`
+and `take_fault` wrote and read at those offsets, and `RemapUnit`'s accessors
+checked the window only with `debug_assert!`, which release builds leave out,
+so a unit reporting IRO or FRO of 256 or more got volatile MMIO past the
+mapped page. `probe_at` now refuses such a unit with `registers_fit`, and the
+accessors check the window in every build.
+
+The theorems below read the three fields exactly, say `registers_fit` holds
+exactly when both register sets end inside the window, and give the
+consequence `probe_at` relies on: for an accepted unit, the IOTLB register and
+every fault record from index 0 to NFR end inside the page.
+
+What these cannot establish: that the unit's register set is no larger than
+the page. The DRHD structure reports that size, and a unit whose registers
+extend past 4 KiB is refused here rather than mapped in full.
+-/
+
+import NonosExtraction.IommuRegsWindow
+import NonosExtraction.Bits
+
+open Aeneas Aeneas.Std Result
+open nonos_x_iommu_regs_window
+
+set_option linter.hashCommand false
+set_option maxRecDepth 100000
+
+namespace NonosExtraction.IommuRegsWindow
+
+/-! ### The forwarding functions add nothing -/
+
+theorem the_registers_fit_wrapper_is_its_method (a : Std.U64) (b : Std.U64) (c : Std.Usize) :
+ registers_fit a b c = arch.x86_64.iommu.regs.window.registers_fit a b c := rfl
+
+/-! ### The register fields and the window -/
+
+private theorem shr_ok (v : Std.U64) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 64) :
+ ∃ z : Std.U64, v >>> k = ok z ∧ z.val = v.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hzv, -⟩ := WP.spec_imp_exists
+ (UScalar.ShiftRight_IScalar_spec v k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hzv, Nat.shiftRight_eq_div_pow]⟩
+
+private theorem to_usize (x : Std.U64) (h : x.val < 2 ^ 32) :
+ (UScalar.cast .Usize x).val = x.val := by
+ rw [UScalar.cast_val_eq]
+ apply Nat.mod_eq_of_lt
+ have : 32 ≤ UScalarTy.Usize.numBits := by
+ simp only [UScalarTy.numBits]; rcases System.Platform.numBits_eq with h | h <;> omega
+ exact Nat.lt_of_lt_of_le h (Nat.pow_le_pow_right (by decide) this)
+
+/-- The fault-recording registers start at the FRO field, CAP bits 24 to 33,
+ in units of sixteen bytes. -/
+theorem fault_recording_offset_is_the_fro_field (cap : Std.U64) :
+ ∃ n : Std.Usize, arch.x86_64.iommu.regs.cap.fault.fault_recording_offset cap = ok n ∧
+ n.val = cap.val / 2 ^ 24 % 1024 * 16 := by
+ unfold arch.x86_64.iommu.regs.cap.fault.fault_recording_offset
+ obtain ⟨z, hz, hzv⟩ := shr_ok cap 24#i32 (by decide) (by decide)
+ simp only [hz, lift, bind_tc_ok]
+ have hf : (z &&& 1023#u64).val = cap.val / 2 ^ 24 % 1024 := by
+ rw [Bits.land_low_mask z 1023#u64 10 rfl, hzv]; rfl
+ have hc := to_usize (z &&& 1023#u64) (by rw [hf]; omega)
+ obtain ⟨n, hn, hnv⟩ := WP.spec_imp_exists
+ (Usize.mul_spec (x := UScalar.cast .Usize (z &&& 1023#u64)) (y := 16#usize) (by scalar_tac))
+ exact ⟨n, hn, by rw [hnv, hc, hf]; rfl⟩
+
+/-- The number of fault-recording registers is the NFR field, CAP bits 40 to
+ 47, plus one. -/
+theorem fault_recording_count_is_nfr_plus_one (cap : Std.U64) :
+ ∃ c : Std.U16, arch.x86_64.iommu.regs.cap.fault.fault_recording_count cap = ok c ∧
+ c.val = cap.val / 2 ^ 40 % 256 + 1 := by
+ unfold arch.x86_64.iommu.regs.cap.fault.fault_recording_count
+ obtain ⟨z, hz, hzv⟩ := shr_ok cap 40#i32 (by decide) (by decide)
+ simp only [hz, lift, bind_tc_ok]
+ have hf : (z &&& 255#u64).val = cap.val / 2 ^ 40 % 256 := by
+ rw [Bits.land_low_mask z 255#u64 8 rfl, hzv]; rfl
+ have hc : (UScalar.cast .U16 (z &&& 255#u64)).val = cap.val / 2 ^ 40 % 256 := by
+ rw [UScalar.cast_val_eq, hf]; simp only [UScalarTy.numBits]; omega
+ obtain ⟨c, hcc, hcv⟩ := WP.spec_imp_exists
+ (U16.add_spec (x := UScalar.cast .U16 (z &&& 255#u64)) (y := 1#u16) (by scalar_tac))
+ exact ⟨c, hcc, by rw [hcv, hc]; rfl⟩
+
+/-- The IOTLB register is eight bytes past the IRO field, ECAP bits 8 to 17, in
+ units of sixteen bytes. -/
+theorem iotlb_offset_is_eight_past_the_iro_field (ecap : Std.U64) :
+ ∃ n : Std.Usize, arch.x86_64.iommu.regs.offsets.invalidate.iotlb_offset ecap = ok n ∧
+ n.val = ecap.val / 2 ^ 8 % 1024 * 16 + 8 := by
+ unfold arch.x86_64.iommu.regs.offsets.invalidate.iotlb_offset
+ arch.x86_64.iommu.regs.offsets.invalidate.iva_offset
+ obtain ⟨z, hz, hzv⟩ := shr_ok ecap 8#i32 (by decide) (by decide)
+ simp only [hz, lift, bind_tc_ok]
+ have hf : (z &&& 1023#u64).val = ecap.val / 2 ^ 8 % 1024 := by
+ rw [Bits.land_low_mask z 1023#u64 10 rfl, hzv]; rfl
+ have hc := to_usize (z &&& 1023#u64) (by rw [hf]; omega)
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists
+ (Usize.mul_spec (x := UScalar.cast .Usize (z &&& 1023#u64)) (y := 16#usize) (by scalar_tac))
+ simp only [hm, bind_tc_ok]
+ obtain ⟨n, hn, hnv⟩ := WP.spec_imp_exists (Usize.add_spec (x := m) (y := 8#usize) (by scalar_tac))
+ exact ⟨n, hn, by rw [hnv, hmv, hc, hf]; rfl⟩
+
+/-- `registers_fit` holds exactly when the IOTLB register, eight bytes wide at
+ `IRO * 16 + 8`, and all `NFR + 1` sixteen-byte fault records from
+ `FRO * 16` end inside the window. -/
+theorem registers_fit_is_both_register_sets_inside_the_window
+ (cap ecap : Std.U64) (w : Std.Usize) :
+ registers_fit cap ecap w =
+ ok (decide (ecap.val / 2 ^ 8 % 1024 * 16 + 16 ≤ w.val ∧
+ cap.val / 2 ^ 24 % 1024 * 16 + (cap.val / 2 ^ 40 % 256 + 1) * 16 ≤ w.val)) := by
+ unfold registers_fit arch.x86_64.iommu.regs.window.registers_fit
+ obtain ⟨o, ho, hov⟩ := fault_recording_offset_is_the_fro_field cap
+ obtain ⟨c, hc, hcv⟩ := fault_recording_count_is_nfr_plus_one cap
+ obtain ⟨t, ht, htv⟩ := iotlb_offset_is_eight_past_the_iro_field ecap
+ simp only [ho, hc, ht, lift, bind_tc_ok]
+ have hcu : (UScalar.cast .Usize c).val = c.val := by
+ rw [UScalar.cast_val_eq]; apply Nat.mod_eq_of_lt
+ have : 16 ≤ UScalarTy.Usize.numBits := by
+ simp only [UScalarTy.numBits]; rcases System.Platform.numBits_eq with h | h <;> omega
+ exact Nat.lt_of_lt_of_le c.hBounds (Nat.pow_le_pow_right (by decide) this)
+ obtain ⟨m, hm, hmv⟩ := WP.spec_imp_exists
+ (Usize.mul_spec (x := UScalar.cast .Usize c) (y := 16#usize) (by scalar_tac))
+ simp only [hm, bind_tc_ok]
+ obtain ⟨e, he, hev⟩ := WP.spec_imp_exists (Usize.add_spec (x := o) (y := m) (by scalar_tac))
+ simp only [he, bind_tc_ok]
+ obtain ⟨t8, ht8, ht8v⟩ := WP.spec_imp_exists (Usize.add_spec (x := t) (y := 8#usize) (by scalar_tac))
+ simp only [ht8, bind_tc_ok]
+ have hev' : e.val = cap.val / 2 ^ 24 % 1024 * 16 + (cap.val / 2 ^ 40 % 256 + 1) * 16 := by
+ rw [hev, hov, hmv, hcu, hcv]; rfl
+ have ht8v' : t8.val = ecap.val / 2 ^ 8 % 1024 * 16 + 16 := by rw [ht8v, htv]; rfl
+ by_cases h1 : t8.val ≤ w.val
+ · have : t8 ≤ w := h1
+ simp only [this, ↓reduceIte]
+ congr 1
+ rw [← ht8v', ← hev']
+ simp [h1]
+ · have : ¬ t8 ≤ w := h1
+ simp only [this, ↓reduceIte]
+ congr 1
+ rw [← ht8v']
+ simp [h1]
+
+/-- The consequence the probe relies on: once `registers_fit` accepts a unit for
+ the 4 KiB window, the IOTLB register and every fault record the driver reads
+ or clears, index `0` to `NFR`, end inside the mapped page. -/
+theorem an_accepted_unit_keeps_every_register_access_in_the_page (cap ecap : Std.U64)
+ (h : registers_fit cap ecap 4096#usize = ok true) :
+ ecap.val / 2 ^ 8 % 1024 * 16 + 8 + 8 ≤ 4096 ∧
+ ∀ i, i < cap.val / 2 ^ 40 % 256 + 1 →
+ cap.val / 2 ^ 24 % 1024 * 16 + i * 16 + 16 ≤ 4096 := by
+ rw [registers_fit_is_both_register_sets_inside_the_window] at h
+ simp only [ok.injEq, decide_eq_true_eq] at h
+ have hw : (4096#usize : Std.Usize).val = 4096 := rfl
+ rw [hw] at h
+ refine ⟨by omega, fun i hi => ?_⟩
+ have : (i + 1) * 16 ≤ (cap.val / 2 ^ 40 % 256 + 1) * 16 := Nat.mul_le_mul_right _ hi
+ omega
+
+/-- At the page edge: IRO 255 and FRO 255 with one record end exactly at 4096
+ and are accepted; IRO 256 or FRO 256, the first positions past the page, are
+ refused. -/
+theorem registers_fit_at_the_page_edge :
+ registers_fit 0#u64 0xFF00#u64 4096#usize = ok true ∧
+ registers_fit 0#u64 0x10000#u64 4096#usize = ok false ∧
+ registers_fit 0xFF000000#u64 0#u64 4096#usize = ok true ∧
+ registers_fit 0x100000000#u64 0#u64 4096#usize = ok false := by
+ simp only [registers_fit_is_both_register_sets_inside_the_window]
+ refine ⟨?_, ?_, ?_, ?_⟩ <;> rfl
+
+/-! ### Axiom profile -/
+
+#print axioms NonosExtraction.IommuRegsWindow.the_registers_fit_wrapper_is_its_method
+#print axioms NonosExtraction.IommuRegsWindow.fault_recording_offset_is_the_fro_field
+#print axioms NonosExtraction.IommuRegsWindow.fault_recording_count_is_nfr_plus_one
+#print axioms NonosExtraction.IommuRegsWindow.iotlb_offset_is_eight_past_the_iro_field
+#print axioms NonosExtraction.IommuRegsWindow.registers_fit_is_both_register_sets_inside_the_window
+#print axioms NonosExtraction.IommuRegsWindow.an_accepted_unit_keeps_every_register_access_in_the_page
+#print axioms NonosExtraction.IommuRegsWindow.registers_fit_at_the_page_edge
+
+end NonosExtraction.IommuRegsWindow
diff --git a/verification/extraction/lean/NonosExtraction/IrqReservedRefinement.lean b/verification/extraction/lean/NonosExtraction/IrqReservedRefinement.lean
index 04bd882185..79f1c90e61 100644
--- a/verification/extraction/lean/NonosExtraction/IrqReservedRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/IrqReservedRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.IrqReserved
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_irq_reserved
@@ -38,9 +39,191 @@ theorem the_reserve_wrapper_is_its_method (a : Std.U32) :
theorem the_is_reserved_wrapper_is_its_method (a : Std.U32) :
is_reserved a = reserved.is_reserved a := rfl
+/-! ### Which bit a line is, and what happens past the table
+
+`RESERVED` is four sixty-four bit words, one bit for each of the 256 lines an
+IOAPIC redirection table can carry. The theorems below fix the arithmetic that
+maps a GSI to its bit and the refusal at the edge. A line at or beyond 256 is
+reported reserved without touching the table, which is what lets
+`bind/intx.rs` refuse a malformed line rather than program it, and reserving
+such a line returns without touching the table either. A line below 256 is word
+`gsi / 64`, bit `gsi % 64`: `reserve` ORs in exactly the one-bit mask `2^(gsi % 64)`
+on that word, and `is_reserved` reports exactly that bit of the word it loads, so
+the bit written by `set_irq` is the bit the broker reads.
+
+What they cannot establish. The words are atomics, and Aeneas models
+`AtomicU64::new`, `load` and `fetch_or` as opaque functions, so nothing here says
+that a load returns what an earlier `fetch_or` stored, or anything about
+ordering between processors. The statements are therefore about which word and
+which mask the code passes to those calls and how it reads their answer. Nor can
+they say that `set_irq` reserves before it unmasks, since that caller is not
+extracted.
+-/
+
+/-- A line below 256 is word `gsi / 64`, bit `gsi % 64`. -/
+theorem index_of_a_line_in_the_table (gsi : Std.U32) (h : gsi.val < 256) :
+ ∃ (w : Std.Usize) (b : Std.U32), w.val = gsi.val / 64 ∧ b.val = gsi.val % 64 ∧
+ reserved.index gsi = ok (some (w, b)) := by
+ unfold reserved.index
+ simp only [lift, bind_tc_ok, reserved.WORDS]
+ have hm : (4#usize * 64#usize : Result Std.Usize) = ok 256#usize := by
+ obtain ⟨z, hz, hv⟩ := WP.spec_imp_exists (Usize.mul_spec (x := 4#usize) (y := 64#usize) (by scalar_tac))
+ rw [hz]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hv]
+ rfl
+ have hg : (UScalar.cast .Usize gsi).val = gsi.val := by
+ rw [UScalar.cast_val_eq]
+ apply Nat.mod_eq_of_lt
+ have : gsi.val < 2 ^ 32 := by scalar_tac
+ rcases System.Platform.numBits_eq with hp | hp <;>
+ simp only [UScalarTy.Usize_numBits_eq, hp] <;> omega
+ rw [hm]
+ simp only [bind_tc_ok]
+ have hlt : ¬ ((UScalar.cast .Usize gsi) >= 256#usize) := by
+ simp only [ge_iff_le, UScalar.le_equiv, not_le]
+ rw [hg]
+ scalar_tac
+ simp only [hlt, if_false]
+ obtain ⟨q, hq, hqv⟩ := UScalar.div_spec (UScalar.cast .Usize gsi) (y := 64#usize) (by simp)
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists
+ (UScalar.rem_spec (UScalar.cast .Usize gsi) (y := 64#usize) (by simp))
+ rw [hq, hr]
+ simp only [bind_tc_ok]
+ refine ⟨q, UScalar.cast .U32 r, ?_, ?_, rfl⟩
+ · rw [hqv, hg]
+ rfl
+ · rw [UScalar.cast_val_eq, hrv, hg]
+ simp only [UScalarTy.numBits]
+ have : gsi.val % (64#usize : Std.Usize).val = gsi.val % 64 := rfl
+ omega
+
+/-- A line at or past 256 has no bit. -/
+theorem index_past_the_table (gsi : Std.U32) (h : 256 ≤ gsi.val) :
+ reserved.index gsi = ok none := by
+ unfold reserved.index
+ simp only [lift, bind_tc_ok, reserved.WORDS]
+ have hm : (4#usize * 64#usize : Result Std.Usize) = ok 256#usize := by
+ obtain ⟨z, hz, hv⟩ := WP.spec_imp_exists (Usize.mul_spec (x := 4#usize) (y := 64#usize) (by scalar_tac))
+ rw [hz]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hv]
+ rfl
+ have hg : (UScalar.cast .Usize gsi).val = gsi.val := by
+ rw [UScalar.cast_val_eq]
+ apply Nat.mod_eq_of_lt
+ have : gsi.val < 2 ^ 32 := by scalar_tac
+ rcases System.Platform.numBits_eq with hp | hp <;>
+ simp only [UScalarTy.Usize_numBits_eq, hp] <;> omega
+ rw [hm]
+ simp only [bind_tc_ok]
+ have hge : ((UScalar.cast .Usize gsi) >= 256#usize) := by
+ simp only [ge_iff_le, UScalar.le_equiv]
+ rw [hg]
+ scalar_tac
+ simp only [hge, if_true]
+
+/-- A line outside every redirection table is reported reserved, and the answer
+ does not depend on the table: `bind/intx.rs` refuses it. -/
+theorem is_reserved_refuses_every_line_past_256 (gsi : Std.U32) (h : 256 ≤ gsi.val) :
+ is_reserved gsi = ok true := by
+ unfold is_reserved reserved.is_reserved
+ rw [index_past_the_table gsi h]
+ rfl
+
+/-- Reserving a line outside every redirection table returns without touching the
+ table (it neither reaches `fetch_or` nor fails on an out-of-range word). -/
+theorem reserve_of_a_line_past_256_does_nothing (gsi : Std.U32) (h : 256 ≤ gsi.val) :
+ reserve gsi = ok () := by
+ unfold reserve reserved.reserve
+ rw [index_past_the_table gsi h]
+ rfl
+
+/-- `is_reserved` of a line in the table loads word `gsi / 64` and answers with
+ bit `gsi % 64` of what it loaded, for whatever value the load returns. -/
+theorem is_reserved_reads_bit_gsi_mod_64_of_word_gsi_div_64 (gsi : Std.U32) (h : gsi.val < 256)
+ (a : Array (core.sync.atomic.Atomic Std.U64 (core.sync.atomic.private.Align8 Std.U64)) 4#usize)
+ (ha : reserved.RESERVED = ok a) :
+ ∃ w : Std.Usize, w.val = gsi.val / 64 ∧
+ is_reserved gsi = (do
+ let a1 ← Array.index_usize a w
+ let v ← core.sync.atomic.AtomicU64Align8U64.load a1 core.sync.atomic.Ordering.Acquire
+ ok (v.val.testBit (gsi.val % 64))) := by
+ obtain ⟨w, b, hw, hb, hi⟩ := index_of_a_line_in_the_table gsi h
+ refine ⟨w, hw, ?_⟩
+ unfold is_reserved reserved.is_reserved
+ rw [hi, ha]
+ simp only [bind_tc_ok]
+ have hb64 : b.val < 64 := by omega
+ obtain ⟨m, hm, hmv, -⟩ := WP.spec_imp_exists (U64.ShiftLeft_spec (x := 1#u64) (y := b) hb64)
+ have hmv' : m.val = 2 ^ (gsi.val % 64) := by
+ rw [hmv, ← hb, Nat.shiftLeft_eq, show (1#u64 : Std.U64).val = 1 from rfl, Nat.one_mul]
+ have : 2 ^ b.val < U64.size := by
+ simp only [U64.size, U64.numBits, UScalarTy.numBits]
+ exact Nat.pow_lt_pow_right (by decide) hb64
+ exact Nat.mod_eq_of_lt this
+ show (do
+ let a1 ← a.index_usize w
+ let i ← core.sync.atomic.AtomicU64Align8U64.load a1 core.sync.atomic.Ordering.Acquire
+ let i1 ← 1#u64 <<< b
+ let i2 ← lift (i &&& i1)
+ ok (i2 != 0#u64)) = _
+ cases hx : Array.index_usize a w with
+ | ok a1 =>
+ simp only [bind_tc_ok]
+ cases hl : core.sync.atomic.AtomicU64Align8U64.load a1 core.sync.atomic.Ordering.Acquire with
+ | ok v =>
+ simp only [bind_tc_ok, hm, lift]
+ rw [Bits.reads_bit v m 0#u64 (gsi.val % 64) hmv' rfl]
+ | fail e => simp only [bind_tc_fail]
+ | div => simp only [bind_tc_div]
+ | fail e => simp only [bind_tc_fail]
+ | div => simp only [bind_tc_div]
+
+/-- `reserve` of a line in the table ORs the one-bit mask `2^(gsi % 64)` into word
+ `gsi / 64`, the same word and bit `is_reserved` reads, with release ordering. -/
+theorem reserve_sets_the_bit_is_reserved_reads (gsi : Std.U32) (h : gsi.val < 256)
+ (a : Array (core.sync.atomic.Atomic Std.U64 (core.sync.atomic.private.Align8 Std.U64)) 4#usize)
+ (ha : reserved.RESERVED = ok a) :
+ ∃ (w : Std.Usize) (m : Std.U64), w.val = gsi.val / 64 ∧ m.val = 2 ^ (gsi.val % 64) ∧
+ reserve gsi = (do
+ let a1 ← Array.index_usize a w
+ let _ ← core.sync.atomic.AtomicU64Align8U64.fetch_or a1 m
+ core.sync.atomic.Ordering.Release
+ ok ()) := by
+ obtain ⟨w, b, hw, hb, hi⟩ := index_of_a_line_in_the_table gsi h
+ have hb64 : b.val < 64 := by omega
+ obtain ⟨m, hm, hmv, -⟩ := WP.spec_imp_exists (U64.ShiftLeft_spec (x := 1#u64) (y := b) hb64)
+ refine ⟨w, m, hw, ?_, ?_⟩
+ · rw [hmv, ← hb, Nat.shiftLeft_eq, show (1#u64 : Std.U64).val = 1 from rfl, Nat.one_mul]
+ have : 2 ^ b.val < U64.size := by
+ simp only [U64.size, U64.numBits, UScalarTy.numBits]
+ exact Nat.pow_lt_pow_right (by decide) hb64
+ exact Nat.mod_eq_of_lt this
+ unfold reserve reserved.reserve
+ rw [hi, ha]
+ simp only [bind_tc_ok]
+ show (do
+ let a1 ← a.index_usize w
+ let i ← 1#u64 <<< b
+ let _ ← core.sync.atomic.AtomicU64Align8U64.fetch_or a1 i core.sync.atomic.Ordering.Release
+ ok ()) = _
+ cases hx : Array.index_usize a w with
+ | ok a1 => simp only [bind_tc_ok, hm]
+ | fail e => simp only [bind_tc_fail]
+ | div => simp only [bind_tc_div]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.IrqReserved.the_reserve_wrapper_is_its_method
#print axioms NonosExtraction.IrqReserved.the_is_reserved_wrapper_is_its_method
+#print axioms NonosExtraction.IrqReserved.index_of_a_line_in_the_table
+#print axioms NonosExtraction.IrqReserved.index_past_the_table
+#print axioms NonosExtraction.IrqReserved.is_reserved_refuses_every_line_past_256
+#print axioms NonosExtraction.IrqReserved.reserve_of_a_line_past_256_does_nothing
+#print axioms NonosExtraction.IrqReserved.is_reserved_reads_bit_gsi_mod_64_of_word_gsi_div_64
+#print axioms NonosExtraction.IrqReserved.reserve_sets_the_bit_is_reserved_reads
end NonosExtraction.IrqReserved
diff --git a/verification/extraction/lean/NonosExtraction/KeyringCapsuleTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/KeyringCapsuleTypesRefinement.lean
index 853174ca34..231609a391 100644
--- a/verification/extraction/lean/NonosExtraction/KeyringCapsuleTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/KeyringCapsuleTypesRefinement.lean
@@ -35,8 +35,62 @@ namespace NonosExtraction.KeyringCapsuleTypes
theorem the_keytype_to_u8_wrapper_is_its_method (a : types.KeyType) :
keytype_to_u8 a = types.KeyType.to_u8 a := rfl
+/-! ### A key type's wire code is its declared discriminant
+
+`store_key` in `client/store.rs` sends `key_type.to_u8()` to the keyring capsule,
+and the capsule decodes it with `KeyType::from_u8`. These theorems establish
+that `to_u8` produces exactly the discriminants 0 to 7 that `types.rs`
+declares, in declaration order, that it always answers, that no two key types
+share a code, and that `from_u8` (restated below from `types.rs`, since it is
+not extracted) inverts it, so a key stored as one type is never read back as
+another.
+
+They cannot establish that the capsule on the far side of the channel uses the
+same table: its decoder is restated here, not extracted.
+-/
+
+/-- `KeyType::from_u8` as `types.rs` writes it. -/
+def keyTypeFromU8 : Nat → Option types.KeyType
+ | 0 => some .Symmetric
+ | 1 => some .PrivateKey
+ | 2 => some .PublicKey
+ | 3 => some .HmacSecret
+ | 4 => some .DerivedKey
+ | 5 => some .SessionKey
+ | 6 => some .MasterKey
+ | 7 => some .SigningKey
+ | _ => none
+
+/-- The code `keytype_to_u8` produces for each key type, in declaration order. -/
+theorem keytype_to_u8_is_the_declared_discriminant :
+ keytype_to_u8 .Symmetric = ok 0#u8 ∧ keytype_to_u8 .PrivateKey = ok 1#u8 ∧
+ keytype_to_u8 .PublicKey = ok 2#u8 ∧ keytype_to_u8 .HmacSecret = ok 3#u8 ∧
+ keytype_to_u8 .DerivedKey = ok 4#u8 ∧ keytype_to_u8 .SessionKey = ok 5#u8 ∧
+ keytype_to_u8 .MasterKey = ok 6#u8 ∧ keytype_to_u8 .SigningKey = ok 7#u8 := by
+ unfold keytype_to_u8 types.KeyType.to_u8
+ refine ⟨?_, ?_, ?_, ?_, ?_, ?_, ?_, ?_⟩ <;>
+ simp [types.KeyType.read_discriminant] <;> rfl
+
+/-- Decoding the code `keytype_to_u8` produces gives back the same key type, so
+ no two key types share a code and every code is below 8. -/
+theorem keytype_to_u8_round_trips_through_from_u8 (t : types.KeyType) :
+ ∃ v, keytype_to_u8 t = ok v ∧ v.val < 8 ∧ keyTypeFromU8 v.val = some t := by
+ obtain ⟨h0, h1, h2, h3, h4, h5, h6, h7⟩ := keytype_to_u8_is_the_declared_discriminant
+ cases t
+ · exact ⟨_, h0, by decide, rfl⟩
+ · exact ⟨_, h1, by decide, rfl⟩
+ · exact ⟨_, h2, by decide, rfl⟩
+ · exact ⟨_, h3, by decide, rfl⟩
+ · exact ⟨_, h4, by decide, rfl⟩
+ · exact ⟨_, h5, by decide, rfl⟩
+ · exact ⟨_, h6, by decide, rfl⟩
+ · exact ⟨_, h7, by decide, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.KeyringCapsuleTypes.the_keytype_to_u8_wrapper_is_its_method
+#print axioms NonosExtraction.KeyringCapsuleTypes.keytype_to_u8_is_the_declared_discriminant
+#print axioms NonosExtraction.KeyringCapsuleTypes.keytype_to_u8_round_trips_through_from_u8
+
end NonosExtraction.KeyringCapsuleTypes
diff --git a/verification/extraction/lean/NonosExtraction/LayoutStackSlots.lean b/verification/extraction/lean/NonosExtraction/LayoutStackSlots.lean
new file mode 100644
index 0000000000..698fa270d7
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/LayoutStackSlots.lean
@@ -0,0 +1,72 @@
+-- THIS FILE WAS AUTOMATICALLY GENERATED BY AENEAS
+-- [nonos_x_layout_stack_slots]
+import Aeneas
+open Aeneas Aeneas.Std Result ControlFlow Error
+set_option linter.dupNamespace false
+set_option linter.hashCommand false
+set_option linter.unusedVariables false
+
+/- You can set the `maxHeartbeats` value with the `-max-heartbeats` CLI option -/
+set_option maxHeartbeats 1000000
+
+/- You can set the `maxRecDepth` value with the `-max-recdepth` CLI option -/
+set_option maxRecDepth 2048
+
+namespace nonos_x_layout_stack_slots
+
+/-- [nonos_x_layout_stack_slots::memory::layout::constants::percpu::GUARD_PAGES]
+ Source: 'src/memory/layout/../../../../../../../src/memory/layout/constants/percpu.rs', lines 22:0-22:33
+ Visibility: public -/
+@[global_simps, irreducible]
+def memory.layout.constants.percpu.GUARD_PAGES : Std.Usize := 1#usize
+
+/-- [nonos_x_layout_stack_slots::memory::layout::constants::percpu::IST_STACK_SIZE]
+ Source: 'src/memory/layout/../../../../../../../src/memory/layout/constants/percpu.rs', lines 21:0-21:44
+ Visibility: public -/
+@[global_simps, irreducible]
+def memory.layout.constants.percpu.IST_STACK_SIZE : Result Std.Usize :=
+ 32#usize * 1024#usize
+
+/-- [nonos_x_layout_stack_slots::memory::layout::constants::percpu::KSTACK_SIZE]
+ Source: 'src/memory/layout/../../../../../../../src/memory/layout/constants/percpu.rs', lines 20:0-20:41
+ Visibility: public -/
+@[global_simps, irreducible]
+def memory.layout.constants.percpu.KSTACK_SIZE : Result Std.Usize :=
+ 64#usize * 1024#usize
+
+/-- [nonos_x_layout_stack_slots::memory::layout::constants::page::PAGE_SIZE]
+ Source: 'src/memory/layout/../../../../../../../src/memory/layout/constants/page.rs', lines 17:0-17:34
+ Visibility: public -/
+@[global_simps, irreducible]
+def memory.layout.constants.page.PAGE_SIZE : Std.Usize := 4096#usize
+
+/-- [nonos_x_layout_stack_slots::memory::layout::manager::stack_slots::stack_slot_offset]:
+ Source: 'src/memory/layout/manager/../../../../../../../../src/memory/layout/manager/stack_slots.rs', lines 23:0-30:1 -/
+def memory.layout.manager.stack_slots.stack_slot_offset
+ (slot : Std.Usize) : Result Std.U64 := do
+ let i ←
+ memory.layout.constants.percpu.GUARD_PAGES *
+ memory.layout.constants.page.PAGE_SIZE
+ let guard ← lift (UScalar.cast .U64 i)
+ if slot = 0#usize
+ then ok guard
+ else
+ let i1 ← lift (UScalar.cast .U64 slot)
+ let ist ← i1 - 1#u64
+ let i2 ← memory.layout.constants.percpu.KSTACK_SIZE
+ let i3 ← lift (UScalar.cast .U64 i2)
+ let i4 ← guard + i3
+ let i5 ← i4 + guard
+ let i6 ← memory.layout.constants.percpu.IST_STACK_SIZE
+ let i7 ← lift (UScalar.cast .U64 i6)
+ let i8 ← i7 + guard
+ let i9 ← ist * i8
+ i5 + i9
+
+/-- [nonos_x_layout_stack_slots::stack_slot_offset]:
+ Source: 'src/lib.rs', lines 11:0-13:1
+ Visibility: public -/
+def stack_slot_offset (slot : Std.Usize) : Result Std.U64 := do
+ memory.layout.manager.stack_slots.stack_slot_offset slot
+
+end nonos_x_layout_stack_slots
diff --git a/verification/extraction/lean/NonosExtraction/LayoutStackSlotsRefinement.lean b/verification/extraction/lean/NonosExtraction/LayoutStackSlotsRefinement.lean
new file mode 100644
index 0000000000..9e4df9139c
--- /dev/null
+++ b/verification/extraction/lean/NonosExtraction/LayoutStackSlotsRefinement.lean
@@ -0,0 +1,106 @@
+/-
+NONOS Operating System
+Copyright (C) 2026 NONOS Contributors
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version. See .
+
+layout_stack_slots, on the extracted code.
+
+`get_all_stack_regions` describes each CPU's kernel stack and eight IST stacks,
+and `get_guard_regions` takes a guard page below and above each one. The stacks
+used to be packed back to back, so the kernel stack's upper guard was the first
+page of IST stack 0 and each IST stack's upper guard the first page of the
+next: `verify_stack_integrity` would have reported a live neighbouring stack as
+a compromised guard, or, with the guards unmapped, a stack would have lost its
+top page. The stacks now start at `stack_slot_offset`, which leaves a guard page
+below the first stack, between every two stacks and above the last.
+
+The theorems below give the offsets exactly and prove the layout property: any
+two slots are separated by a full guard page, and the whole area, guards
+included, fits the per-CPU stride.
+
+What these cannot establish: that the pages are mapped or unmapped as the
+table says. The table is a description that the integrity check and the
+hardening region list read; `get_all_stack_regions` builds a `Vec`, which is
+not extracted.
+-/
+
+import NonosExtraction.LayoutStackSlots
+
+open Aeneas Aeneas.Std Result
+open nonos_x_layout_stack_slots
+
+set_option linter.hashCommand false
+set_option maxRecDepth 100000
+
+namespace NonosExtraction.LayoutStackSlots
+
+/-! ### The forwarding functions add nothing -/
+
+theorem the_stack_slot_offset_wrapper_is_its_method (a : Std.Usize) :
+ stack_slot_offset a = memory.layout.manager.stack_slots.stack_slot_offset a := rfl
+
+/-! ### The stack slots and their guards -/
+
+/-- Slot 0 starts one guard page into the area; slot `i + 1` starts after the
+ kernel stack, its guard, and `i` IST stacks with a guard each. -/
+theorem stack_slot_offset_spec (s : Std.Usize) (hs : s.val ≤ 8) :
+ memory.layout.manager.stack_slots.stack_slot_offset s ⦃ o =>
+ o.val = if s.val = 0 then 4096 else 73728 + (s.val - 1) * 36864 ⦄ := by
+ unfold memory.layout.manager.stack_slots.stack_slot_offset
+ memory.layout.constants.percpu.GUARD_PAGES memory.layout.constants.page.PAGE_SIZE
+ memory.layout.constants.percpu.KSTACK_SIZE memory.layout.constants.percpu.IST_STACK_SIZE
+ step*
+ all_goals (simp_all [U64.max, U64.numBits, UScalarTy.numBits]; try omega)
+
+/-- Size of the stack in slot `s`: the 64 KiB kernel stack, then 32 KiB IST
+ stacks. -/
+def slotSize (s : Nat) : Nat := if s = 0 then 65536 else 32768
+
+/-- For any two slots, the earlier stack and a full guard page above it end at
+ or before the later stack begins. So a stack's upper guard is never a page
+ of the next stack, and a stack's lower guard is never a page of the one
+ before it. -/
+theorem stack_slots_keep_a_guard_page_between_every_two_stacks (a b : Std.Usize)
+ (hab : a.val < b.val) (hb : b.val ≤ 8) :
+ ∃ oa ob : Std.U64,
+ memory.layout.manager.stack_slots.stack_slot_offset a = ok oa ∧
+ memory.layout.manager.stack_slots.stack_slot_offset b = ok ob ∧
+ oa.val + slotSize a.val + 4096 ≤ ob.val := by
+ obtain ⟨oa, ha, hoa⟩ := WP.spec_imp_exists (stack_slot_offset_spec a (by omega))
+ obtain ⟨ob, hb', hob⟩ := WP.spec_imp_exists (stack_slot_offset_spec b hb)
+ refine ⟨oa, ob, ha, hb', ?_⟩
+ unfold slotSize
+ rw [hoa, hob]
+ have step : (a.val - 1) * 36864 + 36864 ≤ (b.val - 1) * 36864 ∨ a.val = 0 := by
+ rcases Nat.eq_zero_or_pos a.val with h | h
+ · exact Or.inr h
+ · left
+ rw [← Nat.succ_mul]
+ exact Nat.mul_le_mul_right _ (by omega)
+ split_ifs <;> omega
+
+/-- The first stack has its guard below it inside the CPU's area, and the last
+ IST stack with its guard above ends inside the 16 MiB per-CPU stride. -/
+theorem the_stack_area_has_a_guard_below_and_above_and_fits_its_stride :
+ ∃ o0 o8 : Std.U64,
+ memory.layout.manager.stack_slots.stack_slot_offset 0#usize = ok o0 ∧
+ memory.layout.manager.stack_slots.stack_slot_offset 8#usize = ok o8 ∧
+ 4096 ≤ o0.val ∧ o8.val + 32768 + 4096 ≤ 0x100_0000 := by
+ obtain ⟨o0, h0, v0⟩ := WP.spec_imp_exists (stack_slot_offset_spec 0#usize (by decide))
+ obtain ⟨o8, h8, v8⟩ := WP.spec_imp_exists (stack_slot_offset_spec 8#usize (by decide))
+ refine ⟨o0, o8, h0, h8, ?_, ?_⟩
+ · rw [v0]; decide
+ · rw [v8]; decide
+
+/-! ### Axiom profile -/
+
+#print axioms NonosExtraction.LayoutStackSlots.the_stack_slot_offset_wrapper_is_its_method
+#print axioms NonosExtraction.LayoutStackSlots.stack_slot_offset_spec
+#print axioms NonosExtraction.LayoutStackSlots.stack_slots_keep_a_guard_page_between_every_two_stacks
+#print axioms NonosExtraction.LayoutStackSlots.the_stack_area_has_a_guard_below_and_above_and_fits_its_stride
+
+end NonosExtraction.LayoutStackSlots
diff --git a/verification/extraction/lean/NonosExtraction/MainModeRefinement.lean b/verification/extraction/lean/NonosExtraction/MainModeRefinement.lean
index 1e333c1f00..5b41cb19e0 100644
--- a/verification/extraction/lean/NonosExtraction/MainModeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/MainModeRefinement.lean
@@ -35,8 +35,20 @@ namespace NonosExtraction.MainMode
theorem the_is_microkernel_wrapper_is_its_method :
is_microkernel = mode.is_microkernel := rfl
+/-! ### The kernel always boots as a microkernel
+
+`is_microkernel` answers whether the boot path runs in microkernel mode. The
+theorem below shows it answers yes, without failing, which agrees with
+`get_boot_mode` in the same source file returning `BootMode::Microkernel`, the
+only variant of that enum (`get_boot_mode` is not extracted, so the agreement is
+with its source). -/
+
+/-- The mode query answers true. -/
+theorem is_microkernel_answers_true : is_microkernel = ok true := rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.MainMode.the_is_microkernel_wrapper_is_its_method
+#print axioms NonosExtraction.MainMode.is_microkernel_answers_true
end NonosExtraction.MainMode
diff --git a/verification/extraction/lean/NonosExtraction/MemoryConstsRefinement.lean b/verification/extraction/lean/NonosExtraction/MemoryConstsRefinement.lean
index b81b15b0e5..463f134c49 100644
--- a/verification/extraction/lean/NonosExtraction/MemoryConstsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/MemoryConstsRefinement.lean
@@ -35,8 +35,95 @@ namespace NonosExtraction.MemoryConsts
theorem the_is_user_space_wrapper_is_its_method (a : Std.U64) (b : Std.Usize) :
is_user_space a b = consts.is_user_space a b := rfl
+/-! ### Which ranges count as user space
+
+`is_user_space(addr, len)` guards `sys_mmap` and `sys_munmap`: a request whose
+range fails it is refused before any page table is touched. The theorems below
+show that on a 64 bit target it accepts exactly the ranges with
+`addr + len ≤ USER_SPACE_MAX`, computed without the sum ever wrapping, and they
+fix the boundary: the canonical lower half ends at `USER_SPACE_MAX`, and a
+range that would run one byte past it is refused. A further theorem shows
+that, because `USER_SPACE_MAX` is the last user address rather than one past
+it, the topmost user page cannot be named by a page sized range, and says why
+that is kept. On a 32 bit target the cast to `usize` truncates, and nothing
+here speaks to that case. -/
+
+/-- The user space limit is the last address of the lower canonical half. -/
+theorem is_user_space_limit_is_the_top_of_the_lower_half :
+ consts.USER_SPACE_MAX.val = 2 ^ 47 - 1 := by
+ unfold consts.USER_SPACE_MAX; rfl
+
+/-- On a 64 bit target a range is user space exactly when its end,
+ `addr + len` computed in unbounded arithmetic, does not exceed
+ `USER_SPACE_MAX`; the check never fails. -/
+theorem is_user_space_holds_exactly_when_the_range_ends_below_the_limit
+ (h64 : System.Platform.numBits = 64) (addr : Std.U64) (len : Std.Usize) :
+ is_user_space addr len = ok (decide (addr.val + len.val ≤ 2 ^ 47 - 1)) := by
+ unfold is_user_space consts.is_user_space
+ have hm : consts.USER_SPACE_MAX.val = 2 ^ 47 - 1 :=
+ is_user_space_limit_is_the_top_of_the_lower_half
+ by_cases ha : addr.val ≤ 2 ^ 47 - 1
+ · have hle : addr ≤ consts.USER_SPACE_MAX := by
+ show addr.val ≤ consts.USER_SPACE_MAX.val; omega
+ simp only [hle, if_true]
+ have ⟨z, hz, hv⟩ := WP.spec_imp_exists
+ (U64.sub_spec (x := consts.USER_SPACE_MAX) (y := addr) (by scalar_tac))
+ rw [hz]
+ simp only [lift, bind_tc_ok]
+ congr 1
+ have hc : (UScalar.cast .Usize z).val = z.val := by
+ rw [UScalar.cast_val_eq]
+ simp only [UScalarTy.numBits, h64]
+ apply Nat.mod_eq_of_lt; scalar_tac
+ have hl : (len ≤ UScalar.cast .Usize z) ↔ len.val ≤ (UScalar.cast .Usize z).val :=
+ Iff.rfl
+ apply decide_eq_decide.mpr
+ rw [hl, hc, hv.1, hm]; omega
+ · have hle : ¬ (addr ≤ consts.USER_SPACE_MAX) := by
+ show ¬ (addr.val ≤ consts.USER_SPACE_MAX.val); omega
+ simp only [hle, if_false]
+ congr 1
+ symm; simp only [decide_eq_false_iff_not]; omega
+
+/-- An address past `USER_SPACE_MAX` is refused whatever the length, even an
+ empty range. -/
+theorem is_user_space_refuses_the_first_kernel_half_address (len : Std.Usize) :
+ is_user_space 140737488355328#u64 len = ok false := by
+ unfold is_user_space consts.is_user_space
+ have hle : ¬ (140737488355328#u64 ≤ consts.USER_SPACE_MAX) := by
+ unfold consts.USER_SPACE_MAX; decide
+ simp only [hle, if_false]
+
+/-- The page at `0x7FFF_FFFF_F000` lies wholly in the lower canonical half, but
+ `is_user_space` refuses the page sized range that names it, because it
+ requires `addr + len ≤ USER_SPACE_MAX` where `USER_SPACE_MAX` is the last
+ user byte rather than one past it. `sys_mmap` with a fixed address and
+ `sys_munmap` therefore reject the topmost user page. This is kept on
+ purpose: a `SYSCALL` in the last bytes of that page returns through
+ `SYSRET` to `2^47`, which is not canonical, and on Intel parts the fault is
+ taken in ring 0 with the user stack. Linux leaves the same page unmapped
+ for the same reason. -/
+theorem is_user_space_refuses_the_topmost_user_page
+ (h64 : System.Platform.numBits = 64) :
+ is_user_space 0x7FFFFFFFF000#u64 4096#usize = ok false := by
+ rw [is_user_space_holds_exactly_when_the_range_ends_below_the_limit h64]
+ rfl
+
+/-- One byte short of the end of the page is accepted, which pins the boundary
+ to exactly `USER_SPACE_MAX`. -/
+theorem is_user_space_accepts_the_topmost_page_less_its_last_byte
+ (h64 : System.Platform.numBits = 64) :
+ is_user_space 0x7FFFFFFFF000#u64 4095#usize = ok true := by
+ rw [is_user_space_holds_exactly_when_the_range_ends_below_the_limit h64]
+ rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.MemoryConsts.the_is_user_space_wrapper_is_its_method
+#print axioms NonosExtraction.MemoryConsts.is_user_space_limit_is_the_top_of_the_lower_half
+#print axioms NonosExtraction.MemoryConsts.is_user_space_holds_exactly_when_the_range_ends_below_the_limit
+#print axioms NonosExtraction.MemoryConsts.is_user_space_refuses_the_first_kernel_half_address
+#print axioms NonosExtraction.MemoryConsts.is_user_space_refuses_the_topmost_user_page
+#print axioms NonosExtraction.MemoryConsts.is_user_space_accepts_the_topmost_page_less_its_last_byte
end NonosExtraction.MemoryConsts
diff --git a/verification/extraction/lean/NonosExtraction/MemoryFrameAllocTypesRange.lean b/verification/extraction/lean/NonosExtraction/MemoryFrameAllocTypesRange.lean
index bb9a89e365..e5a9f88a84 100644
--- a/verification/extraction/lean/NonosExtraction/MemoryFrameAllocTypesRange.lean
+++ b/verification/extraction/lean/NonosExtraction/MemoryFrameAllocTypesRange.lean
@@ -37,18 +37,21 @@ structure memory.frame_alloc.types.range.FrameRange where
def memory.frame_alloc.constants.FRAME_SIZE : Std.U64 := 4096#u64
/-- [nonos_x_memory_frame_alloc_types_range::memory::addr::phys::{nonos_x_memory_frame_alloc_types_range::memory::addr::phys::PhysAddr}::align_up]:
- Source: 'src/memory/addr/../../../../../../../src/memory/addr/phys.rs', lines 50:4-52:5
+ Source: 'src/memory/addr/../../../../../../../src/memory/addr/phys.rs', lines 58:4-68:5
Visibility: public -/
def memory.addr.phys.PhysAddr.align_up
(self : memory.addr.phys.PhysAddr) (align : Std.U64) :
Result memory.addr.phys.PhysAddr
:= do
- let i ← self + align
- let i1 ← i - 1#u64
- let i2 ← align - 1#u64
- let i3 ← lift (~~~ i2)
- let i4 ← lift (i1 &&& i3)
- ok i4
+ if align = 0#u64
+ then ok self
+ else
+ let rem ← self % align
+ if rem = 0#u64
+ then ok self
+ else let i ← align - rem
+ let i1 ← self + i
+ ok i1
/-- [nonos_x_memory_frame_alloc_types_range::memory::addr::phys::{nonos_x_memory_frame_alloc_types_range::memory::addr::phys::PhysAddr}::as_u64]:
Source: 'src/memory/addr/../../../../../../../src/memory/addr/phys.rs', lines 30:4-32:5
diff --git a/verification/extraction/lean/NonosExtraction/MteModeRefinement.lean b/verification/extraction/lean/NonosExtraction/MteModeRefinement.lean
index 76b9cbfea9..40412d5467 100644
--- a/verification/extraction/lean/NonosExtraction/MteModeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/MteModeRefinement.lean
@@ -35,8 +35,43 @@ namespace NonosExtraction.MteMode
theorem the_mtemode_tcf_wrapper_is_its_method (a : mode.MteMode) :
mtemode_tcf a = mode.MteMode.tcf a := rfl
+/-! ### What the tag check fault field can hold
+
+ `set_mte_mode` in `security/mte/control.rs` clears the two-bit fields at
+ SCTLR_EL1 bits 40..41 (TCF) and 38..39 (TCF0) and ors `tcf() << 40` and
+ `tcf() << 38` into them. That is only sound if `tcf` never exceeds two bits: a
+ value of four or more would spill out of TCF0 into TCF, or out of TCF into
+ bit 42 and above. The theorems below establish that bound, that zero (no tag
+ checking) is produced by `Disabled` and by nothing else, and that the four
+ modes select four distinct fields. They cannot establish anything about the
+ register write itself, which is inline assembly behind a CPU feature test
+ and is not extracted.
+-/
+
+/-- Every mode fits in the two-bit TCF field, so shifting it to bit 40 or bit 38
+ touches only the bits `set_mte_mode` cleared. -/
+theorem mtemode_tcf_fits_the_two_bit_field (m : mode.MteMode) :
+ ∃ t, mtemode_tcf m = ok t ∧ t.val < 4 := by
+ cases m <;> exact ⟨_, rfl, by decide⟩
+
+/-- Tag checking is off exactly for `Disabled`: no enabled mode is silently
+ encoded as "tag check faults ignored". -/
+theorem mtemode_tcf_is_zero_only_when_disabled (m : mode.MteMode) :
+ mtemode_tcf m = ok 0#u64 ↔ m = mode.MteMode.Disabled := by
+ cases m <;> simp [mtemode_tcf, mode.MteMode.tcf]
+
+/-- The four modes program four different fields, so asking for one mode never
+ yields another's behaviour. -/
+theorem mtemode_tcf_tells_every_mode_apart (a b : mode.MteMode)
+ (h : mtemode_tcf a = mtemode_tcf b) : a = b := by
+ cases a <;> cases b <;> simp_all [mtemode_tcf, mode.MteMode.tcf]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.MteMode.the_mtemode_tcf_wrapper_is_its_method
+#print axioms NonosExtraction.MteMode.mtemode_tcf_fits_the_two_bit_field
+#print axioms NonosExtraction.MteMode.mtemode_tcf_is_zero_only_when_disabled
+#print axioms NonosExtraction.MteMode.mtemode_tcf_tells_every_mode_apart
+
end NonosExtraction.MteMode
diff --git a/verification/extraction/lean/NonosExtraction/MultibootModulesAcpi.lean b/verification/extraction/lean/NonosExtraction/MultibootModulesAcpi.lean
index 2a30213c45..fd5c05eff4 100644
--- a/verification/extraction/lean/NonosExtraction/MultibootModulesAcpi.lean
+++ b/verification/extraction/lean/NonosExtraction/MultibootModulesAcpi.lean
@@ -15,7 +15,7 @@ set_option maxRecDepth 2048
namespace nonos_x_multiboot_modules_acpi
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp]
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 18:0-27:1
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 18:0-28:1
Visibility: public -/
structure modules_acpi.AcpiRsdp where
signature : Array Std.U8 8#usize
@@ -26,16 +26,17 @@ structure modules_acpi.AcpiRsdp where
length : Option Std.U32
xsdt_address : Option Std.U64
extended_checksum : Option Std.U8
+ reserved : Option (Array Std.U8 3#usize)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::is_acpi2]:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 30:4-32:5
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 31:4-33:5
Visibility: public -/
def modules_acpi.AcpiRsdp.is_acpi2
(self : modules_acpi.AcpiRsdp) : Result Bool := do
ok (self.revision >= 2#u8)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::table_address]:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 36:4-45:5
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 37:4-46:5
Visibility: public -/
def modules_acpi.AcpiRsdp.table_address
(self : modules_acpi.AcpiRsdp) : Result Std.U64 := do
@@ -51,7 +52,7 @@ def modules_acpi.AcpiRsdp.table_address
else ok (UScalar.cast .U64 self.rsdt_address)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop body 0:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 49:8-51:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 50:8-52:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_checksum_loop0.body
@@ -66,7 +67,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop0.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop 0:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 49:8-51:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 50:8-52:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_checksum_loop0
@@ -77,7 +78,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop0
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop body 1:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 53:8-55:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 54:8-56:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_checksum_loop1.body
@@ -92,7 +93,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop1.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop 1:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 53:8-55:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 54:8-56:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_checksum_loop1
@@ -103,7 +104,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop1
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop body 2:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 57:8-59:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 58:8-60:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_checksum_loop2.body
@@ -118,7 +119,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop2.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]: loop 2:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 57:8-59:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 58:8-60:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_checksum_loop2
@@ -129,7 +130,7 @@ def modules_acpi.AcpiRsdp.verify_checksum_loop2
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_checksum]:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 47:4-61:5
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 48:4-62:5
Visibility: public -/
def modules_acpi.AcpiRsdp.verify_checksum
(self : modules_acpi.AcpiRsdp) : Result Bool := do
@@ -150,7 +151,7 @@ def modules_acpi.AcpiRsdp.verify_checksum
ok (sum4 = 0#u8)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 0:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 68:8-70:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 82:8-84:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop0.body
@@ -165,7 +166,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop0.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 0:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 68:8-70:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 82:8-84:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop0
@@ -176,7 +177,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop0
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 1:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 72:8-74:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 86:8-88:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop1.body
@@ -191,7 +192,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop1.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 1:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 72:8-74:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 86:8-88:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop1
@@ -202,7 +203,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop1
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 2:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 76:8-78:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 90:8-92:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop2.body
@@ -217,7 +218,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop2.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 2:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 76:8-78:9
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 90:8-92:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop2
@@ -228,7 +229,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop2
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 3:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 85:12-87:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 93:8-95:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop3.body
@@ -243,7 +244,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop3.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 3:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 85:12-87:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 93:8-95:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop3
@@ -254,7 +255,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop3
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 4:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 80:12-82:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 96:8-98:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop4.body
@@ -269,7 +270,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop4.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 4:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 80:12-82:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 96:8-98:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop4
@@ -280,7 +281,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop4
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop body 5:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 85:12-87:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 100:8-102:9
Visibility: public -/
@[rust_loop_body]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop5.body
@@ -295,7 +296,7 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop5.body
ok (cont (iter1, sum1))
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]: loop 5:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 85:12-87:13
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 100:8-102:9
Visibility: public -/
@[rust_loop]
def modules_acpi.AcpiRsdp.verify_extended_checksum_loop5
@@ -306,76 +307,65 @@ def modules_acpi.AcpiRsdp.verify_extended_checksum_loop5
(iter, sum)
/-- [nonos_x_multiboot_modules_acpi::modules_acpi::{nonos_x_multiboot_modules_acpi::modules_acpi::AcpiRsdp}::verify_extended_checksum]:
- Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 63:4-93:5
+ Source: 'src/../../../../../src/arch/x86_64/multiboot/modules_acpi.rs', lines 69:4-104:5
Visibility: public -/
def modules_acpi.AcpiRsdp.verify_extended_checksum
(self : modules_acpi.AcpiRsdp) : Result Bool := do
let b ← modules_acpi.AcpiRsdp.is_acpi2 self
if b
then
- let iter ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
- self.signature
- let sum ← modules_acpi.AcpiRsdp.verify_extended_checksum_loop0 iter 0#u8
- let sum1 ← lift (core.num.U8.wrapping_add sum self.checksum)
- let iter1 ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
- self.oem_id
- let sum2 ←
- modules_acpi.AcpiRsdp.verify_extended_checksum_loop1 iter1 sum1
- let sum3 ← lift (core.num.U8.wrapping_add sum2 self.revision)
- let a ← lift (core.num.U32.to_le_bytes self.rsdt_address)
- let iter2 ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter a
- let sum4 ←
- modules_acpi.AcpiRsdp.verify_extended_checksum_loop2 iter2 sum3
match self.length with
- | none =>
- match self.xsdt_address with
- | none =>
- let sum5 ←
- match self.extended_checksum with
- | none => ok sum4
- | some ext => ok (core.num.U8.wrapping_add sum4 ext)
- ok (sum5 = 0#u8)
- | some xsdt =>
- let a1 ← lift (core.num.U64.to_le_bytes xsdt)
- let iter3 ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
- a1
- let sum5 ←
- modules_acpi.AcpiRsdp.verify_extended_checksum_loop3 iter3 sum4
- let sum6 ←
- match self.extended_checksum with
- | none => ok sum5
- | some ext => ok (core.num.U8.wrapping_add sum5 ext)
- ok (sum6 = 0#u8)
+ | none => ok false
| some len =>
- let a1 ← lift (core.num.U32.to_le_bytes len)
- let iter3 ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
- a1
- let sum5 ←
- modules_acpi.AcpiRsdp.verify_extended_checksum_loop4 iter3 sum4
match self.xsdt_address with
- | none =>
- let sum6 ←
- match self.extended_checksum with
- | none => ok sum5
- | some ext => ok (core.num.U8.wrapping_add sum5 ext)
- ok (sum6 = 0#u8)
+ | none => ok false
| some xsdt =>
- let a2 ← lift (core.num.U64.to_le_bytes xsdt)
- let iter4 ←
- SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
- a2
- let sum6 ←
- modules_acpi.AcpiRsdp.verify_extended_checksum_loop5 iter4 sum5
- let sum7 ←
- match self.extended_checksum with
- | none => ok sum6
- | some ext => ok (core.num.U8.wrapping_add sum6 ext)
- ok (sum7 = 0#u8)
+ match self.extended_checksum with
+ | none => ok false
+ | some ext =>
+ match self.reserved with
+ | none => ok false
+ | some reserved =>
+ if len != 36#u32
+ then ok false
+ else
+ let iter ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ self.signature
+ let sum ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop0 iter 0#u8
+ let sum1 ← lift (core.num.U8.wrapping_add sum self.checksum)
+ let iter1 ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ self.oem_id
+ let sum2 ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop1 iter1 sum1
+ let sum3 ← lift (core.num.U8.wrapping_add sum2 self.revision)
+ let a ← lift (core.num.U32.to_le_bytes self.rsdt_address)
+ let iter2 ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ a
+ let sum4 ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop2 iter2 sum3
+ let a1 ← lift (core.num.U32.to_le_bytes len)
+ let iter3 ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ a1
+ let sum5 ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop3 iter3 sum4
+ let a2 ← lift (core.num.U64.to_le_bytes xsdt)
+ let iter4 ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ a2
+ let sum6 ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop4 iter4 sum5
+ let sum7 ← lift (core.num.U8.wrapping_add sum6 ext)
+ let iter5 ←
+ SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter
+ reserved
+ let sum8 ←
+ modules_acpi.AcpiRsdp.verify_extended_checksum_loop5 iter5 sum7
+ ok (sum8 = 0#u8)
else ok true
/-- [nonos_x_multiboot_modules_acpi::acpirsdp_is_acpi2]:
diff --git a/verification/extraction/lean/NonosExtraction/MultibootModulesAcpiRefinement.lean b/verification/extraction/lean/NonosExtraction/MultibootModulesAcpiRefinement.lean
index 8fba9f404e..2689c427c4 100644
--- a/verification/extraction/lean/NonosExtraction/MultibootModulesAcpiRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/MultibootModulesAcpiRefinement.lean
@@ -62,10 +62,15 @@ theorem the_acpirsdp_verify_extended_checksum_wrapper_is_its_method (a : modules
ignored and the RSDT address returned. It used to hand out a nonzero XSDT
pointer whatever the revision; `kernel_proofs` keeps a test of that case.
- One property is recorded as it is, not as the ACPI parser has it. With
- revision 2 but no extension fields, which is what `parse_acpi_rsdp` produces for a tag 14 RSDP
- or a short tag 15 one, the extended check is the ACPI 1.0 check and nothing
- more. It also never sums the three reserved bytes or checks `length`.
+ From revision 2 the extended check passes only when every ACPI 2.0 field is
+ present, `length` is exactly 36 and all 36 bytes, the three reserved ones
+ included, sum to zero. The extended checksum covers as many bytes as
+ `length` declares, and the structure keeps 36, so a longer declaration is
+ refused rather than leaving bytes unchecked. It used to pass a revision 2
+ RSDP with no extended fields as the ACPI 1.0 check alone, which is what
+ `parse_acpi_rsdp` produces for a tag 14 RSDP or a short tag 15 one, it never
+ summed the reserved bytes or checked `length`, and for a while after that it
+ accepted any `length` of 36 or more.
These theorems are about the extracted methods only. The parser that fills
the structure from boot memory reads raw pointers and is not extracted, and no
@@ -87,13 +92,15 @@ def rsdpV1ByteSum (r : modules_acpi.AcpiRsdp) : Nat :=
byteSum r.signature.val + r.checksum.val + byteSum r.oem_id.val + r.revision.val
+ leByteSum 4 r.rsdt_address.val
-/-- The unwrapped sum of whichever ACPI 2.0 fields are present: the four
- little-endian bytes of `length`, the eight of `xsdt_address`, and the extended
- checksum byte. An absent field contributes nothing. -/
-def rsdpExtensionByteSum (r : modules_acpi.AcpiRsdp) : Nat :=
- (match r.length with | none => 0 | some l => leByteSum 4 l.val)
- + (match r.xsdt_address with | none => 0 | some x => leByteSum 8 x.val)
- + (match r.extended_checksum with | none => 0 | some e => e.val)
+/-- The extended check the kernel should make: every ACPI 2.0 field present, a
+ length of exactly 36, and the 36 bytes, reserved ones included, summing to
+ zero modulo 256. -/
+def rsdpExtendedCheck (r : modules_acpi.AcpiRsdp) : Bool :=
+ match r.length, r.xsdt_address, r.extended_checksum, r.reserved with
+ | some l, some x, some e, some res =>
+ decide (l.val = 36 ∧ (rsdpV1ByteSum r + leByteSum 4 l.val + leByteSum 8 x.val + e.val
+ + byteSum res.val) % 256 = 0)
+ | _, _, _, _ => false
/-- One pass of the byte loop over a slice from position `i` is a wrapping fold
of the remaining bytes. -/
@@ -243,14 +250,13 @@ theorem acpirsdp_verify_extended_checksum_passes_whatever_is_not_acpi2
unfold acpirsdp_is_acpi2 at h
simp only [h, bind_tc_ok, Bool.false_eq_true, ↓reduceIte]
-/-- From revision 2 up, the extended check is the 20-byte sum plus the bytes of
- whichever ACPI 2.0 fields are present, zero modulo 256. Below revision 2 it
- passes. -/
-theorem acpirsdp_verify_extended_checksum_is_the_sum_of_the_present_fields
+/-- From revision 2 up, the extended check is `rsdpExtendedCheck`: all ACPI 2.0
+ fields present, a length of exactly 36, and the 36 bytes summing to zero.
+ Below revision 2 it passes, as there is nothing extended to check. -/
+theorem acpirsdp_verify_extended_checksum_is_the_full_36_byte_check
(r : modules_acpi.AcpiRsdp) :
acpirsdp_verify_extended_checksum r =
- ok (if 2 ≤ r.revision.val
- then decide ((rsdpV1ByteSum r + rsdpExtensionByteSum r) % 256 = 0) else true) := by
+ ok (if 2 ≤ r.revision.val then rsdpExtendedCheck r else true) := by
have e0 : modules_acpi.AcpiRsdp.verify_extended_checksum_loop0 =
modules_acpi.AcpiRsdp.verify_checksum_loop0 := rfl
have e1 : modules_acpi.AcpiRsdp.verify_extended_checksum_loop1 =
@@ -270,29 +276,54 @@ theorem acpirsdp_verify_extended_checksum_is_the_sum_of_the_present_fields
by_cases hr : 2 ≤ r.revision.val
· have hb : r.revision ≥ 2#u8 := by scalar_tac
simp only [hb, decide_true, ↓reduceIte, hr]
- unfold rsdpV1ByteSum rsdpExtensionByteSum
+ unfold rsdpExtendedCheck rsdpV1ByteSum
rcases hl : r.length with _ | l <;> rcases hx : r.xsdt_address with _ | x <;>
- rcases he : r.extended_checksum with _ | e <;>
- simp only [SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter,
+ rcases he : r.extended_checksum with _ | e <;> rcases hres : r.reserved with _ | res <;>
+ try rfl
+ by_cases h36 : l.val = 36
+ · have hl36 : (l != 36#u32) = false := by
+ have : l = 36#u32 := UScalar.eq_of_val_eq h36
+ simp [this]
+ simp only [hl36, Bool.false_eq_true, ↓reduceIte, SharedArray.Insts.CoreIterTraitsCollectIntoIteratorSharedIter.into_iter,
bind_tc_ok, lift, e0, e1, e2, e3, e4, e5, the_byte_loop_folds_its_iterator,
List.drop_zero, ok.injEq, decide_eq_decide, a_byte_is_zero_exactly_when_its_value_is,
- a_wrapping_fold_is_the_sum_mod_256, a_wrapping_byte_add_is_addition_mod_256, four_little_endian_bytes_sum_to_their_digits,
- eight_little_endian_bytes_sum_to_their_digits, hz] <;> omega
+ a_wrapping_fold_is_the_sum_mod_256, a_wrapping_byte_add_is_addition_mod_256,
+ four_little_endian_bytes_sum_to_their_digits, eight_little_endian_bytes_sum_to_their_digits, hz]
+ omega
+ · have hl36 : (l != 36#u32) = true := by
+ simp only [bne_iff_ne, ne_eq]
+ intro e; exact h36 (by rw [e]; rfl)
+ simp only [hl36, ↓reduceIte]
+ congr 1
+ simp only [Bool.false_eq, decide_eq_false_iff_not, not_and]
+ intro h; exact absurd h h36
· have hb : ¬ r.revision ≥ 2#u8 := by scalar_tac
simp only [hb, hr, decide_false, ↓reduceIte, Bool.false_eq_true]
-/-- This records a defect. With revision 2 or more but none of the ACPI 2.0 fields,
- the extended check is the ACPI 1.0 check: it verifies nothing that
- `verify_checksum` has not, so a caller that asks for both learns no more than
- from one. -/
-theorem without_extension_fields_acpirsdp_verify_extended_checksum_is_the_v1_check
- (r : modules_acpi.AcpiRsdp) (hr : 2 ≤ r.revision.val) (hl : r.length = none)
- (hx : r.xsdt_address = none) (he : r.extended_checksum = none) :
- acpirsdp_verify_extended_checksum r = acpirsdp_verify_checksum r := by
- rw [acpirsdp_verify_extended_checksum_is_the_sum_of_the_present_fields,
- acpirsdp_verify_checksum_is_the_twenty_byte_sum]
- unfold rsdpExtensionByteSum
- simp only [hr, ↓reduceIte, hl, hx, he, Nat.add_zero]
+/-- A revision 2 RSDP missing any ACPI 2.0 field fails the extended check. It
+ used to pass as the ACPI 1.0 check alone. -/
+theorem an_acpi2_rsdp_without_its_extended_fields_fails
+ (r : modules_acpi.AcpiRsdp) (hr : 2 ≤ r.revision.val)
+ (h : r.length = none ∨ r.xsdt_address = none ∨ r.extended_checksum = none ∨
+ r.reserved = none) :
+ acpirsdp_verify_extended_checksum r = ok false := by
+ rw [acpirsdp_verify_extended_checksum_is_the_full_36_byte_check]
+ simp only [hr, ↓reduceIte]
+ unfold rsdpExtendedCheck
+ rcases h with h | h | h | h <;> simp [h]
+
+/-- Any length other than 36 fails it too, whatever the bytes sum to: a shorter
+ table cannot hold the fields, and a longer one would carry bytes past the
+ 36 the structure keeps, which the checksum would then not cover. -/
+theorem an_acpi2_rsdp_whose_length_is_not_36_fails
+ (r : modules_acpi.AcpiRsdp) (l : Std.U32) (hr : 2 ≤ r.revision.val)
+ (hl : r.length = some l) (h36 : l.val ≠ 36) :
+ acpirsdp_verify_extended_checksum r = ok false := by
+ rw [acpirsdp_verify_extended_checksum_is_the_full_36_byte_check]
+ simp only [hr, ↓reduceIte]
+ unfold rsdpExtendedCheck
+ rw [hl]
+ split <;> simp_all
private theorem is_acpi2_true (r : modules_acpi.AcpiRsdp) (hr : 2 ≤ r.revision.val) :
modules_acpi.AcpiRsdp.is_acpi2 r = ok true := by
@@ -384,8 +415,9 @@ theorem acpirsdp_table_address_hands_out_the_xsdt_only_from_revision_two
#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_verify_checksum_is_the_twenty_byte_sum
#print axioms NonosExtraction.MultibootModulesAcpi.exactly_one_checksum_byte_passes_acpirsdp_verify_checksum
#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_verify_extended_checksum_passes_whatever_is_not_acpi2
-#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_verify_extended_checksum_is_the_sum_of_the_present_fields
-#print axioms NonosExtraction.MultibootModulesAcpi.without_extension_fields_acpirsdp_verify_extended_checksum_is_the_v1_check
+#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_verify_extended_checksum_is_the_full_36_byte_check
+#print axioms NonosExtraction.MultibootModulesAcpi.an_acpi2_rsdp_without_its_extended_fields_fails
+#print axioms NonosExtraction.MultibootModulesAcpi.an_acpi2_rsdp_whose_length_is_not_36_fails
#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_table_address_prefers_a_nonzero_xsdt
#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_table_address_falls_back_to_the_rsdt
#print axioms NonosExtraction.MultibootModulesAcpi.acpirsdp_table_address_ignores_the_xsdt_below_revision_two
diff --git a/verification/extraction/lean/NonosExtraction/MultisigConstantsRefinement.lean b/verification/extraction/lean/NonosExtraction/MultisigConstantsRefinement.lean
index e87d4ddc86..2d6fbe74da 100644
--- a/verification/extraction/lean/NonosExtraction/MultisigConstantsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/MultisigConstantsRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.MultisigConstants
+import Nonos.MultiSig
open Aeneas Aeneas.Std Result
open nonos_x_multisig_constants
@@ -38,9 +39,68 @@ theorem the_max_signers_wrapper_is_its_method :
theorem the_max_threshold_wrapper_is_its_method :
max_threshold = constants.max_threshold := rfl
+/-! ### The signer cap is sixteen and the threshold cap is the same bound
+
+`max_signers` reports the cap that `validate_params` in
+`src/capabilities/multisig/create.rs` enforces on the number of authorized
+signers, and `max_threshold` reports the largest threshold a token may carry.
+The two agree, and both are sixteen. Read against the tier-one model of
+`validate_params` in `Nonos.MultiSig`, with the extracted cap plugged in, a
+config of sixteen signers is accepted while seventeen are refused as too many,
+a sixteen of sixteen token is accepted, and every accepted threshold is at most
+`max_threshold`, so the advertised threshold cap is a true bound on what the
+kernel will create.
+
+These theorems rely on `validate_params` reading the same `MAX_SIGNERS` that is
+extracted here; `create.rs` itself is not extracted (it allocates), so that link
+is by the shared `use super::constants::MAX_SIGNERS` import, not by proof. -/
+
+/-- The signer cap is sixteen. -/
+theorem max_signers_is_sixteen : ∃ n, max_signers = ok n ∧ n.val = 16 := by
+ refine ⟨constants.MAX_SIGNERS, rfl, ?_⟩
+ unfold constants.MAX_SIGNERS
+ rfl
+
+/-- The threshold cap and the signer cap are the same number. -/
+theorem max_threshold_is_max_signers : max_threshold = max_signers := by
+ unfold max_threshold max_signers constants.max_threshold constants.max_signers
+ unfold constants.MAX_THRESHOLD
+ rfl
+
+/-- At the extracted cap, sixteen signers pass `validate_params` and seventeen
+ are refused as too many. -/
+theorem max_signers_is_the_exact_boundary_of_validate_params (n : Std.Usize)
+ (h : max_signers = ok n) :
+ Nonos.MultiSig.validateParams 1 16 n.val = .ok ∧
+ Nonos.MultiSig.validateParams 1 17 n.val = .tooManySigners := by
+ obtain ⟨m, hm, h16⟩ := max_signers_is_sixteen
+ rw [hm] at h
+ cases h
+ rw [h16]
+ decide
+
+/-- Every threshold that `validate_params` accepts under the extracted signer
+ cap is at most `max_threshold`, and `max_threshold` itself is accepted with
+ that many signers. -/
+theorem max_threshold_bounds_every_accepted_threshold (t : Std.Usize)
+ (h : max_threshold = ok t) :
+ (∀ k n, Nonos.MultiSig.validateParams k n (t.val) = .ok → k ≤ t.val) ∧
+ Nonos.MultiSig.validateParams t.val t.val t.val = .ok := by
+ rw [max_threshold_is_max_signers] at h
+ obtain ⟨m, hm, h16⟩ := max_signers_is_sixteen
+ rw [hm] at h
+ cases h
+ refine ⟨fun k n hv => ?_, by rw [h16]; decide⟩
+ have := Nonos.MultiSig.valid_config k n _ hv
+ omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.MultisigConstants.the_max_signers_wrapper_is_its_method
#print axioms NonosExtraction.MultisigConstants.the_max_threshold_wrapper_is_its_method
+#print axioms NonosExtraction.MultisigConstants.max_signers_is_sixteen
+#print axioms NonosExtraction.MultisigConstants.max_threshold_is_max_signers
+#print axioms NonosExtraction.MultisigConstants.max_signers_is_the_exact_boundary_of_validate_params
+#print axioms NonosExtraction.MultisigConstants.max_threshold_bounds_every_accepted_threshold
end NonosExtraction.MultisigConstants
diff --git a/verification/extraction/lean/NonosExtraction/OffsetsFaultRefinement.lean b/verification/extraction/lean/NonosExtraction/OffsetsFaultRefinement.lean
index f7b82f0db7..3673a0e45e 100644
--- a/verification/extraction/lean/NonosExtraction/OffsetsFaultRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/OffsetsFaultRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.OffsetsFault
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_offsets_fault
@@ -38,9 +39,85 @@ theorem the_frcd_reason_wrapper_is_its_method (a : Std.U64) :
theorem the_frcd_source_wrapper_is_its_method (a : Std.U64) :
frcd_source a = fault.frcd_source a := rfl
+/-! ### Which bits of a fault record each reader returns
+
+`take_fault` reads the high half of a VT-d fault recording register once and
+builds a `FaultRecord` from it: the requester that faulted from `frcd_source`,
+the reason code from `frcd_reason`, and the fault and read flags from the
+`FRCD_FAULT` and `FRCD_TYPE_READ` masks (bits 63 and 62). The theorems below say
+that `frcd_source` is exactly bits 0 to 15 of the word and `frcd_reason` is
+exactly bits 32 to 39, and that neither ever fails. They then state the
+agreement a caller relies on: for a word laid out as the specification lays
+it out (source id in the low sixteen bits, reason at bit 32, the fault bit set
+and the type bit either way), the two readers give back the source and the
+reason that were put there. A reader that shifted by 24, or kept only seven
+bits of the reason, or whose field overlapped the flag bits, would report the
+wrong device or the wrong cause.
+
+`take_fault` itself, the MMIO reads through `RemapUnit`, and
+`cap::fault_recording_offset` are not in this crate, so which register the word
+comes from is not established here, and the flag masks are kernel constants the
+extraction does not carry: the round trip states their bit positions directly.
+-/
+
+/-- A right shift of a 64-bit word by a constant below 64 succeeds and divides
+ by that power of two. -/
+private theorem shr_u64 (x : Std.U64) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 64) :
+ ∃ z : Std.U64, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+/-- The reason code is bits 32 to 39 of the record's high word. -/
+theorem frcd_reason_is_bits_thirty_two_to_thirty_nine (high : Std.U64) :
+ ∃ r : Std.U8, frcd_reason high = ok r ∧ r.val = high.val / 2 ^ 32 % 256 := by
+ unfold frcd_reason fault.frcd_reason
+ obtain ⟨z, hz, hv⟩ := shr_u64 high 32#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ refine ⟨_, rfl, ?_⟩
+ rw [UScalar.cast_val_eq, Bits.land_low_mask z 255#u64 8 rfl, hv]
+ simp only [show (32#i32 : Std.I32).toNat = 32 from rfl]
+ simp [UScalarTy.numBits]
+
+/-- The source id is the low sixteen bits of the record's high word. -/
+theorem frcd_source_is_the_low_sixteen_bits (high : Std.U64) :
+ ∃ s : Std.U16, frcd_source high = ok s ∧ s.val = high.val % 65536 := by
+ unfold frcd_source fault.frcd_source
+ simp only [bind_tc_ok, lift]
+ refine ⟨_, rfl, ?_⟩
+ rw [UScalar.cast_val_eq, Bits.land_low_mask high 65535#u64 16 rfl]
+ simp [UScalarTy.numBits]
+
+/-- A record laid out as the hardware lays it out reads back as the source and
+ reason it carries, whatever the read flag says. The fault and type bits do
+ not leak into either field. -/
+theorem frcd_source_and_frcd_reason_read_back_a_recorded_fault
+ (sid : Std.U16) (reason : Std.U8) (read : Bool) (high : Std.U64)
+ (h : high.val = 2 ^ 63 + (if read then 2 ^ 62 else 0) + reason.val * 2 ^ 32 + sid.val) :
+ frcd_source high = ok sid ∧ frcd_reason high = ok reason := by
+ have hs := sid.hBounds
+ have hr := reason.hBounds
+ simp [UScalarTy.numBits] at hs hr
+ obtain ⟨s, hs1, hsv⟩ := frcd_source_is_the_low_sixteen_bits high
+ obtain ⟨r, hr1, hrv⟩ := frcd_reason_is_bits_thirty_two_to_thirty_nine high
+ refine ⟨?_, ?_⟩
+ · rw [hs1]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hsv, h]
+ cases read <;> simp <;> omega
+ · rw [hr1]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hrv, h]
+ cases read <;> simp <;> omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.OffsetsFault.the_frcd_reason_wrapper_is_its_method
#print axioms NonosExtraction.OffsetsFault.the_frcd_source_wrapper_is_its_method
+#print axioms NonosExtraction.OffsetsFault.frcd_reason_is_bits_thirty_two_to_thirty_nine
+#print axioms NonosExtraction.OffsetsFault.frcd_source_is_the_low_sixteen_bits
+#print axioms NonosExtraction.OffsetsFault.frcd_source_and_frcd_reason_read_back_a_recorded_fault
end NonosExtraction.OffsetsFault
diff --git a/verification/extraction/lean/NonosExtraction/OffsetsInvalidateRefinement.lean b/verification/extraction/lean/NonosExtraction/OffsetsInvalidateRefinement.lean
index a5982b7f09..9de23d6c27 100644
--- a/verification/extraction/lean/NonosExtraction/OffsetsInvalidateRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/OffsetsInvalidateRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.OffsetsInvalidate
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_offsets_invalidate
@@ -38,9 +39,110 @@ theorem the_iva_offset_wrapper_is_its_method (a : Std.U64) :
theorem the_iotlb_offset_wrapper_is_its_method (a : Std.U64) :
iotlb_offset a = invalidate.iotlb_offset a := rfl
+/-! ### Where the IOTLB registers are, and whether they are in the mapped page
+
+VT-d does not fix the offset of the IOTLB invalidation registers: the ECAP
+register carries it in its IRO field, bits 8 to 17, in 16-byte units.
+`iva_offset` decodes that field and `iotlb_offset` is the IOTLB register eight
+bytes above it, which `invalidate_iotlb_global` writes and then polls. The
+theorems below say exactly which ECAP bits are read and how they are scaled,
+that neither function can overflow for any ECAP value, and that the IOTLB
+register is always the eight bytes after the IVA register.
+
+They also relate these offsets to the register window. The kernel maps one
+4 KiB page per remapping unit (`UNIT_WINDOW` in `unit/access.rs`, used by
+`probe`). A ten-bit IRO field can place the IOTLB register as far as 16376
+bytes in, and it lies inside the page exactly when the field is at most 255.
+The offsets themselves are not bounded, so the bound has to be enforced by a
+caller. It used not to be: `RemapUnit::read64` and `write64` checked
+`offset + 8 <= UNIT_WINDOW` only with `debug_assert!`, and nothing between
+`probe` and `invalidate_iotlb_global` compared the decoded offset with the
+window, so an ECAP with a larger IRO sent a release build's volatile write
+outside the mapping. `probe_at` now refuses such a unit through
+`registers_fit`, proven in `NonosExtraction.IommuRegsWindow`, and the accessors
+assert the bound in every build. The `RemapUnit` accessors, the mapping, and
+the MMIO accesses are not in this crate; the window size is restated here as
+the literal 4096.
+-/
+
+/-- A right shift of a 64-bit word by a constant below 64 succeeds and divides
+ by that power of two. -/
+private theorem shr_u64 (x : Std.U64) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 64) :
+ ∃ z : Std.U64, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+/-- The IVA register offset is the ten-bit IRO field, ECAP bits 8 to 17, times
+ 16. It never fails, whatever ECAP holds. -/
+theorem iva_offset_is_the_iro_field_in_sixteen_byte_units (ecap : Std.U64) :
+ ∃ v : Std.Usize, iva_offset ecap = ok v ∧ v.val = ecap.val / 2 ^ 8 % 1024 * 16 := by
+ unfold iva_offset invalidate.iva_offset
+ obtain ⟨z, hz, hv⟩ := shr_u64 ecap 8#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ have hm : (z &&& 1023#u64).val = ecap.val / 2 ^ 8 % 1024 := by
+ rw [Bits.land_low_mask z 1023#u64 10 rfl, hv]
+ rfl
+ have hc : (UScalar.cast .Usize (z &&& 1023#u64)).val = ecap.val / 2 ^ 8 % 1024 := by
+ rw [UScalar.cast_val_eq, hm]
+ have : ecap.val / 2 ^ 8 % 1024 < 1024 := Nat.mod_lt _ (by decide)
+ apply Nat.mod_eq_of_lt
+ have := Usize.numBits_eq
+ rcases System.Platform.numBits_eq with h | h <;> simp_all [UScalarTy.numBits] <;> omega
+ obtain ⟨w, hw, hwv, -⟩ := WP.spec_imp_exists
+ (Usize.mul_bv_spec (x := UScalar.cast .Usize (z &&& 1023#u64)) (y := 16#usize)
+ (by rw [hc]; have := Nat.mod_lt (ecap.val / 2 ^ 8) (show 1024 > 0 by decide); scalar_tac))
+ exact ⟨w, hw, by rw [hwv, hc]; rfl⟩
+
+/-- The IOTLB register is the eight bytes after the IVA register, for every
+ ECAP, and the addition never overflows. -/
+theorem iotlb_offset_is_eight_past_iva_offset (ecap : Std.U64) :
+ ∃ v w : Std.Usize, iva_offset ecap = ok v ∧ iotlb_offset ecap = ok w ∧
+ w.val = v.val + 8 := by
+ obtain ⟨v, hv, hvv⟩ := iva_offset_is_the_iro_field_in_sixteen_byte_units ecap
+ have hlt : v.val ≤ 16368 := by
+ have := Nat.mod_lt (ecap.val / 2 ^ 8) (show 1024 > 0 by decide)
+ omega
+ obtain ⟨w, hw, hwv, -⟩ := WP.spec_imp_exists
+ (Usize.add_bv_spec (x := v) (y := 8#usize) (by scalar_tac))
+ refine ⟨v, w, hv, ?_, by rw [hwv]; rfl⟩
+ unfold iotlb_offset invalidate.iotlb_offset
+ rw [show invalidate.iva_offset ecap = ok v from hv, bind_tc_ok, hw]
+
+/-- The IOTLB register fits inside the one 4 KiB page the kernel maps for a
+ unit exactly when the IRO field is at most 255. `iotlb_offset` does not
+ bound the field to the window; `registers_fit` in `probe_at` does, so a
+ unit outside this range is never brought up. -/
+theorem iotlb_offset_is_inside_the_unit_window_only_for_small_iro (ecap : Std.U64) :
+ ∃ w : Std.Usize, iotlb_offset ecap = ok w ∧
+ (w.val + 8 ≤ 4096 ↔ ecap.val / 2 ^ 8 % 1024 ≤ 255) := by
+ obtain ⟨v, w, hv, hw, hwv⟩ := iotlb_offset_is_eight_past_iva_offset ecap
+ obtain ⟨v', hv', hvv⟩ := iva_offset_is_the_iro_field_in_sixteen_byte_units ecap
+ rw [hv] at hv'
+ cases hv'
+ exact ⟨w, hw, by omega⟩
+
+/-- A concrete ECAP whose IRO field is 256: the IOTLB register lands at byte
+ 4104, past the end of the 4096-byte window. -/
+theorem iotlb_offset_of_iro_256_is_past_the_unit_window :
+ ∃ w : Std.Usize, iotlb_offset 65536#u64 = ok w ∧ w.val = 4104 ∧ 4096 < w.val + 8 := by
+ obtain ⟨w, hw, hi⟩ := iotlb_offset_is_inside_the_unit_window_only_for_small_iro 65536#u64
+ obtain ⟨v, w', hv, hw', hwv⟩ := iotlb_offset_is_eight_past_iva_offset 65536#u64
+ obtain ⟨v', hv', hvv⟩ := iva_offset_is_the_iro_field_in_sixteen_byte_units 65536#u64
+ rw [hv] at hv'
+ cases hv'
+ rw [hw] at hw'
+ cases hw'
+ have : v.val = 4096 := by rw [hvv]; rfl
+ exact ⟨w, hw, by omega, by omega⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.OffsetsInvalidate.the_iva_offset_wrapper_is_its_method
#print axioms NonosExtraction.OffsetsInvalidate.the_iotlb_offset_wrapper_is_its_method
+#print axioms NonosExtraction.OffsetsInvalidate.iva_offset_is_the_iro_field_in_sixteen_byte_units
+#print axioms NonosExtraction.OffsetsInvalidate.iotlb_offset_is_eight_past_iva_offset
+#print axioms NonosExtraction.OffsetsInvalidate.iotlb_offset_is_inside_the_unit_window_only_for_small_iro
+#print axioms NonosExtraction.OffsetsInvalidate.iotlb_offset_of_iro_256_is_past_the_unit_window
end NonosExtraction.OffsetsInvalidate
diff --git a/verification/extraction/lean/NonosExtraction/PacKeyRefinement.lean b/verification/extraction/lean/NonosExtraction/PacKeyRefinement.lean
index 819b3bbe89..f6a4c6d9c5 100644
--- a/verification/extraction/lean/NonosExtraction/PacKeyRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PacKeyRefinement.lean
@@ -35,8 +35,39 @@ namespace NonosExtraction.PacKey
theorem the_packey_new_wrapper_is_its_method (a : Std.U64) (b : Std.U64) :
packey_new a b = key.PacKey.new a b := rfl
+/-! ### A key keeps its halves where they were given
+
+`keygen.rs` builds every pointer-authentication key as
+`PacKey::new(read_rndr(), read_rndr())` and `pac_apply` later loads `lo` and
+`hi` into the `*KEYLO_EL1` and `*KEYHI_EL1` registers. These theorems establish
+that the constructor stores each argument in its own field, so the two halves
+are neither swapped nor collapsed into one, and that distinct argument pairs
+always give distinct keys (no entropy from either draw is dropped).
+
+They cannot establish anything about the randomness itself: `read_rndr`, the
+system register writes and `from_bytes` are not in this crate.
+-/
+
+/-- The low half is `lo` and the high half is `hi`, and construction always
+ succeeds. -/
+theorem packey_new_stores_lo_in_lo_and_hi_in_hi (lo hi : Std.U64) :
+ packey_new lo hi = ok { lo := lo, hi := hi } := by
+ unfold packey_new key.PacKey.new
+ rfl
+
+/-- Two keys built by `packey_new` are equal only when both halves were equal,
+ so a key depends on every bit of both arguments. -/
+theorem packey_new_keeps_every_bit_of_both_halves (a b c d : Std.U64)
+ (h : packey_new a b = packey_new c d) : a = c ∧ b = d := by
+ rw [packey_new_stores_lo_in_lo_and_hi_in_hi, packey_new_stores_lo_in_lo_and_hi_in_hi] at h
+ cases h
+ exact ⟨rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PacKey.the_packey_new_wrapper_is_its_method
+#print axioms NonosExtraction.PacKey.packey_new_stores_lo_in_lo_and_hi_in_hi
+#print axioms NonosExtraction.PacKey.packey_new_keeps_every_bit_of_both_halves
+
end NonosExtraction.PacKey
diff --git a/verification/extraction/lean/NonosExtraction/PagingRefinement.lean b/verification/extraction/lean/NonosExtraction/PagingRefinement.lean
index 1f4b6d7a0a..c019765d3a 100644
--- a/verification/extraction/lean/NonosExtraction/PagingRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PagingRefinement.lean
@@ -37,6 +37,7 @@ where a polarity error shows.
-/
import NonosExtraction.Paging
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_paging
@@ -288,6 +289,39 @@ theorem a_table_entry_is_present :
(do let e ← aarch64_table 0x2000#u64 false; aPresent e) = ok true := by
refine ⟨?_, ?_⟩ <;> emit
+/-! ### Block entries -/
+
+/-- An x86_64 descriptor is a huge page exactly when it is present and bit 7 is
+ set. A clear present bit makes it no block whatever bit 7 holds. -/
+theorem x86_is_block_is_present_and_huge (e : Std.U64) :
+ arch.paging.descriptor.x86_64.is_block e =
+ ok (e.val.testBit 0 && e.val.testBit 7) := by
+ have hp : (1#u64 <<< 0#i32 : Result Std.U64) = ok 1#u64 := by rfl
+ have hh : (1#u64 <<< 7#i32 : Result Std.U64) = ok 128#u64 := by rfl
+ unfold arch.paging.descriptor.x86_64.is_block arch.paging.descriptor.x86_64.is_present
+ arch.paging.descriptor.flags.PRESENT arch.paging.descriptor.flags.HUGE
+ simp only [hp, hh, lift, bind_tc_ok, Bits.reads_bit e 1#u64 0#u64 0 rfl rfl,
+ Bits.reads_bit e 128#u64 0#u64 7 rfl rfl]
+ cases e.val.testBit 0 <;> rfl
+
+/-- An aarch64 descriptor is a block exactly when it is valid and bit 1, which
+ marks a table or a page, is clear. -/
+theorem aarch64_is_block_is_valid_and_not_table (e : Std.U64) :
+ arch.paging.descriptor.aarch64.read.is_block e =
+ ok (e.val.testBit 0 && !e.val.testBit 1) := by
+ have hv : (1#u64 <<< 0#i32 : Result Std.U64) = ok 1#u64 := by rfl
+ have ht : (1#u64 <<< 1#i32 : Result Std.U64) = ok 2#u64 := by rfl
+ unfold arch.paging.descriptor.aarch64.read.is_block
+ arch.paging.descriptor.aarch64.bits.VALID arch.paging.descriptor.aarch64.bits.TABLE_OR_PAGE
+ have hd : decide (e &&& 2#u64 = 0#u64) = !e.val.testBit 1 := by
+ rw [← Bits.reads_bit e 2#u64 0#u64 1 rfl rfl]
+ by_cases h : e &&& 2#u64 = 0#u64 <;> simp [h]
+ simp only [hv, ht, lift, bind_tc_ok, Bits.reads_bit e 1#u64 0#u64 0 rfl rfl]
+ cases e.val.testBit 0
+ · rfl
+ · simp only [↓reduceIte, Bool.true_and]
+ rw [← hd]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.absence_is_absence
@@ -305,5 +339,7 @@ theorem a_table_entry_is_present :
#print axioms NonosExtraction.the_aarch64_table_ignores_the_request
#print axioms NonosExtraction.the_backends_disagree_on_tables
#print axioms NonosExtraction.a_table_entry_is_present
+#print axioms NonosExtraction.x86_is_block_is_present_and_huge
+#print axioms NonosExtraction.aarch64_is_block_is_valid_and_not_table
end NonosExtraction
diff --git a/verification/extraction/lean/NonosExtraction/PciInfoRefinement.lean b/verification/extraction/lean/NonosExtraction/PciInfoRefinement.lean
index 33f02a4e61..794123ac18 100644
--- a/verification/extraction/lean/NonosExtraction/PciInfoRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PciInfoRefinement.lean
@@ -35,8 +35,43 @@ namespace NonosExtraction.PciInfo
theorem the_pcihost_has_io_window_wrapper_is_its_method (a : info.PciHost) :
pcihost_has_io_window a = info.PciHost.has_io_window a := rfl
+/-! ### When the host bridge has an I/O window
+
+ `has_io_window` is decided by the size of the window alone. The boot path in
+ `arch/aarch64/boot/pci_windows.rs` makes the same decision with
+ `io_size > 0` before it maps the window, and the theorem below proves the two
+ tests agree on every host, including one whose window starts at CPU address
+ zero or port zero. The theorems cannot say whether the device tree parser
+ filled `io_size` correctly; `take_io_window` reads raw device tree bytes and
+ is not extracted.
+-/
+
+/-- The window exists exactly when its size is positive, whatever its base and
+ port offset are; this is the test the boot path uses. -/
+theorem pcihost_has_io_window_is_a_positive_io_size (h : info.PciHost) :
+ pcihost_has_io_window h = ok (decide (0 < h.io_size.val)) := by
+ unfold pcihost_has_io_window info.PciHost.has_io_window
+ congr 1
+ by_cases h0 : h.io_size.val = 0
+ · have : h.io_size = 0#u64 := UScalar.eq_of_val_eq (by simp [h0])
+ simp [this]
+ · have : h.io_size ≠ 0#u64 := fun e => h0 (by simp [e])
+ rw [bne_iff_ne.mpr this]
+ simp
+ omega
+
+/-- The boundary: a one-byte window at address zero counts, and an all-zero host
+ (the `Default` one, what the parser starts from) has none. -/
+theorem pcihost_has_io_window_at_the_boundary :
+ pcihost_has_io_window ⟨0#u64, 0#u64, 0#u64, 1#u64, 0#u64⟩ = ok true ∧
+ pcihost_has_io_window ⟨0#u64, 0#u64, 0#u64, 0#u64, 0#u64⟩ = ok false := by
+ constructor <;> rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PciInfo.the_pcihost_has_io_window_wrapper_is_its_method
+#print axioms NonosExtraction.PciInfo.pcihost_has_io_window_is_a_positive_io_size
+#print axioms NonosExtraction.PciInfo.pcihost_has_io_window_at_the_boundary
+
end NonosExtraction.PciInfo
diff --git a/verification/extraction/lean/NonosExtraction/PciTypesMsixRefinement.lean b/verification/extraction/lean/NonosExtraction/PciTypesMsixRefinement.lean
index 2569a2f4d9..f579fd370d 100644
--- a/verification/extraction/lean/NonosExtraction/PciTypesMsixRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PciTypesMsixRefinement.lean
@@ -35,8 +35,52 @@ namespace NonosExtraction.PciTypesMsix
theorem the_msixtableentry_new_wrapper_is_its_method (a : Std.U64) (b : Std.U32) :
msixtableentry_new a b = types_msix.MsixTableEntry.new a b := rfl
+/-! ### An MSI-X entry is the address split in two, unmasked
+
+The MSI-X table entry layout in the PCI specification holds the 64-bit message
+address as a low and a high dword. These theorems establish that
+`MsixTableEntry::new` splits the address losslessly (high times 2^32 plus low
+is the address, for every address), that the low dword is the address modulo
+2^32 and the high dword its upper half, that the data word is stored
+unchanged, and that the vector control word is zero, so bit 0 (`MASKED`) is
+clear and the entry is live as soon as it is written.
+
+They cannot establish that the address is a valid LAPIC message address, nor
+anything about the MMIO writes that place the entry in the table: those are
+not in this crate.
+-/
+
+/-- The two address dwords of `msixtableentry_new` put back together give the
+ address, the data is kept, and the entry starts unmasked. -/
+theorem msixtableentry_new_splits_the_address_losslessly_and_starts_unmasked
+ (addr : Std.U64) (data : Std.U32) :
+ ∃ e, msixtableentry_new addr data = ok e ∧
+ e.message_addr_low.val = addr.val % 2 ^ 32 ∧
+ e.message_addr_high.val = addr.val / 2 ^ 32 ∧
+ e.message_addr_high.val * 2 ^ 32 + e.message_addr_low.val = addr.val ∧
+ e.message_data = data ∧
+ e.vector_control.val % 2 = 0 := by
+ unfold msixtableentry_new types_msix.MsixTableEntry.new
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec addr 32#i32 (by decide) (by decide))
+ simp only [lift, bind_tc_ok, hz]
+ have hb := addr.hBounds
+ have hzv : z.val = addr.val / 2 ^ 32 := by
+ rw [hv, Nat.shiftRight_eq_div_pow]; rfl
+ have hlo : (UScalar.cast .U32 addr).val = addr.val % 2 ^ 32 := by
+ simp [UScalar.cast_val_eq, UScalarTy.numBits]
+ have hhi : (UScalar.cast .U32 z).val = addr.val / 2 ^ 32 := by
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, hzv]
+ simp [UScalarTy.numBits] at hb
+ omega
+ refine ⟨_, rfl, hlo, hhi, ?_, rfl, rfl⟩
+ simp only [hlo, hhi]
+ omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PciTypesMsix.the_msixtableentry_new_wrapper_is_its_method
+#print axioms NonosExtraction.PciTypesMsix.msixtableentry_new_splits_the_address_losslessly_and_starts_unmasked
+
end NonosExtraction.PciTypesMsix
diff --git a/verification/extraction/lean/NonosExtraction/PioTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/PioTypesRefinement.lean
index 357f652658..f843d8c0d9 100644
--- a/verification/extraction/lean/NonosExtraction/PioTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PioTypesRefinement.lean
@@ -35,8 +35,38 @@ namespace NonosExtraction.PioTypes
theorem the_piowidth_bytes_wrapper_is_its_method (a : types.PioWidth) :
piowidth_bytes a = types.PioWidth.bytes a := rfl
+/-! ### A port width is its size in bytes
+
+`bytes` is the discriminant of `PioWidth` read as a `u16`, and the broker uses it
+as the number of bytes one port access moves. The theorems below fix it to 1, 2
+and 4 for the three widths, agreeing with the byte values `from_byte` accepts
+(`from_byte` itself is not extracted, so the agreement is with its source), and
+show that distinct widths never report the same size. They cannot establish
+anything about the port instructions that consume the size, which are inline
+assembly outside the extraction. -/
+
+/-- Byte, word and doubleword accesses move one, two and four bytes. -/
+theorem piowidth_bytes_is_the_access_size :
+ piowidth_bytes .U8 = ok 1#u16 ∧ piowidth_bytes .U16 = ok 2#u16 ∧
+ piowidth_bytes .U32 = ok 4#u16 := by
+ refine ⟨rfl, rfl, rfl⟩
+
+/-- Every width is a power of two no larger than four, so an access of that
+ size never straddles a naturally aligned doubleword. -/
+theorem piowidth_bytes_is_a_power_of_two_at_most_four (w : types.PioWidth) :
+ ∃ r, piowidth_bytes w = ok r ∧ (r.val = 1 ∨ r.val = 2 ∨ r.val = 4) := by
+ cases w <;> exact ⟨_, rfl, by decide⟩
+
+/-- The size identifies the width. -/
+theorem piowidth_bytes_is_injective (a b : types.PioWidth)
+ (h : piowidth_bytes a = piowidth_bytes b) : a = b := by
+ cases a <;> cases b <;> first | rfl | (simp [piowidth_bytes, types.PioWidth.bytes, types.PioWidth.read_discriminant] at h)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PioTypes.the_piowidth_bytes_wrapper_is_its_method
+#print axioms NonosExtraction.PioTypes.piowidth_bytes_is_the_access_size
+#print axioms NonosExtraction.PioTypes.piowidth_bytes_is_a_power_of_two_at_most_four
+#print axioms NonosExtraction.PioTypes.piowidth_bytes_is_injective
end NonosExtraction.PioTypes
diff --git a/verification/extraction/lean/NonosExtraction/PlonkTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/PlonkTypesRefinement.lean
index be6f212623..7d625cbd3c 100644
--- a/verification/extraction/lean/NonosExtraction/PlonkTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PlonkTypesRefinement.lean
@@ -38,9 +38,38 @@ theorem the_plonkevaluations_new_wrapper_is_its_method :
theorem the_plonkcircuit_new_wrapper_is_its_method :
plonkcircuit_new = types.PlonkCircuit.new := rfl
+/-! ### Fresh values start empty
+
+ `PlonkCircuit::new` is where `plonk_prove` starts before adding its one
+ multiplication gate, so a fresh circuit must have no gates and no public
+ inputs, or the proof would describe a circuit larger than the one built.
+ `PlonkEvaluations::new` is the scratch value that proof deserialisation
+ overwrites field by field, and every one of its six evaluations is the
+ all-zero 32 byte string, so a field the parser failed to fill would read as
+ zero rather than as stale data. These theorems cannot establish anything
+ about `add_mul_gate`, the prover or the parser, which are not extracted, nor
+ that zero is a meaningful field element to the verifier.
+-/
+
+/-- A fresh circuit has no gates and no public inputs. -/
+theorem plonkcircuit_new_has_no_gates_and_no_public_inputs :
+ ∃ c, plonkcircuit_new = ok c ∧ c.num_gates.val = 0 ∧ c.public_inputs.val = [] := by
+ refine ⟨_, rfl, ?_, ?_⟩ <;> rfl
+
+/-- Each of the six fresh evaluations is exactly thirty-two zero bytes. -/
+theorem plonkevaluations_new_is_zero_in_every_field :
+ ∃ e, plonkevaluations_new = ok e ∧
+ e.a.val = List.replicate 32 0#u8 ∧ e.b.val = List.replicate 32 0#u8 ∧
+ e.c.val = List.replicate 32 0#u8 ∧ e.z_omega.val = List.replicate 32 0#u8 ∧
+ e.s_sigma1.val = List.replicate 32 0#u8 ∧ e.s_sigma2.val = List.replicate 32 0#u8 := by
+ refine ⟨_, rfl, ?_⟩
+ simp
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PlonkTypes.the_plonkevaluations_new_wrapper_is_its_method
#print axioms NonosExtraction.PlonkTypes.the_plonkcircuit_new_wrapper_is_its_method
+#print axioms NonosExtraction.PlonkTypes.plonkcircuit_new_has_no_gates_and_no_public_inputs
+#print axioms NonosExtraction.PlonkTypes.plonkevaluations_new_is_zero_in_every_field
end NonosExtraction.PlonkTypes
diff --git a/verification/extraction/lean/NonosExtraction/PortStatsSnapshot.lean b/verification/extraction/lean/NonosExtraction/PortStatsSnapshot.lean
index a660658c5a..7aa8075059 100644
--- a/verification/extraction/lean/NonosExtraction/PortStatsSnapshot.lean
+++ b/verification/extraction/lean/NonosExtraction/PortStatsSnapshot.lean
@@ -27,20 +27,20 @@ structure stats_snapshot.PortStatsSnapshot where
io_delays : Std.U64
/-- [nonos_x_port_stats_snapshot::stats_snapshot::{nonos_x_port_stats_snapshot::stats_snapshot::PortStatsSnapshot}::total_ops]:
- Source: 'src/../../../../../src/arch/x86_64/port/stats_snapshot.rs', lines 29:4-31:5
+ Source: 'src/../../../../../src/arch/x86_64/port/stats_snapshot.rs', lines 30:4-35:5
Visibility: public -/
def stats_snapshot.PortStatsSnapshot.total_ops
(self : stats_snapshot.PortStatsSnapshot) : Result Std.U64 := do
- let i ← self.read_ops + self.write_ops
- let i1 ← i + self.string_read_ops
- i1 + self.string_write_ops
+ let i ← lift (core.num.U64.saturating_add self.read_ops self.write_ops)
+ let i1 ← lift (core.num.U64.saturating_add i self.string_read_ops)
+ ok (core.num.U64.saturating_add i1 self.string_write_ops)
/-- [nonos_x_port_stats_snapshot::stats_snapshot::{nonos_x_port_stats_snapshot::stats_snapshot::PortStatsSnapshot}::total_bytes]:
- Source: 'src/../../../../../src/arch/x86_64/port/stats_snapshot.rs', lines 32:4-34:5
+ Source: 'src/../../../../../src/arch/x86_64/port/stats_snapshot.rs', lines 36:4-38:5
Visibility: public -/
def stats_snapshot.PortStatsSnapshot.total_bytes
(self : stats_snapshot.PortStatsSnapshot) : Result Std.U64 := do
- self.bytes_read + self.bytes_written
+ ok (core.num.U64.saturating_add self.bytes_read self.bytes_written)
/-- [nonos_x_port_stats_snapshot::portstatssnapshot_total_ops]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/PortStatsSnapshotRefinement.lean b/verification/extraction/lean/NonosExtraction/PortStatsSnapshotRefinement.lean
index 612c71be7a..99bf6a08a6 100644
--- a/verification/extraction/lean/NonosExtraction/PortStatsSnapshotRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/PortStatsSnapshotRefinement.lean
@@ -38,9 +38,76 @@ theorem the_portstatssnapshot_total_ops_wrapper_is_its_method (a : stats_snapsho
theorem the_portstatssnapshot_total_bytes_wrapper_is_its_method (a : stats_snapshot.PortStatsSnapshot) :
portstatssnapshot_total_bytes a = stats_snapshot.PortStatsSnapshot.total_bytes a := rfl
+/-! ### What the totals add up
+
+`total_bytes` and `total_ops` are the two figures a port statistics report
+reads off a snapshot, and `PortStats::total_ops`, which `PortManager` reports,
+takes a snapshot and asks it. The theorems below say which fields each one
+sums: bytes read and written for the first, the four operation counters for
+the second, and never `io_delays`. Both saturate at `u64::MAX` and never fail.
+Saturating each partial sum gives the saturated whole, because every field is
+unsigned and a partial sum never exceeds the whole.
+
+They used to add with `+`. The kernel is built with `overflow-checks = true`
+in every profile, so a sum past `u64::MAX` panicked, and the counters wrap in
+`fetch_add`, so a snapshot can hold any values at all.
+
+Nothing here reaches `PortStats`, the live atomic counters a snapshot is copied
+from, or the `fetch_add` calls that bump them: those are atomics Aeneas leaves
+opaque.
+-/
+
+private theorem sat (x y : Std.U64) :
+ (core.num.U64.saturating_add x y).val = min (2 ^ 64 - 1) (x.val + y.val) := by
+ simp only [core.num.U64.saturating_add, UScalar.saturating_add, UScalar.val, UScalar.max]
+ rw [BitVec.toNat_ofNat]
+ show min (2 ^ 64 - 1) _ % 2 ^ 64 = _
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- The byte total never fails and is bytes read plus bytes written, and
+ nothing else in the snapshot, saturated at `u64::MAX`. -/
+theorem portstatssnapshot_total_bytes_is_the_saturated_read_plus_written
+ (s : stats_snapshot.PortStatsSnapshot) :
+ ∃ v : Std.U64, portstatssnapshot_total_bytes s = ok v ∧
+ v.val = min (2 ^ 64 - 1) (s.bytes_read.val + s.bytes_written.val) := by
+ unfold portstatssnapshot_total_bytes stats_snapshot.PortStatsSnapshot.total_bytes
+ exact ⟨_, rfl, sat _ _⟩
+
+/-- The operation total never fails and counts plain reads, plain writes,
+ string reads and string writes, once each, saturated at `u64::MAX`. I/O
+ delays are not operations and are not counted. -/
+theorem portstatssnapshot_total_ops_counts_four_kinds_and_not_delays
+ (s : stats_snapshot.PortStatsSnapshot) :
+ ∃ v : Std.U64, portstatssnapshot_total_ops s = ok v ∧
+ v.val = min (2 ^ 64 - 1) (s.read_ops.val + s.write_ops.val + s.string_read_ops.val
+ + s.string_write_ops.val) := by
+ unfold portstatssnapshot_total_ops stats_snapshot.PortStatsSnapshot.total_ops
+ simp only [lift, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ rw [sat, sat, sat]
+ omega
+
+/-- The first sums past the limit saturate instead of halting: `u64::MAX` bytes
+ read and one written, and `u64::MAX` reads and one string write. -/
+theorem portstatssnapshot_totals_saturate_past_the_u64_limit :
+ portstatssnapshot_total_bytes ⟨0xFFFFFFFFFFFFFFFF#u64, 1#u64, 0#u64, 0#u64, 0#u64, 0#u64, 0#u64⟩
+ = ok 0xFFFFFFFFFFFFFFFF#u64 ∧
+ portstatssnapshot_total_ops ⟨0#u64, 0#u64, 0xFFFFFFFFFFFFFFFF#u64, 0#u64, 0#u64, 1#u64, 0#u64⟩
+ = ok 0xFFFFFFFFFFFFFFFF#u64 := by
+ refine ⟨?_, ?_⟩
+ · obtain ⟨v, hv, hvv⟩ := portstatssnapshot_total_bytes_is_the_saturated_read_plus_written
+ ⟨0xFFFFFFFFFFFFFFFF#u64, 1#u64, 0#u64, 0#u64, 0#u64, 0#u64, 0#u64⟩
+ rw [hv]; congr 1; apply UScalar.eq_of_val_eq; rw [hvv]; rfl
+ · obtain ⟨v, hv, hvv⟩ := portstatssnapshot_total_ops_counts_four_kinds_and_not_delays
+ ⟨0#u64, 0#u64, 0xFFFFFFFFFFFFFFFF#u64, 0#u64, 0#u64, 1#u64, 0#u64⟩
+ rw [hv]; congr 1; apply UScalar.eq_of_val_eq; rw [hvv]; rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.PortStatsSnapshot.the_portstatssnapshot_total_ops_wrapper_is_its_method
#print axioms NonosExtraction.PortStatsSnapshot.the_portstatssnapshot_total_bytes_wrapper_is_its_method
+#print axioms NonosExtraction.PortStatsSnapshot.portstatssnapshot_total_bytes_is_the_saturated_read_plus_written
+#print axioms NonosExtraction.PortStatsSnapshot.portstatssnapshot_total_ops_counts_four_kinds_and_not_delays
+#print axioms NonosExtraction.PortStatsSnapshot.portstatssnapshot_totals_saturate_past_the_u64_limit
end NonosExtraction.PortStatsSnapshot
diff --git a/verification/extraction/lean/NonosExtraction/ProcfsPidInode.lean b/verification/extraction/lean/NonosExtraction/ProcfsPidInode.lean
index 130fc8aac7..8eea9d1df3 100644
--- a/verification/extraction/lean/NonosExtraction/ProcfsPidInode.lean
+++ b/verification/extraction/lean/NonosExtraction/ProcfsPidInode.lean
@@ -14,16 +14,19 @@ set_option maxRecDepth 2048
namespace nonos_x_procfs_pid_inode
+/-- [nonos_x_procfs_pid_inode::pid_inode::PID_INODE_SHIFT]
+ Source: 'src/../../../../../src/fs/procfs/pid_inode.rs', lines 21:0-21:43 -/
+@[global_simps, irreducible] def pid_inode.PID_INODE_SHIFT : Std.U32 := 20#u32
+
/-- [nonos_x_procfs_pid_inode::pid_inode::pid_dir_inode]:
- Source: 'src/../../../../../src/fs/procfs/pid_inode.rs', lines 19:0-24:1 -/
+ Source: 'src/../../../../../src/fs/procfs/pid_inode.rs', lines 23:0-28:1 -/
def pid_inode.pid_dir_inode (pid : Std.I32) : Result (Option Std.U64) := do
- if pid < 0#i32
+ if pid <= 0#i32
then ok none
else
let i ← lift (IScalar.hcast .U64 pid)
- let i1 ← i * 1000#u64
- let i2 ← i1 + 100#u64
- ok (some i2)
+ let i1 ← i <<< pid_inode.PID_INODE_SHIFT
+ ok (some i1)
/-- [nonos_x_procfs_pid_inode::pid_dir_inode]:
Source: 'src/lib.rs', lines 11:0-13:1
diff --git a/verification/extraction/lean/NonosExtraction/ProcfsPidInodeRefinement.lean b/verification/extraction/lean/NonosExtraction/ProcfsPidInodeRefinement.lean
index 4741c46bcf..10692fe21c 100644
--- a/verification/extraction/lean/NonosExtraction/ProcfsPidInodeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ProcfsPidInodeRefinement.lean
@@ -33,27 +33,36 @@ theorem the_pid_dir_inode_wrapper_is_its_method (a : Std.I32) :
/-! ### The inode number of a pid directory
- `lookup_root` and `procfs_readdir` in `src/fs/procfs/inode.rs` number the
- directory of process `pid` as `pid * 1000 + 100`. They used to compute that
- as `pid as u64 * 1000 + 100` on a pid parsed from the name as an `i32`, so
- the name `-1` cast to `2^64 - 1` and overflowed the product: a panic in a debug
- build and a wrapped inode in a release one. `pid_dir_inode` refuses a
- negative pid, and the theorems below say it never fails, answers `none`
- exactly for a negative pid, gives every other pid the number
- `pid * 1000 + 100`, and gives distinct pids distinct numbers.
-
- What these cannot establish: that the number does not collide with the
- fixed root entries' inodes, which `procfs_root_entries` chooses and which
- are not extracted, or that the process exists. `lookup_root` also accepts
- only names made of ASCII digits, so `+5` no longer names process 5; that
- test is on a `str` and is not extracted either.
+ Every inode under a pid directory is `(pid << 20) | k`: the directory is
+ `k = 0` and its entries are `k = 1` to `103` (`pid_entries`), and the root
+ entries all sit below `2 ^ 20` (`procfs_root_entries`, 105 at most).
+ `lookup_root` and `procfs_readdir` take the directory's number from
+ `pid_dir_inode`, which answers `none` for a pid of zero or below.
+
+ The numbering used to be `pid as u64 * 1000 + 100` on a pid parsed as an
+ `i32`. The name `-1` overflowed that product, a panic in a debug build and
+ a wrapped inode in a release one; pid 0's directory got 100, the root
+ `sys` entry's inode, and pid 0's `status` entry got 1, which
+ `procfs_lookup` dispatches as the root; and pid 131072's directory got
+ 131072100, the inode of pid 125's `task` entry.
+
+ The theorems below say `pid_dir_inode` never fails, answers `none` exactly
+ for a pid of zero or below, gives every other pid `pid * 2 ^ 20`, and that
+ this number is at least `2 ^ 20`, so above every root entry, and is no
+ entry inode `(q << 20) | k` with `1 ≤ k < 2 ^ 20` of any pid.
+
+ What these cannot establish: that the process exists. `pid_entries` and
+ `procfs_root_entries` build `Vec`s and are not extracted, so their inode
+ numbers are taken from the source as stated above. `lookup_root` also
+ accepts only names made of ASCII digits, so `+5` no longer names process 5;
+ that test is on a `str` and is not extracted either.
-/
-/-- A negative pid has no directory inode. -/
-theorem pid_dir_inode_refuses_a_negative_pid (p : Std.I32) (h : p.val < 0) :
+/-- A pid of zero or below has no directory inode. -/
+theorem pid_dir_inode_refuses_a_pid_below_one (p : Std.I32) (h : p.val ≤ 0) :
pid_dir_inode p = ok none := by
unfold pid_dir_inode pid_inode.pid_dir_inode
- have : p < 0#i32 := by scalar_tac
+ have : p <= 0#i32 := by scalar_tac
simp only [this, ↓reduceIte]
/-- A non-negative `i32` cast to `u64` keeps its value: the sign bit is clear,
@@ -73,65 +82,88 @@ private theorem cast_of_nonneg (p : Std.I32) (h : 0 ≤ p.val) :
rw [Nat.mod_eq_of_lt (by simp at *; omega)]
omega
-/-- Every other pid gets `pid * 1000 + 100`, and the computation cannot
- overflow: the largest `i32` gives a number far below `2^64`. -/
-theorem pid_dir_inode_is_pid_times_1000_plus_100 (p : Std.I32) (h : 0 ≤ p.val) :
- ∃ n : Std.U64, pid_dir_inode p = ok (some n) ∧ n.val = p.val.toNat * 1000 + 100 := by
- unfold pid_dir_inode pid_inode.pid_dir_inode
- have hn : ¬ p < 0#i32 := by scalar_tac
+/-- Every pid from 1 gets `pid * 2 ^ 20`, and the shift cannot lose bits: the
+ largest `i32` gives a number below `2 ^ 51`. -/
+theorem pid_dir_inode_is_pid_shifted_by_twenty (p : Std.I32) (h : 0 < p.val) :
+ ∃ n : Std.U64, pid_dir_inode p = ok (some n) ∧ n.val = p.val.toNat * 2 ^ 20 := by
+ unfold pid_dir_inode pid_inode.pid_dir_inode pid_inode.PID_INODE_SHIFT
+ have hn : ¬ p <= 0#i32 := by scalar_tac
simp only [hn, ↓reduceIte, lift, bind_tc_ok]
- have hc := cast_of_nonneg p h
- have ⟨m, hm, hmv⟩ := WP.spec_imp_exists
- (U64.mul_spec (x := IScalar.hcast .U64 p) (y := 1000#u64) (by scalar_tac))
- simp only [hm, bind_tc_ok]
- have ⟨a, ha, hav⟩ := WP.spec_imp_exists
- (U64.add_spec (x := m) (y := 100#u64) (by scalar_tac))
- simp only [ha]
- exact ⟨a, rfl, by scalar_tac⟩
-
-/-- `pid_dir_inode` never fails, and answers `none` exactly for a negative
- pid. -/
-theorem pid_dir_inode_is_none_exactly_for_a_negative_pid (p : Std.I32) :
- ∃ r, pid_dir_inode p = ok r ∧ (r = none ↔ p.val < 0) := by
- by_cases h : p.val < 0
- · exact ⟨none, pid_dir_inode_refuses_a_negative_pid p h, by simp [h]⟩
- · obtain ⟨n, hn, -⟩ := pid_dir_inode_is_pid_times_1000_plus_100 p (by omega)
+ have hc := cast_of_nonneg p (by omega)
+ have hp : p.val.toNat < 2 ^ 31 := by scalar_tac
+ obtain ⟨z, hz, hzv, -⟩ := WP.spec_imp_exists
+ (UScalar.ShiftLeft_spec (IScalar.hcast .U64 p) 20#u32 (2 ^ 64) (by decide)
+ (by simp [U64.size, U64.numBits]))
+ simp only [hz, bind_tc_ok]
+ refine ⟨z, rfl, ?_⟩
+ rw [hzv, hc, Nat.shiftLeft_eq, show (20#u32 : Std.U32).val = 20 from rfl]
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- `pid_dir_inode` never fails, and answers `none` exactly for a pid of zero
+ or below. -/
+theorem pid_dir_inode_is_none_exactly_below_one (p : Std.I32) :
+ ∃ r, pid_dir_inode p = ok r ∧ (r = none ↔ p.val ≤ 0) := by
+ by_cases h : p.val ≤ 0
+ · exact ⟨none, pid_dir_inode_refuses_a_pid_below_one p h, by simp [h]⟩
+ · obtain ⟨n, hn, -⟩ := pid_dir_inode_is_pid_shifted_by_twenty p (by omega)
exact ⟨some n, hn, by simp [h]⟩
+/-- A directory inode is at least `2 ^ 20`, above every root entry, and is not
+ the inode `(q << 20) | k` of any entry, `1 ≤ k < 2 ^ 20`, of any pid `q`. -/
+theorem a_pid_directory_inode_is_no_root_or_entry_inode (p : Std.I32) (n : Std.U64)
+ (h : pid_dir_inode p = ok (some n)) :
+ 2 ^ 20 ≤ n.val ∧ ∀ q k : Nat, 1 ≤ k → k < 2 ^ 20 → n.val ≠ q * 2 ^ 20 + k := by
+ have hp : 0 < p.val := by
+ by_contra hc
+ rw [pid_dir_inode_refuses_a_pid_below_one p (by omega)] at h
+ simp at h
+ obtain ⟨m, hm, hmv⟩ := pid_dir_inode_is_pid_shifted_by_twenty p hp
+ rw [hm] at h
+ simp only [ok.injEq, Option.some.injEq] at h
+ subst h
+ refine ⟨by rw [hmv]; omega, fun q k hk1 hk2 heq => ?_⟩
+ rw [hmv] at heq
+ omega
+
/-- Distinct pids get distinct directory inodes, so a lookup by inode names one
process. -/
theorem pid_dir_inode_separates_pids (p q : Std.I32) (n : Std.U64)
(hp : pid_dir_inode p = ok (some n)) (hq : pid_dir_inode q = ok (some n)) : p = q := by
- have np : 0 ≤ p.val := by
+ have np : 0 < p.val := by
by_contra h
- rw [pid_dir_inode_refuses_a_negative_pid p (by omega)] at hp
+ rw [pid_dir_inode_refuses_a_pid_below_one p (by omega)] at hp
simp at hp
- have nq : 0 ≤ q.val := by
+ have nq : 0 < q.val := by
by_contra h
- rw [pid_dir_inode_refuses_a_negative_pid q (by omega)] at hq
+ rw [pid_dir_inode_refuses_a_pid_below_one q (by omega)] at hq
simp at hq
- obtain ⟨a, ha, hav⟩ := pid_dir_inode_is_pid_times_1000_plus_100 p np
- obtain ⟨b, hb, hbv⟩ := pid_dir_inode_is_pid_times_1000_plus_100 q nq
+ obtain ⟨a, ha, hav⟩ := pid_dir_inode_is_pid_shifted_by_twenty p np
+ obtain ⟨b, hb, hbv⟩ := pid_dir_inode_is_pid_shifted_by_twenty q nq
rw [ha] at hp; rw [hb] at hq
simp only [ok.injEq, Option.some.injEq] at hp hq
subst hp; subst hq
apply IScalar.eq_of_val_eq
omega
-/-- Concrete edges: `-1` and the smallest `i32` have no inode, `0` is 100, and
- the largest `i32` is `2147483647100`. -/
+/-- Concrete edges: `-1` and `0` have no inode, `1` is `2 ^ 20`, and pid
+ 131072, which used to share pid 125's `task` inode, is `2 ^ 37`. -/
theorem pid_dir_inode_at_the_edges :
- pid_dir_inode (-1)#i32 = ok none ∧ pid_dir_inode (-2147483648)#i32 = ok none ∧
- pid_dir_inode 0#i32 = ok (some 100#u64) ∧
- pid_dir_inode 2147483647#i32 = ok (some 2147483647100#u64) := by
- refine ⟨rfl, rfl, ?_, ?_⟩ <;> rfl
+ pid_dir_inode (-1)#i32 = ok none ∧ pid_dir_inode 0#i32 = ok none ∧
+ pid_dir_inode 1#i32 = ok (some 0x100000#u64) ∧
+ pid_dir_inode 131072#i32 = ok (some 0x2000000000#u64) := by
+ refine ⟨rfl, rfl, ?_, ?_⟩
+ · obtain ⟨n, hn, hv⟩ := pid_dir_inode_is_pid_shifted_by_twenty 1#i32 (by decide)
+ rw [hn, UScalar.eq_of_val_eq (hv.trans rfl : n.val = (0x100000#u64 : Std.U64).val)]
+ · obtain ⟨n, hn, hv⟩ := pid_dir_inode_is_pid_shifted_by_twenty 131072#i32 (by decide)
+ rw [hn, UScalar.eq_of_val_eq (hv.trans rfl : n.val = (0x2000000000#u64 : Std.U64).val)]
/-! ### Axiom profile -/
#print axioms NonosExtraction.ProcfsPidInode.the_pid_dir_inode_wrapper_is_its_method
-#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_refuses_a_negative_pid
-#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_is_pid_times_1000_plus_100
-#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_is_none_exactly_for_a_negative_pid
+#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_refuses_a_pid_below_one
+#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_is_pid_shifted_by_twenty
+#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_is_none_exactly_below_one
+#print axioms NonosExtraction.ProcfsPidInode.a_pid_directory_inode_is_no_root_or_entry_inode
#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_separates_pids
#print axioms NonosExtraction.ProcfsPidInode.pid_dir_inode_at_the_edges
diff --git a/verification/extraction/lean/NonosExtraction/ProcfsTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/ProcfsTypesRefinement.lean
index fc92c346b4..e694f52490 100644
--- a/verification/extraction/lean/NonosExtraction/ProcfsTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ProcfsTypesRefinement.lean
@@ -54,9 +54,9 @@ left opaque by the extraction, so every statement is conditional on the
constructor returning `ok`, and the characterisation in terms of the root is how
the opaque call is shared rather than assumed. They say nothing about the inode
numbers callers choose. `lookup_root` and `procfs_readdir` take them from
-`pid_dir_inode`, which refuses a negative pid instead of overflowing on it;
-`NonosExtraction.ProcfsPidInode` proves that. Collisions with the fixed root
-entries are outside these statements.
+`pid_dir_inode`, which numbers pid `p` as `p << 20` from pid 1 up;
+`NonosExtraction.ProcfsPidInode` proves those numbers miss the root entries and
+every pid entry.
-/
/-- The root is inode 1, a directory, and carries no pid, which is exactly what
diff --git a/verification/extraction/lean/NonosExtraction/RandomApiEntropyCheckRefinement.lean b/verification/extraction/lean/NonosExtraction/RandomApiEntropyCheckRefinement.lean
index 6bee2550c4..55bf24ef33 100644
--- a/verification/extraction/lean/NonosExtraction/RandomApiEntropyCheckRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RandomApiEntropyCheckRefinement.lean
@@ -35,8 +35,70 @@ namespace NonosExtraction.RandomApiEntropyCheck
theorem the_required_entropy_bytes_wrapper_is_its_method (a : Std.Usize) :
required_entropy_bytes a = entropy_check.required_entropy_bytes a := rfl
+/-! ### Bytes needed to hold a number of bits
+
+ `get_bytes_checked` refuses a buffer shorter than
+ `required_entropy_bytes(min_entropy)`. The theorems below say the function
+ never fails on any `usize`, including `usize::MAX` where a careless
+ `(bits + 7) / 8` would overflow, that it returns exactly the ceiling of
+ `bits / 8`, and so that the byte count is always enough to carry the bits
+ asked for and never one byte more than needed. They cannot establish that the
+ bytes `fill_random_bytes` then writes carry that much entropy; the generator
+ is not extracted here.
+-/
+
+/-- The result is `ceil(bits / 8)`, for every `usize`. -/
+theorem required_entropy_bytes_is_the_ceiling_of_bits_over_eight (bits : Std.Usize) :
+ ∃ n : Std.Usize, required_entropy_bytes bits = ok n ∧ n.val = (bits.val + 7) / 8 := by
+ unfold required_entropy_bytes entropy_check.required_entropy_bytes
+ obtain ⟨q, hq, hqv⟩ := UScalar.div_spec bits (y := 8#usize) (by simp)
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (UScalar.rem_spec bits (y := 8#usize) (by simp))
+ have e8 : (8#usize : Std.Usize).val = 8 := rfl
+ rw [e8] at hqv hrv
+ rw [hr]
+ simp only [bind_tc_ok]
+ split
+ · rename_i heq
+ have h0 : r.val = 0 := by rw [heq]; rfl
+ exact ⟨q, hq, by omega⟩
+ · rename_i hne
+ have h0 : r.val ≠ 0 := fun h => hne (UScalar.eq_of_val_eq (by rw [h]; rfl))
+ rw [hq]
+ simp only [bind_tc_ok]
+ have hb := bits.hBounds
+ obtain ⟨n, hn, hnv⟩ := WP.spec_imp_exists
+ (Usize.add_spec (x := q) (y := 1#usize) (by
+ have : (1#usize : Std.Usize).val = 1 := rfl
+ rw [this, hqv]; scalar_tac))
+ refine ⟨n, hn, ?_⟩
+ rw [hnv, hqv]
+ have : (1#usize : Std.Usize).val = 1 := rfl
+ rw [this]
+ omega
+
+/-- The caller's contract: a buffer of the returned length holds every bit asked
+ for, and one byte fewer would not. -/
+theorem required_entropy_bytes_is_enough_and_no_more (bits : Std.Usize) :
+ ∃ n : Std.Usize, required_entropy_bytes bits = ok n ∧
+ bits.val ≤ 8 * n.val ∧ 8 * n.val < bits.val + 8 := by
+ obtain ⟨n, hn, hv⟩ := required_entropy_bytes_is_the_ceiling_of_bits_over_eight bits
+ exact ⟨n, hn, by omega, by omega⟩
+
+/-- A request for 256 bits needs 32 bytes, 257 bits need 33, and no bits need no
+ bytes. -/
+theorem required_entropy_bytes_at_a_byte_boundary :
+ required_entropy_bytes 256#usize = ok 32#usize ∧
+ required_entropy_bytes 257#usize = ok 33#usize ∧
+ required_entropy_bytes 0#usize = ok 0#usize := by
+ refine ⟨?_, ?_, ?_⟩ <;>
+ (obtain ⟨n, hn, hv⟩ := required_entropy_bytes_is_the_ceiling_of_bits_over_eight _
+ rw [hn]; congr 1; apply UScalar.eq_of_val_eq; rw [hv]; rfl)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RandomApiEntropyCheck.the_required_entropy_bytes_wrapper_is_its_method
+#print axioms NonosExtraction.RandomApiEntropyCheck.required_entropy_bytes_is_the_ceiling_of_bits_over_eight
+#print axioms NonosExtraction.RandomApiEntropyCheck.required_entropy_bytes_is_enough_and_no_more
+#print axioms NonosExtraction.RandomApiEntropyCheck.required_entropy_bytes_at_a_byte_boundary
end NonosExtraction.RandomApiEntropyCheck
diff --git a/verification/extraction/lean/NonosExtraction/RedistributorDeviceRefinement.lean b/verification/extraction/lean/NonosExtraction/RedistributorDeviceRefinement.lean
index 1df8dc4486..c09576842d 100644
--- a/verification/extraction/lean/NonosExtraction/RedistributorDeviceRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RedistributorDeviceRefinement.lean
@@ -35,8 +35,29 @@ namespace NonosExtraction.RedistributorDevice
theorem the_gicredistributor_new_wrapper_is_its_method (a : Std.U64) :
gicredistributor_new a = device.GicRedistributor.new a := rfl
+/-! ### The redistributor frame is where the firmware put it
+
+ `new` records the base address it is given without masking, rounding or
+ offsetting it, so every register access the driver makes is relative to the
+ address the device tree or ACPI table reported, including each frame `find.rs` walks. The theorems cannot say that
+ the address is a valid, mapped redistributor frame; that comes from the firmware
+ tables and the MMIO mapping, neither of which is extracted.
+-/
+
+/-- The constructed redistributor keeps the base exactly as given. -/
+theorem gicredistributor_new_keeps_the_firmware_base (b : Std.U64) :
+ ∃ d, gicredistributor_new b = ok d ∧ d.base = b := ⟨_, rfl, rfl⟩
+
+/-- Different bases give different devices, so two frames are never collapsed
+ onto one. -/
+theorem gicredistributor_new_tells_bases_apart (a b : Std.U64) (h : gicredistributor_new a = gicredistributor_new b) : a = b := by
+ simpa [gicredistributor_new, device.GicRedistributor.new] using h
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RedistributorDevice.the_gicredistributor_new_wrapper_is_its_method
+#print axioms NonosExtraction.RedistributorDevice.gicredistributor_new_keeps_the_firmware_base
+#print axioms NonosExtraction.RedistributorDevice.gicredistributor_new_tells_bases_apart
+
end NonosExtraction.RedistributorDevice
diff --git a/verification/extraction/lean/NonosExtraction/RegionTypesStatsRefinement.lean b/verification/extraction/lean/NonosExtraction/RegionTypesStatsRefinement.lean
index 5046daf3ce..119fe2b594 100644
--- a/verification/extraction/lean/NonosExtraction/RegionTypesStatsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RegionTypesStatsRefinement.lean
@@ -38,9 +38,110 @@ theorem the_regionstats_new_wrapper_is_its_method :
theorem the_regionstats_total_memory_wrapper_is_its_method (a : stats.RegionStats) :
regionstats_total_memory a = stats.RegionStats.total_memory a := rfl
+/-! ### A fresh record counts nothing, and total memory is the exact sum
+
+`get_total_memory` in the region manager reports `total_memory` of the live
+statistics, so the value is only meaningful if it is the allocated bytes plus
+the free bytes with nothing dropped. The theorems below establish that
+`new` starts every counter at zero, that `total_memory` returns exactly
+`allocated_bytes + free_bytes` whenever that sum fits in 64 bits, and that a
+sum past `2^64` is refused rather than reported as a small wrapped total.
+They cannot establish that the manager keeps `allocated_bytes` and
+`free_bytes` consistent with the regions it holds: the manager, its lock and
+its static statistics are not extracted. The refusal is the checked
+arithmetic Aeneas models; a kernel built without overflow checks would wrap
+instead. -/
+
+/-- Every counter of a fresh statistics record is zero, not only the byte
+totals, so no region, allocation, merge, split or fragment is counted before
+the manager records one. -/
+theorem regionstats_new_counts_nothing :
+ regionstats_new = ok
+ { total_regions := 0#usize, free_regions := 0#usize,
+ allocated_bytes := 0#u64, free_bytes := 0#u64,
+ allocation_count := 0#u64, deallocation_count := 0#u64,
+ merge_count := 0#u64, split_count := 0#u64,
+ fragment_count := 0#usize, largest_free_block := 0#u64 } := rfl
+
+/-- `total_memory` returns exactly `allocated_bytes + free_bytes` when that
+sum fits in 64 bits, and only then. -/
+theorem regionstats_total_memory_is_allocated_plus_free_when_it_fits
+ (s : stats.RegionStats) (v : Std.U64) :
+ regionstats_total_memory s = ok v ↔
+ (s.allocated_bytes.val + s.free_bytes.val ≤ U64.max ∧
+ v.val = s.allocated_bytes.val + s.free_bytes.val) := by
+ unfold regionstats_total_memory stats.RegionStats.total_memory
+ have h := UScalar.add_equiv s.allocated_bytes s.free_bytes
+ constructor
+ · intro hv
+ rw [hv] at h
+ simp only [U64.max_eq] at *
+ obtain ⟨h1, h2, _⟩ := h
+ simp [UScalarTy.numBits] at h1
+ omega
+ · rintro ⟨hle, hv⟩
+ cases hr : (s.allocated_bytes + s.free_bytes : Result Std.U64) with
+ | ok z =>
+ rw [hr] at h
+ obtain ⟨_, h2, _⟩ := h
+ have : z = v := UScalar.eq_of_val_eq (by omega)
+ rw [this]
+ | fail e =>
+ rw [hr] at h
+ simp [UScalar.inBounds, UScalarTy.numBits] at h
+ simp only [U64.max_eq] at hle
+ omega
+ | div => rw [hr] at h; exact h.elim
+
+/-- A total that would pass `2^64` is refused with an overflow, never
+reported as the wrapped remainder. -/
+theorem regionstats_total_memory_refuses_a_sum_past_u64_max
+ (s : stats.RegionStats) (h : U64.max < s.allocated_bytes.val + s.free_bytes.val) :
+ regionstats_total_memory s = fail .integerOverflow := by
+ unfold regionstats_total_memory stats.RegionStats.total_memory
+ have ha := UScalar.add_equiv s.allocated_bytes s.free_bytes
+ cases hr : (s.allocated_bytes + s.free_bytes : Result Std.U64) with
+ | ok z =>
+ rw [hr] at ha
+ simp only [U64.max_eq] at h
+ simp [UScalarTy.numBits] at ha
+ omega
+ | fail e =>
+ have : e = .integerOverflow := by
+ revert hr
+ simp only [HAdd.hAdd, UScalar.add, UScalar.tryMk, UScalar.tryMkOpt]
+ split <;> simp_all
+ rw [this]
+ | div => rw [hr] at ha; exact ha.elim
+
+/-- The boundary: all of memory allocated with one free byte on top overflows,
+while the same total with the last byte taken back fits exactly. -/
+theorem regionstats_total_memory_at_the_u64_boundary (s : stats.RegionStats) :
+ regionstats_total_memory
+ { s with allocated_bytes := 0xFFFFFFFFFFFFFFFF#u64, free_bytes := 1#u64 } = fail .integerOverflow ∧
+ regionstats_total_memory
+ { s with allocated_bytes := 0xFFFFFFFFFFFFFFFE#u64, free_bytes := 1#u64 } = ok 0xFFFFFFFFFFFFFFFF#u64 := by
+ refine ⟨regionstats_total_memory_refuses_a_sum_past_u64_max _ (by
+ simp only [U64.max_eq]; simp), ?_⟩
+ exact (regionstats_total_memory_is_allocated_plus_free_when_it_fits _ _).2
+ (by simp only [U64.max_eq]; simp)
+
+/-- A fresh record reports zero total memory. -/
+theorem regionstats_total_memory_of_new_is_zero :
+ (do let s ← regionstats_new; regionstats_total_memory s) = ok 0#u64 := by
+ rw [regionstats_new_counts_nothing]
+ simp only [bind_tc_ok]
+ exact (regionstats_total_memory_is_allocated_plus_free_when_it_fits _ _).2
+ (by simp only [U64.max_eq]; simp)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RegionTypesStats.the_regionstats_new_wrapper_is_its_method
#print axioms NonosExtraction.RegionTypesStats.the_regionstats_total_memory_wrapper_is_its_method
+#print axioms NonosExtraction.RegionTypesStats.regionstats_new_counts_nothing
+#print axioms NonosExtraction.RegionTypesStats.regionstats_total_memory_is_allocated_plus_free_when_it_fits
+#print axioms NonosExtraction.RegionTypesStats.regionstats_total_memory_refuses_a_sum_past_u64_max
+#print axioms NonosExtraction.RegionTypesStats.regionstats_total_memory_at_the_u64_boundary
+#print axioms NonosExtraction.RegionTypesStats.regionstats_total_memory_of_new_is_zero
end NonosExtraction.RegionTypesStats
diff --git a/verification/extraction/lean/NonosExtraction/RegistersPlicRefinement.lean b/verification/extraction/lean/NonosExtraction/RegistersPlicRefinement.lean
index 7eba0972c2..9d2265ff0c 100644
--- a/verification/extraction/lean/NonosExtraction/RegistersPlicRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RegistersPlicRefinement.lean
@@ -38,9 +38,41 @@ theorem the_plic_new_wrapper_is_its_method (a : Std.U64) :
theorem the_plic_base_wrapper_is_its_method (a : plic.Plic) :
plic_base a = plic.Plic.impl.base a := rfl
+/-! ### The handle keeps the address it was built with
+
+ A `Plic` is only the base address of the controller's register window, and
+ every register access in `priority`, `enable`, `pending` and `context_ops`
+ adds an offset to what `base` returns. The theorems below show that `base`
+ answers exactly the address `new` was given, and that `new` applied to that
+ answer rebuilds the same handle, so no field is lost or altered between the
+ two.
+
+ They cannot say the address is the right one. `state` loads it from the
+ `PLIC_BASE` atomic and `init` writes through it with volatile accesses, and
+ neither the atomic nor the MMIO is in the extracted crate.
+-/
+
+/-- Reading the base of a freshly built handle gives back the address it was
+ built with, for every address including zero and the largest. -/
+theorem plic_base_returns_what_plic_new_was_given (b : Std.U64) :
+ (do let p ← plic_new b; plic_base p) = ok b := rfl
+
+/-- Every handle is the one `new` builds from its own base, so `base` loses no
+ information and two handles with the same base are the same handle. -/
+theorem plic_new_rebuilds_a_handle_from_what_plic_base_returns
+ (p : plic.Plic) (b : Std.U64) (h : plic_base p = ok b) :
+ plic_new b = ok p := by
+ unfold plic_base plic.Plic.impl.base at h
+ cases p
+ simp only [ok.injEq] at h
+ subst h
+ rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RegistersPlic.the_plic_new_wrapper_is_its_method
#print axioms NonosExtraction.RegistersPlic.the_plic_base_wrapper_is_its_method
+#print axioms NonosExtraction.RegistersPlic.plic_base_returns_what_plic_new_was_given
+#print axioms NonosExtraction.RegistersPlic.plic_new_rebuilds_a_handle_from_what_plic_base_returns
end NonosExtraction.RegistersPlic
diff --git a/verification/extraction/lean/NonosExtraction/RegistryVersionRefinement.lean b/verification/extraction/lean/NonosExtraction/RegistryVersionRefinement.lean
index ffc3af90d5..5a254c23a9 100644
--- a/verification/extraction/lean/NonosExtraction/RegistryVersionRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RegistryVersionRefinement.lean
@@ -35,8 +35,24 @@ namespace NonosExtraction.RegistryVersion
theorem the_libraryversion_new_wrapper_is_its_method (a : Std.U32) (b : Std.U32) (c : Std.U32) :
libraryversion_new a b c = version.LibraryVersion.new a b c := rfl
+/-! ### A version stores its three parts in order
+
+`LibraryVersion::new` builds a version from major, minor and patch numbers. The
+theorem below shows each argument lands in its own field, so a constructor that
+swapped any two parts, which would silently reorder version comparisons, is
+ruled out. It says nothing about the comparison methods on `LibraryVersion`,
+which this crate does not extract. -/
+
+/-- Construction never fails and each part is stored in the field of its name. -/
+theorem libraryversion_new_keeps_major_minor_and_patch_in_place
+ (major minor patch : Std.U32) :
+ ∃ v, libraryversion_new major minor patch = ok v ∧
+ v.major = major ∧ v.minor = minor ∧ v.patch = patch :=
+ ⟨_, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RegistryVersion.the_libraryversion_new_wrapper_is_its_method
+#print axioms NonosExtraction.RegistryVersion.libraryversion_new_keeps_major_minor_and_patch_in_place
end NonosExtraction.RegistryVersion
diff --git a/verification/extraction/lean/NonosExtraction/Riscv64ContextTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/Riscv64ContextTypesRefinement.lean
index e32619b359..06505064df 100644
--- a/verification/extraction/lean/NonosExtraction/Riscv64ContextTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/Riscv64ContextTypesRefinement.lean
@@ -38,9 +38,65 @@ theorem the_userentry_zeroed_wrapper_is_its_method :
theorem the_saveduser_zeroed_wrapper_is_its_method :
saveduser_zeroed = types.SavedUser.zeroed := rfl
+/-! ### Zeroed contexts carry nothing and cannot be entered
+
+`save_user_frame` starts from `SavedUser::zeroed` and then writes all 31
+general registers, `sepc`, `sstatus` and the kernel stack, so any slot the
+copy missed would hold the zero from `zeroed` rather than a stale value.
+`enter_user` refuses a `UserEntry` whose `entry` or `user_sp` is zero or whose
+`kernel_sp` is zero, and `riscv64_enter_user` loads `args[0..8]` into `a0` to
+`a7` at `sret`. The theorems below establish that every register slot and
+every argument of the zeroed records is zero, and that a zeroed `UserEntry`
+has exactly the zero entry point, user stack and kernel stack that the entry
+guard refuses. Its `sstatus` is zero, so `SPP` is clear, but so is `SPIE`: it
+is not `SSTATUS_USER_INITIAL`. They cannot establish that `enter_user`,
+`gprs::copy` or the assembly behave as described: those are not extracted,
+and the guard's conditions are read from the kernel source. -/
+
+/-- A zeroed saved user context holds zero in all 31 general register slots
+and in `sepc`, `sstatus` and `kernel_sp`, so nothing from an earlier context
+survives into a fresh one. -/
+theorem saveduser_zeroed_holds_no_register :
+ ∃ s, saveduser_zeroed = ok s ∧
+ s.gprs.val = List.replicate 31 0#u64 ∧
+ s.sepc = 0#u64 ∧ s.sstatus = 0#u64 ∧ s.kernel_sp = 0#u64 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- Read slot by slot: each of the 31 general register slots of a zeroed saved
+context, the slots `gprs::copy` writes as `ra` through `t6`, reads zero. -/
+theorem saveduser_zeroed_reads_zero_in_every_gpr_slot :
+ ∃ s, saveduser_zeroed = ok s ∧ s.gprs.val.length = 31 ∧
+ ∀ i, i < 31 → s.gprs.val[i]! = 0#u64 := by
+ refine ⟨_, rfl, rfl, ?_⟩
+ intro i hi
+ show (List.replicate 31 0#u64)[i]! = 0#u64
+ rw [getElem!_pos (List.replicate 31 0#u64) i (by simp; omega)]
+ exact List.getElem_replicate _
+
+/-- A zeroed user entry has entry point, user stack and kernel stack all zero,
+each of which the `enter_user` guard refuses, so a caller that forgets to fill
+one of them is turned away rather than sent to address zero. Its `sstatus` is
+zero, so the supervisor previous privilege bit (bit 8) is clear. -/
+theorem userentry_zeroed_is_what_the_entry_guard_refuses :
+ ∃ u, userentry_zeroed = ok u ∧
+ u.entry.val = 0 ∧ u.user_sp.val = 0 ∧ u.kernel_sp.val = 0 ∧
+ u.sstatus.val = 0 ∧ u.sstatus.val.testBit 8 = false :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- A zeroed user entry passes zero in all eight argument registers `a0` to
+`a7`, so no kernel value reaches user mode through them. -/
+theorem userentry_zeroed_passes_zero_in_every_argument :
+ ∃ u, userentry_zeroed = ok u ∧
+ u.args.val = List.replicate 8 0#u64 ∧ u.args.val.length = 8 :=
+ ⟨_, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.Riscv64ContextTypes.the_userentry_zeroed_wrapper_is_its_method
#print axioms NonosExtraction.Riscv64ContextTypes.the_saveduser_zeroed_wrapper_is_its_method
+#print axioms NonosExtraction.Riscv64ContextTypes.saveduser_zeroed_holds_no_register
+#print axioms NonosExtraction.Riscv64ContextTypes.saveduser_zeroed_reads_zero_in_every_gpr_slot
+#print axioms NonosExtraction.Riscv64ContextTypes.userentry_zeroed_is_what_the_entry_guard_refuses
+#print axioms NonosExtraction.Riscv64ContextTypes.userentry_zeroed_passes_zero_in_every_argument
end NonosExtraction.Riscv64ContextTypes
diff --git a/verification/extraction/lean/NonosExtraction/Riscv64FpuContextRefinement.lean b/verification/extraction/lean/NonosExtraction/Riscv64FpuContextRefinement.lean
index 5c03a86fbc..4489638c7f 100644
--- a/verification/extraction/lean/NonosExtraction/Riscv64FpuContextRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/Riscv64FpuContextRefinement.lean
@@ -35,8 +35,60 @@ namespace NonosExtraction.Riscv64FpuContext
theorem the_fpcontext_zeroed_wrapper_is_its_method :
fpcontext_zeroed = context.FpContext.zeroed := rfl
+/-! ### A zeroed save area is a safe first state on riscv64
+
+A task's first floating-point state is whatever `riscv64_fpu_restore` loads
+from `FpContext::zeroed` when the lazy-enable path first grants the task the
+FPU, so the value that function returns is the register file every fresh task
+begins with. The theorems below establish that no `f` register and no padding
+word carries a value, so nothing an earlier task left can reach a new one
+through this area, and that `fcsr` selects round to nearest, ties to even, with
+no accrued exception flag set.
+
+The rounding point matters because not every `frm` value is legal: 5 and 6 are
+reserved and 7 (dynamic) is invalid in the register itself, and any
+floating-point instruction that uses dynamic rounding raises an illegal
+instruction exception while `frm` holds one of them. Zero is the valid mode
+0b000. Unlike x86_64 `MXCSR`, `fcsr` has no trap enables, so there is no
+polarity question of the kind `Nonos.FpuState` records.
+
+These are statements about the value the kernel builds, not about the machine.
+The restore itself is assembly outside the extraction, the byte offsets it uses
+(`256` for `fcsr`) depend on the `repr(C)` layout that Aeneas does not model,
+and `FpSlot::zeroed` and `try_enable_for_current_task`, which reach this
+function, are not extracted.
+-/
+
+/-- Read one bit of a 32-bit control word. -/
+def fcsrBit (w : Std.U32) (i : Nat) : Bool := w.val.testBit i
+
+/-- The zeroed area leaves every `f` register and the padding word at zero, so
+ a fresh task observes nothing of the task that used the FPU before it. -/
+theorem fpcontext_zeroed_leaves_no_register_value :
+ ∃ c, fpcontext_zeroed = ok c ∧
+ (∀ x ∈ c.f.val, x = 0#u64) ∧ c._pad = 0#u32 := by
+ refine ⟨_, rfl, ?_, rfl⟩
+ intro x hx
+ rw [Array.repeat_val] at hx
+ exact List.eq_of_mem_replicate hx
+
+/-- The zeroed `fcsr` has rounding mode field `frm` (bits 5 to 7) equal to 0,
+ round to nearest with ties to even, which is a legal static mode, and
+ none of the accrued flags `NX`, `UF`, `OF`, `DZ`, `NV` (bits 0 to 4) set.
+ A fresh task therefore rounds the IEEE 754 default way and does not start
+ with a condition it never raised. -/
+theorem fpcontext_zeroed_rounds_to_nearest_even_with_no_flag :
+ ∃ c, fpcontext_zeroed = ok c ∧
+ c.fcsr.val / 2 ^ 5 % 8 = 0 ∧
+ ∀ i ∈ [0, 1, 2, 3, 4], fcsrBit c.fcsr i = false := by
+ refine ⟨_, rfl, ?_⟩
+ unfold fcsrBit
+ decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.Riscv64FpuContext.the_fpcontext_zeroed_wrapper_is_its_method
+#print axioms NonosExtraction.Riscv64FpuContext.fpcontext_zeroed_leaves_no_register_value
+#print axioms NonosExtraction.Riscv64FpuContext.fpcontext_zeroed_rounds_to_nearest_even_with_no_flag
end NonosExtraction.Riscv64FpuContext
diff --git a/verification/extraction/lean/NonosExtraction/RtcBcdRefinement.lean b/verification/extraction/lean/NonosExtraction/RtcBcdRefinement.lean
index bdd3be0c88..12ca1c2aa9 100644
--- a/verification/extraction/lean/NonosExtraction/RtcBcdRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/RtcBcdRefinement.lean
@@ -41,10 +41,188 @@ theorem the_bin_to_bcd_wrapper_is_its_method (a : Std.U8) :
theorem the_is_valid_bcd_wrapper_is_its_method (a : Std.U8) :
is_valid_bcd a = rtc_bcd.is_valid_bcd a := rfl
+/-! ### Nibbles, round trips and the edge of two decimal digits
+
+The RTC keeps its time registers in packed BCD unless status register B says
+binary, and `read_rtc`, `write_rtc` and the alarm code convert every field with
+these three functions. The theorems below give each function an exact value on
+every byte, and from those values they establish what the callers rely on:
+`bcd_to_bin` reads the high nibble as tens and the low nibble as units and
+cannot overflow on any byte the hardware returns (a garbled `0xFF` reads as
+165, not a panic); `bin_to_bcd` inverts it on `0..=99` and produces a byte that
+`is_valid_bcd` accepts; `is_valid_bcd` accepts exactly the bytes whose two
+nibbles are decimal digits, and on those `bcd_to_bin` followed by `bin_to_bcd`
+gives the byte back. The boundary is stated too: from 100 on `bin_to_bcd`
+produces bytes that are not BCD, and from 160 on the tens digit is shifted off
+the top of the byte.
+
+Nothing here establishes that the values a caller passes are in range. That is
+the job of `RtcTime::validate` and of the CMOS itself, and neither is extracted
+in this crate; the port I/O in `cmos_read` and `cmos_write` is outside the
+extraction altogether.
+-/
+
+private theorem shr_u8 (x : Std.U8) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 8) :
+ ∃ z : Std.U8, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+private theorem shl_u8 (x : Std.U8) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 8) :
+ ∃ z : Std.U8, x <<< k = ok z ∧ z.val = x.val * 2 ^ k.toNat % 256 := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftLeft_IScalar_spec x k (UScalar.size .U8) h0
+ (by simpa using h1) rfl)
+ exact ⟨z, hz, by rw [hv, Nat.shiftLeft_eq]; simp [U8.size, U8.numBits]⟩
+
+/-- `bcd_to_bin` succeeds on every byte, and its value is the high nibble times
+ ten plus the low nibble. The largest value, 165 at `0xFF`, is below 256, so
+ the multiplication and the addition that Rust checks can never trap. -/
+theorem bcd_to_bin_is_tens_nibble_times_ten_plus_units_nibble (b : Std.U8) :
+ ∃ r : Std.U8, bcd_to_bin b = ok r ∧ r.val = b.val / 16 * 10 + b.val % 16 := by
+ unfold bcd_to_bin rtc_bcd.bcd_to_bin
+ obtain ⟨i, hi, hiv⟩ := shr_u8 b 4#i32 (by decide) (by decide)
+ have hb := b.hBounds
+ simp only [UScalarTy.numBits] at hb
+ have hmax : UScalar.max .U8 = 255 := by simp [U8.max, U8.numBits]
+ have hiv' : i.val = b.val / 16 := by simpa using hiv
+ have hten : (10#u8 : Std.U8).val = 10 := rfl
+ have ⟨j, hj, hjv⟩ := WP.spec_imp_exists
+ (UScalar.mul_spec (x := i) (y := 10#u8) (by rw [hmax, hten, hiv']; omega))
+ have hand : (b &&& 15#u8).val = b.val % 16 := by
+ rw [UScalar.val_and]
+ exact Nat.and_two_pow_sub_one_eq_mod b.val 4
+ have ⟨k, hk, hkv⟩ := WP.spec_imp_exists
+ (UScalar.add_spec (x := j) (y := b &&& 15#u8) (by rw [hmax, hjv, hten, hiv', hand]; omega))
+ simp only [hi, hj, hk, bind_tc_ok, lift]
+ refine ⟨k, rfl, ?_⟩
+ rw [hkv, hjv, hten, hiv', hand]
+
+/-- `bin_to_bcd` succeeds on every byte, and its value is the tens digit
+ shifted into the high nibble, reduced modulo 256, with the units digit
+ or-ed into the low nibble. The reduction is where bytes from 160 up lose
+ their tens digit. -/
+theorem bin_to_bcd_packs_tens_over_units (b : Std.U8) :
+ ∃ r : Std.U8, bin_to_bcd b = ok r ∧ r.val = b.val / 10 * 16 % 256 ||| b.val % 10 := by
+ unfold bin_to_bcd rtc_bcd.bin_to_bcd
+ have ⟨i, hi, hiv⟩ := UScalar.div_spec b (y := 10#u8) (by decide)
+ obtain ⟨j, hj, hjv⟩ := shl_u8 i 4#i32 (by decide) (by decide)
+ have ⟨k, hk, hkv⟩ := WP.spec_imp_exists (UScalar.rem_spec b (y := 10#u8) (by decide))
+ simp only [hi, hj, hk, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ rw [UScalar.val_or, hjv, hkv, hiv]
+ simp
+
+/-- `is_valid_bcd` accepts exactly the bytes whose low nibble and high nibble
+ are both at most nine. -/
+theorem is_valid_bcd_accepts_exactly_two_decimal_nibbles (b : Std.U8) :
+ is_valid_bcd b = ok (decide (b.val % 16 ≤ 9 ∧ b.val / 16 ≤ 9)) := by
+ unfold is_valid_bcd rtc_bcd.is_valid_bcd
+ have hb := b.hBounds
+ simp only [UScalarTy.numBits] at hb
+ have hand : (b &&& 15#u8).val = b.val % 16 := by
+ rw [UScalar.val_and]
+ exact Nat.and_two_pow_sub_one_eq_mod b.val 4
+ obtain ⟨i, hi, hiv⟩ := shr_u8 b 4#i32 (by decide) (by decide)
+ have hiv' : i.val = b.val / 16 := by simpa using hiv
+ have hand2 : (i &&& 15#u8).val = b.val / 16 := by
+ rw [UScalar.val_and, hiv']
+ rw [show (15#u8 : Std.U8).val = 2 ^ 4 - 1 from rfl, Nat.and_two_pow_sub_one_eq_mod]
+ omega
+ simp only [lift, bind_tc_ok, hi]
+ by_cases h : b.val % 16 ≤ 9
+ · have h' : (b &&& 15#u8) ≤ 9#u8 := by
+ show (b &&& 15#u8).val ≤ (9#u8 : Std.U8).val
+ rw [hand]; simpa using h
+ rw [if_pos h']
+ congr 1
+ apply decide_eq_decide.mpr
+ show (i &&& 15#u8).val ≤ (9#u8 : Std.U8).val ↔ _
+ rw [hand2]; simp [h]
+ · have h' : ¬ (b &&& 15#u8) ≤ 9#u8 := by
+ show ¬ (b &&& 15#u8).val ≤ (9#u8 : Std.U8).val
+ rw [hand]; simpa using h
+ rw [if_neg h']
+ simp [h]
+
+/-- On every two digit number, `bin_to_bcd` then `bcd_to_bin` returns the
+ number, and the intermediate byte is one `is_valid_bcd` accepts. This is
+ the agreement `write_rtc` and `read_rtc` rely on for a value to survive a
+ trip through the CMOS. -/
+theorem bin_to_bcd_then_bcd_to_bin_is_identity_below_one_hundred (b : Std.U8)
+ (h : b.val < 100) :
+ ∃ r : Std.U8, bin_to_bcd b = ok r ∧ is_valid_bcd r = ok true ∧ bcd_to_bin r = ok b := by
+ obtain ⟨r, hr, hrv⟩ := bin_to_bcd_packs_tens_over_units b
+ obtain ⟨s, hs, hsv⟩ := bcd_to_bin_is_tens_nibble_times_ten_plus_units_nibble r
+ refine ⟨r, hr, ?_, ?_⟩
+ · rw [is_valid_bcd_accepts_exactly_two_decimal_nibbles, hrv]
+ have key : ∀ n, n < 100 →
+ ((n / 10 * 16 % 256 ||| n % 10) % 16 ≤ 9 ∧ (n / 10 * 16 % 256 ||| n % 10) / 16 ≤ 9) := by
+ decide
+ simp [key b.val h]
+ · rw [hs]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hsv, hrv]
+ have key : ∀ n, n < 100 →
+ (n / 10 * 16 % 256 ||| n % 10) / 16 * 10 + (n / 10 * 16 % 256 ||| n % 10) % 16 = n := by
+ decide
+ exact key b.val h
+
+/-- On every byte `is_valid_bcd` accepts, `bcd_to_bin` then `bin_to_bcd`
+ returns the byte, so a valid register value is read back to the same
+ number it will be written as. -/
+theorem bcd_to_bin_then_bin_to_bcd_is_identity_on_valid_bcd (b : Std.U8)
+ (h : is_valid_bcd b = ok true) :
+ ∃ r : Std.U8, bcd_to_bin b = ok r ∧ r.val < 100 ∧ bin_to_bcd r = ok b := by
+ rw [is_valid_bcd_accepts_exactly_two_decimal_nibbles] at h
+ have hv : b.val % 16 ≤ 9 ∧ b.val / 16 ≤ 9 := by simpa using h
+ have hb := b.hBounds
+ simp only [UScalarTy.numBits] at hb
+ obtain ⟨r, hr, hrv⟩ := bcd_to_bin_is_tens_nibble_times_ten_plus_units_nibble b
+ obtain ⟨s, hs, hsv⟩ := bin_to_bcd_packs_tens_over_units r
+ refine ⟨r, hr, by omega, ?_⟩
+ rw [hs]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ rw [hsv, hrv]
+ have key : ∀ n, n < 256 → n % 16 ≤ 9 → n / 16 ≤ 9 →
+ ((n / 16 * 10 + n % 16) / 10 * 16 % 256 ||| (n / 16 * 10 + n % 16) % 10) = n := by
+ decide
+ exact key b.val hb hv.1 hv.2
+
+/-- One hundred is where the encoding stops: `bin_to_bcd 100` is `0xA0`, whose
+ high nibble is ten, and `is_valid_bcd` refuses it. Every value a caller
+ passes must be at most 99. -/
+theorem bin_to_bcd_of_one_hundred_is_not_bcd :
+ bin_to_bcd 100#u8 = ok 0xA0#u8 ∧ is_valid_bcd 0xA0#u8 = ok false := by
+ obtain ⟨r, hr, hrv⟩ := bin_to_bcd_packs_tens_over_units 100#u8
+ refine ⟨?_, ?_⟩
+ · rw [hr]; congr 1; apply UScalar.eq_of_val_eq; rw [hrv]; decide
+ · rw [is_valid_bcd_accepts_exactly_two_decimal_nibbles]; simp
+
+/-- From 160 the tens digit no longer fits in the high nibble and is shifted
+ out of the byte: `bin_to_bcd 160` is `0x00`, the same byte as
+ `bin_to_bcd 0`. The shift wraps rather than traps. -/
+theorem bin_to_bcd_of_one_hundred_sixty_wraps_to_zero :
+ bin_to_bcd 160#u8 = ok 0#u8 ∧ bin_to_bcd 0#u8 = ok 0#u8 := by
+ obtain ⟨r, hr, hrv⟩ := bin_to_bcd_packs_tens_over_units 160#u8
+ obtain ⟨s, hs, hsv⟩ := bin_to_bcd_packs_tens_over_units 0#u8
+ refine ⟨?_, ?_⟩
+ · rw [hr]; congr 1; apply UScalar.eq_of_val_eq; rw [hrv]; decide
+ · rw [hs]; congr 1; apply UScalar.eq_of_val_eq; rw [hsv]; decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.RtcBcd.the_bcd_to_bin_wrapper_is_its_method
#print axioms NonosExtraction.RtcBcd.the_bin_to_bcd_wrapper_is_its_method
#print axioms NonosExtraction.RtcBcd.the_is_valid_bcd_wrapper_is_its_method
+#print axioms NonosExtraction.RtcBcd.bcd_to_bin_is_tens_nibble_times_ten_plus_units_nibble
+#print axioms NonosExtraction.RtcBcd.bin_to_bcd_packs_tens_over_units
+#print axioms NonosExtraction.RtcBcd.is_valid_bcd_accepts_exactly_two_decimal_nibbles
+#print axioms NonosExtraction.RtcBcd.bin_to_bcd_then_bcd_to_bin_is_identity_below_one_hundred
+#print axioms NonosExtraction.RtcBcd.bcd_to_bin_then_bin_to_bcd_is_identity_on_valid_bcd
+#print axioms NonosExtraction.RtcBcd.bin_to_bcd_of_one_hundred_is_not_bcd
+#print axioms NonosExtraction.RtcBcd.bin_to_bcd_of_one_hundred_sixty_wraps_to_zero
end NonosExtraction.RtcBcd
diff --git a/verification/extraction/lean/NonosExtraction/SdtEntryCountRefinement.lean b/verification/extraction/lean/NonosExtraction/SdtEntryCountRefinement.lean
index f3e72ccae9..a46276ed1b 100644
--- a/verification/extraction/lean/NonosExtraction/SdtEntryCountRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SdtEntryCountRefinement.lean
@@ -35,8 +35,82 @@ namespace NonosExtraction.SdtEntryCount
theorem the_sdt_entry_count_wrapper_is_its_method (a : Std.Usize) (b : Std.Usize) (c : Std.Usize) :
sdt_entry_count a b c = entry_count.sdt_entry_count a b c := rfl
+/-! ### How many entries follow a table header
+
+ The RSDT, XSDT and MCFG parsers loop `for i in 0..sdt_entry_count(..)` and
+ read entry `i` at the header size plus `i` entry sizes. These theorems say
+ that the count is the number of whole entries in the bytes after the header,
+ that every counted entry lies inside the table's stated length, that no
+ whole entry is left out, and that a length shorter than the header gives no
+ entries instead of a wrapped, enormous count. They cannot say that the
+ stated length matches the memory actually mapped: that comes from firmware
+ and the parsers are not extracted. -/
+
+/-- With a nonzero entry size the count is the bytes after the header, divided
+ down to whole entries, and the subtraction stops at zero. -/
+theorem sdt_entry_count_is_the_whole_entries_after_the_header (len hdr es : Std.Usize)
+ (hes : es.val ≠ 0) :
+ ∃ r, sdt_entry_count len hdr es = ok r ∧ r.val = (len.val - hdr.val) / es.val := by
+ unfold sdt_entry_count entry_count.sdt_entry_count
+ have hne : ¬ es = 0#usize := fun h => hes (by rw [h]; rfl)
+ simp only [hne, if_false, lift, bind_tc_ok]
+ have hv : (core.num.Usize.saturating_sub len hdr).val = len.val - hdr.val := by
+ simp only [core.num.Usize.saturating_sub, UScalar.saturating_sub]
+ have := len.hBounds
+ simp only [UScalar.val, BitVec.toNat_ofNat] at *
+ rw [Nat.zero_max, Nat.mod_eq_of_lt (by omega)]
+ obtain ⟨z, hz, hzv⟩ := UScalar.div_spec (core.num.Usize.saturating_sub len hdr) (y := es) hes
+ exact ⟨z, hz, by rw [hzv, hv]⟩
+
+/-- The contract the table loops rely on: either there are no entries, or the
+ last counted entry ends at or before the table's length; and one more entry
+ would not fit, so no entry the table holds is skipped. -/
+theorem sdt_entry_count_covers_the_table_and_never_reads_past_it (len hdr es : Std.Usize)
+ (hes : es.val ≠ 0) :
+ ∃ r, sdt_entry_count len hdr es = ok r ∧
+ (r.val = 0 ∨ hdr.val + r.val * es.val ≤ len.val) ∧
+ len.val < hdr.val + (r.val + 1) * es.val := by
+ obtain ⟨r, hr, hrv⟩ := sdt_entry_count_is_the_whole_entries_after_the_header len hdr es hes
+ refine ⟨r, hr, ?_, ?_⟩
+ · rw [hrv]
+ by_cases hl : hdr.val ≤ len.val
+ · right
+ have := Nat.div_mul_le_self (len.val - hdr.val) es.val
+ omega
+ · left
+ rw [Nat.sub_eq_zero_of_le (by omega)]
+ simp
+ · rw [hrv]
+ have := Nat.lt_div_mul_add (a := len.val - hdr.val) (Nat.pos_of_ne_zero hes)
+ rw [Nat.add_mul, Nat.one_mul]
+ omega
+
+/-- The three edges at the RSDT's thirty six byte header and four byte entries:
+ fourteen bytes after the header hold three entries and a two byte tail that
+ is ignored; a stated length of twenty, shorter than the header, holds none;
+ and an entry size of zero is refused with a count of zero rather than a
+ division by zero. -/
+theorem sdt_entry_count_at_the_edges_of_an_rsdt :
+ sdt_entry_count 50#usize 36#usize 4#usize = ok 3#usize ∧
+ sdt_entry_count 20#usize 36#usize 4#usize = ok 0#usize ∧
+ sdt_entry_count 4096#usize 36#usize 0#usize = ok 0#usize := by
+ refine ⟨?_, ?_, rfl⟩
+ · obtain ⟨r, hr, hrv⟩ :=
+ sdt_entry_count_is_the_whole_entries_after_the_header 50#usize 36#usize 4#usize (by decide)
+ rw [hr]
+ congr 1
+ exact UScalar.eq_of_val_eq (by rw [hrv]; rfl)
+ · obtain ⟨r, hr, hrv⟩ :=
+ sdt_entry_count_is_the_whole_entries_after_the_header 20#usize 36#usize 4#usize (by decide)
+ rw [hr]
+ congr 1
+ exact UScalar.eq_of_val_eq (by rw [hrv]; rfl)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SdtEntryCount.the_sdt_entry_count_wrapper_is_its_method
+#print axioms NonosExtraction.SdtEntryCount.sdt_entry_count_is_the_whole_entries_after_the_header
+#print axioms NonosExtraction.SdtEntryCount.sdt_entry_count_covers_the_table_and_never_reads_past_it
+#print axioms NonosExtraction.SdtEntryCount.sdt_entry_count_at_the_edges_of_an_rsdt
end NonosExtraction.SdtEntryCount
diff --git a/verification/extraction/lean/NonosExtraction/SecurityMmioRangeRefinement.lean b/verification/extraction/lean/NonosExtraction/SecurityMmioRangeRefinement.lean
index 0fc3313d02..275299e3cb 100644
--- a/verification/extraction/lean/NonosExtraction/SecurityMmioRangeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SecurityMmioRangeRefinement.lean
@@ -35,8 +35,86 @@ namespace NonosExtraction.SecurityMmioRange
theorem the_range_ok_wrapper_is_its_method (a : Std.Usize) (b : Std.Usize) :
range_ok a b = mmio_range.range_ok a b := rfl
+/-! ### The admitted windows are exactly the non-empty ones that fit
+
+`range_ok` is the arithmetic `validate_mmio_region` applies first, before it
+checks page alignment and the MMIO address bands. The theorems below give its
+exact answer for every base and size, show that it agrees with the window
+reading of the tier-one model `Nonos.Mmio` (an admitted window holds its own
+base and every address it holds lies below `usize::MAX`), and record the edge at
+the top of the address space: a window whose last byte is `usize::MAX` itself
+is refused, because its one-past-the-end address does not fit in a `usize`.
+They say nothing about the alignment and band checks that follow in
+`validate_mmio_region`, which is not extracted, nor about whether the device
+behind an admitted window exists.
+-/
+
+/-- `range_ok` never fails, and it answers `true` exactly when the size is
+ non-zero and `base + size` does not exceed `usize::MAX`. A version that
+ dropped the zero-size test would admit empty windows, and one that treated
+ an overflowing `checked_add` as success would admit a wrapping window. -/
+theorem range_ok_is_nonempty_and_does_not_overflow (base size : Std.Usize) :
+ range_ok base size =
+ ok (decide (0 < size.val ∧ base.val + size.val ≤ Usize.max)) := by
+ unfold range_ok mmio_range.range_ok
+ split
+ · rename_i h
+ have h0 : size.val = 0 := by rw [h]; rfl
+ simp [h0]
+ · rename_i h
+ have h0 : size.val ≠ 0 := by
+ intro hv; apply h; exact UScalar.eq_of_val_eq (by simpa using hv)
+ simp only [lift, bind_tc_ok]
+ have ha := Usize.checked_add_bv_spec base size
+ cases hc : Usize.checked_add base size with
+ | none =>
+ rw [hc] at ha
+ simp only [ok.injEq, Bool.false_eq, decide_eq_false_iff_not, not_and, not_le]
+ intro _; exact ha
+ | some e =>
+ rw [hc] at ha
+ obtain ⟨hle, hv, _⟩ := ha
+ simp only [ok.injEq, gt_iff_lt, decide_eq_decide]
+ constructor
+ · intro _; exact ⟨by omega, hle⟩
+ · intro _; show base.val < e.val; omega
+
+/-- Agreement with the tier-one model. `Nonos.Mmio.contains` says a window
+ holds `addr` when `base ≤ addr < base + size`; that predicate is written out
+ here because the model is not built for this tier. `range_ok` admits a
+ window exactly when the window holds its own base (so it is not empty) and
+ every address it holds lies strictly below `usize::MAX`. -/
+theorem range_ok_admits_the_model_windows_that_fit (base size : Std.Usize) :
+ range_ok base size = ok true ↔
+ (base.val < base.val + size.val ∧
+ ∀ addr, base.val ≤ addr ∧ addr < base.val + size.val → addr < Usize.max) := by
+ rw [range_ok_is_nonempty_and_does_not_overflow]
+ simp only [ok.injEq, decide_eq_true_eq]
+ constructor
+ · rintro ⟨h0, hle⟩
+ exact ⟨by omega, fun addr ⟨_, h⟩ => by omega⟩
+ · rintro ⟨h0, hall⟩
+ refine ⟨by omega, ?_⟩
+ have := hall (base.val + size.val - 1) ⟨by omega, by omega⟩
+ omega
+
+/-- The edge at the top of the address space. A window whose end is
+ exactly one past `usize::MAX`, so that its last byte is the highest address,
+ is refused: `checked_add` overflows and `range_ok` answers `false`. This is
+ conservative rather than unsafe, and the kernel's MMIO bands do not reach
+ that address. -/
+theorem range_ok_refuses_a_window_ending_at_the_top_of_memory (base size : Std.Usize)
+ (htop : base.val + size.val = Usize.max + 1) :
+ range_ok base size = ok false := by
+ rw [range_ok_is_nonempty_and_does_not_overflow]
+ simp only [ok.injEq, decide_eq_false_iff_not, not_and, not_le]
+ intro _; omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SecurityMmioRange.the_range_ok_wrapper_is_its_method
+#print axioms NonosExtraction.SecurityMmioRange.range_ok_is_nonempty_and_does_not_overflow
+#print axioms NonosExtraction.SecurityMmioRange.range_ok_admits_the_model_windows_that_fit
+#print axioms NonosExtraction.SecurityMmioRange.range_ok_refuses_a_window_ending_at_the_top_of_memory
end NonosExtraction.SecurityMmioRange
diff --git a/verification/extraction/lean/NonosExtraction/SemaphorePureRefinement.lean b/verification/extraction/lean/NonosExtraction/SemaphorePureRefinement.lean
index a580b0fc05..5403f9b56b 100644
--- a/verification/extraction/lean/NonosExtraction/SemaphorePureRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SemaphorePureRefinement.lean
@@ -21,8 +21,9 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.SemaphorePure
+import Nonos.Semaphore
-open Aeneas Aeneas.Std Result
+open Aeneas Aeneas.Std Result WP
open nonos_x_semaphore_pure
set_option linter.hashCommand false
@@ -41,10 +42,111 @@ theorem the_acquire_count_wrapper_is_its_method (a : Std.Usize) :
theorem the_release_count_wrapper_is_its_method (a : Std.Usize) (b : Std.Usize) :
release_count a b = pure.release_count a b := rfl
+/-! ### Permit arithmetic against the tier-one semaphore model
+
+The theorems below tie the three permit computations to `Nonos.Semaphore`, the
+hand-written model whose invariants the tier-one proofs establish. The
+availability test is exactly the model's `canAcquire`. Taking a permit agrees
+with the model's `acquire` whenever a permit exists, and at the one input where
+the model's natural subtraction saturates to zero the code aborts with an
+integer overflow instead of wrapping to the largest count. The availability
+test is exactly the guard that keeps a caller away from that abort. Returning a
+permit agrees with the model's `release` for every pair of machine words,
+including counts already above the ceiling and the largest `usize`, so it never
+overflows and never leaves the count above the capacity. A matched acquire and
+release restores the count.
+
+These are statements about the arithmetic only. The compare-exchange loops in
+`acquire.rs` and `release.rs` that apply it to an atomic counter, and the
+scheduler calls that park and wake waiters, are not extracted, so nothing here
+says the loop retries correctly or that a waiter is ever woken.
+-/
+
+/-- The availability test reads the count as the model does: a permit can be
+ taken exactly when the count is positive. -/
+theorem can_acquire_is_the_models_availability_test (c : Std.Usize) (cap : Nat) :
+ ∃ b, can_acquire c = ok b ∧ (b = true ↔ Nonos.Semaphore.canAcquire ⟨c.val, cap⟩) := by
+ refine ⟨_, rfl, ?_⟩
+ simp [Nonos.Semaphore.canAcquire]
+
+/-- Taking a permit from a positive count agrees with the model and lowers the
+ count by exactly one. -/
+theorem acquire_count_refines_the_model_when_a_permit_exists (c : Std.Usize) (cap : Nat)
+ (h : 0 < c.val) :
+ ∃ r, acquire_count c = ok r ∧ r.val = (Nonos.Semaphore.acquire ⟨c.val, cap⟩).count ∧
+ r.val + 1 = c.val := by
+ unfold acquire_count pure.acquire_count
+ obtain ⟨z, hz, hv⟩ := spec_imp_exists (Usize.sub_spec (x := c) (y := 1#usize) (by simp; omega))
+ refine ⟨z, hz, ?_, ?_⟩ <;> simp [Nonos.Semaphore.acquire] at hv ⊢ <;> omega
+
+/-- On an empty semaphore the code aborts rather than agreeing with the model,
+ whose `acquire` saturates the count at zero. A wrapping subtraction would
+ instead hand back `usize::MAX` permits. -/
+theorem acquire_count_aborts_on_an_empty_semaphore :
+ acquire_count 0#usize = fail .integerOverflow := rfl
+
+/-- The availability test is exactly the precondition of taking a permit: when
+ it answers yes the subtraction succeeds, and when it answers no the
+ subtraction would abort. This is the contract `try_acquire` relies on when
+ it calls `acquire_count` only after `can_acquire`. -/
+theorem can_acquire_guards_acquire_count_exactly (c : Std.Usize) :
+ (can_acquire c = ok true → ∃ r, acquire_count c = ok r ∧ r.val + 1 = c.val) ∧
+ (can_acquire c = ok false → acquire_count c = fail .integerOverflow) := by
+ constructor
+ · intro hc
+ have h : 0 < c.val := by
+ simpa [can_acquire, pure.can_acquire] using hc
+ obtain ⟨r, hr, _, hv⟩ := acquire_count_refines_the_model_when_a_permit_exists c 0 h
+ exact ⟨r, hr, hv⟩
+ · intro hc
+ have h : c.val = 0 := by
+ simpa [can_acquire, pure.can_acquire] using hc
+ have : c = 0#usize := UScalar.eq_of_val_eq (by simp [h])
+ subst this
+ rfl
+
+/-- Returning a permit agrees with the model's saturating `release` for every
+ count and capacity, never overflows, and never leaves more permits than the
+ capacity, even when the count starts above the capacity or at `usize::MAX`. -/
+theorem release_count_refines_the_model_and_respects_the_ceiling (c cap : Std.Usize) :
+ ∃ r, release_count c cap = ok r ∧
+ r.val = (Nonos.Semaphore.release ⟨c.val, cap.val⟩).count ∧ r.val ≤ cap.val := by
+ unfold release_count pure.release_count
+ simp only [Nonos.Semaphore.release]
+ split
+ · rename_i h
+ refine ⟨cap, rfl, ?_, le_refl _⟩
+ have : cap.val ≤ c.val := h
+ omega
+ · rename_i h
+ have : c.val < cap.val := by simpa using h
+ obtain ⟨z, hz, hv⟩ := spec_imp_exists (Usize.add_spec (x := c) (y := 1#usize) (by scalar_tac))
+ refine ⟨z, hz, ?_, ?_⟩ <;> simp at hv <;> omega
+
+/-- A permit taken and then returned leaves the count where it was, for any
+ count that is positive and within the capacity. -/
+theorem release_count_undoes_acquire_count (c cap : Std.Usize)
+ (h : 0 < c.val) (hv : c.val ≤ cap.val) :
+ ∃ r, acquire_count c = ok r ∧ release_count r cap = ok c := by
+ obtain ⟨r, hr, _, hr1⟩ := acquire_count_refines_the_model_when_a_permit_exists c 0 h
+ obtain ⟨s, hs, hs1, _⟩ := release_count_refines_the_model_and_respects_the_ceiling r cap
+ refine ⟨r, hr, ?_⟩
+ rw [hs]
+ congr 1
+ apply UScalar.eq_of_val_eq
+ simp only [Nonos.Semaphore.release] at hs1
+ omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SemaphorePure.the_can_acquire_wrapper_is_its_method
#print axioms NonosExtraction.SemaphorePure.the_acquire_count_wrapper_is_its_method
#print axioms NonosExtraction.SemaphorePure.the_release_count_wrapper_is_its_method
+#print axioms NonosExtraction.SemaphorePure.can_acquire_is_the_models_availability_test
+#print axioms NonosExtraction.SemaphorePure.acquire_count_refines_the_model_when_a_permit_exists
+#print axioms NonosExtraction.SemaphorePure.acquire_count_aborts_on_an_empty_semaphore
+#print axioms NonosExtraction.SemaphorePure.can_acquire_guards_acquire_count_exactly
+#print axioms NonosExtraction.SemaphorePure.release_count_refines_the_model_and_respects_the_ceiling
+#print axioms NonosExtraction.SemaphorePure.release_count_undoes_acquire_count
end NonosExtraction.SemaphorePure
diff --git a/verification/extraction/lean/NonosExtraction/SeqlockPureRefinement.lean b/verification/extraction/lean/NonosExtraction/SeqlockPureRefinement.lean
index 8dcfadebd2..2eeff642aa 100644
--- a/verification/extraction/lean/NonosExtraction/SeqlockPureRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SeqlockPureRefinement.lean
@@ -21,8 +21,9 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.SeqlockPure
+import Nonos.Seqlock
-open Aeneas Aeneas.Std Result
+open Aeneas Aeneas.Std Result WP
open nonos_x_seqlock_pure
set_option linter.hashCommand false
@@ -41,10 +42,147 @@ theorem the_bump_wrapper_is_its_method (a : Std.U32) :
theorem the_read_valid_wrapper_is_its_method (a : Std.U32) (b : Std.U32) :
read_valid a b = pure.read_valid a b := rfl
+/-! ### The sequence discipline against the tier-one seqlock model
+
+The theorems below tie the three sequence computations to `Nonos.Seqlock`, the
+hand-written model whose consistency argument the tier-one proofs make. The
+stability test reads the low bit of the counter, which is the model's `stable`.
+The writer's step is the model's `writeBegin` taken modulo `2^32`, and it wraps
+the largest counter to zero. Because `2^32` is even, that wrap does not disturb
+the parity alternation: every step flips stability, so the store `write.rs`
+makes on entry is odd and the store on exit is even again, for every starting
+counter including the last two. The reader's test is exactly the model's
+`readAccepts`, and a reader whose two samples straddle a single writer step
+always rejects.
+
+These are statements about the counter arithmetic only. The atomic loads and
+stores, the fences that order them against the copy of the protected value,
+and the requirement that writers are serialised are outside the extracted code,
+so nothing here says the orderings are strong enough. The counter is 32 bits,
+so a reader that sleeps across exactly `2^31` complete writes sees the same
+even value on both sides and accepts; that limit is inherent to the width and
+is not refuted here.
+-/
+
+/-- The stability test reads the parity of the counter. -/
+theorem is_stable_reads_the_parity_of_the_counter (s : Std.U32) :
+ is_stable s = ok (decide (s.val % 2 = 0)) := by
+ unfold is_stable pure.is_stable
+ obtain ⟨z, hz, hv⟩ := spec_imp_exists (U32.rem_spec s (y := 2#u32) (by decide))
+ rw [hz]
+ simp only [bind_tc_ok]
+ congr 2
+ have : (2#u32).val = 2 := rfl
+ rw [this] at hv
+ apply propext
+ constructor
+ · intro h; rw [← hv, h]; rfl
+ · intro h; exact UScalar.eq_of_val_eq (by rw [hv, h]; rfl)
+
+/-- The stability test is the model's `stable`. -/
+theorem is_stable_is_the_models_stability (s : Std.U32) :
+ ∃ b, is_stable s = ok b ∧ (b = true ↔ Nonos.Seqlock.stable ⟨s.val⟩) := by
+ refine ⟨_, is_stable_reads_the_parity_of_the_counter s, ?_⟩
+ simp [Nonos.Seqlock.stable]
+
+/-- The writer's step is the model's `writeBegin` (equally its `writeEnd`)
+ reduced modulo `2^32`. -/
+theorem bump_is_the_models_write_step_modulo_the_word (s : Std.U32) :
+ ∃ r, bump s = ok r ∧ r.val = (Nonos.Seqlock.writeBegin ⟨s.val⟩).seq % 2 ^ 32 := by
+ refine ⟨_, rfl, ?_⟩
+ simp only [Nonos.Seqlock.writeBegin, core.num.U32.wrapping_add,
+ UScalar.wrapping_add_val_eq, UScalar.size, UScalarTy.U32_numBits_eq]
+ simp
+
+/-- At the top of the counter the writer's step wraps to zero rather than
+ failing or sticking. -/
+theorem bump_wraps_the_largest_counter_to_zero :
+ bump 4294967295#u32 = ok 0#u32 := rfl
+
+/-- Every writer step flips stability, including the step that wraps. So the
+ first store in `SeqLock::write` is odd exactly when the counter it started
+ from was even, and the second store restores the starting parity. -/
+theorem bump_flips_stability_even_across_the_wrap (s : Std.U32) :
+ ∃ r, bump s = ok r ∧ is_stable r = ok (!decide (s.val % 2 = 0)) := by
+ obtain ⟨r, hr, hv⟩ := bump_is_the_models_write_step_modulo_the_word s
+ refine ⟨r, hr, ?_⟩
+ rw [is_stable_reads_the_parity_of_the_counter]
+ simp only [Nonos.Seqlock.writeBegin] at hv
+ have hs := s.hBounds
+ simp only [UScalarTy.U32_numBits_eq] at hs
+ congr 1
+ by_cases h : s.val % 2 = 0 <;> simp [h] <;> omega
+
+/-- A complete write, begin and end, returns a stable counter to a stable
+ counter that differs from where it started. -/
+theorem two_bumps_return_a_stable_counter_to_stability (s : Std.U32)
+ (h : is_stable s = ok true) :
+ ∃ m e, bump s = ok m ∧ is_stable m = ok false ∧ bump m = ok e ∧ is_stable e = ok true ∧
+ e ≠ s := by
+ rw [is_stable_reads_the_parity_of_the_counter] at h
+ have h0 : s.val % 2 = 0 := by simpa using h
+ obtain ⟨m, hm, hmst⟩ := bump_flips_stability_even_across_the_wrap s
+ obtain ⟨e, he, hest⟩ := bump_flips_stability_even_across_the_wrap m
+ obtain ⟨_, hm', hmv⟩ := bump_is_the_models_write_step_modulo_the_word s
+ obtain ⟨_, he', hev⟩ := bump_is_the_models_write_step_modulo_the_word m
+ rw [hm] at hm'; cases hm'
+ rw [he] at he'; cases he'
+ simp only [Nonos.Seqlock.writeBegin] at hmv hev
+ have hs := s.hBounds
+ simp only [UScalarTy.U32_numBits_eq] at hs
+ refine ⟨m, e, hm, by simpa [h0] using hmst, he, ?_, ?_⟩
+ · rw [hest]
+ have : m.val % 2 = 1 := by omega
+ simp [this]
+ · intro hes
+ have : e.val = s.val := by rw [hes]
+ omega
+
+/-- The reader's acceptance test is the model's `readAccepts`: the two samples
+ agree and the first is even. -/
+theorem read_valid_is_the_models_acceptance (before after : Std.U32) :
+ ∃ b, read_valid before after = ok b ∧
+ (b = true ↔ Nonos.Seqlock.readAccepts before.val after.val) := by
+ unfold read_valid pure.read_valid Nonos.Seqlock.readAccepts
+ split
+ · rename_i h
+ subst h
+ refine ⟨_, is_stable_reads_the_parity_of_the_counter before, ?_⟩
+ simp
+ · rename_i h
+ refine ⟨false, rfl, ?_⟩
+ simp only [Bool.false_eq_true, false_iff, not_and]
+ intro hv
+ exact absurd (UScalar.eq_of_val_eq hv) h
+
+/-- A reader whose first sample is taken before a writer step and whose second
+ is taken after it rejects the read, whatever the counter was. -/
+theorem read_valid_rejects_samples_straddling_a_bump (s : Std.U32) :
+ ∃ r, bump s = ok r ∧ read_valid s r = ok false := by
+ obtain ⟨r, hr, hv⟩ := bump_is_the_models_write_step_modulo_the_word s
+ refine ⟨r, hr, ?_⟩
+ simp only [Nonos.Seqlock.writeBegin] at hv
+ have hne : s ≠ r := by
+ intro h
+ have : s.val = r.val := by rw [h]
+ have hs := s.hBounds
+ simp only [UScalarTy.U32_numBits_eq] at hs
+ omega
+ unfold read_valid pure.read_valid
+ simp [hne]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SeqlockPure.the_is_stable_wrapper_is_its_method
#print axioms NonosExtraction.SeqlockPure.the_bump_wrapper_is_its_method
#print axioms NonosExtraction.SeqlockPure.the_read_valid_wrapper_is_its_method
+#print axioms NonosExtraction.SeqlockPure.is_stable_reads_the_parity_of_the_counter
+#print axioms NonosExtraction.SeqlockPure.is_stable_is_the_models_stability
+#print axioms NonosExtraction.SeqlockPure.bump_is_the_models_write_step_modulo_the_word
+#print axioms NonosExtraction.SeqlockPure.bump_wraps_the_largest_counter_to_zero
+#print axioms NonosExtraction.SeqlockPure.bump_flips_stability_even_across_the_wrap
+#print axioms NonosExtraction.SeqlockPure.two_bumps_return_a_stable_counter_to_stability
+#print axioms NonosExtraction.SeqlockPure.read_valid_is_the_models_acceptance
+#print axioms NonosExtraction.SeqlockPure.read_valid_rejects_samples_straddling_a_bump
end NonosExtraction.SeqlockPure
diff --git a/verification/extraction/lean/NonosExtraction/SignalErrorRefinement.lean b/verification/extraction/lean/NonosExtraction/SignalErrorRefinement.lean
index 494c33a58e..085d60dd13 100644
--- a/verification/extraction/lean/NonosExtraction/SignalErrorRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SignalErrorRefinement.lean
@@ -35,8 +35,61 @@ namespace NonosExtraction.SignalError
theorem the_signalerror_as_errno_wrapper_is_its_method (a : error.SignalError) :
signalerror_as_errno a = error.SignalError.as_errno a := rfl
+/-! ### Every signal error is a negative POSIX code, and only synonyms collide
+
+`as_errno` is how a signal failure leaves the kernel as a syscall return value.
+The theorems below show that every variant maps to a negative number in the
+range a caller reads as an error (`-4095` to `-1`), that each value is the
+negated POSIX constant the kernel defines in
+`src/syscall/types/errnos/posix.rs` and `src/syscall/types/errnos/net.rs`, and
+exactly which variants share a code: `InvalidSignal` with `InvalidHandler`
+(both `EINVAL`) and `QueueFull` with `Again` (both `EAGAIN`), and no others, so
+a caller that sees a code can tell every other pair of failures apart. They do
+not establish which kernel path raises which variant; no caller of `as_errno`
+is extracted.
+-/
+
+/-- Every signal error leaves as a negative code of at most 4095 in magnitude,
+ the range a syscall caller reads as failure. A variant mapped to its
+ positive POSIX number, or to zero, would read as success. -/
+theorem signalerror_as_errno_is_a_negative_errno (e : error.SignalError) :
+ ∃ v : Std.I32, signalerror_as_errno e = ok v ∧ -4095 ≤ v.val ∧ v.val < 0 := by
+ cases e <;> exact ⟨_, rfl, by decide, by decide⟩
+
+/-- Each variant is the negation of the kernel's POSIX constant for it:
+ `EINVAL = 22`, `EPERM = 1`, `ESRCH = 3`, `EAGAIN = 11`, `EINTR = 4`,
+ `ETIMEDOUT = 110`, `ENOMEM = 12` and `EFAULT = 14`. `PermissionDenied` is
+ `EPERM`, not `EACCES`, and `BadAddress` is `EFAULT`. -/
+theorem signalerror_as_errno_negates_the_posix_constants :
+ signalerror_as_errno .InvalidSignal = ok (-22)#i32 ∧
+ signalerror_as_errno .InvalidHandler = ok (-22)#i32 ∧
+ signalerror_as_errno .PermissionDenied = ok (-1)#i32 ∧
+ signalerror_as_errno .ProcessNotFound = ok (-3)#i32 ∧
+ signalerror_as_errno .QueueFull = ok (-11)#i32 ∧
+ signalerror_as_errno .Interrupted = ok (-4)#i32 ∧
+ signalerror_as_errno .Timeout = ok (-110)#i32 ∧
+ signalerror_as_errno .NoMemory = ok (-12)#i32 ∧
+ signalerror_as_errno .BadAddress = ok (-14)#i32 ∧
+ signalerror_as_errno .Again = ok (-11)#i32 := by
+ refine ⟨rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- Two signal errors share a code exactly when they are the same variant or
+ one of the two synonym pairs: `InvalidSignal` and `InvalidHandler`, or
+ `QueueFull` and `Again`. Any other collision would lose information a
+ caller needs to tell failures apart. -/
+theorem signalerror_as_errno_collides_only_on_synonyms (a b : error.SignalError) :
+ signalerror_as_errno a = signalerror_as_errno b ↔
+ (a = b ∨
+ ((a = .InvalidSignal ∨ a = .InvalidHandler) ∧
+ (b = .InvalidSignal ∨ b = .InvalidHandler)) ∨
+ ((a = .QueueFull ∨ a = .Again) ∧ (b = .QueueFull ∨ b = .Again))) := by
+ cases a <;> cases b <;> simp [signalerror_as_errno, error.SignalError.as_errno]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SignalError.the_signalerror_as_errno_wrapper_is_its_method
+#print axioms NonosExtraction.SignalError.signalerror_as_errno_is_a_negative_errno
+#print axioms NonosExtraction.SignalError.signalerror_as_errno_negates_the_posix_constants
+#print axioms NonosExtraction.SignalError.signalerror_as_errno_collides_only_on_synonyms
end NonosExtraction.SignalError
diff --git a/verification/extraction/lean/NonosExtraction/SurfaceRegistryRingMathRefinement.lean b/verification/extraction/lean/NonosExtraction/SurfaceRegistryRingMathRefinement.lean
index 4ad186d0a7..c8eea76a49 100644
--- a/verification/extraction/lean/NonosExtraction/SurfaceRegistryRingMathRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/SurfaceRegistryRingMathRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.SurfaceRegistryRingMath
+import Nonos.Ring
open Aeneas Aeneas.Std Result
open nonos_x_surface_registry_ring_math
@@ -38,9 +39,120 @@ theorem the_wrap_wrapper_is_its_method (a : Std.Usize) (b : Std.Usize) :
theorem the_is_full_wrapper_is_its_method (a : Std.Usize) (b : Std.Usize) (c : Std.Usize) :
is_full a b c = ring_math.is_full a b c := rfl
+/-! ### Advancing a position, and when the ring is full
+
+ `wrap` is `(pos + 1) % cap` and returns exactly that for every position below
+ the largest word and every non-zero capacity. It fails in exactly two ways,
+ both panics in the kernel: a zero capacity is a division by zero, and the
+ largest word overflows on the increment rather than wrapping to zero. For a
+ position already inside the buffer, which is how the post and drain sites
+ call it with `INPUT_RING_CAP`, the result is again an index inside the buffer:
+ one more, or zero after the last slot.
+
+ `is_full` is then checked against `Nonos.Ring`. With head and tail inside the
+ buffer, the number of queued events is `head - tail` read modulo the
+ capacity, and `is_full` answers true exactly when that number is one less
+ than the capacity, which is the point at which the model ring of capacity
+ `cap - 1` refuses a push. So the ring stores at most `cap - 1` events, as the
+ kernel file says. These theorems say nothing about the lock, the buffer
+ writes or the atomic counters in `post_input` and `drain`, which are not
+ extracted, and `post_input` repeats the comparison inline rather than calling
+ `is_full`, so its agreement with `is_full` rests on reading the source.
+-/
+
+theorem wrap_advances_a_position_by_one_modulo_the_capacity (pos cap : Std.Usize)
+ (hp : pos.val < Usize.max) (hc : 0 < cap.val) :
+ ∃ w : Std.Usize, wrap pos cap = ok w ∧ w.val = (pos.val + 1) % cap.val := by
+ unfold wrap ring_math.wrap
+ have ⟨i, hi, hiv⟩ := WP.spec_imp_exists
+ (Usize.add_spec (x := pos) (y := 1#usize) (by scalar_tac))
+ simp only [hi, bind_tc_ok]
+ have ⟨j, hj, hjv⟩ := WP.spec_imp_exists (Usize.rem_spec i (y := cap) (by omega))
+ refine ⟨j, hj, ?_⟩
+ rw [hjv, hiv]; rfl
+
+/-- The increment is checked: at the largest word `wrap` overflows instead of
+ wrapping to zero, whatever the capacity. -/
+theorem wrap_at_the_largest_word_overflows_rather_than_wrapping (pos cap : Std.Usize)
+ (hp : pos.val = Usize.max) :
+ wrap pos cap = fail .integerOverflow := by
+ unfold wrap ring_math.wrap
+ have hb : ¬ (pos.val + 1 < 2 ^ System.Platform.numBits) := by
+ have : Usize.max = 2 ^ System.Platform.numBits - 1 := by simp [Usize.max, Usize.numBits]
+ have := Nat.two_pow_pos System.Platform.numBits
+ omega
+ show (UScalar.tryMk _ (pos.val + (1#usize : Std.Usize).val) >>= _) = _
+ simp [UScalar.tryMk, UScalar.tryMkOpt, UScalar.check_bounds, Result.ofOption, hb]
+
+/-- A ring of capacity zero is refused by a division by zero, not given index zero. -/
+theorem wrap_with_a_zero_capacity_divides_by_zero (pos : Std.Usize)
+ (hp : pos.val < Usize.max) :
+ wrap pos 0#usize = fail .divisionByZero := by
+ unfold wrap ring_math.wrap
+ have ⟨i, hi, hiv⟩ := WP.spec_imp_exists
+ (Usize.add_spec (x := pos) (y := 1#usize) (by scalar_tac))
+ simp only [hi, bind_tc_ok]
+ simp [HMod.hMod, UScalar.rem]
+
+/-- The contract the post and drain sites rely on when they index `ring.buf`
+ with the result: from a slot inside the buffer, `wrap` lands on a slot inside
+ the buffer, the next one, or slot zero after the last. -/
+theorem wrap_keeps_an_index_in_the_buffer_and_returns_to_zero_after_the_last_slot
+ (pos cap : Std.Usize) (hp : pos.val < cap.val) :
+ ∃ w : Std.Usize, wrap pos cap = ok w ∧ w.val < cap.val ∧
+ w.val = if pos.val + 1 = cap.val then 0 else pos.val + 1 := by
+ have hmax : pos.val < Usize.max := by scalar_tac
+ obtain ⟨w, hw, hwv⟩ := wrap_advances_a_position_by_one_modulo_the_capacity pos cap hmax (by omega)
+ refine ⟨w, hw, ?_, ?_⟩
+ · rw [hwv]; exact Nat.mod_lt _ (by omega)
+ · rw [hwv]
+ split
+ · next h => rw [h, Nat.mod_self]
+ · exact Nat.mod_eq_of_lt (by omega)
+
+/-- With head and tail inside the buffer, `is_full` is true exactly when the
+ queued count (`head - tail` modulo the capacity) is one below the capacity,
+ and that is exactly when the `Nonos.Ring` model of capacity `cap - 1`, holding
+ that count, refuses a push. -/
+theorem is_full_exactly_when_the_ring_holds_one_less_than_its_capacity
+ (head tail cap : Std.Usize) (hh : head.val < cap.val) (ht : tail.val < cap.val) :
+ ∃ b, is_full head tail cap = ok b ∧
+ (b = true ↔ (if tail.val ≤ head.val then head.val - tail.val
+ else head.val + cap.val - tail.val) = cap.val - 1) ∧
+ (b = true ↔
+ let r : Nonos.Ring.Ring := ⟨if tail.val ≤ head.val then head.val - tail.val
+ else head.val + cap.val - tail.val, cap.val - 1⟩
+ Nonos.Ring.push r = r) := by
+ obtain ⟨w, hw, hlt, hwv⟩ :=
+ wrap_keeps_an_index_in_the_buffer_and_returns_to_zero_after_the_last_slot head cap hh
+ unfold is_full ring_math.is_full
+ rw [show ring_math.wrap head cap = wrap head cap from rfl, hw, bind_tc_ok]
+ have hle : (if tail.val ≤ head.val then head.val - tail.val
+ else head.val + cap.val - tail.val) ≤ cap.val - 1 := by split <;> omega
+ have key : (w = tail) ↔ (if tail.val ≤ head.val then head.val - tail.val
+ else head.val + cap.val - tail.val) = cap.val - 1 := by
+ constructor
+ · intro h; subst h; split <;> split at hwv <;> omega
+ · intro h; apply UScalar.eq_of_val_eq; split at h <;> split at hwv <;> omega
+ refine ⟨_, rfl, by simpa using key, ?_⟩
+ generalize (if tail.val ≤ head.val then head.val - tail.val
+ else head.val + cap.val - tail.val) = occ at key hle ⊢
+ simp only [decide_eq_true_eq, Nonos.Ring.push, key]
+ constructor
+ · intro h; rw [if_neg (by omega)]
+ · intro h
+ split at h
+ · simp only [Nonos.Ring.Ring.mk.injEq] at h; omega
+ · omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.SurfaceRegistryRingMath.the_wrap_wrapper_is_its_method
#print axioms NonosExtraction.SurfaceRegistryRingMath.the_is_full_wrapper_is_its_method
+#print axioms NonosExtraction.SurfaceRegistryRingMath.wrap_advances_a_position_by_one_modulo_the_capacity
+#print axioms NonosExtraction.SurfaceRegistryRingMath.wrap_at_the_largest_word_overflows_rather_than_wrapping
+#print axioms NonosExtraction.SurfaceRegistryRingMath.wrap_with_a_zero_capacity_divides_by_zero
+#print axioms NonosExtraction.SurfaceRegistryRingMath.wrap_keeps_an_index_in_the_buffer_and_returns_to_zero_after_the_last_slot
+#print axioms NonosExtraction.SurfaceRegistryRingMath.is_full_exactly_when_the_ring_holds_one_less_than_its_capacity
end NonosExtraction.SurfaceRegistryRingMath
diff --git a/verification/extraction/lean/NonosExtraction/TablesMemoryDesc.lean b/verification/extraction/lean/NonosExtraction/TablesMemoryDesc.lean
index 3ce0b8b26f..021a05cd27 100644
--- a/verification/extraction/lean/NonosExtraction/TablesMemoryDesc.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesMemoryDesc.lean
@@ -32,22 +32,25 @@ def memory_desc.MemoryDescriptor.EFI_MEMORY_RUNTIME : Std.U64 :=
9223372036854775808#u64
/-- [nonos_x_tables_memory_desc::memory_desc::{nonos_x_tables_memory_desc::memory_desc::MemoryDescriptor}::size_bytes]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 43:4-45:5
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 45:4-50:5
Visibility: public -/
def memory_desc.MemoryDescriptor.size_bytes
(self : memory_desc.MemoryDescriptor) : Result Std.U64 := do
- self.number_of_pages * 4096#u64
+ let i ← core.num.U64.MAX / 4096#u64
+ if self.number_of_pages > i
+ then ok core.num.U64.MAX
+ else self.number_of_pages * 4096#u64
/-- [nonos_x_tables_memory_desc::memory_desc::{nonos_x_tables_memory_desc::memory_desc::MemoryDescriptor}::end_address]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 46:4-48:5
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 51:4-53:5
Visibility: public -/
def memory_desc.MemoryDescriptor.end_address
(self : memory_desc.MemoryDescriptor) : Result Std.U64 := do
let i ← memory_desc.MemoryDescriptor.size_bytes self
- self.physical_start + i
+ ok (core.num.U64.saturating_add self.physical_start i)
/-- [nonos_x_tables_memory_desc::memory_desc::{nonos_x_tables_memory_desc::memory_desc::MemoryDescriptor}::is_runtime]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 49:4-51:5
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 54:4-56:5
Visibility: public -/
def memory_desc.MemoryDescriptor.is_runtime
(self : memory_desc.MemoryDescriptor) : Result Bool := do
@@ -57,7 +60,7 @@ def memory_desc.MemoryDescriptor.is_runtime
ok (i != 0#u64)
/-- [nonos_x_tables_memory_desc::memory_desc::{nonos_x_tables_memory_desc::memory_desc::MemoryDescriptor}::is_usable]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 52:4-54:5
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/memory_desc.rs', lines 57:4-59:5
Visibility: public -/
def memory_desc.MemoryDescriptor.is_usable
(self : memory_desc.MemoryDescriptor) : Result Bool := do
diff --git a/verification/extraction/lean/NonosExtraction/TablesMemoryDescRefinement.lean b/verification/extraction/lean/NonosExtraction/TablesMemoryDescRefinement.lean
index 6c2156ba47..c1aa601a66 100644
--- a/verification/extraction/lean/NonosExtraction/TablesMemoryDescRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesMemoryDescRefinement.lean
@@ -49,10 +49,11 @@ theorem the_memorydescriptor_is_usable_wrapper_is_its_method (a : memory_desc.Me
A UEFI memory descriptor counts its extent in 4 KiB pages, whatever the page size
of the machine. The theorems below fix the arithmetic exactly: `size_bytes` is the
-page count times 4096 and aborts from 2^52 pages upward, and `end_address` is the
-exclusive end `physical_start + size`, which aborts exactly when that sum leaves
-the 64 bit range. The kernel builds with overflow checks in every profile, so an
-abort here is a controlled halt and never a wrapped length. `is_runtime` reads bit
+page count times 4096, saturated at `2^64 - 1` from 2^52 pages upward, and
+`end_address` is the exclusive end `physical_start + size`, saturated the same
+way. Neither can fail. Both used to overflow on such a descriptor, and with
+overflow checks on in every profile a firmware map with a descriptor reaching
+the top of the address space, or claiming 2^52 pages or more, halted the kernel. `is_runtime` reads bit
63 of the attribute word and nothing else, and `is_usable` accepts exactly the
types 1, 2, 3, 4 and 7, which are the discriminants `LoaderCode`, `LoaderData`,
`BootServicesCode`, `BootServicesData` and `ConventionalMemory` of the kernel's
@@ -61,81 +62,52 @@ the ACPI types 9 and 10, are refused).
What this cannot establish: the descriptors come from firmware, so nothing here
says a firmware map is well formed, and no kernel caller of these methods is
-extracted (none exists in the tree at present) to show that an abort is
-unreachable in practice.
+extracted (none exists in the tree at present).
-/
-private theorem u64_mul_cases (x y : Std.U64) :
- (∃ z, x * y = ok z ∧ z.val = x.val * y.val ∧ x.val * y.val < 2 ^ 64) ∨
- (x * y = fail .integerOverflow ∧ 2 ^ 64 ≤ x.val * y.val) := by
+private theorem u64_mul_ok (x y : Std.U64) (h : x.val * y.val < 2 ^ 64) :
+ ∃ z, x * y = ok z ∧ z.val = x.val * y.val := by
have hm := UScalar.mul_equiv x y
rw [show (x * y : Result Std.U64) = UScalar.mul x y from rfl]
- cases h : UScalar.mul x y with
- | ok z =>
- rw [h] at hm
- obtain ⟨hle, hv, -⟩ := hm
- simp only [UScalar.max_UScalarTy_U64_eq, U64.max_eq] at hle
- exact Or.inl ⟨z, rfl, hv, by omega⟩
+ cases hr : UScalar.mul x y with
+ | ok z => rw [hr] at hm; exact ⟨z, rfl, hm.2.1⟩
| fail e =>
- rw [h] at hm
+ rw [hr] at hm
simp only [UScalar.max_UScalarTy_U64_eq, U64.max_eq] at hm
- have he : e = .integerOverflow := by
- simp only [UScalar.mul, UScalar.tryMk, Result.ofOption] at h
- split at h <;> simp_all
- subst he
- exact Or.inr ⟨rfl, by omega⟩
- | div => rw [h] at hm; exact hm.elim
-
-private theorem u64_add_cases (x y : Std.U64) :
- (∃ z, x + y = ok z ∧ z.val = x.val + y.val ∧ x.val + y.val < 2 ^ 64) ∨
- (x + y = fail .integerOverflow ∧ 2 ^ 64 ≤ x.val + y.val) := by
- have hm := UScalar.add_equiv x y
- cases h : x + y with
- | ok z =>
- rw [h] at hm
- obtain ⟨hle, hv, -⟩ := hm
- exact Or.inl ⟨z, rfl, hv, by simpa using hle⟩
- | fail e =>
- rw [h] at hm
- have he : e = .integerOverflow := by
- rw [show (x + y : Result Std.U64) = UScalar.add x y from rfl] at h
- simp only [UScalar.add, UScalar.tryMk, Result.ofOption] at h
- split at h <;> simp_all
- subst he
- simp only [UScalar.inBounds] at hm
- exact Or.inr ⟨rfl, by simp at hm; omega⟩
- | div => rw [h] at hm; exact hm.elim
-
-/-- `size_bytes` answers, and answers the page count times 4096, exactly when the
- descriptor has fewer than 2^52 pages. A page size other than 4096 would move
- both the value and the bound. -/
-theorem memorydescriptor_size_bytes_is_pages_of_4096_below_two_pow_52
- (d : memory_desc.MemoryDescriptor) :
- (∃ s, memorydescriptor_size_bytes d = ok s ∧ s.val = d.number_of_pages.val * 4096) ↔
- d.number_of_pages.val < 2 ^ 52 := by
- unfold memorydescriptor_size_bytes memory_desc.MemoryDescriptor.size_bytes
- have h4 : (4096#u64 : Std.U64).val = 4096 := rfl
- rcases u64_mul_cases d.number_of_pages 4096#u64 with ⟨z, hz, hv, hlt⟩ | ⟨hz, hge⟩
- · rw [hz]; rw [h4] at hv hlt
- exact ⟨fun _ => (by omega), fun _ => ⟨z, rfl, hv⟩⟩
- · rw [hz]; rw [h4] at hge
- exact ⟨fun h => (by obtain ⟨_, h, _⟩ := h; cases h), fun _ => (by omega)⟩
-
-/-- The other side of the same boundary: from 2^52 pages upward the product
- overflows and the call aborts rather than returning a wrapped length. -/
-theorem memorydescriptor_size_bytes_aborts_exactly_from_two_pow_52_pages
+ omega
+ | div => rw [hr] at hm; exact hm.elim
+
+private theorem u64_saturating_add_val (a b : Std.U64) :
+ (core.num.U64.saturating_add a b).val = min (2 ^ 64 - 1) (a.val + b.val) := by
+ simp only [core.num.U64.saturating_add, UScalar.saturating_add, UScalar.val, UScalar.max]
+ rw [BitVec.toNat_ofNat]
+ show min (2 ^ 64 - 1) _ % 2 ^ 64 = _
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- `size_bytes` never fails. It is the page count times 4096 below 2^52 pages
+ and `2^64 - 1` from there up. A page size other than 4096 would move both the
+ value and the bound. -/
+theorem memorydescriptor_size_bytes_is_pages_of_4096_saturated
(d : memory_desc.MemoryDescriptor) :
- memorydescriptor_size_bytes d = fail .integerOverflow ↔ 2 ^ 52 ≤ d.number_of_pages.val := by
+ ∃ s, memorydescriptor_size_bytes d = ok s ∧
+ s.val = if d.number_of_pages.val < 2 ^ 52 then d.number_of_pages.val * 4096
+ else 2 ^ 64 - 1 := by
unfold memorydescriptor_size_bytes memory_desc.MemoryDescriptor.size_bytes
- have h4 : (4096#u64 : Std.U64).val = 4096 := rfl
- rcases u64_mul_cases d.number_of_pages 4096#u64 with ⟨z, hz, hv, hlt⟩ | ⟨hz, hge⟩
- · rw [hz]; rw [h4] at hlt
- exact ⟨fun h => (by cases h), fun _ => (by omega)⟩
- · rw [hz]; rw [h4] at hge
- exact ⟨fun _ => (by omega), fun _ => rfl⟩
-
-/-- The boundary itself, on concrete descriptors: 2^52 - 1 pages is the largest
- count that fits and covers every page but the last of the 64 bit range. -/
+ have hq : (core.num.U64.MAX / 4096#u64 : Result Std.U64) = ok 0xFFFFFFFFFFFFF#u64 := by
+ unfold core.num.U64.MAX; rfl
+ simp only [hq, bind_tc_ok]
+ by_cases h : d.number_of_pages.val < 2 ^ 52
+ · have : ¬ d.number_of_pages > 0xFFFFFFFFFFFFF#u64 := by scalar_tac
+ simp only [this, ↓reduceIte, h]
+ obtain ⟨z, hz, hv⟩ := u64_mul_ok d.number_of_pages 4096#u64 (by
+ show d.number_of_pages.val * 4096 < 2 ^ 64; omega)
+ exact ⟨z, hz, hv⟩
+ · have : d.number_of_pages > 0xFFFFFFFFFFFFF#u64 := by scalar_tac
+ simp only [this, ↓reduceIte, h]
+ exact ⟨_, rfl, by unfold core.num.U64.MAX; rfl⟩
+
+/-- The boundary on concrete descriptors: 2^52 - 1 pages is the largest count
+ whose size is exact, and one page more saturates instead of halting. -/
theorem memorydescriptor_size_bytes_at_the_page_count_limit :
memorydescriptor_size_bytes
{ memory_type := 7#u32, physical_start := 0#u64, virtual_start := 0#u64,
@@ -144,37 +116,44 @@ theorem memorydescriptor_size_bytes_at_the_page_count_limit :
memorydescriptor_size_bytes
{ memory_type := 7#u32, physical_start := 0#u64, virtual_start := 0#u64,
number_of_pages := 0x10000000000000#u64, «attribute» := 0#u64 } =
- fail .integerOverflow := by
- refine ⟨rfl, rfl⟩
-
-/-- `end_address` is the exclusive end of the physical range, start plus the size
- in bytes, and it answers exactly when that sum is below 2^64. The virtual start
- plays no part. -/
-theorem memorydescriptor_end_address_is_physical_start_plus_size
+ ok 0xFFFFFFFFFFFFFFFF#u64 := by
+ constructor
+ · obtain ⟨s, hs, hv⟩ := memorydescriptor_size_bytes_is_pages_of_4096_saturated
+ { memory_type := 7#u32, physical_start := 0#u64, virtual_start := 0#u64,
+ number_of_pages := 0xFFFFFFFFFFFFF#u64, «attribute» := 0#u64 }
+ rw [hs]; congr 1; apply UScalar.eq_of_val_eq; rw [hv]; rfl
+ · obtain ⟨s, hs, hv⟩ := memorydescriptor_size_bytes_is_pages_of_4096_saturated
+ { memory_type := 7#u32, physical_start := 0#u64, virtual_start := 0#u64,
+ number_of_pages := 0x10000000000000#u64, «attribute» := 0#u64 }
+ rw [hs]; congr 1; apply UScalar.eq_of_val_eq; rw [hv]; rfl
+
+/-- `end_address` never fails. It is the start plus the size, both as above,
+ saturated at `2^64 - 1`; the virtual start plays no part. Below 2^52 pages
+ and below the top of the address space it is the exact exclusive end. -/
+theorem memorydescriptor_end_address_is_physical_start_plus_size_saturated
(d : memory_desc.MemoryDescriptor) :
- (∃ e, memorydescriptor_end_address d = ok e ∧
- e.val = d.physical_start.val + d.number_of_pages.val * 4096) ↔
- d.physical_start.val + d.number_of_pages.val * 4096 < 2 ^ 64 := by
+ ∃ e, memorydescriptor_end_address d = ok e ∧
+ e.val = min (2 ^ 64 - 1) (d.physical_start.val +
+ if d.number_of_pages.val < 2 ^ 52 then d.number_of_pages.val * 4096
+ else 2 ^ 64 - 1) := by
unfold memorydescriptor_end_address memory_desc.MemoryDescriptor.end_address
- memory_desc.MemoryDescriptor.size_bytes
- have h4 : (4096#u64 : Std.U64).val = 4096 := rfl
- rcases u64_mul_cases d.number_of_pages 4096#u64 with ⟨z, hz, hv, hlt⟩ | ⟨hz, hge⟩
- · rw [hz, bind_tc_ok]; rw [h4] at hv hlt
- rcases u64_add_cases d.physical_start z with ⟨w, hw, hwv, hwlt⟩ | ⟨hw, hwge⟩
- · rw [hw]; exact ⟨fun _ => (by omega), fun _ => ⟨w, rfl, by omega⟩⟩
- · rw [hw]; exact ⟨fun h => (by obtain ⟨_, h, _⟩ := h; cases h), fun _ => (by omega)⟩
- · rw [hz, bind_tc_fail]; rw [h4] at hge
- exact ⟨fun h => (by obtain ⟨_, h, _⟩ := h; cases h), fun _ => (by omega)⟩
-
-/-- This records a defect. Because the end is exclusive, a descriptor that covers
- the last page of the physical address space has an end of 2^64, which a `u64`
- cannot hold, so `end_address` halts on a descriptor that is legal in form. -/
-theorem memorydescriptor_end_address_halts_on_the_top_page :
+ obtain ⟨s, hs, hv⟩ := memorydescriptor_size_bytes_is_pages_of_4096_saturated d
+ unfold memorydescriptor_size_bytes at hs
+ rw [hs, bind_tc_ok]
+ exact ⟨_, rfl, by rw [u64_saturating_add_val, hv]⟩
+
+/-- A descriptor that covers the last page of the physical address space, whose
+ exclusive end is 2^64, gets the saturated end `2^64 - 1`. It used to halt
+ the kernel. -/
+theorem memorydescriptor_end_address_saturates_on_the_top_page :
memorydescriptor_end_address
{ memory_type := 7#u32, physical_start := 0xFFFFFFFFFFFFF000#u64,
virtual_start := 0#u64, number_of_pages := 1#u64, «attribute» := 0#u64 } =
- fail .integerOverflow := by
- rfl
+ ok 0xFFFFFFFFFFFFFFFF#u64 := by
+ obtain ⟨e, he, hv⟩ := memorydescriptor_end_address_is_physical_start_plus_size_saturated
+ { memory_type := 7#u32, physical_start := 0xFFFFFFFFFFFFF000#u64,
+ virtual_start := 0#u64, number_of_pages := 1#u64, «attribute» := 0#u64 }
+ rw [he]; congr 1; apply UScalar.eq_of_val_eq; rw [hv]; rfl
/-- `is_runtime` is bit 63 of the attribute word, `EFI_MEMORY_RUNTIME` in the UEFI
specification, and no other bit. -/
@@ -228,11 +207,10 @@ theorem memorydescriptor_is_usable_accepts_exactly_types_1_2_3_4_7
#print axioms NonosExtraction.TablesMemoryDesc.the_memorydescriptor_end_address_wrapper_is_its_method
#print axioms NonosExtraction.TablesMemoryDesc.the_memorydescriptor_is_runtime_wrapper_is_its_method
#print axioms NonosExtraction.TablesMemoryDesc.the_memorydescriptor_is_usable_wrapper_is_its_method
-#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_size_bytes_is_pages_of_4096_below_two_pow_52
-#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_size_bytes_aborts_exactly_from_two_pow_52_pages
+#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_size_bytes_is_pages_of_4096_saturated
#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_size_bytes_at_the_page_count_limit
-#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_end_address_is_physical_start_plus_size
-#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_end_address_halts_on_the_top_page
+#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_end_address_is_physical_start_plus_size_saturated
+#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_end_address_saturates_on_the_top_page
#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_is_runtime_reads_bit_63
#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_is_runtime_is_the_upper_half_of_attributes
#print axioms NonosExtraction.TablesMemoryDesc.memorydescriptor_is_usable_accepts_exactly_types_1_2_3_4_7
diff --git a/verification/extraction/lean/NonosExtraction/TablesMemoryTypeRefinement.lean b/verification/extraction/lean/NonosExtraction/TablesMemoryTypeRefinement.lean
index 045cbd3dd7..d030244538 100644
--- a/verification/extraction/lean/NonosExtraction/TablesMemoryTypeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesMemoryTypeRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TablesMemoryType
+import NonosExtraction.TablesMemoryDesc
open Aeneas Aeneas.Std Result
open nonos_x_tables_memory_type
@@ -38,9 +39,85 @@ theorem the_memorytype_is_usable_wrapper_is_its_method (a : memory_type.MemoryTy
theorem the_memorytype_is_reserved_wrapper_is_its_method (a : memory_type.MemoryType) :
memorytype_is_reserved a = memory_type.MemoryType.is_reserved a := rfl
+/-! ### Usable and reserved memory, and the raw-descriptor copy
+
+ The kernel classifies UEFI memory twice: once on the `MemoryType` enum here,
+ and once on the raw `u32` type field of a memory descriptor in
+ tables/memory_desc.rs. `memorytype_is_usable` agrees with
+ `memorydescriptor_is_usable` on every one of the seventeen types, read
+ through the enum's `u32` discriminant, so neither copy can hand runtime
+ services, ACPI, MMIO, persistent or unaccepted memory to an allocator that
+ the other would refuse. The usable types are exactly the loader, boot
+ services and conventional types. `memorytype_is_reserved` never overlaps
+ `memorytype_is_usable`, but the two do not cover the enum: nine types are
+ neither, among them `MemoryMappedIOPortSpace` (while `MemoryMappedIO` is
+ reserved) and `ACPIMemoryNVS`, which firmware requires the operating system
+ to preserve. Reading "not reserved" as "free to use" is therefore wrong, and
+ `memorytype_is_usable` is the only safe test. These theorems cannot establish
+ anything about the memory map walk that consumes them, which is not
+ extracted, nor about descriptors whose raw type is 17 or above, which have no
+ enum value.
+-/
+
+open nonos_x_tables_memory_desc in
+/-- The enum classifier and the raw descriptor classifier give the same answer
+ for every memory type, whatever the other descriptor fields hold. -/
+theorem memorytype_is_usable_agrees_with_the_descriptor_copy
+ (t : memory_type.MemoryType) (d : memory_desc.MemoryDescriptor)
+ (h : d.memory_type = memory_type.MemoryType.read_discriminant t) :
+ memorytype_is_usable t = memorydescriptor_is_usable d := by
+ obtain ⟨ty, _, _, _, _⟩ := d
+ cases h
+ cases t <;> rfl
+
+/-- Exactly five types are usable: loader code and data, boot services code and
+ data, and conventional memory. -/
+theorem memorytype_is_usable_exactly_for_loader_boot_services_and_conventional
+ (t : memory_type.MemoryType) :
+ (memorytype_is_usable t = ok true ↔ t = .LoaderCode ∨ t = .LoaderData ∨
+ t = .BootServicesCode ∨ t = .BootServicesData ∨ t = .ConventionalMemory) ∧
+ (memorytype_is_usable t = ok false ↔ ¬ (t = .LoaderCode ∨ t = .LoaderData ∨
+ t = .BootServicesCode ∨ t = .BootServicesData ∨ t = .ConventionalMemory)) := by
+ cases t <;> simp [memorytype_is_usable, memory_type.MemoryType.is_usable]
+
+/-- No type is both usable and reserved. -/
+theorem memorytype_is_reserved_never_overlaps_memorytype_is_usable
+ (t : memory_type.MemoryType) :
+ ¬ (memorytype_is_usable t = ok true ∧ memorytype_is_reserved t = ok true) := by
+ cases t <;> simp [memorytype_is_usable, memory_type.MemoryType.is_usable,
+ memorytype_is_reserved, memory_type.MemoryType.is_reserved]
+
+/-- Exactly three types are reserved: the reserved type, unusable memory and
+ memory-mapped I/O. -/
+theorem memorytype_is_reserved_exactly_for_reserved_unusable_and_mmio
+ (t : memory_type.MemoryType) :
+ (memorytype_is_reserved t = ok true ↔ t = .ReservedMemoryType ∨
+ t = .UnusableMemory ∨ t = .MemoryMappedIO) ∧
+ (memorytype_is_reserved t = ok false ↔ ¬ (t = .ReservedMemoryType ∨
+ t = .UnusableMemory ∨ t = .MemoryMappedIO)) := by
+ cases t <;> simp [memorytype_is_reserved, memory_type.MemoryType.is_reserved]
+
+/-- Nine types are neither usable nor reserved, so "not reserved" does not mean
+ usable. This records a latent hazard rather than a present defect: nothing
+ in the kernel calls `is_reserved` today, but a caller that allocated from
+ every type it does not report reserved would reuse port-space MMIO, ACPI
+ NVS, runtime services, PAL code, persistent and unaccepted memory. -/
+theorem memorytype_is_reserved_is_false_on_nine_types_that_are_not_usable :
+ ∀ t ∈ [memory_type.MemoryType.RuntimeServicesCode, .RuntimeServicesData,
+ .ACPIReclaimMemory, .ACPIMemoryNVS, .MemoryMappedIOPortSpace, .PalCode,
+ .PersistentMemory, .UnacceptedMemoryType, .MaxMemoryType],
+ memorytype_is_reserved t = ok false ∧ memorytype_is_usable t = ok false := by
+ simp [memorytype_is_usable, memory_type.MemoryType.is_usable,
+ memorytype_is_reserved, memory_type.MemoryType.is_reserved]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TablesMemoryType.the_memorytype_is_usable_wrapper_is_its_method
#print axioms NonosExtraction.TablesMemoryType.the_memorytype_is_reserved_wrapper_is_its_method
+#print axioms NonosExtraction.TablesMemoryType.memorytype_is_usable_agrees_with_the_descriptor_copy
+#print axioms NonosExtraction.TablesMemoryType.memorytype_is_usable_exactly_for_loader_boot_services_and_conventional
+#print axioms NonosExtraction.TablesMemoryType.memorytype_is_reserved_never_overlaps_memorytype_is_usable
+#print axioms NonosExtraction.TablesMemoryType.memorytype_is_reserved_exactly_for_reserved_unusable_and_mmio
+#print axioms NonosExtraction.TablesMemoryType.memorytype_is_reserved_is_false_on_nine_types_that_are_not_usable
end NonosExtraction.TablesMemoryType
diff --git a/verification/extraction/lean/NonosExtraction/TablesSratProcessorRefinement.lean b/verification/extraction/lean/NonosExtraction/TablesSratProcessorRefinement.lean
index a18432d8e3..9a55d72f42 100644
--- a/verification/extraction/lean/NonosExtraction/TablesSratProcessorRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesSratProcessorRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TablesSratProcessor
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_tables_srat_processor
@@ -41,10 +42,153 @@ theorem the_sratprocessoraffinity_is_enabled_wrapper_is_its_method (a : srat_pro
theorem the_sratx2apicaffinity_is_enabled_wrapper_is_its_method (a : srat_processor.SratX2ApicAffinity) :
sratx2apicaffinity_is_enabled a = srat_processor.SratX2ApicAffinity.is_enabled a := rfl
+/-! ### The enabled flag and the proximity domain
+
+ Both SRAT processor entries test `flags & (1 << 0) != 0`, which is bit 0 of
+ the flags word and no other bit: an entry with only reserved flag bits set is
+ disabled, as ACPI requires, and the legacy APIC entry and the x2APIC entry
+ agree on every flags word. `parse_processor_affinity` skips an entry this
+ test calls disabled.
+
+ `proximity_domain` reassembles the 32-bit domain from the low byte and the
+ three high bytes in little-endian order, each byte in its own eight bits, so
+ it never fails and different byte sequences give different domains. That is
+ the value `parse_processor_affinity` stores into the processor record.
+
+ What these cannot establish: that the entry was read from a well-formed
+ table (the volatile read and the length check in the parser are not
+ extracted), or that the domain names a memory affinity entry that exists.
+-/
+
+private theorem shl_u32 (x : Std.U32) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 32) :
+ ∃ z : Std.U32, x <<< k = ok z ∧ z.val = x.val * 2 ^ k.toNat % 2 ^ 32 := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftLeft_IScalar_spec x k (UScalar.size .U32) h0
+ (by simpa using h1) rfl)
+ exact ⟨z, hz, by rw [hv, Nat.shiftLeft_eq]; simp [U32.size, U32.numBits]⟩
+
+theorem sratprocessoraffinity_is_enabled_reads_bit_zero
+ (s : srat_processor.SratProcessorAffinity) :
+ sratprocessoraffinity_is_enabled s = ok (s.flags.val.testBit 0) := by
+ unfold sratprocessoraffinity_is_enabled srat_processor.SratProcessorAffinity.is_enabled
+ have hs : srat_processor.SratProcessorAffinity.ENABLED = ok 1#u32 := by
+ unfold srat_processor.SratProcessorAffinity.ENABLED; rfl
+ simp only [hs, lift, bind_tc_ok]
+ rw [Bits.reads_bit s.flags 1#u32 0#u32 0 rfl rfl]
+
+theorem sratx2apicaffinity_is_enabled_reads_bit_zero
+ (x : srat_processor.SratX2ApicAffinity) :
+ sratx2apicaffinity_is_enabled x = ok (x.flags.val.testBit 0) := by
+ unfold sratx2apicaffinity_is_enabled srat_processor.SratX2ApicAffinity.is_enabled
+ have hs : srat_processor.SratX2ApicAffinity.ENABLED = ok 1#u32 := by
+ unfold srat_processor.SratX2ApicAffinity.ENABLED; rfl
+ simp only [hs, lift, bind_tc_ok]
+ rw [Bits.reads_bit x.flags 1#u32 0#u32 0 rfl rfl]
+
+/-- The legacy and x2APIC entries decide enablement identically from the same
+ flags word. -/
+theorem sratprocessoraffinity_is_enabled_agrees_with_sratx2apicaffinity_is_enabled
+ (s : srat_processor.SratProcessorAffinity) (x : srat_processor.SratX2ApicAffinity)
+ (h : s.flags = x.flags) :
+ sratprocessoraffinity_is_enabled s = sratx2apicaffinity_is_enabled x := by
+ rw [sratprocessoraffinity_is_enabled_reads_bit_zero,
+ sratx2apicaffinity_is_enabled_reads_bit_zero, h]
+
+/-- A flags word with every bit set except bit 0 is a disabled entry, for both
+ entry kinds: no reserved bit can enable a processor. -/
+theorem reserved_flag_bits_do_not_enable_sratprocessoraffinity_is_enabled
+ (s : srat_processor.SratProcessorAffinity) (x : srat_processor.SratX2ApicAffinity)
+ (hs : s.flags = 4294967294#u32) (hx : x.flags = 4294967294#u32) :
+ sratprocessoraffinity_is_enabled s = ok false ∧
+ sratx2apicaffinity_is_enabled x = ok false := by
+ rw [sratprocessoraffinity_is_enabled_reads_bit_zero,
+ sratx2apicaffinity_is_enabled_reads_bit_zero, hs, hx]
+ exact ⟨rfl, rfl⟩
+
+/-- The domain is the low byte plus the three high bytes at weights 2^8, 2^16
+ and 2^24: the ACPI little-endian layout, with nothing lost or overlapped. -/
+theorem sratprocessoraffinity_proximity_domain_is_the_little_endian_word
+ (s : srat_processor.SratProcessorAffinity) (h0 h1 h2 : Std.U8)
+ (hh : s.proximity_domain_high.val = [h0, h1, h2]) :
+ ∃ r, sratprocessoraffinity_proximity_domain s = ok r ∧
+ r.val = s.proximity_domain_low.val + 2 ^ 8 * h0.val + 2 ^ 16 * h1.val +
+ 2 ^ 24 * h2.val := by
+ unfold sratprocessoraffinity_proximity_domain
+ srat_processor.SratProcessorAffinity.proximity_domain
+ have hl := s.proximity_domain_low.hBounds
+ have hb0 := h0.hBounds
+ have hb1 := h1.hBounds
+ have hb2 := h2.hBounds
+ simp [UScalarTy.numBits] at hl hb0 hb1 hb2
+ have e0 : Array.index_usize s.proximity_domain_high 0#usize = ok h0 := by
+ simp [Array.index_usize, hh]
+ have e1 : Array.index_usize s.proximity_domain_high 1#usize = ok h1 := by
+ simp [Array.index_usize, hh]
+ have e2 : Array.index_usize s.proximity_domain_high 2#usize = ok h2 := by
+ simp [Array.index_usize, hh]
+ obtain ⟨z1, hz1, hv1⟩ := shl_u32 (UScalar.cast .U32 h0) 8#i32 (by decide) (by decide)
+ obtain ⟨z2, hz2, hv2⟩ := shl_u32 (UScalar.cast .U32 h1) 16#i32 (by decide) (by decide)
+ obtain ⟨z3, hz3, hv3⟩ := shl_u32 (UScalar.cast .U32 h2) 24#i32 (by decide) (by decide)
+ simp only [lift, bind_tc_ok, e0, e1, e2, hz1, hz2, hz3]
+ refine ⟨_, rfl, ?_⟩
+ simp only [show (8#i32 : Std.I32).toNat = 8 from rfl, show (16#i32 : Std.I32).toNat = 16 from rfl,
+ show (24#i32 : Std.I32).toNat = 24 from rfl, UScalar.cast_val_eq, UScalarTy.numBits]
+ at hv1 hv2 hv3
+ rw [Nat.mod_eq_of_lt (by omega : h0.val < 2 ^ 32)] at hv1
+ rw [Nat.mod_eq_of_lt (by omega : h1.val < 2 ^ 32)] at hv2
+ rw [Nat.mod_eq_of_lt (by omega : h2.val < 2 ^ 32)] at hv3
+ rw [Nat.mod_eq_of_lt (by omega)] at hv1 hv2 hv3
+ simp only [UScalar.val_or, UScalar.cast_val_eq, UScalarTy.numBits, hv1, hv2, hv3]
+ rw [Nat.mod_eq_of_lt (by omega : s.proximity_domain_low.val < 2 ^ 32)]
+ -- Each OR adds a byte above every bit already set, so it is an addition.
+ have c1 : s.proximity_domain_low.val ||| h0.val * 2 ^ 8 =
+ s.proximity_domain_low.val + h0.val * 2 ^ 8 := by
+ rw [Nat.lor_comm, ← Nat.shiftLeft_eq, ← Nat.shiftLeft_add_eq_or_of_lt (by omega),
+ Nat.shiftLeft_eq, Nat.add_comm]
+ have c2 : (s.proximity_domain_low.val + h0.val * 2 ^ 8) ||| h1.val * 2 ^ 16 =
+ s.proximity_domain_low.val + h0.val * 2 ^ 8 + h1.val * 2 ^ 16 := by
+ rw [Nat.lor_comm, ← Nat.shiftLeft_eq, ← Nat.shiftLeft_add_eq_or_of_lt (by omega),
+ Nat.shiftLeft_eq, Nat.add_comm]
+ have c3 : (s.proximity_domain_low.val + h0.val * 2 ^ 8 + h1.val * 2 ^ 16) |||
+ h2.val * 2 ^ 24 =
+ s.proximity_domain_low.val + h0.val * 2 ^ 8 + h1.val * 2 ^ 16 + h2.val * 2 ^ 24 := by
+ rw [Nat.lor_comm, ← Nat.shiftLeft_eq, ← Nat.shiftLeft_add_eq_or_of_lt (by omega),
+ Nat.shiftLeft_eq, Nat.add_comm]
+ rw [c1, c2, c3]
+ omega
+
+/-- Two entries whose domain bytes differ get different domains, so no two NUMA
+ nodes are merged by the decode. -/
+theorem sratprocessoraffinity_proximity_domain_separates_different_bytes
+ (s t : srat_processor.SratProcessorAffinity) (a0 a1 a2 b0 b1 b2 : Std.U8)
+ (hs : s.proximity_domain_high.val = [a0, a1, a2])
+ (ht : t.proximity_domain_high.val = [b0, b1, b2])
+ (hne : (s.proximity_domain_low.val, a0.val, a1.val, a2.val) ≠
+ (t.proximity_domain_low.val, b0.val, b1.val, b2.val)) :
+ sratprocessoraffinity_proximity_domain s ≠ sratprocessoraffinity_proximity_domain t := by
+ obtain ⟨r, hr, hrv⟩ := sratprocessoraffinity_proximity_domain_is_the_little_endian_word s a0 a1 a2 hs
+ obtain ⟨q, hq, hqv⟩ := sratprocessoraffinity_proximity_domain_is_the_little_endian_word t b0 b1 b2 ht
+ rw [hr, hq]
+ intro heq
+ apply hne
+ have hv : r.val = q.val := by rw [ok.injEq] at heq; rw [heq]
+ have := s.proximity_domain_low.hBounds; have := t.proximity_domain_low.hBounds
+ have := a0.hBounds; have := a1.hBounds; have := a2.hBounds
+ have := b0.hBounds; have := b1.hBounds; have := b2.hBounds
+ simp only [UScalarTy.numBits] at *
+ simp only [Prod.mk.injEq]
+ omega
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TablesSratProcessor.the_sratprocessoraffinity_proximity_domain_wrapper_is_its_method
#print axioms NonosExtraction.TablesSratProcessor.the_sratprocessoraffinity_is_enabled_wrapper_is_its_method
#print axioms NonosExtraction.TablesSratProcessor.the_sratx2apicaffinity_is_enabled_wrapper_is_its_method
+#print axioms NonosExtraction.TablesSratProcessor.sratprocessoraffinity_is_enabled_reads_bit_zero
+#print axioms NonosExtraction.TablesSratProcessor.sratx2apicaffinity_is_enabled_reads_bit_zero
+#print axioms NonosExtraction.TablesSratProcessor.sratprocessoraffinity_is_enabled_agrees_with_sratx2apicaffinity_is_enabled
+#print axioms NonosExtraction.TablesSratProcessor.reserved_flag_bits_do_not_enable_sratprocessoraffinity_is_enabled
+#print axioms NonosExtraction.TablesSratProcessor.sratprocessoraffinity_proximity_domain_is_the_little_endian_word
+#print axioms NonosExtraction.TablesSratProcessor.sratprocessoraffinity_proximity_domain_separates_different_bytes
end NonosExtraction.TablesSratProcessor
diff --git a/verification/extraction/lean/NonosExtraction/TablesTime.lean b/verification/extraction/lean/NonosExtraction/TablesTime.lean
index ea22bca707..d9d74695c1 100644
--- a/verification/extraction/lean/NonosExtraction/TablesTime.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesTime.lean
@@ -36,6 +36,34 @@ structure time.EfiTime where
@[global_simps, irreducible]
def time.EfiTime.TIMEZONE_UNSPECIFIED : Std.I16 := 2047#i16
+/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::is_leap_year]:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 104:4-106:5 -/
+def time.EfiTime.is_leap_year (year : Std.U16) : Result Bool := do
+ let b ← core.num.U16.is_multiple_of year 4#u16
+ if b
+ then
+ let b1 ← core.num.U16.is_multiple_of year 100#u16
+ if b1
+ then core.num.U16.is_multiple_of year 400#u16
+ else ok true
+ else core.num.U16.is_multiple_of year 400#u16
+
+/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::days_in_month]:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 90:4-102:5 -/
+def time.EfiTime.days_in_month
+ (year : Std.U16) (month : Std.U8) : Result Std.U8 := do
+ match month with
+ | 2#uscalar =>
+ let b ← time.EfiTime.is_leap_year year
+ if b
+ then ok 29#u8
+ else ok 28#u8
+ | 4#uscalar => ok 30#u8
+ | 6#uscalar => ok 30#u8
+ | 9#uscalar => ok 30#u8
+ | 11#uscalar => ok 30#u8
+ | _ => ok 31#u8
+
/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::is_valid]:
Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 38:4-51:5
Visibility: public -/
@@ -50,7 +78,8 @@ def time.EfiTime.is_valid (self : time.EfiTime) : Result Bool := do
then
if self.day >= 1#u8
then
- if self.day <= 31#u8
+ let i ← time.EfiTime.days_in_month self.year self.month
+ if self.day <= i
then
if self.hour <= 23#u8
then
@@ -77,18 +106,6 @@ def time.EfiTime.is_valid (self : time.EfiTime) : Result Bool := do
else ok false
else ok false
-/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::is_leap_year]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 85:4-87:5 -/
-def time.EfiTime.is_leap_year (year : Std.U16) : Result Bool := do
- let b ← core.num.U16.is_multiple_of year 4#u16
- if b
- then
- let b1 ← core.num.U16.is_multiple_of year 100#u16
- if b1
- then core.num.U16.is_multiple_of year 400#u16
- else ok true
- else core.num.U16.is_multiple_of year 400#u16
-
/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop body 0:
Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 62:8-64:9
Visibility: public -/
@@ -122,10 +139,42 @@ def time.EfiTime.to_unix_timestamp_loop0
(iter, days)
/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop body 1:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 66:8-71:9
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 66:8-68:9
Visibility: public -/
@[rust_loop_body]
def time.EfiTime.to_unix_timestamp_loop1.body
+ (iter : core.ops.range.Range Std.I64) (days : Std.I64) :
+ Result (ControlFlow ((core.ops.range.Range Std.I64) × Std.I64) Std.I64)
+ := do
+ let (o, iter1) ←
+ core.iter.range.IteratorRange.next core.iter.range.StepI64 iter
+ match o with
+ | none => ok (done days)
+ | some y =>
+ let i ← lift (IScalar.hcast .U16 y)
+ let b ← time.EfiTime.is_leap_year i
+ let i1 ← if b
+ then ok 366#i64
+ else ok 365#i64
+ let days1 ← days - i1
+ ok (cont (iter1, days1))
+
+/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop 1:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 66:8-68:9
+ Visibility: public -/
+@[rust_loop]
+def time.EfiTime.to_unix_timestamp_loop1
+ (iter : core.ops.range.Range Std.I64) (days : Std.I64) : Result Std.I64 := do
+ loop
+ (fun (iter1, days1) => time.EfiTime.to_unix_timestamp_loop1.body iter1
+ days1)
+ (iter, days)
+
+/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop body 2:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 70:8-75:9
+ Visibility: public -/
+@[rust_loop_body]
+def time.EfiTime.to_unix_timestamp_loop2.body
(days_per_month : Array Std.I64 12#usize) (year : Std.I64)
(iter : core.ops.range.Range Std.I64) (days : Std.I64) :
Result (ControlFlow ((core.ops.range.Range Std.I64) × Std.I64) Std.I64)
@@ -149,22 +198,22 @@ def time.EfiTime.to_unix_timestamp_loop1.body
else ok (cont (iter1, days1))
else ok (cont (iter1, days1))
-/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop 1:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 66:8-71:9
+/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]: loop 2:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 70:8-75:9
Visibility: public -/
@[rust_loop]
-def time.EfiTime.to_unix_timestamp_loop1
+def time.EfiTime.to_unix_timestamp_loop2
(iter : core.ops.range.Range Std.I64)
(days_per_month : Array Std.I64 12#usize) (year : Std.I64) (days : Std.I64) :
Result Std.I64
:= do
loop
- (fun (iter1, days1) => time.EfiTime.to_unix_timestamp_loop1.body
+ (fun (iter1, days1) => time.EfiTime.to_unix_timestamp_loop2.body
days_per_month year iter1 days1)
(iter, days)
/-- [nonos_x_tables_time::time::{nonos_x_tables_time::time::EfiTime}::to_unix_timestamp]:
- Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 53:4-83:5
+ Source: 'src/../../../../../src/arch/x86_64/uefi/tables/time.rs', lines 53:4-87:5
Visibility: public -/
def time.EfiTime.to_unix_timestamp (self : time.EfiTime) : Result Std.I64 := do
let year ← lift (UScalar.hcast .I64 self.year)
@@ -174,14 +223,17 @@ def time.EfiTime.to_unix_timestamp (self : time.EfiTime) : Result Std.I64 := do
time.EfiTime.to_unix_timestamp_loop0 { start := 1970#i64, «end» := year }
0#i64
let days1 ←
- time.EfiTime.to_unix_timestamp_loop1 { start := 1#i64, «end» := month }
+ time.EfiTime.to_unix_timestamp_loop1 { start := year, «end» := 1970#i64 }
+ days
+ let days2 ←
+ time.EfiTime.to_unix_timestamp_loop2 { start := 1#i64, «end» := month }
(Array.make 12#usize [
31#i64, 28#i64, 31#i64, 30#i64, 31#i64, 30#i64, 31#i64, 31#i64, 30#i64,
31#i64, 30#i64, 31#i64
- ]) year days
+ ]) year days1
let i ← day - 1#i64
- let days2 ← days1 + i
- let i1 ← days2 * 86400#i64
+ let days3 ← days2 + i
+ let i1 ← days3 * 86400#i64
let i2 ← lift (UScalar.hcast .I64 self.hour)
let i3 ← i2 * 3600#i64
let i4 ← i1 + i3
diff --git a/verification/extraction/lean/NonosExtraction/TablesTimeRefinement.lean b/verification/extraction/lean/NonosExtraction/TablesTimeRefinement.lean
index 24852c6407..d5052ce9f1 100644
--- a/verification/extraction/lean/NonosExtraction/TablesTimeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TablesTimeRefinement.lean
@@ -21,9 +21,11 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TablesTime
+import NonosExtraction.CivilDays
open Aeneas Aeneas.Std Result
open nonos_x_tables_time
+open ControlFlow
set_option linter.hashCommand false
set_option maxRecDepth 100000
@@ -38,9 +40,731 @@ theorem the_efitime_is_valid_wrapper_is_its_method (a : time.EfiTime) :
theorem the_efitime_to_unix_timestamp_wrapper_is_its_method (a : time.EfiTime) :
efitime_to_unix_timestamp a = time.EfiTime.to_unix_timestamp a := rfl
+/-! ### Field ranges, the Gregorian year, and seconds since 1970
+
+ `efitime_is_valid` is characterised exactly: it accepts the UEFI field ranges
+ with the day bounded by the length of its month, reads the timezone as a
+ signed offset so that offsets west of Greenwich pass, and treats 2047 as the
+ unspecified sentinel. `efitime_to_unix_timestamp` is characterised on every
+ month from one to twelve and every value of the other fields: all three
+ loops are run to completion against pure day counts, the leap rule they
+ consult is proven to be the Gregorian rule and to agree with the second copy
+ in the civil clock, no step of the arithmetic overflows `i64`, and the offset
+ is subtracted at sixty seconds per minute. Anchors at the epoch, at
+ 2000-03-01, before 1970 and under an offset pin the result to known values.
+
+ Two defects are fixed and their theorems restated. Years from 1900 to 1969
+ passed `efitime_is_valid`, but the only year loop ran forward from 1970 and
+ was empty for them, so they were counted as 1970 and collided with dates a
+ year later; a second loop now counts them back from the epoch. The day check
+ was a range check, so 2021-02-31 passed and was read as 2021-03-03; it now
+ asks `days_in_month`. These theorems cannot establish anything about the
+ timestamp for months 0 or 13 and above, which `efitime_is_valid` rejects, nor
+ about the firmware call in the UEFI manager that fills an `EfiTime`, which is
+ not extracted. Nothing in the kernel converts an `EfiTime` to a timestamp
+ today.
+-/
+
+/-- The leap rule inside the timestamp is the full Gregorian rule on every `u16`
+ year, including the four hundred year exception. -/
+theorem the_timestamp_leap_rule_is_gregorian (y : Std.U16) :
+ time.EfiTime.is_leap_year y =
+ ok (decide ((y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0)) := by
+ unfold time.EfiTime.is_leap_year
+ simp only [core.num.U16.is_multiple_of, UScalar.is_multiple_of, bind_tc_ok]
+ have h4 : (4#u16 : Std.U16).val = 4 := rfl
+ have h100 : (100#u16 : Std.U16).val = 100 := rfl
+ have h400 : (400#u16 : Std.U16).val = 400 := rfl
+ rw [h4, h100, h400]
+ by_cases p : y.val % 4 = 0 <;> by_cases q : y.val % 100 = 0 <;>
+ by_cases r : y.val % 400 = 0 <;> simp [p, q, r]
+
+/-- The UEFI time table and the civil clock in sys/clock/civil/days.rs keep two
+ copies of the leap rule. They agree on every year. -/
+theorem the_timestamp_leap_rule_agrees_with_the_civil_clock (y : Std.U16) :
+ time.EfiTime.is_leap_year y = nonos_x_civil_days.is_leap_year y := by
+ rw [the_timestamp_leap_rule_is_gregorian]
+ unfold nonos_x_civil_days.is_leap_year nonos_x_civil_days.days.is_leap_year
+ simp only [core.num.U16.is_multiple_of, UScalar.is_multiple_of, bind_tc_ok]
+ have h4 : (4#u16 : Std.U16).val = 4 := rfl
+ have h100 : (100#u16 : Std.U16).val = 100 := rfl
+ have h400 : (400#u16 : Std.U16).val = 400 := rfl
+ rw [h4, h100, h400]
+ by_cases a : y.val % 4 = 0 <;> by_cases b : y.val % 100 = 0 <;>
+ by_cases c : y.val % 400 = 0 <;> simp [a, b, c]
+
+/-- Checked `i64` addition succeeds with the exact sum when the sum is in range. -/
+theorem adding_i64_in_range_succeeds (x y : Std.I64) (h1 : -9223372036854775808 ≤ x.val + y.val)
+ (h2 : x.val + y.val ≤ 9223372036854775807) :
+ ∃ z : Std.I64, x + y = ok z ∧ z.val = x.val + y.val :=
+ WP.spec_imp_exists (IScalar.add_spec (by scalar_tac) (by scalar_tac))
+
+/-- Checked `i64` subtraction succeeds with the exact difference when it is in range. -/
+theorem subtracting_i64_in_range_succeeds (x y : Std.I64) (h1 : -9223372036854775808 ≤ x.val - y.val)
+ (h2 : x.val - y.val ≤ 9223372036854775807) :
+ ∃ z : Std.I64, x - y = ok z ∧ z.val = x.val - y.val :=
+ WP.spec_imp_exists (IScalar.sub_spec (by scalar_tac) (by scalar_tac))
+
+/-- Checked `i64` multiplication succeeds with the exact product when it is in range. -/
+theorem multiplying_i64_in_range_succeeds (x y : Std.I64) (h1 : -9223372036854775808 ≤ x.val * y.val)
+ (h2 : x.val * y.val ≤ 9223372036854775807) :
+ ∃ z : Std.I64, x * y = ok z ∧ z.val = x.val * y.val :=
+ WP.spec_imp_exists (IScalar.mul_spec (by scalar_tac) (by scalar_tac))
+
+theorem widening_a_u8_keeps_its_value (x : Std.U8) : (UScalar.hcast .I64 x).val = x.val := by
+ obtain ⟨z, hz, hzv⟩ := WP.spec_imp_exists (UScalar.hcast_inBounds_spec .I64 x (by scalar_tac))
+ simp only [lift, ok.injEq] at hz; rw [hz]; exact hzv
+
+theorem a_u8_is_at_most_255 (x : Std.U8) : x.val ≤ 255 := by scalar_tac
+
+theorem a_u16_is_at_most_65535 (x : Std.U16) : x.val ≤ 65535 := by scalar_tac
+
+theorem an_i16_lies_in_its_range (x : Std.I16) : -32768 ≤ x.val ∧ x.val ≤ 32767 := by scalar_tac
+
+theorem widening_a_u16_keeps_its_value (x : Std.U16) : (UScalar.hcast .I64 x).val = x.val := by
+ obtain ⟨z, hz, hzv⟩ := WP.spec_imp_exists (UScalar.hcast_inBounds_spec .I64 x (by scalar_tac))
+ simp only [lift, ok.injEq] at hz; rw [hz]; exact hzv
+
+theorem widening_an_i16_keeps_its_value (x : Std.I16) : (IScalar.cast .I64 x).val = x.val := by
+ obtain ⟨z, hz, hzv⟩ := WP.spec_imp_exists (IScalar.cast_inBounds_spec .I64 x
+ (by constructor <;> scalar_tac))
+ simp only [lift, ok.injEq] at hz; rw [hz]; exact hzv
+
+/-- A nonempty `i64` range yields its start and moves the start up by one. -/
+theorem an_i64_range_yields_its_start_and_steps_by_one (s e : Std.I64) (h : s.val < e.val) :
+ ∃ s' : Std.I64, s'.val = s.val + 1 ∧
+ core.iter.range.IteratorRange.next core.iter.range.StepI64 { start := s, «end» := e } =
+ ok (some s, { start := s', «end» := e }) := by
+ simp [core.iter.range.IteratorRange.next, core.iter.range.IScalarStep,
+ core.iter.range.IScalarStep.forward_checked, core.cmp.impls.PartialOrdI64.lt, I64.max_eq,
+ h]
+ have hs := s.hBounds
+ have he := e.hBounds
+ have hb : s.val + 1 ≤ 9223372036854775807 := by
+ simp only [IScalarTy.numBits] at he; omega
+ refine ⟨_, ?_, by rw [dif_pos hb]; rfl⟩
+ exact IScalar.ofInt_val_eq _
+
+/-- An `i64` range whose start has reached its end yields nothing. -/
+theorem an_i64_range_at_its_end_yields_nothing (s e : Std.I64) (h : e.val ≤ s.val) :
+ core.iter.range.IteratorRange.next core.iter.range.StepI64
+ { start := s, «end» := e } = ok (none, { start := s, «end» := e }) := by
+ simp [core.iter.range.IteratorRange.next, core.iter.range.IScalarStep,
+ core.cmp.impls.PartialOrdI64.lt, h]
+
+/-- The length of a Gregorian year. -/
+def yearLength (y : Nat) : Int :=
+ if (y % 4 = 0 ∧ y % 100 ≠ 0) ∨ y % 400 = 0 then 366 else 365
+
+/-- The days in the `n` years from year `a`. -/
+def daysInYearsFrom : Nat → Nat → Int
+ | _, 0 => 0
+ | a, n + 1 => yearLength a + daysInYearsFrom (a + 1) n
+
+theorem a_year_has_365_or_366_days (y : Nat) : 365 ≤ yearLength y ∧ yearLength y ≤ 366 := by
+ unfold yearLength; split <;> omega
+
+
+/-- One pass of the year loop adds 366 in a Gregorian leap year and 365 otherwise. -/
+theorem one_pass_of_the_year_loop_adds_that_years_length (s e d : Std.I64) (k : Nat) (hs : s.val = k) (hk : (k : Int) < e.val)
+ (hk16 : k ≤ 65535) (hd : 0 ≤ d.val) (hdm : d.val + 366 ≤ 9223372036854775807) :
+ ∃ s' d' : Std.I64, s'.val = k + 1 ∧ d'.val = d.val + yearLength k ∧
+ time.EfiTime.to_unix_timestamp_loop0.body { start := s, «end» := e } d =
+ ok (cont ({ start := s', «end» := e }, d')) := by
+ obtain ⟨s', hs', hn⟩ := an_i64_range_yields_its_start_and_steps_by_one s e (by omega)
+ have hu : (IScalar.hcast .U16 s).val = k := by
+ obtain ⟨u, hu, huv⟩ := WP.spec_imp_exists (IScalar.hcast_inBounds_spec .U16 s
+ (by rw [hs]; constructor <;> simp [U16.max_eq]; omega))
+ simp only [lift, ok.injEq] at hu; rw [hu]; omega
+ have hyd := a_year_has_365_or_366_days k
+ have e366 : (366#i64 : Std.I64).val = 366 := rfl
+ have e365 : (365#i64 : Std.I64).val = 365 := rfl
+ obtain ⟨z, hz, hzv⟩ := adding_i64_in_range_succeeds d
+ (if (k % 4 = 0 ∧ k % 100 ≠ 0) ∨ k % 400 = 0 then 366#i64 else 365#i64)
+ (by split <;> omega) (by split <;> omega)
+ refine ⟨s', z, by omega, ?_, ?_⟩
+ · rw [hzv]; unfold yearLength; split <;> rfl
+ · unfold time.EfiTime.to_unix_timestamp_loop0.body
+ rw [hn]
+ simp only [bind_tc_ok, lift, uncurry, the_timestamp_leap_rule_is_gregorian, hu]
+ by_cases p : (k % 4 = 0 ∧ k % 100 ≠ 0) ∨ k % 400 = 0 <;>
+ simp only [p, if_true, if_false] at hz <;> simp [p, hz]
+
+theorem the_year_loop_over_an_empty_range_adds_nothing (s e d : Std.I64) (h : e.val ≤ s.val) :
+ time.EfiTime.to_unix_timestamp_loop0 { start := s, «end» := e } d = ok d := by
+ unfold time.EfiTime.to_unix_timestamp_loop0
+ rw [loop]
+ simp only [time.EfiTime.to_unix_timestamp_loop0.body, an_i64_range_at_its_end_yields_nothing s e h, bind_tc_ok, uncurry]
+
+/-- The year loop, started at year `k` with `n` years to go, adds the lengths of
+ exactly those `n` years. -/
+theorem the_year_loop_adds_the_lengths_of_the_years_it_walks : ∀ n k : Nat, ∀ s e : Std.I64, s.val = k → (k : Int) + n = e.val →
+ k + n ≤ 65535 → ∀ d : Std.I64, 0 ≤ d.val → d.val + 366 * n ≤ 9223372036854775807 →
+ ∃ r : Std.I64, time.EfiTime.to_unix_timestamp_loop0 { start := s, «end» := e } d = ok r ∧
+ r.val = d.val + daysInYearsFrom k n := by
+ intro n
+ induction n with
+ | zero =>
+ intro k s e hs he _ d _ _
+ exact ⟨d, the_year_loop_over_an_empty_range_adds_nothing s e d (by omega), by simp [daysInYearsFrom]⟩
+ | succ n ih =>
+ intro k s e hs he hk d hd hdm
+ obtain ⟨s', d', hs', hd', hb⟩ := one_pass_of_the_year_loop_adds_that_years_length s e d k hs (by omega) (by omega) hd (by omega)
+ have hyd := a_year_has_365_or_366_days k
+ obtain ⟨r, hr, hrv⟩ := ih (k + 1) s' e (by rw [hs']; push_cast; rfl) (by push_cast; omega)
+ (by omega) d' (by omega) (by omega)
+ refine ⟨r, ?_, ?_⟩
+ · unfold time.EfiTime.to_unix_timestamp_loop0
+ rw [loop]
+ simp only []
+ rw [hb]
+ simp only []
+ unfold time.EfiTime.to_unix_timestamp_loop0 at hr
+ exact hr
+ · rw [hrv, hd']; simp only [daysInYearsFrom]; ring
+
+/-- One pass of the backward year loop takes away 366 in a Gregorian leap year
+ and 365 otherwise. -/
+theorem one_pass_of_the_backward_year_loop_takes_that_years_length (s e d : Std.I64) (k : Nat)
+ (hs : s.val = k) (hk : (k : Int) < e.val) (hk16 : k ≤ 65535) (hd : d.val ≤ 0)
+ (hdm : -9223372036854775808 ≤ d.val - 366) :
+ ∃ s' d' : Std.I64, s'.val = k + 1 ∧ d'.val = d.val - yearLength k ∧
+ time.EfiTime.to_unix_timestamp_loop1.body { start := s, «end» := e } d =
+ ok (cont ({ start := s', «end» := e }, d')) := by
+ obtain ⟨s', hs', hn⟩ := an_i64_range_yields_its_start_and_steps_by_one s e (by omega)
+ have hu : (IScalar.hcast .U16 s).val = k := by
+ obtain ⟨u, hu, huv⟩ := WP.spec_imp_exists (IScalar.hcast_inBounds_spec .U16 s
+ (by rw [hs]; constructor <;> simp [U16.max_eq]; omega))
+ simp only [lift, ok.injEq] at hu; rw [hu]; omega
+ have hyd := a_year_has_365_or_366_days k
+ have e366 : (366#i64 : Std.I64).val = 366 := rfl
+ have e365 : (365#i64 : Std.I64).val = 365 := rfl
+ obtain ⟨z, hz, hzv⟩ := subtracting_i64_in_range_succeeds d
+ (if (k % 4 = 0 ∧ k % 100 ≠ 0) ∨ k % 400 = 0 then 366#i64 else 365#i64)
+ (by split <;> omega) (by split <;> omega)
+ refine ⟨s', z, by omega, ?_, ?_⟩
+ · rw [hzv]; unfold yearLength; split <;> rfl
+ · unfold time.EfiTime.to_unix_timestamp_loop1.body
+ rw [hn]
+ simp only [bind_tc_ok, lift, uncurry, the_timestamp_leap_rule_is_gregorian, hu]
+ by_cases p : (k % 4 = 0 ∧ k % 100 ≠ 0) ∨ k % 400 = 0 <;>
+ simp only [p, if_true, if_false] at hz <;> simp [p, hz]
+
+theorem the_backward_year_loop_over_an_empty_range_takes_nothing (s e d : Std.I64)
+ (h : e.val ≤ s.val) :
+ time.EfiTime.to_unix_timestamp_loop1 { start := s, «end» := e } d = ok d := by
+ unfold time.EfiTime.to_unix_timestamp_loop1
+ rw [loop]
+ simp only [time.EfiTime.to_unix_timestamp_loop1.body,
+ an_i64_range_at_its_end_yields_nothing s e h, bind_tc_ok, uncurry]
+
+/-- The backward year loop, started at year `k` with `n` years to go, takes away
+ the lengths of exactly those `n` years. -/
+theorem the_backward_year_loop_takes_the_lengths_of_the_years_it_walks :
+ ∀ n k : Nat, ∀ s e : Std.I64, s.val = k → (k : Int) + n = e.val →
+ k + n ≤ 65535 → ∀ d : Std.I64, d.val ≤ 0 → -9223372036854775808 ≤ d.val - 366 * n →
+ ∃ r : Std.I64, time.EfiTime.to_unix_timestamp_loop1 { start := s, «end» := e } d = ok r ∧
+ r.val = d.val - daysInYearsFrom k n := by
+ intro n
+ induction n with
+ | zero =>
+ intro k s e hs he _ d _ _
+ exact ⟨d, the_backward_year_loop_over_an_empty_range_takes_nothing s e d (by omega),
+ by simp [daysInYearsFrom]⟩
+ | succ n ih =>
+ intro k s e hs he hk d hd hdm
+ obtain ⟨s', d', hs', hd', hb⟩ :=
+ one_pass_of_the_backward_year_loop_takes_that_years_length s e d k hs (by omega)
+ (by omega) hd (by omega)
+ have hyd := a_year_has_365_or_366_days k
+ obtain ⟨r, hr, hrv⟩ := ih (k + 1) s' e (by rw [hs']; push_cast; rfl) (by push_cast; omega)
+ (by omega) d' (by omega) (by omega)
+ refine ⟨r, ?_, ?_⟩
+ · unfold time.EfiTime.to_unix_timestamp_loop1
+ rw [loop]
+ simp only []
+ rw [hb]
+ simp only []
+ unfold time.EfiTime.to_unix_timestamp_loop1 at hr
+ exact hr
+ · rw [hrv, hd']; simp only [daysInYearsFrom]; ring
+
+/-- The whole days from 1970-01-01 to January 1 of year `y`: the years from 1970
+ up to `y` counted forward, or the years from `y` up to 1970 counted back.
+ One of the two walks is always empty. -/
+def daysToYear (y : Nat) : Int :=
+ daysInYearsFrom 1970 (y - 1970) - daysInYearsFrom y (1970 - y)
+
+/-- The calendar's month lengths, February common. -/
+def monthLengths : List Int := [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+
+/-- The Gregorian leap rule. -/
+def gregorianLeap (y : Nat) : Prop := (y % 4 = 0 ∧ y % 100 ≠ 0) ∨ y % 400 = 0
+
+instance (y : Nat) : Decidable (gregorianLeap y) := by unfold gregorianLeap; infer_instance
+
+/-- The length of month `m` of year `y`. -/
+def monthLength (y m : Nat) : Int :=
+ monthLengths[m - 1]! + (if m = 2 ∧ gregorianLeap y then 1 else 0)
+
+/-- The month table exactly as the extracted `to_unix_timestamp` builds it. -/
+abbrev daysPerMonth : Array Std.I64 12#usize := Array.make 12#usize [
+ 31#i64, 28#i64, 31#i64, 30#i64, 31#i64, 30#i64, 31#i64, 31#i64, 30#i64,
+ 31#i64, 30#i64, 31#i64
+ ]
+
+theorem every_month_in_the_table_has_28_to_31_days (i : Nat) (h : i < 12) :
+ 28 ≤ monthLengths[i]! ∧ monthLengths[i]! ≤ 31 := by
+ have hi : i = 0 ∨ i = 1 ∨ i = 2 ∨ i = 3 ∨ i = 4 ∨ i = 5 ∨ i = 6 ∨ i = 7 ∨ i = 8 ∨
+ i = 9 ∨ i = 10 ∨ i = 11 := by omega
+ rcases hi with rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl <;> decide
+
+/-- The twelve month lengths the extracted code indexes are the calendar's, with
+ a 28 day February. -/
+theorem the_extracted_month_table_is_the_calendar_table (i : Nat) (h : i < 12) :
+ (daysPerMonth.val[i]'(by simp [daysPerMonth, Array.make]; omega)).val = monthLengths[i]! := by
+ have hi : i = 0 ∨ i = 1 ∨ i = 2 ∨ i = 3 ∨ i = 4 ∨ i = 5 ∨ i = 6 ∨ i = 7 ∨ i = 8 ∨
+ i = 9 ∨ i = 10 ∨ i = 11 := by omega
+ rcases hi with rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl <;> rfl
+
+/-- `days_in_month` gives the calendar length of every month from one to twelve,
+ with a 29 day February exactly in Gregorian leap years. -/
+theorem days_in_month_is_the_month_length (y : Std.U16) (m : Std.U8) (h1 : 1 ≤ m.val)
+ (h2 : m.val ≤ 12) :
+ ∃ x : Std.U8, time.EfiTime.days_in_month y m = ok x ∧ (x.val : Int) = monthLength y.val m.val := by
+ have hm : m.val = 1 ∨ m.val = 2 ∨ m.val = 3 ∨ m.val = 4 ∨ m.val = 5 ∨ m.val = 6 ∨
+ m.val = 7 ∨ m.val = 8 ∨ m.val = 9 ∨ m.val = 10 ∨ m.val = 11 ∨ m.val = 12 := by omega
+ unfold time.EfiTime.days_in_month
+ rcases hm with h|h|h|h|h|h|h|h|h|h|h|h
+ · obtain rfl : m = 1#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 2#u8 := UScalar.eq_of_val_eq h
+ simp only [the_timestamp_leap_rule_is_gregorian, bind_tc_ok]
+ by_cases p : gregorianLeap y.val
+ · have p' : (y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0 := p
+ refine ⟨_, by simp [p']; rfl, ?_⟩
+ simp [monthLength, monthLengths, p]
+ · have p' : ¬ ((y.val % 4 = 0 ∧ y.val % 100 ≠ 0) ∨ y.val % 400 = 0) := p
+ refine ⟨_, by simp [p']; rfl, ?_⟩
+ simp [monthLength, monthLengths, p]
+ · obtain rfl : m = 3#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 4#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 5#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 6#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 7#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 8#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 9#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 10#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 11#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+ · obtain rfl : m = 12#u8 := UScalar.eq_of_val_eq h
+ exact ⟨_, rfl, by simp [monthLength, monthLengths]⟩
+
+/-- `efitime_is_valid` holds exactly on the UEFI field ranges with the day
+ bounded by the length of its month, February having 29 days exactly in
+ Gregorian leap years. The timezone is compared as a signed `i16`, so offsets
+ west of Greenwich down to -1440 are accepted, and 2047 (unspecified) is
+ accepted on its own. -/
+theorem efitime_is_valid_is_exactly_the_uefi_calendar (t : time.EfiTime) :
+ efitime_is_valid t = ok (decide (1900 ≤ t.year.val ∧ t.year.val ≤ 9999 ∧
+ 1 ≤ t.month.val ∧ t.month.val ≤ 12 ∧ 1 ≤ t.day.val ∧
+ (t.day.val : Int) ≤ monthLength t.year.val t.month.val ∧
+ t.hour.val ≤ 23 ∧ t.minute.val ≤ 59 ∧ t.second.val ≤ 59 ∧
+ t.nanosecond.val ≤ 999999999 ∧
+ (t.timezone.val = 2047 ∨ (-1440 ≤ t.timezone.val ∧ t.timezone.val ≤ 1440)))) := by
+ unfold efitime_is_valid time.EfiTime.is_valid
+ unfold time.EfiTime.TIMEZONE_UNSPECIFIED
+ have hb : ∀ (c : Prop) [Decidable c] (b : Bool),
+ (if c then ok b else ok false : Result Bool) = ok (decide c && b) := by
+ intro c _ b; by_cases h : c <;> simp [h]
+ have ht : ∀ (c : Prop) [Decidable c] (b : Bool),
+ (if c then ok true else ok b : Result Bool) = ok (decide c || b) := by
+ intro c _ b; by_cases h : c <;> simp [h]
+ by_cases hm : 1 ≤ t.month.val ∧ t.month.val ≤ 12
+ · obtain ⟨x, hx, hxv⟩ := days_in_month_is_the_month_length t.year t.month hm.1 hm.2
+ rw [hx]
+ simp only [bind_tc_ok, hb, ht, ok.injEq]
+ rw [Bool.eq_iff_iff]
+ simp only [Bool.and_eq_true, Bool.or_eq_true, decide_eq_true_eq, ge_iff_le,
+ UScalar.le_equiv, IScalar.le_equiv, IScalar.eq_equiv]
+ have key : ((t.day.val : Int) ≤ monthLength t.year.val t.month.val) ↔ t.day.val ≤ x.val := by
+ rw [← hxv]; omega
+ rw [key]
+ exact Iff.rfl
+ · have hm' : ¬ (t.month ≥ 1#u8 ∧ t.month ≤ 12#u8) := by
+ simp only [ge_iff_le, UScalar.le_equiv]; exact hm
+ by_cases h1 : t.month ≥ 1#u8
+ · have h2 : ¬ t.month ≤ 12#u8 := fun h2 => hm' ⟨h1, h2⟩
+ simp only [h1, h2, if_false, ite_self]
+ symm; simp only [ok.injEq, decide_eq_false_iff_not]; omega
+ · simp only [h1, if_false, ite_self]
+ symm; simp only [ok.injEq, decide_eq_false_iff_not]
+ simp only [ge_iff_le, UScalar.le_equiv] at h1
+ omega
+
+/-- One pass of the month loop adds the table length of that month, and one more
+ day for February of a leap year. The index `m - 1` stays inside the table for
+ months one to twelve. -/
+theorem one_pass_of_the_month_loop_adds_that_months_length (s e d yr : Std.I64) (k y : Nat) (hs : s.val = k) (hy : yr.val = y)
+ (hy16 : y ≤ 65535) (hk1 : 1 ≤ k) (hk : (k : Int) < e.val) (hk12 : k ≤ 12)
+ (hd : -9223372036854775808 ≤ d.val) (hdm : d.val + 32 ≤ 9223372036854775807) :
+ ∃ s' d' : Std.I64, s'.val = k + 1 ∧ d'.val = d.val + monthLength y k ∧
+ time.EfiTime.to_unix_timestamp_loop2.body daysPerMonth yr { start := s, «end» := e } d =
+ ok (cont ({ start := s', «end» := e }, d')) := by
+ obtain ⟨s', hs', hn⟩ := an_i64_range_yields_its_start_and_steps_by_one s e (by omega)
+ have hs'' : s'.val = k + 1 := by omega
+ have e1 : (1#i64 : Std.I64).val = 1 := rfl
+ obtain ⟨i, hi, hiv⟩ := subtracting_i64_in_range_succeeds s 1#i64 (by omega) (by omega)
+ have hiv' : i.val = (k : Int) - 1 := by omega
+ obtain ⟨j, hj, hjv⟩ := WP.spec_imp_exists (IScalar.hcast_inBounds_spec .Usize i
+ (by constructor <;> scalar_tac))
+ simp only [lift, ok.injEq] at hj
+ have hjv' : j.val = k - 1 := by omega
+ obtain ⟨x, hx, hxv⟩ := WP.spec_imp_exists (Array.index_usize_spec daysPerMonth j
+ (by simp [hjv']; omega))
+ have hxval : x.val = monthLengths[k - 1]! := by
+ subst hxv; simp only [hjv']; exact the_extracted_month_table_is_the_calendar_table (k - 1) (by omega)
+ have hmt := every_month_in_the_table_has_28_to_31_days (k - 1) (by omega)
+ obtain ⟨a, ha, hav⟩ := adding_i64_in_range_succeeds d x (by omega) (by omega)
+ unfold time.EfiTime.to_unix_timestamp_loop2.body
+ rw [hn]
+ simp only [bind_tc_ok, lift, uncurry, hi, hj, hx, ha]
+ by_cases hk2 : k = 2
+ · subst hk2
+ have hu : (IScalar.hcast .U16 yr).val = y := by
+ obtain ⟨u, hu, huv⟩ := WP.spec_imp_exists (IScalar.hcast_inBounds_spec .U16 yr
+ (by rw [hy]; constructor <;> simp [U16.max_eq]; omega))
+ simp only [lift, ok.injEq] at hu; rw [hu]; omega
+ have h2 : s = 2#i64 := IScalar.eq_of_val_eq (by rw [hs]; rfl)
+ simp only [h2, if_true, the_timestamp_leap_rule_is_gregorian, hu, bind_tc_ok]
+ by_cases p : gregorianLeap y
+ · obtain ⟨b, hb, hbv⟩ := adding_i64_in_range_succeeds a 1#i64 (by omega) (by omega)
+ refine ⟨s', b, hs'', ?_, ?_⟩
+ · rw [hbv, hav, hxval, e1]; simp only [monthLength, p, and_self, if_true]; omega
+ · have p' : (y % 4 = 0 ∧ y % 100 ≠ 0) ∨ y % 400 = 0 := p
+ simp [p', hb]
+ · refine ⟨s', a, hs'', ?_, ?_⟩
+ · rw [hav, hxval]; simp only [monthLength, p, and_false, if_false]; omega
+ · have p' : ¬ ((y % 4 = 0 ∧ y % 100 ≠ 0) ∨ y % 400 = 0) := p
+ simp [p']
+ · have hne : ¬ s = 2#i64 := by
+ intro h; have := congrArg IScalar.val h; rw [hs] at this
+ have : (k : Int) = 2 := this
+ omega
+ refine ⟨s', a, hs'', ?_, ?_⟩
+ · rw [hav, hxval]; simp only [monthLength, hk2, false_and, if_false]; omega
+ · simp only [hne, if_false]
+
+/-- The days in the `n` months from month `a` of year `y`. -/
+def daysInMonthsFrom (y : Nat) : Nat → Nat → Int
+ | _, 0 => 0
+ | a, n + 1 => monthLength y a + daysInMonthsFrom y (a + 1) n
+
+theorem a_month_has_28_to_31_days (y k : Nat) (h1 : 1 ≤ k) (h : k ≤ 12) :
+ 28 ≤ monthLength y k ∧ monthLength y k ≤ 31 := by
+ have hk' : k = 1 ∨ k = 2 ∨ k = 3 ∨ k = 4 ∨ k = 5 ∨ k = 6 ∨ k = 7 ∨ k = 8 ∨ k = 9 ∨
+ k = 10 ∨ k = 11 ∨ k = 12 := by omega
+ unfold monthLength
+ rcases hk' with rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl|rfl <;>
+ simp only [monthLengths] <;> split <;>
+ first | decide | (rename_i h; exact absurd h.1 (by decide))
+
+theorem the_month_loop_over_an_empty_range_adds_nothing (s e d yr : Std.I64) (h : e.val ≤ s.val) :
+ time.EfiTime.to_unix_timestamp_loop2 { start := s, «end» := e } daysPerMonth yr d = ok d := by
+ unfold time.EfiTime.to_unix_timestamp_loop2
+ rw [loop]
+ simp only [time.EfiTime.to_unix_timestamp_loop2.body, an_i64_range_at_its_end_yields_nothing s e h, bind_tc_ok, uncurry]
+
+/-- The month loop, started at month `k` with `n` months to go, adds the lengths
+ of exactly those months in the given year. -/
+theorem the_month_loop_adds_the_lengths_of_the_months_it_walks (yr : Std.I64) (y : Nat) (hy : yr.val = y) (hy16 : y ≤ 65535) :
+ ∀ n k : Nat, ∀ s e : Std.I64, s.val = k → (k : Int) + n = e.val → 1 ≤ k → k + n ≤ 13 →
+ ∀ d : Std.I64, -9223372036854775808 ≤ d.val → d.val + 32 * n ≤ 9223372036854775807 →
+ ∃ r : Std.I64, time.EfiTime.to_unix_timestamp_loop2 { start := s, «end» := e } daysPerMonth yr d
+ = ok r ∧ r.val = d.val + daysInMonthsFrom y k n := by
+ intro n
+ induction n with
+ | zero =>
+ intro k s e hs he _ _ d _ _
+ exact ⟨d, the_month_loop_over_an_empty_range_adds_nothing s e d yr (by omega), by simp [daysInMonthsFrom]⟩
+ | succ n ih =>
+ intro k s e hs he hk1 hk d hd hdm
+ obtain ⟨s', d', hs', hd', hb⟩ := one_pass_of_the_month_loop_adds_that_months_length s e d yr k y hs hy hy16 hk1 (by omega) (by omega)
+ hd (by omega)
+ have hmd := a_month_has_28_to_31_days y k hk1 (by omega)
+ obtain ⟨r, hr, hrv⟩ := ih (k + 1) s' e (by rw [hs']; push_cast; rfl) (by push_cast; omega)
+ (by omega) (by omega) d' (by omega) (by omega)
+ refine ⟨r, ?_, ?_⟩
+ · unfold time.EfiTime.to_unix_timestamp_loop2
+ rw [loop]
+ simp only []
+ rw [hb]
+ simp only []
+ unfold time.EfiTime.to_unix_timestamp_loop2 at hr
+ exact hr
+ · rw [hrv, hd']; simp only [daysInMonthsFrom]; ring
+
+theorem the_years_walked_add_at_most_366_days_each : ∀ n a : Nat, 0 ≤ daysInYearsFrom a n ∧ daysInYearsFrom a n ≤ 366 * n := by
+ intro n; induction n with
+ | zero => intro a; simp [daysInYearsFrom]
+ | succ n ih => intro a; have := ih (a + 1); have := a_year_has_365_or_366_days a; simp only [daysInYearsFrom]; omega
+
+theorem the_months_walked_add_at_most_31_days_each (y : Nat) : ∀ n a : Nat, 1 ≤ a → a + n ≤ 13 →
+ 0 ≤ daysInMonthsFrom y a n ∧ daysInMonthsFrom y a n ≤ 31 * n := by
+ intro n; induction n with
+ | zero => intro a _ _; simp [daysInMonthsFrom]
+ | succ n ih =>
+ intro a h1 h2; have := ih (a + 1) (by omega) (by omega)
+ have := a_month_has_28_to_31_days y a h1 (by omega); simp only [daysInMonthsFrom]; omega
+
+/-- For every month from one to twelve and every other field, `efitime_to_unix_timestamp`
+ succeeds and returns the whole days since 1970-01-01 (the signed days to
+ January 1 of the year, then the lengths of the months before this one, then
+ `day - 1`) in seconds, plus the time of day, minus sixty seconds per minute of
+ timezone offset unless the offset is the unspecified 2047. A year before 1970
+ counts back from the epoch, so its dates are negative. -/
+theorem efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (t : time.EfiTime) (hm1 : 1 ≤ t.month.val) (hm : t.month.val ≤ 12) :
+ efitime_to_unix_timestamp t ⦃ r => r.val =
+ 86400 * (daysToYear t.year.val + daysInMonthsFrom t.year.val 1 (t.month.val - 1)
+ + t.day.val - 1) + 3600 * t.hour.val + 60 * t.minute.val + t.second.val
+ - (if t.timezone.val = 2047 then 0 else 60 * t.timezone.val) ⦄ := by
+ unfold efitime_to_unix_timestamp time.EfiTime.to_unix_timestamp
+ have hy16 := a_u16_is_at_most_65535 t.year
+ have hyr := widening_a_u16_keeps_its_value t.year
+ have hmo := widening_a_u8_keeps_its_value t.month
+ have hdb := the_years_walked_add_at_most_366_days_each (t.year.val - 1970) 1970
+ have hmb := the_months_walked_add_at_most_31_days_each t.year.val (t.month.val - 1) 1 le_rfl (by omega)
+ obtain ⟨r0, hr0, hr0v⟩ : ∃ r0 : Std.I64, time.EfiTime.to_unix_timestamp_loop0
+ { start := 1970#i64, «end» := UScalar.hcast .I64 t.year } 0#i64 = ok r0 ∧
+ r0.val = daysInYearsFrom 1970 (t.year.val - 1970) := by
+ by_cases hy1 : 1970 ≤ t.year.val
+ · have h0 : (0#i64 : Std.I64).val = 0 := rfl
+ obtain ⟨r0, h, hv⟩ := the_year_loop_adds_the_lengths_of_the_years_it_walks (t.year.val - 1970) 1970 1970#i64
+ (UScalar.hcast .I64 t.year) rfl (by omega) (by omega) 0#i64 (by decide) (by omega)
+ exact ⟨r0, h, by rw [hv, h0]; omega⟩
+ · refine ⟨0#i64, the_year_loop_over_an_empty_range_adds_nothing _ _ _ ?_, ?_⟩
+ · have : (1970#i64 : Std.I64).val = 1970 := rfl
+ omega
+ · have : t.year.val - 1970 = 0 := by omega
+ rw [this]; rfl
+ have hbb := the_years_walked_add_at_most_366_days_each (1970 - t.year.val) t.year.val
+ obtain ⟨q0, hq0, hq0v⟩ : ∃ q0 : Std.I64, time.EfiTime.to_unix_timestamp_loop1
+ { start := UScalar.hcast .I64 t.year, «end» := 1970#i64 } r0 = ok q0 ∧
+ q0.val = daysToYear t.year.val := by
+ have e1970 : (1970#i64 : Std.I64).val = 1970 := rfl
+ by_cases hy1 : 1970 ≤ t.year.val
+ · refine ⟨r0, the_backward_year_loop_over_an_empty_range_takes_nothing _ _ _
+ (by rw [hyr]; omega), ?_⟩
+ have : 1970 - t.year.val = 0 := by omega
+ rw [hr0v, daysToYear, this]; simp [daysInYearsFrom]
+ · have hz : t.year.val - 1970 = 0 := by omega
+ have hr00 : r0.val = 0 := by rw [hr0v, hz]; rfl
+ obtain ⟨q0, h, hv⟩ := the_backward_year_loop_takes_the_lengths_of_the_years_it_walks
+ (1970 - t.year.val) t.year.val (UScalar.hcast .I64 t.year) 1970#i64 hyr
+ (by rw [e1970]; omega) (by omega) r0 (by omega) (by omega)
+ refine ⟨q0, h, ?_⟩
+ rw [hv, hr00, daysToYear, hz]; simp [daysInYearsFrom]
+ obtain ⟨r1, hr1, hr1v⟩ := the_month_loop_adds_the_lengths_of_the_months_it_walks (UScalar.hcast .I64 t.year) t.year.val (by omega) hy16
+ (t.month.val - 1) 1 1#i64 (UScalar.hcast .I64 t.month) rfl (by omega) le_rfl (by omega) q0
+ (by rw [hq0v, daysToYear]; omega) (by rw [hq0v, daysToYear]; omega)
+ have hr1' : time.EfiTime.to_unix_timestamp_loop2 { start := 1#i64, «end» := UScalar.hcast .I64 t.month }
+ (Array.make 12#usize [
+ 31#i64, 28#i64, 31#i64, 30#i64, 31#i64, 30#i64, 31#i64, 31#i64, 30#i64,
+ 31#i64, 30#i64, 31#i64
+ ]) (UScalar.hcast .I64 t.year) q0 = ok r1 := hr1
+ have hd := widening_a_u8_keeps_its_value t.day
+ have hh := widening_a_u8_keeps_its_value t.hour
+ have hmi := widening_a_u8_keeps_its_value t.minute
+ have hs := widening_a_u8_keeps_its_value t.second
+ have htz := widening_an_i16_keeps_its_value t.timezone
+ have hdy := a_u8_is_at_most_255 t.day
+ have hhy := a_u8_is_at_most_255 t.hour
+ have hmy := a_u8_is_at_most_255 t.minute
+ have hsy := a_u8_is_at_most_255 t.second
+ have htzb := an_i16_lies_in_its_range t.timezone
+ have hr1b : -366 * 1970 ≤ r1.val ∧ r1.val ≤ 366 * 65535 + 31 * 12 := by
+ rw [hr1v, hq0v, daysToYear]; omega
+ have e1 : (1#i64 : Std.I64).val = 1 := rfl
+ have e86400 : (86400#i64 : Std.I64).val = 86400 := rfl
+ have e3600 : (3600#i64 : Std.I64).val = 3600 := rfl
+ have e60 : (60#i64 : Std.I64).val = 60 := rfl
+ obtain ⟨i, hi, hiv⟩ := subtracting_i64_in_range_succeeds (UScalar.hcast .I64 t.day) 1#i64 (by omega) (by omega)
+ rw [e1] at hiv
+ obtain ⟨d2, hd2, hd2v⟩ := adding_i64_in_range_succeeds r1 i (by omega) (by omega)
+ obtain ⟨i1, hi1, hi1v⟩ := multiplying_i64_in_range_succeeds d2 86400#i64 (by rw [e86400]; omega) (by rw [e86400]; omega)
+ rw [e86400] at hi1v
+ obtain ⟨i3, hi3, hi3v⟩ := multiplying_i64_in_range_succeeds (UScalar.hcast .I64 t.hour) 3600#i64
+ (by rw [e3600]; omega) (by rw [e3600]; omega)
+ rw [e3600] at hi3v
+ obtain ⟨i4, hi4, hi4v⟩ := adding_i64_in_range_succeeds i1 i3 (by omega) (by omega)
+ obtain ⟨i6, hi6, hi6v⟩ := multiplying_i64_in_range_succeeds (UScalar.hcast .I64 t.minute) 60#i64
+ (by rw [e60]; omega) (by rw [e60]; omega)
+ rw [e60] at hi6v
+ obtain ⟨i7, hi7, hi7v⟩ := adding_i64_in_range_succeeds i4 i6 (by omega) (by omega)
+ obtain ⟨sc, hsc, hscv⟩ := adding_i64_in_range_succeeds i7 (UScalar.hcast .I64 t.second) (by omega) (by omega)
+ obtain ⟨i10, hi10, hi10v⟩ := multiplying_i64_in_range_succeeds (IScalar.cast .I64 t.timezone) 60#i64
+ (by rw [e60]; omega) (by rw [e60]; omega)
+ rw [e60] at hi10v
+ obtain ⟨r, hr, hrv⟩ := subtracting_i64_in_range_succeeds sc i10 (by omega) (by omega)
+ simp only [lift, bind_tc_ok, hr0, hq0, hr1', hi, hd2, hi1, hi3, hi4, hi6, hi7, hsc, hi10, hr]
+ by_cases hz : t.timezone.val = 2047
+ · have hz' : t.timezone = time.EfiTime.TIMEZONE_UNSPECIFIED := by
+ unfold time.EfiTime.TIMEZONE_UNSPECIFIED; exact IScalar.eq_of_val_eq hz
+ have hb : (t.timezone != time.EfiTime.TIMEZONE_UNSPECIFIED) = false := by
+ rw [hz']; exact bne_self_eq_false _
+ simp only [hb, Bool.false_eq_true, if_false, WP.spec_ok, if_pos hz]
+ omega
+ · have hz' : (t.timezone != time.EfiTime.TIMEZONE_UNSPECIFIED) = true := by
+ unfold time.EfiTime.TIMEZONE_UNSPECIFIED
+ simp only [bne_iff_ne, ne_eq]
+ intro h; exact hz (by rw [h]; rfl)
+ simp only [hz', if_true, WP.spec_ok, if_neg hz]
+ omega
+
+/-- A calendar date at midnight, with every padding field zero. -/
+def midnightOn (y : Std.U16) (m d : Std.U8) (tz : Std.I16) : time.EfiTime :=
+ { year := y, month := m, day := d, hour := 0#u8, minute := 0#u8, second := 0#u8,
+ pad1 := 0#u8, nanosecond := 0#u32, timezone := tz, daylight := 0#u8, pad2 := 0#u8 }
+
+/-- The offset boundaries: -1440 and 1440 are accepted, -1441 and 1441 are not,
+ and 2047 is accepted although it lies outside them. -/
+theorem efitime_is_valid_accepts_offsets_from_minus_1440_to_1440_and_2047 :
+ efitime_is_valid (midnightOn 2024#u16 6#u8 30#u8 (-1440)#i16) = ok true ∧
+ efitime_is_valid (midnightOn 2024#u16 6#u8 30#u8 (-1441)#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2024#u16 6#u8 30#u8 1440#i16) = ok true ∧
+ efitime_is_valid (midnightOn 2024#u16 6#u8 30#u8 1441#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2024#u16 6#u8 30#u8 2047#i16) = ok true := by
+ simp only [efitime_is_valid_is_exactly_the_uefi_calendar]
+ refine ⟨rfl, rfl, rfl, rfl, rfl⟩
+
+/-- Midnight on 1970-01-01 is second zero in UTC, and `-60 * tz` for a real
+ offset of `tz` minutes: local midnight east of Greenwich came earlier. -/
+theorem efitime_to_unix_timestamp_at_the_epoch_is_minus_the_offset (tz : Std.I16) :
+ ∃ r : Std.I64, efitime_to_unix_timestamp
+ (midnightOn 1970#u16 1#u8 1#u8 tz) = ok r ∧
+ r.val = (if tz.val = 2047 then 0 else -60 * tz.val) := by
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists
+ (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (midnightOn 1970#u16 1#u8 1#u8 tz) (Nat.le_refl 1) (by decide : (1 : Nat) ≤ 12))
+ refine ⟨r, hr, ?_⟩
+ rw [hrv]
+ show 86400 * (daysInYearsFrom 1970 0 + daysInMonthsFrom 1970 1 0 + ((1 : Nat) : Int) - 1)
+ + 3600 * ((0 : Nat) : Int) + 60 * ((0 : Nat) : Int) + ((0 : Nat) : Int)
+ - (if tz.val = 2047 then 0 else 60 * tz.val) = _
+ simp only [daysInYearsFrom, daysInMonthsFrom]
+ split <;> omega
+
+/-- 2000-03-01 is second 951868800, which requires 2000 to be a leap year. A leap
+ rule without the four hundred year exception gives 951782400. -/
+theorem efitime_to_unix_timestamp_counts_2000_as_a_leap_year :
+ efitime_to_unix_timestamp
+ (midnightOn 2000#u16 3#u8 1#u8 2047#i16) = ok 951868800#i64 := by
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (midnightOn 2000#u16 3#u8 1#u8 2047#i16) (by decide) (by decide))
+ rw [hr]
+ congr 1
+ apply IScalar.eq_of_val_eq
+ rw [hrv]
+ rfl
+
+/-- A date before 1970 counts back from the epoch: 1969-12-31 is second -86400,
+ a day before 1970-01-01, and 1900-01-01, the earliest date
+ `efitime_is_valid` accepts, is second -2208988800. The year loop used to be
+ empty before 1970, so a pre-1970 date was counted as if it fell in 1970:
+ 1969-12-31 and 1970-12-31 both mapped to second 31449600. -/
+theorem efitime_to_unix_timestamp_counts_back_before_1970 :
+ efitime_is_valid (midnightOn 1969#u16 12#u8 31#u8 2047#i16) = ok true ∧
+ efitime_to_unix_timestamp (midnightOn 1969#u16 12#u8 31#u8 2047#i16) = ok (-86400)#i64 ∧
+ efitime_to_unix_timestamp (midnightOn 1970#u16 12#u8 31#u8 2047#i16) = ok 31449600#i64 ∧
+ efitime_is_valid (midnightOn 1900#u16 1#u8 1#u8 2047#i16) = ok true ∧
+ efitime_to_unix_timestamp (midnightOn 1900#u16 1#u8 1#u8 2047#i16) = ok (-2208988800)#i64 := by
+ refine ⟨by rw [efitime_is_valid_is_exactly_the_uefi_calendar]; simp [midnightOn, monthLength, monthLengths, gregorianLeap], ?_, ?_,
+ by rw [efitime_is_valid_is_exactly_the_uefi_calendar]; simp [midnightOn, monthLength, monthLengths, gregorianLeap], ?_⟩
+ · obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (midnightOn 1969#u16 12#u8 31#u8 2047#i16) (by decide) (by decide))
+ rw [hr]; congr 1; apply IScalar.eq_of_val_eq; rw [hrv]; decide
+ · obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (midnightOn 1970#u16 12#u8 31#u8 2047#i16) (by decide) (by decide))
+ rw [hr]; congr 1; apply IScalar.eq_of_val_eq; rw [hrv]; decide
+ · obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset (midnightOn 1900#u16 1#u8 1#u8 2047#i16) (by decide) (by decide))
+ rw [hr]; congr 1; apply IScalar.eq_of_val_eq; rw [hrv]; decide
+
+/-- A real offset moves the result by exactly sixty seconds per minute, in the
+ direction that converts local time to UTC, against the same fields with the
+ offset unspecified. -/
+theorem efitime_to_unix_timestamp_subtracts_sixty_seconds_per_offset_minute (t : time.EfiTime) (hm1 : 1 ≤ t.month.val) (hm : t.month.val ≤ 12)
+ (htz : t.timezone.val ≠ 2047) :
+ ∃ r r' : Std.I64, efitime_to_unix_timestamp t = ok r ∧
+ efitime_to_unix_timestamp { t with timezone := 2047#i16 } = ok r' ∧
+ r.val = r'.val - 60 * t.timezone.val := by
+ obtain ⟨r, hr, hrv⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset t hm1 hm)
+ obtain ⟨r', hr', hrv'⟩ := WP.spec_imp_exists (efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset { t with timezone := 2047#i16 } hm1 hm)
+ refine ⟨r, r', hr, hr', ?_⟩
+ rw [hrv, hrv', if_neg htz]
+ have : ((2047#i16 : Std.I16).val : Int) = 2047 := rfl
+ simp only [this, if_true]
+ omega
+
+/-- The day check follows the calendar: 2021-02-31 and 2023-02-29 are refused,
+ 2024-02-29 and 2000-02-29 are accepted, and 2100-02-29 is refused because a
+ century is not a leap year unless it is a multiple of 400. The check used to
+ bound the day by 31 in every month, so 2021-02-31 passed and
+ `efitime_to_unix_timestamp` read it as 2021-03-03. -/
+theorem efitime_is_valid_refuses_days_past_the_end_of_the_month :
+ efitime_is_valid (midnightOn 2021#u16 2#u8 31#u8 2047#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2023#u16 2#u8 29#u8 2047#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2024#u16 2#u8 29#u8 2047#i16) = ok true ∧
+ efitime_is_valid (midnightOn 2000#u16 2#u8 29#u8 2047#i16) = ok true ∧
+ efitime_is_valid (midnightOn 2100#u16 2#u8 29#u8 2047#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2024#u16 4#u8 31#u8 2047#i16) = ok false ∧
+ efitime_is_valid (midnightOn 2024#u16 12#u8 31#u8 2047#i16) = ok true := by
+ simp only [efitime_is_valid_is_exactly_the_uefi_calendar]
+ refine ⟨?_, ?_, ?_, ?_, ?_, ?_, ?_⟩ <;> simp [midnightOn, monthLength, monthLengths, gregorianLeap]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TablesTime.the_efitime_is_valid_wrapper_is_its_method
#print axioms NonosExtraction.TablesTime.the_efitime_to_unix_timestamp_wrapper_is_its_method
+#print axioms NonosExtraction.TablesTime.efitime_is_valid_is_exactly_the_uefi_calendar
+#print axioms NonosExtraction.TablesTime.the_timestamp_leap_rule_is_gregorian
+#print axioms NonosExtraction.TablesTime.the_timestamp_leap_rule_agrees_with_the_civil_clock
+#print axioms NonosExtraction.TablesTime.adding_i64_in_range_succeeds
+#print axioms NonosExtraction.TablesTime.subtracting_i64_in_range_succeeds
+#print axioms NonosExtraction.TablesTime.multiplying_i64_in_range_succeeds
+#print axioms NonosExtraction.TablesTime.widening_a_u8_keeps_its_value
+#print axioms NonosExtraction.TablesTime.a_u8_is_at_most_255
+#print axioms NonosExtraction.TablesTime.a_u16_is_at_most_65535
+#print axioms NonosExtraction.TablesTime.an_i16_lies_in_its_range
+#print axioms NonosExtraction.TablesTime.widening_a_u16_keeps_its_value
+#print axioms NonosExtraction.TablesTime.widening_an_i16_keeps_its_value
+#print axioms NonosExtraction.TablesTime.an_i64_range_yields_its_start_and_steps_by_one
+#print axioms NonosExtraction.TablesTime.an_i64_range_at_its_end_yields_nothing
+#print axioms NonosExtraction.TablesTime.a_year_has_365_or_366_days
+#print axioms NonosExtraction.TablesTime.one_pass_of_the_year_loop_adds_that_years_length
+#print axioms NonosExtraction.TablesTime.the_year_loop_over_an_empty_range_adds_nothing
+#print axioms NonosExtraction.TablesTime.the_year_loop_adds_the_lengths_of_the_years_it_walks
+#print axioms NonosExtraction.TablesTime.one_pass_of_the_backward_year_loop_takes_that_years_length
+#print axioms NonosExtraction.TablesTime.the_backward_year_loop_over_an_empty_range_takes_nothing
+#print axioms NonosExtraction.TablesTime.the_backward_year_loop_takes_the_lengths_of_the_years_it_walks
+#print axioms NonosExtraction.TablesTime.every_month_in_the_table_has_28_to_31_days
+#print axioms NonosExtraction.TablesTime.the_extracted_month_table_is_the_calendar_table
+#print axioms NonosExtraction.TablesTime.days_in_month_is_the_month_length
+#print axioms NonosExtraction.TablesTime.one_pass_of_the_month_loop_adds_that_months_length
+#print axioms NonosExtraction.TablesTime.a_month_has_28_to_31_days
+#print axioms NonosExtraction.TablesTime.the_month_loop_over_an_empty_range_adds_nothing
+#print axioms NonosExtraction.TablesTime.the_month_loop_adds_the_lengths_of_the_months_it_walks
+#print axioms NonosExtraction.TablesTime.the_years_walked_add_at_most_366_days_each
+#print axioms NonosExtraction.TablesTime.the_months_walked_add_at_most_31_days_each
+#print axioms NonosExtraction.TablesTime.efitime_to_unix_timestamp_counts_days_from_1970_and_subtracts_the_offset
+#print axioms NonosExtraction.TablesTime.efitime_is_valid_accepts_offsets_from_minus_1440_to_1440_and_2047
+#print axioms NonosExtraction.TablesTime.efitime_to_unix_timestamp_at_the_epoch_is_minus_the_offset
+#print axioms NonosExtraction.TablesTime.efitime_to_unix_timestamp_counts_2000_as_a_leap_year
+#print axioms NonosExtraction.TablesTime.efitime_to_unix_timestamp_counts_back_before_1970
+#print axioms NonosExtraction.TablesTime.efitime_to_unix_timestamp_subtracts_sixty_seconds_per_offset_minute
+#print axioms NonosExtraction.TablesTime.efitime_is_valid_refuses_days_past_the_end_of_the_month
end NonosExtraction.TablesTime
diff --git a/verification/extraction/lean/NonosExtraction/TimerStateRefinement.lean b/verification/extraction/lean/NonosExtraction/TimerStateRefinement.lean
index 6c16f2b42a..d033bf32af 100644
--- a/verification/extraction/lean/NonosExtraction/TimerStateRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TimerStateRefinement.lean
@@ -41,10 +41,72 @@ theorem the_increment_ticks_wrapper_is_its_method :
theorem the_reset_ticks_wrapper_is_its_method :
reset_ticks = state.reset_ticks := rfl
+/-! ### What the clock operations do with the tick counter
+
+The tick counter is one `AtomicU64` static, and Aeneas leaves the atomic
+operations opaque: `new`, `load`, `fetch_add` and `store` are axioms with no
+behaviour attached. The theorems below therefore say what each operation asks
+of the atomic and what it does with the answer. `get_ticks` returns the
+relaxed load unchanged, with no unit conversion or offset. `increment_ticks`
+adds exactly one and fails only if the atomic itself fails, whatever old value
+comes back, so a counter at `u64::MAX` wraps in the timer interrupt rather than
+panicking there. `reset_ticks` stores exactly the value the static is created
+with.
+
+What they cannot establish: that the atomic read-modify-write is indivisible,
+that a relaxed load observes the latest increment, or anything about the
+interrupt path in `interrupts/timer/tick.rs` that calls these functions, none of
+which is extracted. They also cannot square `reset_ticks` with the tier-one
+model in `Nonos.Timer`, where the clock only ever advances: after a reset the
+next `get_ticks` would read zero. Nothing in the tree calls `reset_ticks` today
+(it is only re-exported from `interrupts`).
+-/
+
+/-- The clock read is one relaxed load of the tick counter, returned as it is. -/
+theorem get_ticks_returns_the_relaxed_load_unchanged
+ (c : core.sync.atomic.Atomic Std.U64 (core.sync.atomic.private.Align8 Std.U64))
+ (v : Std.U64) (hc : state.TICK_COUNT = ok c)
+ (hv : core.sync.atomic.AtomicU64Align8U64.load c core.sync.atomic.Ordering.Relaxed = ok v) :
+ get_ticks = ok v := by
+ unfold get_ticks state.get_ticks
+ simp only [hc, bind_tc_ok, hv]
+
+/-- The increment asks the atomic to add exactly one, and succeeds exactly when
+ that `fetch_add` succeeds. The old value it returns plays no part, so there
+ is no overflow check that could fail in interrupt context: a counter at
+ `u64::MAX` wraps rather than panicking. -/
+theorem increment_ticks_succeeds_exactly_when_adding_one_succeeds
+ (c : core.sync.atomic.Atomic Std.U64 (core.sync.atomic.private.Align8 Std.U64))
+ (hc : state.TICK_COUNT = ok c) :
+ increment_ticks = ok () ↔
+ ∃ v, core.sync.atomic.AtomicU64Align8U64.fetch_add c 1#u64
+ core.sync.atomic.Ordering.Relaxed = ok v := by
+ unfold increment_ticks state.increment_ticks
+ simp only [hc, bind_tc_ok]
+ cases h : core.sync.atomic.AtomicU64Align8U64.fetch_add c 1#u64
+ core.sync.atomic.Ordering.Relaxed with
+ | ok v => simp
+ | fail e => simp
+ | div => simp
+
+/-- The reset stores the same value the tick counter is created with, so after
+ a reset the counter is back at its boot value (zero). -/
+theorem reset_ticks_stores_the_value_the_counter_starts_with :
+ ∃ z : Std.U64, z.val = 0 ∧ state.TICK_COUNT = core.sync.atomic.AtomicU64Align8U64.new z ∧
+ reset_ticks = (do
+ let c ← state.TICK_COUNT
+ core.sync.atomic.AtomicU64Align8U64.store c z core.sync.atomic.Ordering.Relaxed) := by
+ refine ⟨0#u64, rfl, ?_, rfl⟩
+ unfold state.TICK_COUNT
+ rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TimerState.the_get_ticks_wrapper_is_its_method
#print axioms NonosExtraction.TimerState.the_increment_ticks_wrapper_is_its_method
#print axioms NonosExtraction.TimerState.the_reset_ticks_wrapper_is_its_method
+#print axioms NonosExtraction.TimerState.get_ticks_returns_the_relaxed_load_unchanged
+#print axioms NonosExtraction.TimerState.increment_ticks_succeeds_exactly_when_adding_one_succeeds
+#print axioms NonosExtraction.TimerState.reset_ticks_stores_the_value_the_counter_starts_with
end NonosExtraction.TimerState
diff --git a/verification/extraction/lean/NonosExtraction/TrampolinePerApRefinement.lean b/verification/extraction/lean/NonosExtraction/TrampolinePerApRefinement.lean
index a49f3380aa..ea7c2f822b 100644
--- a/verification/extraction/lean/NonosExtraction/TrampolinePerApRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TrampolinePerApRefinement.lean
@@ -35,8 +35,42 @@ namespace NonosExtraction.TrampolinePerAp
theorem the_perapbootcontext_new_wrapper_is_its_method (a : Std.U64) (b : Std.U64) (c : Std.U64) (d : Std.U32) :
perapbootcontext_new a b c d = per_ap.PerApBootContext.new a b c d := rfl
+/-! ### The boot context carries each value to its own field
+
+`ap_unit` builds one `PerApBootContext` per application processor from the
+page-table root, the processor's stack top, the Rust entry point and its CPU
+number, and the trampoline reads these back at fixed offsets. The theorems
+below show that `perapbootcontext_new` never fails and stores each argument in
+the field of the same name, so the page-table root, stack and entry point
+cannot be exchanged, and that it loses no argument. They cannot establish the
+`#[repr(C)]` byte layout the assembly trampoline depends on, nor the
+narrowing of the CPU number to `u32` that the caller performs; neither is
+visible in the extracted code.
+-/
+
+/-- `perapbootcontext_new` stores each argument in its own field. A version
+ that exchanged `pml4_phys` and `stack_top` would load a stack address into
+ `CR3` on the new processor. -/
+theorem perapbootcontext_new_keeps_every_field
+ (pml4 stack entry : Std.U64) (cpu : Std.U32) :
+ ∃ c, perapbootcontext_new pml4 stack entry cpu = ok c ∧
+ c.pml4_phys = pml4 ∧ c.stack_top = stack ∧ c.entry_ptr = entry ∧ c.cpu_id = cpu :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- `perapbootcontext_new` loses no argument: two processors given different
+ stacks, roots, entry points or CPU numbers receive different contexts. -/
+theorem perapbootcontext_new_loses_no_argument
+ (p s e p' s' e' : Std.U64) (c c' : Std.U32)
+ (h : perapbootcontext_new p s e c = perapbootcontext_new p' s' e' c') :
+ p = p' ∧ s = s' ∧ e = e' ∧ c = c' := by
+ simp only [perapbootcontext_new, per_ap.PerApBootContext.new, ok.injEq,
+ per_ap.PerApBootContext.mk.injEq] at h
+ exact h
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TrampolinePerAp.the_perapbootcontext_new_wrapper_is_its_method
+#print axioms NonosExtraction.TrampolinePerAp.perapbootcontext_new_keeps_every_field
+#print axioms NonosExtraction.TrampolinePerAp.perapbootcontext_new_loses_no_argument
end NonosExtraction.TrampolinePerAp
diff --git a/verification/extraction/lean/NonosExtraction/TypesBridgeRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesBridgeRefinement.lean
index 6011375539..ef82785620 100644
--- a/verification/extraction/lean/NonosExtraction/TypesBridgeRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesBridgeRefinement.lean
@@ -35,8 +35,34 @@ namespace NonosExtraction.TypesBridge
theorem the_bridgeinfo_new_wrapper_is_its_method :
bridgeinfo_new = bridge.BridgeInfo.new := rfl
+/-! ### What the default bridge describes
+
+ `BridgeInfo::new` (and `Default`) is the all-zero record. The theorem below
+ reads that as a PCI-to-PCI bridge description: its bridge control word sets
+ no bit (no secondary bus reset, no VGA forwarding, no ISA mode), it claims
+ only bus zero on every side, and each of its three windows is the inclusive
+ range from zero to zero. Under the PCI convention a window is disabled only
+ when its base exceeds its limit, so these windows are not disabled windows;
+ they are one-unit windows at address zero. The record is also not in the
+ image of the register decoder in `config/bridge.rs`, whose `memory_window`
+ always sets the low twenty bits of the limit. No kernel code currently
+ constructs a `BridgeInfo`, so there is no caller whose contract this can be
+ checked against.
+-/
+
+/-- The default bridge sets no control bit, claims only bus zero, and has every
+ window with base at most limit; its memory limit is not a decoded value. -/
+theorem bridgeinfo_new_claims_bus_zero_and_forwards_nothing_extra :
+ ∃ b, bridgeinfo_new = ok b ∧ b.bridge_control.val = 0 ∧
+ b.primary_bus.val = 0 ∧ b.secondary_bus.val = 0 ∧ b.subordinate_bus.val = 0 ∧
+ b.io_base.val ≤ b.io_limit.val ∧ b.memory_base.val ≤ b.memory_limit.val ∧
+ b.prefetch_base.val ≤ b.prefetch_limit.val ∧
+ b.memory_limit.val % 0x100000 ≠ 0xFFFFF := by
+ refine ⟨_, rfl, rfl, rfl, rfl, rfl, ?_, ?_, ?_, ?_⟩ <;> decide
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesBridge.the_bridgeinfo_new_wrapper_is_its_method
+#print axioms NonosExtraction.TypesBridge.bridgeinfo_new_claims_bus_zero_and_forwards_nothing_extra
end NonosExtraction.TypesBridge
diff --git a/verification/extraction/lean/NonosExtraction/TypesDeviceIdRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesDeviceIdRefinement.lean
index b0ef057017..94716f78cd 100644
--- a/verification/extraction/lean/NonosExtraction/TypesDeviceIdRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesDeviceIdRefinement.lean
@@ -38,9 +38,69 @@ theorem the_deviceid_new_wrapper_is_its_method (a : Std.U16) (b : Std.U16) :
theorem the_deviceid_matches_wrapper_is_its_method (a : types_device_id.DeviceId) (b : Std.U16) (c : Std.U16) :
deviceid_matches a b c = types_device_id.DeviceId.matches a b c := rfl
+/-! ### A PCI identity matches on vendor and device alone
+
+`matches` is how the PCI layer asks whether a probed function is a given part:
+it accepts exactly when both the vendor and the device ID agree, and it ignores
+the subsystem IDs and the revision, so two boards built on the same chip match
+the same query. `new` builds an identity from a vendor and device pair with the
+subsystem IDs and revision zeroed, and an identity built by `new` matches the
+pair it was built from and no other; in particular the `0xFFFF, 0xFFFF`
+placeholder that `src/drivers/pci/types/device.rs` installs for a slot with no
+function answers to the all-ones query and to nothing else.
+
+These theorems cannot say that a probed identity was read from the right
+configuration space offsets: `src/drivers/pci/manager/probe.rs` builds the
+struct from port reads that are not extracted. -/
+
+/-- `matches` holds exactly when vendor and device both agree. -/
+theorem deviceid_matches_iff_vendor_and_device_agree
+ (d : types_device_id.DeviceId) (v dev : Std.U16) :
+ deviceid_matches d v dev = ok (decide (d.vendor_id = v ∧ d.device_id = dev)) := by
+ unfold deviceid_matches types_device_id.DeviceId.matches
+ by_cases hv : d.vendor_id = v <;> by_cases hd : d.device_id = dev <;> simp [hv, hd]
+
+/-- The subsystem IDs and the revision play no part in a match. -/
+theorem deviceid_matches_ignores_subsystem_and_revision
+ (d : types_device_id.DeviceId) (sv s : Std.U16) (r : Std.U8) (v dev : Std.U16) :
+ deviceid_matches { d with subsystem_vendor_id := sv, subsystem_id := s, revision := r } v dev
+ = deviceid_matches d v dev := by
+ rfl
+
+/-- `new` keeps the two IDs it was given and zeroes the rest. -/
+theorem deviceid_new_keeps_the_pair_and_zeroes_the_rest (v dev : Std.U16) :
+ ∃ d, deviceid_new v dev = ok d ∧ d.vendor_id = v ∧ d.device_id = dev ∧
+ d.subsystem_vendor_id.val = 0 ∧ d.subsystem_id.val = 0 ∧ d.revision.val = 0 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- The two functions agree: an identity built by `deviceid_new` matches a
+ query exactly when the query names the pair it was built from. -/
+theorem deviceid_new_matches_exactly_its_own_pair (v dev v' dev' : Std.U16) :
+ (do let d ← deviceid_new v dev; deviceid_matches d v' dev')
+ = ok (decide (v = v' ∧ dev = dev')) := by
+ simp only [deviceid_new, types_device_id.DeviceId.new, bind_tc_ok]
+ exact deviceid_matches_iff_vendor_and_device_agree _ v' dev'
+
+/-- The empty slot placeholder answers the all-ones query and only that one. -/
+theorem the_empty_slot_placeholder_deviceid_matches_only_all_ones (v' dev' : Std.U16) :
+ (do let d ← deviceid_new 0xFFFF#u16 0xFFFF#u16; deviceid_matches d v' dev')
+ = ok (decide (v'.val = 0xFFFF ∧ dev'.val = 0xFFFF)) := by
+ rw [deviceid_new_matches_exactly_its_own_pair]
+ congr 1
+ apply decide_eq_decide.mpr
+ constructor
+ · rintro ⟨h1, h2⟩; subst h1; subst h2; exact ⟨rfl, rfl⟩
+ · rintro ⟨h1, h2⟩
+ exact ⟨UScalar.eq_of_val_eq (by simp [h1]), UScalar.eq_of_val_eq (by simp [h2])⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesDeviceId.the_deviceid_new_wrapper_is_its_method
#print axioms NonosExtraction.TypesDeviceId.the_deviceid_matches_wrapper_is_its_method
+#print axioms NonosExtraction.TypesDeviceId.deviceid_matches_iff_vendor_and_device_agree
+#print axioms NonosExtraction.TypesDeviceId.deviceid_matches_ignores_subsystem_and_revision
+#print axioms NonosExtraction.TypesDeviceId.deviceid_new_keeps_the_pair_and_zeroes_the_rest
+#print axioms NonosExtraction.TypesDeviceId.deviceid_new_matches_exactly_its_own_pair
+#print axioms NonosExtraction.TypesDeviceId.the_empty_slot_placeholder_deviceid_matches_only_all_ones
end NonosExtraction.TypesDeviceId
diff --git a/verification/extraction/lean/NonosExtraction/TypesFirmwareRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesFirmwareRefinement.lean
index c19a09fd28..57e01ea3cf 100644
--- a/verification/extraction/lean/NonosExtraction/TypesFirmwareRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesFirmwareRefinement.lean
@@ -38,9 +38,56 @@ theorem the_firmwareentry_empty_wrapper_is_its_method :
theorem the_firmwarehandoff_new_wrapper_is_its_method :
firmwarehandoff_new = firmware.FirmwareHandoff.new := rfl
+/-! ### An empty table has no entries in use and no image in any slot
+
+The bootloader hands the kernel a `FirmwareHandoff` whose `count` says how many
+of its `MAX_FIRMWARE_ENTRIES` (64) slots are filled, and `get_firmware` scans
+`entries[0..count]` for a matching `fw_type`. The theorems below establish that
+`firmwareentry_empty` is an entry of type `Unknown` pointing at address zero
+with size zero, and that `firmwarehandoff_new` returns a table with `count`
+zero, 64 slots, and every one of those slots such an entry. So a table built
+by `new` answers no lookup, and a slot read past `count` names no image.
+
+They cannot establish anything about a table the bootloader filled: that
+`count` stays at most 64 after entries are added, and the scan in
+`get_firmware`, are not in this crate. Nor do they say that an `Unknown` entry
+is all zero bytes in memory, because the `repr(C)` layout and the enum
+discriminant are not part of the extracted model.
+-/
+
+/-- The empty entry is of type `Unknown`, points at address zero and has size
+ zero, and its reserved word is zero. -/
+theorem firmwareentry_empty_is_unknown_at_address_zero_with_size_zero :
+ ∃ e, firmwareentry_empty = ok e ∧ e.fw_type = firmware.FirmwareType.Unknown ∧
+ e.ptr.val = 0 ∧ e.size.val = 0 ∧ e.reserved.val = 0 := by
+ exact ⟨_, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- A new table has no entries in use, so a scan of `entries[0..count]`
+ touches no slot and every slot index below `count` is in bounds. -/
+theorem firmwarehandoff_new_has_no_entries_in_use :
+ ∃ h, firmwarehandoff_new = ok h ∧ h.count.val = 0 ∧ h.count.val ≤ h.entries.val.length := by
+ exact ⟨_, rfl, rfl, Nat.zero_le _⟩
+
+/-- Every one of the 64 slots of a new table is an `Unknown` entry at address
+ zero with size zero, the entry `firmwareentry_empty` returns. -/
+theorem firmwarehandoff_new_fills_all_sixty_four_slots_with_the_empty_entry :
+ ∃ h e, firmwarehandoff_new = ok h ∧ firmwareentry_empty = ok e ∧
+ h.entries.val.length = 64 ∧
+ ∀ i < 64, h.entries.val[i]? = some e ∧
+ e.fw_type = firmware.FirmwareType.Unknown ∧ e.ptr.val = 0 ∧ e.size.val = 0 := by
+ refine ⟨_, _, rfl, rfl, ?_, ?_⟩
+ · exact List.length_replicate
+ · intro i hi
+ refine ⟨?_, rfl, rfl, rfl⟩
+ show (List.replicate 64 _)[i]? = _
+ rw [List.getElem?_replicate, if_pos hi]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesFirmware.the_firmwareentry_empty_wrapper_is_its_method
#print axioms NonosExtraction.TypesFirmware.the_firmwarehandoff_new_wrapper_is_its_method
+#print axioms NonosExtraction.TypesFirmware.firmwareentry_empty_is_unknown_at_address_zero_with_size_zero
+#print axioms NonosExtraction.TypesFirmware.firmwarehandoff_new_has_no_entries_in_use
+#print axioms NonosExtraction.TypesFirmware.firmwarehandoff_new_fills_all_sixty_four_slots_with_the_empty_entry
end NonosExtraction.TypesFirmware
diff --git a/verification/extraction/lean/NonosExtraction/TypesPercpu.lean b/verification/extraction/lean/NonosExtraction/TypesPercpu.lean
index c25b2af2f5..af1ae8181c 100644
--- a/verification/extraction/lean/NonosExtraction/TypesPercpu.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesPercpu.lean
@@ -36,16 +36,18 @@ def percpu.PercpuRegion.new
Visibility: public -/
def percpu.PercpuRegion.end (self : percpu.PercpuRegion) : Result Std.U64 := do
let i ← lift (UScalar.cast .U64 self.size)
- self.base + i
+ ok (core.num.U64.saturating_add self.base i)
/-- [nonos_x_types_percpu::percpu::{nonos_x_types_percpu::percpu::PercpuRegion}::contains]:
- Source: 'src/../../../../../src/memory/layout/types/percpu.rs', lines 35:4-37:5
+ Source: 'src/../../../../../src/memory/layout/types/percpu.rs', lines 37:4-39:5
Visibility: public -/
def percpu.PercpuRegion.contains
(self : percpu.PercpuRegion) (addr : Std.U64) : Result Bool := do
if addr >= self.base
- then let i ← percpu.PercpuRegion.end self
- ok (addr < i)
+ then
+ let i ← addr - self.base
+ let i1 ← lift (UScalar.cast .U64 self.size)
+ ok (i < i1)
else ok false
/-- [nonos_x_types_percpu::percpuregion_new]:
diff --git a/verification/extraction/lean/NonosExtraction/TypesPercpuRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesPercpuRefinement.lean
index f9481ebc7f..ed09eef1c1 100644
--- a/verification/extraction/lean/NonosExtraction/TypesPercpuRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesPercpuRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TypesPercpu
+import Nonos.Interval
open Aeneas Aeneas.Std Result
open nonos_x_types_percpu
@@ -41,10 +42,113 @@ theorem the_percpuregion_end_wrapper_is_its_method (a : percpu.PercpuRegion) :
theorem the_percpuregion_contains_wrapper_is_its_method (a : percpu.PercpuRegion) (b : Std.U64) :
percpuregion_contains a b = percpu.PercpuRegion.contains a b := rfl
+/-! ### Where a per CPU region ends, and which addresses it holds
+
+`new` stores its three arguments in their own fields. `end` is `base + size`
+saturated at `2^64 - 1`, so it never fails. `contains` measures the address from
+the base and compares the offset with the size, so it is membership in the half
+open interval `[base, base + size)` of `Nonos.Interval` for every region, the
+base held for a non-empty region and the end not.
+
+Both used to compute `base + size` with overflow checks: for a region flush
+against the top of the address space, `end` aborted and `contains` aborted on
+every address at or above the base. The last page can now be asked whether it
+holds its own base, and it does.
+
+These theorems cannot see the callers, `get_percpu_regions` and
+`get_percpu_region_for` in `layout/manager/percpu.rs`, which are not extracted,
+nor the constants they read. The last theorem restates the region those callers
+build for a CPU below `MAX_CPUS = 64`, with `PERCPU_BASE = 0xFFFF_FFC0_0000_0000`
+and `PERCPU_STRIDE = 0x100_0000` copied from `layout/constants`, and shows that
+it has an end and that neighbouring CPUs' regions meet without a gap.
+-/
+
+open Nonos.Interval (Iv mem)
+
+instance (a : Iv) (x : Nat) : Decidable (mem a x) := inferInstanceAs (Decidable (_ ∧ _))
+
+/-- A fresh region keeps each argument in its own field. -/
+theorem percpuregion_new_keeps_each_argument_in_its_field
+ (b : Std.U64) (s : Std.Usize) (c : Std.U32) :
+ ∃ r, percpuregion_new b s c = ok r ∧ r.base = b ∧ r.size = s ∧ r.cpu_id = c :=
+ ⟨_, rfl, rfl, rfl, rfl⟩
+
+private theorem u64_saturating_add_val (a b : Std.U64) :
+ (core.num.U64.saturating_add a b).val = min (2 ^ 64 - 1) (a.val + b.val) := by
+ simp only [core.num.U64.saturating_add, UScalar.saturating_add, UScalar.val, UScalar.max]
+ rw [BitVec.toNat_ofNat]
+ show min (2 ^ 64 - 1) _ % 2 ^ 64 = _
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- `end` never fails and is `base + size`, saturated at `2^64 - 1`. -/
+theorem percpuregion_end_is_the_saturated_sum (r : percpu.PercpuRegion) :
+ ∃ e, percpuregion_end r = ok e ∧ e.val = min (2 ^ 64 - 1) (r.base.val + r.size.val) := by
+ unfold percpuregion_end percpu.PercpuRegion.end
+ simp only [lift, bind_tc_ok]
+ have hc : (UScalar.cast .U64 r.size : Std.U64).val = r.size.val := by simp
+ exact ⟨_, rfl, by rw [u64_saturating_add_val, hc]⟩
+
+/-- `contains` is membership in `[base, base + size)`, for every region. -/
+theorem percpuregion_contains_is_interval_membership
+ (r : percpu.PercpuRegion) (x : Std.U64) :
+ percpuregion_contains r x =
+ ok (decide (mem ⟨r.base.val, r.base.val + r.size.val⟩ x.val)) := by
+ unfold percpuregion_contains percpu.PercpuRegion.contains mem
+ have hc : (UScalar.cast .U64 r.size : Std.U64).val = r.size.val := by simp
+ split_ifs with hb
+ · have hle : r.base.val ≤ x.val := hb
+ have e := UScalar.sub_equiv x r.base
+ cases hs : (x - r.base : Result Std.U64) with
+ | ok z =>
+ rw [hs] at e
+ simp only [lift, bind_tc_ok, ok.injEq, decide_eq_decide]
+ show z.val < (UScalar.cast .U64 r.size : Std.U64).val ↔ _
+ rw [hc]; omega
+ | fail _ => rw [hs] at e; simp at e; omega
+ | div => rw [hs] at e; exact e.elim
+ · simp only [ok.injEq]
+ symm
+ simp only [decide_eq_false_iff_not, not_and]
+ intro h1
+ scalar_tac
+
+/-- The last page of the address space, `[0xFFFF_FFFF_FFFF_F000, 2^64)`, holds its
+own base and its last byte; its end saturates at `2^64 - 1`. -/
+theorem percpuregion_contains_the_base_and_last_byte_of_the_last_page :
+ percpuregion_contains ⟨0xFFFFFFFFFFFFF000#u64, 0x1000#usize, 0#u32⟩
+ 0xFFFFFFFFFFFFF000#u64 = ok true ∧
+ percpuregion_contains ⟨0xFFFFFFFFFFFFF000#u64, 0x1000#usize, 0#u32⟩
+ 0xFFFFFFFFFFFFFFFF#u64 = ok true := by
+ simp only [percpuregion_contains_is_interval_membership, mem, ok.injEq, decide_eq_true_eq]
+ constructor <;> simp
+
+/-- The region the layout manager builds for a CPU `c` below `MAX_CPUS = 64`,
+based at `PERCPU_BASE + c * PERCPU_STRIDE` and `PERCPU_STRIDE` bytes long, has
+an end, that end is the next CPU's base, and the region holds its own base. -/
+theorem percpuregion_end_of_a_kernel_cpu_region_is_the_next_cpus_base
+ (b : Std.U64) (c : Std.U32) (hc : c.val < 64)
+ (hb : b.val = 0xFFFFFFC000000000 + c.val * 0x1000000) :
+ (∃ e, percpuregion_end ⟨b, 0x1000000#usize, c⟩ = ok e ∧
+ e.val = 0xFFFFFFC000000000 + (c.val + 1) * 0x1000000) ∧
+ percpuregion_contains ⟨b, 0x1000000#usize, c⟩ b = ok true := by
+ refine ⟨?_, ?_⟩
+ · obtain ⟨e, he, hv⟩ := percpuregion_end_is_the_saturated_sum ⟨b, 0x1000000#usize, c⟩
+ refine ⟨e, he, ?_⟩
+ simp at hv
+ omega
+ · rw [percpuregion_contains_is_interval_membership]
+ simp only [mem, ok.injEq, decide_eq_true_eq]
+ simp
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesPercpu.the_percpuregion_new_wrapper_is_its_method
#print axioms NonosExtraction.TypesPercpu.the_percpuregion_end_wrapper_is_its_method
#print axioms NonosExtraction.TypesPercpu.the_percpuregion_contains_wrapper_is_its_method
+#print axioms NonosExtraction.TypesPercpu.percpuregion_new_keeps_each_argument_in_its_field
+#print axioms NonosExtraction.TypesPercpu.percpuregion_end_is_the_saturated_sum
+#print axioms NonosExtraction.TypesPercpu.percpuregion_contains_is_interval_membership
+#print axioms NonosExtraction.TypesPercpu.percpuregion_contains_the_base_and_last_byte_of_the_last_page
+#print axioms NonosExtraction.TypesPercpu.percpuregion_end_of_a_kernel_cpu_region_is_the_next_cpus_base
end NonosExtraction.TypesPercpu
diff --git a/verification/extraction/lean/NonosExtraction/TypesProtectionRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesProtectionRefinement.lean
index 20f4672918..ce7561d81a 100644
--- a/verification/extraction/lean/NonosExtraction/TypesProtectionRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesProtectionRefinement.lean
@@ -38,9 +38,48 @@ theorem the_protectionflags_new_wrapper_is_its_method :
theorem the_protectionflags_is_fully_protected_wrapper_is_its_method (a : protection.ProtectionFlags) :
protectionflags_is_fully_protected a = protection.ProtectionFlags.is_fully_protected a := rfl
+/-! ### Full protection is the four mandatory features, and bring-up starts with none
+
+ The kernel file promises that a caller gating on these flags never gets a
+ false yes. These theorems establish that `protectionflags_new` claims none of
+ the five features, so an MMU read before bring-up is not fully protected;
+ that `protectionflags_is_fully_protected` is true exactly when SMEP, SMAP, NX
+ and WP are all set, so clearing any one of them makes it false; and that
+ UMIP, which the source marks best-effort, does not affect the answer either
+ way. They cannot establish that the fields reflect the control registers,
+ since the register reads that set them and the mutex around them are not
+ extracted.
+-/
+
+theorem protectionflags_new_claims_no_protection :
+ protectionflags_new = ok (protection.ProtectionFlags.mk
+ (smep_enabled := false) (smap_enabled := false) (nx_enabled := false)
+ (wp_enabled := false) (umip_enabled := false)) := rfl
+
+theorem protectionflags_new_is_not_fully_protected :
+ (do let f ← protectionflags_new; protectionflags_is_fully_protected f) = ok false := rfl
+
+theorem protectionflags_is_fully_protected_exactly_when_smep_smap_nx_and_wp_are_set
+ (f : protection.ProtectionFlags) :
+ protectionflags_is_fully_protected f =
+ ok (f.smep_enabled && f.smap_enabled && f.nx_enabled && f.wp_enabled) := by
+ unfold protectionflags_is_fully_protected protection.ProtectionFlags.is_fully_protected
+ cases f.smep_enabled <;> cases f.smap_enabled <;> cases f.nx_enabled <;> rfl
+
+/-- UMIP is tracked but not required: setting or clearing it never changes
+ whether the flags count as fully protected. -/
+theorem protectionflags_is_fully_protected_ignores_umip (f : protection.ProtectionFlags) (u : Bool) :
+ protectionflags_is_fully_protected { f with umip_enabled := u } =
+ protectionflags_is_fully_protected f := by
+ simp only [protectionflags_is_fully_protected_exactly_when_smep_smap_nx_and_wp_are_set]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesProtection.the_protectionflags_new_wrapper_is_its_method
#print axioms NonosExtraction.TypesProtection.the_protectionflags_is_fully_protected_wrapper_is_its_method
+#print axioms NonosExtraction.TypesProtection.protectionflags_new_claims_no_protection
+#print axioms NonosExtraction.TypesProtection.protectionflags_new_is_not_fully_protected
+#print axioms NonosExtraction.TypesProtection.protectionflags_is_fully_protected_exactly_when_smep_smap_nx_and_wp_are_set
+#print axioms NonosExtraction.TypesProtection.protectionflags_is_fully_protected_ignores_umip
end NonosExtraction.TypesProtection
diff --git a/verification/extraction/lean/NonosExtraction/TypesPteRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesPteRefinement.lean
index 201137a087..742f3b2d96 100644
--- a/verification/extraction/lean/NonosExtraction/TypesPteRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesPteRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TypesPte
+import NonosExtraction.MemoryMmuTypesPteDecode
open Aeneas Aeneas.Std Result
open nonos_x_types_pte
@@ -35,8 +36,58 @@ namespace NonosExtraction.TypesPte
theorem the_pagetableentry_empty_wrapper_is_its_method :
pagetableentry_empty = pte.PageTableEntry.empty := rfl
+/-! ### The empty entry is the decoding of a zero word
+
+`PageTable::new` fills all 512 slots of a fresh table with
+`PageTableEntry::empty()`. The theorems below show that `pagetableentry_empty`
+never fails and agrees field by field with what the kernel's own decoder,
+`PageTableEntry::from_raw`, reads from the zero word the hardware sees in an
+untouched slot, and that the entry is therefore not present, grants no write
+or user access, and names physical frame zero. They do not cover the encoder
+`to_raw`, which is not extracted, nor the separate raw-word `PageTableEntry`
+of the process address space.
+-/
+
+/-- Agreement with the decoder: every field of `pagetableentry_empty` equals
+ the field `pagetableentry_from_raw` decodes from `0`. A version of `empty`
+ that set any flag, or any non-zero address, would disagree with what the
+ hardware reads from a cleared slot. -/
+theorem pagetableentry_empty_is_the_decoding_of_zero :
+ ∃ e d, pagetableentry_empty = ok e ∧
+ nonos_x_memory_mmu_types_pte_decode.pagetableentry_from_raw 0#u64 = ok d ∧
+ e.present = d.present ∧ e.writable = d.writable ∧
+ e.user_accessible = d.user_accessible ∧ e.write_through = d.write_through ∧
+ e.cache_disabled = d.cache_disabled ∧ e.accessed = d.accessed ∧
+ e.dirty = d.dirty ∧ e.huge_page = d.huge_page ∧ e.global = d.global ∧
+ e.no_execute = d.no_execute ∧ e.physical_address = d.physical_address := by
+ open nonos_x_memory_mmu_types_pte_decode in
+ have hd : pagetableentry_from_raw 0#u64 =
+ ok { present := false, writable := false, user_accessible := false,
+ write_through := false, cache_disabled := false, accessed := false,
+ dirty := false, huge_page := false, global := false, no_execute := false,
+ physical_address := 0#u64 } := by
+ unfold pagetableentry_from_raw memory.mmu.types.pte_decode.PageTableEntry.from_raw
+ unfold memory.mmu.constants.pte.PTE_PRESENT memory.mmu.constants.pte.PTE_WRITABLE
+ memory.mmu.constants.pte.PTE_USER memory.mmu.constants.pte.PTE_WRITE_THROUGH
+ memory.mmu.constants.pte.PTE_CACHE_DISABLE memory.mmu.constants.pte.PTE_ACCESSED
+ memory.mmu.constants.pte.PTE_DIRTY memory.mmu.constants.pte.PTE_HUGE_PAGE
+ memory.mmu.constants.pte.PTE_GLOBAL memory.mmu.constants.pte.PTE_NO_EXECUTE
+ memory.mmu.constants.pte.PTE_ADDR_MASK
+ rfl
+ exact ⟨_, _, rfl, hd, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- The empty entry maps nothing: it is not present, not writable, not
+ reachable from user mode, and names physical frame zero. -/
+theorem pagetableentry_empty_maps_nothing :
+ ∃ e, pagetableentry_empty = ok e ∧
+ e.present = false ∧ e.writable = false ∧ e.user_accessible = false ∧
+ e.physical_address.val = 0 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesPte.the_pagetableentry_empty_wrapper_is_its_method
+#print axioms NonosExtraction.TypesPte.pagetableentry_empty_is_the_decoding_of_zero
+#print axioms NonosExtraction.TypesPte.pagetableentry_empty_maps_nothing
end NonosExtraction.TypesPte
diff --git a/verification/extraction/lean/NonosExtraction/TypesRateRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesRateRefinement.lean
index fc81df8677..a242e4bc34 100644
--- a/verification/extraction/lean/NonosExtraction/TypesRateRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesRateRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TypesRate
+import Nonos.TimerLiveness
open Aeneas Aeneas.Std Result
open nonos_x_types_rate
@@ -41,10 +42,88 @@ theorem the_periodicrate_frequency_hz_wrapper_is_its_method (a : rate.PeriodicRa
theorem the_periodicrate_period_us_wrapper_is_its_method (a : rate.PeriodicRate) :
periodicrate_period_us a = rate.PeriodicRate.period_us a := rfl
+/-! ### The rate code, its frequency and its period
+
+ `value` is the code `set_periodic_rate` ORs into the low nibble of CMOS status
+ register A, after clearing that nibble with `RATE_MASK` (0x0F). Every code is
+ below 16, so the write never reaches the divider bits above it, and no two
+ rates share a code, so the code read back names the rate that was set.
+
+ `frequency_hz` agrees with the MC146818 divider chain: code `k` from 3 to 15
+ gives `32768 >> (k - 1)` hertz, and codes 1 and 2 repeat codes 8 and 9 (256
+ and 128 hertz), the aliasing the hardware documents. `period_us` is a million
+ over that frequency, rounded down, for every rate, so the two tables cannot
+ drift apart. The 128 hertz period is the one the tier-one liveness model
+ takes as its heartbeat.
+
+ What these cannot establish: that the port writes in `set_periodic_rate`
+ reach the device, or that `RTC_STATE` records the rate the device runs at.
+ The caller is not extracted, and the lock and CMOS accesses are opaque.
+-/
+
+/-- The rate a register code selects, read the other way. -/
+def rateOfCode : Nat → rate.PeriodicRate
+ | 1 => .Hz256 | 2 => .Hz128 | 3 => .Hz8192 | 4 => .Hz4096 | 5 => .Hz2048
+ | 6 => .Hz1024 | 7 => .Hz512 | 8 => .Hz256_2 | 9 => .Hz128_2 | 10 => .Hz64
+ | 11 => .Hz32 | 12 => .Hz16 | 13 => .Hz8 | 14 => .Hz4 | 15 => .Hz2 | _ => .Disabled
+
+/-- The output of the MC146818 periodic divider for a rate code, in hertz. -/
+def dividerHz (k : Nat) : Nat :=
+ if k = 0 then 0 else if k ≤ 2 then 32768 >>> (k + 6) else 32768 >>> (k - 1)
+
+/-- The code `set_periodic_rate` writes stays inside the four bits `RATE_MASK`
+ clears. -/
+theorem periodicrate_value_fits_under_the_rate_mask (r : rate.PeriodicRate) :
+ ∃ v : Std.U8, periodicrate_value r = ok v ∧ v.val < 16 := by
+ cases r <;> exact ⟨_, rfl, by decide⟩
+
+/-- Each rate writes the code the datasheet gives it, 0 for disabled through 15
+ for two hertz. -/
+theorem periodicrate_value_is_the_datasheet_code (r : rate.PeriodicRate) :
+ ∃ v : Std.U8, periodicrate_value r = ok v ∧ rateOfCode v.val = r := by
+ cases r <;> exact ⟨_, rfl, rfl⟩
+
+theorem periodicrate_value_is_injective (a b : rate.PeriodicRate) (v : Std.U8)
+ (ha : periodicrate_value a = ok v) (hb : periodicrate_value b = ok v) : a = b := by
+ obtain ⟨va, hva, ea⟩ := periodicrate_value_is_the_datasheet_code a
+ obtain ⟨vb, hvb, eb⟩ := periodicrate_value_is_the_datasheet_code b
+ rw [hva, ok.injEq] at ha
+ rw [hvb, ok.injEq] at hb
+ subst ha hb
+ rw [← ea, ← eb]
+
+theorem periodicrate_frequency_hz_is_the_divider_output (r : rate.PeriodicRate) :
+ ∃ v f, periodicrate_value r = ok v ∧ periodicrate_frequency_hz r = ok f ∧
+ f.val = dividerHz v.val := by
+ cases r <;> exact ⟨_, _, rfl, rfl, by decide⟩
+
+/-- Zero hertz is exactly the disabled rate: every other code ticks. -/
+theorem periodicrate_frequency_hz_is_zero_only_when_disabled (r : rate.PeriodicRate) :
+ ∃ f, periodicrate_frequency_hz r = ok f ∧ (f.val = 0 ↔ r = .Disabled) := by
+ cases r <;> exact ⟨_, rfl, by simp⟩
+
+theorem periodicrate_period_us_is_a_million_over_the_frequency (r : rate.PeriodicRate) :
+ ∃ f p, periodicrate_frequency_hz r = ok f ∧ periodicrate_period_us r = ok p ∧
+ p.val = 1000000 / f.val := by
+ cases r <;> exact ⟨_, _, rfl, rfl, by decide⟩
+
+/-- Both 128 hertz codes give the heartbeat period `Nonos.TimerLiveness` assumes. -/
+theorem periodicrate_period_us_at_128_hz_is_the_liveness_heartbeat :
+ periodicrate_period_us .Hz128 = ok ⟨Nonos.TimerLiveness.rtcHeartbeat.period⟩ ∧
+ periodicrate_period_us .Hz128_2 = ok ⟨Nonos.TimerLiveness.rtcHeartbeat.period⟩ := by
+ exact ⟨rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesRate.the_periodicrate_value_wrapper_is_its_method
#print axioms NonosExtraction.TypesRate.the_periodicrate_frequency_hz_wrapper_is_its_method
#print axioms NonosExtraction.TypesRate.the_periodicrate_period_us_wrapper_is_its_method
+#print axioms NonosExtraction.TypesRate.periodicrate_value_fits_under_the_rate_mask
+#print axioms NonosExtraction.TypesRate.periodicrate_value_is_the_datasheet_code
+#print axioms NonosExtraction.TypesRate.periodicrate_value_is_injective
+#print axioms NonosExtraction.TypesRate.periodicrate_frequency_hz_is_the_divider_output
+#print axioms NonosExtraction.TypesRate.periodicrate_frequency_hz_is_zero_only_when_disabled
+#print axioms NonosExtraction.TypesRate.periodicrate_period_us_is_a_million_over_the_frequency
+#print axioms NonosExtraction.TypesRate.periodicrate_period_us_at_128_hz_is_the_liveness_heartbeat
end NonosExtraction.TypesRate
diff --git a/verification/extraction/lean/NonosExtraction/TypesRegionStatsRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesRegionStatsRefinement.lean
index ed97caf407..549209ee38 100644
--- a/verification/extraction/lean/NonosExtraction/TypesRegionStatsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesRegionStatsRefinement.lean
@@ -35,8 +35,48 @@ namespace NonosExtraction.TypesRegionStats
theorem the_regionstats_free_memory_wrapper_is_its_method (a : region_stats.RegionStats) :
regionstats_free_memory a = region_stats.RegionStats.free_memory a := rfl
+/-! ### Free memory is a saturating difference
+
+`free_memory` reports available memory less allocated memory, and reports zero
+instead of failing when more is allocated than is available. The theorems below
+fix the value on both sides of that comparison, show the query never fails on
+any statistics record, and show the equal case lands on zero. They say nothing
+about whether the counters in a record were filled consistently; the code that
+accumulates them walks the boot memory map and is not extracted. -/
+
+/-- Free memory is the plain difference when available memory exceeds allocated
+ memory, and zero otherwise; the query never fails. -/
+theorem regionstats_free_memory_is_the_saturating_difference
+ (s : region_stats.RegionStats) :
+ ∃ r, regionstats_free_memory s = ok r ∧
+ r.val = if s.allocated_memory.val < s.available_memory.val
+ then s.available_memory.val - s.allocated_memory.val else 0 := by
+ unfold regionstats_free_memory region_stats.RegionStats.free_memory
+ by_cases h : s.allocated_memory.val < s.available_memory.val
+ · have hgt : s.available_memory > s.allocated_memory := h
+ simp only [hgt, if_true, h]
+ have ⟨z, hz, hv⟩ := WP.spec_imp_exists
+ (U64.sub_spec (x := s.available_memory) (y := s.allocated_memory) (by scalar_tac))
+ exact ⟨z, hz, hv.1⟩
+ · have hgt : ¬ (s.available_memory > s.allocated_memory) := h
+ simp only [hgt, if_false, h]
+ exact ⟨_, rfl, rfl⟩
+
+/-- Over-committed statistics, where more is allocated than is available,
+ report zero free memory rather than wrapping to a huge value. -/
+theorem regionstats_free_memory_is_zero_when_allocation_meets_availability
+ (s : region_stats.RegionStats)
+ (h : s.available_memory.val ≤ s.allocated_memory.val) :
+ regionstats_free_memory s = ok 0#u64 := by
+ unfold regionstats_free_memory region_stats.RegionStats.free_memory
+ have hgt : ¬ (s.available_memory > s.allocated_memory) := by
+ show ¬ (s.allocated_memory.val < s.available_memory.val); omega
+ simp only [hgt, if_false]
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesRegionStats.the_regionstats_free_memory_wrapper_is_its_method
+#print axioms NonosExtraction.TypesRegionStats.regionstats_free_memory_is_the_saturating_difference
+#print axioms NonosExtraction.TypesRegionStats.regionstats_free_memory_is_zero_when_allocation_meets_availability
end NonosExtraction.TypesRegionStats
diff --git a/verification/extraction/lean/NonosExtraction/TypesSnapshotRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesSnapshotRefinement.lean
index 0d7460353b..a4f531a1e2 100644
--- a/verification/extraction/lean/NonosExtraction/TypesSnapshotRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesSnapshotRefinement.lean
@@ -35,8 +35,27 @@ namespace NonosExtraction.TypesSnapshot
theorem the_dmastatssnapshot_new_wrapper_is_its_method :
dmastatssnapshot_new = snapshot.DmaStatsSnapshot.new := rfl
+/-! ### A fresh snapshot reports no DMA activity
+
+The theorem below shows that `dmastatssnapshot_new` never fails and reports
+zero for every counter: no coherent allocations, no streaming mappings, no
+bounce-buffer use, no DMA memory and no operations. The live snapshot the
+kernel returns from `get_stats` is read from atomic counters instead, which
+Aeneas leaves opaque, so nothing here speaks to it.
+-/
+
+/-- Every counter of a fresh snapshot is zero, so their sum is zero. A version
+ that seeded any counter with a non-zero value would report DMA activity
+ that never happened. -/
+theorem dmastatssnapshot_new_reports_no_activity :
+ ∃ s, dmastatssnapshot_new = ok s ∧
+ s.coherent_allocations.val + s.streaming_mappings.val +
+ s.bounce_buffer_usage.val + s.total_dma_memory.val + s.dma_operations.val = 0 :=
+ ⟨_, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesSnapshot.the_dmastatssnapshot_new_wrapper_is_its_method
+#print axioms NonosExtraction.TypesSnapshot.dmastatssnapshot_new_reports_no_activity
end NonosExtraction.TypesSnapshot
diff --git a/verification/extraction/lean/NonosExtraction/TypesStackRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesStackRefinement.lean
index d3a8893d21..1d436ac06e 100644
--- a/verification/extraction/lean/NonosExtraction/TypesStackRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesStackRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.TypesStack
+import NonosExtraction.LayoutStackSlotsRefinement
open Aeneas Aeneas.Std Result
open nonos_x_types_stack
@@ -55,7 +56,7 @@ never reported as a wrapped small number. The guard never enters `stack_top`.
These theorems are about the five functions as extracted. They cannot see the
callers that build regions, `get_all_stack_regions` in
`layout/manager/percpu.rs` and `get_guard_regions` in
-`safety/manager/guards.rs`, which are not extracted; the last theorem records
+`safety/manager/guards.rs`, which are not extracted; the last two theorems say
what `stack_top` implies for the layout those callers produce.
-/
@@ -202,13 +203,10 @@ theorem stackregion_total_size_refuses_a_footprint_past_usize_max
| fail e => exact ⟨e, rfl⟩
| div => rw [hr] at ha; exact ha.elim
-/-- Records a layout defect that `stack_top` makes visible. `get_all_stack_regions`
-places each CPU's first IST stack at `stack_base + KSTACK_SIZE` (64 KiB), with a
-one page guard, and `get_guard_regions` takes a region's upper guard to start at
-its `stack_top`. For every per CPU kernel stack built that way, the top is exactly
-the base of the IST stack above it, so the kernel stack's upper guard page is the
-IST stack's first page and there is no unmapped page between the two. -/
-theorem stackregion_stack_top_of_a_kernel_stack_is_the_next_ist_base
+/-- A per CPU kernel stack, 64 KiB with a one page guard, tops out exactly
+64 KiB above its base, and `get_guard_regions` takes its upper guard to start
+there. -/
+theorem stackregion_stack_top_of_a_kernel_stack_is_its_base_plus_64_kib
(b : Std.U64) (c : Std.U32) (hb : b.val + 0x10000 ≤ U64.max) :
∃ k t, stackregion_per_cpu b 0x10000#usize 0x1000#usize c = ok k ∧
stackregion_stack_top k = ok t ∧ t.val = b.val + 0x10000 := by
@@ -216,6 +214,34 @@ theorem stackregion_stack_top_of_a_kernel_stack_is_the_next_ist_base
⟨b, 0x10000#usize, 0x1000#usize, some c, none⟩ (by simpa using hb)
exact ⟨_, t, rfl, ht, by simpa using hv⟩
+/-- The kernel stack's upper guard page is unmapped: with the kernel stack at
+slot 0 of a CPU's stack area and the first IST stack at slot 1, as
+`get_all_stack_regions` places them through `stack_slot_offset`, the kernel
+stack's top lies exactly one page below the IST stack's base. The layout used
+to put the first IST stack at `stack_base + KSTACK_SIZE`, which is the kernel
+stack's top, so the page `get_guard_regions` treated as the kernel stack's
+upper guard was the IST stack's first page. -/
+theorem a_kernel_stack_ends_one_guard_page_below_the_first_ist_stack
+ (area : Std.U64) (c : Std.U32) (ha : area.val + 73728 ≤ U64.max) :
+ ∃ o0 o1 : Std.U64,
+ nonos_x_layout_stack_slots.stack_slot_offset 0#usize = ok o0 ∧
+ nonos_x_layout_stack_slots.stack_slot_offset 1#usize = ok o1 ∧
+ ∀ b : Std.U64, b.val = area.val + o0.val →
+ ∃ k t, stackregion_per_cpu b 0x10000#usize 0x1000#usize c = ok k ∧
+ stackregion_stack_top k = ok t ∧ t.val + 4096 = area.val + o1.val := by
+ obtain ⟨o0, h0, hv0⟩ := WP.spec_imp_exists
+ (NonosExtraction.LayoutStackSlots.stack_slot_offset_spec 0#usize (by decide))
+ obtain ⟨o1, h1, hv1⟩ := WP.spec_imp_exists
+ (NonosExtraction.LayoutStackSlots.stack_slot_offset_spec 1#usize (by decide))
+ have e0 : (0#usize : Std.Usize).val = 0 := rfl
+ have e1 : (1#usize : Std.Usize).val = 1 := rfl
+ rw [e0, if_pos rfl] at hv0
+ rw [e1, if_neg (by decide)] at hv1
+ refine ⟨o0, o1, h0, h1, fun b hb => ?_⟩
+ obtain ⟨k, t, hk, ht, htv⟩ :=
+ stackregion_stack_top_of_a_kernel_stack_is_its_base_plus_64_kib b c (by omega)
+ exact ⟨k, t, hk, ht, by omega⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesStack.the_stackregion_new_wrapper_is_its_method
@@ -230,6 +256,7 @@ theorem stackregion_stack_top_of_a_kernel_stack_is_the_next_ist_base
#print axioms NonosExtraction.TypesStack.stackregion_stack_top_refuses_a_stack_ending_at_two_to_the_64
#print axioms NonosExtraction.TypesStack.stackregion_total_size_is_size_plus_one_guard_when_it_fits
#print axioms NonosExtraction.TypesStack.stackregion_total_size_refuses_a_footprint_past_usize_max
-#print axioms NonosExtraction.TypesStack.stackregion_stack_top_of_a_kernel_stack_is_the_next_ist_base
+#print axioms NonosExtraction.TypesStack.stackregion_stack_top_of_a_kernel_stack_is_its_base_plus_64_kib
+#print axioms NonosExtraction.TypesStack.a_kernel_stack_ends_one_guard_page_below_the_first_ist_stack
end NonosExtraction.TypesStack
diff --git a/verification/extraction/lean/NonosExtraction/TypesStatsSnapshot.lean b/verification/extraction/lean/NonosExtraction/TypesStatsSnapshot.lean
index 9af9ce179b..affd02e39f 100644
--- a/verification/extraction/lean/NonosExtraction/TypesStatsSnapshot.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesStatsSnapshot.lean
@@ -37,11 +37,11 @@ def stats_snapshot.MmioStatsSnapshot.new
}
/-- [nonos_x_types_stats_snapshot::stats_snapshot::{nonos_x_types_stats_snapshot::stats_snapshot::MmioStatsSnapshot}::total_operations]:
- Source: 'src/../../../../../src/memory/mmio/types/stats_snapshot.rs', lines 30:4-32:5
+ Source: 'src/../../../../../src/memory/mmio/types/stats_snapshot.rs', lines 31:4-33:5
Visibility: public -/
def stats_snapshot.MmioStatsSnapshot.total_operations
(self : stats_snapshot.MmioStatsSnapshot) : Result Std.U64 := do
- self.read_operations + self.write_operations
+ ok (core.num.U64.saturating_add self.read_operations self.write_operations)
/-- [nonos_x_types_stats_snapshot::mmiostatssnapshot_new]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/TypesStatsSnapshotRefinement.lean b/verification/extraction/lean/NonosExtraction/TypesStatsSnapshotRefinement.lean
index 50bd4eafb8..a6e3f8232a 100644
--- a/verification/extraction/lean/NonosExtraction/TypesStatsSnapshotRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/TypesStatsSnapshotRefinement.lean
@@ -38,9 +38,66 @@ theorem the_mmiostatssnapshot_new_wrapper_is_its_method :
theorem the_mmiostatssnapshot_total_operations_wrapper_is_its_method (a : stats_snapshot.MmioStatsSnapshot) :
mmiostatssnapshot_total_operations a = stats_snapshot.MmioStatsSnapshot.total_operations a := rfl
+/-! ### A fresh snapshot counts nothing, and the total saturates
+
+`new` is the snapshot the kernel reports before any region is mapped, so every
+counter in it is zero and its operation total is zero. `total_operations` adds
+the read and write counters saturating at `u64::MAX`, so it never fails: it
+returns their exact sum whenever that sum fits in a `u64` and `u64::MAX`
+otherwise. It used Rust's checked `+`, and with `overflow-checks = true` in
+every profile a sum past the limit halted the kernel.
+
+These theorems say nothing about how the counters get their values: they are
+loaded from `AtomicU64` fields in `src/memory/mmio/stats`, which bump each
+counter with a wrapping `fetch_add`, and that code is not extracted. Nor do they
+say the overflow is reachable in practice; it needs more than 2^63 recorded
+operations on at least one counter. -/
+
+/-- Every field of a fresh snapshot is zero. -/
+theorem mmiostatssnapshot_new_is_all_zero :
+ ∃ s, mmiostatssnapshot_new = ok s ∧
+ s.total_regions.val = 0 ∧ s.total_mapped_size.val = 0 ∧
+ s.read_operations.val = 0 ∧ s.write_operations.val = 0 :=
+ ⟨_, rfl, rfl, rfl, rfl, rfl⟩
+
+/-- The total never fails and is reads plus writes, saturated at `u64::MAX`. -/
+theorem mmiostatssnapshot_total_operations_is_the_saturated_sum
+ (s : stats_snapshot.MmioStatsSnapshot) :
+ ∃ z, mmiostatssnapshot_total_operations s = ok z ∧
+ z.val = min (2 ^ 64 - 1) (s.read_operations.val + s.write_operations.val) := by
+ unfold mmiostatssnapshot_total_operations stats_snapshot.MmioStatsSnapshot.total_operations
+ refine ⟨_, rfl, ?_⟩
+ simp only [core.num.U64.saturating_add, UScalar.saturating_add, UScalar.val, UScalar.max]
+ rw [BitVec.toNat_ofNat]
+ show min (2 ^ 64 - 1) _ % 2 ^ 64 = _
+ exact Nat.mod_eq_of_lt (by omega)
+
+/-- The two functions agree on the starting point: a fresh snapshot has
+ performed zero operations. -/
+theorem a_fresh_snapshot_has_zero_mmiostatssnapshot_total_operations :
+ (do let s ← mmiostatssnapshot_new; mmiostatssnapshot_total_operations s)
+ = ok 0#u64 := by
+ obtain ⟨s, hs, -, -, hr, hw⟩ := mmiostatssnapshot_new_is_all_zero
+ rw [hs, bind_tc_ok]
+ obtain ⟨z, hz, hv⟩ := mmiostatssnapshot_total_operations_is_the_saturated_sum s
+ rw [hz]; congr 1; apply UScalar.eq_of_val_eq; rw [hv, hr, hw]; rfl
+
+/-- `u64::MAX` reads and one write, the first sum past the limit, total
+ `u64::MAX` rather than halting. -/
+theorem mmiostatssnapshot_total_operations_saturates_past_the_u64_limit :
+ mmiostatssnapshot_total_operations
+ ⟨0#usize, 0#u64, 0xFFFFFFFFFFFFFFFF#u64, 1#u64⟩ = ok 0xFFFFFFFFFFFFFFFF#u64 := by
+ obtain ⟨z, hz, hv⟩ := mmiostatssnapshot_total_operations_is_the_saturated_sum
+ ⟨0#usize, 0#u64, 0xFFFFFFFFFFFFFFFF#u64, 1#u64⟩
+ rw [hz]; congr 1; apply UScalar.eq_of_val_eq; rw [hv]; rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.TypesStatsSnapshot.the_mmiostatssnapshot_new_wrapper_is_its_method
#print axioms NonosExtraction.TypesStatsSnapshot.the_mmiostatssnapshot_total_operations_wrapper_is_its_method
+#print axioms NonosExtraction.TypesStatsSnapshot.mmiostatssnapshot_new_is_all_zero
+#print axioms NonosExtraction.TypesStatsSnapshot.a_fresh_snapshot_has_zero_mmiostatssnapshot_total_operations
+#print axioms NonosExtraction.TypesStatsSnapshot.mmiostatssnapshot_total_operations_is_the_saturated_sum
+#print axioms NonosExtraction.TypesStatsSnapshot.mmiostatssnapshot_total_operations_saturates_past_the_u64_limit
end NonosExtraction.TypesStatsSnapshot
diff --git a/verification/extraction/lean/NonosExtraction/Uefi.lean b/verification/extraction/lean/NonosExtraction/Uefi.lean
index 5995e6c663..6242628b7e 100644
--- a/verification/extraction/lean/NonosExtraction/Uefi.lean
+++ b/verification/extraction/lean/NonosExtraction/Uefi.lean
@@ -36,6 +36,14 @@ def attributes.VariableAttributes.RUNTIME_ACCESS
: attributes.VariableAttributes :=
4#u32
+/-- [nonos_uefi_attrs::attributes::{nonos_uefi_attrs::attributes::VariableAttributes}::AUTHENTICATED_WRITE_ACCESS]
+ Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 28:4-28:66
+ Visibility: public -/
+@[global_simps, irreducible]
+def attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
+ : attributes.VariableAttributes :=
+ 16#u32
+
/-- [nonos_uefi_attrs::attributes::{nonos_uefi_attrs::attributes::VariableAttributes}::TIME_BASED_AUTHENTICATED_WRITE_ACCESS]
Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 29:4-29:77
Visibility: public -/
@@ -116,21 +124,27 @@ def attributes.VariableAttributes.is_runtime_access
attributes.VariableAttributes.RUNTIME_ACCESS
/-- [nonos_uefi_attrs::attributes::{nonos_uefi_attrs::attributes::VariableAttributes}::requires_authentication]:
- Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 76:4-79:5
+ Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 78:4-82:5
Visibility: public -/
def attributes.VariableAttributes.requires_authentication
(self : attributes.VariableAttributes) : Result Bool := do
let b ←
attributes.VariableAttributes.contains self
- attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
+ attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
if b
then ok true
else
- attributes.VariableAttributes.contains self
- attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
+ let b1 ←
+ attributes.VariableAttributes.contains self
+ attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
+ if b1
+ then ok true
+ else
+ attributes.VariableAttributes.contains self
+ attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
/-- [nonos_uefi_attrs::attributes::{nonos_uefi_attrs::attributes::VariableAttributes}::intersection]:
- Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 106:4-108:5
+ Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 109:4-111:5
Visibility: public -/
def attributes.VariableAttributes.intersection
(self : attributes.VariableAttributes)
@@ -141,7 +155,7 @@ def attributes.VariableAttributes.intersection
ok i
/-- [nonos_uefi_attrs::attributes::{nonos_uefi_attrs::attributes::VariableAttributes}::union]:
- Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 111:4-113:5
+ Source: 'src/../../../../src/arch/x86_64/uefi/types/attributes.rs', lines 114:4-116:5
Visibility: public -/
def attributes.VariableAttributes.union
(self : attributes.VariableAttributes)
diff --git a/verification/extraction/lean/NonosExtraction/UefiRefinement.lean b/verification/extraction/lean/NonosExtraction/UefiRefinement.lean
index 052a046293..cb138350d8 100644
--- a/verification/extraction/lean/NonosExtraction/UefiRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/UefiRefinement.lean
@@ -156,14 +156,16 @@ theorem runtime_access_is_bit_two (a : Std.U32) :
unfold isRuntimeAccess holds attributes.VariableAttributes.RUNTIME_ACCESS
simp only [Std.lift, bind_tc_ok]
-/-- Authentication is required when either authenticated-write flag is present,
- and the witnesses cover each one alone, both together, and neither. -/
-theorem authentication_is_either_flag :
- needsAuth 0x20#u32 = ok true ∧ needsAuth 0x80#u32 = ok true ∧
- needsAuth 0xA0#u32 = ok true ∧ needsAuth 0x00#u32 = ok false ∧
- needsAuth 0x5F#u32 = ok false := by
- refine ⟨?_, ?_, ?_, ?_, ?_⟩ <;>
- (unfold needsAuth holds attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
+/-- Authentication is required when any authenticated-write flag is present,
+ and the witnesses cover each one alone, all together, and none: `0x4F`
+ carries every other defined flag. -/
+theorem authentication_is_any_authenticated_flag :
+ needsAuth 0x10#u32 = ok true ∧ needsAuth 0x20#u32 = ok true ∧
+ needsAuth 0x80#u32 = ok true ∧ needsAuth 0xB0#u32 = ok true ∧
+ needsAuth 0x00#u32 = ok false ∧ needsAuth 0x4F#u32 = ok false := by
+ refine ⟨?_, ?_, ?_, ?_, ?_, ?_⟩ <;>
+ (unfold needsAuth holds attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
+ attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
simp only [Std.lift, bind_tc_ok]
rfl)
@@ -339,31 +341,35 @@ theorem runtime_access_is_bit_two_of_the_word (a : Std.U32) :
simp only [Std.lift, bind_tc_ok]
rw [Bits.reads_bit_eq a 4#u32 2 rfl]
-/-- Authentication is required exactly when bit 5
- (`TIME_BASED_AUTHENTICATED_WRITE_ACCESS`) or bit 7
- (`ENHANCED_AUTHENTICATED_ACCESS`) is set, for every word. This replaces the
- five witnesses of `authentication_is_either_flag` with the closed form. -/
-theorem authentication_is_bit_five_or_bit_seven (a : Std.U32) :
+/-- Authentication is required exactly when bit 4
+ (`AUTHENTICATED_WRITE_ACCESS`), bit 5 (`TIME_BASED_AUTHENTICATED_WRITE_ACCESS`)
+ or bit 7 (`ENHANCED_AUTHENTICATED_ACCESS`) is set, for every word. This
+ replaces the witnesses of `authentication_is_any_authenticated_flag` with the
+ closed form. -/
+theorem authentication_is_bit_four_five_or_seven (a : Std.U32) :
variableattributes_requires_authentication a =
- ok (a.val.testBit 5 || a.val.testBit 7) := by
+ ok (a.val.testBit 4 || a.val.testBit 5 || a.val.testBit 7) := by
unfold variableattributes_requires_authentication needsAuth holds
+ attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
simp only [Std.lift, bind_tc_ok]
- rw [Bits.reads_bit_eq a 32#u32 5 rfl, Bits.reads_bit_eq a 128#u32 7 rfl]
- cases a.val.testBit 5 <;> rfl
-
-/-- Records a gap, or at least a choice that should be visible. The older
- counter-based flag, `AUTHENTICATED_WRITE_ACCESS` (bit 4, `0x10`), is not
- counted: a set carrying only it is answered as needing no authentication,
- while the same set with bit 5 added is answered as needing it. UEFI 2.3.1
- deprecates that flag, so this may be intended, but firmware can still report
- it on an existing variable. No kernel caller asks `requires_authentication`
- today. -/
-theorem the_counter_based_authenticated_write_flag_is_not_counted :
- variableattributes_requires_authentication 0x10#u32 = ok false ∧
+ rw [Bits.reads_bit_eq a 16#u32 4 rfl, Bits.reads_bit_eq a 32#u32 5 rfl,
+ Bits.reads_bit_eq a 128#u32 7 rfl]
+ cases a.val.testBit 4 <;> cases a.val.testBit 5 <;> rfl
+
+/-- The older count-based flag, `AUTHENTICATED_WRITE_ACCESS` (bit 4, `0x10`),
+ counts on its own. It used not to: a set carrying only it was answered as
+ needing no authentication, while the same set with bit 5 added was answered
+ as needing it. UEFI 2.3.1 deprecates the flag, but firmware still reports it
+ on variables written before, and a write to one still has to carry an
+ authentication descriptor. `kernel_proofs::uefi_attrs` fails against the
+ old code. -/
+theorem the_count_based_authenticated_write_flag_counts :
+ variableattributes_requires_authentication 0x10#u32 = ok true ∧
variableattributes_requires_authentication 0x30#u32 = ok true := by
unfold variableattributes_requires_authentication needsAuth holds
+ attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
exact ⟨rfl, rfl⟩
@@ -400,7 +406,7 @@ theorem the_empty_set_is_the_identity_for_union_and_holds_no_flag (a : Std.U32)
variableattributes_requires_authentication e) = ok false := by
unfold variableattributes_empty none' variableattributes_union join
simp only [Std.lift, bind_tc_ok, non_volatility_is_bit_zero_of_the_word,
- runtime_access_is_bit_two_of_the_word, authentication_is_bit_five_or_bit_seven]
+ runtime_access_is_bit_two_of_the_word, authentication_is_bit_four_five_or_seven]
refine ⟨?_, rfl, rfl, rfl, rfl⟩
congr 1
apply UScalar.eq_of_val_eq
@@ -488,7 +494,7 @@ theorem truncation_changes_no_named_predicate (w : Std.U32) :
have hw : (w &&& 255#u32).val = w.val % 2 ^ 8 := Bits.land_low_mask w 255#u32 8 rfl
unfold variableattributes_from_bits_truncate ofBitsTruncated
simp only [Std.lift, bind_tc_ok, non_volatility_is_bit_zero_of_the_word,
- runtime_access_is_bit_two_of_the_word, authentication_is_bit_five_or_bit_seven, hw,
+ runtime_access_is_bit_two_of_the_word, authentication_is_bit_four_five_or_seven, hw,
Nat.testBit_mod_two_pow]
simp
@@ -536,13 +542,14 @@ theorem authentication_survives_union_but_not_intersection (a b : Std.U32) :
variableattributes_requires_authentication m) = ok false) := by
refine ⟨?_, ?_, ?_, ?_⟩
· unfold variableattributes_union join
- simp only [Std.lift, bind_tc_ok, authentication_is_bit_five_or_bit_seven, UScalar.val_or,
+ simp only [Std.lift, bind_tc_ok, authentication_is_bit_four_five_or_seven, UScalar.val_or,
Nat.testBit_or, ok.injEq]
- cases a.val.testBit 5 <;> cases b.val.testBit 5 <;> cases a.val.testBit 7 <;>
- cases b.val.testBit 7 <;> rfl
+ cases a.val.testBit 4 <;> cases b.val.testBit 4 <;> cases a.val.testBit 5 <;>
+ cases b.val.testBit 5 <;> cases a.val.testBit 7 <;> cases b.val.testBit 7 <;> rfl
all_goals
try unfold variableattributes_intersection meet
unfold variableattributes_requires_authentication needsAuth holds
+ attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
rfl
@@ -573,6 +580,7 @@ theorem the_default_word_contradicts_the_firmware_words :
variableattributes_is_runtime_access isRuntimeAccess
variableattributes_requires_authentication needsAuth holds
attributes.VariableAttributes.NON_VOLATILE attributes.VariableAttributes.RUNTIME_ACCESS
+ attributes.VariableAttributes.AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.TIME_BASED_AUTHENTICATED_WRITE_ACCESS
attributes.VariableAttributes.ENHANCED_AUTHENTICATED_ACCESS
exact ⟨rfl, rfl, rfl⟩
@@ -606,7 +614,7 @@ theorem the_default_word_contradicts_the_firmware_words :
#print axioms NonosExtraction.Uefi.from_bits_admits_undefined_flags
#print axioms NonosExtraction.Uefi.non_volatile_is_bit_zero
#print axioms NonosExtraction.Uefi.runtime_access_is_bit_two
-#print axioms NonosExtraction.Uefi.authentication_is_either_flag
+#print axioms NonosExtraction.Uefi.authentication_is_any_authenticated_flag
#print axioms NonosExtraction.Uefi.words_are_equal_when_their_bits_are
#print axioms NonosExtraction.Uefi.carrying_a_mask_is_carrying_its_bits
#print axioms NonosExtraction.Uefi.holding_a_set_is_holding_each_of_its_bits
@@ -615,8 +623,8 @@ theorem the_default_word_contradicts_the_firmware_words :
#print axioms NonosExtraction.Uefi.the_intersection_contains_exactly_what_both_contain
#print axioms NonosExtraction.Uefi.non_volatility_is_bit_zero_of_the_word
#print axioms NonosExtraction.Uefi.runtime_access_is_bit_two_of_the_word
-#print axioms NonosExtraction.Uefi.authentication_is_bit_five_or_bit_seven
-#print axioms NonosExtraction.Uefi.the_counter_based_authenticated_write_flag_is_not_counted
+#print axioms NonosExtraction.Uefi.authentication_is_bit_four_five_or_seven
+#print axioms NonosExtraction.Uefi.the_count_based_authenticated_write_flag_counts
#print axioms NonosExtraction.Uefi.a_set_is_empty_exactly_when_every_set_contains_it
#print axioms NonosExtraction.Uefi.the_empty_set_is_the_identity_for_union_and_holds_no_flag
#print axioms NonosExtraction.Uefi.an_empty_set_reaches_firmware_as_the_zero_word
diff --git a/verification/extraction/lean/NonosExtraction/UefiSecureBootStatusRefinement.lean b/verification/extraction/lean/NonosExtraction/UefiSecureBootStatusRefinement.lean
index fac20cf0db..1f55e23688 100644
--- a/verification/extraction/lean/NonosExtraction/UefiSecureBootStatusRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/UefiSecureBootStatusRefinement.lean
@@ -38,9 +38,85 @@ theorem the_securebootstatus_is_fully_configured_wrapper_is_its_method (a : secu
theorem the_securebootstatus_can_modify_keys_wrapper_is_its_method (a : secure_boot_status.SecureBootStatus) :
securebootstatus_can_modify_keys a = secure_boot_status.SecureBootStatus.can_modify_keys a := rfl
+/-! ### What fully configured requires, and what key modification reads
+
+ `securebootstatus_is_fully_configured` holds exactly when secure boot is
+ enabled and the platform key, the key exchange key and the signature
+ database are all present. Each of the four is necessary on its own: a status
+ missing any one of them is refused whatever the other fields hold. The
+ revocation database is not among them, and neither are the entry counts, so
+ a status with no `dbx` and an empty `db` still reports fully configured.
+ `securebootstatus_can_modify_keys` is the setup mode flag and nothing else:
+ it does not look at whether secure boot is enabled or a platform key is
+ present. The two can both hold, so the status type itself does not encode
+ the UEFI rule that a platform in setup mode has no platform key; that rule
+ lives in whatever fills the fields. These theorems cannot establish anything
+ about secure_boot_ops::get_status, which reads the fields from firmware
+ variables through the UEFI manager and is not extracted.
+-/
+
+/-- Fully configured is the conjunction of enabled, PK, KEK and db, independent
+ of setup mode, dbx and the entry counts. -/
+theorem securebootstatus_is_fully_configured_is_enabled_with_pk_kek_and_db
+ (s : secure_boot_status.SecureBootStatus) :
+ securebootstatus_is_fully_configured s =
+ ok (s.enabled && s.has_pk && s.has_kek && s.has_db) := by
+ unfold securebootstatus_is_fully_configured
+ secure_boot_status.SecureBootStatus.is_fully_configured
+ cases s.enabled <;> cases s.has_pk <;> cases s.has_kek <;> simp
+
+/-- Dropping any one of the four requirements makes the status not fully
+ configured, so none of the checks is redundant. -/
+theorem securebootstatus_is_fully_configured_needs_each_of_the_four
+ (s : secure_boot_status.SecureBootStatus)
+ (h : s.enabled = false ∨ s.has_pk = false ∨ s.has_kek = false ∨ s.has_db = false) :
+ securebootstatus_is_fully_configured s = ok false := by
+ rw [securebootstatus_is_fully_configured_is_enabled_with_pk_kek_and_db]
+ rcases h with h | h | h | h <;> simp [h]
+
+/-- A status with no revocation database and no signature entries still reports
+ fully configured. -/
+theorem securebootstatus_is_fully_configured_ignores_dbx_and_entry_counts :
+ securebootstatus_is_fully_configured
+ { enabled := true, setup_mode := false, has_pk := true, has_kek := true,
+ has_db := true, has_dbx := false, db_entry_count := 0#usize,
+ dbx_entry_count := 0#usize } = ok true := rfl
+
+/-- Key modification is permitted exactly in setup mode, whatever else the
+ status says. -/
+theorem securebootstatus_can_modify_keys_is_setup_mode
+ (s : secure_boot_status.SecureBootStatus) :
+ securebootstatus_can_modify_keys s = ok s.setup_mode := rfl
+
+/-- A status can report both fully configured and key modification permitted:
+ the type does not exclude setup mode alongside a platform key. -/
+theorem securebootstatus_can_modify_keys_while_fully_configured :
+ ∃ s : secure_boot_status.SecureBootStatus,
+ securebootstatus_is_fully_configured s = ok true ∧
+ securebootstatus_can_modify_keys s = ok true :=
+ ⟨{ enabled := true, setup_mode := true, has_pk := true, has_kek := true,
+ has_db := true, has_dbx := true, db_entry_count := 1#usize,
+ dbx_entry_count := 1#usize }, rfl, rfl⟩
+
+/-- The kernel's default status (secure boot off, setup mode on, no keys) is not
+ fully configured and permits key modification. -/
+theorem the_default_status_is_not_fully_configured_and_securebootstatus_can_modify_keys :
+ let d : secure_boot_status.SecureBootStatus :=
+ { enabled := false, setup_mode := true, has_pk := false, has_kek := false,
+ has_db := false, has_dbx := false, db_entry_count := 0#usize,
+ dbx_entry_count := 0#usize }
+ securebootstatus_is_fully_configured d = ok false ∧
+ securebootstatus_can_modify_keys d = ok true := ⟨rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.UefiSecureBootStatus.the_securebootstatus_is_fully_configured_wrapper_is_its_method
#print axioms NonosExtraction.UefiSecureBootStatus.the_securebootstatus_can_modify_keys_wrapper_is_its_method
+#print axioms NonosExtraction.UefiSecureBootStatus.securebootstatus_is_fully_configured_is_enabled_with_pk_kek_and_db
+#print axioms NonosExtraction.UefiSecureBootStatus.securebootstatus_is_fully_configured_needs_each_of_the_four
+#print axioms NonosExtraction.UefiSecureBootStatus.securebootstatus_is_fully_configured_ignores_dbx_and_entry_counts
+#print axioms NonosExtraction.UefiSecureBootStatus.securebootstatus_can_modify_keys_is_setup_mode
+#print axioms NonosExtraction.UefiSecureBootStatus.securebootstatus_can_modify_keys_while_fully_configured
+#print axioms NonosExtraction.UefiSecureBootStatus.the_default_status_is_not_fully_configured_and_securebootstatus_can_modify_keys
end NonosExtraction.UefiSecureBootStatus
diff --git a/verification/extraction/lean/NonosExtraction/UtilsTypes.lean b/verification/extraction/lean/NonosExtraction/UtilsTypes.lean
index 344b8d9d0a..9354c36e64 100644
--- a/verification/extraction/lean/NonosExtraction/UtilsTypes.lean
+++ b/verification/extraction/lean/NonosExtraction/UtilsTypes.lean
@@ -37,10 +37,11 @@ inductive types.SensitivityLevel where
| Critical : types.SensitivityLevel
/-- [nonos_x_utils_types::types::ScanConfig]
- Source: 'src/../../../../../src/fs/utils/types.rs', lines 86:0-94:1
+ Source: 'src/../../../../../src/fs/utils/types.rs', lines 86:0-95:1
Visibility: public -/
structure types.ScanConfig where
include_hidden : Bool
+ only_hidden : Bool
max_depth : Std.Usize
max_files : Std.Usize
extensions : alloc.vec.Vec String
@@ -49,12 +50,13 @@ structure types.ScanConfig where
follow_symlinks : Bool
/-- [nonos_x_utils_types::types::{nonos_x_utils_types::types::ScanConfig}::new]:
- Source: 'src/../../../../../src/fs/utils/types.rs', lines 111:4-121:5
+ Source: 'src/../../../../../src/fs/utils/types.rs', lines 113:4-124:5
Visibility: public -/
def types.ScanConfig.new : Result types.ScanConfig := do
ok
{
include_hidden := true,
+ only_hidden := false,
max_depth := types.MAX_SCAN_DEPTH,
max_files := types.MAX_SCAN_FILES,
extensions := (alloc.vec.Vec.new String),
@@ -64,7 +66,7 @@ def types.ScanConfig.new : Result types.ScanConfig := do
}
/-- [nonos_x_utils_types::types::{nonos_x_utils_types::types::ScanConfig}::with_max_depth]:
- Source: 'src/../../../../../src/fs/utils/types.rs', lines 123:4-126:5
+ Source: 'src/../../../../../src/fs/utils/types.rs', lines 126:4-129:5
Visibility: public -/
def types.ScanConfig.with_max_depth
(self : types.ScanConfig) (depth : Std.Usize) : Result types.ScanConfig := do
@@ -72,11 +74,20 @@ def types.ScanConfig.with_max_depth
ok { self with max_depth := i }
/-- [nonos_x_utils_types::types::{nonos_x_utils_types::types::ScanConfig}::hidden_only]:
- Source: 'src/../../../../../src/fs/utils/types.rs', lines 133:4-136:5
+ Source: 'src/../../../../../src/fs/utils/types.rs', lines 136:4-140:5
Visibility: public -/
def types.ScanConfig.hidden_only
(self : types.ScanConfig) : Result types.ScanConfig := do
- ok { self with include_hidden := true }
+ ok { self with include_hidden := true, only_hidden := true }
+
+/-- [nonos_x_utils_types::types::{nonos_x_utils_types::types::ScanConfig}::admits_hidden]:
+ Source: 'src/../../../../../src/fs/utils/types.rs', lines 145:4-151:5
+ Visibility: public -/
+def types.ScanConfig.admits_hidden
+ (self : types.ScanConfig) (hidden : Bool) : Result Bool := do
+ if hidden
+ then ok self.include_hidden
+ else ok (¬ self.only_hidden)
/-- [nonos_x_utils_types::scanconfig_new]:
Source: 'src/lib.rs', lines 10:0-12:1
@@ -98,4 +109,11 @@ def scanconfig_hidden_only
(this : types.ScanConfig) : Result types.ScanConfig := do
types.ScanConfig.hidden_only this
+/-- [nonos_x_utils_types::scanconfig_admits_hidden]:
+ Source: 'src/lib.rs', lines 23:0-25:1
+ Visibility: public -/
+def scanconfig_admits_hidden
+ (this : types.ScanConfig) (hidden : Bool) : Result Bool := do
+ types.ScanConfig.admits_hidden this hidden
+
end nonos_x_utils_types
diff --git a/verification/extraction/lean/NonosExtraction/UtilsTypesRefinement.lean b/verification/extraction/lean/NonosExtraction/UtilsTypesRefinement.lean
index cc086412b8..ea573b5787 100644
--- a/verification/extraction/lean/NonosExtraction/UtilsTypesRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/UtilsTypesRefinement.lean
@@ -41,35 +41,42 @@ theorem the_scanconfig_with_max_depth_wrapper_is_its_method (a : types.ScanConfi
theorem the_scanconfig_hidden_only_wrapper_is_its_method (a : types.ScanConfig) :
scanconfig_hidden_only a = types.ScanConfig.hidden_only a := rfl
+theorem the_scanconfig_admits_hidden_wrapper_is_its_method (a : types.ScanConfig) (b : Bool) :
+ scanconfig_admits_hidden a b = types.ScanConfig.admits_hidden a b := rfl
+
/-! ### The scan configuration builders
`scan_with_config` in src/fs/utils/scan_config.rs skips an entry deeper than
-`max_depth` and drops a hidden file only when `include_hidden` is false, so the
-builders decide how far and how widely a scan walks. The theorems below establish
-that a fresh configuration is bounded (depth 64, 65536 files), does not follow
-symbolic links, includes hidden files and is already inside the depth clamp; that
-`with_max_depth` never fails, sets the depth to the smaller of the request and
+`max_depth` and keeps an entry only if `admits_hidden` passes it, so the builders
+decide how far and how widely a scan walks. The theorems below establish that a
+fresh configuration is bounded (depth 64, 65536 files), does not follow symbolic
+links, admits every file, hidden or not, and is already inside the depth clamp;
+that `with_max_depth` never fails, sets the depth to the smaller of the request and
`MAX_SCAN_DEPTH` (so 65 and `usize::MAX` both give 64) and changes no other field;
-and that `hidden_only` is the identity on every configuration that already
-includes hidden files, which records a defect described on that theorem.
+and that after `hidden_only` a configuration admits exactly the hidden files.
+
+`hidden_only` used to set only `include_hidden`, which every fresh configuration
+already has, so `ScanConfig::new().hidden_only()` was `ScanConfig::new()` and a
+scan built that way returned every visible file too. The configuration had no
+field that could say "hidden files only"; `only_hidden` is that field.
-They cannot establish what `scan_with_config` does with the configuration: the
-scanner, the filesystem it walks and `is_hidden` are not extracted into this
-module. The vectors of extensions and name patterns are carried through as opaque
+They cannot establish what `scan_with_config` does beyond calling `admits_hidden`
+on each entry: the scanner, the filesystem it walks and `is_hidden` are not
+extracted into this module. The vectors of extensions and name patterns are carried through as opaque
values, so nothing here says anything about their contents beyond that the
builders leave them unchanged.
-/
/-- The default configuration is bounded in depth and file count, refuses symbolic
-links, includes hidden files and applies no sensitivity threshold, and its depth is
-a fixed point of the clamp. -/
+links, admits hidden and visible files alike and applies no sensitivity threshold,
+and its depth is a fixed point of the clamp. -/
theorem scanconfig_new_is_bounded_and_inside_the_depth_clamp :
∃ c, scanconfig_new = ok c ∧ c.max_depth.val = 64 ∧ c.max_files.val = 65536 ∧
- c.include_hidden = true ∧ c.follow_symlinks = false ∧
+ c.include_hidden = true ∧ c.only_hidden = false ∧ c.follow_symlinks = false ∧
c.sensitivity_threshold = types.SensitivityLevel.None ∧
scanconfig_with_max_depth c c.max_depth = ok c := by
unfold scanconfig_new types.ScanConfig.new
- refine ⟨_, rfl, ?_, ?_, rfl, rfl, rfl, ?_⟩
+ refine ⟨_, rfl, ?_, ?_, rfl, rfl, rfl, rfl, ?_⟩
· unfold types.MAX_SCAN_DEPTH; rfl
· unfold types.MAX_SCAN_FILES; rfl
· unfold scanconfig_with_max_depth types.ScanConfig.with_max_depth
@@ -102,20 +109,37 @@ theorem scanconfig_with_max_depth_never_exceeds_the_cap
cases h
omega
-/-- This records a defect. `hidden_only` sets `include_hidden`, which every fresh
-configuration already has, and the scanner reads that field only to exclude hidden
-files when it is false. So `hidden_only` is the identity on any configuration that
-already includes hidden files, and `ScanConfig::new().hidden_only()` is
-`ScanConfig::new()`: a scan built this way still returns every non-hidden file. The
-configuration has no field that could express "hidden files only". -/
-theorem scanconfig_hidden_only_changes_no_config_that_already_includes_hidden_files
- (c : types.ScanConfig) (h : c.include_hidden = true) :
- scanconfig_hidden_only c = ok c ∧
- (do let c ← scanconfig_new; scanconfig_hidden_only c) = scanconfig_new := by
- refine ⟨?_, rfl⟩
+/-- A hidden file passes exactly when `include_hidden` is set, and a visible one
+exactly when `only_hidden` is clear, for every configuration. -/
+theorem scanconfig_admits_hidden_reads_the_two_flags (c : types.ScanConfig) (hidden : Bool) :
+ scanconfig_admits_hidden c hidden =
+ ok (if hidden then c.include_hidden else !c.only_hidden) := by
+ unfold scanconfig_admits_hidden types.ScanConfig.admits_hidden
+ cases hidden <;> simp
+
+/-- After `hidden_only`, from any configuration, a hidden file passes and a
+visible one does not, and nothing but the two hidden-file flags changes. It used
+to leave a visible file passing whenever the configuration already included
+hidden files, which every fresh one does. -/
+theorem scanconfig_hidden_only_admits_exactly_the_hidden_files (c : types.ScanConfig) :
+ ∃ c', scanconfig_hidden_only c = ok c' ∧
+ scanconfig_admits_hidden c' true = ok true ∧
+ scanconfig_admits_hidden c' false = ok false ∧
+ c' = { c with include_hidden := true, only_hidden := true } := by
unfold scanconfig_hidden_only types.ScanConfig.hidden_only
- cases c
- simp_all
+ refine ⟨_, rfl, ?_, ?_, rfl⟩ <;>
+ simp [scanconfig_admits_hidden_reads_the_two_flags]
+
+/-- A fresh configuration admits every file, hidden or visible, and the same
+configuration after `hidden_only` drops the visible ones, so the builder is no
+longer the identity on it. -/
+theorem scanconfig_hidden_only_changes_the_fresh_configuration :
+ (do let c ← scanconfig_new; scanconfig_admits_hidden c false) = ok true ∧
+ (do let c ← scanconfig_new; scanconfig_admits_hidden c true) = ok true ∧
+ (do let c ← scanconfig_new
+ let c ← scanconfig_hidden_only c
+ scanconfig_admits_hidden c false) = ok false := by
+ refine ⟨rfl, rfl, rfl⟩
/-! ### Axiom profile -/
@@ -125,6 +149,9 @@ theorem scanconfig_hidden_only_changes_no_config_that_already_includes_hidden_fi
#print axioms NonosExtraction.UtilsTypes.scanconfig_new_is_bounded_and_inside_the_depth_clamp
#print axioms NonosExtraction.UtilsTypes.scanconfig_with_max_depth_saturates_at_64_and_changes_nothing_else
#print axioms NonosExtraction.UtilsTypes.scanconfig_with_max_depth_never_exceeds_the_cap
-#print axioms NonosExtraction.UtilsTypes.scanconfig_hidden_only_changes_no_config_that_already_includes_hidden_files
+#print axioms NonosExtraction.UtilsTypes.the_scanconfig_admits_hidden_wrapper_is_its_method
+#print axioms NonosExtraction.UtilsTypes.scanconfig_admits_hidden_reads_the_two_flags
+#print axioms NonosExtraction.UtilsTypes.scanconfig_hidden_only_admits_exactly_the_hidden_files
+#print axioms NonosExtraction.UtilsTypes.scanconfig_hidden_only_changes_the_fresh_configuration
end NonosExtraction.UtilsTypes
diff --git a/verification/extraction/lean/NonosExtraction/ValidationSimdLevelRefinement.lean b/verification/extraction/lean/NonosExtraction/ValidationSimdLevelRefinement.lean
index 0bb42af153..f98643116a 100644
--- a/verification/extraction/lean/NonosExtraction/ValidationSimdLevelRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/ValidationSimdLevelRefinement.lean
@@ -35,8 +35,50 @@ namespace NonosExtraction.ValidationSimdLevel
theorem the_simdlevel_register_width_wrapper_is_its_method (a : simd_level.SimdLevel) :
simdlevel_register_width a = simd_level.SimdLevel.register_width a := rfl
+/-! ### Register width follows the SIMD level order
+
+ `SimdLevel` derives `PartialOrd` and `Ord` from its declared discriminants
+ (`None = 0` up to `Avx512 = 9`), and `highest_level` in `simd_types.rs` picks
+ the greatest level a processor supports. The theorems below say that
+ `register_width` agrees with that order: a higher level never has narrower
+ registers, only `None` has width zero, and the widths are the architectural
+ ones (128 bits for every SSE generation, 256 for AVX and AVX2, 512 for
+ AVX-512). They cannot establish that the CPUID probing behind
+ `highest_level` is correct; it is not extracted here.
+-/
+
+/-- A level at or above another in the declared order has registers at least as
+ wide, so comparing levels and comparing widths never disagree in direction. -/
+theorem simdlevel_register_width_is_monotone_in_the_declared_order
+ (a b : simd_level.SimdLevel)
+ (h : (simd_level.SimdLevel.read_discriminant a).val ≤
+ (simd_level.SimdLevel.read_discriminant b).val) :
+ ∃ wa wb : Std.Usize, simdlevel_register_width a = ok wa ∧
+ simdlevel_register_width b = ok wb ∧ wa.val ≤ wb.val := by
+ cases a <;> cases b <;> first
+ | exact ⟨_, _, rfl, rfl, by decide⟩
+ | (simp [simd_level.SimdLevel.read_discriminant] at h)
+
+/-- Only the absence of SIMD reports zero width. -/
+theorem simdlevel_register_width_is_zero_only_without_simd (a : simd_level.SimdLevel) :
+ simdlevel_register_width a = ok 0#usize ↔ a = simd_level.SimdLevel.None := by
+ cases a <;> simp [simdlevel_register_width, simd_level.SimdLevel.register_width]
+
+/-- The widths are the architectural register sizes: XMM for every SSE level,
+ YMM for AVX and AVX2, ZMM for AVX-512. -/
+theorem simdlevel_register_width_is_the_architectural_register_size :
+ simdlevel_register_width .Sse = ok 128#usize ∧
+ simdlevel_register_width .Sse42 = ok 128#usize ∧
+ simdlevel_register_width .Avx = ok 256#usize ∧
+ simdlevel_register_width .Avx2 = ok 256#usize ∧
+ simdlevel_register_width .Avx512 = ok 512#usize := by
+ exact ⟨rfl, rfl, rfl, rfl, rfl⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.ValidationSimdLevel.the_simdlevel_register_width_wrapper_is_its_method
+#print axioms NonosExtraction.ValidationSimdLevel.simdlevel_register_width_is_monotone_in_the_declared_order
+#print axioms NonosExtraction.ValidationSimdLevel.simdlevel_register_width_is_zero_only_without_simd
+#print axioms NonosExtraction.ValidationSimdLevel.simdlevel_register_width_is_the_architectural_register_size
end NonosExtraction.ValidationSimdLevel
diff --git a/verification/extraction/lean/NonosExtraction/VariableFirmware.lean b/verification/extraction/lean/NonosExtraction/VariableFirmware.lean
index 4008a34468..29ac4d0789 100644
--- a/verification/extraction/lean/NonosExtraction/VariableFirmware.lean
+++ b/verification/extraction/lean/NonosExtraction/VariableFirmware.lean
@@ -14,6 +14,19 @@ set_option maxRecDepth 2048
namespace nonos_x_variable_firmware
+/-- [nonos_x_variable_firmware::revisions::uefi_revision]:
+ Source: 'src/../../../../../src/arch/x86_64/uefi/constants/revisions.rs', lines 20:0-22:1
+ Visibility: public -/
+def revisions.uefi_revision
+ (major : Std.U16) (minor : Std.U16) (patch : Std.U16) : Result Std.U32 := do
+ let i ← lift (UScalar.cast .U32 major)
+ let i1 ← i <<< 16#i32
+ let i2 ← lift (UScalar.cast .U32 minor)
+ let i3 ← i2 * 10#u32
+ let i4 ← lift (UScalar.cast .U32 patch)
+ let i5 ← i3 + i4
+ ok (i1 ||| i5)
+
/-- [nonos_x_variable_firmware::firmware::FirmwareInfo]
Source: 'src/../../../../../src/arch/x86_64/uefi/variable/firmware.rs', lines 22:0-31:1
Visibility: public -/
@@ -43,17 +56,24 @@ def firmware.FirmwareInfo.uefi_minor_version
ok (UScalar.cast .U16 self.revision)
/-- [nonos_x_variable_firmware::firmwareinfo_uefi_major_version]:
- Source: 'src/lib.rs', lines 10:0-12:1
+ Source: 'src/lib.rs', lines 14:0-16:1
Visibility: public -/
def firmwareinfo_uefi_major_version
(this : firmware.FirmwareInfo) : Result Std.U16 := do
firmware.FirmwareInfo.uefi_major_version this
/-- [nonos_x_variable_firmware::firmwareinfo_uefi_minor_version]:
- Source: 'src/lib.rs', lines 14:0-16:1
+ Source: 'src/lib.rs', lines 18:0-20:1
Visibility: public -/
def firmwareinfo_uefi_minor_version
(this : firmware.FirmwareInfo) : Result Std.U16 := do
firmware.FirmwareInfo.uefi_minor_version this
+/-- [nonos_x_variable_firmware::uefi_revision]:
+ Source: 'src/lib.rs', lines 23:0-25:1
+ Visibility: public -/
+def uefi_revision
+ (major : Std.U16) (minor : Std.U16) (patch : Std.U16) : Result Std.U32 := do
+ revisions.uefi_revision major minor patch
+
end nonos_x_variable_firmware
diff --git a/verification/extraction/lean/NonosExtraction/VariableFirmwareRefinement.lean b/verification/extraction/lean/NonosExtraction/VariableFirmwareRefinement.lean
index 9ed85ce841..02ee2b2df8 100644
--- a/verification/extraction/lean/NonosExtraction/VariableFirmwareRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/VariableFirmwareRefinement.lean
@@ -38,9 +38,158 @@ theorem the_firmwareinfo_uefi_major_version_wrapper_is_its_method (a : firmware.
theorem the_firmwareinfo_uefi_minor_version_wrapper_is_its_method (a : firmware.FirmwareInfo) :
firmwareinfo_uefi_minor_version a = firmware.FirmwareInfo.uefi_minor_version a := rfl
+theorem the_uefi_revision_wrapper_is_its_function (a b c : Std.U16) :
+ uefi_revision a b c = revisions.uefi_revision a b c := rfl
+
+/-! ### Major and minor are the two halves of the revision
+
+UEFI packs a revision into one `u32` with the major version in the upper sixteen
+bits and, in the lower sixteen, the minor version times ten plus a patch digit:
+2.3.1 is `0x0002001F` and 2.8 is `0x00020050`. The theorems below show that
+`uefi_major_version` reads exactly the upper half, `uefi_minor_version` exactly the
+lower half, that neither can fail, and that the two together give back every bit of
+the revision: nothing is lost and nothing is read twice.
+
+`uefi_revision` builds a revision from its parts and every `UEFI_REVISION_*`
+constant is defined through it. The theorems show it never fails, that it lays out
+major, minor and patch as the specification does whenever the tens-and-units field
+fits sixteen bits (for every version UEFI has published), and that the readers
+above give back the major and the tens-and-units field it packed. The constants
+used to disagree: `UEFI_REVISION_2_8` was `0x00020800`, minor 2048 to these
+readers, `detect_firmware_info` stored `0x00020008`, minor eight, and only
+`UEFI_REVISION_2_3_1` had the specification's `0x0002001F`, so a comparison of a
+firmware's real revision with the constants would have called 2.8 firmware older
+than 2.3.1. `detect_firmware_info` now stores `UEFI_REVISION_2_8`.
+
+What these theorems cannot establish: that the Rust constants are the calls they
+are written as (Charon does not extract an unused constant, so that step is read
+off the source), or what a firmware put in its own header, which is not read here.
+-/
+
+/-- The major version is the upper sixteen bits of the revision, and reading it
+ cannot fail. -/
+theorem firmwareinfo_uefi_major_version_is_the_upper_half (f : firmware.FirmwareInfo) :
+ ∃ m, firmwareinfo_uefi_major_version f = ok m ∧ m.val = f.revision.val / 65536 := by
+ unfold firmwareinfo_uefi_major_version firmware.FirmwareInfo.uefi_major_version
+ obtain ⟨z, hz, hv, -⟩ := WP.spec_imp_exists
+ (U32.ShiftRight_IScalar_spec (x := f.revision) (y := 16#i32) (by decide) (by decide))
+ rw [hz]
+ refine ⟨_, rfl, ?_⟩
+ have hb : f.revision.val < 2 ^ 32 := by scalar_tac
+ rw [UScalar.cast_val_eq, hv]
+ simp only [UScalarTy.numBits, Nat.shiftRight_eq_div_pow]
+ have : (16#i32 : Std.I32).toNat = 16 := rfl
+ rw [this]
+ omega
+
+/-- The minor version is the lower sixteen bits of the revision, and reading it
+ cannot fail. -/
+theorem firmwareinfo_uefi_minor_version_is_the_lower_half (f : firmware.FirmwareInfo) :
+ ∃ m, firmwareinfo_uefi_minor_version f = ok m ∧ m.val = f.revision.val % 65536 := by
+ unfold firmwareinfo_uefi_minor_version firmware.FirmwareInfo.uefi_minor_version
+ refine ⟨_, rfl, ?_⟩
+ rw [UScalar.cast_val_eq]
+ rfl
+
+/-- Major and minor together are the whole revision: `major * 65536 + minor` gives
+ back the field, so two distinct revisions never report the same pair. -/
+theorem firmwareinfo_uefi_major_and_minor_version_rebuild_the_revision
+ (f : firmware.FirmwareInfo) (major minor : Std.U16)
+ (hM : firmwareinfo_uefi_major_version f = ok major)
+ (hm : firmwareinfo_uefi_minor_version f = ok minor) :
+ major.val * 65536 + minor.val = f.revision.val := by
+ obtain ⟨M, hM', hMv⟩ := firmwareinfo_uefi_major_version_is_the_upper_half f
+ obtain ⟨m, hm', hmv⟩ := firmwareinfo_uefi_minor_version_is_the_lower_half f
+ rw [hM] at hM'
+ rw [hm] at hm'
+ cases hM'
+ cases hm'
+ omega
+
+/-- `uefi_revision` never fails: the minor times ten plus the patch is at most
+ `65535 * 10 + 65535`, far inside a `u32`. The result is the major shifted
+ into the upper half, ORed with the tens-and-units field. -/
+theorem uefi_revision_ors_the_major_over_the_minor_field (M m p : Std.U16) :
+ ∃ r : Std.U32, uefi_revision M m p = ok r ∧
+ r.val = M.val * 2 ^ 16 ||| (m.val * 10 + p.val) := by
+ unfold uefi_revision revisions.uefi_revision
+ have hM := M.hBounds
+ have hm := m.hBounds
+ have hp := p.hBounds
+ simp [UScalarTy.numBits] at hM hm hp
+ obtain ⟨z1, hz1, hv1, -⟩ := WP.spec_imp_exists
+ (UScalar.ShiftLeft_IScalar_spec (UScalar.cast .U32 M) 16#i32 (UScalar.size .U32)
+ (by decide) (by decide) rfl)
+ obtain ⟨z2, hz2, hv2⟩ := WP.spec_imp_exists
+ (UScalar.mul_spec (x := UScalar.cast .U32 m) (y := 10#u32)
+ (by simp [UScalarTy.numBits]; scalar_tac))
+ obtain ⟨z3, hz3, hv3⟩ := WP.spec_imp_exists
+ (UScalar.add_spec (x := z2) (y := UScalar.cast .U32 p)
+ (by simp [UScalarTy.numBits] at hv2 ⊢; scalar_tac))
+ simp only [lift, bind_tc_ok, hz1, hz2, hz3]
+ refine ⟨_, rfl, ?_⟩
+ simp only [UScalar.cast_val_eq, UScalarTy.numBits, show (16#i32 : Std.I32).toNat = 16 from rfl]
+ at hv1 hv2 hv3
+ rw [UScalar.val_or, hv1, hv3, hv2, Nat.shiftLeft_eq]
+ simp only [UScalar.size, UScalarTy.numBits]
+ rw [Nat.mod_eq_of_lt (by omega : M.val < 2 ^ 32), Nat.mod_eq_of_lt (by omega : m.val < 2 ^ 32),
+ Nat.mod_eq_of_lt (by omega : p.val < 2 ^ 32), Nat.mod_eq_of_lt (by omega)]
+ rfl
+
+/-- Whenever the tens-and-units field fits sixteen bits, the revision is
+ `major * 65536 + minor * 10 + patch`, the specification's packing. -/
+theorem uefi_revision_is_the_specification_packing (M m p : Std.U16)
+ (h : m.val * 10 + p.val < 2 ^ 16) :
+ ∃ r : Std.U32, uefi_revision M m p = ok r ∧
+ r.val = M.val * 65536 + (m.val * 10 + p.val) := by
+ obtain ⟨r, hr, hv⟩ := uefi_revision_ors_the_major_over_the_minor_field M m p
+ refine ⟨r, hr, ?_⟩
+ rw [hv, ← Nat.shiftLeft_eq, ← Nat.shiftLeft_add_eq_or_of_lt h, Nat.shiftLeft_eq]
+
+/-- The readers give back what `uefi_revision` packed: a firmware record holding
+ the revision built from major `M`, minor `m` and patch `p` reports major `M`
+ and minor field `m * 10 + p`. -/
+theorem uefi_revision_reads_back_through_the_firmware_record (M m p : Std.U16)
+ (h : m.val * 10 + p.val < 2 ^ 16) (r : Std.U32) (hr : uefi_revision M m p = ok r)
+ (f : firmware.FirmwareInfo) (hf : f.revision = r) :
+ (∃ x, firmwareinfo_uefi_major_version f = ok x ∧ x.val = M.val) ∧
+ (∃ x, firmwareinfo_uefi_minor_version f = ok x ∧ x.val = m.val * 10 + p.val) := by
+ obtain ⟨r', hr', hv⟩ := uefi_revision_is_the_specification_packing M m p h
+ rw [hr] at hr'
+ cases hr'
+ obtain ⟨X, hX, hXv⟩ := firmwareinfo_uefi_major_version_is_the_upper_half f
+ obtain ⟨x, hx, hxv⟩ := firmwareinfo_uefi_minor_version_is_the_lower_half f
+ rw [hf, hv] at hXv hxv
+ have hM := M.hBounds
+ simp [UScalarTy.numBits] at hM
+ exact ⟨⟨X, hX, by omega⟩, ⟨x, hx, by omega⟩⟩
+
+/-- The values the constants take: 2.3.1 is `0x0002001F`, 2.8 is `0x00020050` and
+ 2.10 is `0x00020064`. The old `UEFI_REVISION_2_8`, `0x00020800`, and the
+ `0x00020008` `detect_firmware_info` stored are neither of them. -/
+theorem the_revision_constants_take_the_specification_values :
+ uefi_revision 2#u16 3#u16 1#u16 = ok 0x0002001F#u32 ∧
+ uefi_revision 2#u16 8#u16 0#u16 = ok 0x00020050#u32 ∧
+ uefi_revision 2#u16 10#u16 0#u16 = ok 0x00020064#u32 := by
+ refine ⟨?_, ?_, ?_⟩
+ · obtain ⟨r, hr, hv⟩ := uefi_revision_is_the_specification_packing 2#u16 3#u16 1#u16 (by decide)
+ rw [hr]; congr 1; exact UScalar.eq_of_val_eq (by rw [hv]; rfl)
+ · obtain ⟨r, hr, hv⟩ := uefi_revision_is_the_specification_packing 2#u16 8#u16 0#u16 (by decide)
+ rw [hr]; congr 1; exact UScalar.eq_of_val_eq (by rw [hv]; rfl)
+ · obtain ⟨r, hr, hv⟩ := uefi_revision_is_the_specification_packing 2#u16 10#u16 0#u16 (by decide)
+ rw [hr]; congr 1; exact UScalar.eq_of_val_eq (by rw [hv]; rfl)
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.VariableFirmware.the_firmwareinfo_uefi_major_version_wrapper_is_its_method
#print axioms NonosExtraction.VariableFirmware.the_firmwareinfo_uefi_minor_version_wrapper_is_its_method
+#print axioms NonosExtraction.VariableFirmware.firmwareinfo_uefi_major_version_is_the_upper_half
+#print axioms NonosExtraction.VariableFirmware.firmwareinfo_uefi_minor_version_is_the_lower_half
+#print axioms NonosExtraction.VariableFirmware.firmwareinfo_uefi_major_and_minor_version_rebuild_the_revision
+#print axioms NonosExtraction.VariableFirmware.the_uefi_revision_wrapper_is_its_function
+#print axioms NonosExtraction.VariableFirmware.uefi_revision_ors_the_major_over_the_minor_field
+#print axioms NonosExtraction.VariableFirmware.uefi_revision_is_the_specification_packing
+#print axioms NonosExtraction.VariableFirmware.uefi_revision_reads_back_through_the_firmware_record
+#print axioms NonosExtraction.VariableFirmware.the_revision_constants_take_the_specification_values
end NonosExtraction.VariableFirmware
diff --git a/verification/extraction/lean/NonosExtraction/VgaColors.lean b/verification/extraction/lean/NonosExtraction/VgaColors.lean
index e0c54c4876..53c59cb605 100644
--- a/verification/extraction/lean/NonosExtraction/VgaColors.lean
+++ b/verification/extraction/lean/NonosExtraction/VgaColors.lean
@@ -15,25 +15,26 @@ set_option maxRecDepth 2048
namespace nonos_x_vga_colors
/-- [nonos_x_vga_colors::colors::make_attr]:
- Source: 'src/../../../../../src/boot/vga/colors.rs', lines 34:0-36:1
+ Source: 'src/../../../../../src/boot/vga/colors.rs', lines 36:0-38:1
Visibility: public -/
def colors.make_attr (fg : Std.U8) (bg : Std.U8) : Result Std.U8 := do
- let i ← bg <<< 4#i32
- let i1 ← lift (fg &&& 15#u8)
- ok (i ||| i1)
+ let i ← lift (bg &&& 7#u8)
+ let i1 ← i <<< 4#i32
+ let i2 ← lift (fg &&& 15#u8)
+ ok (i1 ||| i2)
/-- [nonos_x_vga_colors::colors::fg_color]:
- Source: 'src/../../../../../src/boot/vga/colors.rs', lines 38:0-40:1
+ Source: 'src/../../../../../src/boot/vga/colors.rs', lines 40:0-42:1
Visibility: public -/
def colors.fg_color (attr : Std.U8) : Result Std.U8 := do
ok (attr &&& 15#u8)
/-- [nonos_x_vga_colors::colors::bg_color]:
- Source: 'src/../../../../../src/boot/vga/colors.rs', lines 42:0-44:1
+ Source: 'src/../../../../../src/boot/vga/colors.rs', lines 44:0-46:1
Visibility: public -/
def colors.bg_color (attr : Std.U8) : Result Std.U8 := do
let i ← attr >>> 4#i32
- ok (i &&& 15#u8)
+ ok (i &&& 7#u8)
/-- [nonos_x_vga_colors::make_attr]:
Source: 'src/lib.rs', lines 10:0-12:1
diff --git a/verification/extraction/lean/NonosExtraction/VgaColorsRefinement.lean b/verification/extraction/lean/NonosExtraction/VgaColorsRefinement.lean
index 49831fb31c..f8310c8126 100644
--- a/verification/extraction/lean/NonosExtraction/VgaColorsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/VgaColorsRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.VgaColors
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_vga_colors
@@ -41,10 +42,109 @@ theorem the_fg_color_wrapper_is_its_method (a : Std.U8) :
theorem the_bg_color_wrapper_is_its_method (a : Std.U8) :
bg_color a = colors.bg_color a := rfl
+/-! ### Packing and unpacking a text-mode attribute byte
+
+A VGA text attribute is one byte: the foreground colour in the low nibble, the
+background in bits 4 to 6, and bit 7, which the Attribute Controller shows as
+blink with its power-on setting, which nothing in the tree changes. The
+theorems below establish that `fg_color` reads exactly the low nibble, that
+`bg_color` reads exactly bits 4 to 6, and that `make_attr` never fails, keeps
+the foreground's low nibble and the background's low three bits, and never
+sets bit 7. So a foreground above 15 cannot bleed into the background, and no
+background makes the text blink.
+
+`make_attr` used to shift the whole background into the high nibble and
+`bg_color` to read bit 7 as a fourth background bit, so a bright background
+rendered as blinking text on the dark one. `arch::x86_64::vga::ColorCode`
+already read the byte this way. No kernel code calls `make_attr`, `fg_color`
+or `bg_color` today; the boot splash passes the colour constants of this module
+to its own writer.
+-/
+
+private theorem shr_u8 (x : Std.U8) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 8) :
+ ∃ z : Std.U8, x >>> k = ok z ∧ z.val = x.val / 2 ^ k.toNat := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftRight_IScalar_spec x k h0 (by simpa using h1))
+ exact ⟨z, hz, by rw [hv, Nat.shiftRight_eq_div_pow]⟩
+
+private theorem shl_u8 (x : Std.U8) (k : Std.I32) (h0 : 0 ≤ k.val) (h1 : k.val < 8) :
+ ∃ z : Std.U8, x <<< k = ok z ∧ z.val = x.val * 2 ^ k.toNat % 2 ^ 8 := by
+ obtain ⟨z, hz, hv, -⟩ :=
+ WP.spec_imp_exists (UScalar.ShiftLeft_IScalar_spec x k (UScalar.size .U8) h0
+ (by simpa using h1) rfl)
+ exact ⟨z, hz, by rw [hv, Nat.shiftLeft_eq]; simp [U8.size, U8.numBits]⟩
+
+/-- The foreground is the low nibble of the attribute, all four bits of it. -/
+theorem fg_color_is_the_low_nibble (a : Std.U8) :
+ ∃ f : Std.U8, fg_color a = ok f ∧ f.val = a.val % 16 := by
+ unfold fg_color colors.fg_color
+ exact ⟨_, rfl, Bits.land_low_mask a 15#u8 4 rfl⟩
+
+/-- The background is bits 4 to 6 of the attribute; bit 7 is not read. -/
+theorem bg_color_is_bits_four_to_six (a : Std.U8) :
+ ∃ g : Std.U8, bg_color a = ok g ∧ g.val = a.val / 16 % 8 := by
+ unfold bg_color colors.bg_color
+ obtain ⟨z, hz, hv⟩ := shr_u8 a 4#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok]
+ refine ⟨_, rfl, ?_⟩
+ rw [Bits.land_low_mask z 7#u8 3 rfl, hv]
+ rfl
+
+/-- The attribute `0x8F`, white with bit 7 set, decodes to background 0, as the
+ arch `ColorCode::background` does, where it used to decode to background 8. -/
+theorem bg_color_leaves_bit_seven_to_blink :
+ bg_color 0x8F#u8 = ok 0#u8 ∧ fg_color 0x8F#u8 = ok 15#u8 := by
+ constructor <;> rfl
+
+/-- `make_attr` never fails, and the byte it builds is the background's low three
+ bits times sixteen plus the foreground's low nibble; bit 7 is never set. -/
+theorem make_attr_packs_the_colour_fields (fg bg : Std.U8) :
+ ∃ a : Std.U8, make_attr fg bg = ok a ∧ a.val = bg.val % 8 * 16 + fg.val % 16 ∧
+ a.val < 128 := by
+ unfold make_attr colors.make_attr
+ have hb : (bg &&& 7#u8).val = bg.val % 8 := Bits.land_low_mask bg 7#u8 3 rfl
+ obtain ⟨z, hz, hv⟩ := shl_u8 (bg &&& 7#u8) 4#i32 (by decide) (by decide)
+ simp only [hz, bind_tc_ok, lift]
+ refine ⟨_, rfl, ?_⟩
+ rw [UScalar.val_or, Bits.land_low_mask fg 15#u8 4 rfl, hv, hb]
+ simp only [show (4#i32 : Std.I32).toNat = 4 from rfl]
+ have hsh : bg.val % 8 * 2 ^ 4 % 2 ^ 8 = (bg.val % 8) <<< 4 := by
+ rw [Nat.shiftLeft_eq]; omega
+ have hlt : fg.val % 2 ^ 4 < 2 ^ 4 := Nat.mod_lt _ (by decide)
+ rw [hsh, ← Nat.shiftLeft_add_eq_or_of_lt hlt, Nat.shiftLeft_eq]
+ omega
+
+/-- Decoding what `make_attr` built gives back the foreground's low nibble and
+ the background's low three bits: neither field reaches the other or bit 7. -/
+theorem make_attr_round_trips_through_fg_color_and_bg_color (fg bg : Std.U8) :
+ ∃ a f g : Std.U8, make_attr fg bg = ok a ∧ fg_color a = ok f ∧ bg_color a = ok g ∧
+ f.val = fg.val % 16 ∧ g.val = bg.val % 8 := by
+ obtain ⟨a, ha, hav, -⟩ := make_attr_packs_the_colour_fields fg bg
+ obtain ⟨f, hf, hfv⟩ := fg_color_is_the_low_nibble a
+ obtain ⟨g, hg, hgv⟩ := bg_color_is_bits_four_to_six a
+ refine ⟨a, f, g, ha, hf, hg, ?_, ?_⟩ <;> omega
+
+/-- For a foreground below 16 and a background below 8, the round trip is the
+ identity. -/
+theorem make_attr_then_decode_is_the_identity_on_the_fields (fg bg : Std.U8)
+ (hf : fg.val < 16) (hb : bg.val < 8) :
+ ∃ a, make_attr fg bg = ok a ∧ fg_color a = ok fg ∧ bg_color a = ok bg := by
+ obtain ⟨a, f, g, ha, hfa, hga, hfv, hgv⟩ :=
+ make_attr_round_trips_through_fg_color_and_bg_color fg bg
+ have e1 : f = fg := UScalar.eq_of_val_eq (by omega)
+ have e2 : g = bg := UScalar.eq_of_val_eq (by omega)
+ exact ⟨a, ha, e1 ▸ hfa, e2 ▸ hga⟩
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.VgaColors.the_make_attr_wrapper_is_its_method
#print axioms NonosExtraction.VgaColors.the_fg_color_wrapper_is_its_method
#print axioms NonosExtraction.VgaColors.the_bg_color_wrapper_is_its_method
+#print axioms NonosExtraction.VgaColors.fg_color_is_the_low_nibble
+#print axioms NonosExtraction.VgaColors.bg_color_is_bits_four_to_six
+#print axioms NonosExtraction.VgaColors.bg_color_leaves_bit_seven_to_blink
+#print axioms NonosExtraction.VgaColors.make_attr_packs_the_colour_fields
+#print axioms NonosExtraction.VgaColors.make_attr_round_trips_through_fg_color_and_bg_color
+#print axioms NonosExtraction.VgaColors.make_attr_then_decode_is_the_identity_on_the_fields
end NonosExtraction.VgaColors
diff --git a/verification/extraction/lean/NonosExtraction/VgaConstants.lean b/verification/extraction/lean/NonosExtraction/VgaConstants.lean
index 2fc4d05638..3037dd15de 100644
--- a/verification/extraction/lean/NonosExtraction/VgaConstants.lean
+++ b/verification/extraction/lean/NonosExtraction/VgaConstants.lean
@@ -14,21 +14,59 @@ set_option maxRecDepth 2048
namespace nonos_x_vga_constants
+/-- [nonos_x_vga_constants::constants::Color]
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 38:0-55:1
+ Visibility: public -/
+@[discriminant u8]
+inductive constants.Color where
+| Black : constants.Color
+| Blue : constants.Color
+| Green : constants.Color
+| Cyan : constants.Color
+| Red : constants.Color
+| Magenta : constants.Color
+| Brown : constants.Color
+| LightGray : constants.Color
+| DarkGray : constants.Color
+| LightBlue : constants.Color
+| LightGreen : constants.Color
+| LightCyan : constants.Color
+| LightRed : constants.Color
+| Pink : constants.Color
+| Yellow : constants.Color
+| White : constants.Color
+
/-- [nonos_x_vga_constants::constants::ColorCode]
Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 82:0-82:25
Visibility: public -/
@[reducible]
def constants.ColorCode := Std.U8
+/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ColorCode}::new]:
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 88:4-90:5
+ Visibility: public -/
+def constants.ColorCode.new
+ (foreground : constants.Color) (background : constants.Color) :
+ Result constants.ColorCode
+ := do
+ let background1 := read_discriminant background
+ let i ← lift (UScalar.cast .U8 background1)
+ let i1 ← lift (i &&& 7#u8)
+ let i2 ← i1 <<< 4#i32
+ let foreground1 := read_discriminant foreground
+ let i3 ← lift (UScalar.cast .U8 foreground1)
+ let i4 ← lift (i2 ||| i3)
+ ok i4
+
/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ColorCode}::foreground]:
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 93:4-95:5
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 96:4-98:5
Visibility: public -/
def constants.ColorCode.foreground
(self : constants.ColorCode) : Result Std.U8 := do
ok (self &&& 15#u8)
/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ColorCode}::background]:
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 97:4-99:5
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 100:4-102:5
Visibility: public -/
def constants.ColorCode.background
(self : constants.ColorCode) : Result Std.U8 := do
@@ -36,7 +74,7 @@ def constants.ColorCode.background
ok (i &&& 7#u8)
/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ColorCode}::is_blinking]:
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 101:4-103:5
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 104:4-106:5
Visibility: public -/
def constants.ColorCode.is_blinking
(self : constants.ColorCode) : Result Bool := do
@@ -44,21 +82,21 @@ def constants.ColorCode.is_blinking
ok (i != 0#u8)
/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ColorCode}::value]:
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 105:4-107:5
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 108:4-110:5
Visibility: public -/
def constants.ColorCode.value
(self : constants.ColorCode) : Result Std.U8 := do
ok self
/-- [nonos_x_vga_constants::constants::ScreenChar]
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 118:0-121:1
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 121:0-124:1
Visibility: public -/
structure constants.ScreenChar where
character : Std.U8
color : constants.ColorCode
/-- [nonos_x_vga_constants::constants::{nonos_x_vga_constants::constants::ScreenChar}::as_u16]:
- Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 132:4-134:5
+ Source: 'src/../../../../../src/arch/x86_64/vga/constants.rs', lines 135:4-137:5
Visibility: public -/
def constants.ScreenChar.as_u16
(self : constants.ScreenChar) : Result Std.U16 := do
@@ -98,4 +136,13 @@ def colorcode_value (this : constants.ColorCode) : Result Std.U8 := do
def screenchar_as_u16 (this : constants.ScreenChar) : Result Std.U16 := do
constants.ScreenChar.as_u16 this
+/-- [nonos_x_vga_constants::colorcode_new]:
+ Source: 'src/lib.rs', lines 31:0-33:1
+ Visibility: public -/
+def colorcode_new
+ (foreground : constants.Color) (background : constants.Color) :
+ Result constants.ColorCode
+ := do
+ constants.ColorCode.new foreground background
+
end nonos_x_vga_constants
diff --git a/verification/extraction/lean/NonosExtraction/VgaConstantsRefinement.lean b/verification/extraction/lean/NonosExtraction/VgaConstantsRefinement.lean
index a2d57254f0..055d771e86 100644
--- a/verification/extraction/lean/NonosExtraction/VgaConstantsRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/VgaConstantsRefinement.lean
@@ -49,6 +49,9 @@ theorem the_colorcode_value_wrapper_is_its_method (a : constants.ColorCode) :
theorem the_screenchar_as_u16_wrapper_is_its_method (a : constants.ScreenChar) :
screenchar_as_u16 a = constants.ScreenChar.as_u16 a := rfl
+theorem the_colorcode_new_wrapper_is_its_method (a b : constants.Color) :
+ colorcode_new a b = constants.ColorCode.new a b := rfl
+
/-! ### How an attribute byte splits, and how a cell is packed
A VGA text attribute is one byte: the low nibble is the foreground (with its
@@ -59,10 +62,12 @@ with nothing counted twice or dropped, and that `as_u16` puts the attribute in
the high byte of the cell and the character in the low byte, which is the
layout the hardware reads at `0xB8000`.
-`ColorCode::new` and `with_blink` are `const fn` constructors that are not
-extracted, so the bytes they build are written out here as arithmetic
-(`bg * 16 + fg`, and `128 + bg * 16 + fg`) rather than taken from the code.
-Nothing here covers the volatile writes that put a cell in the buffer.
+`ColorCode::new` is extracted: it keeps three bits of the background, so a
+bright background is drawn dark and the cell never blinks. It used to shift the
+whole background into place, and a bright background set bit 7 and blinked on
+the dark one. `with_blink` is not extracted, so the byte it builds is written
+out here as arithmetic (`128 + bg * 16 + fg`). Nothing here covers the volatile
+writes that put a cell in the buffer.
-/
theorem shifting_a_byte_right_by_four_is_the_bitvector_shift (x : Std.U8) :
@@ -146,21 +151,31 @@ theorem the_readers_recover_the_colours_new_and_with_blink_pack
rw [colorcode_is_blinking_is_the_top_half_of_the_byte, hc]
cases b <;> simp <;> omega
-/-- This records a defect in `ColorCode::new`. It computes `bg << 4 | fg` without
- masking the background to three bits, and `Color` has eight bright
- backgrounds (discriminants 8 to 15). For those the byte has bit 7 set, and
- the readers here, which treat bit 7 as blink, decode it as a blinking cell
- on the dark background `bg - 8`. `ColorCode::new(Color::White,
- Color::DarkGray)` is the byte `0x8F`: blinking white on black. -/
-theorem a_bright_background_from_new_decodes_as_blinking_on_the_dark_one
- (c : constants.ColorCode) (fg bg : Nat) (hfg : fg < 16) (hbg : 8 ≤ bg) (hbg' : bg < 16)
- (hc : c.val = bg * 16 + fg) :
- colorcode_is_blinking c = ok true ∧
- ∃ g, colorcode_background c = ok g ∧ g.val = bg - 8 := by
- obtain ⟨g, hg, hgv⟩ := colorcode_background_is_bits_four_to_six c
- refine ⟨?_, g, hg, by rw [hgv, hc]; omega⟩
- rw [colorcode_is_blinking_is_the_top_half_of_the_byte, hc]
- simp; omega
+/-- The index of each `Color`, its `repr(u8)` discriminant. -/
+def colorIndex : constants.Color → Nat
+ | .Black => 0 | .Blue => 1 | .Green => 2 | .Cyan => 3 | .Red => 4 | .Magenta => 5
+ | .Brown => 6 | .LightGray => 7 | .DarkGray => 8 | .LightBlue => 9 | .LightGreen => 10
+ | .LightCyan => 11 | .LightRed => 12 | .Pink => 13 | .Yellow => 14 | .White => 15
+
+/-- `ColorCode::new` builds the background's low three bits times sixteen plus
+ the foreground, for all 256 pairs of colours. -/
+theorem colorcode_new_masks_the_background_to_three_bits (f b : constants.Color) :
+ ∃ c, colorcode_new f b = ok c ∧ c.val = colorIndex b % 8 * 16 + colorIndex f := by
+ cases f <;> cases b <;> exact ⟨_, rfl, rfl⟩
+
+/-- A cell built by `new` never blinks, reads back its foreground whole and its
+ background's low three bits. `new(White, DarkGray)` is white on black, where
+ it used to be blinking white on black. -/
+theorem colorcode_new_never_blinks (f b : constants.Color) :
+ ∃ c, colorcode_new f b = ok c ∧ colorcode_is_blinking c = ok false ∧
+ (∃ g, colorcode_background c = ok g ∧ g.val = colorIndex b % 8) ∧
+ (∃ h, colorcode_foreground c = ok h ∧ h.val = colorIndex f) := by
+ obtain ⟨c, hc, hv⟩ := colorcode_new_masks_the_background_to_three_bits f b
+ have hf : colorIndex f < 16 := by cases f <;> decide
+ obtain ⟨⟨h, hh, hhv⟩, ⟨g, hg, hgv⟩, hbl⟩ :=
+ the_readers_recover_the_colours_new_and_with_blink_pack c (colorIndex f) (colorIndex b % 8)
+ hf (Nat.mod_lt _ (by decide)) false (by rw [hv]; simp)
+ exact ⟨c, hc, hbl, ⟨g, hg, hgv⟩, ⟨h, hh, hhv⟩⟩
/-- `screenchar_as_u16` never fails and packs the attribute into the high byte
and the character into the low byte, so the attribute is `w / 256`, the
@@ -207,6 +222,7 @@ theorem screenchar_as_u16_of_the_blank_cell_is_0x0720 :
#print axioms NonosExtraction.VgaConstants.the_colorcode_is_blinking_wrapper_is_its_method
#print axioms NonosExtraction.VgaConstants.the_colorcode_value_wrapper_is_its_method
#print axioms NonosExtraction.VgaConstants.the_screenchar_as_u16_wrapper_is_its_method
+#print axioms NonosExtraction.VgaConstants.the_colorcode_new_wrapper_is_its_method
#print axioms NonosExtraction.VgaConstants.shifting_a_byte_right_by_four_is_the_bitvector_shift
#print axioms NonosExtraction.VgaConstants.shifting_a_word_left_by_eight_is_the_bitvector_shift
#print axioms NonosExtraction.VgaConstants.colorcode_foreground_is_the_low_nibble
@@ -215,7 +231,8 @@ theorem screenchar_as_u16_of_the_blank_cell_is_0x0720 :
#print axioms NonosExtraction.VgaConstants.colorcode_is_blinking_is_the_top_half_of_the_byte
#print axioms NonosExtraction.VgaConstants.colorcode_value_is_blink_background_and_foreground_recombined
#print axioms NonosExtraction.VgaConstants.the_readers_recover_the_colours_new_and_with_blink_pack
-#print axioms NonosExtraction.VgaConstants.a_bright_background_from_new_decodes_as_blinking_on_the_dark_one
+#print axioms NonosExtraction.VgaConstants.colorcode_new_masks_the_background_to_three_bits
+#print axioms NonosExtraction.VgaConstants.colorcode_new_never_blinks
#print axioms NonosExtraction.VgaConstants.screenchar_as_u16_is_attribute_high_and_character_low
#print axioms NonosExtraction.VgaConstants.screenchar_as_u16_of_the_blank_cell_is_0x0720
diff --git a/verification/extraction/lean/NonosExtraction/WalkerAlignRefinement.lean b/verification/extraction/lean/NonosExtraction/WalkerAlignRefinement.lean
index 1323d2e964..df81733bc3 100644
--- a/verification/extraction/lean/NonosExtraction/WalkerAlignRefinement.lean
+++ b/verification/extraction/lean/NonosExtraction/WalkerAlignRefinement.lean
@@ -21,6 +21,7 @@ them take are stated once in NonosExtraction.Shapes.
-/
import NonosExtraction.WalkerAlign
+import NonosExtraction.Bits
open Aeneas Aeneas.Std Result
open nonos_x_walker_align
@@ -35,8 +36,60 @@ namespace NonosExtraction.WalkerAlign
theorem the_align4_wrapper_is_its_method (a : Std.Usize) :
align4 a = align.align4 a := rfl
+/-! ### Rounding a cursor up to the next four byte boundary
+
+ The flattened device tree walker advances its cursor with `align4` after a
+ node name and after a property's data, and the format pads both to four
+ bytes. These theorems say that `align4` returns the least multiple of four
+ at or above its argument whenever `n + 3` fits in a `usize`, and that it
+ halts with an overflow, rather than wrapping to a small cursor, exactly when
+ it does not. They cannot say that the walker's cursor stays inside the
+ structure block: the walker is not extracted, and its bounds checks happen
+ before the call. -/
+
+/-- Below the top of the word, `align4` is the ceiling of `n` to a multiple of
+ four: `(n + 3) / 4 * 4`, which is at least `n`, less than `n + 4`, and
+ divisible by four. -/
+theorem align4_rounds_up_to_the_next_multiple_of_four (n : Std.Usize)
+ (h : n.val + 3 ≤ Usize.max) :
+ ∃ r, align4 n = ok r ∧ r.val = (n.val + 3) / 4 * 4 ∧
+ r.val % 4 = 0 ∧ n.val ≤ r.val ∧ r.val < n.val + 4 := by
+ unfold align4 align.align4
+ have he := UScalar.add_equiv n 3#usize
+ cases hz : n + 3#usize with
+ | ok z =>
+ rw [hz] at he
+ obtain ⟨_, hzv, _⟩ := he
+ simp only [lift, bind_tc_ok]
+ have hr : (z &&& ~~~3#usize).val = (n.val + 3) / 4 * 4 := by
+ rw [Bits.land_not_low_mask z 3#usize 2 (by rfl), hzv]
+ rfl
+ refine ⟨_, rfl, hr, ?_, ?_, ?_⟩ <;> rw [hr] <;> omega
+ | fail e =>
+ rw [hz] at he
+ simp [UScalar.inBounds] at he
+ scalar_tac
+ | div => rw [hz] at he; exact he.elim
+
+/-- In the last three values of the word there is no multiple of four at or
+ above the input that the word can hold, and `align4` halts on the addition
+ instead of returning a masked sum near zero. -/
+theorem align4_halts_exactly_when_rounding_leaves_the_word (n : Std.Usize)
+ (h : Usize.max < n.val + 3) :
+ align4 n = fail .integerOverflow := by
+ unfold align4 align.align4
+ have hf : n + 3#usize = fail .integerOverflow := by
+ show UScalar.tryMk _ _ = _
+ simp only [UScalar.tryMk, UScalar.tryMkOpt]
+ rw [dif_neg (by scalar_tac)]
+ rfl
+ rw [hf]
+ rfl
+
/-! ### Axiom profile -/
#print axioms NonosExtraction.WalkerAlign.the_align4_wrapper_is_its_method
+#print axioms NonosExtraction.WalkerAlign.align4_rounds_up_to_the_next_multiple_of_four
+#print axioms NonosExtraction.WalkerAlign.align4_halts_exactly_when_rounding_leaves_the_word
end NonosExtraction.WalkerAlign
diff --git a/verification/extraction/paging/Cargo.lock b/verification/extraction/paging/Cargo.lock
new file mode 100644
index 0000000000..36c26bf50f
--- /dev/null
+++ b/verification/extraction/paging/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_paging"
+version = "0.2.0"
diff --git a/verification/extraction/rv_flags/Cargo.lock b/verification/extraction/rv_flags/Cargo.lock
new file mode 100644
index 0000000000..f679306aa8
--- /dev/null
+++ b/verification/extraction/rv_flags/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_rv_flags"
+version = "0.1.0"
diff --git a/verification/extraction/signal/Cargo.lock b/verification/extraction/signal/Cargo.lock
new file mode 100644
index 0000000000..9ea035d10d
--- /dev/null
+++ b/verification/extraction/signal/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_signal"
+version = "0.2.0"
diff --git a/verification/extraction/sweep/bti_pad/Cargo.lock b/verification/extraction/sweep/bti_pad/Cargo.lock
new file mode 100644
index 0000000000..5517e44813
--- /dev/null
+++ b/verification/extraction/sweep/bti_pad/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_x_bti_pad"
+version = "0.1.0"
diff --git a/verification/extraction/sweep/bti_pad/Cargo.toml b/verification/extraction/sweep/bti_pad/Cargo.toml
new file mode 100644
index 0000000000..36c5adab8f
--- /dev/null
+++ b/verification/extraction/sweep/bti_pad/Cargo.toml
@@ -0,0 +1,14 @@
+# NONOS Operating System
+# Copyright (C) 2026 NONOS Contributors
+#
+# Extraction root mirroring src/arch/aarch64/security/bti/pad.rs. The kernel source is included with
+# #[path], never copied, so Charon reads the MIR rustc compiles.
+[package]
+name = "nonos_x_bti_pad"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
diff --git a/verification/extraction/sweep/bti_pad/src/lib.rs b/verification/extraction/sweep/bti_pad/src/lib.rs
new file mode 100644
index 0000000000..c1501651e9
--- /dev/null
+++ b/verification/extraction/sweep/bti_pad/src/lib.rs
@@ -0,0 +1,13 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+//! Extraction root mirroring `src/arch/aarch64/security/bti/pad.rs`.
+//!
+//! The forwarding functions below exist because a Charon entry point cannot
+//! name an inherent method. They add no logic.
+
+#[path = "../../../../../src/arch/aarch64/security/bti/pad.rs"]
+pub mod pad;
+
+pub fn is_bti_landing_pad(instruction: u32) -> bool {
+ pad::is_bti_landing_pad(instruction)
+}
+
diff --git a/verification/extraction/sweep/utils_types/src/lib.rs b/verification/extraction/sweep/utils_types/src/lib.rs
index e6349bacff..3420d89403 100644
--- a/verification/extraction/sweep/utils_types/src/lib.rs
+++ b/verification/extraction/sweep/utils_types/src/lib.rs
@@ -19,3 +19,7 @@ pub fn scanconfig_hidden_only(this: types::ScanConfig) -> types::ScanConfig {
this.hidden_only()
}
+
+pub fn scanconfig_admits_hidden(this: types::ScanConfig, hidden: bool) -> bool {
+ this.admits_hidden(hidden)
+}
diff --git a/verification/extraction/sweep/variable_firmware/src/lib.rs b/verification/extraction/sweep/variable_firmware/src/lib.rs
index 15c702baae..ed26d6bc6e 100644
--- a/verification/extraction/sweep/variable_firmware/src/lib.rs
+++ b/verification/extraction/sweep/variable_firmware/src/lib.rs
@@ -1,5 +1,6 @@
// NONOS Operating System (AGPL-3.0-or-later)
-//! Extraction root mirroring `src/arch/x86_64/uefi/variable/firmware.rs`.
+//! Extraction root mirroring `src/arch/x86_64/uefi/variable/firmware.rs` and
+//! `src/arch/x86_64/uefi/constants/revisions.rs`.
//!
//! The forwarding functions below exist because a Charon entry point cannot
//! name an inherent method. They add no logic.
@@ -7,6 +8,9 @@
#[path = "../../../../../src/arch/x86_64/uefi/variable/firmware.rs"]
pub mod firmware;
+#[path = "../../../../../src/arch/x86_64/uefi/constants/revisions.rs"]
+pub mod revisions;
+
pub fn firmwareinfo_uefi_major_version(this: firmware::FirmwareInfo) -> u16 {
this.uefi_major_version()
}
@@ -15,3 +19,7 @@ pub fn firmwareinfo_uefi_minor_version(this: firmware::FirmwareInfo) -> u16 {
this.uefi_minor_version()
}
+
+pub fn uefi_revision(major: u16, minor: u16, patch: u16) -> u32 {
+ revisions::uefi_revision(major, minor, patch)
+}
diff --git a/verification/extraction/sweep/vga_constants/src/lib.rs b/verification/extraction/sweep/vga_constants/src/lib.rs
index b7f5ac0435..a639471f1f 100644
--- a/verification/extraction/sweep/vga_constants/src/lib.rs
+++ b/verification/extraction/sweep/vga_constants/src/lib.rs
@@ -27,3 +27,7 @@ pub fn screenchar_as_u16(this: constants::ScreenChar) -> u16 {
this.as_u16()
}
+
+pub fn colorcode_new(foreground: constants::Color, background: constants::Color) -> constants::ColorCode {
+ constants::ColorCode::new(foreground, background)
+}
diff --git a/verification/extraction/tree/iommu_regs_window/Cargo.lock b/verification/extraction/tree/iommu_regs_window/Cargo.lock
new file mode 100644
index 0000000000..73c9a471a6
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_x_iommu_regs_window"
+version = "0.1.0"
diff --git a/verification/extraction/tree/iommu_regs_window/Cargo.toml b/verification/extraction/tree/iommu_regs_window/Cargo.toml
new file mode 100644
index 0000000000..b74542c2bf
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/Cargo.toml
@@ -0,0 +1,14 @@
+# NONOS Operating System
+# Copyright (C) 2026 NONOS Contributors
+#
+# Extraction root mirroring src/arch/x86_64/iommu/regs/window.rs. The kernel source is included with
+# #[path], never copied, so Charon reads the MIR rustc compiles.
+[package]
+name = "nonos_x_iommu_regs_window"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
diff --git a/verification/extraction/tree/iommu_regs_window/src/arch/mod.rs b/verification/extraction/tree/iommu_regs_window/src/arch/mod.rs
new file mode 100644
index 0000000000..b4434d329c
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/arch/mod.rs
@@ -0,0 +1,5 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+pub mod x86_64;
+
+pub use x86_64::*;
diff --git a/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/mod.rs b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/mod.rs
new file mode 100644
index 0000000000..c96f265664
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/mod.rs
@@ -0,0 +1,3 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+pub mod regs;
diff --git a/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/mod.rs b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/mod.rs
new file mode 100644
index 0000000000..e9a09a7439
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/mod.rs
@@ -0,0 +1,9 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+#[path = "../../../../../../../../../src/arch/x86_64/iommu/regs/cap/mod.rs"]
+pub mod cap;
+
+pub mod offsets;
+
+#[path = "../../../../../../../../../src/arch/x86_64/iommu/regs/window.rs"]
+pub mod window;
diff --git a/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/offsets/mod.rs b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/offsets/mod.rs
new file mode 100644
index 0000000000..523fa057b5
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/iommu/regs/offsets/mod.rs
@@ -0,0 +1,6 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+#[path = "../../../../../../../../../../src/arch/x86_64/iommu/regs/offsets/invalidate.rs"]
+pub mod invalidate;
+
+pub use invalidate::{iotlb_offset, iva_offset, CCMD, CCMD_CIRG_GLOBAL, CCMD_ICC, IOTLB_IAIG_MASK, IOTLB_IIRG_GLOBAL, IOTLB_IVT};
diff --git a/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/mod.rs b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/mod.rs
new file mode 100644
index 0000000000..ba53d9a33b
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/arch/x86_64/mod.rs
@@ -0,0 +1,3 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+pub mod iommu;
diff --git a/verification/extraction/tree/iommu_regs_window/src/lib.rs b/verification/extraction/tree/iommu_regs_window/src/lib.rs
new file mode 100644
index 0000000000..ef4a8474ef
--- /dev/null
+++ b/verification/extraction/tree/iommu_regs_window/src/lib.rs
@@ -0,0 +1,14 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+//! Extraction root, with the modules it refers to, mirroring `src/arch/x86_64/iommu/regs/window.rs`.
+//!
+//! The forwarding functions below exist because a Charon entry point cannot
+//! name an inherent method. They add no logic.
+
+extern crate alloc;
+
+pub mod arch;
+
+pub fn registers_fit(cap: u64, ecap: u64, window: usize) -> bool {
+ crate::arch::x86_64::iommu::regs::window::registers_fit(cap, ecap, window)
+}
+
diff --git a/verification/extraction/tree/layout_stack_slots/Cargo.lock b/verification/extraction/tree/layout_stack_slots/Cargo.lock
new file mode 100644
index 0000000000..5c7d38d6e4
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_x_layout_stack_slots"
+version = "0.1.0"
diff --git a/verification/extraction/tree/layout_stack_slots/Cargo.toml b/verification/extraction/tree/layout_stack_slots/Cargo.toml
new file mode 100644
index 0000000000..e8059d7f1f
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/Cargo.toml
@@ -0,0 +1,14 @@
+# NONOS Operating System
+# Copyright (C) 2026 NONOS Contributors
+#
+# Extraction root mirroring src/memory/layout/manager/stack_slots.rs. The kernel source is included with
+# #[path], never copied, so Charon reads the MIR rustc compiles.
+[package]
+name = "nonos_x_layout_stack_slots"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/lib.rs"
+
+[dependencies]
diff --git a/verification/extraction/tree/layout_stack_slots/src/lib.rs b/verification/extraction/tree/layout_stack_slots/src/lib.rs
new file mode 100644
index 0000000000..0055ee0fbe
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/src/lib.rs
@@ -0,0 +1,14 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+//! Extraction root, with the modules it refers to, mirroring `src/memory/layout/manager/stack_slots.rs`.
+//!
+//! The forwarding functions below exist because a Charon entry point cannot
+//! name an inherent method. They add no logic.
+
+extern crate alloc;
+
+pub mod memory;
+
+pub fn stack_slot_offset(slot: usize) -> u64 {
+ crate::memory::layout::manager::stack_slots::stack_slot_offset(slot)
+}
+
diff --git a/verification/extraction/tree/layout_stack_slots/src/memory/layout/manager/mod.rs b/verification/extraction/tree/layout_stack_slots/src/memory/layout/manager/mod.rs
new file mode 100644
index 0000000000..fb775fc66f
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/src/memory/layout/manager/mod.rs
@@ -0,0 +1,4 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+#[path = "../../../../../../../../src/memory/layout/manager/stack_slots.rs"]
+pub mod stack_slots;
diff --git a/verification/extraction/tree/layout_stack_slots/src/memory/layout/mod.rs b/verification/extraction/tree/layout_stack_slots/src/memory/layout/mod.rs
new file mode 100644
index 0000000000..7da31d820a
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/src/memory/layout/mod.rs
@@ -0,0 +1,9 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+#[path = "../../../../../../../src/memory/layout/constants/mod.rs"]
+pub mod constants;
+
+pub mod manager;
+
+pub use constants::*;
+pub use manager::*;
diff --git a/verification/extraction/tree/layout_stack_slots/src/memory/mod.rs b/verification/extraction/tree/layout_stack_slots/src/memory/mod.rs
new file mode 100644
index 0000000000..3d4cb6f31d
--- /dev/null
+++ b/verification/extraction/tree/layout_stack_slots/src/memory/mod.rs
@@ -0,0 +1,3 @@
+// NONOS Operating System (AGPL-3.0-or-later)
+
+pub mod layout;
diff --git a/verification/extraction/uefi_attrs/Cargo.lock b/verification/extraction/uefi_attrs/Cargo.lock
new file mode 100644
index 0000000000..dd914a9611
--- /dev/null
+++ b/verification/extraction/uefi_attrs/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_uefi_attrs"
+version = "0.1.0"
diff --git a/verification/extraction/vectors/Cargo.lock b/verification/extraction/vectors/Cargo.lock
new file mode 100644
index 0000000000..b7163e3c41
--- /dev/null
+++ b/verification/extraction/vectors/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "nonos_vectors"
+version = "0.2.0"