Skip to content

Fix buffer overflows and unbounded allocations from untrusted input (CVE-2026-90559 and others) - #739

Merged
xerial merged 1 commit into
mainfrom
fix/cve-bounds-checks
Oct 3, 2026
Merged

xerial merged 1 commit into
mainfrom
fix/cve-bounds-checks

Conversation

@xerial

@xerial xerial commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Summary

The JNI layer (SnappyNative.cpp) writes through raw pointers with no bounds checks. This PR adds validation in the Java API before every native call, so bad sizes now raise a Java exception instead of crashing the JVM or corrupting memory. The native libraries don't need to be rebuilt.

Issue Problem Fix
#728 (CVE-2026-90559) uncompress(ByteBuffer, ByteBuffer) writes past an undersized destination Require uncompressed.remaining() >= uncompressedLength(compressed)
#732 compress(ByteBuffer, ByteBuffer) writes past an undersized destination Require compressed.remaining() >= maxCompressedLength(len)
#717 rawCompress / rawUncompress / uncompress(byte[]…) accept out-of-range offsets and lengths and undersized outputs Check input ranges against the array's byte size; check output space for compress and uncompress
#729 uncompress{Char,Short,Int,Long,Float,Double}Array under-allocate when the length isn't a multiple of the element size Reject such lengths with SnappyIOException
#625 uncompress(byte[]) with a 5-byte input leads to an OOM or NegativeArraySizeException uncompressedLength() rejects negative lengths and lengths above 64/3 × the compressed size
#730 SnappyFramedInputStream allocates from the declared length before the CRC check Same uncompressedLength() bound: allocation is now proportional to the bytes actually read
#731 Runs of 4-byte skippable chunks cause a StackOverflowError through recursion Skip chunks in a loop

Why a ratio bound instead of a fixed cap

The densest Snappy element is a 3-byte copy that expands to 64 bytes, so a valid stream can never declare more than 64/3 times its compressed size. The bound rejects only data that would fail to decompress anyway, and it can't break legitimate data. It lives in uncompressedLength(), so it also covers third-party code that does new byte[Snappy.uncompressedLength(x)].

For the framed stream, I left out a hard 64 KiB per-chunk cap (the framing spec limit). The existing testLargerFrames_* tests show the reader accepts larger frames on purpose for compatibility.

Behavior change

Passing an output buffer smaller than maxCompressedLength(n) to compress now throws IllegalArgumentException. The native RawCompress already required that size, and an undersized buffer was undefined behavior before.

Related community PRs

#733, #734, #735, #736 and #737 each fix one of these issues. This PR covers all of them in one consistent change, and thanks to their authors for the reports and proposed fixes.

Test plan

  • New SnappyBoundsCheckTest (20 tests) covering each issue's PoC plus valid edge cases: max-ratio data, offset output, empty input
  • ./sbt test: 124 passed, 0 failed

🤖 Generated with Claude Code

The JNI layer writes through raw pointers without any bounds check, so
the Java API must validate every range it hands to native code.

- compress/rawCompress: require maxCompressedLength(inputLength) bytes of
  output space and an in-range input (#732, #717)
- uncompress/rawUncompress: require the declared uncompressed length to
  fit in the output buffer (#728, CVE-2026-90559, #717)
- uncompress*Array: reject uncompressed lengths that are not a multiple
  of the element size, which under-allocated the result array (#729)
- uncompressedLength: reject negative lengths and lengths no valid Snappy
  stream of the given size can produce (> 64/3 of the compressed size),
  preventing huge allocations from a few bytes of input (#625, #730)
- SnappyFramedInputStream: skip consecutive skippable chunks iteratively
  instead of recursing once per 4-byte chunk (#731)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug label Oct 3, 2026
@xerial
xerial merged commit 943c604 into main Oct 3, 2026
9 checks passed
@xerial
xerial deleted the fix/cve-bounds-checks branch October 3, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant