Skip to content

Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics - #618

Open
ivmat wants to merge 1 commit into
model-checking:mainfrom
ivmat:challenge-2-intrinsics-memory
Open

Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics#618
ivmat wants to merge 1 commit into
model-checking:mainfrom
ivmat:challenge-2-intrinsics-memory

Conversation

@ivmat

@ivmat ivmat commented Jul 30, 2026

Copy link
Copy Markdown

Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics

Closes a substantial part of Challenge 2, and states plainly what it does not close. Per criterion 4, the assumptions are listed rather than implied.

Criterion 1 requires all 20; this PR delivers 15 and documents precisely why the other five are blocked. The challenge sets no partial threshold — whether to land partial progress is the maintainers' call.

Headline: 15 of the 20 mandated intrinsics are verified, all 15 in the mandated contract form. The remaining 5 are not verifiable by official Kani at the pinned commit (d4df833) — Kani emits an explicit "not currently supported" diagnostic for each. That is a tool gap, not a proof failure; the upstream fix for all five is filed (see Residuals).

What is verified

15 of the 20 mandated intrinsics, each with a doc-derived safety contract, verified via #[kani::proof_for_contract]:

typed_swap (in-tree today as typed_swap_nonoverlapping) · vtable_size · vtable_align · copy · copy_nonoverlapping · write_bytes · size_of_val · arith_offset · volatile_load · volatile_store · ptr_offset_from · ptr_offset_from_unsigned · compare_bytes · read_via_copy · write_via_move

(The table's 21st name, min_align_of_val, is core::mem::min_align_of_val — a deprecated one-line wrapper over align_of_val, not a separate entry in core::intrinsics. It is exercised directly against an independent oracle by check_min_align_of_val_no_ub. Note that align_of_val itself is outside the mandated set and carries no contract in this PR, so min_align_of_val's safety reduces to an intrinsic this PR does not separately verify.)

Whole-module run on the currently pinned toolchain:

Complete - 345 successfully verified harnesses, 0 failures, 345 total.

Toolchain identity captured at run time rather than inferred, because the version string 0.67.0 is
identical for the 0.67.0 release and the pinned dev commit:

Kani d4df833c8f8f18e632e7b0a7945bb2161f708990 (the commit tool_config/kani-version.toml pins)
CBMC 6.8.0 — the version that Kani commit's kani-dependencies declares
Flags --no-assert-contracts, --object-bits 12

Non-vacuity is machine-checked, not asserted: 18 kani::cover properties across 16 harnesses,
all satisfied. None is unsatisfied — which matters, because Kani reports a harness whose cover is
unsatisfiable as VERIFICATION: SUCCESSFUL, so an uninspected cover count can hide a vacuous proof.

Every verified intrinsic carries doc-derived #[requires]/#[ensures], verified with #[kani::proof_for_contract].

Because kani#3325 blocks contracts directly on bodyless #[rustc_intrinsic] declarations, contracts sit on a thin unsafe fn <name>_wrapper — the same pattern already used in-tree for transmute_unchecked_wrapper, and the workaround recorded in kani#3345.

Each proof uses an independent oracle: the property is checked against a separately-computed expected value, never by re-calling the intrinsic under test.

Criterion-by-criterion

# Criterion Status
1 All listed intrinsics annotated with safety contracts 15/20. All 15 verified ones are in contract form. The 5 residuals are not, because Kani cannot codegen them.
2 Any fallback intrinsic implementation must be verified Closed. Of the 21 listed names, min_align_of_val is a non-intrinsic core::mem wrapper, so criterion 2 does not range over it; it resolves to the bodyless align_of_val intrinsic. Of the 20 intrinsics, 19 are declared with no body — nothing for the criterion to range over — and exactly one, typed_swap_nonoverlapping, carries a fallback body (unsafe { ptr::swap_nonoverlapping(x, y, 1) }), verified against the intrinsic's own contract via a wrapper that forces symbolic execution of the body rather than Kani's built-in model. This shows the fallback satisfies the contract, not that Kani's model and the fallback are equivalent — a claim criterion 2 does not make.
3 Explain how modelled intrinsics match their definition Stated per intrinsic: the independent-oracle construction, plus the contracts themselves as the machine-checked correspondence.
4 State assumptions, auditability, guaranteed properties See Assumptions and bounds below — deliberately explicit.
5 Documented conditions are sufficient for safe use Satisfied for the 15 in contract form (requires ⇒ no-UB ∧ ensures). Open for the 5 residuals.

