A pipeline stage moves its chunk buffer onward and then reads the length from the old handle — what is wrong?
answer
- the handle gave away what it names
- not the same as use-after-free
- compile error, empty value, or dangling
- zero length is a plausible lie
- capture the length before the transfer
basics
~20 sThat 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 sAfter 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// 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 valuego deeper
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.
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.
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.
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