Skip to content

Off heap OOB Write in ByteBuffer Uncompress #728

Description

@August829

Missing Capacity Check in Snappy.uncompress(ByteBuffer, ByteBuffer)

Public disclosure — redacted. Root cause, impact, and fix are described for defenders. A ready-to-run proof of concept, exact line numbers, and native crash offsets are intentionally omitted to reduce trivial weaponization. The confirmed impact is a denial of service (JVM crash), not memory-corruption exploitation or code execution.

Summary

Field Value
Component org.xerial:snappy-java
Affected 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)
Confirmed impact Reliable JVM process crash (denial of service)

Overview

Snappy.uncompress(ByteBuffer, ByteBuffer) does not verify that the destination buffer is large enough before writing decompressed data into it. The decompressed size is taken from the compressed input's own length prefix, which is fully attacker-controlled, and the native layer writes that many bytes into the destination regardless of its actual capacity.

A valid, well-formed Snappy blob whose declared decompressed size exceeds the destination direct buffer's capacity causes a write past the end of the off-heap allocation, terminating the JVM.

The sibling API BitShuffle.shuffle(ByteBuffer, ByteBuffer) already performs the equivalent capacity check before calling native code; only this path lacks it. The method's Javadoc suggests calling isValidCompressedBuffer first, but that only checks stream well-formedness — not whether the output fits the destination.

Root Cause

The ByteBuffer overload computes source/destination positions and lengths and then calls the native rawUncompress routine without a uncompressed.remaining() >= decompressedSize check. On the native side, the declared uncompressed length is read from the compressed stream via GetUncompressedLength(), and RawUncompress() then writes that many bytes into the destination. No capacity or bounds parameter exists anywhere in this path.

Impact

Any application that decompresses attacker-supplied Snappy bytes through this ByteBuffer overload — a common pattern in zero-copy data pipelines (columnar formats, network-framework integrations, and similar) — can be crashed with a single well-formed compressed blob. The confirmed impact is a reliable process-level denial of service.

Reproduction (conceptual)

A full runnable PoC and step-by-step build instructions are intentionally withheld. Conceptually:

  1. Construct a well-formed Snappy block whose declared decompressed size is much larger than the capacity of a target direct ByteBuffer.
  2. Call the ByteBuffer overload of uncompress with that undersized destination.
  3. The native copy writes past the destination allocation, and the JVM terminates with a fatal signal.

This has been reproduced reliably on affected versions.

Suggested Fix

Add a capacity check before the native rawUncompress call, matching the pattern already used in BitShuffle.shuffle / unshuffle:

int requiredSize = uncompressedLength(compressed);
if (uncompressed.remaining() < requiredSize) {
    throw new IllegalArgumentException(
        "not enough space for output: need " + requiredSize
        + " bytes, but only " + uncompressed.remaining() + " remaining");
}

Mitigations for Consumers

  • Upgrade to a fixed version once available.
  • Before decompressing, validate the declared size with uncompressedLength() and pre-allocate a destination of sufficient capacity.
  • Enforce an upper bound on the accepted decompressed size for untrusted input.

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