Set up Kani and add proof harnesses for memory/paging - #47
Open
chbaker0 wants to merge 4 commits into
Open
Conversation
Harnesses live in `#[cfg(kani)] mod verify` alongside each module's tests. Fixes: align_u64_up overflow, next_level clearing PRESENT, fill_bitmap over-marking, Map::iter_type scanning the dummy tail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the README alias-list conflict, wraps two harness `deallocate` calls now that the trait method is `unsafe`, and drops a `needless_range_loop` in `mark_frames_free` for the newly-gated clippy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2ab6bda89
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
It aligned the extent's start up unchecked, so a start in the final partial page panicked. Returns None now, like shrink_to_alignment. The harness had assumed that input away on an incorrect premise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
It has never produced an explained verdict: FAILURE in 30s once, then no result in ~50 minutes. Dropping #[kani::proof] is the only way to exclude a harness — Kani ignores #[ignore]. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Filed by an AI coding agent (Claude Code).
Sets up Kani over
shared, adds 76proof harnesses concentrated on
memory/memory::paging/memory::alloc,and fixes the four defects they turned up.
Why Kani here
cargo stestsamples inputs andcargo smiriwatches one concrete executionfor UB. Neither says anything about the rest of the input domain, which is
exactly where the paging and bitmap-allocator code is interesting: a page-table
walk that is right for the addresses a test happens to pick and wrong for one
index quadruple is invisible to both. Kani proves a property over every input in
a symbolic domain, or hands back a concrete counterexample.
Kani and Miri turn out to be complements on
paging.rsspecifically: Miri drivesPhysTableStore's realread_volatile/write_volatilewalks and checks them forUB; Kani drives the same
Mapper::maptraversal through a pointer-freearray-backed store and proves it computes the right answer for every page.
Neither subsumes the other.
Defects found and fixed
Full write-ups, counterexamples and reachability analysis in
docs/kani-findings.md. In short:align_u64_upoverflowed near the top of the address space, reached viaExtent::shrink_to_alignment. Panics with overflow checks on; wraps withthem off, which would have
iter_map_frameshand the allocator frames nearaddress 0 that have nothing to do with its input. Added
align_u64_up_checked/Address::align_up_checked;shrink_to_alignmentpropagates
None, which is the correct answer.next_levelcould clearPRESENTand detach a live subtree whilemapcarried on writing into it and returned
Ok(())— a mapping that silentlydoes not exist, plus a leaked table on the next
mapthrough that slot. Theallocate branch already forced
PRESENT; only the reuse branch didn't. Fixedand documented on both
next_levelandMapper::map.fill_bitmap_from_mapmis-marked small regions, two ways: a subtractionunderflow for any region ending below frame 8, and — the dangerous one — a
region confined to a single bitmap byte marked the whole byte free. The
leading and trailing partial-byte phases each widened to a byte boundary and
ORed together.
BitmapFrameAllocator::new'sunsafecontract is "all framesmarked free must be available and not used by other code", so those extra bits
are firmware memory, kernel image, or MMIO handed out as ordinary RAM. Both
are reachable at boot — we just haven't seen a UEFI map that produces such a
region. Replaced the three-phase split with a uniform per-byte loop that clips
the range to each byte it touches; the uniform form cannot express either bug.
Map::iter_typefilteredself.entries, the whole 128-slot backing array,instead of
self.entries(), thenum_entriesprefix — returning up to 128phantom
Reservedextents at address 0.Nice detail on (4): that bug was also the tractability blocker. The end-to-end
fill_bitmapharness had to unwind two 128-iteration loops and wouldn't settlein ten minutes; with the fix it verifies in 5 seconds. A representation bug and a
verification-performance problem turned out to be the same bug.
Known-failing harnesses
Five harnesses fail on purpose and are marked
KNOWN FAILUREin their doccomments, catalogued under "Open" in the findings doc. They pin two real,
unfixed defects in
alloc::phys— left out of this PR to keep it to theverification work plus the fixes that were needed to make the harnesses pass:
find_bit_group's mask is(len << 1) - 1— that's2·len − 1, not2^len − 1. Coincides forlen1 and 2, diverges at 4. The existing unittests pass by accident: every
len == 4byte they try happens to have itsfourth bit agree with the other three. The consequence is a physical frame
handed out twice:
That breaks
FrameAllocator's documentedunsafeinvariant, which everySAFETYcomment inmm.rsabout "frames not in use anywhere else" rests on.Not hit today only because
allocate()uses order 0;HeapProviderderivesits order from a chunk count, so a 4-chunk heap request reaches it.
allocate_rangepanics instead of reporting exhaustion. Thesize >= 8path returns
Noneonly from inside the loop; when the bitmap length dividesevenly by the chunk length that early return never fires and control reaches
unreachable!(). So the ordinary "no run of 8 free frames available" outcomethe
Optionexists to express panics the kernel.Both fixes are one-liners (
(1usize << len) - 1;Noneafter the loop) and theharnesses that prove them are already written and fast — happy to fold them in
here or do them separately, whichever you prefer for review.
A third open item,
Extent::from_range_exclusivebypassingnew_checked'snon-empty invariant, is left alone deliberately: it's an API-semantics decision
(returning
Optionwould changeconstcall sites inmm::VirtualMap), not amechanical fix.
How the harnesses are written
Documented in
docs/verification.md; three conventions:use a universally-quantified probe address —
contains_addr(result, p) == spec(p)is extensional set equality. Two-extent ops name their answer'sendpoints directly (
overlap=[max(starts), min(lasts)]), which isequivalent for intervals but far cheaper than a third symbolic
u64.Mapper::mapgets an independenttranslateoracle that walks tables the wayhardware does and shares no code with
Mapper.assumeis a precondition, written down. Where a harness narrows itsdomain, that narrowing is the contract; if it isn't in the doc comment, the
harness says so. This is how (1) surfaced.
#[kani::should_panic]pins the other side of a contract —num_pagesatzero,
set_addrat 2^52,deallocateon a double free.Harnesses sit in a
#[cfg(kani)] mod verifyblock at the bottom of each module,same placement and style as
mod tests. Being a child of the module under proofis what lets them reach private items —
TableStore,align_u64_down,find_bit_group,PageTableEntry::raw,BlockAdapter— where most of theinteresting invariants actually live.
Not done here
harnesses take 2-3 minutes each, and five currently fail by design. Wiring up
model-checking/kani-github-actionwants a decision about which subset to gateon and probably wants the two
alloc::physfixes landed first.two_mappings_do_not_interfere, is held out of the suite —its
#[kani::proof]attribute is deliberately absent. It has never producedan explained verdict: FAILURE in ~30s with a placeholder message once, then no
verdict at all after ~50 minutes on a later run of the same code. Suspicion is
an unwinding-assertion artifact on
ArrayStore<6>'s array initializer ratherthan a real counterexample, but that is untested, and the nearly identical
map_leaves_every_other_page_unmapped(ArrayStore<3>, same unwind bound)passes. The body still compiles so it doesn't rot. Worth knowing: Kani does
not honour
#[ignore], and 0.67 has no--exclude-harness, so dropping theproof attribute is the only mechanism available.
Test plan
cargo stest— 56 passed (3 new unit tests inalloc::physcovering theregion shapes that defect 3 got wrong).
cargo kcheck/lcheck/icheck— clean, no warnings.cargo kani -p shared --only-codegen— all harnesses compile;cargo kani listreports 76.
map_then_translate_round_trips_for_any_page_and_frame(127s, all pages ×all frames),
remap_replaces_the_leaf_entirely(181s),map_2m/map_1g_translates_across_the_whole_huge_page,mark_frames_free_marks_exactly_its_frames(3s),fill_bitmap_marks_exactly_the_available_frames(5s).shared, and CI'ssmokejob covers it.