Skip to content

The Rust Engineering Handbook / Chapter 68

Raw Pointers, Provenance, Aliasing, and References

Audit raw-pointer code by separating address, allocation provenance, range, alignment, initialization, aliasing, lifetime, and reference creation.

What is missing from this apparently defensive constructor?

pub unsafe fn words<'a>(ptr: *const u8, byte_len: usize) -> &'a [u32] {
    assert!(!ptr.is_null());
    assert_eq!(byte_len % 4, 0);
    unsafe { std::slice::from_raw_parts(ptr.cast(), byte_len / 4) }
}

It checks nullness and length divisibility. It does not establish that the pointer identifies a live allocation, that the full range stays inside that one allocation, that the address is aligned for u32, that every byte is initialized and readable, that no incompatible mutation occurs, or that the invented lifetime 'a is bounded by an owner. The signature permits a caller to choose 'static even when the allocation dies at the next statement.

Raw-pointer review fails when all of those facts are compressed into the sentence “the pointer is valid.” A pointer is valid only relative to an operation: valid for a zero-sized comparison is different from valid for a four-byte aligned read, a 4,096-byte slice, an exclusive write, or a shared reference lasting for a request.

The audit vocabulary in this chapter has eight dimensions:

  1. allocation identity and liveness — which live allocated object authorizes the access;
  2. numeric address and range — which bytes the operation touches and whether they remain in bounds;
  3. provenance — the abstract history or authority connecting the pointer to an allocation;
  4. alignment — whether the address satisfies the pointee type’s requirement;
  5. initialization and typed validity — whether the accessed bytes may be read as the target type;
  6. access permission — read, write, atomic, volatile, or some narrower operation;
  7. aliasing and mutation — which competing accesses or references exist while the operation is live;
  8. lifetime and metadata — how long a created reference remains valid and whether slice length or trait metadata describes a valid pointee.

Treat these as separate proof rows. A check in one row rarely discharges another.

A raw pointer carries no borrow-checker promise

Rust raw pointers are *const T and *mut T. Creating, copying, storing, comparing, or dropping one is generally safe because those actions need not access its pointee. A raw pointer may be null, dangling, misaligned, or out of bounds. Dereferencing it is unsafe because the operation asserts the conditions required for that access.

*const T and *mut T describe the permitted operation at the point of use; they do not by themselves prove immutability or unique ownership. A *mut T can be copied. A *const T can be derived from memory that some owner may later mutate. The public contract and actual access history determine whether a read or write is sound.

Raw pointers arise from several sources:

  • coercing a reference, such as let p: *const T = reference;
  • borrowing a place with &raw const or &raw mut without creating an intermediate reference;
  • allocation APIs such as Box::into_raw;
  • foreign functions or device interfaces;
  • pointer arithmetic or a cast from another pointer type;
  • integer-to-pointer reconstruction under a documented provenance model.

The source controls the obligations. A pointer derived from &T begins with an owner and a borrow relationship. Box::into_raw transfers responsibility to eventually reconstruct or otherwise deallocate exactly once. A pointer from C needs an ABI and ownership protocol. An arbitrary integer has an address-like number but may lack the provenance needed to access a Rust allocation.

Use &raw when even temporarily creating a reference would assert too much. A field of a packed struct may be misaligned, and a field inside MaybeUninit<T> may not yet hold a valid value. &raw const place or &raw mut place can form a raw pointer without first creating &place or &mut place. Later access still needs its own alignment, range, initialization, and aliasing proof; use read_unaligned only when the layout contract permits an unaligned value read.

Nullness is only one dimension. A non-null pointer may point one byte past a live allocation, into a freed allocation, to an address aligned for u8 but not u64, or into a different allocation that happens to reuse the same numeric address. Conversely, standard APIs often require a non-null aligned pointer even for a zero-length slice because reference and enum layout contracts can rely on those properties. Boundary adapters need an explicit empty-input policy rather than folklore.

Address, allocation, and provenance are different facts

