← Writeups

The sibling a same-day public fix missed: four memory-safety bugs in zchunk

3 Aug 2026 coordinated-disclosurezchunkfuzzingvariant-analysismemory-safety

TL;DR: Fuzzing zchunk — the chunked-file format behind efficient delta downloads in the Fedora/DNF ecosystem — turned up four memory-safety bugs in the parsing of untrusted .zck files. I reported them privately. That same afternoon, an independent contributor opened a public PR fixing one of the four. It was a good fix — for exactly one of two sibling over-reads sitting a few lines apart. The maintainer’s combined patch caught both and shipped in zchunk 1.5.4. The story here isn’t the bugs; it’s how a narrow fix leaves a matching hole one field over.

The findings

zchunk splits a file into content-defined chunks so a client can download only the parts that changed. The header carries an index of chunk offsets, sizes, and digests; a .zck file is entirely attacker-controlled input to anything that reads one. Fuzzing the header and streaming-read paths (AFL++ with AddressSanitizer/UBSan) surfaced four issues:

A fifth, low-severity item — two zero-length memcpy() calls with a NULL-derived pointer — rode along as a UBSan note.

None of these are exotic. They’re the ordinary shape of hand-written binary parsers: a length field trusted one step too far, a bounds check that guards the first read but not the second, a count that’s believed without being verified.

The same-day collision

I sent the report to maintainer Jonathan Dieter on 22 July. Five days later, as I was checking the live tracker before a follow-up, a public pull request — #117, from an independent contributor — appeared, fixing the read_preface() over-read. Same bug, found independently, out in the open.

That’s an awkward moment in coordinated disclosure: part of your private report is now public, filed by someone you’ve never spoken to. It’s also a useful forcing function — it tells you the bug is real and reachable enough that two people found it, and it puts a clock on the rest of the cluster.

The sibling that nearly shipped

Here’s the part worth keeping. PR #117 fixed read_preface() cleanly. But the index_read() over-read is its structural sibling — the same “check room for the first digest, forget the second” mistake, a few lines away, in a function the public PR never touched. A fix scoped to the one bug someone tripped over would have merged, closed the issue, and left a matching out-of-bounds read live in the next function down.

This is the entire argument for variant analysis, and for reporting the cluster rather than the single crash your fuzzer happened to land on. The fuzzer found one over-read; reading the code around it found the twin. When you disclose, you’re not just handing over a PoC — you’re handing over the pattern, so the fix can be drawn around the whole shape of the mistake instead of the one instance that surfaced.

The combined fix, and the release

Jonathan confirmed the reports the same day and took the broader route. His header patch covers both over-reads — the read_preface() one and the index_read() sibling PR #117 missed — plus the NULL-deref (by rejecting an index whose parsed entry count is zero or disagrees with the declared count) and the range write. It touches four files and supersedes #117’s scope. He released it as zchunk 1.5.4 on 2 August, and #117 was closed as covered.

I did the last step the process asks for: rebuilt the 1.5.4 release from source — no local patches — and replayed every original PoC under ASan/UBSan. All clean; the header, index, range, and zero-length paths all clear. Fixed, verified, closed.

Takeaways

Full technical detail is in the advisory. Thanks to Jonathan Dieter for a fast, collaborative turnaround.