Raw Pointers (*const T, *mut T), Provenance & Aliasing Models

Master raw pointer semantics, pointer provenance models, Stacked Borrows, integer-to-pointer casting pitfalls, and pointer arithmetic.

04. Unsafe Rust, Layout Control & Systems Internals7 min read

Unlike standard Rust references (&T and &mut T), raw pointers (*const T and *mut T) do not carry compiler-enforced lifetimes, do not track borrow exclusivity, and do not execute automatic drop glue when they exit scope.

They can alias freely, can be null, and can point to unaligned memory. However, to dereference them, an engineer must understand the concept of Pointer Provenance.

Pointer IdentityMemory Model
Provenance

A pointer is not just an integer address; it carries a compiler permission tag tied to a specific memory allocation.

1. What is Pointer Provenance?#

In low-level C programming, developers often treat pointers as plain 64-bit unsigned integers representing byte addresses in virtual memory. In modern optimizing compilers, this mental model is insufficient.

A pointer is mathematically modeled as a pair consisting of a memory address AA and an Allocation Provenance Tag IDallocID_{\text{alloc}}:

P=(A, IDalloc)P = (A, \, ID_{\text{alloc}})
Architecture Diagram
Rendering diagram...

The Valid Dereference Invariant#

A raw pointer dereference is valid if and only if the target address AA lies within the valid bounds of allocation IDallocID_{\text{alloc}} and satisfies the hardware alignment requirement:

ValidDereference(P)  ⟺  A∈[Base(IDalloc), Base(IDalloc)+Size(IDalloc))∧A≡0(modAlign(T))\text{ValidDereference}(P) \iff A \in [\text{Base}(ID_{\text{alloc}}), \, \text{Base}(ID_{\text{alloc}}) + \text{Size}(ID_{\text{alloc}})) \quad \land \quad A \equiv 0 \pmod{\text{Align}(T)}

Casting a pointer to an integer (as usize) strips its provenance tag. If you cast that integer back to a pointer (as *const T), the compiler loses the ability to prove which memory allocation the pointer is authorized to access, which can trigger Undefined Behavior under memory models like Stacked Borrows and Tree Borrows.

2. Raw Pointer Arithmetic#

When performing arithmetic on raw pointers, Rust provides explicit methods that scale automatically by the size of type TT:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() { let numbers = [10u32, 20, 30, 40, 50]; let base_ptr: *const u32 = numbers.as_ptr(); unsafe { // .add(offset) scales by size_of::<u32>() (4 bytes per element): // Address: base + (2 * 4 bytes) let third_ptr = base_ptr.add(2); println!("Third value: {}", *third_ptr); // 30 // .offset() supports signed offsets: let second_ptr = third_ptr.offset(-1); println!("Second value: {}", *second_ptr); // 20 } }
Pointer Math Invariant
Using .add(n) requires that both the starting address and the resulting address fall within (or one byte past) the same allocated object. Calculating an address completely outside the bounds of the allocation is immediate Undefined Behavior, even if you never dereference the pointer!

3. Creating Raw Pointers Safely#

In early Rust, developers frequently cast references to raw pointers directly (&mut val as *mut T). However, creating intermediate references asserts borrow checker exclusivity rules that can accidentally invalidate existing raw pointers!

Modern Rust provides the std::ptr::addr_of! and std::ptr::addr_of_mut! macros:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::ptr; struct Packet { header: u8, payload: [u8; 16], } fn main() { let mut packet = Packet { header: 1, payload: [0; 16] }; // IDIOMATIC: addr_of_mut! creates a raw pointer without // creating intermediate Rust references that could assert aliasing rules: let payload_ptr: *mut [u8; 16] = ptr::addr_of_mut!(packet.payload); unsafe { (*payload_ptr)[0] = 0xFF; } }

4. Concept Verification#

Test your understanding of raw pointers and memory provenance:

Concept check

Why can casting a pointer to usize and back to *const T cause Undefined Behavior in optimizing compilers?

Concept check

How does pointer.add(3) calculate the new memory address for a pointer of type *const u64?