Add support for unaligned_volatile_load and unaligned_volatile_store - #4673
Conversation
89ad219 to
68437b9
Compare
|
Marking this as a draft until #4672 is reviewed and lands. Nothing has changed in the content — this is just to make the ordering explicit, since GitHub cannot base a cross-fork PR on a fork branch and this diff therefore currently shows #4672's commits as well. Once #4672 merges I will rebase this down to its own two commits and mark it ready. |
68437b9 to
ebfdfb8
Compare
|
Pushed a follow-up commit after a review pass. One item was a regression this PR itself introduced, so flagging it explicitly rather than folding it in quietly. ICE. Adding the The same omission existed in ZST guard. Endianness. The oracle tests no longer assume little-endian — they use Verified: The soundness comment was also reworded — see the note on #4672; the same conservative-direction correction applies here. |
There was a problem hiding this comment.
Pull request overview
This PR completes support for Rust’s remaining volatile intrinsics tracked in #1163 by implementing unaligned_volatile_load / unaligned_volatile_store and (via the included dependency commit) volatile_copy_memory, volatile_copy_nonoverlapping_memory, and volatile_set_memory. It removes the last unstable_codegen!-gated volatile intrinsics from kani-compiler codegen and adds regression tests to validate both correct behavior and expected failure modes.
Changes:
- Implement codegen support for
unaligned_volatile_store(newIntrinsicvariant) and un-gateunaligned_volatile_loadby lowering it to a dereference without alignment assertions. - Implement codegen for
volatile_copy_memory,volatile_copy_nonoverlapping_memory, andvolatile_set_memory, including proper argument reordering for the copy intrinsics and plumbing through analyses. - Add passing and expected-failure tests for the new intrinsics and update the intrinsics support documentation to reflect partial support.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/kani/VolatileIntrinsics/core_intrinsics.rs | Removes kani-verify-fail header now that the intrinsic set used by the test is supported. |
| tests/kani/Intrinsics/Volatile/unaligned.rs | Adds byte-precise oracle proofs for unaligned volatile load/store (including symbolic offset + ZST store). |
| tests/kani/Intrinsics/Volatile/set.rs | Adds passing proofs for volatile_set_memory behavior. |
| tests/kani/Intrinsics/Volatile/copy.rs | Adds passing proofs for volatile copy intrinsics, including an overlap case for memmove semantics. |
| tests/expected/intrinsics/volatile_set/out-of-bounds/main.rs | Adds an expected-failure test for out-of-bounds volatile_set_memory. |
| tests/expected/intrinsics/volatile_set/out-of-bounds/expected | Expected output for the volatile set out-of-bounds failure. |
| tests/expected/intrinsics/volatile_copy/unaligned/main.rs | Adds an expected-failure test for unaligned volatile_copy_memory destination. |
| tests/expected/intrinsics/volatile_copy/unaligned/expected | Expected output for the unaligned volatile copy failure. |
| tests/expected/intrinsics/volatile_copy/overlapping/main.rs | Adds an expected-failure test for overlapping volatile_copy_nonoverlapping_memory. |
| tests/expected/intrinsics/volatile_copy/overlapping/expected | Expected output for the overlapping volatile copy failure. |
| kani-compiler/src/kani_middle/transform/check_uninit/ptr_uninit/uninit_visitor.rs | Extends memory-init tracking for UnalignedVolatileStore and models VolatileSetMemory like WriteBytes. |
| kani-compiler/src/kani_middle/points_to/points_to_analysis.rs | Updates aliasing and store modeling for UnalignedVolatileStore and marks VolatileSetMemory identity-aliasing. |
| kani-compiler/src/intrinsics.rs | Adds UnalignedVolatileStore and VolatileSetMemory to the intrinsic enum and instance decoding. |
| kani-compiler/src/codegen_cprover_gotoc/codegen/intrinsic.rs | Implements new intrinsic codegen arms, swaps args for volatile copy, reuses codegen_write_bytes for volatile set, and removes unstable_codegen!. |
| docs/src/rust-feature-support/intrinsics.md | Updates support status for the affected volatile intrinsics from No → Partial. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ebfdfb8 to
8dc149d
Compare
|
One documentation fix, and a note on where the other review findings went. The oracle comment named the wrong constructor, and that was self-inflicted. The codegen comment for The other two findings were about #4672's files, not this PR's. This PR currently contains #4672's On a question that may come up: the Regression re-run locally on CBMC 6.10.0 against a baseline build of |
Completes the volatile intrinsic family on tracking issue model-checking#1163, on top of the volatile copy/set support. With these two, `unstable_codegen!` has no remaining uses and is removed: these were the last gated intrinsics in the codegen match. Modelling --------- Neither intrinsic has an alignment requirement, so neither emits an alignment assertion -- tolerating a misaligned pointer is the whole point of the `unaligned_*` variants. `unaligned_volatile_load` was already sketched (behind the gate) as a plain dereference; `unaligned_volatile_store` had no codegen and no Intrinsic variant at all, and is added mirroring `volatile_store` minus the alignment check. Dereferenceability is still checked by --pointer-check, as for the aligned variants. The question this leaves is whether a misaligned typed dereference is modelled byte-precisely rather than silently assuming alignment. The tests answer it directly rather than assuming: each proof compares the accessed value against a byte-wise oracle at a deliberately misaligned offset, so an implementation that quietly touched the *aligned* word would fail rather than pass. Reading a u32 at byte offset 1 of a known pattern, the only correct little-endian answer is 0x55443322; an alignment-assuming read from offset 0 would give 0x44332211. A third proof leaves the offset symbolic and compares against u32::from_le_bytes at every offset, so the result cannot be reached by constant folding. Tests ----- tests/kani/Intrinsics/Volatile/unaligned.rs byte-precise load at a misaligned offset; byte-precise store checking the neighbouring bytes are untouched; symbolic-offset load against a byte-wise oracle. tests/kani/VolatileIntrinsics/core_intrinsics.rs was marked kani-verify-fail; that expectation existed only because unaligned_volatile_store (and volatile_set_memory) were unsupported. With both implemented the proof verifies -- 0 of 120 failed -- so the kani-verify-fail header is removed and it becomes a passing test. Note: tests/kani/VolatileIntrinsics/main_fixme.rs is left untouched (it is skipped as a fixme test). Its test_copy_volatile names its arguments as though volatile_copy_memory were (src, dst, count); the assertion it makes happens to hold under either argument order, so it neither catches nor is broken by the ordering. Worth a separate look if that suite is ever revived.
…ort table Same reasoning as for the copy/set trio: the table listed both as `No`, and `Partial` matches the neighbouring volatile rows. The memory-safety semantics are modelled -- including the byte-precise misaligned access these two exist for -- but the volatile guarantees are not, since Kani assumes sequential execution.
…rop endianness assumption
Review findings on the previous commit. The first is a regression the commit
introduced and is the reason for this one.
1. ICE. Adding the `UnalignedVolatileStore` variant without teaching the
points-to analysis about it routed it into that pass's terminal
`unimplemented!()`. Before this feature the intrinsic resolved to
`Intrinsic::Unimplemented`, which the pass handles gracefully -- so the
feature turned a clean "unsupported" into a compiler panic for anyone running
-Z uninit-checks over code calling it. It now shares the `VolatileStore` arm;
the aliasing semantics (`*a = b`) are identical. The asymmetry was visible in
the diff: the *load* was handled, the store was not.
2. Same omission in the memory-initialization visitor: `unaligned_volatile_store`
fell to the catch-all and reported "unsupported" rather than marking its
destination initialized. It now shares the `VolatileStore` arm there too.
Both were the same root cause -- the compiler matches on `Intrinsic` in three
places (codegen, points-to, uninit visitor) and they must be updated as a set.
3. ZST. `unaligned_volatile_store` dereferenced unconditionally, while
`codegen_volatile_store` guards exactly that case ("do not attempt to
dereference (and assign) a ZST"). A ZST pointer may legally be
dangling-but-aligned, so the path is reachable. Guard replicated and a
regression test added.
4. Tests no longer assume little-endian. The oracles now use
`u32::from_ne_bytes` / `to_ne_bytes`, which is equally byte-precise and
discriminates the same way -- an alignment-assuming read of bytes 0..4 differs
from the correct 1..5 under either endianness -- without pinning the suite to
one target.
Verified: -Z uninit-checks over code calling both `unaligned_volatile_store` and
`volatile_set_memory` now verifies successfully, with no panic and no spurious
memory-initialization failure.
…uses The codegen comment for the `unaligned_*` intrinsics said the oracle tests compare against `u32::from_le_bytes`. They compare against `u32::from_ne_bytes`, and deliberately so: native order keeps the oracle byte-precise without assuming an endianness. This is a stale comment of my own making. The endianness assumption was dropped from the tests in the earlier follow-up commit on this branch and the comment documenting the oracle was not updated with it.
…ntrinsics The existing tests for unaligned_volatile_load/store all use a valid, in-bounds pointer, so two claims the implementation makes went untested. The codegen comment says dereferenceability is still checked by --pointer-check even though this path builds the dereference directly and bypasses place codegen. Nothing exercised that. The new expected-fail test reads and writes a u32 at byte offset 1 of a 4-byte array: the pointer arithmetic stays in bounds, so it is the access that overruns the object and the failure is attributable to the dereference rather than to the offset computation. The expected file asserts that split explicitly -- the offset safety check must SUCCEED while pointer_dereference reports "dereference failure: pointer outside object bounds". The ZST guard was in the same position. check_zst_unaligned_volatile_store stores through &mut zst, a valid pointer, which a dereference would happily succeed on, so the guard could be deleted without failing it. The new harness uses without_provenance_mut(align_of::<()>()) -- dangling but aligned, which is legal for a ZST and is the case the guard exists for. Both were fault-injected before being committed. Deleting the ZST guard turns the new harness from 0 VCCs into 6, of which 5 fail with pointer dereference errors; restoring it returns the file to 5/5 verified. Running the out-of-bounds test with --no-memory-safety-checks, which drops CBMC's --pointer-check, turns both of its harnesses green and removes every pointer_dereference property, confirming that check is what catches them.
…ile per harness Two gaps left by the previous commit's review. The store had no symbolic-offset proof, so its byte-precision rested on a single constant offset, which a correct simplifier could satisfy while the general path stayed wrong -- exactly the escape the load's symbolic harness exists to close. The new harness writes at a symbolic offset and checks both that the four bytes land at that offset and that every byte outside the written range is untouched, so a wrongly-aligned write shows up at every offset rather than at one. It generates 120 VCCs (85 after simplification, 40 checks), and pointing its oracle at the aligned word instead fails it. The expected file asserted the offset-safety SUCCESS without naming which harness produced it, so one match satisfied both and the load/store attribution was only half pinned. The offset check is monomorphised per pointer type, so *const u8 and *mut u8 discriminate the two harnesses exactly; both are now pinned. Breaking the discriminator fails the gate. Full regression is unchanged: kani 597 passed / 3 failed, expected 463 / 2, the same five failures that a from-scratch build of origin/main reproduces.
8dc149d to
8cd635f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
tests/kani/Intrinsics/Volatile/unaligned.rs:24
- This assertion message says "bytes 1..5" but the expected value is built from
buf[1]..buf[4](i.e., bytes [1..5) end-exclusive). Updating the message avoids ambiguity when diagnosing failures.
assert_eq!(v, expect, "unaligned load must read bytes 1..5, byte-precisely");
tests/kani/Intrinsics/Volatile/unaligned.rs:15
- The header comment uses ranges like "bytes 1..5" / "0..4" which can be read as inclusive; since the test is indexing
[1,2,3,4], it’s clearer to spell these as end-exclusive ranges to avoid confusion when reading failures.
This issue also appears on line 24 of the same file.
// than silently pass. The oracle is built with `u32::from_ne_bytes` so the test
// is byte-precise without assuming an endianness: reading a `u32` at byte offset
// 1 must equal the native-order interpretation of bytes 1..5, whereas an
// alignment-assuming read from offset 0 would give the interpretation of bytes
// 0..4 -- different under either endianness.
tests/expected/intrinsics/unaligned_volatile/out_of_bounds/expected:18
- The expected output here is much more verbose and relies on specific property IDs/status formatting (e.g.,
*.pointer_dereference,kani::rustc_intrinsics::offset::<...>). In thetests/expected/intrinsics/**/main.rssuites, expected files are typically the conciseFAILURE\\+ message form (seetests/expected/intrinsics/simd-div-rem-overflow/expected). Consider switching to that stable format to avoid brittleness across CBMC/Kani output changes.
check_unaligned_volatile_load_out_of_bounds.pointer_dereference\
Status: FAILURE\
Description: "dereference failure: pointer outside object bounds"
check_unaligned_volatile_store_out_of_bounds.pointer_dereference\
|
@ivmat could you address all Copilot comments? |
|
@feliperodri on it |
All three were suppressed rather than surfaced, which is a UI state and not a verdict, so they were checked rather than waved off. Two were right and one was right about the convention even though the notation it flagged was not actually wrong. Ranges in prose. `1..5` is correct Rust range syntax for the bytes this test reads, so the notation was not an error. But it appears in a comment and an assertion message rather than in code, and read inclusively "bytes 1..5" describes five bytes for a four-byte load -- precisely the off-by-one someone would be entertaining while diagnosing a failure here. Spelled out as "bytes 1, 2, 3 and 4" in both places; nothing is lost and the ambiguity goes. Expected-file format. The claim that the concise form is the house style is correct: 44 of the 47 expected files under tests/expected/intrinsics use it, and this file was one of three outliers. Two things were separable, though. Harness-qualified property assertions have in-directory precedent -- size_of_dst.expected uses the same property-id/Status/Description shape -- and they are what makes this test non-vacuous, so they stay. What was genuinely unconventional and brittle was pinning the monomorphised internal symbol kani::rustc_intrinsics::offset::<u8, *const u8, usize> to prove the offset check passed while the dereference failed. That is a Kani-internal name, it is the most likely thing to drift, and the test's own construction plus its comment already establish that the pointer arithmetic is in bounds. Dropped. The file still discriminates, checked both ways rather than assumed: changing the expected failure description fails the gate, and changing a harness name fails it too, so the per-harness attribution is real. Both suites green.
Completes the volatile intrinsic family on the tracking issue #1163, following #4672
(volatile copy/set), which has since landed. This branch is rebased onto
mainand nowcontains only its own work.
With #4672 merged and these two arms implemented,
unstable_codegen!has no remaining usesanywhere in
kani-compilerand is removed — these volatile/unaligned entries were the lastgated intrinsics in the codegen match.
Modelling
Neither intrinsic has an alignment requirement, so neither emits an alignment assertion —
tolerating a misaligned pointer is the entire purpose of the
unaligned_*variants.unaligned_volatile_loadwas already sketched behind the gate as a plain dereference;unaligned_volatile_storehad no codegen and noIntrinsicvariant at all, and is addedmirroring
volatile_storeminus the alignment check, including the same zero-sized-typeguard.
That leaves two questions worth being explicit about, and the tests answer both rather than
assume them.
Is a misaligned typed dereference modelled byte-precisely, or does it quietly assume
alignment? Each proof compares the accessed value against a byte-wise oracle at a
deliberately misaligned offset, so an implementation that touched the aligned word instead
would fail rather than silently pass. The oracle uses
u32::from_ne_bytes, which keeps itbyte-precise without baking in an endianness: reading a
u32at byte offset 1 must equal thenative-order interpretation of bytes 1..5, whereas an alignment-assuming read from offset 0
would give the interpretation of bytes 0..4 — different under either endianness. Both
directions also have a symbolic-offset proof, so neither can be satisfied by constant
folding, and the store proofs check that bytes outside the written range are untouched.
Is dereferenceability still checked, given this path builds the dereference directly rather
than going through place codegen? It is, by
--pointer-check, and there is now a test thatpins that down instead of asserting it in a comment.
Tests
tests/kani/Intrinsics/Volatile/unaligned.rs— six proofs: byte-precise load and store,each at a constant and at a symbolic offset, plus two ZST stores — one through a valid
pointer and one through a dangling but aligned pointer,
without_provenance_mut(align_of::<()>()), which is the case the ZST guard actuallyexists for.
tests/expected/intrinsics/unaligned_volatile/out_of_bounds/— an expected-fail test foran out-of-bounds unaligned load and store. It accesses a
u32at byte offset 1 of a4-byte array, so the pointer arithmetic stays in bounds and it is the access that
overruns the object; the failure is therefore attributable to the dereference rather than
to the offset computation. The
expectedfile pins that split for each harnessseparately — the offset safety check must report SUCCESS while
pointer_dereferencereports
dereference failure: pointer outside object bounds.tests/kani/VolatileIntrinsics/core_intrinsics.rs— this was markedkani-verify-fail,but that expectation existed only because
unaligned_volatile_store(andvolatile_set_memory) were unsupported. With both implemented the proof verifies, so theheader is removed and it becomes a passing test.
The two review-driven tests were fault-injected before being committed, in both directions.
Deleting the ZST guard turns the dangling-ZST harness from 0 VCCs into 6, of which 5 fail
with pointer dereference errors; restoring it returns the file to green. Running the
out-of-bounds test with
--no-memory-safety-checks, which drops CBMC's--pointer-check,turns both of its harnesses green and removes every
pointer_dereferenceproperty —confirming that check is what catches them.
One asymmetry worth pre-empting
The ZST guard exists on the store paths only.
codegen_volatile_loadand this PR'sunaligned_volatile_loadboth dereference unconditionally, so a zero-sized read through adangling-but-aligned pointer — which is legal Rust — currently produces a spurious
verification failure. I measured this: it fails identically on the already-merged
volatile_load, with the guarded store passing on the same input, so it is pre-existingbehaviour that the unaligned variant inherits rather than introduces. I have deliberately
left it alone here rather than widen this PR into
codegen_volatile_load, but I am happy tofix both in a follow-up, or here if you would prefer it.
Note
tests/kani/VolatileIntrinsics/main_fixme.rsis left untouched (it is skipped as a fixmetest). Its
test_copy_volatilenames its arguments as thoughvolatile_copy_memorywere(src, dst, count); the assertion it makes happens to hold under either argument order, soit neither catches nor is broken by the ordering. Worth a separate look if that suite is
ever revived.
By submitting this pull request, I confirm that my contribution is made under the terms of
the Apache 2.0 and MIT licenses.