From cccb0587a48a0fb5b3e95d188bb02a8ca2c2b076 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 15 Oct 2026 15:18:38 +0200 Subject: [PATCH] Bound the compressed length of oversized LZ4 blocks With acceptOversizedBlocks enabled, LZ4BlockInputStream allocated the compressed buffer for the compressedLen from the block header with no upper bound other than Integer.MAX_VALUE. An LZ4 block cannot be larger than the LZ4 compress bound of its original length, so reject LZ4 blocks whose compressed length exceeds LZ4Utils.maxCompressedLength(originalLen) in all modes. This limits the allocation in the opt-in mode to slightly more than the original length, which is itself limited by the block size in the header. RAW blocks already require compressedLen == originalLen. Co-Authored-By: Claude Opus 5.5 --- .../net/jpountz/lz4/LZ4BlockInputStream.java | 12 ++++++++--- .../jpountz/lz4/LZ4BlockStreamingTest.java | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4BlockInputStream.java b/src/java/net/jpountz/lz4/LZ4BlockInputStream.java index 3408d64..3467cf1 100644 --- a/src/java/net/jpountz/lz4/LZ4BlockInputStream.java +++ b/src/java/net/jpountz/lz4/LZ4BlockInputStream.java @@ -44,7 +44,9 @@ *

Use {@link Builder#withAcceptOversizedBlocks(boolean)} only for trusted * inputs that may contain noncanonical legacy LZ4 blocks. Enabling it restores * acceptance of blocks whose compressed length is greater than or equal to the - * original length, which may permit large attacker-controlled allocations.

+ * original length, which may permit large attacker-controlled allocations. + * Even then, blocks whose compressed length exceeds the LZ4 compress bound of + * their original length are rejected.

* @see LZ4BlockOutputStream */ public class LZ4BlockInputStream extends FilterInputStream { @@ -310,7 +312,9 @@ private boolean readBlock() throws IOException { || (originalLen == 0 && compressedLen != 0) || (originalLen != 0 && compressedLen == 0) || (compressionMethod == COMPRESSION_METHOD_RAW && originalLen != compressedLen) - || (compressionMethod == COMPRESSION_METHOD_LZ4 && !acceptOversizedBlocks && compressedLen >= originalLen)) { + || (compressionMethod == COMPRESSION_METHOD_LZ4 && !acceptOversizedBlocks && compressedLen >= originalLen) + // even an oversized LZ4 block cannot be larger than the LZ4 compress bound of its original length + || (compressionMethod == COMPRESSION_METHOD_LZ4 && compressedLen > LZ4Utils.maxCompressedLength(originalLen))) { throw new IOException("Stream is corrupted"); } if (originalLen == 0 && compressedLen == 0) { @@ -436,7 +440,9 @@ public Builder withStopOnEmptyBlock(boolean stopOnEmptyBlock) { * Registers whether the builder should accept oversized blocks whose * compressed length is greater than or equal to the original length. *

Only enable this for trusted inputs. Accepting such blocks may permit - * large attacker-controlled allocations.

+ * large attacker-controlled allocations. Blocks whose compressed length + * exceeds the LZ4 compress bound of their original length are rejected + * even when this is enabled.

* * @param acceptOversizedBlocks whether oversized blocks should be accepted * @return current builder instance diff --git a/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java b/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java index d9a2576..8648d7b 100644 --- a/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java +++ b/src/test/net/jpountz/lz4/LZ4BlockStreamingTest.java @@ -557,6 +557,26 @@ public void testAcceptsInvalidLz4CompressedLengthInOversizedBlocksMode() throws in.close(); } + @Test + public void testRejectsLz4CompressedLengthAboveCompressBoundInOversizedBlocksMode() { + final int originalLen = 16; + for (int compressedLen : new int[] {LZ4Utils.maxCompressedLength(originalLen) + 1, Integer.MAX_VALUE}) { + // header only: the block is rejected before its payload or checksum is read + final byte[] header = ByteBuffer.allocate(LZ4BlockOutputStream.HEADER_LENGTH) + .order(ByteOrder.LITTLE_ENDIAN) + .put(LZ4BlockOutputStream.MAGIC) + .put((byte) (LZ4BlockOutputStream.COMPRESSION_METHOD_LZ4 | 0x0F)) + .putInt(compressedLen) + .putInt(originalLen) + .array(); + final LZ4BlockInputStream in = lz4BlockInputStreamBuilder() + .withAcceptOversizedBlocks(true) + .build(new ByteArrayInputStream(header)); + final IOException e = assertThrows(IOException.class, in::read); + assertEquals("Stream is corrupted", e.getMessage()); + } + } + // Many empty blocks followed by a regular stream, as could be produced by a malicious peer private static byte[] emptyBlocksFollowedBy(int emptyBlocks, byte[] tail) { final byte[] emptyBlock = new byte[LZ4BlockOutputStream.HEADER_LENGTH];