Skip to content

Lean verification: prove 164 more functions and fix the kernel bugs the proofs found - #591

Merged
eKisNonos merged 48 commits into
mainfrom
lean/substantive
Sep 30, 2026
Merged

eKisNonos merged 48 commits into
mainfrom
lean/substantive

Conversation

@eKisNonos

Copy link
Copy Markdown
Contributor

What this does

This continues the Lean work merged in #590. It raises the number of kernel functions with a real proved property from 330 to 494, tightens the counter so a function only counts when a theorem actually states something about it and fixes 15 kernel defects that the proofs brought to light. Every fix comes with a host test that fails against the old code.

Numbers

All percentages are of the 11,450 functions in src/. The target is 1,717 functions with a property beyond their wrapper.

main this branch
extracted and diffed in CI 981 (8.57%) 987 (8.62%)
carrying a proof 981 987
with a property beyond the wrapper 330 (2.88%) 494 (4.31%)
extracted but unproved 0 0

Main reports 336 with the old counter. The 330 above is what main scores under the stricter counter this PR introduces, so the two columns are measured the same way. The floors in proven_functions.py are raised to 987 and 494 and the gap ceiling stays at zero. No gate was lowered.

A stricter counter

proven_functions.py used to count a function as substantive whenever its name appeared anywhere in a refinement file, prose included. It now reads only theorem statements, follows "open ... renaming" and abbreviations to the names they stand for, and ignores the theorems that only show a wrapper calls its method. At the time of the change that dropped six functions the old counter had credited: the three constant-time select functions, ct_is_zero_u64, ct_clz_u64, the IOMMU second-level index and address-width checks, and the two page table block predicates. Each of them now has a theorem of its own.

Kernel fixes

Each fix is its own commit. The extraction is regenerated in the same commit and the theorem that recorded the defect is restated to the fixed behaviour. A separate kernel_proofs commit follows with a test that includes the kernel file by path and fails against the old code. Every restated theorem was also checked against a deliberately broken copy of the new generated code, and fails on it.

Security relevant:

  • IOMMU register window. The IRO and FRO fields can place a remapping unit's IOTLB and fault registers up to about 20 KiB in, but the kernel maps a single 4 KiB page and only checked the bound with debug_assert, so a release build could write outside the mapping. probe_at now refuses such a unit through registers_fit, and the accessors assert the bound in every build.
  • BTI landing pads on aarch64. The check accepted a NOP, which faults as a branch target on a core with BTI, and refused PACIASP and PACIBSP, which act as a landing pad. is_bti_landing_pad now accepts exactly the five valid instructions.
  • Per-CPU stack guards. Each CPU's first IST stack started where the kernel stack ended, so the page the guard code treated as the kernel stack's upper guard was really the IST stack's first page. stack_slot_offset now puts a guard below, between and above every stack, and the proof shows the kernel stack ends exactly one page below the first IST stack.
  • procfs inode numbers. Directory and entry inodes for a process could collide with root entries or with another process, and pid 0 had a directory. Pids from 1 are now numbered pid shifted left by 20, which provably misses every other inode.
  • PCI configuration address. Device and function were not masked when packed into the 0xCF8 address, so device 32 on bus 0 reached device 0 on bus 1.
  • RSDP extended checksum. A revision 2 RSDP without the ACPI 2.0 fields passed on the old 1.0 checksum alone, and the three reserved bytes and the length were never checked. All 36 bytes are now summed.

Lower severity:

  • Firmware and counter arithmetic that aborted the kernel on overflow now saturates: the UEFI memory descriptor size and end, the per-CPU region end (whose contains test also lost the last byte at the top of memory), the I/O APIC gsi_max, and the MMIO and port statistics totals.
  • is_symtab also accepted the dynamic symbol table, so get_symbol_table could return .dynsym. It now accepts only the full symbol table.
  • VGA attributes shifted the whole background into the top nibble, so any bright background set the blink bit. The background is now masked to three bits.
  • The UEFI revision constants mixed two encodings: 2.8 read back as minor 2048 while 2.3.1 used the specification's form, so 2.3.1 sorted below 2.1, and the manager stored a third encoding. All of them are now built by one function that follows the specification.
  • requires_authentication ignored the older count-based authenticated write flag, 0x10, which firmware still reports on variables written before UEFI 2.3.1.
  • ScanConfig's hidden_only did nothing, because every configuration already included hidden files. It now asks for hidden files only.
  • EFI time validation accepted February 31, and every date from 1900 to 1969 converted as a date in 1970. The day is now checked against the calendar and earlier years count back from the epoch, so 1969-12-31 is -86400.
  • PhysAddr and VirtAddr rounded by clearing low bits, which only works for powers of two: aligning 10 down to 3 gave 8, which is_aligned then refused, and an alignment of 0 halted the kernel. Rounding now uses the remainder and works for any alignment.

Kept on purpose, and said so in the proof: is_user_space refuses the topmost user page, which avoids the SYSRET return to a non-canonical address, the same choice Linux makes. Recorded but not changed: reset_ticks does not fit the model of a clock that only moves forward, and nothing calls it.

Refinements committed

74 refinement files that were written earlier but never committed were checked one by one. Each compiles against the current tree with no error, warning or sorry, was read for padding and dashes, and was run against a broken copy of its generated code: a changed literal, a flipped boolean or operator, or for simple constructors and readers a swapped or zeroed field. Every one of them fails under its broken copy. Two of them recorded the address rounding and port statistics defects fixed above. The rest are committed in five groups by area: CPU context and syscall registers, memory attributes and IOMMU capabilities, interrupt controllers and PCI, firmware tables and clocks, and allocator statistics with the pure synchronisation models. Fifteen other files had nothing worth proving, being empty skeletons, duplicates of committed crates, or stubs, and were left out.