On common targets, printing a pointer usually displays a numeric address. That observation is not the complete semantic value of a pointer. The standard library describes a pointer as containing an address plus provenance: abstract information tying it to the allocation from which it was derived. The full Rust provenance and aliasing models are still being specified, so code should follow documented APIs and avoid depending on maximally permissive interpretations.

Imagine allocations A and B occupy adjacent numeric ranges. Arithmetic beginning from a pointer into A cannot become authorized to access B merely because the calculated number falls inside B. Nor does freeing A, allocating a new object at the same address, and retaining the stale pointer make that pointer a valid handle to the new object. Numeric equality does not prove allocation identity or liveness.

The visual separates the facts. The upper panel shows an address calculation that stays within allocation A and preserves A’s provenance. The lower panel shows that a reference may cover only a live, aligned, initialized subrange with a defensible lifetime and aliasing claim.

A two-panel pointer diagram distinguishes one live allocation and its provenance token from numeric addresses, marks the one-past-the-end pointer as arithmetic-only, and shows an aligned initialized subrange becoming a shared slice while a same-address pointer with different or missing provenance remains unauthorized.

Rust’s stable Strict Provenance APIs make the distinction visible. ptr.addr() obtains the address portion without exposing provenance for a later arbitrary reconstruction. ptr.with_addr(new_addr) creates a pointer with a new address while preserving the provenance of ptr. map_addr performs the same pattern through a mapping closure. These operations do not prove that new_addr lies in bounds; the caller still establishes the allowed range before access.

Exposed Provenance supports cases where a pointer must pass through an integer and no pointer with the required provenance can be retained. expose_provenance or a pointer-to-integer cast exposes provenance as part of that operation; with_exposed_provenance requests a pointer associated with previously exposed provenance. The documentation describes this path as less portable, inherently ambiguous, and potentially unsupported by tools that check memory-model conformance. Prefer Strict Provenance when the system can preserve a base pointer.

Some boundaries genuinely use integer addresses: memory-mapped I/O, kernels, allocators, debuggers, or foreign ABIs may define authority outside ordinary Rust allocations. That does not make every integer cast sound. Record the platform contract, address-space ownership, volatility or atomicity requirements, lifetime, and exclusion from ordinary allocator-controlled memory.

Pointer arithmetic proves a path within one allocation

Pointer arithmetic APIs have different contracts. For ptr.add(count), the byte offset count * size_of::<T>() must fit in isize, and the whole range between the starting and resulting pointer must stay in bounds of the same allocation. A pointer exactly one element past the allocation may be computed for traversal, but it may not be dereferenced. offset permits signed displacement under related rules.

wrapping_add can compute an address without immediately requiring the intermediate range to be in bounds, but wrapping does not authorize a later access. Before dereference, the resulting pointer still needs valid provenance, range, alignment, and access permission. Replacing add with wrapping_add can defer one obligation; it does not erase it.

Arithmetic uses units of the pointee type unless a byte-oriented method is selected. ptr.add(3) on *const u32 advances three u32 elements, usually twelve bytes. byte_add(3) advances three bytes. Mixing element counts and byte counts is a common way to produce a range that passes a superficial length check.

Use checked integer arithmetic before pointer arithmetic when counts come from input:

let byte_len = element_count
    .checked_mul(std::mem::size_of::<Record>())
    .ok_or(ParseError::LengthOverflow)?;

Then prove the resulting range fits the allocation or owning slice. Checking only that start + len <= owner.len() can itself overflow; use checked_add, Range validation, or slice operations that encode the check safely. Zero-sized types need special reasoning because many logical elements can share one address while alignment and slice metadata rules still apply.

Do not infer same-allocation membership from two addresses. Two separately allocated objects may be adjacent. The slice::from_raw_parts documentation includes precisely this warning: a slice cannot span allocations even when numeric addresses appear contiguous. The proof must originate from one allocation contract, not an address comparison.

Audit from_raw_parts one precondition at a time

