We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ownership I: Moves, Copy, Clone, Drop step 19 of 20
Leaking is safe
Run a script of four commands and produce a log in which most of the destructors never run.
pub fn leak_report(script: Vec<String>) -> Vec<String>
The correction
Everything you have internalised in this track has an expert-tier asterisk on it, and this is where it goes:
Destructors are not guaranteed to run.
Not “usually run”. Not “run unless you do something unsafe”. std::mem::forget
is a safe function — no unsafe block, no feature flag — with the signature
pub fn forget<T>(t: T)
and the documented behaviour of taking ownership and then never dropping the
value. Rc cycles leak by construction. ManuallyDrop<T> is a safe wrapper
whose destructor does nothing. And std::process::exit terminates without
unwinding anything at all.
Why Rust made this safe
mem::forget was unsafe before Rust 1.0 and was changed deliberately. The
reasoning is worth understanding because it is a rare case of a language
conceding a point:
Leaks cannot be prevented in safe code anyway. Two Rcs pointing at each
other leak, using nothing but safe std. Marking forget unsafe would have made
unsafe mean “may leak”, which would have been a lie in both directions — safe
code leaks, and most unsafe code does not.
Therefore unsafe code may never rely on a destructor for soundness. This is
the real consequence and it is a load-bearing rule of the entire ecosystem. If
your unsafe block is correct only because some guard’s destructor will run,
your code is unsound, because a caller can mem::forget the guard using nothing
but safe code.
The Rustonomicon documents three case studies of exactly this, and they are worth knowing by name:
-
Vec::drainand leak amplification.Drainmoves elements out of a vector while the vector still exists. If its destructor is skipped, the vector must not be left claiming to own elements that have been moved out — that would be a genuine use-after-free the next time anything touched it. std’s answer is to set the vector’s length to zero up front, and restore the true length inDrain‘s destructor. Forget theDrainand you get an empty vector: everything still in it is leaked, but nothing is invalid. Deliberately leaking more than was leaked, in order to stay consistent, is called leak amplification, and it is the standard technique for making a type sound in the face ofmem::forget. -
Rcrefcount overflow.Rc::cloneincrements ausize. Forget enough clones and the count could wrap to zero, freeing memory other handles still point at. std’s answer is to abort the process on overflow rather than risk a use-after-free — a rare place where the standard library chooses “kill everything” over “be wrong”. -
thread::scoped, removed in 2015. It let a thread borrow stack data, relying on a join-on-drop guard to guarantee the thread finished before the frame died.mem::forgetthe guard and the thread outlives the borrowed stack. The API was removed in Rust 1.0’s run-up; the modernthread::scope(stable since 1.63) has a closure-based design where the join point cannot be skipped.
The takeaway, in one line: safe code may assume destructors run; unsafe code may not.
::: question If a destructor is not guaranteed, how is RAII trustworthy at all? Because the guarantee that matters for RAII is weaker than “always runs” and still strong enough.
For safe code, a value that goes out of scope normally is dropped. There
is no path in safe Rust that skips a destructor except one you asked for by
name: calling forget, wrapping in ManuallyDrop, building a cycle, or exiting
the process. A MutexGuard in ordinary safe code will release the lock, and you
may rely on that when reasoning about correctness — deadlocks, not
use-after-free.
For unsafe code the bar is different, because unsafe code must be sound in the presence of any safe caller, including a hostile one. So the rule becomes: a destructor may make things nicer, but it may never be the thing standing between your program and undefined behaviour.
And note what leaking is not: leaking memory is not undefined behaviour. It is a bug, sometimes a serious one, but it never invalidates a pointer or corrupts memory. That is why Rust could afford to concede the point. :::
What to build
The grader injects the usual scaffolding — tracked(name, log) logging
"new <name>", a Drop impl logging "drop <name>", note(log, line),
new_log(), snapshot(log) — plus one extra:
fn make_cycle(log: Log)
which builds two Rc nodes pointing at each other, each owning a Tracked
named cyc0 and cyc1, and lets both local handles fall out of scope. Neither
refcount reaches zero, so neither destructor is ever reached.
Walk the script and handle four commands:
| Command | Do |
|---|---|
normal N |
build a value named N and release it the ordinary way |
forget N |
build a value named N and std::mem::forget it |
manual N |
build a value named N and wrap it in std::mem::ManuallyDrop::new |
cycle |
call make_cycle |
Anything else: emit format!("? {cmd}").
For ["normal a", "forget b", "manual c", "normal d"] the log is
new a, drop a, new b, new c, new d, drop d
The assertion is the absence of the drop lines, which is exactly the lesson. Nothing panics, nothing is unsafe, and three of those four values are simply never destroyed.
Two traps
mem::forget(t.name) does not work. Reaching into a Drop type to pull one
field out is E0509 — the same wall as earlier in this track. forget takes the
whole value or nothing.
Clippy will not stop you. clippy::mem_forget exists, and it is in the
restriction group, which means it is off by default and stays off unless a
project opts in. Learners routinely assume the linter polices this; it does not.
The lints that are on by default here are forget_non_drop and rustc’s
forgetting_copy_types, and both fire on the useless cases — forgetting a
value that has no destructor, or forgetting a Copy type (which leaves the
original sitting right there). Neither has anything to say about the real thing.
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.