5 older refinements were restated because the defects they recorded are now fixed: the invalidation and fault register offsets, the per-CPU stack region, the user space bound, and the tick counter.

Housekeeping

  • core.num.U64.wrapping_neg, which the extraction leaves opaque, is registered in ASSUMPTIONS.md. The theorem that reads it takes two's complement negation as a stated hypothesis, so no proof leans on the axiom.
  • Ten extraction crates that were committed without a Cargo.lock now have one, like the other 281. Each lockfile names only its own crate.

Checks run locally

  • Full lake build of the extraction project, 2,301 jobs, with no sorry anywhere.
  • stated_axioms.py passes on that build, and proven_functions.py reports the counts above.
  • regen.py shows no drift for every crate touched, and collect-evidence.sh matches EVIDENCE.json on a clean worktree of every proof commit.
  • Kernel cargo check for x86_64 after every kernel commit, with the same 43 warnings as main.
  • kernel_proofs: all 84 tests pass and clippy is clean with warnings denied.
  • The stub, allow and unreachable hygiene scripts pass.

A strict audit found six functions counted as substantive because they
appeared in an open ... renaming line or a local simp attribute list,
with no theorem statement about them. Each now has one:

- ct_select_usize returns its first operand on true and its second on
  false, on a 32-bit or 64-bit usize; ct_select_u32 and ct_select_u64
  the same for every pair of operands.
- ct_clz_u64 returns 64 for zero and otherwise the shift n with
  2^63 <= x * 2^n < 2^64, given the Rust meaning of wrapping_neg, which
  the extraction leaves opaque; ct_is_zero_u64 is one exactly at zero
  under the same hypothesis.
- the IOMMU index_for is the nine-bit slice above the level shift for
  levels one to six and fails at level zero; fits_address_width is
  exact for every width.
- is_block is present-and-huge on x86_64 and valid-and-not-table on
  aarch64.

Each theorem was checked against a mutant of the generated code.
The classifier counted a function as substantive when its name appeared
anywhere outside the wrapper theorems, and comments were the only thing
it stripped. An open ... renaming line, a local simp attribute list or a
#print axioms line was enough, and six functions counted that way with
no theorem about them. It now reads only the statements of non-wrapper
theorems, following the names a file gives a function through open ...
renaming and abbrev. The previous commit proved those six, so the count
stays at the floor.
probe_at maps 4 KiB of a remapping unit, but the IOTLB register sits at
IRO * 16 + 8 and the fault records at FRO * 16, with IRO and FRO read
from ECAP and CAP and reaching 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!, so in a release build a unit
reporting IRO or FRO of 256 or more got volatile MMIO reads and writes
outside the mapping.

probe_at now refuses such a unit with ProbeError::RegistersOutsideWindow,
using registers_fit, and the accessors assert the window in every build.
A unit with a register set larger than a page is refused rather than
mapped in full.

registers_fit is extracted. Its refinement reads the IRO, FRO and NFR
fields exactly, states registers_fit holds exactly when both register
sets end inside the window, and proves that for an accepted unit the
IOTLB register and every fault record end inside the page.
Runs registers_fit over every IRO and a spread of FRO and NFR values
against the byte bounds. The test does not build against the old probe,
which had no window check.
check_bti_landing_pad accepted 0xD503201F, the NOP that BtiGuard::None
emits, and on a core with FEAT_BTI an indirect branch to a NOP in a
guarded page raises a Branch Target exception. It also rejected PACIASP
and PACIBSP, which the architecture treats as BTI c. The decision is now
is_bti_landing_pad in pad.rs, which accepts BTI c, BTI j, BTI jc,
PACIASP and PACIBSP, and the check reads the word and asks it. A bare
BTI, which accepts no branch type, stays rejected.

is_bti_landing_pad is extracted. Its refinement gives the accepted set
exactly and proves every BtiGuard but None emits a landing pad. The
BtiGuard refinement drops its theorem about the old accepted list.
Calls the kernel landing-pad check on BTI c, j, jc, PACIASP, PACIBSP,
the NOP and a bare BTI. The test fails against the check that
accepted the NOP.
get_all_stack_regions packed each CPU's kernel stack and IST stacks back
to back, while get_guard_regions takes a guard page below and above each
one. 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 report a live neighbouring stack as a
compromised guard, or, with the guards unmapped, a stack would lose its
top page. The stacks now start at stack_slot_offset, which leaves a
guard page below the first stack, between every two and above the last.

stack_slot_offset is extracted. Its refinement gives the offsets exactly,
proves any two slots are separated by a full guard page, and that the
area with its guards fits the per-CPU stride.
Checks every stack slot's guard pages against every other slot and the
area against the per-CPU stride. The test does not build against the
old layout, which had no stack_slot_offset.
Pid directories were numbered pid * 1000 + 100 and their entries
(pid << 20) | k. 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
/proc root. Pid 131072's directory got 131072100, the inode of pid 125's
task entry. A pid directory is now pid << 20, its entries keep k from 1
to 103, and only pids from 1 get a directory, so every pid inode lies
above the root entries (at most 105) and no two collide.

The refinement is restated: pid_dir_inode is none exactly for a pid of
zero or below, pid * 2^20 otherwise, at least 2^20, and never an entry
inode (q << 20) | k of any pid.
Checks that pids below one have no directory inode and that the
inodes of pids 1 to 4095 sit above the root entries with clear entry
bits. Both tests fail against the pid * 1000 + 100 numbering.
pci_config_address shifted the device and function into place without
masking them, so device 32 on bus 0 produced the address of device 0 on
bus 1 and a configuration read or write went to another device.
PciAddress::new does not check its fields, so only from_bdf callers were
in range. Device is now masked to five bits and function to three.