slice::from_raw_parts(data, len) creates a shared slice reference. Its documented contract gives a useful audit ledger:

Obligation What must establish it Why a local check is insufficient
non-null and aligned, including empty or zero-sized slices pointer source or an explicit empty policy len == 0 does not turn null into an acceptable slice data pointer
readable range of len * size_of::<T>() bytes allocation owner and checked range arithmetic an address and length do not reveal allocation boundaries
one allocation derivation from one owner/allocation contract adjacent addresses can belong to separate allocations
initialized valid T elements constructor, parser, or foreign producer contract readable bytes may still be invalid for T
no incompatible mutation for 'a borrowing or synchronization protocol *const T does not freeze memory
total size at most isize::MAX and no wrap checked size/range proof multiplication and addition may overflow first
lifetime 'a bounded by the owner signature or enclosing safe abstraction return-position lifetime inference can invent an excessive lifetime

The call itself cannot inspect most rows. That is why a raw-slice constructor is often necessarily unsafe: the caller owns evidence that the callee cannot reconstruct. The safer design is to encode more evidence in typed inputs.

The memory-contracts-lab fixture retains an unsafe boundary for foreign-style callers:

/// # Safety
/// For byte_len > 0, ptr must carry provenance for one live allocation.
/// The complete range must be readable, initialized, and unchanged for 'a.
/// The caller must tie 'a to the allocation owner.
pub unsafe fn words_from_raw_parts<'a>(
    ptr: *const u8,
    byte_len: usize,
) -> Result<&'a [u32], RawSliceError> {
    if byte_len == 0 {
        return Ok(&[]);
    }
    if ptr.is_null() {
        return Err(RawSliceError::NullWithElements);
    }
    if byte_len % std::mem::size_of::<u32>() != 0 {
        return Err(RawSliceError::ByteLengthNotWholeWords);
    }

    let word_ptr = ptr.cast::<u32>();
    if !word_ptr.is_aligned() {
        return Err(RawSliceError::Misaligned);
    }

    // SAFETY: Local checks cover shape and alignment. The caller supplies
    // allocation, provenance, initialization, aliasing, and lifetime evidence.
    Ok(unsafe {
        std::slice::from_raw_parts(
            word_ptr,
            byte_len / std::mem::size_of::<u32>(),
        )
    })
}

The function checks conditions that are cheap and observable. Its unsafe fn contract assigns the rest. It returns &[] for zero bytes without constructing a slice from the supplied pointer, making the FFI-style empty policy explicit. Another API may require a non-null aligned sentinel even when empty; consistency with the named foreign interface matters more than choosing one universal policy.

The result is a native-endian word view. Every u32 bit pattern is valid, so initialized bytes suffice for typed validity. The numeric interpretation still depends on target endianness. A wire protocol should normally decode with u32::from_le_bytes or from_be_bytes rather than use native-word reinterpretation. Soundness does not imply protocol correctness or portability.

Tie lifetime and provenance to an owner

When Rust already has an owning slice, the API can be safe. The fixture’s words_in accepts &[u8] plus a checked range:

pub fn words_in(
    owner: &[u8],
    range: std::ops::Range<usize>,
) -> Result<&[u32], RawSliceError> {
    if range.start > range.end || range.end > owner.len() {
        return Err(RawSliceError::OutOfBounds);
    }
    let byte_len = range.end - range.start;
    if byte_len % std::mem::size_of::<u32>() != 0 {
        return Err(RawSliceError::ByteLengthNotWholeWords);
    }
    if byte_len == 0 {
        return Ok(&[]);
    }

    let base = owner.as_ptr();
    let address = base.addr()
        .checked_add(range.start)
        .ok_or(RawSliceError::AddressOverflow)?;
    let ptr = base.with_addr(address);

    // SAFETY: The checked range remains in owner, with_addr preserves the
    // owner's provenance, and the returned lifetime is tied to owner.
    unsafe { words_from_raw_parts(ptr, byte_len) }
}

