Skip to content

Loop invariants and harnesses for memchr functions #429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e10a8f6
add loop-invariants and harnesses
thanhnguyen-aws Jul 28, 2025
8f02ac9
format fix
thanhnguyen-aws Jul 28, 2025
617804e
fix harness name
thanhnguyen-aws Jul 28, 2025
dcf1882
add crate::kani
thanhnguyen-aws Jul 29, 2025
5ccddc7
fix format
thanhnguyen-aws Jul 29, 2025
4844377
fix format
thanhnguyen-aws Jul 29, 2025
ce54852
fix invariant
thanhnguyen-aws Jul 29, 2025
9bd7cc2
fix invariant
thanhnguyen-aws Jul 29, 2025
da0761a
add quantifiers to loop invariant
thanhnguyen-aws Aug 1, 2025
67098c5
add im port kani
thanhnguyen-aws Aug 4, 2025
abda27a
add cfg kani
thanhnguyen-aws Aug 4, 2025
55e641f
Merge branch 'main' into memchrinvariant
thanhnguyen-aws Aug 11, 2025
864098f
add -Z quantifiers
thanhnguyen-aws Aug 15, 2025
98e10c4
Merge branch 'memchrinvariant' of https://github.com/thanhnguyen-aws/…
thanhnguyen-aws Aug 15, 2025
ab64f85
Make forall compile
tautschnig Aug 15, 2025
b701f3a
Fix formatting, make flux happy
tautschnig Aug 15, 2025
e5515d6
change unwind to solver(cvc5)
thanhnguyen-aws Aug 18, 2025
b4a38b9
Merge branch 'main' into memchrinvariant
thanhnguyen-aws Aug 18, 2025
a96b095
change unwind to solver(cvc5)
thanhnguyen-aws Aug 18, 2025
fac264f
Merge branch 'memchrinvariant' of https://github.com/thanhnguyen-aws/…
thanhnguyen-aws Aug 18, 2025
e411051
add target_arch cfg
thanhnguyen-aws Aug 18, 2025
1780de6
Merge branch 'main' into memchrinvariant
thanhnguyen-aws Aug 19, 2025
c357e47
using quantifiers in CStr
thanhnguyen-aws Aug 19, 2025
be834b0
Merge branch 'memchrinvariant' of https://github.com/thanhnguyen-aws/…
thanhnguyen-aws Aug 19, 2025
b24e668
add import forall
thanhnguyen-aws Aug 19, 2025
797b38e
fix typo
thanhnguyen-aws Aug 19, 2025
7fda174
fix format
thanhnguyen-aws Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 17 additions & 19 deletions library/core/src/ffi/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use safety::{ensures, requires};
use crate::cmp::Ordering;
use crate::error::Error;
use crate::ffi::c_char;
#[cfg(kani)]
use crate::forall;
use crate::intrinsics::const_eval_select;
use crate::iter::FusedIterator;
#[cfg(kani)]
Expand Down Expand Up @@ -204,7 +206,9 @@ impl Invariant for &CStr {
let bytes: &[c_char] = &self.inner;
let len = bytes.len();

!bytes.is_empty() && bytes[len - 1] == 0 && !bytes[..len - 1].contains(&0)
!bytes.is_empty()
&& bytes[len - 1] == 0
&& forall!(|i in (0,len - 1)| unsafe {*bytes.as_ptr().wrapping_add(i)} != 0)
}
}

