Soundness and Safety Invariants
Soundness is the property that no safe Rust code can ever cause undefined behavior when interacting with a type, function, or module through its public safe API—regardless of how adversarial, unusual, or panicky that safe caller is. If any sequence of safe function calls can trigger undefined behavior inside a library's internal unsafe blocks, that library is unsound.
Validity Invariants vs. Safety Invariants
Rust distinguishes two levels of invariants on types:
+-----------------------------------------------------------------------+
| All Bit Patterns of Size N |
| +-----------------------------------------------------------------+ |
| | Validity Invariant (Compiler / Language Requirement) | |
| | - Violating this at ANY instant is immediate UB | |
| | - e.g. `&[u8]` inside `str` must be non-null & aligned | |
| | +-----------------------------------------------------------+ | |
| | | Safety Invariant (Library / Type Contract) | | |
| | | - Unsafe code in the library relies on this holding | | |
| | | - e.g. bytes inside `str` / `String` must be valid UTF-8 | | |
| | +-----------------------------------------------------------+ | |
| +-----------------------------------------------------------------+ |
+-----------------------------------------------------------------------+
| Invariant Kind | Enforced By | Consequence of Violation | Example (String) |
|---|---|---|---|
| Validity Invariant | Language / LLVM abstract machine | Immediate UB the moment the invalid value is constructed | The internal NonNull<u8> pointer is non-null, and len / cap are initialized usize integers |
| Safety Invariant | Encapsulating module's API boundary | Soundness bug (causes UB later when another unsafe block trusts the invariant) |
ptr points to a heap allocation of capacity cap with 0..len initialized as valid UTF-8 bytes |
How Safe Code Can Break a Broken Safety Invariant (Unsoundness Example)
Suppose a custom AsciiString type relies on the safety invariant that its internal Vec<u8> contains only ASCII bytes (0x00..=0x7F), and uses an unsafe block in .as_str() calling str::from_utf8_unchecked(&self.bytes).
If AsciiString accidentally makes pub bytes: Vec<u8> public (or implements DerefMut<Target = Vec<u8>>), safe callers can push 0xFF into bytes without writing unsafe, and then call .as_str() to trigger undefined behavior!
pub struct AsciiString {
// Private field! External safe code cannot push non-ASCII bytes.
// Safety invariant: every byte in `bytes` satisfies `b <= 0x7F`.
bytes: Vec<u8>,
}
impl AsciiString {
pub fn try_new(input: Vec<u8>) -> Result<Self, Vec<u8>> {
if input.is_ascii() {
Ok(Self { bytes: input })
} else {
Err(input)
}
}
pub fn push_ascii(&mut self, byte: u8) -> bool {
if byte.is_ascii() {
self.bytes.push(byte);
true
} else {
false
}
}
pub fn as_str(&self) -> &str {
// SAFETY: Our module invariant guarantees that `self.bytes` only ever
// contains valid ASCII (`0x00..=0x7F`), and all ASCII is valid UTF-8.
unsafe { std::str::from_utf8_unchecked(&self.bytes) }
}
}
fn main() {
let mut s = AsciiString::try_new(b"Rust".to_vec()).unwrap();
assert!(s.push_ascii(b'!'));
assert!(!s.push_ascii(0xFF)); // Rejected; invariant preserved!
println!("sound AsciiString: {:?}", s.as_str());
}
sound AsciiString: "Rust!"
Never Trust Safe Traits for Soundness (Ord, Eq, Hash, Deref)
An unsafe block must never rely on a safe trait implementation (Ord, PartialEq, Hash, Clone, or Deref) returning consistent results for memory safety! Because any safe user can implement Ord with a buggy comparator that returns Ordering::Less on the first call and Ordering::Greater on the second call for the same inputs, an unsafe sorting algorithm that skips bounds checks assuming Ord is a strict total order is unsound. Only unsafe traits can be trusted for memory safety.