The input reference establishes a live initialized byte allocation and prevents ordinary mutation while the returned shared borrow is live. Range checks keep the address inside the same owner. with_addr preserves the base pointer’s provenance. Lifetime elision ties the output to owner, so safe callers cannot select 'static. The unsafe callee still checks u32 alignment.

This wrapper deliberately returns an error for an aligned-range failure rather than reading unaligned words. An alternative parser can copy each four-byte chunk into an array and call u32::from_ne_bytes; that works on unaligned input and avoids creating a u32 slice, at the cost of per-element decoding that the optimizer may or may not eliminate. If the input is a wire format, explicit endian conversion is usually the stronger design anyway.

The safe wrapper remains narrow. It must not accept bytes that foreign code mutates concurrently behind Rust’s shared reference. If the real source is shared memory, a DMA device, or an asynchronous C producer, use a protocol that establishes completion and access rights before creating &[u8]; volatile or atomic operations may be required, and an ordinary slice can be the wrong abstraction.

Alignment belongs to the access, not the integer

An address divisible by align_of::<T>() is numerically aligned for T. That check does not establish allocation, provenance, initialization, or aliasing. Conversely, an unaligned raw pointer may be a legitimate handle so long as code does not use an operation requiring alignment.

ptr::read_unaligned and write_unaligned exist for layouts that genuinely permit unaligned fields or byte streams. They still require a live in-bounds range, initialized input for a read, valid output for a write, and correct aliasing. Reading a non-Copy value with read_unaligned transfers ownership just as ptr::read does; repeating the read can create double ownership.

Packed representations need special care. Writing &packed.field may create an invalid misaligned reference before a later cast makes it look raw. Form a raw pointer directly with &raw const packed.field, then use an operation whose alignment contract matches the layout. Often the safer boundary is to copy bytes into an aligned local value.

Alignment can vary by type and target. Do not hard-code four for u32 in generic code; use align_of::<T>() and size_of::<T>(). An FFI structure also needs an explicit ABI representation and valid nested field types. Chapter 70 develops representation, and Chapters 72–73 cover the full foreign boundary.

Creating a reference is a stronger event than dereferencing later

Code review should mark the exact statement where &T, &mut T, &[T], Box<T>, or another reference-bearing value is created. At that point, the program asserts more than “a future load might succeed.” A reference must be non-null, aligned, live, and point to a valid value. Its aliasing and mutability rules apply while it is live according to the language’s constraints.

This is why speculative reference construction is unsound even if a branch never reads through it. Creating &*ptr from a dangling pointer and then deciding not to use it has already produced an invalid reference. Creating &mut T to uninitialized storage so that a foreign function can fill it asserts that a valid exclusive T exists too early. Pass *mut T or *mut MaybeUninit<T> until initialization is complete.

Slice metadata participates in the claim. A *const [T] or &[T] is a wide pointer carrying a data pointer and length. An excessive length can make the described pointee exceed the live allocation or isize::MAX even if the data address itself is valid. Trait-object pointers carry vtable metadata with their own validity requirements. Auditing only the data address misses half the value.

Prefer signatures that derive output lifetimes from input owners:

fn parse<'a>(owner: &'a [u8]) -> Result<View<'a>, Error>

An unsafe function returning an unconstrained &'a T lets the caller choose the lifetime and therefore requires an unusually explicit contract. A raw pointer returned instead can postpone reference creation until an owner-aware layer, but that merely relocates the proof. The boundary should live where all premises are simultaneously visible.

Aliasing rules are real even while the model evolves

The Rust Reference says the exact aliasing rules are not yet fully determined, then gives governing principles. A shared reference &T generally prevents mutation of reachable bytes while it is live except through UnsafeCell. An exclusive reference &mut T generally excludes other references and competing accesses not derived appropriately from it while live. Box<T> carries similarly strong ownership and aliasing expectations.

