We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 9 of 24
`NonNull<T>`, null, and dangling-but-aligned
pub fn stack_ops(script: Vec<String>) -> Vec<i64>
Build a growable stack of i64 out of alloc, dealloc and NonNull<i64>,
then run a script of operations against it. One number comes out per
operation:
| op | emits |
|---|---|
push N |
the new length |
pop |
the popped value, or -1 if empty |
peek |
the top value, or -1 if empty |
len |
the current length |
reset |
0 — and frees the buffer, returning to the un-allocated state |
| anything else |
-2 |
The empty stack must store NonNull::dangling() and must not allocate
until the first push. Start with capacity 4 and double. reset must return
the stack to exactly the state empty() produced — including not leaving a
freed pointer behind, and including not calling dealloc on the dangling one.
Why containers store NonNull<T>, not *mut T
Three reasons, and they are all real.
The niche. NonNull<T> is a *mut T that the compiler knows is never
null. That “never null” is a validity invariant, which means the all-zeroes
bit pattern is free for another use — so Option<NonNull<T>> is the same
size as a pointer, with None represented as the null address. Option<*mut T> is two words. For a linked list node, that is a 50% saving on every link.
Variance. *mut T is invariant in T; NonNull<T> is covariant.
That matters for a container of T that only ever hands out &T: covariance
lets MyVec<&'long str> be used where MyVec<&'short str> is expected, which
is what a caller expects of an owning collection. If your container hands out
&mut T you must undo this and become invariant again, with
PhantomData<*mut T> — item 17.19 is entirely about that trap.
Documentation. A NonNull field says “this is always a real pointer” in
the type, so the null check does not need to exist in every method and cannot
be forgotten in one of them.
::: question NonNull::new_unchecked(std::ptr::null_mut()) — you construct it
and never use it. Is anything wrong yet?
Yes. That is instant undefined behaviour, at the construction.
Non-nullness is NonNull‘s validity invariant, not a safety convention.
Producing a value that violates its type’s validity invariant is UB the moment
the value exists — assigned to a place, passed as an argument, returned. There
is no “I only made a bad one, I did not use it”.
The safe constructor NonNull::new(p) returns Option<NonNull<T>> and costs
nothing at runtime, precisely because of the niche: the check and the Option
compile away. Reach for new_unchecked only when the non-nullness is already
proven by something else on the line above, and say what in the safety comment.
Item 17.12 draws the general line between validity invariants (never break
them, not even briefly) and safety invariants (you may break them inside your
own module, which is what set_len is for).
:::
Dangling is not null
These are two different failures and conflating them causes real bugs.
Null means address zero. Reading it faults on every platform you care about.
Dangling means “the bytes are not all inside one live allocation”. A freed
pointer is dangling. A pointer to a stack slot whose frame has returned is
dangling. NonNull::dangling() is dangling on purpose: it returns the
address align_of::<T>() — a small, well-aligned, non-null, deliberately
invalid address.
That is exactly the right thing to store in an empty container, and here is
why it has to exist at all: you cannot ask the allocator for zero bytes.
Layout::array::<T>(0) produces a zero-sized layout, and passing a zero-sized
layout to alloc is undefined behaviour. So an empty Vec has no allocation
to point at, and it still needs something in the pointer field that is
non-null (for the niche) and aligned (because slice::from_raw_parts(ptr, 0)
requires alignment even for length zero). NonNull::dangling() is that
something.
The consequence you must get right in this problem: cap == 0 means there is
nothing to free. Calling dealloc on NonNull::dangling() hands the
allocator an address it never issued.
::: question Your reset frees the buffer and sets cap = 0. A later push
allocates again. What must reset leave in the ptr field, and why not just
leave the old address?
It must write NonNull::dangling() back.
Leaving the freed address there is not immediately UB — nothing dereferences
it — but it is a loaded gun. The next person to add a method that reads ptr
without checking cap has a use-after-free, and the invariant they violated
was never written down anywhere. Restoring the dangling sentinel makes the
type have exactly one representation of “empty”, which is what lets every
other method reason about cap == 0 instead of about history.
This is the general shape of a safety invariant: a property no compiler checks, that every method in the module both relies on and must restore. Item 17.20 is about why that makes the module — not the block — the unit you have to audit. :::
Two more facts worth carrying
NonNull::as_ref() and as_mut() produce references with an unbounded
lifetime: the caller picks it, and picking too long is UB. Prefer to mint
references at the point of use, with a lifetime the signature ties down.
A pointer to a zero-sized type is never dangling. Every ZST access is a
zero-sized access, and zero-sized accesses are exempt from the
“dereferenceable” requirement. That is why Vec<()> works and why item 17.16
explicitly excludes ZSTs from the hand-rolled Vec.
What this grader cannot check
There is no clippy lint for NonNull::new_unchecked misuse, and none for
deallocating a dangling pointer. Both are graded here only through behaviour:
the reset-then-push cases exercise the dangling round trip, and a
dealloc on the sentinel will usually — not always — abort. Your reasoning is
the real check.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.