You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
snappy-java: Off-heap Out-of-Bounds Write in Snappy.compress(ByteBuffer, ByteBuffer)
Summary
Snappy.compress(ByteBuffer uncompressed, ByteBuffer compressed) never checks that compressed.remaining() >= Snappy.maxCompressedLength(uncompressed.remaining()) before invoking
the native RawCompress, which writes the compressed output directly into the destination buffer
at the caller-supplied position with no capacity check on either the Java or native side. This is
the mirror-image defect of Finding #1 (Snappy.uncompress(ByteBuffer,ByteBuffer)), on the compress
path instead of the decompress path.
CVSS 3.1 Score:5.9 (Medium) Vector:AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Rationale: scored lower than the mirror decompress-side finding (Create an index to allow parallel compression and decompression #1) because the size an attacker influences here is the size of the input being compressed — typically the application's own data being prepared for transport/storage — a less direct attacker-control path than a fully attacker-crafted compressed blob with an arbitrary declared decompressed length. Still exploitable whenever an application compresses attacker-influenced data (e.g., forwarding/re-compressing user-supplied content) into a fixed-size destination buffer.
Source → Sink chain: the source data is the content of uncompressed (the application's own
payload, potentially containing attacker-influenced bytes forwarded/re-compressed by the
application). Its size (uLen) determines maxCompressedLength(uLen), the worst-case compressed
output size — but this value is never compared against compressed.remaining() before the native RawCompress call writes its output. The native sink (SnappyNative.cpp:66-79) writes the
compressed bytes unconditionally at compressed's buffer address + position, with no destination
capacity parameter to check against.
Reproduction Environment
Same as Finding #1: 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 libsnappyjava.dylib shipped for Mac/aarch64.
Reproduction Steps
Build the project's Java sources with javac, placing the unmodified, prebuilt native library
resources on the classpath (no source modification of any kind).
Generate 1MB of random (incompressible) data — this guarantees its worst-case compressed size, Snappy.maxCompressedLength(uncompressed.remaining()), is far larger than any small destination
buffer, without needing any adversarial crafting of the input bytes.
Copy that data into a direct ByteBuffer (src) to satisfy the API's isDirect() requirement.
Allocate an undersized destination direct ByteBuffer (undersizedDest, 64 bytes) — representing
a fixed-size buffer-pool entry sized for a typical/expected payload rather than derived from maxCompressedLength() first, a realistic zero-copy/buffer-reuse pattern.
Call Snappy.compress(src, undersizedDest) directly, with no isValidCompressedBuffer-style
guard available on this path (none exists for compress) and no capacity check performed by the
library itself.
Observe the JVM process outcome rather than a catchable Java exception.
Proof of Concept
importorg.xerial.snappy.Snappy;
importjava.nio.ByteBuffer;
publicclassFinding02_CompressOob {
publicstaticvoidmain(String[] args) throwsException {
byte[] incompressible = newbyte[1024 * 1024];
newjava.util.Random(42).nextBytes(incompressible); // random data compresses poorly / expandsByteBuffersrc = ByteBuffer.allocateDirect(incompressible.length);
src.put(incompressible);
src.flip();
ByteBufferundersizedDest = ByteBuffer.allocateDirect(64);
System.out.println("src remaining=" + src.remaining() + " maxCompressedLength=" + Snappy.maxCompressedLength(src.remaining()));
System.out.println("dest capacity=" + undersizedDest.capacity());
System.out.println("calling Snappy.compress() with 1MB random input into a 64-byte direct buffer...");
intn = Snappy.compress(src, undersizedDest);
System.out.println("UNEXPECTED: returned normally, n=" + n);
}
}
Observed Result
src remaining=1048576 maxCompressedLength=1223370
dest capacity=64
calling Snappy.compress() with 1MB random input into a 64-byte direct buffer...
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x000000010b4b3aa0, pid=85532, tid=27907
#
# JRE version: OpenJDK Runtime Environment Zulu25.28+85-CA (25.0+36) (build 25+36-LTS)
# Java VM: OpenJDK 64-Bit Server VM Zulu25.28+85-CA (25+36-LTS, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, bsd-aarch64)
# Problematic frame:
# V [libjvm.dylib+0x84faa0] InlineMatcher::match(methodHandle const&, int)+0x58
Exit code: 134. Note the crash surfaced inside the JIT compiler's own internal data structures
(InlineMatcher) rather than directly inside a memcpy frame as in Finding #1 — the classic
signature of heap-metadata corruption from an off-heap overwrite manifesting asynchronously in an
unrelated thread, rather than a contained, immediately-local segfault. This is consistent with
memory corruption of the native/off-heap allocator's bookkeeping structures, not merely a clean
single-object overrun.
Impact
Any application that compresses attacker-influenced or attacker-forwarded data into a fixed-size
destination buffer using this zero-copy overload can be driven to crash the JVM process — denial
of service — with data sized only slightly beyond the destination's capacity.
Remediation
intrequiredCapacity = maxCompressedLength(uLen);
if (compressed.remaining() < requiredCapacity) {
thrownewIllegalArgumentException("not enough space for output: need " + requiredCapacity
+ " bytes, but only " + compressed.remaining() + " remaining");
}
snappy-java: Off-heap Out-of-Bounds Write in
Snappy.compress(ByteBuffer, ByteBuffer)Summary
Snappy.compress(ByteBuffer uncompressed, ByteBuffer compressed)never checks thatcompressed.remaining() >= Snappy.maxCompressedLength(uncompressed.remaining())before invokingthe native
RawCompress, which writes the compressed output directly into the destination bufferat the caller-supplied position with no capacity check on either the Java or native side. This is
the mirror-image defect of Finding #1 (
Snappy.uncompress(ByteBuffer,ByteBuffer)), on the compresspath instead of the decompress path.
org.xerial:snappy-javasrc/main/java/org/xerial/snappy/Snappy.java(compress(ByteBuffer, ByteBuffer), lines 137–161),src/main/java/org/xerial/snappy/SnappyNative.cpp(rawCompressJNI implementation, lines 66–79)Vector:
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LRationale: scored lower than the mirror decompress-side finding (Create an index to allow parallel compression and decompression #1) because the size an attacker influences here is the size of the input being compressed — typically the application's own data being prepared for transport/storage — a less direct attacker-control path than a fully attacker-crafted compressed blob with an arbitrary declared decompressed length. Still exploitable whenever an application compresses attacker-influenced data (e.g., forwarding/re-compressing user-supplied content) into a fixed-size destination buffer.
Root Cause Analysis
Source → Sink chain: the source data is the content of
uncompressed(the application's ownpayload, potentially containing attacker-influenced bytes forwarded/re-compressed by the
application). Its size (
uLen) determinesmaxCompressedLength(uLen), the worst-case compressedoutput size — but this value is never compared against
compressed.remaining()before the nativeRawCompresscall writes its output. The native sink (SnappyNative.cpp:66-79) writes thecompressed bytes unconditionally at
compressed's buffer address + position, with no destinationcapacity parameter to check against.
Reproduction Environment
Same as Finding #1: 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
libsnappyjava.dylibshipped forMac/aarch64.Reproduction Steps
javac, placing the unmodified, prebuilt native libraryresources on the classpath (no source modification of any kind).
Snappy.maxCompressedLength(uncompressed.remaining()), is far larger than any small destinationbuffer, without needing any adversarial crafting of the input bytes.
ByteBuffer(src) to satisfy the API'sisDirect()requirement.ByteBuffer(undersizedDest, 64 bytes) — representinga fixed-size buffer-pool entry sized for a typical/expected payload rather than derived from
maxCompressedLength()first, a realistic zero-copy/buffer-reuse pattern.Snappy.compress(src, undersizedDest)directly, with noisValidCompressedBuffer-styleguard available on this path (none exists for compress) and no capacity check performed by the
library itself.
Proof of Concept
Observed Result
Exit code: 134. Note the crash surfaced inside the JIT compiler's own internal data structures
(
InlineMatcher) rather than directly inside amemcpyframe as in Finding #1 — the classicsignature of heap-metadata corruption from an off-heap overwrite manifesting asynchronously in an
unrelated thread, rather than a contained, immediately-local segfault. This is consistent with
memory corruption of the native/off-heap allocator's bookkeeping structures, not merely a clean
single-object overrun.
Impact
Any application that compresses attacker-influenced or attacker-forwarded data into a fixed-size
destination buffer using this zero-copy overload can be driven to crash the JVM process — denial
of service — with data sized only slightly beyond the destination's capacity.
Remediation