Challenge 13: safety of CStr - #638
stefanzetzsche wants to merge 2 commits into
Conversation
…lver resource abort The CI failure on this harness is a CBMC resource abort (no property counterexample; the SAT instance reaches ~21M clauses and CBMC dies), not a spec defect: from_bytes_with_nul carries the module's heaviest ensures — a three-way branch characterization with two slice comparisons — and evaluating it under proof_for_contract over an any-length 16-byte input exceeds runner resources. Reducing the backing array to 8 bytes (unwind 9) roughly halves the symbolic input while preserving every behavior class the contract distinguishes; three added cover witnesses prove the Ok, InteriorNul, and NotNulTerminated branches all remain reachable at the reduced bound, so the shrink cannot silently vacuate the proof.
feliperodri
left a comment
There was a problem hiding this comment.
Approving — leading solution for Challenge 13
Thanks @stefanzetzsche. After reviewing all four open Challenge 13 (CStr safety) submissions, this is the most complete and we're prioritizing it in the review process.
What stood out:
- All four success criteria covered. The Invariant impl is a real predicate (non-empty + NUL-terminated + no interior NUL, correctly not requiring UTF-8), all 9 safe methods are contract- or behaviorally-verified (including
bytes/to_str, which other submissions leave as plain proofs), the 3 unsafe fns keep their real contracts +proof_for_contract, and both trait impls (CloneToUninit,Index<RangeFrom>) are verified via genuineproof_for_contract— includingCloneToUninitwrite-validity on aMaybeUninitdestination. - Sound. Clean on our vacuity checks (no cfg(kani) body swaps, no trivial invariant, no decorative/unverified contracts, no assume-the-conclusion); inputs are fully symbolic with
kani::covernon-vacuity anchors; no runtime std logic changed. - Nice addition: the two-oracle invariant-fidelity harness that cross-checks
is_safe()against independent structural/semantic oracles on arbitrary (possibly-invalid) bytes.
One non-blocking note: since main already ships harnesses for criteria 1–3, the −80 lines here rewrite some already-merged harnesses into contract form. Reviewers should diff each replaced harness against main to confirm no existing coverage is weakened (e.g. a smaller bound). This doesn't hold up the approval.
Bounded harnesses are explicitly allowed for this challenge, so the documented bounds are fine.
|
@patricklam could you review this PR proposed solution? I believe it's the winner for Challenge 13. |
|
Will aim to get to it later this week. Thanks for your patience. |
Towards #150. Solves Challenge 13: Safety of
CStr: a fidelity harness for the safety invariant (criterion 1), contracts and Kani harnesses for all 9 safe methods (criterion 2), safety contracts for the 3 unsafe functions (criterion 3), and proofs for the 2 trait implementations (criterion 4). All 15 harnesses (14 inffi::c_str::verify, 1 inclone::verify) pass viascripts/run-kani.sh.Changes
c_str.rsclone.rsunsafe impl CloneToUninit for CStrplus aproof_for_contractharness in a newclone::verifymoduleCriterion 1: safety invariant
CStralready implementsInvariant(is_safe: non-empty, nul-terminated, no interior nul). What was missing is evidence that the predicate itself is right: every existing harness only ever evaluatesis_safe()on an already-validCStrobtained from a constructor, so a too-loose invariant (e.g. one missing the interior-nul clause) would pass all of them. The newcheck_invariantharness reinterprets an arbitrary — possibly invalid — byte sequence as&CStr(the exact castfrom_bytes_with_nul_uncheckedperforms;is_safeonly reads initialized bytes, so this is sound) and pins down exactly whenis_safe()holds against two independent oracles: a structural one (the FIRST nul sits at the final index, phrased withpositionrather than the invariant's own form, so the equivalence is a theorem and not a restatement) and a semantic one (is_safe()agrees withfrom_bytes_with_nul(..).is_ok()— the invariant accepts exactly what the safe constructor accepts).Criterion 2: the 9 safe methods
Each harness feeds an arbitrary valid
CStr(any content length in0..=31, viafrom_bytes_until_nulover a bounded nul-terminated array) and confirms the invariant still holds after the call.from_bytes_until_nul,from_bytes_with_nul#[ensures]contracts characterizing both result branches exactly (Ok: prefix up to and including the first nul / the whole slice, satisfying the invariant; eachErrvariant pinned to the first-nul position), verified withproof_for_contractcount_bytes,is_empty,to_bytes,to_bytes_with_nul#[ensures]contracts pinning each result to the public byte views so no method can silently drift from the others (to_bytes_with_nul, the most primitive view, gets the invariant restated on its output; the others are phrased against it, keeping the specification acyclic), verified withproof_for_contractbytes,to_str,as_ptrResult<&str, _>, raw pointer) cannot carry a return-value#[ensures].bytesyields exactlyto_bytes()and stops at the terminator;to_stronOkreturns byte-for-byteto_bytes()(a nul is valid UTF-8, so only exact equality proves the terminator is excluded) and onErrreports a failure position inside the content;as_ptris non-null and valid for reads of the full nul-terminated view, reproduced byte-for-byteCriterion 3: the 3 unsafe functions
from_bytes_with_nul_uncheckedrequires: the safety invariant restated on the input slice (its documented safety requirement);ensures: the result upholds the invariant. The harness adds a round-trip check (output view == input bytes), kept out of the shared contract so the callers that stub this function are unaffectedfrom_ptrrequires: non-null and nul-terminated withinisize::MAX(pre-existing);ensures:is_safe(). Verifying the contract also covers the internalstrlenwalk and thefrom_raw_partsprojectionstrlenrequires: nul-terminated (pre-existing);ensures: the result is a legal offset pointing at a nul. The harness additionally asserts the FIRST-nul property — a mutant returning a later nul index would satisfy the contract but break the semanticsfrom_ptr'sis_saferelies on; it stays in the harness so stubbing callers are unaffectedCriterion 4: the 2 trait implementations
Index<RangeFrom<usize>>is a safe fn, so its harness is behavioral, on the defined (non-panicking) pathstart < lenwhere the body performs the unsafe reinterpret: the sub-CStrupholds the invariant, and its nul-terminated view is exactly the suffixbytes[start..]— which kills a wrong-offset mutant that would still beis_safe().CloneToUninit::clone_to_uninitis an unsafe fn and gets a safety contract (per the challenge's[^unsafe-fn]footnote):requiresdemandsdestbe writable forsize_of_val(self)bytes (ub_checks::can_writeover exactly the write footprint, which also carries thekani::modifiesclause),ensuresstates the written bytes equal the source's nul-terminated view, so*destis left a validCStr— the trait's documented promise. Theproof_for_contractharness clones into a fresh, deliberately uninitialized buffer (MaybeUninit— the contract claims validity for writes only, so the proof must not rely ondest's contents) and additionally asserts that the written bytes reinterpreted as&CStruphold the invariant: cloning a validCStryields a validCStr.All harnesses are bounded (explicitly allowed by the challenge's stated assumptions), with backing arrays of 16 or 32 bytes and matching unwind bounds that fully unroll every scan. Wherever a harness restricts its inputs,
kani::coverwitnesses confirm that the interesting shapes — empty and non-empty C strings, both result branches, valid and invalid byte sequences — are actually reachable, so no proof passes vacuously.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.