Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
244: Miri for Detecting Undefined Behavior
I've been spending a lot of time lately optimizing some low-level buffer logic using unsafe. As you know, the compiler is great at protecting us in safe Rust, but the moment we drop into unsafe, we're essentially telling the compiler, "Trust me, I know what I'm doing." The problem is, sometimes I don't actually know what I'm doing, and the compiler is more than happy to let me walk right off a cliff.
When the compiler lets you get away with murder
I wrote this little snippet to try and access an element in an array using some raw pointer arithmetic. I wanted to see if I could manually offset a pointer to a specific index without using the standard indexing syntax. It looks innocent enough:
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr();
unsafe {
// I'm trying to get the 6th element (which doesn't exist)
let bad_ptr = ptr.add(5);
println!("Value at index 5: {}", *bad_ptr);
}
}
I ran this with cargo run. To my surprise, it didn't crash. It actually printed a value—some random garbage number from memory. Now, if I were just glancing at this, I might think, "Cool, it works." But in Rust, this is a disaster. We've just triggered Undefined Behavior (UB). The program didn't crash this time, but LLVM is now free to optimize this code in ways that could make the rest of my program behave like a haunted house.
Bringing in the interpreter
Since the standard compiler doesn't catch UB at runtime (it just assumes you're following the rules), I need something that actually tracks the state of memory. That's where Miri comes in. Miri isn't a compiler; it's an interpreter for Rust's Mid-level Intermediate Representation (MIR). It basically simulates your code execution on a "virtual" machine that keeps a strict ledger of every memory allocation and pointer move.
First, I have to install it, since it's not part of the standard toolchain:
rustup component add miri
Now, instead of cargo run, I'll run my code through Miri:
cargo miri run
Reading the crime scene report
Miri didn't just run the code; it screamed at me. The output was an absolute goldmine of information. It didn't just say "Error"; it told me exactly why the operation was illegal:
error: Undefined Behavior: memory access failed:
allocation 1 contains 5 elements, but 5 was accessed
This is the "aha!" moment. Miri knows that data is an array of 5 elements. When I called ptr.add(5), I created a pointer that points exactly one element past the end of the allocation. While creating a pointer to one-past-the-end is actually allowed in some contexts, dereferencing it is a strict violation. Miri caught the exact instruction where I crossed the line.
Verifying the fix
I'll adjust the code to stay within bounds and run it through Miri again. I want to make sure that my "fix" isn't just masking another problem.
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr();
unsafe {
// Now we stay within the 0-4 index range
let safe_ptr = ptr.add(4);
println!("Value at index 4: {}", *safe_ptr);
}
}
Running cargo miri run now yields a clean exit. No warnings, no errors. It's a strange feeling—I'm not just trusting my eyes or the fact that the program "doesn't crash"; I have a mathematical guarantee from the interpreter that I haven't violated the memory model in this specific execution path.
One thing to keep in mind: Miri is slow. Because it's interpreting every single operation and tracking every byte of memory, it's significantly slower than native execution. You don't run your whole test suite through it every time you save a file; you use it when you're writing unsafe blocks or when you have a bug that only appears in release mode (which is a classic sign of UB).
📋 Practical Task
Hunting Out-of-Bounds Pointers in a Circular Buffer
You have been handed a simplified implementation of a circular buffer that uses unsafe for performance. The original author claimed it was optimized, but it's behaving erratically in production. Your task is to use Miri to identify the Undefined Behavior and fix it.
The Buggy Code:
struct SimpleBuffer {
data: Vec<u8>,
head: usize,
}
impl SimpleBuffer {
fn new(size: usize) -> Self {
Self {
data: vec![0; size],
head: 0,
}
}
unsafe fn read_next(&mut self) -> u8 {
let ptr = self.data.as_ptr();
// BUG: The author forgot to wrap the index using modulo
let val = *ptr.add(self.head);
self.head += 1;
val
}
}
fn main() {
let mut buffer = SimpleBuffer::new(3);
unsafe {
println!("Read 1: {}", buffer.read_next());
println!("Read 2: {}", buffer.read_next());
println!("Read 3: {}", buffer.read_next());
// This fourth call should trigger UB
println!("Read 4: {}", buffer.read_next());
}
}
Your Instructions:
- Set up a new Rust project and paste the code above into
main.rs. - Run the program using
cargo run. Observe if it crashes or prints a value. - Install Miri via
rustup component add miri. - Run the program using
cargo miri runand analyze the error message to confirm exactly where the memory violation occurs. - Fix the
read_nextmethod so that theheadindex wraps around the buffer size, ensuring the pointer never exceeds the allocation bounds. - Verify the fix by running
cargo miri runagain until it completes without errors.
There are no comments for now.