Two mistakes follow from the model’s evolving status. The first is pretending no aliasing rules exist until a final formal specification arrives. The second is presenting one dynamic model or tool—such as Stacked Borrows or Tree Borrows—as the final language specification. These models are valuable for finding bugs and explaining disciplined derivation, but a chapter must label them as evolving explanatory and tool models.

Use conservative engineering rules:

  • create references only for the period and access mode actually needed;
  • derive child pointers from the owner or active reference that authorizes access;
  • avoid retaining raw pointers across moves, reallocations, destruction, or ownership transfers unless the representation guarantees address stability;
  • use UnsafeCell as the primitive that permits interior mutation behind shared references, then add the synchronization or single-thread protocol the system needs;
  • do not create simultaneous &mut references to overlapping memory;
  • keep foreign callbacks and reentrancy out of periods where Rust assumes exclusive access;
  • run Miri on focused tests where available, while retaining the written proof.

Raw pointers do not suspend reference rules from which they were derived. Converting &mut T to *mut T, creating another access, and then using the original exclusive reference can violate aliasing even though the intermediate operation uses raw syntax. Audit the full access history, not just the type at the final dereference.

Concurrency adds another dimension. A pointer may be locally well formed while another thread frees or mutates the allocation. Thread transfer requires a synchronization and ownership protocol, not only an unsafe impl Send. Chapter 71 composes unsafe pointer, destructor, and auto-trait obligations.

Integer round trips need an explicit portability decision

Systems sometimes tag pointer addresses, store handles in integer fields, or cross an interface that accepts only uintptr_t. Classify the operation before choosing an API.

If a live base pointer can be retained, use addr to manipulate the numeric portion and with_addr or map_addr to restore that address with the base provenance. Prove masking and arithmetic preserve an address within the authorized allocation before access. This is the Strict Provenance path and is generally friendlier to tooling and nontraditional pointer representations.

If no base pointer survives, Exposed Provenance may model the round trip. Document why the system requires it, which platforms support it, where provenance is exposed, and how reconstruction is paired. Do not assume that serializing a pointer integer to disk and reading it in a later process or allocation epoch restores authority. Address-space layout, process identity, allocation lifetime, and provenance all differ.

A third category is a numeric hardware or operating-system address whose authority comes from outside the Rust abstract machine. Use the platform’s pointer-construction and volatile/atomic access rules. Keep that code target-specific and prevent ordinary safe code from manufacturing device handles.

Pointer formatting and logging should be treated as sensitive. Addresses can reveal memory layout and weaken exploit mitigations. Prefer stable allocation IDs or offsets in production telemetry when those answer the operational question. If raw addresses are necessary for a crash dump, control access and retention.

FFI pointers need a protocol, not a cast

A C signature such as (const uint8_t *ptr, size_t len) does not fully describe Rust safety. The adapter must define:

  • whether null is allowed when len == 0;
  • who owns the allocation and how long it remains live;
  • whether the foreign side may mutate it during or after the call;
  • whether length is in bytes or elements and which arithmetic can overflow;
  • alignment and initialization guarantees;
  • whether callbacks can re-enter or free the allocation;
  • which thread may access it;
  • how errors and partial initialization are reported;
  • whether Rust may retain a reference after returning.

For a borrowed input, copy when the foreign lifetime or mutation contract cannot support a Rust reference. The copy costs allocation or bandwidth but turns an external temporal promise into Rust-owned state. For large data, a lease object or callback-scoped view can encode a shorter valid period. A process boundary may be preferable when the producer is untrusted or crash-prone.

Never trust len merely because it has type size_t. Validate it against the actual object contract before pointer arithmetic. For an output buffer, use MaybeUninit storage until the foreign function reports how many elements it completely initialized. On partial failure, drop only initialized Rust-owned values; plain C bytes may have different cleanup rules.

Crossing the boundary does not make native-endian reinterpretation a portable file or network format. Decode explicit-width bytes with defined endianness. If zero-copy viewing is a measured requirement, make alignment, representation, target, and producer version part of the compatibility contract.

