Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics - #618
Challenge 2 (partial): safety contracts + verification for 15 of 20 raw-pointer core::intrinsics#618ivmat wants to merge 7 commits into
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
Factor the intrinsic's fallback body into a private typed_swap_nonoverlapping_fallback helper, marked rustc_const_stable_indirect so it stays callable from the const-stable-indirect intrinsic. The intrinsic and the Kani verification wrapper in mod verify now both call this same helper instead of the wrapper carrying a separate copy of the body, so the proof covers the production fallback path and the two cannot drift apart. Updates the wrapper's comment to match.
|
Implemented the changes and scope relabelling at 9dc6083, and the shared-helper extraction you 1. vtable_size / vtable_align hard-coded to u32. Added verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4636 to 4834 in e7b1bc1 The harnesses include your u64 counterexample and six further size/alignment cases, among them[u8; 8] (size 8, align 1), a ZST, and an over-aligned type. The raw *const () wrappers remain asu32/u64 probes, labelled as such and excluded from the verified count. One residual statedin-code: rustc's layout_of supplies both sides of the comparison.
2. size_of_val_wrapper precondition stronger than documented. verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4393 to 4537 in e7b1bc1 Residuals: composite unsized tails are not instantiated, and extern type is excluded becauseKani's model panics on it. 3. compare_bytes_wrapper tractability cap in #[requires]. Done as asked. verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4351 to 4391 in e7b1bc1 4. volatile_load / volatile_store exclude the MMIO case. No code change. Taking your second verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4539 to 4571 in e7b1bc1 5. arith_offset_wrapper's undocumented 0..=8 bound. No contract change. Taking your framing: the verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4228 to 4257 in e7b1bc1 Non-blocking: check_copy_untyped oracle asymmetry. Fixed at the helper — verify-rust-std/library/core/src/intrinsics/mod.rs Lines 2963 to 2981 in e7b1bc1 verify-rust-std/library/core/src/intrinsics/mod.rs Lines 3580 to 3610 in e7b1bc1 ptr_offset_from precondition never exercised. Strengthened the fixture rather than noting the verify-rust-std/library/core/src/intrinsics/mod.rs Lines 4259 to 4315 in e7b1bc1 typed_swap_fallback_wrapper verifies a copied body. Took your suggestion and extracted the verify-rust-std/library/core/src/intrinsics/mod.rs Lines 2558 to 2567 in e7b1bc1 verify-rust-std/library/core/src/intrinsics/mod.rs Lines 3511 to 3528 in e7b1bc1 The PR body is updated to match the current scope. Re-requesting review. |
|
if you are interested, you can check acceptance file for this PR based on acceptance format i've been developing. |
|
Not yet complete, found some issuea |
Challenge 2 (partial): safety contracts and verification for 15 of 20 raw-pointer
core::intrinsicsTracking issue: #16. This PR does not close that issue.
Scope: 14 safety contracts checked by
#[kani::proof_for_contract]at bounded finite instantiations,plus
arith_offset's unbounded no-UB proof. No generic theorem over all types and sizes is claimed.Contracts use forwarding wrappers because kani#3325 blocks contracts on bodyless intrinsics (kani#3345).
Covered:
typed_swap(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. Not covered: the five volatileintrinsics under Residuals.
Response to the 2026-08-16 review
Blocking items, in review order:
vtable_size_coerced_wrapper/vtable_align_coerced_wrapper: unsize*const Tand compare withsize_of::<T>()/align_of::<T>().Checked at seven types through
Debug, relative to compiler layout; raw*const ()probes are excluded from the count.size_of_val_wrapper: no#[requires]for sized pointers; separatedynand symbolic-length slice wrappers.Slices require only
size * len <= isize::MAX; composite unsized tails andextern typeremain residuals.compare_bytes_wrapper: requires only both regions readable forbytes.COMPARE_BYTES_CAPis a harnessassume;covers witness non-zero length and the exact cap boundary.
volatile_load_wrapper/volatile_store_wrapper: scope is Rust-allocation-backed memory only.Documented aligned, non-trapping external-memory/MMIO access remains unverified.
arith_offset_bounded_probe/check_arith_offset_bounded_probe: excluded from the contract count.Safety rests on
check_arith_offset_unconditional_safety, with a fully symbolicisizeoffset.Non-blocking items:
check_copy_untyped: pairssrc.add(elem)withdst.add(elem);copy_wrappercaptures the source usingold(...).check_copy_wrapper_overlapping_nonuniform_contractchecks the pre-state oracle on an overlapping non-uniform fixture.check_ptr_offset_from_u32_wrapper_contract,check_ptr_offset_from_unsigned_u32_wrapper_contract, andcheck_ptr_offset_u32_fixture_partitionsexercise same/cross-allocation, forward/reverse order, and divisible/non-divisible distances; controls below.typed_swap_nonoverlapping_fallbackis shared by the production fallback and verification wrapper.Evidence
Targeted/module runs:
c35c201b46a8be04daaef874defa438c8a5e945c; the latercf5fd23c45cb38273a314d6229a9b214a16ebf3cchanges comments only. Repository-suite run:cf5fd23c.d4df833c8f8f18e632e7b0a7945bb2161f708990(tool_config/kani-version.toml)-Z unstable-options,-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing,--cbmc-args --object-bits 12kani::coversatisfied in both modesintrinsics::verify, CI mode (--no-assert-contracts)intrinsics::verify, dependency contracts assertedcf5fd23c,--no-assert-contractsThe asserting-mode failures,
check_transmute_slice_metadataandcheck_transmute_unchecked_slice_metadata,stop at
--object-bits 12: “too many addressed objects” (4096 objects), CBMC status 6, not a property violation.The stop reproduces on the untouched base for
check_transmute_slice_metadataand on the prior PR head for both;the base
_uncheckedrun timed out without a verdict. This revision does not introduce the capacity stop.A larger object budget is untested.
All five added/renamed harnesses named in the review response appear in
kani list, used for sharding.macOS was not run outside CI. The PR's CI run remains authoritative; no CI outcome is claimed here.
Controls
copy_wrappercall replaced bywrite_bytes(dst, 0, count)ensurespostcondition fails, both modesmodifiesvalidity, and postcondition destination read fail, both modesu32wrappersmem::swapdrop detectorforgetreplaced bydropDroppanic; tests the detector, not the implementationReversing each ablation restores success. The two copy memory-precondition ablations also hit an unsupported
same_allocationcheck and leave checks undetermined; only their separate memory-safety failures count as evidence.Criterion by criterion
arith_offsetsafety proof. Not satisfied for all 20.typed_swap_nonoverlapping, shares the helper symbolically executed through a wrapper. Equivalence to Kani's built-in model is not shown.requires; caps in harnesses. Carve-outs: volatile MMIO is unverified;arith_offsetuses the no-UB proof. Open for the five tool-blocked intrinsics.Assumptions and bounds
--no-assert-contracts); asserting-mode results are listed separately above.T = u32,N = 4,SHIFT = 1,COUNT = 3, four distinct non-zero words;every element in
0..3and byte in0..4. No genericTor count result.check_copy:T = char, twoPointerGenerator<100>allocations, symbolic pointers/count;DanglingandDeadObjectexcluded by assumption.-Z uninit-checksis off;can_dereference's initialization conjunct is inert. Copy checks are model-relative byte-value checks,with no Rust-semantic initialization claim or mixed-initialization relation.
check_copy_overlapping_shift_no_ub:SHIFT = 1,COUNT = 3, symbolic index; no exhaustive overlap sweep.u8, one 8-byte allocation (divisibility unexercised);u32, two 16-byte allocations, offsets0..16; no genericTresult.mem::swap:T = u8and a panic-on-DropADT; onlykani::modifies(x)/modifies(y), norequiresorensures.No value exchange proved; the ADT fixture witnesses no operand drop.
compare_bytescap 4, unwind 5, cap assumed in harness.Debug; expected/result share compiler layout. Includes[u8; 8], zero-sized, mixed-alignment, over-aligned types.size_of_val: sizedu32(null, dangling, non-deterministic-address, real pointers), fourdynerased types, four slice element types.arith_offset_bounded_probe:0..=8; its unbounded safety proof is separate.--object-bits 12fails loudly on overflow; no silent cap.kani::stuborstub_verified, or compiles out a shipped body;-Z stubbingserves pre-existing transmute harnesses.Residuals
Five intrinsics are unsupported at the pin:
volatile_copy_memory,volatile_copy_nonoverlapping_memory,volatile_set_memory,unaligned_volatile_load,unaligned_volatile_store. Their harnesses remain under#[cfg(not(kani))].Fixes are merged in kani#4672 (first three) and kani#4673 (last two), but the repository still pins
d4df833: 15/20 remains.A stand-in would omit volatile semantics and bypass codegen; kani#4672's investigation found
(dst, src)reversed in the sketchedvolatile_copy_memoryimplementation.Other open items:
mem::swap/typed_swap;implementation mutants for volatile, vtable,
size_of_val, andcompare_bytesoracles.size_of_val: composite unsized tails andextern typemetadata; volatile external memory/MMIO.Debug, and final emitted vtables independently of compiler layout.Usage and exposing tables
The challenge's five usage sites and five exposing functions are separate from the
15/20count.[T]::copy_from_slicecore::mem::swapcore::mem::align_of_valu32real-caller probe; documentedT: ?Sizeddomain remains residual.MaybeUninit::zeroedu32real-body probe exists elsewhere, not integrated; residual.parse_u64_intostd::ptr::*exposing rowsstd::ptr::swapresolves; all five residual pending intended targets.Claim ledger
A machine-checkable manifest of the claims above — one entry per intrinsic with its harnesses, bounds,
assumptions, and an assurance band that says whether the claim's oracle was ever watched to fail — is at
https://github.com/ivmat/acceptance-format/tree/main/examples/verify-rust-std-pr618 (revision 2, for
cf5fd23c). Why it is there: this description states scope in prose; the manifest states the samescope per claim in a fixed shape a reader can audit line by line and re-validate with the checker in
that repository, including the claims that have no control (all 15 per-intrinsic claims sit at the lowest band for
that reason; the one claim above it is the
copy_wrapperbyte-value oracle, which carries the observed-redimplementation mutation listed under Controls). It is hand-authored, is not part of this PR, and implies nothing about
acceptance.
Notes for the maintainers
parse_u64_into: removed by26d30a058104fe049a8b1d6d10d18ddf8c04864a(nightly-2025-06-17 subtree update).The current formatting path is not a one-to-one rename; the usage row needs an agreed disposition.
std::ptrtargets:copy_from_slice,parse_u64_into,align_of_val, andzeroeddo not resolve there.Please clarify intended targets:
MaybeUninit::zeroedandmem::zeroed, for example, have different safety boundaries.No proof is transferred from another row.
The removed
write_byteskani#90FIXMEcan be restored if preferred until upstream closure;its guarded configuration verifies here with a cover witnessing the trigger.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.