.
C Thirty-two\sbytes\sand\sa\sserver\sgoes\sdown
D 2026-09-26T17:56:41.426
E 2026-09-23T20:31:21 42e5a2289aee837717e2eb9272f6f6b82925ec73
N text/x-markdown
U dmestas
W 2355
# Thirty-two bytes and a server goes down
0.3.29 fixes a crash in the zlib decoder that every sync request and reply
passes through. Before it, a single anonymous 32-byte POST to `/xfer` took
down a `zit serve` process: no login, no valid project code, nothing but the
bytes. Upgrade any server running 0.3.28 or earlier.
It surfaced as a nightly soak that failed four nights running, each time on
the same fuzz target, `sync_container_round_trip`, each time with a different
seed, and each time as a bare segmentation fault with no stack trace. The
changing seed was the first clue that this was not one unlucky input: the
soak draws fresh seeds every night, and this bug was common enough that any
seed reached it within a quarter of a million rounds.
The crash was not where a stack overflow would put it. At the fault, the
decoder's input reader had its read position at 29 and its end at 28. A
slice from 29 to 28 has a length of 2⁶⁴−1, and the next read walked up the
stack until it met the unmapped page above it.
What moved the position past the end was a check before skipping a few bits
of input. The inflate zit carries is the one from Zig 0.16's standard
library, and its `tossBitsShort` asked whether enough bits remained as
`buffered bytes × 8 + bits already consumed`. The consumed bits have to be
subtracted, not added. With one byte left and six of its bits used, a
seven-bit skip passed the check and stepped off the end. It was the same kind
of mistake the soak had found in the same file a week earlier: arithmetic at
the end of the input. Both checks are now written with no subtraction, and
those two are the only guards on the end of input in the decoder.
The regression test is the soak's own input, and it took a detour to make it
honest. The first version passed without the fix, because it gave `decode` a
1 MiB output cap and these bytes declare 287 MB, so the input was refused
before the inflate ever ran. The fuzzer uses the protocol's real cap. With
that, the test panics a Debug build without the fix and passes with it.
One thing the four red nights also hid: the soak stopped at its first
failure, and this target is sixteenth of the sweep, so the twelve after it
went four nights without a deep run. A full soak after the fix covered them,
all clean, and the soak now records a failure and keeps sweeping.
Z cb69d68bec7bf67d4b33b35e9b155776