Skip to content

Add support for volatile_copy_memory, volatile_copy_nonoverlapping_memory and volatile_set_memory#4672

Open
ivmat wants to merge 3 commits into
model-checking:mainfrom
ivmat:volatile-intrinsics-support
Open

Add support for volatile_copy_memory, volatile_copy_nonoverlapping_memory and volatile_set_memory#4672
ivmat wants to merge 3 commits into
model-checking:mainfrom
ivmat:volatile-intrinsics-support

Conversation

@ivmat

@ivmat ivmat commented Jul 26, 2026

Copy link
Copy Markdown

Implements three of the intrinsics still unchecked on the tracking issue #1163:
volatile_copy_memory, volatile_copy_nonoverlapping_memory and volatile_set_memory.

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 as ($($tt:tt)*) and never expands its tokens — it always emits a
codegen_unimplemented_expr. The bodies above 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 Rust intrinsics take the destination first:

pub unsafe fn volatile_copy_memory<T>(dst: *mut T, src: *const T, count: usize);
pub unsafe fn volatile_copy_nonoverlapping_memory<T>(dst: *mut T, src: *const T, count: usize);

whereas codegen_copy consumes source first (let src = fargs.remove(0); let dst = fargs.remove(0);)
and indexes farg_types[0] / farg_types[1] for the src and dst alignment checks respectively.
Both fargs and farg_types are therefore swapped before delegating. Without swapping both, the two
alignment 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) 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's codegen 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, which already delegate to their non-volatile counterparts.

Points-to analysis

Adding the VolatileSetMemory variant required one further line. points_to_analysis.rs matches
exhaustively over Intrinsic with a final unimplemented!() wildcard; WriteBytes is protected from it
by being listed in is_identity_aliasing_intrinsic. Before this change volatile_set_memory reached the
analysis as Intrinsic::Unimplemented { .. }, which is explicitly handled as a no-op. Introducing the new
variant would have routed it to the panicking wildcard instead, so it is listed alongside WriteBytes in
the 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) have
non-panicking fallbacks, so behaviour there is unchanged.

Tests

  • tests/kani/Intrinsics/Volatile/copy.rs — both copy intrinsics move the expected bytes, including an
    overlapping case for volatile_copy_memory (memmove semantics) to distinguish it from the
    non-overlapping variant.
  • tests/kani/Intrinsics/Volatile/set.rsvolatile_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
    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_load support.

Related

Towards #1163. The remaining unchecked volatile entries — unaligned_volatile_load and
unaligned_volatile_store — are deliberately left out of this PR: the gated body for
unaligned_volatile_load is a plain dereference, which does not model the unaligned access itself, so
those 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.

…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.
@ivmat
ivmat requested a review from a team as a code owner July 26, 2026 16:21
@github-actions github-actions Bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Jul 26, 2026
ivmat added 2 commits July 26, 2026 18:54
`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.
@ivmat

ivmat commented Jul 26, 2026

Copy link
Copy Markdown
Author

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 ptr::read_volatile), which copy/copy_nonoverlapping/write_bytes do not. The modelling is still sound, but for a narrower reason, and the comments now say so: these intrinsics document their safety requirements as consistent with copy_nonoverlapping / write_bytes, so reusing that codegen checks at least the documented UB conditions. It is conservative rather than exact — a legal MMIO access may be rejected by --pointer-check, but real UB is never missed.

volatile_set_memory was missing from the memory-initialization visitor. The compiler matches on Intrinsic in three places — codegen, points-to, and check_uninit's visitor — and only the first two had been updated, so under -Z uninit-checks the intrinsic produced a spurious "does not support reasoning about memory initialization" failure. It now shares the write_bytes arm: same (dst, val, count) shape, same initialization effect.

Also added 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 carries its safety checks across.

Full regression run locally on CBMC 6.10.0: no failures introduced (the only failures are three pre-existing Quantifiers tests that fail identically on unmodified main here, for want of z3).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-CompilerBenchCI Tag a PR to run benchmark CI Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant