Skip to content

Add fuzz tests for LZ4FrameInputStream and LZ4BlockInputStream - #179

Open
Dominik-O22 wants to merge 1 commit into
yawkat:mainfrom
Dominik-O22:stream-fuzz-tests
Open

Dominik-O22 wants to merge 1 commit into
yawkat:mainfrom
Dominik-O22:stream-fuzz-tests

Conversation

@Dominik-O22

Copy link
Copy Markdown

Closes #122. The fuzz suite covered the block codecs and XXHash but not LZ4FrameInputStream or LZ4BlockInputStream, which is where recent bugs turned up.

LZ4InputStreamTest follows the issue's plan. The first 16 input bytes are ops, and the rest is the stream, raw with no round-trip. Each op picks read(), read(byte[]), skip() or available() and a size, and the ops repeat until the stream ends or fails, then keep going for 32 more calls. A run fails on:

  • any exception other than IOException
  • a result outside the InputStream contract, including a negative available() or skip()
  • data after the end of the stream, or a failure that isn't sticky
  • allocating more than 32 MiB + 4096 × the input length, measured with ThreadMXBean

There's one target per stream class, decompressor and flag combination: frame_multi, frame_single, and block_{safe,fast}_{stop,nostop}[_oversized]. Each has its own execution in the fuzz profile.

A few things beyond the issue's sketch:

  • Seeds. Each target has a small valid stream under src/test-resources/.../LZ4InputStreamTestInputs/. Without them, no fuzzed input ever decoded data in a 2-minute run, because the fuzzer can't produce a valid XXHash32 over the block contents. CI runs the seeds in regression mode, so they also give the streams a deterministic run on every build.
  • Dictionaries with the frame, skippable-frame and block magic numbers. Before the seeds existed, they took block coverage from 135 to 247.
  • The allocation check skips runs that load a class. The JVM charges class loading and Jazzer's instrumentation to the fuzzing thread, so on first use a 0-byte input "allocated" 184 MB.

I didn't add the deep-recursion regression test the issue mentions, because LZ4BlockStreamingTest.testManyEmptyBlocksRead/Skip already cover it. The NPE, the descriptor RuntimeException and the per-frame reallocation the issue expected these targets to flag are all fixed on main. Fuzzing each target for 2 minutes found nothing.

Proof: mvn verify passes (4127 tests). mvn test -Dtest=LZ4InputStreamTest passes all 20 regression cases. Each of the 10 targets ran clean for 120 s with JAZZER_FUZZ=1.

Reviewed with review-loop: 2 rounds with Claude Opus. Verdict: approved

Created with Claude Opus 5.5 in Claude Code.

The fuzz suite covered the block codecs and XXHash but not the stream
classes. LZ4InputStreamTest feeds raw input to both streams through a mix of
read(), read(byte[]), skip() and available() calls taken from a fixed op
prefix, and keeps calling after the stream ends or fails. It fails on
anything but IOException, on results outside the InputStream contract, on
data after the end, on a failure that isn't sticky, and on allocating more
than 32 MiB + 4096 bytes per input byte.

There is one target per stream class, decompressor and flag combination,
each with its own execution in the fuzz profile. Dictionaries hold the magic
numbers, and each target has a small valid seed stream, since the fuzzer
can't produce a valid block checksum on its own.

Closes yawkat#122

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add fuzz tests for LZ4FrameInputStream and LZ4BlockInputStream

1 participant