Residuals — 5 intrinsics official Kani cannot verify at the pinned commit

volatile_copy_memory, volatile_copy_nonoverlapping_memory, volatile_set_memory, unaligned_volatile_load, unaligned_volatile_store.

These are not proof failures. Running each harness produces Kani's own diagnostic, e.g.:

volatile_set_memory is not currently supported by Kani. Please post your example at https://github.com/model-checking/kani/issues/new/choose

The harnesses are written and kept in-tree under #[cfg(not(kani))], so the exact attempted proof is preserved and can be un-gated the moment support lands.

I have filed the upstream fix covering all five:

  • kani#4672volatile_copy_memory, volatile_copy_nonoverlapping_memory, volatile_set_memory
  • kani#4673unaligned_volatile_load, unaligned_volatile_store

What the investigation found, all checkable by reading Kani's tree: the placeholders were never compiled (unstable_codegen! never expands its tokens), so the gated bodies had bit-rotted and called a macro that no longer exists; the sketched implementation had (dst, src) reversed relative to Kani's codegen_copy; and adding the missing volatile_set_memory variant ICEs the points-to analysis unless handled. With all five implemented, unstable_codegen! has zero remaining uses and is removed.

All five harnesses have also been run and verify locally on a build of the pinned Kani commit with
both PRs cherry-picked on top.
That build models the post-merge world — it is not official Kani,
and it is reported here only so maintainers can see the residuals are genuinely tool-gated rather than
quietly failing proofs.

One caveat on that run, because it matters to anyone who un-gates these after the Kani PRs land:
check_volatile_copy_memory_no_ub as originally written combined a symbolic shift with
for i in 0..(N - shift), putting a symbolic trip count in the formula. It ran 40 minutes without
converging — the same self-overlap wall plain copy hits, and unrelated to the new codegen. Rewritten
to the pattern check_copy_overlapping_shift_no_ub already uses in this file (fixed representative
SHIFT, symbolic index), it verifies in 0.13s. That is a real weakening, identical to the one
copy's own overlap harness carries: it proves the property for a representative shift, not for all
shifts. Flagged here rather than buried: the gated harnesses could never have run end-to-end on official
Kani — the "not supported" verdict fires before codegen — so this defect was unreachable until now.

This PR does not claim those five. Filing a PR is not landing one, and landing one is not a release. The count above is what verifies against official Kani, and it stays 15/20 until Kani merges and releases the support and this repo's pinned kani-version.toml moves.

Assumptions and bounds (criterion 4)

