Verify CStr CloneToUninit and Index<RangeFrom> safety (Challenge 13) - #543
Verify CStr CloneToUninit and Index<RangeFrom> safety (Challenge 13)#543jrey8343 wants to merge 7 commits into
Conversation
Add the final 2 verification harnesses to complete Challenge 13: - check_clone_to_uninit: Verifies the unsafe CloneToUninit impl for CStr correctly copies all bytes (including NUL terminator) and produces a valid CStr at the destination. Includes safety contract on clone_to_uninit requiring non-null dest pointer. - check_index_from: Verifies ops::Index<RangeFrom<usize>> for CStr produces a valid CStr that maintains the safety invariant and matches the expected tail of the original bytes. Both harnesses are bounded (MAX_SIZE=16/32) with appropriate unwind limits and verify the CStr is_safe() invariant holds.
The harness was timing out (10 min CBMC limit) due to expensive symbolic pointer arithmetic in clone_to_uninit combined with a symbolic-length verification loop. Fix: reduce MAX_SIZE from 16 to 8 bytes (sufficient to cover empty, single-char, and multi-char C strings) and remove the byte-by-byte verification loop (the CStr reconstruction check still validates the safety invariant).
b3f7293 to
f0e5049
Compare
|
CI is passing — ready for review. |
There was a problem hiding this comment.
Pull request overview
Completes Challenge 13 by adding Kani verification coverage for CStr’s CloneToUninit and Index<RangeFrom<usize>> safety properties, and by annotating the CStr CloneToUninit impl with an explicit precondition.
Changes:
- Added Kani harness
check_index_fromto verify&c_str[idx..]preservesCStr::is_safe()and matches the expected byte tail. - Added Kani harness
check_clone_to_uninitto exerciseCStr’sCloneToUninitimplementation. - Added a
#[requires(!dest.is_null())]contract onCStr’sclone_to_uninitimplementation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
library/core/src/ffi/c_str.rs |
Adds two new Kani harnesses for CStr indexing safety and CloneToUninit behavior. |
library/core/src/clone.rs |
Adds a requires precondition to the CloneToUninit impl for CStr (plus supporting imports). |
Address review feedback: - Use initialized buffer to avoid UB from reading uninitialized memory - Assert exact byte-for-byte match with source - Document full safety contract requirements - Fix typo: trasnsparent -> transparent
feliperodri
left a comment
There was a problem hiding this comment.
Thanks — this is solid, real verification work: the two harnesses exercise the actual CStr impls with symbolic inputs and meaningful assertions (is_safe() plus exact byte-for-byte assert_eq!), the arbitrary_cstr helper isn't over-constrained, and you've already addressed the earlier automated-review notes (typo, exact-byte check, initialized destination buffer to keep the harness itself UB-free). Bounded verification is the right fit for Challenge 13 here.
I'm requesting changes on one substantive point plus a minor one, both around the clone_to_uninit safety contract.
1. The clone_to_uninit contract is both too weak and never verified
Challenge 13's criterion 4 requires the unsafe CloneToUninit impl to have a verified safety contract (footnote: "Unsafe functions will require safety contracts"). As written, the contract doesn't meet that bar:
(a) Too weak. #[requires(!dest.is_null())] under-specifies the documented precondition. clone_to_uninit requires dest to be valid for size_of_val(self) writes and properly aligned — non-nullness alone doesn't capture that. Please strengthen it using the repo's memory predicates (e.g. kani::mem::can_write / ub_checks::can_write for size_of_val(self) bytes), consistent with how pointer-based contracts are written elsewhere in core.
(b) Never verified. There is no #[kani::proof_for_contract(CStr::clone_to_uninit)]. In Kani, #[requires]/#[ensures] are only exercised by a proof_for_contract harness (or when the contract is used as a stub); a plain #[kani::proof] that calls the function runs the real body and ignores its contract. So check_clone_to_uninit does not check this contract — the annotation is currently decorative. This is inconsistent with the other unsafe functions in the same file (from_bytes_with_nul_unchecked, strlen, from_ptr), which are all verified via #[kani::proof_for_contract].
Please add a #[kani::proof_for_contract(CStr::clone_to_uninit)] harness that verifies the (strengthened) contract, matching the existing pattern. check_clone_to_uninit is a good functional/bounded check and can stay alongside it.
2. PR description doesn't match the code
The description states check_clone_to_uninit uses MAX_SIZE=16, unwind=17 (~159s), but the code uses MAX_SIZE=8, unwind=9. Please reconcile so the stated verification evidence matches what's actually run.
Nits (already mostly handled)
- Typo fix
trasnsparent→transparent: 👍 check_index_fromlooks good —idx < bytes_with_nul.len()keeps the tail NUL-terminated, and theassert_eq!against&bytes_with_nul[idx..]is a nice functional check.
Once the contract is faithful and verified (and the numbers reconciled), this should be good to go — the harness structure itself is sound.
…ract Per review on model-checking#543: - The CloneToUninit for CStr contract now uses the repo's memory predicates: requires can_write(slice_from_raw_parts_mut(dest, size_of_val(self))) plus a matching kani::modifies clause, replacing the weaker !dest.is_null() (which can_write subsumes). - New check_clone_to_uninit_contract harness: #[kani::proof_for_contract(CStr::clone_to_uninit)] with a deliberately uninitialized MaybeUninit destination, following the existing contract harness pattern in c_str.rs. The functional byte-for-byte check stays in check_clone_to_uninit. - Branch merged with current main: the previously pinned Kani 0.65 miscompiles contracts attached to this trait impl method (builtin memcpy assigns-check failure); the current pin (0.67, d4df833) verifies the impl contract directly. Local verification with the pinned Kani and CI flags: full ffi::c_str::verify suite passes, 15 of 15 harnesses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@feliperodri Thanks for the review — both points are addressed, and the description is reconciled with the code. 1(a) — contract strengthened. #[requires(crate::ub_checks::can_write(
crate::ptr::slice_from_raw_parts_mut(dest, crate::mem::size_of_val(self))
))]
#[cfg_attr(kani, kani::modifies(crate::ptr::slice_from_raw_parts_mut(dest, crate::mem::size_of_val(self))))]
1(b) — contract now verified. Added One finding worth flagging: under the previously pinned Kani 0.65, a contract attached to this trait impl method cannot be verified — the 2 — description numbers reconciled. The functional harness runs MAX_SIZE=8/unwind=9 (the code was right; the description was stale). The new contract harness runs MAX_SIZE=16/unwind=17. The updated description lists fresh local results for every harness under CI's exact flags (including |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Completes Challenge 13 (CStr safety) criterion 4: a verified safety contract for the unsafe
CloneToUninit for CStrimpl, plus the existingIndex<RangeFrom<usize>>harness.check_clone_to_uninit_contract(new):#[kani::proof_for_contract(CStr::clone_to_uninit)]harness matching the existing contract-harness pattern inc_str.rs(check_from_bytes_with_nul_unchecked,check_from_ptr). The destination buffer is deliberately uninitialized (MaybeUninit): the contract claims validity for writes only, so the harness must not rely ondest's contents.check_clone_to_uninit: unchanged bounded functional check (exact byte-for-byte copy including the NUL terminator), with an initialized buffer.check_index_from: verifiesops::Index<RangeFrom<usize>>maintainsis_safe()on the resulting sub-CStr.Changes
library/core/src/clone.rs: theclone_to_uninitcontract is strengthened from#[requires(!dest.is_null())]to the documented precondition, using the repo's memory predicates:can_writecovers non-null, single-allocation bounds forsize_of_val(self)bytes, and alignment (trivial foru8), in the same form as theNonNull::write_bytescontract; the old!dest.is_null()is subsumed. The modifies clause captures the exact write footprint of the body.library/core/src/ffi/c_str.rs: addscheck_clone_to_uninit_contractalongside the existing harnesses.main(Kani pind4df833, 0.67): the previously pinned Kani 0.65 miscompiles contracts attached to this trait impl method — aproof_for_contractharness fails the builtin-memcpy assigns check regardless of the modifies clause, while the identical contract on a free function over the identical body verifies. Kani 0.67 checks the impl contract directly.Verification
Local, pinned Kani 0.67.0 (
d4df833), CI's exact flags (-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing --no-assert-contracts --cbmc-args --object-bits 12):check_clone_to_uninit_contractcheck_clone_to_uninit(functional)check_index_fromffi::c_str::verifysuite (post-rebase regression)All harnesses are bounded, per Challenge 13's stated assumption ("Harnesses may be bounded"). The functional harness's bound is MAX_SIZE=8/unwind=9 — the description now matches the code (the earlier text incorrectly said 16/17).
Resolves
Challenge 13: Safety of
CStr(#150), criterion 4 (CloneToUninit,Index<RangeFrom>), completing the criteria already covered by the existing harnesses inc_str.rs.