Repository navigation
Open
Labels
Z-AutoharnessIssue related to autoharness subcommandIssue related to autoharness subcommand[C] BugThis is a bug. Something isn't working.This is a bug. Something isn't working.[F] Spurious FailureIssues that cause Kani verification to fail despite the code being correct.Issues that cause Kani verification to fail despite the code being correct.
Milestone
Description
Under verify-std, the &str values that autoharness generates with --bounded-arguments (via any_str_ref) are not always valid UTF-8, so harnesses for functions taking &str can fail on inputs that no real caller could pass.
Mechanism
any_str_ref(added in Autoharness: support slice and string arguments (bounded) #4691) builds the string as the longest valid UTF-8 prefix of nondeterministic bytes, usingcore::str::from_utf8andUtf8Error::valid_up_to.- In verify-rust-std,
run_utf8_validationcarriesloop_invariants (added in Add loop contracts and harness forrun_utf8_validationverify-rust-std#159). These invariants only bound the loop index; they say nothing about the bytes being valid UTF-8. kani autoharnessalways enables loop contracts (enable_autoharnessinkani-driver/src/autoharness/mod.rs), so under verify-std the validator loops are replaced by their invariants.- As a result,
from_utf8/valid_up_tocan return imprecise results there:Okfor invalid bytes,Errfor valid bytes, or avalid_up_tothat falls inside a character.any_str_refthen hands an invalid&strto the function under test.
Minimal reproduction
The function below checks UTF-8 validity byte by byte (RFC 3629), without calling from_utf8 or anything in str::validations.
Reproduction
fn kani_repro_assert_valid_utf8(s: &str) {
let b = s.as_bytes();
let n = b.len();
crate::kani::cover(n > 0 && b[0] >= 0x80, "non-ASCII first byte");
crate::kani::cover(n >= 2, "at least two bytes");
let mut i: usize = 0;
while i < n {
let x = b[i];
// (sequence width, allowed range of the second byte)
let (w, lo, hi): (usize, u8, u8) = if x < 0x80 {
(1, 0x80, 0xBF)
} else if x >= 0xC2 && x <= 0xDF {
(2, 0x80, 0xBF)
} else if x == 0xE0 {
(3, 0xA0, 0xBF)
} else if x == 0xED {
(3, 0x80, 0x9F)
} else if (x >= 0xE1 && x <= 0xEC) || x == 0xEE || x == 0xEF {
(3, 0x80, 0xBF)
} else if x == 0xF0 {
(4, 0x90, 0xBF)
} else if x >= 0xF1 && x <= 0xF3 {
(4, 0x80, 0xBF)
} else if x == 0xF4 {
(4, 0x80, 0x8F)
} else {
(0, 0, 0)
};
assert!(w != 0, "repro: invalid lead byte");
assert!(i + w <= n, "repro: truncated sequence");
if w >= 2 {
assert!(b[i + 1] >= lo && b[i + 1] <= hi, "repro: bad second byte");
}
let mut k: usize = 2;
while k < w {
assert!(b[i + k] >= 0x80 && b[i + k] <= 0xBF, "repro: bad continuation byte");
k += 1;
}
i += w;
}
}- verify-std: append the function to
library/core/src/str/validations.rsunder#[cfg(kani)](plus#[allow(dead_code)]), then from the verify-rust-std root run:
kani autoharness -Z autoharness -Z unstable-options -Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing --std ./library --bounded-arguments --include-pattern kani_repro_assert_valid_utf8 --harness kani_repro
(I used a release build of Kani here: with a debug build, --harness in this setup trips a debug assertion in determine_targets, which is unrelated to this issue.)
- Regular crate: put the same function in a library crate as
pub fn, replacingcrate::kani::cover(withkani::cover!(, and run:
cargo kani autoharness -Z autoharness --bounded-arguments
Results
Default string bound of 4 in both cases:
- verify-std: FAILED, 4 of 832 checks failed. All four assertions fail (invalid lead byte, truncated sequence, bad second byte, bad continuation byte). Both cover properties are satisfied.
- Regular crate: SUCCESS, 0 of 204 checks failed. Both cover properties are satisfied, so multi-byte inputs are reached.
Counterexamples
Taken from CBMC traces (--keep-temps, then cbmc --property <id> --trace) under the same verify-std setup:
- Bytes
57 ED ED ED(invalid:EDmust be followed by80..=9F), from the autoharness for<char as FromStr>::from_str.from_utf8returnedOk, so the generated&stris all four bytes. Decoding later yields code point0xDB6D, and thefrom_u32_uncheckedpreconditionchar_try_from_u32(i).is_ok()fails. - Bytes
CD 8F CD CD(the valid prefix is 2 bytes), from the same harness.from_utf8returnedErrwithvalid_up_to() == 1, so the generated&stris the lone lead byteCD.next_code_pointthen callsunwrap_uncheckedonNone, and therequires(false)onhint::unreachable_uncheckedfails. - The opposite direction also occurs: in a harness for the searcher models I'm adding for Autoharness: support the
MultiCharEqsearcher types incore::str::pattern#4811 (which build their haystack withany_str_ref), the valid sequenceF2 8F BF BF(U+8FFFF) gotvalid_up_to() == 3.
Impact
- Under verify-std, autoharness harnesses with
&strarguments can report spurious failures, typically on the preconditions ofchar::from_u32_uncheckedandhint::unreachable_uncheckedreached while decoding. - Whether this can also hide real failures (for example, if some valid strings can no longer be generated) is not confirmed. I haven't checked that.
- My guess is that CI has not hit this because the verify-rust-std workflows pass
--no-assert-contracts, so these preconditions are not checked there and invalid strings go unnoticed. I haven't verified this.
Related
- Autoharness: support slice and string arguments (bounded) #4691: introduced
any_str_refand the longest-valid-prefix approach. - Add loop contracts and harness for
run_utf8_validationverify-rust-std#159: added the loop contracts torun_utf8_validation. - Verify safety of char-related Searcher methods (Challenge 20) verify-rust-std#537: notes that under
-Z loop-contractsthefrom_utf8result "cannot soundly filter symbolic bytes", and builds haystacks constructively instead. - Challenge 10: Verify memory safety of
Stringfunctions with Kani verify-rust-std#702: notes that the abstracted validator can returnErrfor valid multi-byte UTF-8, and stubsfrom_utf8in aretainharness.
Possible directions
Just ideas, happy to follow whatever you prefer:
- Compute the longest valid UTF-8 prefix inside
kani_coreitself, so that it does not go throughstr::validationsand is not affected by its loop contracts. - Build the string constructively from nondeterministic
chars viaencode_utf8, as verify-rust-std#537 does. - Either way,
String'sBoundedArbitraryimplementation uses the same prefix approach and is probably affected too.
Versions
- Kani 0.68.0, built from a local branch based on main at 9461e07. The branch does not modify
any_str_reforString'sBoundedArbitrary; it only adds new models that callany_str_ref. - CBMC 6.11.0.
- Rust toolchain nightly-2026-10-12.
- verify-rust-std at bcb233071e1 (
sync-2026-10-11). - aarch64 Linux (Ubuntu 26.04).
Metadata
Metadata
Assignees
Labels
Z-AutoharnessIssue related to autoharness subcommandIssue related to autoharness subcommand[C] BugThis is a bug. Something isn't working.This is a bug. Something isn't working.[F] Spurious FailureIssues that cause Kani verification to fail despite the code being correct.Issues that cause Kani verification to fail despite the code being correct.
Activity
Autoharness: support MultiCharEq searcher arguments under --bounded-a…