These are the honest limits of what the proofs establish.

  • Bounded, not unbounded. Proofs use small concrete sizes (typically N = 4) with symbolic contents. They establish the properties for those sizes, not for all sizes.
  • Type coverage is finite, not generic. Each harness instantiates fixed concrete types (e.g. u8/char/NonZero for typed_swap, &u32 for size_of_val and min_align_of_val, a single concrete dyn fixture for vtable_size/vtable_align). Properties are established per instantiated type, not for all T; size_of_val is exercised on the sized path only. The arbitrary-pointer harnesses additionally exclude Dangling/DeadObject allocation states, which Kani's memory predicates cannot currently model.
  • Overlap coverage is representative, not exhaustive. The copy overlap harness — and the gated volatile_copy_memory harness — uses a fixed representative SHIFT with a symbolic index. The property is proved for that shift distance, not universally quantified over distances. This applies to copy, one of the 15 claimed above.
  • No harness in this module is stub-mediated. The -Z stubbing flag in the reproduction command is required only because the module also contains pre-existing upstream transmute harnesses using #[kani::stub_verified] (substitution of a separately verified contract, not a fabricated body). No challenge-2 harness uses kani::stub or stub_verified.
  • --no-assert-contracts. Dependency contracts are assumed, not asserted. The contracts under proof_for_contract are themselves fully checked.
  • --object-bits 12. Fails loudly if exceeded — a sound bound, not a silent cap.
  • Pointer-shaped preconditions are present and doc-correct but not falsifiable by the fixtures. The same-allocation / non-overlap requires on ptr_offset_from(_unsigned) and copy_nonoverlapping are the real documented preconditions, but a single-array fixture cannot violate them, so the proofs do not demonstrate their necessity.
  • Non-vacuity is machine-witnessed where this PR adds harnesses. Every harness authored for this work carries a satisfied post-state kani::cover (the 18/18 above). The pre-existing typed_swap harnesses predate this discipline and carry none; their non-vacuity rests on proof_for_contract with satisfiable preconditions rather than an explicit witness.
  • Table-2 "usage sites" are argued by code inspection, not machine-checked. swap, copy_from_slice and the align_of_val site each delegate to the named intrinsic, so the verified contract on that intrinsic is the load-bearing fact — but no separate harness proves the delegation. zeroed likewise delegates to write_bytes, which is verified here. The fifth, parse_u64_into, is the naming finding below.

Existing code this PR replaces (called out deliberately)

The diff touches one file, library/core/src/intrinsics/mod.rs. Three pre-existing pieces are removed rather than left alongside, so it's worth saying exactly what and why:

  • The commented-out check_copy and check_copy_nonoverlapping sketches. Both are superseded by working proof_for_contract harnesses on copy_wrapper / copy_nonoverlapping_wrapper. The sketches' own comment notes they couldn't use ArbitraryPointer because contract checking rejects calls to the function under verification; the wrapper pattern is what resolves that.
  • The #[cfg(not(kani))]-disabled write_bytes harness and its kani#90 FIXME. kani#90 remains open upstream; what no longer reproduces on the pinned toolchain is the specific configuration this FIXME guarded: check_write_bytes verifies against write_bytes_wrapper (all 296 checks pass), and the harness carries a kani::cover witnessing the exact kani#90 trigger — a 0-count write to a non-dereferenceable pointer — as reachable, not assumed away. If maintainers would rather keep the FIXME until kani#90 is formally closed, say so and I'll restore it — I removed it because leaving a FIXME next to a passing harness is its own kind of stale.

No other upstream code is modified, and nothing outside this file is touched.

One finding for the maintainers

The challenge's mandated table lists parse_u64_into, which git grep parse_u64_into across library/ at this branch's HEAD does not find (this subtree mirrors rust-lang/rust, so a rename would have happened there). It may be renamed or removed upstream. Flagging it rather than silently dropping it — happy to open a separate issue if useful.

Reproducing

kani verify-std -Z unstable-options ./library \
    -Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi \
    -Z loop-contracts -Z quantifiers -Z stubbing \
    --no-assert-contracts \
    --harness intrinsics::verify:: \
    --cbmc-args --object-bits 12

All harnesses live in library/core/src/intrinsics/mod.rs under mod verify. The diff touches that
one file and nothing else.

@ivmat
ivmat requested a review from a team as a code owner July 30, 2026 20:01
@feliperodri feliperodri added the Challenge Used to tag a challenge label Aug 15, 2026
@feliperodri
feliperodri requested a balanced review from Copilot August 15, 2026 20:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds partial Kani verification for Challenge 2, targeting 15 raw-pointer intrinsics while preserving five unsupported harnesses.

Changes:

  • Adds safety-contract wrappers and Kani proofs.
  • Adds independent-oracle and non-vacuity checks.
  • Documents unsupported volatile intrinsic residuals.
