We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 9 of 14
Capstone: audit and repair an unsound abstraction
Audit a small slot-based arena and repair five issues: an unchecked-access
bug, stale handles, incorrect live counts, drain cleanup, and missing safety
documentation. Keep the supplied audit_script function unchanged.
pub fn audit_script(ops: Vec<String>) -> Vec<String>
The arena stores strings in reusable slots. A handle contains a slot index and a generation number. Its public fields allow callers to construct any handle, so safe lookup and removal must validate it before accessing a slot.
Command behavior
The script starts with an empty arena of capacity three. Commands are
case-sensitive and split on whitespace. Each command needs exactly the
tokens shown below; stored text is one token. Invalid commands or numbers
produce bad <original line> and do not perform an operation.
| Command | Required behavior and log |
|---|---|
alloc <text> |
Use the lowest vacant slot, or append a slot if capacity allows. Log alloc <text> = <index>:<generation>. If full, log alloc <text> full without changing arena state. |
get <i> <g> |
Log get <i>:<g> = <text> for an occupied slot with a matching generation, otherwise get <i>:<g> none. |
raw <i> <g> |
Validate with get before using the unsafe lookup. Log the same result with the raw prefix. |
free <i> <g> |
Remove a matching occupied slot and log free <i>:<g> ok. Otherwise log free <i>:<g> no and change nothing. |
live |
Log live <count> for the number of occupied slots. |
drain |
Empty the arena and log its strings in slot order, separated by commas: drain a,b. Use drain - when empty. |
forget |
Collect every item from a drain, then forget the exhausted guard without running its destructor. Log forget <items> using the same formatting. The arena must still be empty. |
Indices parse as usize; generations parse as u32. Decimal leading zeros
and a leading + are accepted, while negative or out-of-range numbers are
rejected. Lookup and removal logs preserve the supplied numeric tokens.
Allocation logs use the arena's numeric values.
New slots start at generation zero. Every successful removal, including
removal by draining, increments that slot's generation using wrapping u32
arithmetic. Failed removal leaves it unchanged. Slots are reused without
resetting their generation.
This detects stale handles until a generation repeats. After enough reuse,
u32 wraps and an old handle can match again. The exercise does not promise
permanent identity or distinguish handles from different arenas. A design
requiring those guarantees needs an additional policy.
Separate memory safety from correctness
A sound safe API must not let safe callers cause undefined behavior, even with forged handles. An unsafe function can impose extra caller obligations, but must document them. Panic, an incorrect count, or a memory leak is not automatically undefined behavior.
In this starter, the out-of-bounds unchecked access is a soundness defect. The stale-handle and live-count defects violate the arena's intended behavior. They do not, by themselves, create dangling references: slots still contain valid enum values, and lookup returns references tied to the arena's borrow. Repair both categories, but explain them accurately.
Five places to inspect
- Lookup bounds.
getrejects indices greater than the slot count but allows an index equal to it. That index is out of bounds, including zero when there are no slots. Check the condition before unchecked access. - Slot reuse. Removing a value must advance its generation. Otherwise an old handle can unexpectedly retrieve the next value in that slot.
- Failed allocation. A full arena must reject allocation without
incrementing
live. Change the count only after inserting a value. - Drain cleanup. The starter restores
livein the guard's destructor. Forgetting the guard skips that repair. Move the necessary state changes to a point where forgetting cannot leave an incorrect count or retain values that the drain is required to remove. - Unsafe documentation. Add a
# Safetysection toget_unchecked_str, and give every remaining unsafe block a precise, valid// SAFETY:justification. Do not claim that an issued handle is necessarily still valid after removal or reuse.
For the repaired unsafe lookup, require that the handle currently resolves
to an occupied slot with its generation. The supplied script establishes
this with get(h).is_some() and performs no mutation before the unsafe call.
A bounds check only establishes what it actually checks; it is not evidence
for unrelated invariants or a claim that the unchecked version is faster.
A drain must tolerate being forgotten
mem::forget is safe and
does not run the forgotten value's destructor. Unsafe implementations must
remain memory-safe when a caller uses it.
In this starter, Drain::next replaces each yielded slot with Vacant, so
the arena does not retain an uninitialized or already-moved string there.
Forgetting an exhausted guard leaves the count wrong. This is different
from an implementation that leaves accessible pointers to moved-out values.
The reference removes all occupied values when drain is called, advances
their generations, and resets live before returning a guard. The guard
owns an iterator over the removed strings. Dropping it disposes of any
unconsumed strings; forgetting it can leak that iterator's allocation and
remaining strings, but leaves the arena empty and usable. Test partial
iteration as well as the script's fully consumed forget command.
Check your understanding What does “leak amplification” mean in a draining container? Show answer Hide answer
It is a safety strategy in which forgetting a guard can leak additional resources instead of leaving invalid data accessible. The Rustonomicon's drain example explains why a container must establish a safe state before relying on later cleanup.
It is not the name of every drain bug, and it does not mean that all drains use the same implementation. This arena moves removed values into a separate vector. Its safety argument should describe that code rather than assume it matches the standard library's implementation.
What the grader checks
The eight scripts include three ordinary-use examples and five adversarial examples. The latter exercise stale reuse, rejected allocation, forgetting a drain, an index equal to the slot count, and lookup in an empty arena. They are not one test for each of the five issues: the final two both probe the bounds defect. Clippy separately checks the missing safety documentation.
Correct output is not a proof of soundness. Undefined behavior may appear to work, and this exercise does not run Miri. The scripts also cannot prove that safety comments are true, inspect every public API call sequence, or establish behavior across generation wraparound.
Keep the public method signatures and the script unchanged. Preserve the
intended command results above, including the ordinary-use examples. Do not
silence lints with #[allow(...)]. Review each unsafe block's bounds,
initialization and lifetime assumptions, and state the caller obligations
of the unsafe method. Those review requirements are part of the exercise,
even where output tests cannot enforce them.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.