Low-Level Systems Programming

Pointer Representation

In Rust, a pointer is more than a raw numeric memory address: at the machine level, pointers are either thin pointers (1 machine word) or wide/fat pointers (2 machine words), and at the abstract machine level, every pointer carries provenance—invisible compile-time metadata tracking which allocation the pointer is authorized to access.

Thin Pointer (`*const i32`, `&u64`, `Box<T>` where `T: Sized`):
  +-----------------------------------+
  | Address + Provenance (8 Bytes)    |  = 8 bytes on 64-bit targets
  +-----------------------------------+

Wide / Fat Pointer (`&[T]`, `*const str`, `&dyn Trait`):
  +-----------------------------------+-----------------------------------+
  | Data Ptr + Provenance (8 Bytes)   | Metadata: Length or Vtable (8 B)  | = 16 bytes
  +-----------------------------------+-----------------------------------+

Thin vs Wide Pointers Across Types

Pointer Type Pointee T Kind Size (64-bit) Second Word Metadata
&T, &mut T, *const T, *mut T, NonNull<T> T: Sized 8 bytes (1 * usize) None (Thin pointer)
&[U], &mut [U], *const [U], &str Slice DST ([U], str) 16 bytes (2 * usize) Element count (len: usize)
&dyn Trait, Box<dyn Trait>, *const dyn Trait Trait Object DST 16 bytes (2 * usize) Pointer to static virtual method table (vtable)
fn(i32) -> i32 Function Item / Code 8 bytes (1 * usize) None (Code address)

Pointer Provenance and the Strict Provenance API (Rust 1.84+ / 2024)

Two pointers can have the exact same numeric address in memory while having different permissions (provenance)—for example, a pointer to one array that was incremented one-past-the-end (arr1.as_ptr().add(len)) may numerically equal the start address of an adjacent variable arr2, but dereferencing the first pointer to read arr2 is Undefined Behavior!

Historically, systems programmers cast pointers to usize (ptr as usize), performed bitwise math (such as tagging the lowest alignments bits of an 8-byte-aligned pointer), and cast back (addr as *const T). However, usize does not carry pointer provenance, confusing compiler alias analysis.

Rust 1.84+ stabilizes the Strict Provenance API so low-level code can inspect or transform a pointer's address bits while preserving its original provenance:

Strict Provenance Method Signature Purpose
ptr.addr() fn addr(self) -> usize Extracts the numeric address of ptr without exposing provenance.
ptr.with_addr(addr) fn with_addr(self, addr: usize) -> Self Creates a new pointer with numeric address addr while retaining self's provenance!
ptr.map_addr(f) fn map_addr(self, f: impl FnOnce(usize) -> usize) -> Self Transforms the address bits (e.g., setting or clearing tag bits) while preserving provenance.
ptr::without_provenance(addr) fn without_provenance<T>(addr: usize) -> *const T Creates a pointer from a fixed hardware/MMIO address (or sentinel integer) that has no Rust allocation provenance.
use std::mem::size_of;

/// Packs a 3-bit tag (`0..=7`) into the unused low bits of an 8-byte-aligned pointer
/// using Strict Provenance (`map_addr`) so Miri and LLVM preserve full pointer provenance!
struct TaggedPtr<T> {
    raw: *const T,
}

impl<T> TaggedPtr<T> {
    const TAG_MASK: usize = 0b111;

    fn new(reference: &T, tag: usize) -> Self {
        assert!(align_of::<T>() >= 8, "T must be at least 8-byte aligned");
        assert!(tag <= Self::TAG_MASK, "tag must fit in 3 bits");

        let ptr: *const T = reference;
        // Preserve `ptr`'s provenance while OR-ing `tag` into the low 3 bits:
        let tagged = ptr.map_addr(|addr| addr | tag);
        Self { raw: tagged }
    }

    fn tag(&self) -> usize {
        self.raw.addr() & Self::TAG_MASK
    }

    fn deref_value(&self) -> &T {
        // Strip the 3-bit tag while keeping the original allocation provenance intact!
        let untagged = self.raw.map_addr(|addr| addr & !Self::TAG_MASK);
        // SAFETY: `untagged` has the exact address and provenance of the original `&T`.
        unsafe { &*untagged }
    }
}

fn main() {
    println!("Thin pointer `&u64`:     {} bytes", size_of::<&u64>());
    println!("Wide pointer `&[u64]`:   {} bytes", size_of::<&[u64]>());

    let value: u64 = 0xCAFE_BABE_1234_5678;
    let tagged = TaggedPtr::new(&value, 0b101);

    assert_eq!(tagged.tag(), 0b101);
    assert_eq!(*tagged.deref_value(), 0xCAFE_BABE_1234_5678);
    println!("Recovered tag={:#b} and value={:#x}", tagged.tag(), *tagged.deref_value());
}
Thin pointer `&u64`:     8 bytes
Wide pointer `&[u64]`:   16 bytes
Recovered tag=0b101 and value=0xcafebabe12345678