CVE-2024-36114 (Java, published 2024-05-29T21:15Z)
Description: value: Aircompressor is a library with ports of the Snappy, LZO, LZ4, and Zstandard compression algorithms to Java. All decompressor implementations of Aircompressor (LZ4, LZO, Snappy, Zstandard) can crash the JVM for certain input, and in some cases also leak the content of other memory of the Java process (which could contain sensitive information). When decompressing certain data, the decompressors try to access memory outside the bounds of the given byte arrays or byte buffers. Because Aircompressor uses the JDK class `sun.misc.Unsafe` to speed up memory access, no additional bounds checks are performed and this has similar security consequences as out-of-bounds access in C or C++, namely it can lead to non-deterministic behavior or crash the JVM. Users should update to Aircompressor 0.27 or newer where these issues have been fixed. When decompressing data from untrusted users, this can be exploited for a denial-of-service attack by crashing the JVM, or to leak other sensitive information from the Java process. There are no known workarounds for this issue.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/airlift/aircompressor
Files changed:
src/main/java/io/airlift/compress/lz4/Lz4RawDecompressor.java
src/main/java/io/airlift/compress/lzo/LzoRawDecompressor.java
src/main/java/io/airlift/compress/snappy/SnappyRawDecompressor.java
src/main/java/io/airlift/compress/zstd/Huffman.java
src/test/java/io/airlift/compress/lz4/TestLz4.java
src/test/java/io/airlift/compress/lzo/TestLzo.java
src/test/java/io/airlift/compress/snappy/TestSnappy.java
src/test/java/io/airlift/compress/zstd/TestZstd.java
Commit message: Fix out of bounds read due to negative length

If the computed literal or match length was greater than
Integer.MAX_VALUE, the value would overflow to a negative
and cause a read before the beginning of the buffer.


Vulnerable code:

Fixed code:
            if (literalLength < 0) {
                throw new MalformedInputException(input - inputAddress);
            }
            if (matchLength < 0) {
                throw new MalformedInputException(input - inputAddress);
            }
                if (matchLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }

                if (literalLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
                if (literalLength < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
                if (matchOffset < 0) {
                    throw new MalformedInputException(input - inputAddress);
                }
        verify(start2 < start3 && start3 < start4 && start4 < inputLimit, inputAddress, "Input is corrupted");

import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testLiteralLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.write((byte) 0b1111_0000); // token
        // Causes overflow for `literalLength`
        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
        Arrays.fill(literalLengthBytes, (byte) 255);
        buffer.write(literalLengthBytes);
        buffer.write(1);
        buffer.write(new byte[20]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.write((byte) 0b0000_1111); // token
        buffer.write(new byte[2]); // offset

        // Causes overflow for `matchLength`
        byte[] literalLengthBytes = new byte[Integer.MAX_VALUE / 255 + 1]; // ~9MB
        Arrays.fill(literalLengthBytes, (byte) 255);
        buffer.write(literalLengthBytes);
        buffer.write(1);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new Lz4Decompressor().decompress(data, 0, data.length, new byte[2048], 0, 2048))
                .isInstanceOf(MalformedInputException.class);
    }
import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testLiteralLengthOverflow()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Command
        buffer.write(0);
        // Causes overflow for `literalLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow1()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Write some data so that `matchOffset` validation later passes
        // Command
        buffer.write(0);
        buffer.write(new byte[66]);
        buffer.write(8);
        buffer.write(new byte[2107 * 8]);

        // Command
        buffer.write(0b001_0000);
        // Causes overflow for `matchLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);
        // Trailer
        buffer.write(0b0000_0000);
        buffer.write(0b0000_0100);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }

    @Test
    public void testMatchLengthOverflow2()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Write some data so that `matchOffset` validation later passes
        // Command
        buffer.write(0);
        buffer.write(246);
        buffer.write(new byte[264]);

        // Command
        buffer.write(0b0010_0000);
        // Causes overflow for `matchLength`
        buffer.write(new byte[Integer.MAX_VALUE / 255 + 1]); // ~9MB
        buffer.write(1);
        // Trailer
        buffer.write(0b0000_0000);
        buffer.write(0b0000_0100);

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new LzoDecompressor().decompress(data, 0, data.length, new byte[20000], 0, 20000))
                .isInstanceOf(MalformedInputException.class);
    }
import io.airlift.compress.MalformedInputException;
import org.testng.annotations.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

    @Test
    public void testInvalidLiteralLength()
    {
        byte[] data = {
                // Encoded uncompressed length 1024
                -128, 8,
                // op-code
                (byte) 252,
                // Trailer value Integer.MAX_VALUE
                (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b1111_1111, (byte) 0b0111_1111,
                // Some arbitrary data
                0, 0, 0, 0, 0, 0, 0, 0
        };

        assertThatThrownBy(() -> new SnappyDecompressor().decompress(data, 0, data.length, new byte[1024], 0, 1024))
                .isInstanceOf(MalformedInputException.class);
    }
import java.io.ByteArrayOutputStream;

    @Test
    public void testBadHuffmanData()
            throws IOException
    {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        // Magic
        buffer.write(new byte[] {
                (byte) 0b0010_1000,
                (byte) 0b1011_0101,
                (byte) 0b0010_1111,
                (byte) 0b1111_1101,
        });
        // Frame header
        buffer.write(0);
        buffer.write(0);
        // Block header COMPRESSED_BLOCK
        buffer.write(new byte[] {
                (byte) 0b1111_0100,
                (byte) 0b0000_0000,
                (byte) 0b0000_0000,
        });
        // Literals header
        buffer.write(new byte[] {
                // literalsBlockType COMPRESSED_LITERALS_BLOCK
                // + literals type
                0b0000_1010,
                // ... header remainder
                0b0000_0000,
                // compressedSize
                0b0011_1100,
                0b0000_0000,
        });
        // Huffman inputSize
        buffer.write(128);
        // weight value
        buffer.write(0b0001_0000);
        // Bad start values
        buffer.write(new byte[] {(byte) 255, (byte) 255});
        buffer.write(new byte[] {(byte) 255, (byte) 255});
        buffer.write(new byte[] {(byte) 255, (byte) 255});

        buffer.write(new byte[10]);

        byte[] data = buffer.toByteArray();

        assertThatThrownBy(() -> new ZstdDecompressor().decompress(data, 0, data.length, new byte[10], 0, 10))
                .isInstanceOf(MalformedInputException.class)
                .hasMessageStartingWith("Input is corrupted");
    }