Bounds Checking Deep Dive: Code Companion¶
Reference code for the Bounds Checking Deep Dive lecture. Sections correspond to the lecture document.
Section 1: The Vulnerability Rust Eliminates¶
#include <string.h>
void vulnerable_copy(char* dest, const char* src) {
strcpy(dest, src); // NO BOUNDS CHECKING!
// strcpy copies until null terminator, ignoring dest size
}
int main() {
char buffer[10]; // Only 10 bytes allocated on stack
char* long_string = "This is a very long string that will overflow";
// This string is 46 characters - 36 bytes overflow!
vulnerable_copy(buffer, long_string); // BUFFER OVERFLOW!
// Those extra 36 bytes overwrite the stack frame,
// potentially corrupting the return address
return 0;
}
This C code represents the class of vulnerability that Rust eliminates by design. The strcpy function has no way to know the destination buffer's size—it's just a raw pointer with no length information.
Section 2: Rust's Safe Copy Pattern¶
/// Rust's safe equivalent to strcpy
/// Note: slices are "fat pointers" containing (ptr, length)
pub fn safe_copy(dest: &mut [u8], src: &[u8]) -> Result<(), &'static str> {
// Length information travels WITH the slice - can't be forgotten
if dest.len() < src.len() {
return Err("Destination buffer too small");
}
// dest[..src.len()] creates a sub-slice of exact size needed
// copy_from_slice performs its own bounds verification
dest[..src.len()].copy_from_slice(src);
Ok(()) // Result type forces caller to handle potential failure
}
The signature &mut [u8] and &[u8] are slices—fat pointers that carry their length. The Result return type makes error handling explicit and non-ignorable.
Section 3: The Two Faces of Indexed Access¶
pub fn safe_array_access() {
let array = [1, 2, 3, 4, 5];
// Direct indexing: bounds-checked at runtime, panics if invalid
let _first = array[0]; // OK - returns i32 directly
// This would panic with a clear error message:
// let _invalid = array[10]; // thread 'main' panicked at 'index out of bounds'
// Safe alternative: get() returns Option<&T>
match array.get(10) {
Some(value) => println!("Value: {}", value), // Never reached here
None => println!("Index out of bounds (safely handled)"),
}
}
| Access Method | Return Type | Out-of-Bounds Behavior | Use When |
|---|---|---|---|
array[i] |
T |
Panics immediately | Bug if index invalid |
array.get(i) |
Option<&T> |
Returns None |
Invalid index is expected |
Section 4: Vectors and Dynamic Safety¶
pub fn safe_vector_usage() {
let mut vec = Vec::new(); // Dynamically-sized, heap-allocated
vec.push(1);
vec.push(2);
vec.push(3);
// Iterator-based access: bounds checking happens once at creation
// Cannot iterate past the end - iterator knows exact length
for item in &vec {
println!("{}", item);
}
// get() works identically to arrays - same Option<&T> pattern
if let Some(value) = vec.get(5) {
println!("Value at index 5: {}", value);
} else {
println!("Index 5 doesn't exist"); // This branch executes
}
}
The iterator &vec yields exactly vec.len() references. The if let syntax is a concise way to handle the Some case when you don't need the None branch to do anything special.
Section 5: Strings as Growable Buffers¶
pub fn safe_string_operations() {
// Initial capacity is just a hint - not a hard limit
let mut dest = String::with_capacity(10);
let src = "This is a very long string that would overflow a fixed buffer";
// In C: catastrophic overflow. In Rust: automatic reallocation
dest.push_str(src); // String grows to accommodate 61 characters
println!("String length: {} (automatically managed)", dest.len());
// Prints: String length: 61 (automatically managed)
}
String::with_capacity(10) pre-allocates 10 bytes but imposes no limit. When push_str needs more space, String automatically reallocates to a larger buffer.
Section 6: Compile-Time Size Enforcement¶
pub fn demonstration_bounds_checking() {
let buffer: [u8; 10] = [0; 10]; // Fixed-size array: 10 bytes
let data: [u8; 20] = [1; 20]; // Fixed-size array: 20 bytes
// This would NOT compile - size mismatch caught at compile time:
// buffer.copy_from_slice(&data);
// error: source slice length (20) does not match destination slice length (10)
// Safe alternative: explicitly slice to matching size
let safe_copy = &data[..buffer.len()]; // Take first 10 bytes only
let mut mutable_buffer = buffer;
mutable_buffer.copy_from_slice(safe_copy); // Now sizes match
println!("Safe copy completed: {:?}", mutable_buffer);
// Prints: Safe copy completed: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
When array sizes are known at compile time, the compiler catches mismatches before the code ever runs. The slice expression &data[..buffer.len()] creates a view of exactly 10 elements.
Section 7: Testing Bounds Behavior¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_safe_copy_success() {
let mut dest = [0u8; 10];
let src = [1, 2, 3, 4, 5];
assert!(safe_copy(&mut dest, &src).is_ok());
assert_eq!(&dest[..5], &src); // First 5 bytes copied
}
#[test]
fn test_safe_copy_overflow_prevented() {
let mut dest = [0u8; 5];
let src = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Overflow attempt returns Err, buffer unchanged
assert!(safe_copy(&mut dest, &src).is_err());
}
#[test]
#[should_panic] // Test expects a panic - controlled failure
#[allow(unconditional_panic, clippy::out_of_bounds_indexing)]
fn test_out_of_bounds_panic() {
let array = [1, 2, 3];
let _ = array[10]; // Panics - deterministic, debuggable failure
}
#[test]
fn test_safe_get() {
let array = [1, 2, 3];
assert_eq!(array.get(1), Some(&2)); // Valid index
assert_eq!(array.get(10), None); // Invalid index - no panic
}
}
The #[should_panic] attribute lets you verify that dangerous operations fail predictably. Unlike C's undefined behavior, Rust's panics are consistent and testable.
Quick Reference¶
Slice Types¶
| Type | Description | Length Known |
|---|---|---|
[T; N] |
Fixed-size array | Compile time |
&[T] |
Immutable slice (fat pointer) | Runtime |
&mut [T] |
Mutable slice (fat pointer) | Runtime |
Vec<T> |
Growable vector | Runtime |
String |
Growable UTF-8 string | Runtime |
Access Methods¶
// Direct indexing - panics on out-of-bounds
let value: T = collection[index];
// Safe access - returns Option
let maybe_value: Option<&T> = collection.get(index);
// Slice creation - panics if range invalid
let sub: &[T] = &collection[start..end];
// Safe slice - returns Option
let maybe_sub: Option<&[T]> = collection.get(start..end);
Key Copy Operations¶
// Requires exact size match - panics otherwise
dest.copy_from_slice(src);
// Clone elements (requires T: Clone)
dest.clone_from_slice(src);
// Safe copying via slicing
dest[..src.len()].copy_from_slice(src);
C vs Rust Comparison¶
| C Pattern | Rust Equivalent | Safety Mechanism |
|---|---|---|
strcpy(dest, src) |
dest.copy_from_slice(src) |
Size match required |
array[i] |
array.get(i) |
Returns Option<&T> |
malloc + manual tracking |
Vec::new() |
Length stored with data |
char buffer[N] |
String |
Automatic growth |