Challenge 13: safety of CStr - #638
Open
stefanzetzsche wants to merge 2 commits into
Open
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.