Missing Capacity Check in Snappy.uncompress(ByteBuffer, ByteBuffer)
Public disclosure — redacted. Root cause, impact, and fix are described for defenders. A ready-to-run proof of concept, exact line numbers, and native crash offsets are intentionally omitted to reduce trivial weaponization. The confirmed impact is a denial of service (JVM crash), not memory-corruption exploitation or code execution.
Summary
| Field |
Value |
| Component |
org.xerial:snappy-java |
| Affected |
1.1.10.8 and all prior versions sharing this code path |
| Severity |
High |
| CVSS 3.1 |
7.5 — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| CWE |
CWE-787 (Out-of-bounds Write) |
| Confirmed impact |
Reliable JVM process crash (denial of service) |
Overview
Snappy.uncompress(ByteBuffer, ByteBuffer) does not verify that the destination buffer is large enough before writing decompressed data into it. The decompressed size is taken from the compressed input's own length prefix, which is fully attacker-controlled, and the native layer writes that many bytes into the destination regardless of its actual capacity.
A valid, well-formed Snappy blob whose declared decompressed size exceeds the destination direct buffer's capacity causes a write past the end of the off-heap allocation, terminating the JVM.
The sibling API BitShuffle.shuffle(ByteBuffer, ByteBuffer) already performs the equivalent capacity check before calling native code; only this path lacks it. The method's Javadoc suggests calling isValidCompressedBuffer first, but that only checks stream well-formedness — not whether the output fits the destination.
Root Cause
The ByteBuffer overload computes source/destination positions and lengths and then calls the native rawUncompress routine without a uncompressed.remaining() >= decompressedSize check. On the native side, the declared uncompressed length is read from the compressed stream via GetUncompressedLength(), and RawUncompress() then writes that many bytes into the destination. No capacity or bounds parameter exists anywhere in this path.
Impact
Any application that decompresses attacker-supplied Snappy bytes through this ByteBuffer overload — a common pattern in zero-copy data pipelines (columnar formats, network-framework integrations, and similar) — can be crashed with a single well-formed compressed blob. The confirmed impact is a reliable process-level denial of service.
Reproduction (conceptual)
A full runnable PoC and step-by-step build instructions are intentionally withheld. Conceptually:
- Construct a well-formed Snappy block whose declared decompressed size is much larger than the capacity of a target direct
ByteBuffer.
- Call the
ByteBuffer overload of uncompress with that undersized destination.
- The native copy writes past the destination allocation, and the JVM terminates with a fatal signal.
This has been reproduced reliably on affected versions.
Suggested Fix
Add a capacity check before the native rawUncompress call, matching the pattern already used in BitShuffle.shuffle / unshuffle:
int requiredSize = uncompressedLength(compressed);
if (uncompressed.remaining() < requiredSize) {
throw new IllegalArgumentException(
"not enough space for output: need " + requiredSize
+ " bytes, but only " + uncompressed.remaining() + " remaining");
}
Mitigations for Consumers
- Upgrade to a fixed version once available.
- Before decompressing, validate the declared size with
uncompressedLength() and pre-allocate a destination of sufficient capacity.
- Enforce an upper bound on the accepted decompressed size for untrusted input.
Missing Capacity Check in
Snappy.uncompress(ByteBuffer, ByteBuffer)Summary
org.xerial:snappy-javaAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HOverview
Snappy.uncompress(ByteBuffer, ByteBuffer)does not verify that the destination buffer is large enough before writing decompressed data into it. The decompressed size is taken from the compressed input's own length prefix, which is fully attacker-controlled, and the native layer writes that many bytes into the destination regardless of its actual capacity.A valid, well-formed Snappy blob whose declared decompressed size exceeds the destination direct buffer's capacity causes a write past the end of the off-heap allocation, terminating the JVM.
The sibling API
BitShuffle.shuffle(ByteBuffer, ByteBuffer)already performs the equivalent capacity check before calling native code; only this path lacks it. The method's Javadoc suggests callingisValidCompressedBufferfirst, but that only checks stream well-formedness — not whether the output fits the destination.Root Cause
The
ByteBufferoverload computes source/destination positions and lengths and then calls the nativerawUncompressroutine without auncompressed.remaining() >= decompressedSizecheck. On the native side, the declared uncompressed length is read from the compressed stream viaGetUncompressedLength(), andRawUncompress()then writes that many bytes into the destination. No capacity or bounds parameter exists anywhere in this path.Impact
Any application that decompresses attacker-supplied Snappy bytes through this
ByteBufferoverload — a common pattern in zero-copy data pipelines (columnar formats, network-framework integrations, and similar) — can be crashed with a single well-formed compressed blob. The confirmed impact is a reliable process-level denial of service.Reproduction (conceptual)
A full runnable PoC and step-by-step build instructions are intentionally withheld. Conceptually:
ByteBuffer.ByteBufferoverload ofuncompresswith that undersized destination.This has been reproduced reliably on affected versions.
Suggested Fix
Add a capacity check before the native
rawUncompresscall, matching the pattern already used inBitShuffle.shuffle/unshuffle:Mitigations for Consumers
uncompressedLength()and pre-allocate a destination of sufficient capacity.