Skip to content

Verify CStr CloneToUninit and Index<RangeFrom> safety (Challenge 13) - #543

Open
jrey8343 wants to merge 7 commits into
model-checking:mainfrom
jrey8343:challenge-13-cstr
Open

Verify CStr CloneToUninit and Index<RangeFrom> safety (Challenge 13)#543
jrey8343 wants to merge 7 commits into
model-checking:mainfrom
jrey8343:challenge-13-cstr

Conversation

@jrey8343

@jrey8343 jrey8343 commented Feb 8, 2026

Copy link
Copy Markdown

Summary

Completes Challenge 13 (CStr safety) criterion 4: a verified safety contract for the unsafe CloneToUninit for CStr impl, plus the existing Index<RangeFrom<usize>> harness.

  • check_clone_to_uninit_contract (new): #[kani::proof_for_contract(CStr::clone_to_uninit)] harness matching the existing contract-harness pattern in c_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 on dest'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: verifies ops::Index<RangeFrom<usize>> maintains is_safe() on the resulting sub-CStr.

Changes

  • library/core/src/clone.rs: the clone_to_uninit contract is strengthened from #[requires(!dest.is_null())] to the documented precondition, using the repo's memory predicates:
    #[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))))]
    can_write covers non-null, single-allocation bounds for size_of_val(self) bytes, and alignment (trivial for u8), in the same form as the NonNull::write_bytes contract; 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: adds check_clone_to_uninit_contract alongside the existing harnesses.
  • Merged with current main (Kani pin d4df833, 0.67): the previously pinned Kani 0.65 miscompiles contracts attached to this trait impl method — a proof_for_contract harness 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):

Harness Bound Result
check_clone_to_uninit_contract MAX_SIZE=16, unwind=17 306 checks, 0 failures, ~8s
check_clone_to_uninit (functional) MAX_SIZE=8, unwind=9 290 checks, 0 failures, ~38s
check_index_from MAX_SIZE=32, unwind=33 0 failures, ~132s
full ffi::c_str::verify suite (post-rebase regression) 15 of 15 harnesses verified, 0 failures

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 in c_str.rs.

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.
@jrey8343
jrey8343 requested a review from a team as a code owner February 8, 2026 02:28
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).
@jrey8343

Copy link
Copy Markdown
Author

CI is passing — ready for review.

@feliperodri feliperodri added the Challenge Used to tag a challenge label Mar 9, 2026
@feliperodri
feliperodri requested a review from Copilot March 31, 2026 22:17

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

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_from to verify &c_str[idx..] preserves CStr::is_safe() and matches the expected byte tail.
  • Added Kani harness check_clone_to_uninit to exercise CStr’s CloneToUninit implementation.
  • Added a #[requires(!dest.is_null())] contract on CStr’s clone_to_uninit implementation.

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).

Comment thread library/core/src/ffi/c_str.rs
Comment thread library/core/src/ffi/c_str.rs Outdated
Comment thread library/core/src/clone.rs
Comment thread library/core/src/clone.rs Outdated
@feliperodri feliperodri assigned jrey8343 and unassigned jrey8343 Apr 1, 2026
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 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.

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 trasnsparenttransparent: 👍
  • check_index_from looks good — idx < bytes_with_nul.len() keeps the tail NUL-terminated, and the assert_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.

jrey8343 and others added 2 commits August 18, 2026 21:24
…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>
@jrey8343

Copy link
Copy Markdown
Author

@feliperodri Thanks for the review — both points are addressed, and the description is reconciled with the code.

1(a) — contract strengthened. clone_to_uninit now requires dest valid for writes of size_of_val(self) bytes using the repo's memory predicates, in the same form as the NonNull::write_bytes contract:

#[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))))]

can_write subsumes the old !dest.is_null() (non-null + single-allocation bounds + alignment), and the modifies clause captures the body's exact write footprint.

1(b) — contract now verified. Added check_clone_to_uninit_contract, a #[kani::proof_for_contract(CStr::clone_to_uninit)] harness following the existing pattern (check_from_bytes_with_nul_unchecked, check_from_ptr), with a deliberately uninitialized MaybeUninit destination — the contract claims writes-only validity, so the harness must not rely on dest's contents. check_clone_to_uninit stays as the functional byte-for-byte check, as you suggested.

One finding worth flagging: under the previously pinned Kani 0.65, a contract attached to this trait impl method cannot be verified — the proof_for_contract harness fails the builtin-memcpy assigns check regardless of the modifies clause. We isolated it empirically: the identical contract on a free function over the identical body verifies, and with any #[requires] present on the impl, even contract proofs that never call it fail the same check. The current pin (d4df833, Kani 0.67) handles the impl contract correctly, so this branch is merged with current main and the harness targets the impl directly. Happy to file a minimized repro upstream against 0.65 if that's useful.

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 --no-assert-contracts and --object-bits 12), plus a post-rebase regression run of the whole ffi::c_str::verify suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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