We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 21 of 24
Leaking is safe, and exception safety
pub struct Drain<'a, T> { /* yours */ }
pub fn drain_script(ops: Vec<String>) -> Vec<String>
The starter hands you a MyVec<T> with push, pop and Deref. Write a
Drain<'_, T> guard for it — an iterator that empties the vector, yields its
elements by value, and drops whatever the caller did not take.
| op | emits |
|---|---|
push S |
"ok" |
drain_take N |
drains, yields the first N elements joined by |, then drops the drainer |
drain_forget N |
drains, yields the first N, then mem::forgets the drainer |
len |
the length |
join |
the elements joined by , |
The starter’s Drain repairs the vector in its destructor, which is the
natural first design and is unsound. The drain_forget cases fail, and
they fail in a way you can read.
The rule
Unsafe code may not assume destructors run.
mem::forget is a safe function. So is Box::leak. So is building an Rc
cycle. So is ManuallyDrop. Rust makes leaking safe on purpose — a leak
wastes memory but cannot corrupt it — and the consequence for anyone writing
unsafe code is absolute:
Any API whose soundness depends on a destructor executing is unsound. Full stop.
Not “risky”. Not “please don’t”. Unsound, in the same sense as a dangling
pointer, because a safe caller can reach the bad state with no unsafe block
anywhere.
::: question mem::forget is safe, but it can obviously break invariants. Why
did the language not make it unsafe?
Because it cannot be prevented, so pretending otherwise would be worse.
Even if mem::forget were removed tomorrow, Rc::new(cell) where the cell
points back at the Rc leaks with no unsafe and no forget. So does
Box::leak. So does a loop {} inside a destructor. So does aborting the
process. Leaking is reachable from safe code by construction, and a guarantee
the language cannot enforce is not a guarantee.
Rust’s response was to draw the line in the honest place: memory safety is
guaranteed; memory reclamation is not. That is why Drop is a best-effort
convenience rather than a linear-type obligation, and why every unsafe
abstraction has to be written to survive its own destructors never running.
The history is worth knowing. Pre-1.0, thread::scoped returned a JoinGuard
whose destructor joined the thread — so borrowing a stack local into the thread
was “safe”, provided the guard was dropped. mem::forget the guard and the
thread kept running with a reference to a dead stack frame. The API was pulled
days before 1.0. The episode is remembered as the leakpocalypse, and it is
exactly why today’s thread::scope takes a closure: if the API owns the scope,
forgetting is impossible.
:::
Leak amplification
Here is the technique, and it is one line.
A naive Drain records the vector’s length, starts moving elements out, and
repairs len in its own destructor. Between construction and destruction the
vector is inconsistent: len claims elements that have been moved out and
dropped. Forget the drainer and that inconsistency becomes permanent — a
Vec whose len describes freed values. Reading it is a use-after-free
from safe code.
Leak amplification inverts it. Set len = 0 at construction, before a
single element moves:
pub fn drain(&mut self) -> Drain<'_, T> {
let start = self.ptr.as_ptr();
// SAFETY: len <= cap
let end = unsafe { start.add(self.len) };
self.len = 0; // <- the entire technique
Drain { start, end, _marker: PhantomData }
}
Now at every instant len describes exactly the slots the vector still
owns: none. Forget the drainer and the un-yielded elements leak — the vector
is consistent but lossy. Push into it afterwards and everything works
normally, because the buffer was never freed and len was never a lie.
Leak more rather than corrupt. That is the slogan. Leaking is an acceptable outcome; undefined behaviour is not.
::: question After drain_forget 1 on a three-element vector, what should
len and join report — and what does the naive version report?
Correct: len is 0 and join is the empty string. The two unyielded
strings have leaked, and the vector is a perfectly ordinary empty vector that
you can push into again.
Naive: len is 3, because the repair lived in a destructor that never ran.
join then builds a slice over three slots, one of which was ptr::read out
and dropped — so it formats a String whose heap buffer has been freed. On a
good day that is garbage; on a bad day it is a segfault; on the worst day it
is plausible-looking output.
This is the rare problem where the harness can observe the fix, because
leak amplification has a visible behavioural signature: the len assertion
fails deterministically. Most soundness fixes in this track have no signature
at all.
:::
Exception safety, the same rule wearing a different hat
A panic can unwind out of a user closure, a Clone impl, an allocation
failure path, or arithmetic overflow in a debug build. If it unwinds while
your structure is temporarily violating its own invariant, the destructors
that run during unwinding will observe the broken state.
Two disciplines cover almost everything:
- update the length last — never publish slots before they hold values;
-
use a drop guard whose own
Droprepairs the invariant, for the cases where “last” is not achievable.
Distinguish the two levels. Basic exception safety — no UB, no leak of the container itself, the structure still valid — is what unsafe code must guarantee. Strong exception safety — all-or-nothing, the operation either completes or leaves everything untouched — is a design nicety you provide when it is cheap.
Two traps. A drop guard whose own Drop can panic aborts the process
(panic during unwind is a double panic). And you may not assume
panic = "abort" — that is a binary-level choice, and a library cannot make
it. Related: unwinding across an extern "C" boundary is undefined
behaviour, which is why extern "C-unwind" exists.
Lints, and one that will not fire
clippy::mem_forget is restriction/allow. It will not fire, here or
anywhere, unless somebody switches it on. Say that out loud, because learners
assume clippy polices mem::forget and it does not — the whole point of this
page is that mem::forget is a normal, legitimate, safe function.
What is on: clippy::forget_non_drop (forgetting a type with no destructor
is pointless) and clippy::undropped_manually_drops (deny —
drop(manually_drop_value) does nothing at all, because ManuallyDrop has no
Drop to run; you need ManuallyDrop::drop).
What this grader cannot check
The leak itself. After drain_forget 3, three Strings are gone forever and
nothing here can see it — which is precisely the point: that leak is the
correct behaviour. What is checked is that the vector survives it intact.
Remember the grade is compile + tests + clippy -D warnings.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.