Compare the designs before accepting an unsafe slice

Boundary design Favors Costs Reject when
borrowed typed slice &[T] a Rust caller already has valid typed elements caller must construct the slice earlier the real source is raw, foreign, or concurrently mutated
owner plus checked range one Rust allocation owns initialized bytes adapter performs validation; alignment may reject ownership cannot be represented by a borrow
copy and decode alignment, endian, lifetime, or trust is uncertain allocation or per-element copy/decoding measured cost is unacceptable and a stronger lease exists
unsafe pointer-plus-length caller alone possesses allocation evidence broad caller contract and specialist review a safe typed signature can encode the facts
lease/guard object external owner can promise a bounded access epoch protocol and type complexity callbacks or producer behavior can violate the lease
process boundary producer is untrusted or failure isolation matters serialization, IPC, latency, operations in-process sharing is essential and auditable

The default is to keep or recover an owner in the signature. Pointer-plus-length is appropriate at genuine foreign, allocator, kernel, or low-level abstraction seams. It should not spread through ordinary application code because each hop must repeat lifetime and aliasing evidence.

Failure modes that pass superficial review

Null checked, allocation unknown. The pointer is non-null but stale or fabricated. Add an owner or an unsafe caller contract that identifies the allocation and liveness epoch.

Endpoints look contiguous. The start and end addresses enclose bytes that cross two allocations. Derive the entire range from one owner; do not infer allocation identity numerically.

Aligned start, overflowing length. len * size_of::<T>() wraps before a bounds comparison. Use checked multiplication and enforce the isize::MAX limit required by slice construction.

wrapping_add treated as access permission. Address calculation succeeds, then code dereferences outside the allocation. Prove in-bounds range before access.

Reference created before validation. Code forms &T, then validates a discriminant or initialization flag. Validate through bytes or raw pointers first; create the reference only after typed validity exists.

Lifetime invented by return type. An unsafe helper returns arbitrary &'a T from a raw pointer. Tie the output to an input owner or return a raw pointer until an owner-aware layer.

Native word view used as wire decoding. The code is memory-safe on one target but reverses byte order elsewhere. Use explicit endian conversion and test golden bytes.

Integer round trip assumed universal. The code discards provenance and relies on a conventional flat address space. Preserve a base pointer with Strict Provenance or document a narrower platform-specific Exposed Provenance contract.

Miri used as certification. Focused tests pass one modeled execution set. Keep the proof table, test more transitions, and treat tool findings as strong counterexamples rather than universal proof.

Production and security consequences

Pointer bugs often corrupt state far from their origin. Put assertions and metrics at safe boundaries: rejected lengths, alignment fallbacks, copy-versus-view selection, foreign error codes, and lease violations. Do not attempt to log by dereferencing a suspect pointer. Capture numeric offsets and protocol metadata before access.

Fuzz safe parsers and checked adapters with arbitrary bytes, lengths, and ranges. Run Miri on focused unsafe tests when a compatible nightly toolchain is part of the verification environment. Add sanitizers or foreign-language tooling for cross-boundary allocations where supported. Each tool explores executions under a model; none proves allocation lifetime or all aliasing histories.

Threat modeling must treat pointer and length inputs as a trust boundary. An attacker-controlled length can cause arithmetic overflow, excessive work, address disclosure, or memory access outside the intended object. A pointer supplied by a plugin in the same process has the power of that process; Rust types cannot sandbox native code. Copying and validating data or moving the plugin across a process boundary can reduce that authority.

Performance arguments should compare a checked copy, explicit decode, and borrowed view under a named workload. Unsafe zero-copy code can add alignment branches, lifetime constraints, cache behavior, and maintenance cost. The compiler may optimize small fixed-size copies. Measure before spending a permanent safety budget.

Portability review should include pointer width, alignment, endian, address tagging, target APIs, and FFI type definitions. Compatibility review should pin the Rust version for provenance APIs and label the full provenance model as evolving. The fixture uses stable addr and with_addr APIs available under its Rust 1.85.0 MSRV. Its locked checks pass on Rust 1.93.1; the declared MSRV still requires a separate verification environment.

