RFA-413 · Case file with fixtures · Case 385 of 694 · Runtime evidence
NonNull::dangling Is Aligned and Non-Null, but Not Dereferenceable
NonNull::dangling creates a well-aligned non-null placeholder with no allocation or live value behind it. It may represent empty storage internals, but it must never be dereferenced and should not be used as a unique sentinel.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- NonNull promises non-nullness, so dangling constructs a well-aligned placeholder with no allocation behind it; the pointer is suitable only for states that never dereference it.
- First discriminating check
- Separate non-null and aligned representation from dereference validity, and store an explicit state flag or Option when initialization must be represented.
I first read NonNull::dangling() as a null-like marker with a dramatic name. It cannot be null because NonNull<T> promises exactly the opposite.
The failing fixture casts NonNull::<u64>::dangling() to an address and expects zero. Rust produces a nonzero address aligned for u64.
dangling provides representation, not storage
NonNull::dangling creates a pointer that is dangling but well aligned for T. No allocation is created and no T exists at that address.
This can represent the pointer field of an empty low-level collection. When length is zero, the implementation never dereferences the pointer, yet the pointer field still satisfies NonNull's non-null and alignment invariants.
The value proves neither initialization nor accessible bytes. Alignment is only one condition required for dereferencing.
Dereferencing it would be invalid
A usable pointer to T needs valid allocation provenance, enough in-bounds storage, correct alignment, a properly initialized T, compatible aliasing, and a valid lifetime for the access.
dangling() deliberately supplies only a convenient non-null aligned address. Reading or writing through it is not repaired by an unsafe block. The unsafe block only moves the proof obligation to the programmer.
The repaired fixture asserts nonzero and alignment, then stops. It never dereferences the pointer.
It is not a unique sentinel
The standard documentation warns that the dangling address may coincide with the address of a real allocated object. The value is not reserved globally for “empty.”
I therefore do not compare a pointer against NonNull::dangling() to decide whether storage exists. A valid allocation could theoretically use that address, and allocator reuse makes raw addresses poor historical state markers.
An explicit length, capacity, enum, or Option should carry initialization state.
Option can represent absence efficiently
NonNull<T> excludes null. Rust can use the null bit pattern as the None representation for Option<NonNull<T>>, as covered by the standard Option representation guarantees.
This means an explicit optional pointer does not generally require an extra boolean-sized field. I prefer it when absence is a real state:
struct Slot<T> {
pointer: Option<std::ptr::NonNull<T>>,
}
The type now distinguishes absent from present. Presence still does not by itself prove safe dereference; the owner must maintain the rest of the pointer invariant.
Empty Vec explains the pattern
An empty Vec<T> must store a pointer even when it has no allocation. Rust's Vec guarantees include a non-null pointer representation, so an aligned dangling pointer is useful together with length and capacity zero.
Code never indexes an element because length is zero. Before elements appear, allocation and initialization establish a dereferenceable region and update the other fields consistently.
Copying only the pointer out of that state machine loses the length, capacity, allocator, and lifetime facts. I do not treat the raw address as the vector.
Zero-sized types need careful reasoning
For zero-sized T, many logical elements can exist without ordinary storage bytes. Pointers must still satisfy alignment and non-null requirements where references are involved, while iteration may advance logical positions without advancing allocated bytes in the usual way.
This is another reason “pointer exists” does not mean “allocation exists.” Low-level generic containers test zero-sized and non-zero-sized types separately.
The Atlas fixture uses u64 so the alignment property is visible in a familiar allocated-type case.
Provenance is not an integer address
Printing or casting the dangling pointer for evidence does not make arbitrary integer arithmetic a way to construct valid pointers. Modern Rust pointer APIs distinguish address manipulation from provenance.
I keep pointer creation tied to an allocation-producing owner and use documented exposed-provenance operations only when the external contract truly works through integer addresses. Hardware and FFI boundaries need their own proof.
For ordinary optional data, references, boxes, and collection APIs keep these facts in safe types.
My low-level state checklist
Before any unsafe access through NonNull, I prove:
- non-nullness and alignment;
- an allocation or valid external object covers the byte range;
- the target is initialized with a valid
T; - aliasing permits the requested read or write;
- the pointer has appropriate provenance;
- the object remains alive for the access;
- length and capacity metadata agree.
NonNull::dangling() intentionally fails the allocation and initialization parts. It is useful only in states where no access occurs.
The core principle is that pointer representation and pointer validity are different layers. Dangling supplies a non-null aligned placeholder, not a hidden object. I store explicit state beside it, never dereference the empty state, and never assume its address is a unique sentinel.