The extractions are regenerated. The refinement, verified here, states
the word for every input, reads each field back, and proves an
out-of-range device stays on its bus.
Runs pci_config_address over every device and function number and
checks the fields read back. The test fails against the unmasked
encoder.
AcpiRsdp::verify_extended_checksum passed a revision 2 RSDP that carried
none of the ACPI 2.0 fields as the ACPI 1.0 check alone, never summed
the three reserved bytes at offsets 33 to 35, and never checked length.
The ACPI parser's RsdpExtended::validate_extended_checksum does all
three, so the two checkers disagreed. The multiboot parser now keeps the
reserved bytes, and from revision 2 the check requires every extended
field, a length of at least 36, and the 36 bytes summing to zero.

The refinement states the check exactly for every shape of the fields,
and that a revision 2 RSDP missing a field or shorter than 36 bytes
fails. No kernel code calls the check yet.
Builds a revision 2 RSDP with a correct extended checksum, then tampers
with a reserved byte, drops the extended fields and shortens the length;
only the intact one passes. The test does not build against the old
structure, which had no reserved bytes.
Four methods added or multiplied values that firmware or a wrapping
counter controls with an overflow-checked operator, which with
overflow-checks on in every profile halts the kernel:
MemoryDescriptor::size_bytes and end_address on a descriptor reaching
the top of the address space or claiming 2^52 pages, PercpuRegion::end
and contains on a region flush against the top, IoApicInfo::gsi_max on a
MADT base above 0xFFFFFFE8, and MmioStatsSnapshot::total_operations past
u64::MAX. Each now saturates; PercpuRegion::contains measures the offset
from the base, so the last page holds its own bytes. size_bytes uses an
explicit bound rather than saturating_mul, which the extraction leaves
opaque.

The extractions are regenerated. The committed memory descriptor
refinement and the pending per-CPU, I/O APIC and snapshot refinements,
verified here, state the saturated values and that none of the calls
can fail.
Runs the memory descriptor, per-CPU region, I/O APIC and MMIO snapshot
methods at the top of their ranges. All three tests fail against the
overflow-checked arithmetic.
ParsedSection::is_symtab accepted SHT_DYNSYM as well as SHT_SYMTAB, so
ElfLoader::get_symbol_table, which returns the first section it
accepts, returned .dynsym on an image that places it before .symtab.
get_dynsym already exists for that section, and the bootloader's copy
accepts SHT_SYMTAB alone. get_symbol_table has no caller today.

The refinement states is_symtab holds exactly for type 2 and refuses a
dynamic symbol table.
Includes the loader's section type and checks section types 0 to 63.
The test fails against the check that accepted SHT_DYNSYM.
Bit 7 of a text attribute is blink with the attribute controller's
default setting, which nothing in the tree changes. ColorCode::new,
ColorCode::with_blink and the boot make_attr shifted the whole
background into bits 4 to 7, so a bright background set bit 7 and drew
blinking text on the dark one, and the boot bg_color read bit 7 as a
background bit where ColorCode::background treats it as blink. The
background is now masked to three bits everywhere, so a bright
background is drawn dark and only with_blink sets bit 7.

ColorCode::new is added to the vga_constants extraction. Its refinement
proves the packed byte for all 256 colour pairs and that the cell never
blinks; the vga_colors refinement, verified here, states the three-bit
background for make_attr and bg_color.
Runs make_attr over every background and checks bit 7 stays clear and
bg_color reads back three bits. The test fails against the helpers that
used the whole nibble.
The UEFI_REVISION constants put the minor version in the upper byte of
the low half, so 2.8 was 0x00020800 and read back as minor 2048, while
2.3.1 alone used the specification's tens-and-units form and sorted
below 2.1. detect_firmware_info stored a third encoding, 0x00020008,
beside the version string "2.8". A comparison of a firmware's real
revision with these constants would have ordered versions wrongly.

The constants are now built by uefi_revision, (major << 16) | minor * 10
+ patch, and detect_firmware_info stores UEFI_REVISION_2_8.

uefi_revision is extracted beside the firmware record. The refinement
proves it never fails, gives the specification's packing whenever the
tens-and-units field fits sixteen bits, reads back through the record
as the major and minor field it packed, and yields 0x0002001F,
0x00020050 and 0x00020064 for 2.3.1, 2.8 and 2.10.
Checks every UEFI_REVISION constant against the specification's value,
that they sort in version order, and that the firmware record reads
UEFI_REVISION_2_8 back as major 2, minor 80. All three fail against the
old constants.
requires_authentication tested the time-based and enhanced
authenticated-write flags but not AUTHENTICATED_WRITE_ACCESS (0x10).
UEFI 2.3.1 deprecates that flag, but firmware still reports it on
variables written before, and a write to such a variable must still
carry an authentication descriptor. A set carrying only 0x10 was
answered as needing no authentication, while the same set with 0x20
added was answered as needing it.

The extraction is regenerated. The refinement, verified here, restates
the predicate as bit 4, 5 or 7 of the word for every word, and records
that 0x10 alone now needs authentication.
Checks that AUTHENTICATED_WRITE_ACCESS,
TIME_BASED_AUTHENTICATED_WRITE_ACCESS and ENHANCED_AUTHENTICATED_ACCESS
each make a set need authentication, alone and on top of
DEFAULT_NV_BS_RT, and that the other defined flags do not. Fails against
the old requires_authentication, which ignored 0x10. The revision test
is reformatted with the crate's rustfmt.
hidden_only set include_hidden, which every fresh configuration already
has, and the scanner read that field only to drop hidden files when it
was false. ScanConfig::new().hidden_only() was ScanConfig::new(), so a
scan meant to list hidden files returned every visible file as well.

