Null pointer read
| Vulnerability potential | Medium |
| DDoS potential | Medium |
Reading from a null raw pointer is undefined behavior
Impact
Reading through a null raw pointer — *null_ptr, std::ptr::read(null),
(*null_ptr).field, or std::ptr::read_volatile(null) — is undefined behavior
in Rust. In practice, on a hosted OS with the zero page unmapped, the access
faults and the process is killed with SIGSEGV (exit by signal 11). But because
it is UB rather than a defined trap, the compiler is also entitled to assume the
pointer is non-null and optimize accordingly, which can delete checks, reorder
code, or produce results that read adjacent or attacker-influenced memory.
The most common real-world effect is a hard crash. The more dangerous, less visible effect is silent miscompilation around the dereference, since UB is not required to fail at the point of the bad read.
Vulnerability potential
- Denial of service. A reliable null read crashes the process (or panics
the kernel in
no_std/kernel contexts). If an attacker can reach the code path — a missing FFI return value, an uninitialized pointer field — repeated triggering is a dependable crash-based DoS. - Information disclosure. When the pointer is “null + offset” (e.g.
(*(null as *const Struct)).far_field), the effective address is not zero and may land on mapped memory, reading bytes the attacker should not see. - Optimizer-induced bypass. Because the compiler may assume the pointer is non-null, surrounding null/bounds checks can be eliminated, turning a would-be safe path into one that proceeds with garbage — potentially enabling further out-of-bounds access.
It does not, by reading alone, overwrite memory, so it sits below the write/copy variants in severity; the realistic outcomes are crashes and limited info leaks.
Technical details
Raw-pointer dereference is only allowed in unsafe code, and the language
contract requires the pointer to be non-null, properly aligned, and pointing to a
valid, live, initialized T for the access. A null pointer violates that
contract, so the operation is UB independent of what the hardware happens to do.
Hardware vs. language semantics
On most platforms address 0 (and a surrounding guard region) is unmapped, so the
MMU raises a fault and the OS delivers SIGSEGV. On bare-metal targets or
microcontrollers where address 0 is mapped (vector tables, RAM), the read
silently returns whatever lives there with no fault — the same source-level UB,
a quieter and more dangerous outcome. Either way the compiler’s null-non-null
assumption applies, so reasoning must be done at the language level, not from the
observed crash.
Catching the issue
Dynamic detection
Run under Miri (cargo +nightly miri run/test), which detects dereferences
of null/dangling/misaligned pointers precisely. Native builds with
-Z sanitizer=address (AddressSanitizer) catch the faulting access, and on Linux
a SIGSEGV handler or coredumpctl plus a debugger pinpoints it.
Static and review
clippy and rustc flag some constructs (e.g. dereferencing a literal null),
and the broader discipline is to minimize unsafe, wrap raw pointers in
NonNull<T> (which encodes non-nullness in the type), and convert raw pointers
with ptr.as_ref()/as_mut() which return Option<&T> and force an explicit
null check instead of an unchecked dereference.
How to reproduce
Run the following; observe a segmentation fault (or detection under Miri). The read dereferences address zero.
fn main() {
let p: *const i32 = std::ptr::null();
let value = unsafe { *p }; // UB: read through null pointer -> SIGSEGV
println!("{value}");
}