Invariants and Safety


title: “NonNull Invariants and Safety Guarantees in Rust” meta_description: “Understanding NonNull’s safety guarantees, invariants, and what it does NOT promise in Rust unsafe code.” keywords: [“rust nonnull safety”, “nonnull invariants”, “pointer safety rust”, “nonnull guarantees”]

🛡️ Reinventing from Scratch: NonNull<T>

Chapter 2 — “Invariants and Safety”

“NonNull promises one thing and one thing only: this pointer is not null. Everything else is on you.”


🧭 2.1. The Single Promise

NonNull<T> has exactly one invariant:

// INVARIANT: The wrapped pointer is never null
let ptr: NonNull<T> = ...;
assert!(!ptr.as_ptr().is_null());  // Always true

That’s it. That’s the whole promise.


❌ 2.2. What NonNull Does NOT Guarantee

❌ NOT: The Pointer is Valid

let ptr = NonNull::dangling();  // Valid NonNull!
// But reading from it → UB!

❌ NOT: Memory is Initialized

let layout = Layout::new::<i32>();
let ptr = unsafe {
    let raw = alloc(layout) as *mut i32;
    NonNull::new_unchecked(raw)
};

// ptr is non-null, but memory is uninitialized!
unsafe {
    let x = *ptr.as_ptr();  // ❌ UB! Reading uninit memory
}

❌ NOT: Pointer is Aligned

let bytes = [0u8; 8];
let ptr = NonNull::new(bytes.as_ptr() as *mut u64).unwrap();

// Non-null? Yes!
// Aligned for u64? Maybe not! (depends on byte array alignment)
unsafe {
    let x = *ptr.as_ptr();  // Might be UB if misaligned!
}

❌ NOT: You Have Permission to Dereference

fn foo(ptr: NonNull<i32>) {
    // Just because you have a NonNull doesn't mean you can read it!
    // Need to check lifetime/provenance
    unsafe {
        let x = *ptr.as_ptr();  // Might be UB!
    }
}

✅ 2.3. What NonNull DOES Give You

✅ Optimizer Hints

fn process(ptr: Option<NonNull<T>>) {
    match ptr {
        Some(p) => { /* use p */ }
        None => { /* handle null */ }
    }
}

// Compiler knows: if Some, pointer is definitely non-null
// Can optimize null-checks away

✅ Niche Optimization

assert_eq!(
    size_of::<Option<NonNull<u8>>>(),
    size_of::<*mut u8>()
);

// Option<NonNull<T>> is same size as a raw pointer!
// The None case uses the null bit pattern

✅ Better API Design

// Before: Unclear if null is allowed
fn process(ptr: *mut T) { }

// After: Clear that null is forbidden
fn process(ptr: NonNull<T>) { }

🎯 2.4. The Safety Contract

When you create a NonNull<T>, you promise:

pub fn new(ptr: *mut T) -> Option<NonNull<T>> {
    // Checks if null, returns None if so
    if ptr.is_null() {
        None
    } else {
        Some(unsafe { NonNull::new_unchecked(ptr) })
    }
}

pub unsafe fn new_unchecked(ptr: *mut T) -> NonNull<T> {
    // SAFETY: Caller must ensure ptr is not null
    NonNull { pointer: ptr }
}

Your responsibility:

  • Ensure pointer came from valid allocation
  • Ensure alignment is correct
  • Ensure you have permission to access the memory

NonNull’s responsibility:

  • Ensure pointer is not null

🧩 2.5. Using NonNull Safely

Pattern 1: After Allocation

let layout = Layout::new::<i32>();
let ptr = unsafe { alloc(layout) as *mut i32 };

let ptr = NonNull::new(ptr)
    .expect("Allocation failed");  // Handles null case

// Now we know ptr is non-null!
// But still need to initialize before reading
unsafe {
    ptr.as_ptr().write(42);  // Initialize
    let x = ptr.as_ptr().read();  // Now safe to read
}

Pattern 2: From Reference

let value = 42;
let ptr = NonNull::from(&value);  // Always succeeds!

// References are never null, so this is safe
unsafe {
    assert_eq!(*ptr.as_ptr(), 42);
}

Pattern 3: In Data Structures

struct MyVec<T> {
    ptr: NonNull<T>,  // Never null!
    len: usize,
    cap: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        Self {
            ptr: NonNull::dangling(),  // Non-null but don't use
            len: 0,
            cap: 0,
        }
    }
}

⚠️ 2.6. Common Mistakes

Mistake 1: Assuming Valid Memory

let ptr: NonNull<i32> = NonNull::dangling();

// ❌ WRONG: NonNull doesn't mean initialized!
unsafe {
    let x = *ptr.as_ptr();  // UB!
}

Mistake 2: Ignoring Alignment

let bytes = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let ptr = NonNull::new(bytes.as_ptr() as *mut u64).unwrap();

// ❌ WRONG: Misaligned!
unsafe {
    let x = *ptr.as_ptr();  // UB on most platforms!
}

Mistake 3: Outliving the Allocation

fn bad() -> NonNull<i32> {
    let x = 42;
    NonNull::from(&x)  // ❌ Returns pointer to local variable!
}

let ptr = bad();
// ptr is non-null, but points to freed stack memory!

🧠 2.7. Chapter Takeaways

Guarantee NonNull Raw Pointer
Non-null ✅ Yes ❌ No
Valid address ❌ No ❌ No
Initialized ❌ No ❌ No
Aligned ❌ No ❌ No
Permission to access ❌ No ❌ No
Niche optimization ✅ Yes ❌ No

Key point: NonNull is a better raw pointer, but still requires unsafe code to use!


🚀 2.8. What’s Next

We understand what NonNull promises (and doesn’t promise).

Next: Chapter 3 — API Tour

Learn all the NonNull methods:

  • Construction (new, new_unchecked, from)
  • Conversion (as_ptr, as_ref, as_mut)
  • Arithmetic (add, offset, etc.)

💡 Exercises

  1. Create NonNull from an allocation and verify it’s non-null
  2. Test Option<NonNull> size optimization
  3. Write a function that safely dereferences NonNull
  4. Implement a Vec-like struct using NonNull instead of *mut
  5. Use Miri to catch invalid NonNull usage

Next: Chapter 3 — API Tour: Construction and Conversion 🔧