Suppressed comments (3)

library/core/src/intrinsics/mod.rs:3576

  • This postcondition has the same indexing defect in check_copy_untyped: the selected destination element is compared with src[0], not the corresponding src[elem] (lines 2963-2966). Mixed initialized/uninitialized source elements can make a correct copy fail the contract, so the helper must offset both pointers by elem.
    #[ensures(|_| check_copy_untyped(src, dst, count))]

library/core/src/intrinsics/mod.rs:4556

  • This excludes the documented MMIO use case: write_volatile permits aligned, non-trapping writes outside Rust allocations, but can_write and the ordinary-dereference postcondition only describe Rust-backed memory. Add a model for external volatile memory or list this as an unverified residual instead of treating this as the complete volatile_store safety contract.
    #[requires(ub_checks::can_write(dst))]
    #[ensures(|_| unsafe { *dst } == val)]

library/core/src/intrinsics/mod.rs:4594

  • This contract is false for valid vtables whose erased type is not aligned like u32; a dyn Debug vtable for [u8; 8], for example, meets the readable-memory precondition but reports alignment 1 rather than 4. Readability also does not prove that the pointer is a vtable. Preserve the erased type's expected alignment in the wrapper/fixture and encode genuine vtable validity, or do not count this monomorphic probe as the intrinsic contract.
    #[requires(ub_checks::can_dereference(ptr as *const [usize; 3]))]
    #[ensures(|result| *result == core::mem::align_of::<u32>())]

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

