Provenance, Aliasing, and UB


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

  1. Test provenance loss with integer round-trip under Miri
  2. Create aliasing UB and verify Miri catches it
  3. Implement Box using NonNull with proper ownership
  4. Write a function that safely performs pointer arithmetic

Next: Chapter 5 — Option Layout and Niche Optimization 🎯