Expand Down Expand Up @@ -880,18 +884,17 @@ mod verify {
// Helper function
fn arbitrary_cstr(slice: &[u8]) -> &CStr {
// At a minimum, the slice has a null terminator to form a valid CStr.
let len = slice.len();
kani::assume(slice.len() > 0 && slice[slice.len() - 1] == 0);
let result = CStr::from_bytes_until_nul(&slice);
// Given the assumption, from_bytes_until_nul should never fail
assert!(result.is_ok());
let c_str = result.unwrap();
assert!(c_str.is_safe());
c_str
kani::assume(forall!(|i in (0,len-1)| unsafe {*slice.as_ptr().wrapping_add(i)} != 0));
let ptr = slice.as_ptr() as *const c_char;
unsafe { CStr::from_ptr(ptr) }
}

// pub const fn from_bytes_until_nul(bytes: &[u8]) -> Result<&CStr, FromBytesUntilNulError>
#[kani::proof]
#[kani::unwind(32)] // 7.3 seconds when 16; 33.1 seconds when 32
#[kani::solver(cvc5)]
#[cfg(not(all(target_arch = "x86_64", target_feature = "sse2")))]
fn check_from_bytes_until_nul() {
const MAX_SIZE: usize = 32;
let string: [u8; MAX_SIZE] = kani::any();
Expand Down Expand Up @@ -988,7 +991,8 @@ mod verify {

// pub const fn from_bytes_with_nul(bytes: &[u8]) -> Result<&Self, FromBytesWithNulError>
#[kani::proof]
#[kani::unwind(17)]
#[kani::solver(cvc5)]
#[cfg(not(all(target_arch = "x86_64", target_feature = "sse2")))]
fn check_from_bytes_with_nul() {
const MAX_SIZE: usize = 16;
let string: [u8; MAX_SIZE] = kani::any();
Expand All @@ -1002,22 +1006,16 @@ mod verify {

// pub const fn count_bytes(&self) -> usize
#[kani::proof]
#[kani::unwind(32)]
#[kani::solver(cvc5)]
#[cfg(not(all(target_arch = "x86_64", target_feature = "sse2")))]
fn check_count_bytes() {
const MAX_SIZE: usize = 32;
let mut bytes: [u8; MAX_SIZE] = kani::any();

// Non-deterministically generate a length within the valid range [0, MAX_SIZE]
let mut len: usize = kani::any_where(|&x| x < MAX_SIZE);

// If a null byte exists before the generated length
// adjust len to its position
if let Some(pos) = bytes[..len].iter().position(|&x| x == 0) {
len = pos;
} else {
// If no null byte, insert one at the chosen length
bytes[len] = 0;
}
kani::assume(forall!(|i in (0,len)| unsafe {*bytes.as_ptr().wrapping_add(i)} != 0));
bytes[len] = 0;

let c_str = CStr::from_bytes_until_nul(&bytes).unwrap();
// Verify that count_bytes matches the adjusted length
Expand Down
33 changes: 33 additions & 0 deletions library/core/src/slice/memchr.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Original implementation taken from rust-memchr.
// Copyright 2015 Andrew Gallant, bluss and Nicolas Koch

#[cfg(kani)]
use crate::forall;
use crate::intrinsics::const_eval_select;
#[cfg(kani)]
use crate::kani;

const LO_USIZE: usize = usize::repeat_u8(0x01);
const HI_USIZE: usize = usize::repeat_u8(0x80);
Expand Down Expand Up @@ -36,6 +40,7 @@ const fn memchr_naive(x: u8, text: &[u8]) -> Option<usize> {
let mut i = 0;

// FIXME(const-hack): Replace with `text.iter().pos(|c| *c == x)`.
#[cfg_attr(kani, kani::loop_invariant(i <= text.len() && forall!(|j in (0,i)| unsafe {*text.as_ptr().wrapping_add(j)} != x)))]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need to use an SMT solver for any function that eventually invokes this function.

while i < text.len() {
if text[i] == x {
return Some(i);
Expand Down Expand Up @@ -78,6 +83,8 @@ const fn memchr_aligned(x: u8, text: &[u8]) -> Option<usize> {

// search the body of the text
let repeated_x = usize::repeat_u8(x);
#[cfg_attr(kani, kani::loop_invariant(len >= 2 * USIZE_BYTES && offset <= len &&
forall!(|j in (0,offset)| unsafe {*text.as_ptr().wrapping_add(j)} != x)))]
while offset <= len - 2 * USIZE_BYTES {
// SAFETY: the while's predicate guarantees a distance of at least 2 * usize_bytes
// between the offset and the end of the slice.
Expand Down Expand Up @@ -139,6 +146,7 @@ pub fn memrchr(x: u8, text: &[u8]) -> Option<usize> {
let repeated_x = usize::repeat_u8(x);
let chunk_bytes = size_of::<Chunk>();

#[cfg_attr(kani, kani::loop_invariant(true))]
while offset > min_aligned_offset {
// SAFETY: offset starts at len - suffix.len(), as long as it is greater than
// min_aligned_offset (prefix.len()) the remaining distance is at least 2 * chunk_bytes.
Expand All @@ -159,3 +167,28 @@ pub fn memrchr(x: u8, text: &[u8]) -> Option<usize> {
// Find the byte before the point the body loop stopped.
text[..offset].iter().rposition(|elt| *elt == x)
}

#[cfg(kani)]
#[unstable(feature = "kani", issue = "none")]
pub mod verify {
use super::*;

#[kani::proof]
#[kani::solver(cvc5)]
#[cfg(not(all(target_arch = "x86_64", target_feature = "sse2")))]
pub fn check_memchr_naive() {
const ARR_SIZE: usize = 64;
let x: u8 = kani::any();
let text: [u8; ARR_SIZE] = kani::any();
let _result = memchr_naive(x, &text);
}

#[kani::proof]
#[cfg(not(all(target_arch = "x86_64", target_feature = "sse2")))]
pub fn check_memchr() {
const ARR_SIZE: usize = 64;
let x: u8 = kani::any();
let text: [u8; ARR_SIZE] = kani::any();
let _result = memrchr(x, &text);
}
}
2 changes: 1 addition & 1 deletion scripts/run-kani.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ REPO_URL=${KANI_REPO_URL:-$DEFAULT_REPO_URL}
BRANCH_NAME=${KANI_BRANCH_NAME:-$DEFAULT_BRANCH_NAME}

# Unstable arguments to pass to Kani
unstable_args="-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z stubbing"
unstable_args="-Z function-contracts -Z mem-predicates -Z float-lib -Z c-ffi -Z loop-contracts -Z quantifiers -Z stubbing"

# Variables used for parallel harness verification
# When we say "parallel," we mean two dimensions of parallelization:
Expand Down
Loading