Why can a program built under a compile-time ownership discipline still leak heap memory with no unchecked code?
answer
- validity of access, not size of the live set
- still owned and still reachable
- release rides on the owner's scope exit
- a long-lived owner has no scope exit
- leaking is safe, so nothing rejects it
basics
~20 sA leak is memory that is still owned and still reachable — a cache that never evicts, a registration nobody cancels. The checks prove every access is valid; they say nothing about how long an owner chooses to keep a block.
solid answer
~40 sMemory safety is a property of accesses: no read of a released block, no reference outside a live buffer, no second release. A leak breaks none of those. The block has exactly one healthy owner, every access to it is valid, and the only problem is that the program will never use it again and will never drop the owner. Release fires when the owner goes out of scope, so a block owned by a long-lived structure — a session map, a subscriber list, a pool that only grows — has no scope exit coming. The checker would have to know the program's intent to object, and it does not model intent. The practical consequence: a compile-time discipline moves leaks from a memory-correctness problem to a capacity-planning problem, which is where they stay.
code
pseudocode · 8 linescache = new map() // the map owns every value it holds
on request(key):
if not cache.contains(key):
cache.put(key, parse(load(key))) // ownership moves into the map
return borrow(cache.get(key)) // non-owning view, lifetime-checked
// no eviction anywhere: every entry keeps one valid owner forevergo deeper
Two different questions hide behind the word safe: is every access valid, and is the memory given back. A compile-time discipline answers only the first.
Explain the mechanism: release is attached to an owner leaving scope, so a block owned by a process-lifetime structure is scheduled for release at process exit and nothing is wrong with that program.
Show the operating consequence: because no tool will flag it, every long-lived container needs a designed bound, and a climbing live set across full workload cycles is the signal you watch.
Frame it as where the guarantee leaves off and capacity planning begins. Decide what the team owns by convention — bounds on shared containers — versus what the toolchain proves, and budget accordingly.
The strongest claim a compile-time ownership discipline makes is about **accesses**: every read and write reaches a block that is currently alive, and the release of that block happens exactly once, by its single owner, at a point the compiler can name. A leak violates none of that, which is why the checker is silent about it. ## What the checks actually prove Unpack the guarantee into its parts and the gap is obvious: | Property | Who decides it | What it forbids | |---|---|---| | Validity of an access | the checker, before the program runs | reading or writing a released or out-of-scope block | | Release exactly once | the checker, from single ownership | a second release, or none at all | | Promptness of release | the program's structure — where the owner lives | nothing automatically | | A bounded live set | the program's design: an eviction rule, a limit | nothing automatically | The first two rows are proved. The last two are design decisions the checker has no vocabulary for. It can answer "is this access valid?"; it cannot answer "should this still be held?", because that question is about intent. ## Why the rule that reclaims memory does not fire Release is attached to an **owner going out of scope**. That is what makes the discipline deterministic and cheap: no runtime tracing, no counts, no pauses. But it means the timing of a release is exactly the lifetime of its owner. Put the owner inside a request handler and the block dies with the request. Put the same block inside a map that lives as long as the process, and its release is scheduled for process exit. Both programs are accepted; one leaks. ## The shapes that leak in fully checked code - **An unbounded lookup table.** Entries are inserted and never removed. One owner, every entry valid, live set climbing for days. - **A forgotten registration.** A component registers itself with a long-lived dispatcher and is never deregistered. The dispatcher owns it; nothing else has to. - **A pool or arena that only grows.** Blocks returned to a pool are reusable, but they are not returned to the allocator; a burst sizes the pool permanently. - **Over-retention.** Keeping a whole decoded frame alive because something holds one small field out of it. Nothing is invalid; the footprint is simply many times what the program needs. - **Deferred cleanup nobody drives.** A queue of items awaiting a drain step that is never called on some path. A related case belongs elsewhere: a cycle of shared counted handles that keeps itself alive is a property of counted shared ownership, not of single ownership. Single ownership admits no cycle of owners at all, because a cycle would need some block owned twice. ## The one leak class the discipline does remove Hand-written release calls skipped on an early return or while an error propagates were a large share of real leak reports in manually managed code. Scope-bound release removes that variant outright: the release is attached to the exit of the owner's scope, and every exit path runs it. So the honest statement is that the discipline removes the *accidental* leak on an error path and leaves the *deliberate* retention untouched. ## What to do instead Because the checker will not raise it, the bound has to be designed in: 1. Give every long-lived container an explicit eviction policy — a size cap, an age cap, or both — and treat an unbounded one as a defect in review the way you would treat a missing timeout. 2. Prefer storing an identifier over storing an owning handle where a registry should not decide the object's lifetime; let the real owner decide when it goes. 3. Watch the live set across a full workload cycle rather than a peak, since a leak is a trend, not a value. Ecosystems differ in what they hand you here — some ship a leak-detection mode that reports blocks still owned at exit, some do not — but none of them can turn intentional retention into a compile error, because at the language level it is indistinguishable from a cache doing its job.
- Would a runtime tracing collector have caught this instead?No. A collector reclaims what nothing can reach, and every entry in a growing table is reachable from a root. The same program leaks under a collector, under counted handles and under compile-time ownership. Only the release mechanism differs; reachable-but-useless memory defeats all three.
- Is over-retention really a leak if the memory is eventually released at exit?Operationally, yes. A leak is any retention that grows with work and is never needed again; whether it is reclaimed at process exit is irrelevant to a service that must survive for weeks inside a fixed limit. The symptom is a live set that climbs across cycles rather than returning to a floor.
saying these in an interview costs you the question
- Claims a memory-safe program cannot leak
- Says the compiler will warn about an unbounded cache
- Confuses a leak with a released block being read again
- Thinks release timing is decided by memory pressure rather than the owner's scope
- Believes switching to a runtime collector would remove this leak