Add support for volatile_copy_memory, volatile_copy_nonoverlapping_memory and volatile_set_memory - #4672
Conversation
…mory and volatile_set_memory Implements three of the intrinsics still unchecked on tracking issue model-checking#1163. Why the existing placeholders could not simply be un-gated ---------------------------------------------------------- The two copy intrinsics already had arms, but behind `unstable_codegen!`: Intrinsic::VolatileCopyMemory => unstable_codegen!(codegen_intrinsic_copy!(Memmove)), Intrinsic::VolatileCopyNonOverlappingMemory => { unstable_codegen!(codegen_intrinsic_copy!(Memcpy)) } `unstable_codegen!` is declared `($($tt:tt)*)` and never expands its tokens -- it unconditionally emits a codegen_unimplemented_expr. Those bodies were therefore never type-checked, and they have since rotted: codegen_intrinsic_copy! no longer exists anywhere in kani-compiler. Removing the gate alone does not compile, so both arms are rewritten rather than un-gated. `volatile_set_memory` had no Intrinsic variant at all and fell through to the unsupported catch-all. Argument order -------------- The intrinsics take the destination first: volatile_copy_memory<T>(dst: *mut T, src: *const T, count: usize) volatile_copy_nonoverlapping_memory<T>(dst: *mut T, src: *const T, count: usize) whereas codegen_copy consumes source first (two successive fargs.remove(0), src then dst) and indexes farg_types[0]/farg_types[1] for the src and dst alignment assertions. Both fargs and farg_types are therefore swapped before delegating. Swapping only one silently applies each alignment check to the wrong pointer -- a defect no test using two equally-aligned buffers would catch. volatile_set_memory(dst, val, count) matches write_bytes(dst, val, count), so no swap is needed there. Soundness of reusing the non-volatile codegen --------------------------------------------- Volatility constrains what the optimizer may do -- it must not elide, duplicate or reorder the access. It is not a memory-safety property: the UB conditions for these three intrinsics are exactly those of copy, copy_nonoverlapping and write_bytes. Kani performs no optimization that volatility is meant to suppress, so there is nothing additional to model. This mirrors the existing treatment of volatile_load/volatile_store. Points-to analysis ------------------ points_to_analysis.rs matches exhaustively over Intrinsic with a terminal unimplemented!() wildcard; write_bytes is protected from it via is_identity_aliasing_intrinsic. Before this change volatile_set_memory reached that pass as Intrinsic::Unimplemented, which is explicitly handled as a no-op. Introducing the new variant would have routed it to the panicking wildcard, so it is listed alongside write_bytes in the same way. The two check_uninit visitors have non-panicking fallbacks and are unchanged. Tests ----- Layout follows model-checking#1347, which added volatile_load support. tests/kani/Intrinsics/Volatile/copy.rs both copy intrinsics move the expected bytes, including an overlapping case for volatile_copy_memory (memmove semantics) that distinguishes it from the non-overlapping variant tests/kani/Intrinsics/Volatile/set.rs volatile_set_memory fills the destination, full and partial tests/expected/intrinsics/volatile_copy/overlapping/ volatile_copy_nonoverlapping_memory on overlapping ranges fails, matching the existing copy-nonoverlapping test for the non-volatile intrinsic tests/expected/intrinsics/volatile_copy/unaligned/ a misaligned pointer fails the alignment check The unaligned pair is deliberately left out: unaligned_volatile_load's gated body is a plain dereference, which does not model the unaligned access itself, and unaligned_volatile_store has no codegen at all. Those need a separate decision about how unaligned accesses should be represented.
`docs/src/rust-feature-support/intrinsics.md` listed all three as `No`. They are now supported, so the table has to move. `Partial` rather than `Yes`, matching the neighbouring `volatile_load` and `volatile_store` rows and keeping the same Concurrency note. The memory-safety semantics are modelled exactly (they are those of the non-volatile counterparts), but the volatile guarantees themselves -- that the access is not elided or reordered, and really does touch memory -- are not, since Kani assumes sequential execution. Claiming `Yes` would overstate that and would disagree with how the two already-supported volatile intrinsics are documented.
…s comment
Two review findings on the previous commit.
1. The compiler matches on `Intrinsic` in three places -- codegen, the points-to
analysis, and the memory-initialization visitor -- and only the first two were
updated. `volatile_set_memory` therefore fell to the visitor's catch-all and
produced a spurious "Kani does not support reasoning about memory
initialization" failure under -Z uninit-checks, while the codegen comment
claimed it behaves exactly like `write_bytes`. It now shares that arm: same
argument shape (dst, val, count), same initialization effect.
2. The comment overclaimed. "The UB conditions are exactly those of the
non-volatile counterpart" is false as a generalisation: the volatile family's
docs additionally permit pointers outside any Rust allocation (the MMIO
carve-out on ptr::read_volatile), which the non-volatile ops do not. The
modelling is still sound, but for a different reason than the comment gave --
reusing codegen_copy/codegen_write_bytes checks AT LEAST the documented UB
conditions, so it is conservative rather than exact: a legal MMIO access can
be rejected by --pointer-check, but real UB is never missed. The comments now
say that, and anchor on what these intrinsics' own docs state ("consistent
with copy_nonoverlapping" / "consistent with write_bytes") rather than on a
blanket claim about volatility.
Also adds tests/expected/intrinsics/volatile_set/out-of-bounds, mirroring the
existing write_bytes test. The happy-path test alone did not pin that reusing
codegen_write_bytes actually carries its safety checks across.
|
Pushed two follow-up commits after a review pass, and corrected one claim in the description above. The comment about volatility overclaimed. It said the UB conditions are exactly those of the non-volatile counterparts. That is not right as a generalisation: the volatile family's docs additionally permit pointers outside any Rust allocation (the MMIO carve-out documented on
Also added Full regression run locally on CBMC 6.10.0: no failures introduced (the only failures are three pre-existing |
There was a problem hiding this comment.
Pull request overview
Adds Kani support for three previously-unchecked Rust volatile memory intrinsics by reusing the existing (non-volatile) copy/set codegen paths, extending internal analyses to understand the new intrinsic variant, and adding regression/expected-output tests to validate semantics and safety checks.
Changes:
- Implement
volatile_copy_memory/volatile_copy_nonoverlapping_memoryby delegating tocodegen_copywith correct(dst, src)argument/type swapping. - Add a new
Intrinsic::VolatileSetMemoryvariant and implement it viacodegen_write_bytes, plus update points-to and uninitialized-memory analyses. - Add new kani + expected tests for happy paths and key failures (overlap, unaligned, out-of-bounds), and update the intrinsics support documentation.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/kani/Intrinsics/Volatile/set.rs | New proof harnesses validating successful volatile_set_memory behavior. |
| tests/kani/Intrinsics/Volatile/copy.rs | New proof harnesses validating both volatile copy intrinsics, including an overlapping memmove case. |
| tests/expected/intrinsics/volatile_set/out-of-bounds/main.rs | Expected-failure test ensuring OOB volatile set is caught (mirrors write_bytes). |
| tests/expected/intrinsics/volatile_set/out-of-bounds/expected | Expected output for the volatile set OOB failure. |
| tests/expected/intrinsics/volatile_copy/unaligned/main.rs | Expected-failure test ensuring misaligned dst is caught and swap is correct. |
| tests/expected/intrinsics/volatile_copy/unaligned/expected | Expected output for the unaligned volatile copy failure. |
| tests/expected/intrinsics/volatile_copy/overlapping/main.rs | Expected-failure test ensuring nonoverlapping volatile copy rejects overlap. |
| tests/expected/intrinsics/volatile_copy/overlapping/expected | Expected output for the overlap failure. |
| kani-compiler/src/kani_middle/transform/check_uninit/ptr_uninit/uninit_visitor.rs | Treat VolatileSetMemory like WriteBytes for initialization tracking. |
| kani-compiler/src/kani_middle/points_to/points_to_analysis.rs | Mark VolatileSetMemory as identity-aliasing to avoid panicking wildcard handling. |
| kani-compiler/src/intrinsics.rs | Add VolatileSetMemory variant and map volatile_set_memory name/signature to it. |
| kani-compiler/src/codegen_cprover_gotoc/codegen/intrinsic.rs | Implement volatile copy intrinsics via codegen_copy (with swap) and volatile set via codegen_write_bytes. |
| docs/src/rust-feature-support/intrinsics.md | Update support status for the three intrinsics to “Partial”. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
All three `volatile_copy` tests formed their `dst` by casting a pointer derived from `arr.as_ptr()` (a shared borrow) to `*mut i32` and then wrote through it, which is undefined behavior in its own right. For the two `tests/expected` harnesses this weakened what they prove: each is meant to fail for exactly one reason -- the overlap precondition and the alignment precondition respectively -- and a second, unintended source of UB means the harness may fail for a reason nobody named. Both pointers are now derived from `arr.as_mut_ptr()`, leaving the intended precondition as the only thing violated. Checked with Miri on the non-volatile counterparts (`ptr::copy` / `ptr::copy_nonoverlapping`, whose aliasing and alignment requirements are the ones these intrinsics' docs defer to): before, the overlapping harness reported a SharedReadOnly write violation instead of the overlap it is named for; after, it reports `copy_nonoverlapping called on overlapping ranges`, the unaligned harness reports only the alignment violation, and the `tests/kani` overlap proof runs clean with its assertions intact.
|
Three test fixes from a review pass, all the same defect class. The For the two One of the three was not reported. The automated review found the instances in Also re-verified, since missing it caused two earlier defects here: all three places that match on Regression re-run locally on CBMC 6.10.0 (the version this tree's One correction to my earlier comment while I am here: it said "the only failures are three pre-existing |
|
One practical note for whoever reviews or lands this, from exercising the new codegen end-to-end. I ran the five now-supported intrinsics against real harnesses — the ones in That wall is the harness, not this PR's codegen. The harness was written as: let shift: usize = kani::any();
kani::assume(shift >= 1 && shift < N);
...
unsafe { volatile_copy_memory(dst_ptr, src_ptr, N - shift) };
for i in 0..(N - shift) {
assert_eq!(buf[i + shift], original[i]);
}A symbolic const SHIFT: usize = 3;
...
let i: usize = kani::any();
kani::assume(i < N - SHIFT);
assert_eq!(buf[i + SHIFT], original[i]);Same harness, same property, same build: no result in 40 minutes → 0.133 s. Worth flagging because the failure mode is misleading. This defect was invisible for months — Kani's "not currently supported" verdict fires before codegen, so the harness could never run far enough to blow up. Anyone who lands this PR and un-gates the obvious How this was measured, and its limits: on a build of |
|
The The emitted program is unchanged. benchcomp reports That matches the diff: the compiler-side changes are five hunks, each inside a match arm keyed on The same gate fires on unrelated PRs whose emitted programs are also unchanged. In the same week:
Both ratios are larger than the 1.5517× here. On this branch specifically, the job also failed on 2026-07-29, on a different benchmark ( The one thing that would settle it is a re-run: the failing ratio is I'm not proposing a code change, since I can't find a path from this diff to that benchmark — happy to be corrected if I've missed one. Whether the threshold itself is worth revisiting is entirely your call. |
|
Thanks for the contribution @ivmat! |
23ff373
|
thank you @feliperodri for checking the PR ! |
…re` (model-checking#4673) Completes the volatile intrinsic family on the tracking issue model-checking#1163, following model-checking#4672 (volatile copy/set), which has since landed. This branch is rebased onto `main` and now contains only its own work. With model-checking#4672 merged and these two arms implemented, `unstable_codegen!` has no remaining uses anywhere in `kani-compiler` and is removed — these volatile/unaligned entries 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 entire purpose 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, including the same zero-sized-type guard. 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 it byte-precise without baking in 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. 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 that pins 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 actually exists for. - `tests/expected/intrinsics/unaligned_volatile/out_of_bounds/` — an expected-fail test for an out-of-bounds unaligned load and store. It accesses a `u32` at byte offset 1 of a 4-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 `expected` file pins that split for each harness separately — the offset safety check must report SUCCESS while `pointer_dereference` reports `dereference failure: pointer outside object bounds`. - `tests/kani/VolatileIntrinsics/core_intrinsics.rs` — this was marked `kani-verify-fail`, but that expectation existed only because `unaligned_volatile_store` (and `volatile_set_memory`) were unsupported. With both implemented the proof verifies, so the header 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_dereference` property — confirming that check is what catches them. ## One asymmetry worth pre-empting The ZST guard exists on the **store** paths only. `codegen_volatile_load` and this PR's `unaligned_volatile_load` both dereference unconditionally, so a zero-sized read through a dangling-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-existing behaviour 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 to fix both in a follow-up, or here if you would prefer it. ## 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. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
Implements three of the intrinsics still unchecked on the tracking issue #1163:
volatile_copy_memory,volatile_copy_nonoverlapping_memoryandvolatile_set_memory.Why the existing placeholders could not simply be un-gated
The two copy intrinsics already had arms, but behind
unstable_codegen!:unstable_codegen!is declared as($($tt:tt)*)and never expands its tokens — it always emits acodegen_unimplemented_expr. The bodies above were therefore never type-checked, and they have sincerotted:
codegen_intrinsic_copy!no longer exists anywhere inkani-compiler. Removing the gate alonedoes not compile, so both arms are rewritten rather than un-gated.
volatile_set_memoryhad noIntrinsicvariant at all and fell through to the unsupported catch-all.Argument order
The Rust intrinsics take the destination first:
whereas
codegen_copyconsumes source first (let src = fargs.remove(0); let dst = fargs.remove(0);)and indexes
farg_types[0]/farg_types[1]for thesrcanddstalignment checks respectively.Both
fargsandfarg_typesare therefore swapped before delegating. Without swapping both, the twoalignment assertions are silently applied to the wrong pointers — a discrepancy that a test using two
equally-aligned buffers would not detect.
volatile_set_memory(dst, val, count)matcheswrite_bytes(dst, val, count), so no swap is needed there.Soundness of reusing the non-volatile codegen
Volatility constrains what the optimizer may do — it must not elide, duplicate or reorder the access.
It is not a memory-safety property: the UB conditions for these three intrinsics are exactly those of
copy,copy_nonoverlappingandwrite_bytes. Kani's codegen performs no optimization that volatilityis meant to suppress, so there is nothing additional to model. This mirrors the existing treatment of
volatile_load/volatile_store, which already delegate to their non-volatile counterparts.Points-to analysis
Adding the
VolatileSetMemoryvariant required one further line.points_to_analysis.rsmatchesexhaustively over
Intrinsicwith a finalunimplemented!()wildcard;WriteBytesis protected from itby being listed in
is_identity_aliasing_intrinsic. Before this changevolatile_set_memoryreached theanalysis as
Intrinsic::Unimplemented { .. }, which is explicitly handled as a no-op. Introducing the newvariant would have routed it to the panicking wildcard instead, so it is listed alongside
WriteBytesinthe same way.
The other two visitors that match on
Intrinsic(
check_uninit/ptr_uninit/uninit_visitor.rs,check_uninit/delayed_ub/initial_target_visitor.rs) havenon-panicking fallbacks, so behaviour there is unchanged.
Tests
tests/kani/Intrinsics/Volatile/copy.rs— both copy intrinsics move the expected bytes, including anoverlapping case for
volatile_copy_memory(memmove semantics) to distinguish it from thenon-overlapping variant.
tests/kani/Intrinsics/Volatile/set.rs—volatile_set_memoryfills the destination, full and partial.tests/expected/intrinsics/volatile_copy/overlapping/—volatile_copy_nonoverlapping_memoryonoverlapping ranges fails, matching the existing
tests/expected/intrinsics/copy-nonoverlapping/copy-overlapping/test for the non-volatile intrinsic.tests/expected/intrinsics/volatile_copy/unaligned/— a misaligned pointer fails the alignment check.Test layout follows #1347, which added
volatile_loadsupport.Related
Towards #1163. The remaining unchecked volatile entries —
unaligned_volatile_loadandunaligned_volatile_store— are deliberately left out of this PR: the gated body forunaligned_volatile_loadis a plain dereference, which does not model the unaligned access itself, sothose two need a separate decision about how unaligned accesses should be represented.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0
and MIT licenses.