title: “Pointer Provenance and Aliasing in Rust” meta_description: “Understanding provenance, aliasing rules, and undefined behavior with NonNull in Rust.” keywords: [“rust provenance”, “pointer aliasing”, “stacked borrows”, “undefined behavior rust”, “nonnull provenance”]
⚡ Reinventing from Scratch: NonNull<T>
Chapter 4 — “Provenance, Aliasing, and UB”
“A pointer is not just an address. It carries a permission slip called provenance.”
🧭 4.1. What is Provenance?
Provenance = The “history” and “permission” attached to a pointer.
let x = 42;
let ptr1 = &x as *const i32;
let ptr2 = &x as *const i32;
// Same address?
assert_eq!(ptr1, ptr2); // Yes!
// Same provenance?
// Yes! Both derived from &x
But:
let x = 42;
let y = 42;
let ptr_x = &x as *const i32;
let ptr_y = &y as *const i32;
// If they happen to have the same address (unlikely but possible):
assert_eq!(ptr_x, ptr_y); // Same address
// But different provenance!
// ptr_x has permission to access x
// ptr_y has permission to access y
🚫 4.2. When Provenance is Lost
Integer Round-Trip
let x = 42;
let ptr = &x as *const i32;
let addr = ptr as usize; // Convert to integer
// Later...
let ptr2 = addr as *const i32; // ❌ LOST PROVENANCE!
unsafe {
let val = *ptr2; // Might be UB!
}
Why? Converting to usize strips provenance. The compiler can assume that address is invalid.
Byte-Level Casting
let x: i32 = 42;
let ptr = &x as *const i32;
// Cast to bytes
let bytes_ptr = ptr as *const u8;
// Cast back
let ptr2 = bytes_ptr as *const i32; // Provenance preserved
// But this is sketchy:
let addr = bytes_ptr as usize;
let ptr3 = addr as *const i32; // ❌ Provenance lost!
👻 4.3. Aliasing and Undefined Behavior
Aliasing = Multiple pointers to the same memory.
Shared Aliasing (OK)
let x = 42;
let ptr1 = NonNull::from(&x);
let ptr2 = NonNull::from(&x);
unsafe {
let a = *ptr1.as_ptr(); // ✅ OK
let b = *ptr2.as_ptr(); // ✅ OK
}
OK because: Both are read-only!
Mutable Aliasing (UB!)
let mut x = 42;
let ptr1 = NonNull::from(&mut x);
// Create another pointer
let ptr2 = unsafe { NonNull::new_unchecked(ptr1.as_ptr()) };
unsafe {
*ptr1.as_ptr() = 100; // ❌ UB!
// Why? ptr2 exists, so aliasing &mut!
}
UB because: Can’t have two mutable aliases!
🎯 4.4. The Stacked Borrows Model
Rust uses Stacked Borrows to reason about aliasing:
let mut x = 42;
{
let r1 = &mut x; // Borrow 1: Exclusive
// x is "frozen" — can't access directly
*r1 = 100; // ✅ OK
// let r2 = &mut x; // ❌ Error: r1 still active!
}
// Now r1 is gone, can access x again
x = 200; // ✅ OK
With raw pointers:
let mut x = 42;
let ptr = &mut x as *mut i32;
unsafe {
*ptr = 100; // ✅ OK
}
x = 200; // ❌ UB! ptr might still be used
unsafe {
*ptr = 300; // This invalidates direct access to x
}
Rule: Once you use a raw pointer, treat it as owning that memory!
🔓 4.5. NonNull and Aliasing
NonNull doesn’t prevent aliasing:
let mut x = 42;
let nn1 = NonNull::from(&mut x);
// Create second NonNull
let nn2 = unsafe { NonNull::new_unchecked(nn1.as_ptr()) };
unsafe {
*nn1.as_ptr() = 100; // ❌ UB!
*nn2.as_ptr() = 200; // Aliasing mutable pointers!
}
Lesson: NonNull prevents null, not aliasing!
🧩 4.6. Safe Usage Patterns
Pattern 1: Exclusive Ownership
struct Box<T> {
ptr: NonNull<T>,
}
impl<T> Box<T> {
pub fn new(value: T) -> Self {
let layout = Layout::new::<T>();
let ptr = unsafe {
let p = alloc(layout) as *mut T;
let nn = NonNull::new_unchecked(p);
nn.as_ptr().write(value);
nn
};
Self { ptr }
}
}
// Only one Box can own ptr → no aliasing!
Pattern 2: Late Borrowing
struct MyVec<T> {
ptr: NonNull<T>,
len: usize,
}
impl<T> MyVec<T> {
pub fn get(&self, index: usize) -> Option<&T> {
if index >= self.len {
return None;
}
unsafe {
// Convert to reference only at the last moment
let ptr = self.ptr.as_ptr().add(index);
Some(&*ptr)
}
}
}
🧠 4.7. Chapter Takeaways
| Concept | Meaning |
|---|---|
| Provenance | Permission attached to pointer |
| Lost provenance | Converting through usize |
| Aliasing | Multiple pointers to same memory |
| Stacked Borrows | Rust’s aliasing model |
| Exclusive ownership | Prevents aliasing UB |
🚀 4.8. What’s Next
Next: Chapter 5 — Option Layout and Niche Optimization
Learn how Option<NonNull<T>> achieves zero-overhead representation!
💡 Exercises
- Test provenance loss with integer round-trip under Miri
- Create aliasing UB and verify Miri catches it
- Implement Box using NonNull with proper ownership
- Write a function that safely performs pointer arithmetic
Next: Chapter 5 — Option Layout and Niche Optimization 🎯