Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics - #618
Conversation
There was a problem hiding this comment.
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 withsrc[0], not the correspondingsrc[elem](lines 2963-2966). Mixed initialized/uninitialized source elements can make a correctcopyfail the contract, so the helper must offset both pointers byelem.
#[ensures(|_| check_copy_untyped(src, dst, count))]
library/core/src/intrinsics/mod.rs:4556
- This excludes the documented MMIO use case:
write_volatilepermits aligned, non-trapping writes outside Rust allocations, butcan_writeand 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 completevolatile_storesafety 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; adyn Debugvtable 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.
| 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))] |
| #[requires(offset >= 0 && offset <= 8)] | ||
| #[ensures(|result| *result as usize == (dst as usize).wrapping_add(offset as usize))] |
| #[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)))] |
| #[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 { |
| #[requires(ub_checks::can_dereference(src))] | ||
| #[ensures(|result| *result == unsafe { *src })] |
| #[requires(ub_checks::can_dereference(ptr as *const [usize; 3]))] | ||
| #[ensures(|result| *result == core::mem::size_of::<u32>())] |
feliperodri
left a comment
There was a problem hiding this comment.
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 removedwrite_bytesgate (L199). No fatal vacuity. - Contract-liveness is complete: 20
#[kani::proof_for_contract]targets, each paired to a contracted wrapper; the 42requires/ 31ensuresare 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 correctcan_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 addedkani::covernon-vacuity witnesses are a nice touch.
Blocking: contracts that don't faithfully capture the documented safety condition (criterion 5)
-
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-u32vtable (e.g.dyn Debugoveru64/[u8;8]) that equally satisfiescan_dereference(ptr as *const [usize;3]). The precondition also doesn't establish "ptris 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. -
size_of_val_wrapper(L486).#[requires(can_dereference(ptr))]is stronger than documented:mem::size_of_val_rawis safe for anyT: Sizedincluding null/dangling data pointers, whichcan_dereferencerejects. So "meeting the documented condition is enough" (criterion 5) is not demonstrated — a stronger condition is. OnlyT = u32(Sized) is covered; the?Sizedmetadata cases are absent. -
compare_bytes_wrapper(L417).bytes <= COMPARE_BYTES_CAPis a tractability bound placed in#[requires], so the contract rejects valid calls over larger readable regions. Keep the cap as a harnessassumeonly; state the contract purely as "both regions readable forbytes." -
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_volatilepermit 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. -
arith_offset_wrapper(L258).#[requires(offset >= 0 && offset <= 8)]on the contract-form wrapper is not a documented precondition (arith_offsethas 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_untypedoracle asymmetry (pre-existing helper,mod.rs:2954, now depended on bycopy_wrapper/copy_nonoverlapping_wrapperensures at diff L109/L119). It offsetsdstbyelembut leavessrcat element 0 (src_data.add(byte)vsdst.add(elem)...add(byte)), so it comparesdst[elem]'s init state againstsrc[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 thecopy/copy_nonoverlappingpostconditions, it should be fixed to offsetsrcbyelemtoo (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 totruestill 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 intotyped_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
2b9b99c to
9dc6083
Compare
Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer
core::intrinsicsCloses 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 astyped_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, iscore::mem::min_align_of_val— a deprecated one-line wrapper overalign_of_val, not a separate entry incore::intrinsics. It is exercised directly against an independent oracle bycheck_min_align_of_val_no_ub. Note thatalign_of_valitself is outside the mandated set and carries no contract in this PR, somin_align_of_val's safety reduces to an intrinsic this PR does not separately verify.)Whole-module run on the currently pinned toolchain:
Toolchain identity captured at run time rather than inferred, because the version string
0.67.0isidentical for the 0.67.0 release and the pinned dev commit:
d4df833c8f8f18e632e7b0a7945bb2161f708990(the committool_config/kani-version.tomlpins)kani-dependenciesdeclares--no-assert-contracts,--object-bits 12Non-vacuity is machine-checked, not asserted: 18
kani::coverproperties 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 thinunsafe fn <name>_wrapper— the same pattern already used in-tree fortransmute_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
min_align_of_valis a non-intrinsiccore::memwrapper, so criterion 2 does not range over it; it resolves to the bodylessalign_of_valintrinsic. 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.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.:
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:
volatile_copy_memory,volatile_copy_nonoverlapping_memory,volatile_set_memoryunaligned_volatile_load,unaligned_volatile_storeWhat 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'scodegen_copy; and adding the missingvolatile_set_memoryvariant 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_ubas originally written combined a symbolicshiftwithfor i in 0..(N - shift), putting a symbolic trip count in the formula. It ran 40 minutes withoutconverging — the same self-overlap wall plain
copyhits, and unrelated to the new codegen. Rewrittento the pattern
check_copy_overlapping_shift_no_ubalready uses in this file (fixed representativeSHIFT, symbolic index), it verifies in 0.13s. That is a real weakening, identical to the onecopy's own overlap harness carries: it proves the property for a representative shift, not for allshifts. 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.tomlmoves.Assumptions and bounds (criterion 4)
These are the honest limits of what the proofs establish.
N = 4) with symbolic contents. They establish the properties for those sizes, not for all sizes.u8/char/NonZerofortyped_swap,&u32forsize_of_valandmin_align_of_val, a single concretedynfixture forvtable_size/vtable_align). Properties are established per instantiated type, not for allT;size_of_valis exercised on the sized path only. The arbitrary-pointer harnesses additionally excludeDangling/DeadObjectallocation states, which Kani's memory predicates cannot currently model.copyoverlap harness — and the gatedvolatile_copy_memoryharness — uses a fixed representativeSHIFTwith a symbolic index. The property is proved for that shift distance, not universally quantified over distances. This applies tocopy, one of the 15 claimed above.-Z stubbingflag 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 useskani::stuborstub_verified.--no-assert-contracts. Dependency contracts are assumed, not asserted. The contracts underproof_for_contractare themselves fully checked.--object-bits 12. Fails loudly if exceeded — a sound bound, not a silent cap.requiresonptr_offset_from(_unsigned)andcopy_nonoverlappingare the real documented preconditions, but a single-array fixture cannot violate them, so the proofs do not demonstrate their necessity.kani::cover(the 18/18 above). The pre-existingtyped_swapharnesses predate this discipline and carry none; their non-vacuity rests onproof_for_contractwith satisfiable preconditions rather than an explicit witness.swap,copy_from_sliceand thealign_of_valsite 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.zeroedlikewise delegates towrite_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:check_copyandcheck_copy_nonoverlappingsketches. Both are superseded by workingproof_for_contractharnesses oncopy_wrapper/copy_nonoverlapping_wrapper. The sketches' own comment notes they couldn't useArbitraryPointerbecause contract checking rejects calls to the function under verification; the wrapper pattern is what resolves that.#[cfg(not(kani))]-disabledwrite_bytesharness and its kani#90FIXME. kani#90 remains open upstream; what no longer reproduces on the pinned toolchain is the specific configuration this FIXME guarded:check_write_bytesverifies againstwrite_bytes_wrapper(all 296 checks pass), and the harness carries akani::coverwitnessing the exact kani#90 trigger — a 0-count write to a non-dereferenceable pointer — as reachable, not assumed away. If maintainers would rather keep theFIXMEuntil 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, whichgit grep parse_u64_intoacrosslibrary/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
All harnesses live in
library/core/src/intrinsics/mod.rsundermod verify. The diff touches thatone file and nothing else.