Exercise: audit and rebuild a raw-slice constructor

You receive an FFI adapter that accepts *const u8, an offset, and an element count, then returns &'static [Header]. Its tests cover one valid allocation and a null pointer with zero elements.

Produce an audit package containing:

  1. a line-by-line table for allocation, numeric range, provenance, alignment, initialization, typed validity, aliasing, lifetime, and metadata;
  2. checked arithmetic for offset, count, and byte length, including the isize::MAX constraint;
  3. a corrected unsafe boundary with a complete # Safety contract;
  4. a safe wrapper whose returned lifetime is tied to an owner or lease;
  5. a decision on copy/decode versus typed view, including endian and alignment costs;
  6. an explicit (null, 0) FFI policy and tests for every locally checkable rejection;
  7. rejected cases for a stale pointer, adjacent allocations, misalignment, concurrent mutation, an excessive lifetime, and address reuse—described but not executed as undefined behavior;
  8. a Strict Provenance design using a retained base pointer, plus justification if Exposed Provenance remains necessary;
  9. a callback and thread-transfer analysis;
  10. a verification plan covering unit tests, fuzzing, Miri, sanitizers or foreign tools, and the limitations of each.

Reject the adapter if it creates a reference before typed validity is established, treats nullness as allocation proof, infers one allocation from adjacent addresses, returns an unconstrained lifetime, uses native word reinterpretation as a portable wire decoder, or claims an evolving aliasing model is the final Rust specification.

Raw-pointer review questions

  • Which live allocation and ownership epoch authorize each non-zero access?
  • Was every derived pointer produced from a base with appropriate provenance?
  • Do checked arithmetic and one-allocation reasoning cover the entire touched range?
  • Is the pointer aligned for the exact operation, or is an unaligned operation intentionally used?
  • Are all bytes initialized, and does the target type accept their bit pattern?
  • What reads, writes, callbacks, device actions, or other threads can occur while the access or reference is live?
  • At which statement is a reference created, and who bounds its lifetime?
  • Does slice or trait-object metadata describe a valid in-bounds pointee?
  • Is an integer round trip Strict, Exposed, or platform-external provenance, and is that choice documented?
  • Does the FFI contract define null-empty behavior, ownership, mutation, errors, and retention?
  • Are dynamic tools used to find violations without replacing the safety argument?

A raw pointer is not dangerous because its syntax is exotic. It is dangerous when the code loses the evidence connecting an operation to one live allocation and then reconstructs a reference with a stronger story than the system can support. Keep the owner, range, provenance, validity, aliasing, and lifetime claims visible at the same boundary.

Chapter 69 uses this ledger to build a safe shell around a small unsafe kernel. The design challenge shifts from proving one call to preserving the proof across every constructor, method, iterator, panic path, destructor, and auto-trait decision.

Sources and version note

The standard library std::ptr module documents pointer validity, alignment, Strict Provenance, and Exposed Provenance while stating that the complete provenance and aliasing models are not finalized. The primitive raw-pointer documentation defines addr, with_addr, arithmetic APIs, alignment queries, and their operation-specific preconditions. slice::from_raw_parts provides the authoritative slice-construction contract and the one-allocation warning. The Rust Reference’s pointer types and undefined-behavior chapters define raw/reference distinctions, invalid reference production, in-bounds projections, and the current outline of aliasing constraints.

These sources were reviewed on 2026-07-21 against the Rust 1.97.1 standard-library documentation. The locked memory-contracts-lab fixture passed formatting, checks, tests, and Clippy on Rust 1.93.1; Rust 1.85.0 remains its declared MSRV but was not locally available for a fresh run. Strict and Exposed Provenance APIs are stable, but the broader language model continues to evolve; the chapter therefore treats dynamic aliasing models as tools rather than final specifications.