The configuration gains only_hidden, hidden_only sets it, and the
scanner asks admits_hidden, which requires include_hidden for a hidden
file and a clear only_hidden for a visible one.

admits_hidden is extracted beside the builders. The refinement proves
it reads the two flags for every configuration, that after hidden_only
any configuration admits exactly the hidden files, and that the fresh
configuration admits every file but no visible one after hidden_only.
Checks that a fresh scan configuration admits hidden and visible files,
that hidden_only admits hidden files and drops visible ones, and that
turning include_hidden off drops hidden files and keeps visible ones.
The old code had no admits_hidden, so the test does not build against
it; its inline filter, !(!include_hidden && hidden), admitted a visible
file under ScanConfig::new().hidden_only(), which the second check
rejects.
EfiTime::is_valid bounded the day by 31 in every month, so 2021-02-31
passed and to_unix_timestamp read it as 2021-03-03. It now bounds the
day by days_in_month, with a 29 day February in Gregorian leap years.

to_unix_timestamp counted years forward from 1970 only. is_valid
accepts years from 1900, and for those the loop was empty, so
1969-12-31 converted to the same second as 1970-12-31. A second loop
now counts years from the date up to 1970 back from the epoch, so
1969-12-31 is -86400 and 1900-01-01 is -2208988800.

The extraction is regenerated. The refinement, verified here and
committed for the first time, characterises is_valid exactly on the
UEFI ranges with the calendar day bound, runs all three loops of
to_unix_timestamp to completion for every month from one to twelve,
proves no step overflows, and pins the epoch, 2000-03-01, 1969-12-31,
1900-01-01 and the February 29 cases.
Checks that is_valid refuses days past the end of the month, including
February 29 outside leap years and in 2100, and accepts the last day of
each case; and that to_unix_timestamp gives 0 at the epoch, -86400 for
1969-12-31, -2208988800 for 1900-01-01 and the known values for
1970-12-31 and 2000-03-01. Both tests fail against the old code.
On a 64 bit target is_user_space accepts exactly the ranges whose
unbounded end is at most 2^47 - 1, never fails, refuses 2^47 for every
length, and refuses the page sized range naming the topmost user page
while accepting that range less its last byte. The prose says why the
top page stays refused: a SYSCALL in its last bytes would SYSRET to a
non-canonical address, the hazard Linux avoids the same way.
iva_offset is the ten-bit IRO field of ECAP in sixteen byte units and
iotlb_offset is eight bytes past it; neither fails for any ECAP. The
IOTLB register fits the one mapped page exactly when IRO is at most
255, and IRO 256 lands at byte 4104. The prose records that the bound
is enforced by registers_fit in probe_at, which used to be missing.
fault_recording_offset is the ten-bit FRO field of CAP in sixteen byte
units and fault_recording_count is the NFR field plus one, between 1
and 256; neither fails for any word. The first record leaves the mapped
page exactly when FRO reaches 256. The prose records that registers_fit
in probe_at now refuses such a unit, where only a debug_assert stood.
TypesStack recorded that the first IST stack began at the kernel
stack's top, so the page get_guard_regions took as the kernel stack's
upper guard was the IST stack's first page. The layout now places
stacks through stack_slot_offset. The recording theorem is renamed to
what it still proves, that the top is the base plus 64 KiB, and a new
theorem joins it with stack_slot_offset: the kernel stack at slot 0
tops out exactly one page below the IST stack at slot 1.
get_ticks returns the relaxed load unchanged, increment_ticks succeeds
exactly when fetch_add of one succeeds whatever the old value, so the
counter wraps rather than panicking in the timer interrupt, and
reset_ticks stores the value the static starts with. The prose records
that reset_ticks cannot be squared with the monotone clock of
Nonos.Timer and that nothing calls it.
PhysAddr and VirtAddr align_down and align_up cleared low bits with
!(align - 1), which rounds only when align is a power of two. Aligning
ten down to three gave eight, which is_aligned then refused, and an
alignment of zero underflowed and halted, where is_aligned answers
false. Every caller passes a page size today.

Both now round with the remainder: align_down subtracts it, align_up
adds what is missing to the next multiple, and an alignment of zero
leaves the address unchanged. align_up overflows only when the rounded
address is past u64::MAX. is_aligned uses is_multiple_of, the same
test, as clippy asks.

The extractions are regenerated. The refinements, verified here and
committed for the first time, prove for every address and non-zero
alignment that align_down is the largest multiple at or below and
align_up the smallest at or above when it fits, that both pass
is_aligned and lie within one alignment, and restate the old witnesses.
Checks, over alignments including 3, 7, 12 and 4097, that PhysAddr
align_down and align_up return an address that passes is_aligned and
lies within one alignment on the right side, the three and page
witnesses for VirtAddr, and that an alignment of zero leaves both
unchanged. All three fail against the old masks.
PortStatsSnapshot::total_ops and total_bytes summed counters with +.
The counters wrap in fetch_add, so a snapshot can hold any values, and
with overflow checks on in every profile a sum past u64::MAX aborted
the kernel. PortStats::total_ops, which PortManager reports, did the
same over four live loads. The snapshot sums now saturate, as the MMIO
snapshot's do, and PortStats::total_ops takes a snapshot and asks it.

