Repository navigation
Conversation
Adds a #[cfg(kani)] mod verify to library/alloc/src/string.rs with 16 harnesses covering all 15 functions of Challenge 10. No production code is changed. - Any length (symbolic len/cap up to CBMC's object limit, loop-free): pop, remove, insert, insert_str, split_off, drain (next and drop), into_boxed_str, leak. - Any receiver length, bounded edit/match count: replace_range (<= 4 bytes removed and inserted), remove_matches (<= 4 matches, most-general Searcher). - Bounded because of current Kani limitations (documented in the PR): retain (<= 12 chars), from_utf16le/from_utf16be/from_utf16be_lossy (<= 12 bytes), from_utf16le_lossy (<= 8 bytes). Written with AI assistance.
|
A note on the two failing Kani Metrics jobs. The ubuntu-latest job was stopped by a runner shutdown ( This does not look related to the change. The PR only adds a Could the failed jobs be re-run when convenient? Thanks! |
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 3
Open (4)
String::from_utf8_unchecked(v)is invoked onvwhose bytes are never proven (or constructed) to… · New This constructs an&strfrom arbitrary bytes viafrom_utf8_unchecked, which is UB if the slice… · NewAnySearcheris intended to satisfy theunsafe trait Searchercontract (non-overlapping matches… · New The module-level documentation claims every harness is loop-free and that no#[kani::unwind]… · New
What changed in this PR
Adds Kani verification harnesses for Challenge 10 to check memory safety of String methods (including the “(U)” set) by running harnesses over real stdlib bodies under #[cfg(kani)].
Changes:
- Appends a new
#[cfg(kani)] mod verifycontaining proof harnesses for 15Stringfunctions. - Introduces symbolic input generators (
any_string_bytes,with_char_at) and coverage points for non-vacuity. - Adds specialized harnessing for
replace_range/remove_matchesand bounded harnesses forretain/from_utf16*with documented tool limitations.
| File | Description |
|---|---|
| library/alloc/src/string.rs | Adds a #[cfg(kani)] verification module with Kani harnesses, helpers, and a custom Pattern/Searcher for remove_matches. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…urate docs - String generators: build the buffer with vec![0u8; cap] (a zeroed allocation, still loop-free and of symbolic size) and truncate it, and in with_char_at encode an arbitrary char at an arbitrary offset. Every generated String is now initialized, valid UTF-8; before, set_len exposed uninitialized capacity as string bytes. A ptr::write_bytes fill was tried first, but it made CBMC run out of memory while Kani generated traces. - replace_range: the replacement is a checked &str (str::from_utf8, invalid input assumed away), i.e. any valid UTF-8 of <= 4 bytes. - remove_matches: AnySearcher never repeats an empty match at the same position; its doc and SAFETY comment now follow the Searcher contract. - Module doc: bounds of each harness, and the content-independence argument for the generators. Written with AI assistance.
…nger covers - any_valid_string (retain): encode the chars into a zeroed vec![0u8; 4 * N] and truncate it, instead of writing through a slice over uninitialized spare capacity. - AnySearcher: drop the assumption that an empty match never repeats at one position. The Searcher contract allows it (adjacent, zero-length ranges), so the searcher is the most-general one again; its doc and SAFETY comment now state exactly what is generated. - remove_matches: up to 5 matches (unwind 7), so the rejections Vec grows past its initial capacity of 4. - Covers: remove_matches shows a real removal leaving a long string and the 5-match case; retain shows a multi-byte char kept after a deletion (the unsafe re-encode path). - Comment fixes: check_pop SAFETY (UTF-8 validity), with_char_at capacity bound uses the char's width, panic exclusions. Written with AI assistance.
|
Thanks for the review. ef9133a and 277b53e address the Copilot comments and a further review pass:
All 16 harnesses pass with Kani at 8 GiB and with plain CBMC, and every cover is satisfied. For each changed harness, flipping its assertion makes CBMC report exactly that failure. |
|
CI on 277b53e: all four
|
- Replace the char-boundary assumes with assert!s: on the all-NUL strings every index up to the length is a boundary, and in check_drain_next the end lies in the NUL suffix. The Searcher of check_remove_matches asserts its boundary requirement instead of assuming it. - Express the MAX_ALLOC size limits as any_where domains of the generators (any_string_max_len, with_char_at), so the module has no kani::assume. - replace_range: build the replacement constructively (up to 4 encoded chars in a zeroed buffer) instead of from_utf8 plus assume(false); unwind bound 5 (4 iterations plus the exit test). - Share an any_bounds generator between replace_range and drain, with a cover per Bound variant; covers for the reallocation paths. - retain: assert that the new length equals the bytes of the kept chars. - from_utf16*: covers for a leading surrogate pair that decodes and a leading unpaired surrogate, assert Err for the latter in the strict variants; unwind 7 (6 code units plus the exit test), which also lets from_utf16le_lossy run on inputs of up to 12 bytes like the others. - Add should_panic harnesses for insert, insert_str, split_off, drain and replace_range at indices strictly inside a multi-byte char or past the end. - Document each unwind bound and update the module comment.
A should_panic harness passes as soon as some input panics. With an index that is either inside a multi-byte char or past the end, the past-the-end inputs alone made the harnesses for insert, insert_str, split_off, drain and replace_range pass even without the char-boundary checks (reported by rajath-mk in review). Each of these harnesses is now two: one with an index strictly inside a multi-byte char (for ranges, the other end in bounds, so only a char-boundary check can panic) and one with an index past the end.
|
@rajath-mk thanks again for the review. While you are looking at this PR, could the committee help with two scope questions? The answers decide where I spend the next weeks.
|
- retain: main now has the PanicGuard version, whose drop runs debug_assert!(str::from_utf8(..).is_ok()). Under -Z loop-contracts the abstracted validator can reject valid UTF-8 with a multi-byte char. check_retain_bounded now checks the exact result (up to 5 chars) and replaces from_utf8 by from_utf8_kept, which accepts only the expected bytes. - from_utf16*: check Ok/Err and the exact length against a reference decoder (utf16_expected); big-endian inputs up to 16 bytes; comments follow the new from_utf16_units loop. - New panic harnesses: remove (inside a char, at the end, past the end), and for drain and replace_range a reversed range, ..=usize::MAX and (Excluded(usize::MAX), Unbounded).
drain and replace_range check a range a..b in this order: b <= len and a <= b (slice::range), then that a, and then b, is a char boundary. Split the "inside a char" harnesses into start inside, end inside and both inside, so that without the check that should fail no input of a harness panics. The past-the-end harnesses now take any start, and the reversed-range harnesses any start greater than the end, so that these domains and the valid ranges of the main harnesses cover every pair (a, b).
A should_panic harness accepts any panic. Without the range checks of slice::range, the four *_max harnesses still pass through an arithmetic overflow, and check_replace_range_panics_reversed through later checks. Add three ordinary harnesses that check that slice::try_range rejects ..=usize::MAX, (Excluded(usize::MAX), Unbounded) and every reversed range a..b with b <= len, for every len up to MAX_ALLOC. try_range returns None exactly when slice::range panics, and here an overflow is a failure.
|
Merged |


Partially addresses #61 (Challenge 10): all 15 functions have harnesses over the real bodies; of the 9 "(U)" functions, 2 are verified for arbitrary lengths, 2 for arbitrary receiver length with a bounded edit/match count, and 5 are bounded because of the Kani limitations documented below.
Summary
This PR adds Kani harnesses in
library/alloc/src/string.rs(#[cfg(kani)] mod verify) for all 15 functions listed in Challenge 10. Every harness runs over the real function bodies. The PR changes no production lines, has nocfg(kani)body swaps, nokani::assume, and replaces no function under test. It has one stub: in theretainharness,core::str::from_utf8(called byretaininside adebug_assert!) is replaced by a stand-in that checks the exact expected bytes (see "Bounded harnesses").Updated for current
main(merged at 8d4cbf8: nightly-2026-09-25, Kani1640445, CBMC 6.11.0). Since the previous version,mainhas thePanicGuardversion ofretainand the newfrom_utf16_unitsloop offrom_utf16le/from_utf16be; the harnesses and the explanations below follow that code.insert_strandsplit_off. The strings have symbolic length and capacity, limited only by the largest object CBMC can represent under--object-bits 12.replace_rangeandremove_matches. The receiver has arbitrary length. Only the number of edited bytes or matches is bounded, because those are the only loop trip counts.retain(at most 5 chars, exact result checked) and the fourfrom_utf16*functions (at most 12 input bytes for the little-endian and 16 for the big-endian variants, exact result length andOk/Errchecked).pop,remove,insert,drain,into_boxed_strandleak, verified for arbitrary lengths (their harnesses are loop-free).#[kani::should_panic]harnesses cover the inputs that the other harnesses stay out of, one failing check per harness. With the main harnesses, every index ofinsert,insert_str,split_offandremove, and every rangea..bofdrainandreplace_range, is in the domain of some harness (tables below).slice::try_rangerejects..=usize::MAX,(Excluded(usize::MAX), Unbounded)and every reversed rangea..bwithb <= len, for every length up toMAX_ALLOC.try_rangereturnsNoneexactly whenslice::range, whichdrainandreplace_rangeuse, panics. An arithmetic overflow fails these harnesses, so they show that those panics come from the range check itself.Status per function
insert_strcheck_insert_str,check_insert_str_panics_inside_char,check_insert_str_panics_past_endsplit_offcheck_split_off,check_split_off_panics_inside_char,check_split_off_panics_past_endreplace_rangecheck_replace_range,check_replace_range_panics_start_inside_char,check_replace_range_panics_end_inside_char,check_replace_range_panics_both_inside_char,check_replace_range_panics_past_end,check_replace_range_panics_reversed,check_replace_range_panics_end_included_max,check_replace_range_panics_start_excluded_maxremove_matchescheck_remove_matchesretaincheck_retain_boundedfrom_utf16lecheck_from_utf16le_boundedfrom_utf16le_lossycheck_from_utf16le_lossy_boundedfrom_utf16becheck_from_utf16be_boundedfrom_utf16be_lossycheck_from_utf16be_lossy_boundedpopcheck_popremovecheck_remove,check_remove_panics_inside_char,check_remove_panics_at_end,check_remove_panics_past_endinsertcheck_insert,check_insert_panics_inside_char,check_insert_panics_past_enddrainnext, and drop)check_drain_next,check_drain_drop,check_drain_panics_start_inside_char,check_drain_panics_end_inside_char,check_drain_panics_both_inside_char,check_drain_panics_past_end,check_drain_panics_reversed,check_drain_panics_end_included_max,check_drain_panics_start_excluded_maxinto_boxed_strcheck_into_boxed_strleakcheck_leakThe three
check_range_rejects_*harnesses checkslice::try_range, which runs the same range check as theslice::rangethatdrainandreplace_rangecall (see the range table below).Input classes of the index-taking functions
Each class is the domain of one harness, and the classes of a function together contain all of its inputs. All receivers have symbolic length (and capacity) up to
MAX_ALLOC; where a class needs a multi-byte char,with_char_at()places an arbitrarycharat an arbitrary offset (the only indices of that string that are not char boundaries are inside this char).idxinsertinsert_strsplit_offremoveidx < lencheck_insertcheck_insert_strcheck_split_offcheck_removeidx == lencheck_insertcheck_insert_strcheck_split_offcheck_remove_panics_at_endcheck_insert_panics_inside_charcheck_insert_str_panics_inside_charcheck_split_off_panics_inside_charcheck_remove_panics_inside_charidx > lencheck_insert_panics_past_endcheck_insert_str_panics_past_endcheck_split_off_panics_past_endcheck_remove_panics_past_enddrainandreplace_rangecheck a rangea..bin this order:b <= lenanda <= b(inslice::range), then thata, and thenb, is a char boundary.drainreplace_rangeslice::try_rangea <= b <= len, both char boundaries, everyRangeBoundsshapecheck_drain_next,check_drain_dropcheck_replace_rangeb > len, anyacheck_drain_panics_past_endcheck_replace_range_panics_past_endb <= len,a > bcheck_drain_panics_reversedcheck_replace_range_panics_reversedcheck_range_rejects_reversedainside a char,ba char boundarycheck_drain_panics_start_inside_charcheck_replace_range_panics_start_inside_charaa char boundary,binside a charcheck_drain_panics_end_inside_charcheck_replace_range_panics_end_inside_chara <= b, both inside the same charcheck_drain_panics_both_inside_charcheck_replace_range_panics_both_inside_char..=usize::MAXcheck_drain_panics_end_included_maxcheck_replace_range_panics_end_included_maxcheck_range_rejects_end_included_max(Excluded(usize::MAX), Unbounded)check_drain_panics_start_excluded_maxcheck_replace_range_panics_start_excluded_maxcheck_range_rejects_start_excluded_maxThe panic harnesses give the range as
a..b(or the shape named in the table). The main harnesses give the valid ranges in everyRangeBoundsshape (any_bounds). Invalid ranges in the other shapes (for examplea..=bwithb >= len) have no harness of their own:slice::rangechecks the bounds as given and panics through the sameslice_index_fail, beforedrainorreplace_rangereaches any unsafe code.The harnesses in the last column are ordinary harnesses, not
should_panicones:slice::try_rangereturnsNoneexactly whenslice::rangepanics, and an overflow fails them. So they show that the range check itself rejects these three classes (see the mutation checks below).Approach
Symbolic strings.
any_string()builds aStringwith symbolic capacitycap <= MAX_ALLOCand symbolic lengthlen <= capfrom a zeroed buffer (vec![0u8; cap], thentruncate(len)), so every byte is initialized and the string is valid UTF-8 (any_string_max_len(m)additionally limits the length tom). No loop runs, so there is no unwinding bound.MAX_ALLOC = 2^51 - 1is the largest single object under--object-bits 12, the settingscripts/run-kani.shuses. It is a limit of the memory model, not a bound chosen for tractability; #681 (Challenge 25) limits its symbolic capacity by the CBMC object model in the same way.Contents. Memory safety of these functions depends only on the lengths, the capacity, the given indices, the char-boundary checks at those indices, and at most one decoded char (
pop,removeand theDrainiterator decode exactly one). So the harnesses use NUL-filled strings, and where a char is decoded,with_char_at()places a well-formed encoding of an arbitrarycharat an arbitrary offset inside the zeroed buffer. On an all-NUL string every index up to the length is a char boundary, so the harnesses reach every combination of these quantities for which the boundary checks pass; the combinations for which they fail are the panics below. All inputs are initialized, valid UTF-8 by construction.No assumptions beyond input domains. The module has no
kani::assume. Every restriction of an input is akani::any_wheredomain at the point where the input is generated: sizes that fit in one CBMC object, indices inside the string (which also keeps the main harnesses out of the out-of-range panics), the bounds in the table above, and, in the panic harnesses, the input class of the harness. The documented preconditions (indices on char boundaries) are not assumed: they hold by construction, and the harnesses that pick an indexassert!that it is a char boundary before the call.Panics. Panics are not UB, so the main harnesses stay out of the documented panic conditions by construction, and the 23
#[kani::should_panic]harnesses cover those inputs, with one failing check per harness: ashould_panicharness passes as soon as some input panics, so a harness whose inputs could fail two different checks could pass on one of them alone (found in review). Where a range has both ends inside the same char, both checks fail and the start check fires first; that case has its own harness. Kani accepts such a harness only if a panic-class failure is reachable and every failing check is of that class, so no UB is reachable for any input of the harness (Kani usespanic=abort). For each harness, the logs show that the failing check is exactly the documented panic: anis_char_boundaryassertion, the out-of-range or reversed-range panic ofslice::range(slice_index_fail), the slicing panic ofstr(forremove), orremove's "cannot remove a char from the end of a string".pophas no panic condition.check_removeand#[kani::unwind(2)].self[idx..]has a panic path (str::slice_error_fail_rt) with a loop (ceil_char_boundary). Under this harness that path is infeasible (idxis the start of a well-formed char); the bound only cuts it, and unwinding assertions stay enabled.check_remove_panics_inside_chartakes that path, and its#[kani::unwind(4)]covers the at most 3 iterations of that loop (the next char boundary is at most 3 bytes away).replace_range/remove_matches. Every loop reachable from these functions iterates once per edited byte or per match (forreplace_range: theSplicedrop, drain, fill and extend loops; forremove_matches: the collection of matches and the compaction loop). No loop iterates once per byte of the receiver: the receiver-sized work is done byptr::copy/copy_nonoverlappingcalls (moving the tail, and the reallocation), which are loop-free. So the receiver has arbitrary length and capacity, and only the edit size or match count is bounded, with a matching#[kani::unwind]; an insufficient bound fails the unwinding assertion.(Bound, Bound)encoding of a valid rangestart..end, which covers everyRangeBoundsshape of it; a cover perBoundvariant shows each is reached (thedrainharnesses use the same generator).remove_matches, the pattern is a most-generalSearcher(AnyPattern): it may return any sequence of up to 5 matches allowed by theSearchercontract ("adjacent, non-overlapping, covering the whole haystack, and laying on utf8 boundaries"; ranges may have zero length). The boundary requirement is asserted inside the searcher, not assumed. With 5 matches theVecof matches grows past its initial capacity, and a cover shows that reallocation path is reached.Bounded harnesses.
check_retain_boundedrunsretainon every string of at most 5 arbitrary chars, with every pattern of predicate answers: the answer to thei-th call is an arbitrary bool, fixed before the call. The predicate records the char of each call. Afterretainreturns, the harness checks that the predicate was called once per char, in the order of the chars, and that the string holds exactly the kept chars: first its length, then its bytes. Covers show five paths throughretain: its first loop returns (nothing removed); its second loop starts at offset 0 (the first char removed) and after a kept multi-byte char; a kept char is moved left by less than its length (ptr::copywith overlapping ranges); every char is removed.retain'sPanicGuard::droprunsdebug_assert!(str::from_utf8(..).is_ok()). Under CI's-Z loop-contracts, the loops of the real validator (run_utf8_validation) are replaced by their invariants, which do not keep the loop index on a char boundary, so the abstracted validator can returnErrfor valid UTF-8 with a multi-byte char and the assertion fails although the bytes are valid. The harness replacesfrom_utf8byfrom_utf8_kept, which returnsOkonly if the bytes are exactly the encodings of the kept chars (computed by the harness before the call) and panics otherwise. Those bytes are valid UTF-8, so the real validator returnsOkwhenever the stand-in does, and on any other input the stand-in fails the harness: it can add failures but not hide one.from_utf16*harnesses run on every sub-slice of an arbitrary byte array, so odd lengths and both odd and even start addresses are covered (for the little-endian variants, thealign_tofast path and the unaligned path; on the little-endian CI targets the big-endian variants have only the unaligned path). A reference model ofchar::decode_utf16(utf16_expected) gives the validity of the input and the UTF-8 length of the decoded string; the strict variants must returnOkexactly for valid input, and every returned string must have that length (unpaired surrogates and a trailing odd byte count as U+FFFD in the lossy variants).#[kani::unwind], with a comment on how it follows from the input size, and an insufficient bound fails the unwinding assertion.How the challenge's list of UB is checked
ptr::copyandptr::copy_nonoverlappingare checked for aligned, in-bounds source and destination (and non-overlap for the latter); pointer-to-reference casts are checked for alignment. This covers the unsafe code of these functions: theptr::copys ofinsert,insert_str,remove,retainandremove_matches, theget_uncheckedaccesses ofretainanddrain, the rawStringpointer inDrain, and theVec/RawVecreallocation andSplicecode underneath.kani::any()arrays, encoded chars).&mut self(self.vec.as_mut_ptr(),as_mut_vec()), or, inDrain::drop, from the*mut Stringthatdraincreates from&mut self;leakandinto_boxed_strhand out the owned buffer without writing to it.pop,remove,Drain::nextandretaindecode are asserted to equal the encodedchar, so an invalidcharwould fail the assertion. Reachingunreachable_unchecked(through theunwrap_uncheckedof the decoded char inretain) is reported by Kani'sunreachable codecheck. The CI configuration does not check the validity of other intermediate values.Why
retainandfrom_utf16*are boundedAn unbounded proof of these functions needs a loop contract on every loop whose trip count grows with the input. At the pinned Kani none of these loops can carry one:
retain(two loops: a fast path overchar_indices, then a slow path over the unread tail).get_unchecked(read..len).chars().next().unwrap_unchecked(), so its invariant must say that the unread tail is still valid UTF-8 after the earlier moves: a quantifier over a symbolic range. CI's SAT backend does not support such quantifiers (Kani reports an error instead of dropping them, Fail verification when the solver backend drops quantifiers kani#4719). With the z3 backend (#[kani::solver(z3)], z3 5.1.0 locally) a small model of this loop verifies, but on the verbatim loop (even with ASCII-only input) z3 did not finish within 10 minutes, the per-harness limit of the macOS autoharness job.CharIndicesiterator, whose state is reachable only through method calls, and calls inside loop invariants become nondeterministic (#[kani::loop_invariant]with a method call lowers the call with "not enough arguments", substituting a non-deterministic value kani#4796).loop_modifies, which was not possible (Loop contracts: explicitloop_modifiesrejects writes to temporaries declared in the loop body (Check that var_N is assignable) kani#4906, fixed by Keep loop attributes on rewritten for loops and add loop-only locals to loop_modifies kani#4947, in review).from_utf16*.from_utf16leandfrom_utf16beloop withfor c in char::decode_utf16(..)infrom_utf16_units; Kani's loop contracts needKaniIntoIter, which this iterator does not implement (Loop contracts:forloops over iterators without aKaniIntoIterimpl (e.g.char::DecodeUtf16) cannot carry a contract kani#4907).Iterator::fold(throughcollect::<String>()), where a loop contract has no attach site (Loop contracts have no attach site for loops inside called iterator combinators (fold/try_for_each/collect) kani#4893).Stringand may reallocate, but CBMC's contract instrumentation, which Kani uses for loop contracts, does not allow allocation inside a loop with a contract: a minimal loop whose body isBox::new(i)and its drop fails exactly "dynamic allocation is allowed" under a loop contract (Supportmallocandfreein loops with loop contracts kani#3587). So a fix for Loop contracts:forloops over iterators without aKaniIntoIterimpl (e.g.char::DecodeUtf16) cannot carry a contract kani#4907 alone would not make these proofs go through.On #537 (Challenge 20), @feliperodri asked whether the scan loops could carry loop contracts, and otherwise: "If that is not yet feasible, please document this as an explicit, accepted limitation." This PR does that for these five functions. We are happy to replace the bounded harnesses with unbounded ones once Kani supports these patterns.
Verification
CI command.
scripts/run-kani.sh(kani verify-std ... --cbmc-args --object-bits 12, with the script's-Zflags). All 42 harnesses were run together in one container with 3 vCPUs (and 12 GiB of memory) and--jobs=3 --output-format=terse --harness-timeout 10m --default-unwind 1000, the settings of the macOS autoharness job, and all pass:check_drain_dropcheck_drain_nextcheck_drain_panics_both_inside_charstring.rs:2029check_drain_panics_end_included_maxcore/slice/index.rs:50check_drain_panics_end_inside_charstring.rs:2030check_drain_panics_past_endcore/slice/index.rs:50check_drain_panics_reversedcore/slice/index.rs:41,core/slice/index.rs:59check_drain_panics_start_excluded_maxcore/slice/index.rs:41check_drain_panics_start_inside_charstring.rs:2029check_from_utf16be_boundedcheck_from_utf16be_lossy_boundedcheck_from_utf16le_boundedcheck_from_utf16le_lossy_boundedcheck_insertcheck_insert_panics_inside_charstring.rs:1769check_insert_panics_past_endstring.rs:1769check_insert_strcheck_insert_str_panics_inside_charstring.rs:1826check_insert_str_panics_past_endstring.rs:1826check_into_boxed_strcheck_leakcheck_popcheck_range_rejects_end_included_maxcheck_range_rejects_reversedcheck_range_rejects_start_excluded_maxcheck_removecheck_remove_matchescheck_remove_panics_at_endstring.rs:1569check_remove_panics_inside_charcore/str/traits.rs:530check_remove_panics_past_endcore/str/traits.rs:530check_replace_rangecheck_replace_range_panics_both_inside_charstring.rs:2125check_replace_range_panics_end_included_maxcore/slice/index.rs:50check_replace_range_panics_end_inside_charstring.rs:2129check_replace_range_panics_past_endcore/slice/index.rs:50check_replace_range_panics_reversedcore/slice/index.rs:59,core/slice/index.rs:41check_replace_range_panics_start_excluded_maxcore/slice/index.rs:41check_replace_range_panics_start_inside_charstring.rs:2125check_retain_boundedcheck_split_offcheck_split_off_panics_inside_charstring.rs:1955check_split_off_panics_past_endstring.rs:1955Times are Kani's "Verification Time" with three harnesses in flight on 3 vCPUs (head 89ab0d4). They vary between runs: in five full runs, the slowest harness (
check_remove_matches) took 191 to 458 s, under the 10-minute limit. The largest single process peaked at 4.5 to 4.6 GiB across these runs (check_from_utf16le_lossy_bounded, 4.5 GiB when run alone).kani::coverof the harness was reported satisfied. The 23 panic harnesses reportSUCCESSFUL (encountered one or more panics as expected), and in each of them the only failing check is the documented one (listed in the table); by the construction of its domain, every input of the harness fails that check. In the two reversed-range harnesses,slice::rangereports a start that is also past the length with the message for a start out of range, so their logs show two lines ofslice_index_failfor the one check.kani::covers on its inputs (long strings, interior indices, a full buffer so thatinsert,insert_strandreplace_rangereallocate, grow and shrink cases, eachBoundvariant, aligned, unaligned and odd-length UTF-16 input, a surrogate pair and an unpaired surrogate, the reallocation of the matchVecinremove_matches, the five paths throughretainlisted above). All are satisfied, so no harness is vacuous.Mutation checks (local only, not part of this PR). Each change below was made to the library code, and the harness was run with the CI flags:
retaindoes not move kept chars after a removalcheck_retain_boundedfrom_utf8_kept)retainkeeps the last char without calling the predicatecheck_retain_boundedfrom_utf8_kept)retainsets the length one byte shortcheck_retain_boundedfrom_utf16_unitsaccepts unpaired surrogates (U+FFFD instead ofErr)check_from_utf16le_bounded,check_from_utf16be_boundedOkonly for valid input)from_utf16_unitsdrops NUL charscheck_from_utf16le_bounded,check_from_utf16be_boundedcheck_from_utf16le_lossy_bounded,check_from_utf16be_lossy_boundedremoveslices withget_unchecked(no boundary check)check_remove_panics_inside_char,check_remove_panics_past_enddrainwithout its start boundary checkcheck_drain_panics_start_inside_charcheck_drain_panics_end_inside_charreplace_rangewithout its end boundary checkcheck_replace_range_panics_end_inside_charSpliceloops become reachable)check_replace_range_panics_start_inside_charslice::rangeaccepts reversed rangescheck_drain_panics_reversedcheck_replace_range_panics_reversedVec::drainand the start boundary checkcheck_range_rejects_reversedtry_rangereturnsSome)slice::rangewithout the..=usize::MAX/Excluded(usize::MAX)checks*_maxharnessesattempt to add with overflowinslice::rangecheck_range_rejects_end_included_max,check_range_rejects_start_excluded_maxattempt to add with overflowintry_into_slice_range)The
should_panicrows for the last two changes show its limit: Kani accepts any panic, including an arithmetic overflow, so these harnesses alone cannot tell the documented panic from an overflow. The threecheck_range_rejects_*harnesses close that gap: they are ordinary harnesses, so an overflow fails them, and each of them fails on its change. On unmodifiedmain, the logs also show the documented check in every panic harness.Questions for the committee
retainand the fourfrom_utf16*functions, or should these five stay open until Kani supports the needed loop contracts?replace_rangeandremove_matches. We read the criterion as the length of the receiver, which is arbitrary here; the replaced range and the replacement are at most 4 bytes each, and the searcher returns at most 5 matches. Is that the intended reading?Notes for reviewers
#[cfg(kani)](amod verifyappended tolibrary/alloc/src/string.rs) and changes no production lines.loop_modifiesrejects writes to temporaries declared in the loop body (Check that var_N is assignable) kani#4906, Loop contracts:forloops over iterators without aKaniIntoIterimpl (e.g.char::DecodeUtf16) cannot carry a contract kani#4907, Support stubs and function contracts on trait functions with their own generic parameters (e.g.<String as FromIterator<char>>::from_iter) kani#4908, Quantifier predicates with&&,||orifkeep only one branch, so a trueforall!can fail andassume(forall!(..))can make a harness vacuous kani#5004,stub_verifiedpanics the compiler when amodifiestarget's type does not implementkani::Arbitrarykani#5005, Quantifier whose body is a statement expression: a short-circuit path that skips the final expression is dropped, so false__CPROVER_exists/__CPROVER_forallassertions verify diffblue/cbmc#9189 and--z3/--cvc5:map::atexception (exit status 6) while converting a goto program that contains Rust'score::fmt::Formatterdiffblue/cbmc#9190, and analysis added to#[kani::loop_invariant]with a method call lowers the call with "not enough arguments", substituting a non-deterministic value kani#4796. Related Kani fixes: Fail verification when CBMC exits abnormally after reporting results kani#4910 and Only use CBMC results that are followed by CBMC's overall status kani#4928 (merged), Keep loop attributes on rewritten for loops and add loop-only locals to loop_modifies kani#4947 (in review).By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.