Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion library/core/src/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@

#![stable(feature = "rust1", since = "1.0.0")]

use safety::requires;

#[cfg(kani)]
use crate::kani;
use crate::marker::{Destruct, PointeeSized};

mod uninit;
Expand Down Expand Up @@ -544,8 +548,11 @@ unsafe impl CloneToUninit for str {
#[unstable(feature = "clone_to_uninit", issue = "126799")]
unsafe impl CloneToUninit for crate::ffi::CStr {
#[cfg_attr(debug_assertions, track_caller)]
// Safety contract: dest must be non-null, valid for size_of_val(self) writes,
// and properly aligned (u8 alignment is always satisfied for non-null pointers).
#[requires(!dest.is_null())]
unsafe fn clone_to_uninit(&self, dest: *mut u8) {
// SAFETY: For now, CStr is just a #[repr(trasnsparent)] [c_char] with some invariants.
// SAFETY: For now, CStr is just a #[repr(transparent)] [c_char] with some invariants.
// And we can cast [c_char] to [u8] on all supported platforms (see: to_bytes_with_nul).
Comment on lines 550 to 556
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

The new #[requires(!dest.is_null())] precondition is weaker than the documented safety requirements for CloneToUninit::clone_to_uninit (destination must be valid for size_of_val(self) writes and properly aligned). For consistency with other contracts in core (e.g. pointer APIs using ub_checks::can_write), consider strengthening this contract to reflect the full safety requirements, not just non-nullness.

Copilot uses AI. Check for mistakes.
// The pointer metadata properly preserves the length (so NUL is also copied).
// See: `cstr_metadata_is_length_with_nul` in tests.
Expand Down
56 changes: 56 additions & 0 deletions library/core/src/ffi/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,7 @@ impl FusedIterator for Bytes<'_> {}
#[unstable(feature = "kani", issue = "none")]
mod verify {
use super::*;
use crate::clone::CloneToUninit;

// Helper function
fn arbitrary_cstr(slice: &[u8]) -> &CStr {
Expand Down Expand Up @@ -1096,4 +1097,59 @@ mod verify {
assert_eq!(expected_is_empty, c_str.is_empty());
assert!(c_str.is_safe());
}

// ops::Index<ops::RangeFrom<usize>> for CStr
#[kani::proof]
#[kani::unwind(33)]
fn check_index_from() {
const MAX_SIZE: usize = 32;
let string: [u8; MAX_SIZE] = kani::any();
let slice = kani::slice::any_slice_of_array(&string);
let c_str = arbitrary_cstr(slice);

let bytes_with_nul = c_str.to_bytes_with_nul();
let idx: usize = kani::any();
kani::assume(idx < bytes_with_nul.len());

let indexed = &c_str[idx..];
assert!(indexed.is_safe());
// The indexed result should correspond to the tail of the original bytes
assert_eq!(indexed.to_bytes_with_nul(), &bytes_with_nul[idx..]);
}

// CloneToUninit for CStr
// MAX_SIZE is kept small to avoid CBMC timeout: the symbolic pointer
// arithmetic in clone_to_uninit is expensive; 8 bytes is sufficient to
// cover empty, single-char, and multi-char C strings.
#[kani::proof]
#[kani::unwind(9)]
fn check_clone_to_uninit() {
const MAX_SIZE: usize = 8;
let string: [u8; MAX_SIZE] = kani::any();
let slice = kani::slice::any_slice_of_array(&string);
let c_str = arbitrary_cstr(slice);

let len = c_str.to_bytes_with_nul().len();

// Use an initialized buffer to avoid UB from reading uninitialized
// memory if clone_to_uninit were buggy and failed to write all bytes.
let mut buf = [0u8; MAX_SIZE];
let dest = buf.as_mut_ptr();

Comment on lines +1132 to +1138
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

check_clone_to_uninit only checks that the cloned buffer parses as a valid CStr, but it doesn’t verify that clone_to_uninit copied the exact bytes (including the NUL terminator) from the source. This could pass even if the implementation writes a different (but still valid) C string. Consider asserting that the destination bytes match c_str.to_bytes_with_nul().

Copilot uses AI. Check for mistakes.
// Safety: dest is non-null (stack allocation), valid for len writes,
// properly aligned (u8 has alignment 1)
unsafe {
c_str.clone_to_uninit(dest);
}

// Verify the cloned bytes form a valid CStr
let cloned_slice = unsafe { core::slice::from_raw_parts(dest, len) };
let cloned_cstr = CStr::from_bytes_with_nul(cloned_slice);
assert!(cloned_cstr.is_ok());
let cloned = cloned_cstr.unwrap();
assert!(cloned.is_safe());

// Verify exact byte-for-byte match with source (including NUL terminator)
assert_eq!(cloned.to_bytes_with_nul(), c_str.to_bytes_with_nul());
}
}
Loading