The extraction is regenerated. The refinement, verified here and
committed for the first time, proves both totals never fail and are
the saturated sums of their fields, with io_delays never counted.
Checks that the port snapshot's byte and operation totals saturate at
u64::MAX on the first sum past it and count the four operation kinds
but not I/O delays. Fails against the old unchecked sums.
Zeroed aarch64 and riscv64 user contexts hold no register and are what
the entry guards refuse; the zeroed FPU states round to nearest with no
flag and, on aarch64, enable no trap where a zero MXCSR traps every
condition. rflags sanitisation keeps exactly the bits outside the
privileged positions and resumes user code with IOPL 0 and interrupts
on. Syscall arguments read the register at their index and refuse
index six and above; the CPL is the low two bits of CS; page fault info
reads one bit per flag and agrees with the dispatch model; signal
errors map to their errnos. Each file was checked against a mutant of
its generated code that makes a theorem false.
MAIR attribute indices fit ATTRINDX, are injective and name the byte
each type encodes; an empty page table entry is the decoding of zero
and maps nothing. The VT-d capability readers take exactly their bits:
caching mode, write buffer flush, the domain count, the address width,
the best leaf level and the fault record reason and source. The MMIO
range check admits exactly the non-empty windows that do not overflow,
and align4 rounds up to a multiple of four and halts only when that
leaves the word. Each file was checked against a mutant of its
generated code that makes a theorem false.
GIC and PLIC bases are stored and read where they are created; the IRQ
reservation table reads and sets bit gsi mod 64 of word gsi div 64 and
refuses lines past 256; MADT overrides read both polarity and trigger
bits. HPET timer registers sit 0x20 apart, never alias and fit below
0x500 for the 32 reportable timers. The APIC divider encodes each
supported divisor as the SDM reads it and the calibration stays in its
bounds. MSI-X entries split the address losslessly, device ids match on
vendor and device alone, and the I2C, PIO and bridge readers are exact.
Each file was checked against a mutant of its generated code that makes
a theorem false.
AML controllers are valid exactly when base and size are nonzero; SDT
entry counts cover the table and never read past it; SRAT processor
entries read bit zero and the little endian domain; the L2 associativity
decode matches the CPUID table. Nine UEFI memory types are neither
usable nor reserved, recorded as a hazard for a future caller. The civil
calendar is the Gregorian rule and its months add up to the year; BCD
converts both ways below one hundred; the RTC periodic rate is the
datasheet code and divider output. Each file was checked against a
mutant of its generated code that makes a theorem false.
Buddy, heap and region statistics give the free memory as the
saturating or truncated difference and fill the total; a region total
past u64::MAX is refused, not wrapped. The semaphore, seqlock and ring
index arithmetic refine the tier-one models, including the seqlock
counter wrapping and the ring refusing a zero capacity. Field element
constants denote zero and one, entropy byte counts are the ceiling of
bits over eight, and the constructors keep their arguments. Each file
was checked against a mutant of its generated code that makes a theorem
false.
ct_is_zero_u64 calls u64::wrapping_neg, which the extraction leaves
opaque, and the full build's axiom listing names it. The theorem that
reads it takes two's complement negation as a stated hypothesis, so the
axiom carries no weight in a proof; ASSUMPTIONS.md now says so, and
stated_axioms.py passes on a clean full build.
align, ct, ed_field, elf, iommu, paging, rv_flags, signal, uefi_attrs
and vectors were committed without the Cargo.lock that every other
extraction crate carries. Each names only its own package, so the
lockfile pins nothing external; it keeps the crates alike and stops a
build from leaving them untracked.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Release-mode integer wrapping can bypass new IOMMU bounds checks, and oversized RSDP payload bytes remain unchecked.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Expands Lean verification coverage, adds extraction roots and regression tests, and fixes kernel defects uncovered by proofs.

Changes:

  • Adds substantive Lean properties and tighter proof accounting.
  • Fixes firmware, memory, IOMMU, PCI, procfs, BTI, VGA, and alignment defects.
  • Adds host regression tests and missing extraction lockfiles.
