skip to content

A telemetry routine returns a reference to a record held in its own frame; why is that reference invalid after the return?

level: middleimportance: should knowfreq 50%

answer

  1. frames die at the return
  2. released is not the same as cleared
  3. the next call reuses the region
  4. validity follows the call structure
  5. copy out, or let the caller supply storage

basics

~20 s

The frame is released at the return, so the storage the reference names is no longer reserved for that record and the next call may claim it. The reference is invalid from the return onwards, even though reading through it often still succeeds.

solid answer

~50 s

Frames are released in strict last-in-first-out order, so a call's storage stops being reserved the moment it returns. A reference into that frame names storage the routine no longer owns: the next call pushes its frame over the same region and writes its own parameters and locals into those bytes. The reference is invalid **at the return**, not later — its validity is decided by the call structure, not by who still holds it. What makes this hard to catch is that released stack storage is not cleared, so the first read through the reference usually shows the expected value and only starts returning another call's data once something reuses the region. The fix is to move the record out of the frame: return it by value, have the caller supply the storage, or put it where the call structure does not decide its lifetime.

code

pseudocode · 9 lines
pseudocode
function readSensor()
    local buffer                  // lives in this call's frame
    buffer.temperature = sample()
    return referenceTo(buffer)    // frame is released on this return

a = readSensor()
print a.temperature               // often still correct - nothing reused it yet
b = readSensor()                  // this frame covers the same region
print a.temperature               // now reads whatever the second call wrote

go deeper

for a junior

Recall that a local lives in the frame of its call and that the frame is released when the routine returns. A route into it is not something the caller may keep.

for a middle

Explain the two separate moments: the reference becomes invalid at the return, but the symptom waits for the next call to reuse the region. That gap is why the defect looks intermittent.

for a senior

Diagnose it in the field. Expect corruption whose shape follows the call pattern rather than the data, and check for references that escaped a frame by being returned, stored, or captured by something registered mid-call.

for a principal

Decide the convention that removes the class: who owns produced storage, whether callers supply buffers on hot paths, and what your review and tooling must catch so the answer does not depend on each author remembering the rule.

The stack's bargain is that a frame is created by entering a call and destroyed by returning from it, in strict last-in-first-out order. Allocation is a marker moved by the frame's size; release is the marker moved back. That is why calls are cheap. The price is the subject of this question: **the storage is reclaimed on the return whether or not anybody still holds a route into it.** ## The setting A device driver calls a telemetry ingest routine once per sensor reading. Suppose the routine declares a record as a local, fills it from the sample, and hands the caller a reference to it rather than a copy. The record lived in this call's frame. At the return, that frame is released. ``` function readSensor() local buffer // in this call's frame buffer.temperature = sample() return referenceTo(buffer) // the frame is about to go ``` ## What "released" actually means It does not mean erased. Releasing a frame moves a marker. The bytes are untouched, still holding the record exactly as the routine left them — until something reuses the region. So the failure has two distinct moments, and confusing them is what makes this defect survive testing: 1. **The moment the reference becomes invalid** — the return. From here the program has no claim on that storage and no guarantee about its contents. 2. **The moment the symptom appears** — the first time another call pushes a frame over the same region and writes its own parameters, locals or temporaries into those bytes. Between the two the program reads the expected value and looks correct. The gap's length depends on what the program happens to call next, which depends on the input and on timing, which is precisely why this class of bug reproduces unreliably. ## Why the next call lands on exactly that storage Because the discipline is last-in-first-out and the marker was moved back to where it stood before the call. The very next call at that level starts its frame at the same place. In a loop that calls the routine repeatedly, the second call's parameters often occupy the same bytes the first call's record did: - `a = readSensor()` — the reference names released storage. - `b = readSensor()` — this call's frame covers that storage and overwrites it. - Reading through `a` now yields whatever the second call put there, which is not a record at all, merely the bytes of one. ## What is and is not safe to return | What the routine returns | Safe after the return? | Why | |---|---|---| | A copy of the record's value | Yes | The value was copied out before the frame went; the copy lives in the caller's storage | | A reference to a local | No | The storage is released at the return | | A reference to storage the caller supplied | Yes | That storage belongs to a frame still live, or to storage outside the stack entirely | | A reference to storage whose lifetime is not tied to the call | Yes | Nothing about the return releases it | The general shape of the fix is to stop letting the call structure decide the record's fate. Either copy it out, or have the caller provide the storage and let the routine fill it — the pattern the driver in this example was using all along when it handed the routine a record to fill. ## Related failures with the same root - **A reference stored during the call and used afterwards.** The routine writes a reference to one of its locals into a structure the caller keeps. The return invalidates it just as surely as returning it would. - **A reference passed to something that outlives the call**, such as a handler registered mid-call that runs later. - **A reference into a frame further down the stack** held after the call chain unwinds past it. All three are the same statement: a route into a frame is only meaningful while that frame is live, and a frame is live exactly from its call to its return. ## What an interviewer is checking They want to hear the mechanism, not the slogan. A strong answer says three things in order: the frame is released at the return; released storage is not cleared, so reads keep appearing to work; and the next call reuses the region, which is when the data changes underneath the reference. A candidate who says only "it is a dangling reference" has named the symptom; a candidate who says "the memory is wiped, so it would fault" has the mechanism backwards and will be surprised by every real occurrence of it.

  • What is the safe way to hand a record produced inside a routine back to its caller?
    Copy the value out as the return result, so what survives is in the caller's own storage. Or invert the arrangement: let the caller pass in a record and have the routine fill it, so the storage belongs to a frame that is still live. Or place the record in storage whose lifetime is not tied to the call at all.
  • Why does this defect so often pass every test and then fail in production?
    Because the reference only misbehaves once something reuses the released region, and what gets called next depends on the input, the branch taken and the timing. A small test that reads the value immediately sees the stale-but-correct bytes. A busier path calls something in between and overwrites them.
  • Is storing a reference to a local into a long-lived structure during the call any safer than returning it?
    No — it is the same defect with a longer fuse. The frame is released at the return regardless of where the reference was put, so the stored route becomes invalid at exactly the same moment. It is usually worse in practice, because the use is now far from the routine that created it.

saying these in an interview costs you the question

  • Says the data is safe because reading it still shows the right value
  • Believes released frame storage is wiped, so a read would fault
  • Thinks the reference goes bad only after some later allocation
  • Assumes returning a copy of the record has the same defect
  • Claims the problem appears only for large records