From 50ed0206805912bbe0ad8fd36e335a0e8b84ee18 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 15 Oct 2026 15:13:12 +0200 Subject: [PATCH] Throw LZ4Exception for input shorter than the length prefix LZ4DecompressorWithLength read the 4-byte length prefix without checking that the input is long enough, so input of fewer than 4 bytes surfaced as ArrayIndexOutOfBoundsException, IndexOutOfBoundsException or IllegalArgumentException instead of LZ4Exception. Check the available compressed length in all decompress overloads, for both fast and safe decompressors, before reading the prefix. Co-Authored-By: Claude Opus 5.5 --- .../lz4/LZ4DecompressorWithLength.java | 19 +++++++ src/test/net/jpountz/lz4/OutOfBoundsTest.java | 53 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java index c2cf4099..e113ea85 100644 --- a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java +++ b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java @@ -180,6 +180,13 @@ private static void checkDestinationLength(int decompressedLength, int maxDestin } } + private static void checkCompressedLength(int compressedLength) { + if (compressedLength < 4) { + throw new LZ4Exception("Compressed length " + compressedLength + + " is too short for the 4-byte length prefix"); + } + } + private static int checkWritten(int written, int declared) { if (written != declared) { throw new LZ4Exception("Decompressed " + written @@ -220,6 +227,8 @@ public int decompress(byte[] src, int srcOff, byte[] dest, int destOff) { if (safeDecompressor != null) { return decompress(src, srcOff, src.length - srcOff, dest, destOff); } + SafeUtils.checkRange(src, srcOff, 0); + checkCompressedLength(src.length - srcOff); final int destLen = getDecompressedLength(src, srcOff); SafeUtils.checkRange(dest, destOff, 0); checkDestinationLength(destLen, dest.length - destOff); @@ -246,6 +255,8 @@ public int decompress(byte[] src, int srcOff, int srcLen, byte[] dest, int destO if (safeDecompressor == null) { return decompress(src, srcOff, dest, destOff); } + SafeUtils.checkRange(src, srcOff, srcLen); + checkCompressedLength(srcLen); final int destLen = getDecompressedLength(src, srcOff); SafeUtils.checkRange(dest, destOff, 0); checkDestinationLength(destLen, dest.length - destOff); @@ -284,6 +295,8 @@ public byte[] decompress(byte[] src, int srcOff) { if (safeDecompressor != null) { return decompress(src, srcOff, src.length - srcOff); } + SafeUtils.checkRange(src, srcOff, 0); + checkCompressedLength(src.length - srcOff); final int destLen = getDecompressedLength(src, srcOff); checkDecompressedLength(destLen, src.length - srcOff - 4); return fastDecompressor.decompress(src, srcOff + 4, destLen); @@ -310,6 +323,7 @@ public byte[] decompress(byte[] src, int srcOff, int srcLen) { return decompress(src, srcOff); } SafeUtils.checkRange(src, srcOff, srcLen); + checkCompressedLength(srcLen); final int destLen = getDecompressedLength(src, srcOff); checkDecompressedLength(destLen, srcLen - 4); final byte[] dest = new byte[destLen]; @@ -330,6 +344,7 @@ public byte[] decompress(byte[] src, int srcOff, int srcLen) { * of dest after its position may have been partially overwritten */ public void decompress(ByteBuffer src, ByteBuffer dest) { + checkCompressedLength(src.remaining()); final int destLen = getDecompressedLength(src, src.position()); checkDestinationLength(destLen, dest.remaining()); if (safeDecompressor == null) { @@ -363,6 +378,8 @@ public int decompress(ByteBuffer src, int srcOff, ByteBuffer dest, int destOff) if (safeDecompressor != null) { return decompress(src, srcOff, src.remaining() - srcOff, dest, destOff); } + ByteBufferUtils.checkRange(src, srcOff, 0); + checkCompressedLength(src.limit() - srcOff); final int destLen = getDecompressedLength(src, srcOff); ByteBufferUtils.checkRange(dest, destOff, 0); checkDestinationLength(destLen, dest.capacity() - destOff); @@ -390,6 +407,8 @@ public int decompress(ByteBuffer src, int srcOff, int srcLen, ByteBuffer dest, i if (safeDecompressor == null) { return decompress(src, srcOff, dest, destOff); } + ByteBufferUtils.checkRange(src, srcOff, srcLen); + checkCompressedLength(srcLen); final int destLen = getDecompressedLength(src, srcOff); ByteBufferUtils.checkRange(dest, destOff, 0); checkDestinationLength(destLen, dest.capacity() - destOff); diff --git a/src/test/net/jpountz/lz4/OutOfBoundsTest.java b/src/test/net/jpountz/lz4/OutOfBoundsTest.java index 11be1dbf..b9263488 100644 --- a/src/test/net/jpountz/lz4/OutOfBoundsTest.java +++ b/src/test/net/jpountz/lz4/OutOfBoundsTest.java @@ -149,6 +149,59 @@ public void impossibleDeclaredLength(LZ4DecompressorWithLength decompressor) { assertEquals("Decompressed length 67108865 exceeds configured maximum 67108864", exception.getMessage()); } + @ParameterizedTest + @MethodSource("decompressorsWithLength") + public void inputShorterThanLengthPrefix(LZ4DecompressorWithLength decompressor) { + for (int i = 0; i < 4; i++) { + int length = i; + byte[] src = new byte[length]; + Arrays.fill(src, (byte) 1); + // the same bytes at an offset + byte[] padded = new byte[2 + length]; + System.arraycopy(src, 0, padded, 2, length); + byte[] dest = new byte[16]; + + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src, 0)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(padded, 2, length)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src, dest)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src, 0, dest, 0)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(padded, 2, length, dest, 0)); + + for (boolean direct : new boolean[] {false, true}) { + ByteBuffer srcBuffer = direct ? ByteBuffer.allocateDirect(length) : ByteBuffer.allocate(length); + srcBuffer.put(src).flip(); + ByteBuffer paddedBuffer = direct ? ByteBuffer.allocateDirect(padded.length) : ByteBuffer.allocate(padded.length); + paddedBuffer.put(padded).flip(); + ByteBuffer destBuffer = direct ? ByteBuffer.allocateDirect(16) : ByteBuffer.allocate(16); + + assertThrows(LZ4Exception.class, () -> decompressor.decompress(srcBuffer, destBuffer)); + assertEquals(0, srcBuffer.position()); + assertEquals(0, destBuffer.position()); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(srcBuffer, 0, destBuffer, 0)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(paddedBuffer, 2, length, destBuffer, 0)); + } + } + } + + @Test + public void safeInputShorterThanLengthPrefixWithTrailingBytes() { + LZ4DecompressorWithLength decompressor = + new LZ4DecompressorWithLength(LZ4Factory.safeInstance().safeDecompressor()); + // srcLen bytes are too short for the length prefix, even though src has more bytes after them + byte[] src = new byte[8]; + byte[] dest = new byte[16]; + for (int srcLen = 0; srcLen < 4; srcLen++) { + int len = srcLen; + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src, 0, len)); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(src, 0, len, dest, 0)); + assertThrows(LZ4Exception.class, + () -> decompressor.decompress(ByteBuffer.wrap(src), 0, len, ByteBuffer.wrap(dest), 0)); + ByteBuffer limited = ByteBuffer.wrap(src, 0, len); + assertThrows(LZ4Exception.class, () -> decompressor.decompress(limited, ByteBuffer.wrap(dest))); + } + } + @ParameterizedTest @MethodSource("decompressorsWithLength") public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength decompressor) {