File Description
verification/​extraction/​vectors/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​uefi_attrs/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​tree/​layout_stack_slots/​src/​memory/​mod.rs Mirrors memory module.
verification/​extraction/​tree/​layout_stack_slots/​src/​memory/​layout/​mod.rs Mirrors layout modules.
verification/​extraction/​tree/​layout_stack_slots/​src/​memory/​layout/​manager/​mod.rs Includes stack-slot source.
verification/​extraction/​tree/​layout_stack_slots/​src/​lib.rs Adds extraction wrapper.
verification/​extraction/​tree/​layout_stack_slots/​Cargo.toml Defines extraction crate.
verification/​extraction/​tree/​layout_stack_slots/​Cargo.lock Locks extraction crate.
verification/​extraction/​tree/​iommu_regs_window/​src/​lib.rs Adds register-window wrapper.
verification/​extraction/​tree/​iommu_regs_window/​src/​arch/​x86_64/​mod.rs Mirrors x86 module.
verification/​extraction/​tree/​iommu_regs_window/​src/​arch/​x86_64/​iommu/​regs/​offsets/​mod.rs Includes offset logic.
verification/​extraction/​tree/​iommu_regs_window/​src/​arch/​x86_64/​iommu/​regs/​mod.rs Mirrors register modules.
verification/​extraction/​tree/​iommu_regs_window/​src/​arch/​x86_64/​iommu/​mod.rs Mirrors IOMMU module.
verification/​extraction/​tree/​iommu_regs_window/​src/​arch/​mod.rs Mirrors architecture module.
verification/​extraction/​tree/​iommu_regs_window/​Cargo.toml Defines extraction crate.
verification/​extraction/​tree/​iommu_regs_window/​Cargo.lock Locks extraction crate.
verification/​extraction/​sweep/​vga_constants/​src/​lib.rs Exposes color constructor.
verification/​extraction/​sweep/​variable_firmware/​src/​lib.rs Exposes revision packing.
verification/​extraction/​sweep/​utils_types/​src/​lib.rs Exposes hidden-file predicate.
verification/​extraction/​sweep/​bti_pad/​src/​lib.rs Adds BTI extraction wrapper.
verification/​extraction/​sweep/​bti_pad/​Cargo.toml Defines BTI extraction.
verification/​extraction/​sweep/​bti_pad/​Cargo.lock Locks BTI extraction.
verification/​extraction/​signal/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​rv_flags/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​paging/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​lean/​NonosExtraction/​WalkerAlignRefinement.lean Proves four-byte alignment.
verification/​extraction/​lean/​NonosExtraction/​VgaColors.lean Regenerates VGA model.
verification/​extraction/​lean/​NonosExtraction/​VariableFirmware.lean Models revision packing.
verification/​extraction/​lean/​NonosExtraction/​ValidationSimdLevelRefinement.lean Proves SIMD widths.
verification/​extraction/​lean/​NonosExtraction/​UtilsTypes.lean Models hidden-only scans.
verification/​extraction/​lean/​NonosExtraction/​Uefi.lean Models authentication flags.
verification/​extraction/​lean/​NonosExtraction/​TypesStatsSnapshot.lean Models saturating totals.
verification/​extraction/​lean/​NonosExtraction/​TypesSnapshotRefinement.lean Proves empty DMA statistics.
verification/​extraction/​lean/​NonosExtraction/​TypesRegionStatsRefinement.lean Proves saturating free memory.
verification/​extraction/​lean/​NonosExtraction/​TypesPteRefinement.lean Proves empty PTE semantics.
verification/​extraction/​lean/​NonosExtraction/​TypesProtectionRefinement.lean Proves protection predicates.
verification/​extraction/​lean/​NonosExtraction/​TypesPercpu.lean Models safe region arithmetic.
verification/​extraction/​lean/​NonosExtraction/​TypesFirmwareRefinement.lean Proves empty firmware handoff.
verification/​extraction/​lean/​NonosExtraction/​TypesBridgeRefinement.lean Characterizes default bridge.
verification/​extraction/​lean/​NonosExtraction/​TrampolinePerApRefinement.lean Proves boot-context construction.
verification/​extraction/​lean/​NonosExtraction/​TablesMemoryDesc.lean Models saturating descriptors.
verification/​extraction/​lean/​NonosExtraction/​SignalErrorRefinement.lean Proves errno mappings.
verification/​extraction/​lean/​NonosExtraction/​Riscv64FpuContextRefinement.lean Proves zeroed FPU state.
verification/​extraction/​lean/​NonosExtraction/​RegistryVersionRefinement.lean Proves version construction.
verification/​extraction/​lean/​NonosExtraction/​RegistersPlicRefinement.lean Proves PLIC base preservation.
verification/​extraction/​lean/​NonosExtraction/​RedistributorDeviceRefinement.lean Proves redistributor addresses.
verification/​extraction/​lean/​NonosExtraction/​ProcfsTypesRefinement.lean Updates inode documentation.
verification/​extraction/​lean/​NonosExtraction/​ProcfsPidInode.lean Models collision-free inodes.
verification/​extraction/​lean/​NonosExtraction/​PortStatsSnapshot.lean Models saturating port totals.
verification/​extraction/​lean/​NonosExtraction/​PlonkTypesRefinement.lean Proves empty PLONK values.
verification/​extraction/​lean/​NonosExtraction/​PioTypesRefinement.lean Proves PIO widths.
verification/​extraction/​lean/​NonosExtraction/​PciTypesMsixRefinement.lean Proves MSI-X construction.
verification/​extraction/​lean/​NonosExtraction/​PciInfoRefinement.lean Proves I/O-window detection.
verification/​extraction/​lean/​NonosExtraction/​PagingRefinement.lean Proves block predicates.
verification/​extraction/​lean/​NonosExtraction/​PacKeyRefinement.lean Proves PAC key construction.
verification/​extraction/​lean/​NonosExtraction/​MteModeRefinement.lean Proves MTE encodings.
verification/​extraction/​lean/​NonosExtraction/​MemoryFrameAllocTypesRange.lean Regenerates alignment model.
verification/​extraction/​lean/​NonosExtraction/​MainModeRefinement.lean Proves microkernel mode.
verification/​extraction/​lean/​NonosExtraction/​LayoutStackSlots.lean Models guarded stack offsets.
verification/​extraction/​lean/​NonosExtraction/​IommuProtectionRefinement.lean Proves IOMMU permissions.
verification/​extraction/​lean/​NonosExtraction/​IommuDomainIdRefinement.lean Proves domain identifiers.
verification/​extraction/​lean/​NonosExtraction/​HpetProtectionRefinement.lean Proves HPET decoding.
verification/​extraction/​lean/​NonosExtraction/​HeapTypesStatsRefinement.lean Proves heap free-memory behavior.
verification/​extraction/​lean/​NonosExtraction/​DriversPciTypesAddress.lean Models masked PCI addresses.
verification/​extraction/​lean/​NonosExtraction/​DistributorDeviceRefinement.lean Proves distributor addresses.
verification/​extraction/​lean/​NonosExtraction/​DispatchArgsRefinement.lean Proves syscall argument placement.
verification/​extraction/​lean/​NonosExtraction/​DiagCplRefinement.lean Proves CPL decoding.
verification/​extraction/​lean/​NonosExtraction/​DataStatsRefinement.lean Proves zeroed ACPI statistics.
verification/​extraction/​lean/​NonosExtraction/​DataProcessorRefinement.lean Proves processor construction.
verification/​extraction/​lean/​NonosExtraction/​DataIoapicRefinement.lean Proves saturated GSI maximum.
verification/​extraction/​lean/​NonosExtraction/​DataIoapic.lean Models saturated GSI arithmetic.
verification/​extraction/​lean/​NonosExtraction/​CtPrimitivesRefinement.lean Updates constant-time proof status.
verification/​extraction/​lean/​NonosExtraction/​CoreSection.lean Models strict symbol tables.
verification/​extraction/​lean/​NonosExtraction/​ContractArgsRefinement.lean Proves indexed syscall arguments.
verification/​extraction/​lean/​NonosExtraction/​ContextCpuContextRefinement.lean Proves initial CPU context.
verification/​extraction/​lean/​NonosExtraction/​ConstantsAddressPacking.lean Models masked PCI packing.
verification/​extraction/​lean/​NonosExtraction/​ChainErrorRefinement.lean Proves recoverable errors.
verification/​extraction/​lean/​NonosExtraction/​CensusBufRefinement.lean Proves empty line buffer.
verification/​extraction/​lean/​NonosExtraction/​CapBehaviourRefinement.lean Proves capability bits.
verification/​extraction/​lean/​NonosExtraction/​CacheTypesRefinement.lean Proves cache counter initialization.
verification/​extraction/​lean/​NonosExtraction/​BtiPad.lean Models BTI landing pads.
verification/​extraction/​lean/​NonosExtraction/​BtiGuardRefinement.lean Proves BTI guard encodings.
verification/​extraction/​lean/​NonosExtraction/​AlgIdTypesRefinement.lean Proves algorithm identifiers.
verification/​extraction/​lean/​NonosExtraction/​AddrVirt.lean Regenerates virtual alignment.
verification/​extraction/​lean/​NonosExtraction/​AddrPhys.lean Regenerates physical alignment.
verification/​extraction/​lean/​NonosExtraction/​Aarch64ContextTypesRefinement.lean Proves zeroed AArch64 contexts.
verification/​extraction/​iommu/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​elf/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​ed_field/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​ct/​Cargo.lock Adds extraction lockfile.
verification/​extraction/​crates.json Registers new extraction roots.
verification/​extraction/​ASSUMPTIONS.md Records wrapping-negation assumption.
verification/​extraction/​align/​Cargo.lock Adds extraction lockfile.
userland/​kernel_proofs/​src/​vga_attr/​tests.rs Tests VGA attribute masking.
userland/​kernel_proofs/​src/​vga_attr/​mod.rs Wires VGA regression tests.
userland/​kernel_proofs/​src/​uefi_revision/​tests.rs Tests UEFI revision encoding.
userland/​kernel_proofs/​src/​uefi_revision/​mod.rs Wires revision tests.
userland/​kernel_proofs/​src/​uefi_attrs/​tests.rs Tests authentication flags.
userland/​kernel_proofs/​src/​uefi_attrs/​mod.rs Wires attribute tests.
userland/​kernel_proofs/​src/​scan_hidden/​tests.rs Tests hidden-only scans.
userland/​kernel_proofs/​src/​scan_hidden/​mod.rs Wires scan tests.
userland/​kernel_proofs/​src/​rsdp_address/​tests.rs Tests RSDP validation.
userland/​kernel_proofs/​src/​rsdp_address/​mod.rs Documents RSDP regressions.
userland/​kernel_proofs/​src/​procfs_inode/​tests.rs Tests inode uniqueness.
userland/​kernel_proofs/​src/​procfs_inode/​mod.rs Documents inode regressions.
userland/​kernel_proofs/​src/​pci_address/​tests.rs Tests PCI field masking.
userland/​kernel_proofs/​src/​pci_address/​mod.rs Includes PCI address helper.
userland/​kernel_proofs/​src/​lib.rs Registers new test modules.
userland/​kernel_proofs/​src/​layout_slots/​tests.rs Tests stack guards.
userland/​kernel_proofs/​src/​layout_slots/​mod.rs Wires stack-layout tests.
userland/​kernel_proofs/​src/​layout_slots/​manager/​mod.rs Includes stack-slot source.
userland/​kernel_proofs/​src/​layout_slots/​constants/​mod.rs Includes layout constants.
userland/​kernel_proofs/​src/​iommu_window/​tests.rs Tests IOMMU bounds.
userland/​kernel_proofs/​src/​iommu_window/​mod.rs Wires IOMMU tests.
userland/​kernel_proofs/​src/​firmware_arith/​tests.rs Tests saturating arithmetic.
userland/​kernel_proofs/​src/​firmware_arith/​mod.rs Wires arithmetic tests.
userland/​kernel_proofs/​src/​elf/​loader/​core/​mod.rs Includes section implementation.
userland/​kernel_proofs/​src/​elf_section_tests.rs Tests symbol-table filtering.
userland/​kernel_proofs/​src/​efi_time/​tests.rs Tests EFI calendar conversion.
userland/​kernel_proofs/​src/​efi_time/​mod.rs Wires EFI time tests.
userland/​kernel_proofs/​src/​bti_pad/​tests.rs Tests valid landing pads.
userland/​kernel_proofs/​src/​bti_pad/​mod.rs Wires BTI tests.
userland/​kernel_proofs/​src/​addr_align/​tests.rs Tests arbitrary alignments.
userland/​kernel_proofs/​src/​addr_align/​mod.rs Wires alignment tests.
src/​memory/​mmio/​types/​stats_snapshot.rs Saturates MMIO totals.
src/​memory/​layout/​types/​percpu.rs Fixes top-address regions.
src/​memory/​layout/​manager/​stack_slots.rs Defines guarded stack offsets.
src/​memory/​layout/​manager/​percpu.rs Applies stack offsets.
src/​memory/​layout/​manager/​mod.rs Registers stack-slot module.
src/​memory/​addr/​virt.rs Fixes virtual alignment.
src/​memory/​addr/​phys.rs Fixes physical alignment.
src/​fs/​utils/​types.rs Implements hidden-only configuration.
src/​fs/​utils/​scan_config.rs Applies hidden-file predicate.
src/​fs/​procfs/​pid/​entry.rs Shares inode shift constant.
src/​fs/​procfs/​pid_inode.rs Fixes process inode numbering.
src/​elf/​loader/​core/​section.rs Restricts full symbol tables.
src/​drivers/​pci/​constants/​address_packing.rs Masks PCI fields.
src/​boot/​vga/​colors.rs Masks VGA backgrounds.
src/​arch/​x86_64/​vga/​constants.rs Fixes color-code packing.
src/​arch/​x86_64/​uefi/​types/​attributes.rs Recognizes legacy authentication.
src/​arch/​x86_64/​uefi/​tables/​time.rs Fixes calendar validation.
src/​arch/​x86_64/​uefi/​tables/​memory_desc.rs Saturates descriptor arithmetic.
src/​arch/​x86_64/​uefi/​manager/​init.rs Uses canonical revision constant.
src/​arch/​x86_64/​uefi/​constants/​revisions.rs Standardizes revision encoding.
src/​arch/​x86_64/​port/​stats_types.rs Reuses saturated snapshot total.
src/​arch/​x86_64/​port/​stats_snapshot.rs Saturates port totals.
src/​arch/​x86_64/​multiboot/​state/​parse/​firmware.rs Parses RSDP reserved bytes.
src/​arch/​x86_64/​multiboot/​modules_acpi.rs Strengthens RSDP checksum checks.
src/​arch/​x86_64/​iommu/​unit/​report/​failure.rs Reports window-bound failures.
src/​arch/​x86_64/​iommu/​unit/​probe.rs Rejects oversized register layouts.
src/​arch/​x86_64/​iommu/​unit/​access.rs Makes MMIO checks unconditional.
src/​arch/​x86_64/​iommu/​regs/​window.rs Validates register extents.
src/​arch/​x86_64/​iommu/​regs/​mod.rs Exposes window validation.
src/​arch/​x86_64/​acpi/​data/​ioapic.rs Saturates GSI maximum.
src/​arch/​aarch64/​security/​bti/​pad.rs Defines valid BTI pads.
src/​arch/​aarch64/​security/​bti/​landing.rs Uses shared BTI validation.
src/​arch/​aarch64/​security/​bti.rs Registers BTI pad module.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/arch/x86_64/iommu/unit/access.rs Outdated
Comment thread src/arch/x86_64/multiboot/modules_acpi.rs Outdated
RemapUnit's accessors checked offset + width <= UNIT_WINDOW. An offset
near usize::MAX wraps that sum to a small value wherever overflow checks
are off, so the check passed and the volatile access went outside the
mapped page; with overflow checks on, as in the kernel profiles, the
same offset panicked in the accessor instead of failing its bound. Each
accessor now checks offset <= UNIT_WINDOW - width, which cannot wrap
for any offset. Reported in review of #591.
Runs the kernel's RemapUnit accessors against a buffer standing in for
the mapped page: the last 32 and 64 bit registers in the window read
back, and offsets past it, including ones near usize::MAX that wrap
offset + width, are refused. The second test fails against the old
check.
verify_extended_checksum accepted any declared length of 36 or more
while summing only the 36 bytes the structure keeps. The extended
checksum covers the length the table declares, so a longer declaration
left the bytes past 36 unchecked and still passed. Only the 36 byte
structure is supported, so any other length is now refused. Reported in
review of #591.