Comment on lines +3534 to +3535
unsafe fn typed_swap_fallback_wrapper<T>(x: *mut T, y: *mut T) {
unsafe { crate::ptr::swap_nonoverlapping(x, y, 1) }
&& ub_checks::can_dereference(core::ptr::slice_from_raw_parts(src as *const crate::mem::MaybeUninit<T>, count))
&& ub_checks::can_write(core::ptr::slice_from_raw_parts_mut(dst, count))
&& ub_checks::maybe_is_nonoverlapping(src as *const (), dst as *const (), size_of::<T>(), count))]
#[ensures(|_| check_copy_untyped(src, dst, count))]
Comment on lines +4294 to +4295
#[requires(offset >= 0 && offset <= 8)]
#[ensures(|result| *result as usize == (dst as usize).wrapping_add(offset as usize))]
Comment thread library/core/src/intrinsics/mod.rs Outdated
Comment on lines +4453 to +4455
#[requires(bytes <= COMPARE_BYTES_CAP
&& ub_checks::can_dereference(crate::ptr::slice_from_raw_parts(left, bytes))
&& ub_checks::can_dereference(crate::ptr::slice_from_raw_parts(right, bytes)))]
Comment thread library/core/src/intrinsics/mod.rs Outdated
Comment on lines +4522 to +4525
#[requires(ub_checks::can_dereference(ptr))]
#[ensures(|result| *result == core::mem::size_of::<T>())]
#[allow(dead_code)]
unsafe fn size_of_val_wrapper<T>(ptr: *const T) -> usize {
Comment on lines +4539 to +4540
#[requires(ub_checks::can_dereference(src))]
#[ensures(|result| *result == unsafe { *src })]
Comment thread library/core/src/intrinsics/mod.rs Outdated
Comment on lines +4586 to +4587
#[requires(ub_checks::can_dereference(ptr as *const [usize; 3]))]
#[ensures(|result| *result == core::mem::size_of::<u32>())]

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kani-verification review — PR #618 (Challenge 2, partial: 15/20 raw-pointer intrinsics)

Bottom line

The engineering is careful and unusually honest, the wrapper-around-#[rustc_intrinsic] pattern is the right workaround for kani#3325/rust-lang#3345 (already blessed in-tree via transmute_unchecked_wrapper), and the #[cfg(not(kani))] gates are legitimate — not the fatal body-swap pattern. I'm requesting changes only on contract faithfulness / over-constraint grounds against success criterion 5, which several contracts do not meet as written. Nothing here makes an unsafe operation look safe (all deviations are conservative), so this is a fixable faithfulness bar, not a vacuity/soundness collapse.

What is sound (no action needed)

  • All 5 #[cfg(not(kani))] gates are legitimate. They sit on disabled #[kani::proof] harnesses for intrinsics Kani reports as unsupported — check_volatile_set_memory_no_ub (618.diff L833), check_volatile_copy_nonoverlapping_memory_no_ub (L851), check_volatile_copy_memory_no_ub (L873), check_unaligned_volatile_load_no_ub (L929), check_unaligned_volatile_store_no_ub (L947). None compiles out a verified std function body behind an assume-the-conclusion stub. This is the same idiom as the pre-existing removed write_bytes gate (L199). No fatal vacuity.
  • Contract-liveness is complete: 20 #[kani::proof_for_contract] targets, each paired to a contracted wrapper; the 42 requires / 31 ensures are multi-clause contracts on those 20 functions, so the raw "53 vs 20" is consistent, not decorative.
  • typed_swap_fallback_wrapper (L77), copy_wrapper/copy_nonoverlapping_wrapper/write_bytes_wrapper (L112–134) use the correct can_dereference/can_write/maybe_is_nonoverlapping/alignment preconditions — exactly the right contract shape for raw-pointer memory intrinsics.
  • Bounded fixtures ([u8;8], [u32;4], COMPARE_BYTES_CAP=4) are acceptable; the challenge does not mandate unbounded, and the added kani::cover non-vacuity witnesses are a nice touch.

Blocking: contracts that don't faithfully capture the documented safety condition (criterion 5)

  1. vtable_size_wrapper / vtable_align_wrapper (L550–561). #[ensures(*result == size_of::<u32>())] / align_of::<u32>() is hard-coded to the fixture type; it is false for any non-u32 vtable (e.g. dyn Debug over u64/[u8;8]) that equally satisfies can_dereference(ptr as *const [usize;3]). The precondition also doesn't establish "ptr is actually a vtable." As written this is a monomorphic probe, not the intrinsic's contract. The author's own scoping note concedes this. Either encode the erased type's expected layout generically or don't count these two as verified for the challenge table.

  2. size_of_val_wrapper (L486). #[requires(can_dereference(ptr))] is stronger than documented: mem::size_of_val_raw is safe for any T: Sized including null/dangling data pointers, which can_dereference rejects. So "meeting the documented condition is enough" (criterion 5) is not demonstrated — a stronger condition is. Only T = u32 (Sized) is covered; the ?Sized metadata cases are absent.

  3. compare_bytes_wrapper (L417). bytes <= COMPARE_BYTES_CAP is a tractability bound placed in #[requires], so the contract rejects valid calls over larger readable regions. Keep the cap as a harness assume only; state the contract purely as "both regions readable for bytes."

  4. volatile_load_wrapper / volatile_store_wrapper (L503/L519). can_dereference/can_write + an ordinary-deref postcondition cover only Rust-backed allocations and exclude the documented MMIO case (read/write_volatile permit aligned non-trapping access outside any Rust allocation). Over-constrains valid callers; list the external-memory case as an unverified residual rather than presenting this as the full contract.

  5. arith_offset_wrapper (L258). #[requires(offset >= 0 && offset <= 8)] on the contract-form wrapper is not a documented precondition (arith_offset has none). The author's mitigation is real and appreciated — check_arith_offset_unconditional_safety (L280) proves safety unbounded — so the safety property is genuinely covered. But the bounded wrapper should be presented as a behavioral/pointer-model probe, not "the intrinsic contract."

Non-blocking but worth addressing

  • check_copy_untyped oracle asymmetry (pre-existing helper, mod.rs:2954, now depended on by copy_wrapper/copy_nonoverlapping_wrapper ensures at diff L109/L119). It offsets dst by elem but leaves src at element 0 (src_data.add(byte) vs dst.add(elem)...add(byte)), so it compares dst[elem]'s init state against src[0]'s. For sources with per-element init differences this oracle is checking the wrong pair. It's not introduced by this PR, but since the PR newly relies on it for the copy/copy_nonoverlapping postconditions, it should be fixed to offset src by elem too (or confirmed harmless for these fixtures).
  • ptr_offset_from_wrapper / ptr_offset_from_unsigned_wrapper (L301/L338). The author honestly discloses that dropping the #[requires] "all the way to true still verifies SUCCESSFUL" because the fixture only ever derives both pointers from one [u8;8] array. The contract text is doc-faithful, but the harness does not exercise the precondition (no cross-allocation / reversed-order pointers), so the proof is near-vacuous w.r.t. that precondition. Strengthen the fixture or note it as a known ablation gap in the PR body.
  • typed_swap_fallback_wrapper: verifying a verbatim copy of the fallback body (not the shared implementation) satisfies criterion 2's letter but not Kani-entry into typed_swap_nonoverlapping; the author documents this scope limit clearly. Consider extracting a shared helper.

Partial submission

Partial (15/20) is acceptable for this open challenge, and the 5 uncovered intrinsics (the volatile/unaligned-volatile family) are genuinely Kani-unsupported and honestly documented. The blocker is not the missing 5 — it's that several of the claimed 15 have contracts that over-constrain or hard-code fixture specifics and so don't yet satisfy criterion 5 ("meeting the documented conditions is enough to guarantee safe usage"). Tighten items 1–5 (or relabel the affected ones as bounded probes / residuals) and this becomes approvable.

…of 20 raw-pointer core::intrinsics

Add doc-derived safety contracts and Kani proof harnesses for 15 of the 20
raw-pointer intrinsics in Challenge 2, each verified via
#[kani::proof_for_contract]: typed_swap, vtable_size, vtable_align, copy,
copy_nonoverlapping, write_bytes, size_of_val, arith_offset, volatile_load,
volatile_store, ptr_offset_from, ptr_offset_from_unsigned, compare_bytes,
read_via_copy, and write_via_move.

Kani cannot attach a contract to a bodyless #[rustc_intrinsic] (kani#3325,
kani#3345), so each contract sits on a thin wrapper that calls the intrinsic.
This is the pattern already used in-tree for transmute_unchecked_wrapper. For
vtable_size/vtable_align the wrapper takes *const T and performs the unsize
coercion inside the wrapper, so the pointer handed to the intrinsic is a vtable
for T by construction. The postcondition is the generic size_of::<T>() /
align_of::<T>(), verified at 7 erased types with mutually independent size and
align.

Every proof checks the result against an independent oracle, never by re-calling
the intrinsic under test. Every added harness carries satisfied kani::cover
witnesses for non-vacuity. Tractability bounds live in the harnesses as assumes,
never in the contracts, so each #[requires] states only the documented safety
condition. arith_offset has no documented precondition: its unbounded safety is
proven by a separate plain proof, and its bounded wrapper is a behavioral probe.
The raw *const () vtable wrappers are kept as labelled probes and are not
counted.

The 5 volatile-family intrinsics are not counted. Kani reports them unsupported
at the pinned commit (d4df833), so their harnesses are kept under
#[cfg(not(kani))] with the exact attempted proof preserved.

Whole-module run at the pinned toolchain (kani d4df833, CBMC 6.8.0):
0 failures across the challenge-2 verification module.

Reproduce:
kani verify-std -Z unstable-options ./library \
  -Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi \
  -Z loop-contracts -Z quantifiers -Z stubbing \
  --no-assert-contracts --harness intrinsics::verify:: \
  --cbmc-args --object-bits 12
@ivmat
ivmat force-pushed the challenge-2-intrinsics-memory branch from 2b9b99c to 9dc6083 Compare August 23, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Challenge Used to tag a challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants