skip to content

A pipeline stage moves its chunk buffer onward and then reads the length from the old handle — what is wrong?

level: seniorimportance: must knowfreq 54%

answer

  1. the handle gave away what it names
  2. not the same as use-after-free
  3. compile error, empty value, or dangling
  4. zero length is a plausible lie
  5. capture the length before the transfer

basics

~20 s

That is a use-after-move: the old handle no longer owns the buffer it is being asked about. Disciplines that prove ownership statically reject the read outright; where it compiles, it returns the moved-from empty value or worse, which is a silent data bug.

solid answer

~40 s

After the transfer, the old handle owns nothing, so asking it about the buffer asks about a claim it gave away. Where ownership is checked at compile time, the read simply does not compile — the value is known to have left, and the error points at the transfer. Where the discipline is a convention rather than a proof, the source is typically left as a defined empty value, so the length reads as zero and the stage logs, frames or validates a chunk it believes is empty; that is worse than a crash, because the pipeline keeps running and writes wrong data. The fix is not a re-read: capture whatever you still need before the move, or have the receiving stage hand ownership back.

code

pseudocode · 8 lines
pseudocode
// broken: reads a handle that no longer owns anything
next_stage.accept(move(chunk))
record_metric("bytes_forwarded", chunk.length)   // moved-from: rejected, or reads 0

// fixed: capture first, then transfer
size = chunk.length                              // read while the handle still owns
next_stage.accept(move(chunk))
record_metric("bytes_forwarded", size)           // uses the saved value

go deeper

for a junior

Recall the rule rather than the taxonomy: once a value has been handed on, the old name is not a source of truth about it, so read what you need beforehand.

for a middle

Explain the three outcomes — a compile error, a defined empty value, or a stale address — and why only the last is a memory-safety bug while the middle one is a data bug.

for a senior

Recognise the production signature: payload metrics collapsing to zero while network counters look normal, and size-keyed branches taking the empty path on real chunks.

for a principal

Push the class out of reach structurally — transfers last in scope, reporting owned by the current holder, explicit hand-back in signatures — instead of relying on reviewers to spot the gap.

## What the bug is Use-after-move is the use of a handle that has already given away the thing it named. It is not the same bug as use-after-free — nothing has necessarily been released yet — but it belongs to the same family: a name is being trusted after the claim behind it has ended. In the pipeline, the shape is always the same. A stage transfers the chunk buffer to the next stage and then, a few lines later, still reaches for the old handle: to log the size, to update a metric, to decide whether this was the final chunk. ## What actually happens when you do it This is where disciplines genuinely differ, and an interviewer will want the distinction: - **Ownership proved at compile time.** The compiler tracks that the value left and rejects any later use of that name. The diagnostic points at the transfer and at the use; nothing reaches production. Reading a moved-from name is not a runtime hazard but a compile error. - **Moved-from left as a defined empty value.** The source is still a valid object, just an empty one. The length reads as **zero**, the pointer as nothing. The program runs, and the stage acts on a chunk it believes is empty — a truncated upload, a checksum over no bytes, a log line that says `0 bytes forwarded` for a 32 MiB chunk. Silent and consistent, which is what makes it expensive. - **Convention only, with no invalidation at all.** The source still holds the old address while the destination owns the block. Now the two really are a use-after-free and a double free waiting to happen, and the symptom depends on what the allocator has since done with the block. The common part: in no discipline is the moved-from handle a trustworthy source of truth about the buffer. ## Why it hides A use-after-move that compiles is hard to see in review because the two lines look independent, and it is hard to catch in testing because it is often **quiet and self-consistent**: 1. The empty value has no invalid representation — zero is a plausible length, so no assertion trips. 2. The wrong value flows into telemetry, not into a crash, so dashboards show a fleet quietly reporting zero-byte chunks. 3. Small test payloads make the symptom indistinguishable from a legitimately empty final chunk. 4. The distance between the transfer and the use grows over time, as code is inserted between them. ## How to make it impossible - **Read what you need before you hand the buffer over.** The length, the offset, the chunk index: these are small values; copy them into locals first, then transfer. This is the fix in the overwhelming majority of real cases. - **Give the receiving stage the job of reporting.** If the metric belongs to whoever holds the buffer, it should be emitted by the current owner, not by the previous one. - **Have the stage return ownership** when the caller genuinely needs the buffer again — an explicit hand-back is visible in the signature, unlike an assumption. - **Keep the transfer last in its scope.** If nothing follows the handoff, nothing can use the stale handle; this is a cheap structural rule that survives later edits. - **Reassign before reuse.** Where the discipline allows a moved-from name to be reused, treat it as uninitialised: give it a new value, never read it first. ## Diagnosing it in a running system When the compiler did not catch it, the signature in production is distinctive. Payload-size metrics collapse to zero or to a constant while throughput and network counters stay normal — the bytes are being uploaded, but the stage reporting on them is looking at the wrong object. Conditional logic keyed on size takes the empty branch: retry logic that skips empty chunks skips real ones, and last-chunk detection fires early. The test that proves it is a bisect on the handoff line, not on the payload: hold the payload constant and move the read to before the transfer, and the numbers come back. ## The interview point What separates a strong answer here is refusing to stop at 'it crashes'. A moved-from handle may be a compile error, a defined empty value, or a dangling claim, and only the third crashes. The middle case — well-defined, plausible, and wrong — is the one that ships.

  • Why is a moved-from handle that reads as an empty value more dangerous in production than one that crashes?
    A crash stops the process and names the line. An empty value is well defined and plausible, so the pipeline keeps running while metrics, size checks and last-chunk detection all act on zero. The defect spreads into data and dashboards instead of into a stack trace.
  • Is use-after-move the same bug as use-after-free?
    No. Use-after-move touches a handle whose claim has ended, while the block itself may still be very much alive under its new owner. It becomes use-after-free only in a discipline that leaves the stale address in place and the new owner then releases the block.
  • A stage needs the chunk back after the next stage finishes with it. What is the honest design?
    Have the next stage return ownership — take the handle by value and hand a handle back, so the transfer and the hand-back are both visible in the signature. Reaching for the old handle instead is an assumption the compiler cannot check and a reader cannot see.

saying these in an interview costs you the question

  • Says the source handle is fine because nothing was freed yet
  • Assumes every use-after-move crashes loudly
  • Treats a zero length from a moved-from handle as real data
  • Thinks ownership returns to the source when the call returns
  • Fixes it by re-reading the handle instead of capturing first