The extraction is regenerated and the refinement restates the check as
a length of exactly 36; the theorem for short tables now covers every
length other than 36.
Checks that a revision 2 RSDP whose 36 bytes sum to zero but whose
declared length is 37, 40, 4096 or u32::MAX fails the extended check.
Fails against the old length test, which accepted any length of 36 or
more.
@eKisNonos

Copy link
Copy Markdown
Contributor Author

Review follow-ups: the IOMMU register accessors bound offsets by subtraction so no offset can wrap past the check, and the ACPI 2.0 RSDP check accepts only the 36 byte length the parser keeps.

Fixed in 3cd4611. Each accessor now checks offset <= UNIT_WINDOW - width instead of offset + width <= UNIT_WINDOW, so no offset can wrap past the bound. The kernel profiles build with overflow checks on, so the old sum would have panicked rather than wrapped, but that still turned a bad offset into a crash inside the accessor and a build without overflow checks did wrap. c7a1b01 adds a kernel_proofs test that runs the accessors against a buffer standing in for the page; it fails against the old check for offsets near usize::MAX.

Fixed in 224bc6f. The structure keeps only the 36 bytes of an ACPI 2.0 RSDP, so the extended check now refuses any declared length other than 36 instead of accepting longer ones with unchecked bytes. The Lean refinement is restated to a length of exactly 36, and 175d2e5 adds a test that lengths 37, 40, 4096 and u32::MAX are refused which fails against the old check.

@eKisNonos
eKisNonos merged commit dceba0e into main Sep 30, 2026
66 checks passed
eKisNonos added a commit that referenced this pull request Sep 30, 2026
Main gained #591 (dceba0e). Its new kernel_proofs modules and this
branch's ipc_peers_tests extend the same module list in
userland/kernel_proofs/src/lib.rs; the list keeps both. kernel_proofs:
89 passed; the kernel checks clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants