skip to content

Which memory-bug classes does a compile-time ownership discipline make unrepresentable rather than merely rarer?

level: middleimportance: must knowfreq 66%

answer

  1. safety is about access, not amount
  2. one owner, one release duty
  3. a move invalidates the source handle
  4. borrows checked against the owner's lifetime
  5. leaking is memory-safe, so it stays

basics

~20 s

Use-after-free, double free, and dangling references into a moved or resized container become unrepresentable: single ownership plus lifetime-checked borrows reject them before the program builds. Leaks and logic errors are untouched, because keeping memory reachable is safe.

solid answer

~50 s

Three classes go away by construction. Every heap block has exactly one owner, so the release duty exists once and cannot be discharged twice — that is double free gone. A move transfers that duty and invalidates the source handle, so reading through the old handle is a compile error rather than a use-after-free. Every non-owning reference is checked against the owner's lifetime, so a reference into a buffer that is dropped, moved or reallocated stops compiling instead of dangling. Two things remain: a leak — memory still owned and reachable that the program will never use again — and ordinary logic bugs. Leaking is memory-safe, so the checker has no reason to object. The guarantee covers the validity of every access, not the size of the live set. It holds for the code the checker sees, not for regions that explicitly suspend the checks.

code

pseudocode · 7 lines
pseudocode
buffer = allocate(4096)          // buffer owns the block
parser = make_parser(buffer)     // ownership MOVES into parser

write(buffer, 0, 1)              // REJECTED: buffer was moved out of

release(parser)                  // the single owner releases, once
release(parser)                  // REJECTED: parser was moved into release

go deeper

for a junior

Hold on to the split: the compiler proves that every access reaches live memory, not that memory is given back promptly. A program can be entirely memory-safe and still grow without bound.

for a middle

Name the rule behind each eliminated class — single ownership for double free, move invalidation for use-after-free, lifetime-checked borrows for dangling interior references — and then say out loud which classes survive.

for a senior

Show where the guarantee stops in a real codebase: regions that suspend the checks, foreign boundaries, and long-lived containers. Those are the places a review still has to read line by line.

for a principal

Judge a migration by which tracker rows actually close. Classes the checker proves gone stop consuming review effort; leaks and protocol logic still need budget, so the case rests on the class mix you actually have.

A compile-time ownership discipline is a small set of rules a checker enforces before the program runs: every heap block has exactly one **owner** at a time, the owner releases the block when the owner itself goes out of scope, **moving** a value transfers that release duty and invalidates the source handle, and any non-owning **borrow** is valid only inside a **lifetime** the checker can see is contained in the owner's. These are not style advice — a program that breaks them does not build. The interview question is never the slogan ("it is memory-safe"); it is the boundary, so the useful form of the answer is a ledger. ## The ledger, one row per class | Bug class | What it looks like | Status in code the checker sees | |---|---|---| | Use-after-free | a handle is read after the block it named was released | unrepresentable | | Double free | two paths each release the same block | unrepresentable | | Dangling interior reference | a reference into a buffer that was moved, reallocated or dropped | unrepresentable | | Leak on an error path | an early return skips a hand-written release call | removed, by a different rule | | Unbounded retention | an owner deliberately keeps a block forever | untouched | | Protocol or logic error | the parser accepts a malformed frame | untouched | **Unrepresentable** is meant literally: there is no accepted program text that exhibits the class. The qualifier matters as much as the claim — it is bounded to the code the checker sees. A region that explicitly suspends the checks, and any boundary where control leaves for code with no such discipline, sit outside the proof. ## Which rule kills which class 1. **Single ownership kills double free.** The obligation to release lives with exactly one handle. There is no second handle holding the same duty, so there is no second release to write. Contrast this with a runtime allocator that keeps a record of freed addresses: that *detects* a second release, usually by aborting, which is a very different product than not being able to express one. 2. **Move invalidation kills use-after-free.** Passing a value on does not duplicate the duty; it relocates it. The source handle is marked unusable from that point in the program text, so a later read through it is rejected. Note the shape of the failure: the block may still be perfectly alive — the rejection is about which handle owns it, not about whether the bytes survived. 3. **Lifetime-checked borrows kill dangling interior references.** A reference into the middle of a buffer is only accepted where the checker can prove the owner outlives it. That covers the case reviewers miss most often: a buffer that grows, reallocates and abandons its old block while a reference into the old block is still held. ## What survives, and why it must - **Leaks.** A block that is still owned and still reachable is, from the checker's point of view, perfectly healthy. A lookup table that only ever inserts, or a registration nobody ever cancels, grows the live set without a single invalid access. - **Logic errors.** Parsing the wrong field, accepting a frame that should be refused, mis-sizing a length prefix — none of these are memory errors. - **Deliberate holding.** Keeping a whole decoded frame alive to retain a twelve-byte header is a design defect the checker happily accepts. - **A changed failure shape, not an absent one.** Where the old code would have read past the end of a buffer and corrupted something, a bounds-checked access gives a deterministic stop instead. The class of mistake is still writable; its consequence changes from silent corruption to a loud, local failure. ## What this means for a migration review Reading a rewritten binary-protocol parser beside the old defect tracker, the top three rows close by construction and need no further reasoning: every one of them was a statement about which handle was valid when. The remaining rows still need a human. That is the honest pitch — the discipline does not make the code correct, it removes a category of reasoning from review so the review budget can go somewhere else. It is worth saying plainly what the comparison with a runtime collector is. A tracing collector also removes use-after-free and double free, because the program never releases anything explicitly and reclamation happens only when nothing can reach the object. A collector does not remove leaks either — a reachable object is live by definition. The difference is *when* the answer is computed and what it costs: statically, with no runtime bookkeeping and no pauses, versus at run time with headroom and pause time. Ecosystems differ sharply in which of the two they offer and whether they offer an escape hatch at all.

  • Does a runtime tracing collector remove the same three classes?
    It removes use-after-free and double free too: the program never releases explicitly, and reclamation waits until nothing can reach the object. It does not remove leaks, since a reachable object is live by definition. The difference is when the answer is computed and what it costs — statically, with no bookkeeping, versus at run time with headroom and pauses.
  • Does the leak-on-an-error-path class disappear too?
    The hand-written-release variant does. Release is attached to the owner leaving scope, so it runs on an early return and while an error propagates — exactly where manual release calls were skipped. What survives is memory the program still owns on purpose, which no scope exit will reclaim.
  • What happens to these guarantees inside a region that suspends the checks?
    Both classes are representable again there, and the author restates the invariants by hand. The value is containment: the unchecked surface is small, explicitly marked and easy to locate, so review concentrates on it. A bug written there can still surface anywhere, which is why such a region must present a sound interface.

A library where every book has exactly one borrower card and handing the book on transfers the card. Nobody can return the same book twice, and nobody can read a book they no longer hold the card for — but a borrower who simply never returns anything is breaking no rule.

saying these in an interview costs you the question

  • Claims the discipline also prevents memory leaks
  • Says a growing cache is something the compiler catches
  • Thinks a second release is caught at run time, not rejected at compile time
  • Believes compile-time ownership rules out every crash, including logic errors
  • Says use-after-free becomes merely less likely rather than unrepresentable
  • Assumes regions that suspend the checks inherit the same guarantee