Skip to content

← The Expert Edge: Idiom, Review and Capstones step 9 of 14

Hard End-to-End

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

  1. Lookup bounds. get rejects 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.
  2. Slot reuse. Removing a value must advance its generation. Otherwise an old handle can unexpectedly retrieve the next value in that slot.
  3. Failed allocation. A full arena must reject allocation without incrementing live. Change the count only after inserting a value.
  4. Drain cleanup. The starter restores live in 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.
  5. Unsafe documentation. Add a # Safety section to get_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

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.