A binary decoder allocates a buffer from the four-byte length prefix it just read. What does that enable?
answer
- the prefix is part of the payload
- four bytes buy a gigabyte
- a claim, not an authorisation
- bound it, then grow from arrivals
- aggregate the per-field caps too
basics
~20 sA few bytes declaring four gigabytes make the server reserve four gigabytes, and the sender never has to produce them. A declared length is a claim about the stream, not authorisation to reserve memory on the sender's behalf.
solid answer
~40 sLength-prefixed framing is everywhere: a tag-length-value record, a framed message on a socket, a string header inside a binary encoding. The prefix is written by whoever wrote the bytes, so treating it as an allocation size hands the sender a memory-allocation primitive costing them four bytes per gigabyte. The rule has two halves. First, compare the declaration against a cap **you** chose and against the bytes that remain in the framed message, and reject before reading any payload if it fails. Second, build the value from bytes that have **actually arrived**: grow incrementally, or read into a bounded window, so a stream that ends early costs you what it delivered rather than what it promised. Truncation is then an ordinary parse error, not a half-filled buffer you already paid for.
code
pseudocode · 20 linesdeclared = read_uint32() // written by the sender
if declared > MAX_FIELD_BYTES:
reject("declared length above field cap")
if declared > bytes_remaining_in_frame():
reject("declared length exceeds the frame")
buffer = new growable_bytes()
remaining = declared
while remaining > 0:
chunk = read_at_most(min(remaining, 64 * 1024))
if is_empty(chunk):
discard(buffer)
reject("stream ended before the declared length")
append(chunk, to = buffer)
remaining = remaining - length(chunk)
message_total = message_total + declared
if message_total > MAX_MESSAGE_BYTES:
reject("message total above cap")
return buffergo deeper
Remember that a length written inside a message was chosen by whoever sent it, so it cannot be used as a reason to reserve that much memory.
Explain the asymmetry: inflating a declaration costs the sender four bytes while honouring it costs the server the whole allocation, and describe growing from bytes actually received instead.
Name the places this hides in a real decode path — element counts, declared uncompressed sizes, reassembly buffers — and show how per-field caps aggregate into a message and a fleet budget.
Frame it as an invariant the platform owns: decode memory is a function of bytes received and of constants you chose, and decide how that invariant is reviewed wherever a new wire format enters the system.
## What a length prefix is, and who writes it Binary encodings need to know where a value ends. Rather than scanning for a terminator, most of them declare the length up front: a tag-length-value record, a framed message on a stream, a string or byte-array header inside a self-describing binary format, a chunk header in a container. The decoder reads the length, then reads that many bytes. The length is part of the payload. On an upload endpoint, the payload came from the sender. So the prefix is a **claim**, exactly as untrusted as the bytes after it. ## The attack, stated plainly Send a message whose prefix declares 3 GiB and then send nothing more. A decoder that reserves from the declaration allocates 3 GiB against a request of a few bytes. Repeat on a handful of connections and the service is out of memory without the attacker ever transmitting anything of consequence. The asymmetry is the point: the cost to attack is four bytes, the cost to defend is the whole allocation. Notice what makes this different from an oversized body. An oversized body has to be **sent**, so it is bounded by the attacker's bandwidth and by any byte cap at the edge. A declared length costs nothing to inflate and passes a byte cap trivially, because the request really is tiny. ## The rule Two conditions, both required: 1. **Bound the declaration.** Reject any prefix above a maximum you chose for that field or message, and reject any prefix larger than the bytes remaining in the enclosing frame. This check is free and it happens before a single payload byte is read. 2. **Allocate from arrival, not from declaration.** Build the value from bytes that have actually been received — grow a buffer in bounded chunks, or copy through a fixed window — so that a stream which stops early costs you what it delivered. Once both hold, the declaration becomes useful rather than dangerous: it lets you reject early and it lets you size a single allocation, because the number is now bounded by your cap and by what is really there. ## The same mistake in other clothes - **A declared element count** used to pre-size a collection, or worse, to construct that many empty objects. - **A declared uncompressed size** in a compressed container's header, used to allocate the output buffer. - **A declared field count or dictionary size** in a schema-driven binary encoding. - **A reassembly buffer** sized from a declared total across multiple frames or chunks. - **A declared string length in characters** multiplied by a worst-case bytes-per-character factor — the multiplication makes a small lie bigger. Each one is the same shape: a number under the sender's control becomes a size under your process's cost. ## Doing it properly, step by step 1. Read the prefix. 2. Reject immediately if it exceeds the per-field cap, or exceeds the remaining bytes of the enclosing frame. 3. Read in bounded chunks, appending to a buffer that grows with what arrives. 4. Treat an early end of stream as a parse error and discard the partial buffer. 5. Keep a running total across all fields in the message, so a thousand individually legal fields cannot add up to an illegal message. Step 5 is the one most often missed. Per-field caps that never aggregate let a message of many small legal values exceed any budget you thought you had. ## The budget is not per request Even a correct per-message cap is a **price per connection**, not a bound on the service. If the cap is 8 MiB and a thousand decodes run at once, the honest worst case is 8 GiB. Bound the number of concurrent decodes, or hold a shared byte budget that each decode draws from and returns, so the fleet-level number is something you chose rather than something your connection limit chose for you. ## What good looks like in an interview answer Say the rule as a property, not a recipe: *the memory a decode can consume must be a function of bytes actually received and of constants I chose, never of a number the sender wrote.* Every concrete defence above is a consequence of that sentence, which is why it survives a change of format, framing or ecosystem.
- Is the declared length ever safe to use directly?Yes, once it is bounded. Use it first to reject early, before a payload byte is read, and then — when it sits under a cap you chose and within the bytes remaining in the frame — to size a single allocation. The property that must never break is that the allocation is bounded by a constant you picked and by what has actually arrived, not by the sender's number alone.
- Why is a correct per-message cap still not enough?Because it prices one decode rather than bounding the service. A cap of 8 MiB with a thousand concurrent decodes is 8 GiB of honest worst case. Bound the number of simultaneous decodes, or give them a shared byte budget each one draws from and returns, so the aggregate figure is a number you chose.
- How does this rule read in a self-describing text encoding with no length prefixes?The same property applies with different counters. Without a prefix nothing is declared in advance, so the danger shifts from over-allocation to unbounded accumulation: a string or array that simply keeps going. You bound it with a maximum scalar length and a maximum element count tested as the value grows, which is the identical rule — memory as a function of bytes received and constants you chose.
A caller who books a table for four hundred has not brought four hundred people. You seat the ones who walk through the door, and you never clear the room in advance on the strength of the booking.
saying these in an interview costs you the question
- Trusts the declared length because the framing layer produced it
- Allocates first and validates the declared length afterwards
- Thinks a four-byte length field is self-limiting because it is small
- Sizes a collection from a declared element count in the header
- Caps each field but never the message total
- Assumes a truncated stream is harmless once the buffer is zeroed