Artifact 883c931a4ca9e11e9d893b56df451f3436d728adeff68a2463358be2885ed04b:
- File
docs/RELEASE-NOTES-0.3.29.md
— part of check-in
[362f18d357]
at
2026-09-23 18:33:13
on branch trunk
— inflate: a truncated dynamic-block header is the end of the stream, not a crash
tossBitsShort added the consumed bits where it must subtract them, so a header ending one byte short tossed past the end of the input and the next peek read a wrapped length off the stack. Found by the nightly soak four nights running. Release 0.3.29. (user: dmestas, size: 2826) [annotate] [blame] [check-ins using]
# zit 0.3.29 zit 0.3.29 fixes a crash in the zlib decoder that every sync request and reply passes through. Any peer could trigger it with 32 bytes. No artifact, tag namespace, sync message, or on-disk repository format changed. ## A truncated dynamic-block header no longer crashes the decoder The nightly fuzz soak failed four nights running on the same target, `sync_container_round_trip`, each time with a different random seed and each time as a bare segmentation fault. The cause was in the inflate zit carries from Zig 0.16's standard library, and it's the same kind of slip as the one the soak found there a week ago: end-of-input bit accounting. Before tossing a code-length symbol, `tossBitsShort` checks that enough bits remain. It computed them as `bufferedLen() * 8 + consumed_bits`, adding the bits already consumed from the current byte where it has to subtract them. With one byte left and six of its bits consumed, a seven-bit toss passed the check and advanced the reader past its end. The next peek sliced `buffer[seek..end]` with `seek` past `end`, got a length of 2⁶⁴−1, and read up the stack until it hit an unmapped page. Random garbage reaches it: the soak's input was 32 bytes whose dynamic-block header ends one byte short. A peer could send the same bytes as a sync request or reply. The check is now written without subtraction, like the earlier fix, so a short header is just the end of the stream and `decode` returns an error. The regression test is the soak's own input. It panics a Debug build without the fix and passes with it. All four nights' seeds pass at the soak's round count. The vendored file's header now records both fixes. A note on why it was hard to see. A unit test on the captured bytes passed at first, because it used a 1 MiB output cap and these bytes declare 287 MB, so `decode` refused them before the inflate ran. The fuzzer uses the protocol's own reply cap. The test now admits the declared size. ## The soak stops at its first failure The four failures also hid something. The soak stops at the first failing target, and `sync_container_round_trip` is sixteenth of twenty-eight. The twelve after it, including `transport_frame_garbage`, which drives this same decoder, and `serve_request_garbage`, got no deep run for four nights. A full soak was dispatched with this release to cover them. ## Compatibility - No on-disk repository or wire format changed; Fossil RFC revision 00 remains the target. - A container whose zlib stream ends partway through a dynamic block header is now refused with an error instead of crashing the process. ## Qualification `zig fmt --check`, `zig build test`, `e2e`, `smoke`, `git-import-e2e`, `git-export-e2e`, `dst`, and every fuzz target at 256 rounds, plus the three-runner CI battery and a full-depth soak.