From f4af29e14cde7fe1e9c4e1f428e09805b4ca5720 Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 15 Oct 2026 15:15:37 +0200 Subject: [PATCH 1/2] Use the buffer limit for the safe decompress(ByteBuffer, int, ByteBuffer, int) With a safe decompressor, decompress(ByteBuffer src, int srcOff, ByteBuffer dest, int destOff) computed the compressed length as src.remaining() - srcOff, mixing the buffer's position with the absolute offset. Valid input failed to decompress whenever src.position() was not 0. Use src.limit() - srcOff, which matches the absolute indexing of the other ByteBuffer arguments and the fast path. Co-Authored-By: Claude Opus 5.5 --- .../lz4/LZ4DecompressorWithLength.java | 4 +-- src/test/net/jpountz/lz4/OutOfBoundsTest.java | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java index c2cf4099..5781474f 100644 --- a/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java +++ b/src/java/net/jpountz/lz4/LZ4DecompressorWithLength.java @@ -348,7 +348,7 @@ public void decompress(ByteBuffer src, ByteBuffer dest) { * decompresses src[srcOff:] into dest[destOff:] * and returns the number of bytes read from src, and * when {@link LZ4SafeDecompressor} was specified to the constructor, - * decompresses src[srcOff:src.remaining()] into dest[destOff:] + * decompresses src[srcOff:src.limit()] into dest[destOff:] * and returns the number of decompressed bytes written into dest. * The positions and limits of the {@link ByteBuffer}s remain unchanged. * @@ -361,7 +361,7 @@ public void decompress(ByteBuffer src, ByteBuffer dest) { */ public int decompress(ByteBuffer src, int srcOff, ByteBuffer dest, int destOff) { if (safeDecompressor != null) { - return decompress(src, srcOff, src.remaining() - srcOff, dest, destOff); + return decompress(src, srcOff, src.limit() - srcOff, dest, destOff); } final int destLen = getDecompressedLength(src, srcOff); ByteBufferUtils.checkRange(dest, destOff, 0); diff --git a/src/test/net/jpountz/lz4/OutOfBoundsTest.java b/src/test/net/jpountz/lz4/OutOfBoundsTest.java index 11be1dbf..767cc121 100644 --- a/src/test/net/jpountz/lz4/OutOfBoundsTest.java +++ b/src/test/net/jpountz/lz4/OutOfBoundsTest.java @@ -149,6 +149,37 @@ public void impossibleDeclaredLength(LZ4DecompressorWithLength decompressor) { assertEquals("Decompressed length 67108865 exceeds configured maximum 67108864", exception.getMessage()); } + @ParameterizedTest + @MethodSource("decompressorsWithLength") + public void byteBufferSourceOffsetIgnoresPosition(LZ4DecompressorWithLength decompressor) { + LZ4Factory factory = LZ4Factory.safeInstance(); + byte[] original = new byte[64]; + for (int i = 0; i < original.length; i++) { + original[i] = (byte) (i % 7); + } + byte[] compressed = new LZ4CompressorWithLength(factory.fastCompressor()).compress(original); + int srcOff = 3; + + for (boolean direct : new boolean[] {false, true}) { + ByteBuffer src = direct + ? ByteBuffer.allocateDirect(srcOff + compressed.length) + : ByteBuffer.allocate(srcOff + compressed.length); + src.position(srcOff); + src.put(compressed); + src.clear(); + for (int position : new int[] {0, 1, srcOff, src.limit()}) { + src.position(position); + ByteBuffer dest = direct ? ByteBuffer.allocateDirect(original.length) : ByteBuffer.allocate(original.length); + decompressor.decompress(src, srcOff, dest, 0); + assertEquals(position, src.position()); + assertEquals(0, dest.position()); + byte[] decompressed = new byte[original.length]; + dest.get(decompressed); + assertArrayEquals(original, decompressed); + } + } + } + @ParameterizedTest @MethodSource("decompressorsWithLength") public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength decompressor) { From b80a31f904dc9158176f11774b36650e2a85600b Mon Sep 17 00:00:00 2001 From: Jonas Konrad Date: Thu, 15 Oct 2026 15:16:28 +0200 Subject: [PATCH 2/2] Move the ByteBuffer source offset test Place it after destinationBufferLimitsDecompressedLength so that it does not conflict with the short input test from #160. Co-Authored-By: Claude Opus 5.5 --- src/test/net/jpountz/lz4/OutOfBoundsTest.java | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/test/net/jpountz/lz4/OutOfBoundsTest.java b/src/test/net/jpountz/lz4/OutOfBoundsTest.java index 767cc121..35eb09ca 100644 --- a/src/test/net/jpountz/lz4/OutOfBoundsTest.java +++ b/src/test/net/jpountz/lz4/OutOfBoundsTest.java @@ -149,6 +149,34 @@ public void impossibleDeclaredLength(LZ4DecompressorWithLength decompressor) { assertEquals("Decompressed length 67108865 exceeds configured maximum 67108864", exception.getMessage()); } + @ParameterizedTest + @MethodSource("decompressorsWithLength") + public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength decompressor) { + LZ4Factory factory = LZ4Factory.safeInstance(); + byte[] compressed = new LZ4CompressorWithLength(factory.fastCompressor()).compress(new byte[16]); + + byte[] destination = new byte[16]; + LZ4Exception exception = assertThrows(LZ4Exception.class, + () -> decompressor.decompress(compressed, 0, destination, 1)); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); + + ByteBuffer movingSrc = ByteBuffer.wrap(compressed); + ByteBuffer movingDest = ByteBuffer.allocate(15); + exception = assertThrows(LZ4Exception.class, + () -> decompressor.decompress(movingSrc, movingDest)); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); + assertEquals(0, movingSrc.position()); + assertEquals(0, movingDest.position()); + + ByteBuffer indexedSrc = ByteBuffer.wrap(compressed); + ByteBuffer indexedDest = ByteBuffer.allocate(16); + exception = assertThrows(LZ4Exception.class, + () -> decompressor.decompress(indexedSrc, 0, indexedDest, 1)); + assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); + assertEquals(0, indexedSrc.position()); + assertEquals(0, indexedDest.position()); + } + @ParameterizedTest @MethodSource("decompressorsWithLength") public void byteBufferSourceOffsetIgnoresPosition(LZ4DecompressorWithLength decompressor) { @@ -180,34 +208,6 @@ public void byteBufferSourceOffsetIgnoresPosition(LZ4DecompressorWithLength deco } } - @ParameterizedTest - @MethodSource("decompressorsWithLength") - public void destinationBufferLimitsDecompressedLength(LZ4DecompressorWithLength decompressor) { - LZ4Factory factory = LZ4Factory.safeInstance(); - byte[] compressed = new LZ4CompressorWithLength(factory.fastCompressor()).compress(new byte[16]); - - byte[] destination = new byte[16]; - LZ4Exception exception = assertThrows(LZ4Exception.class, - () -> decompressor.decompress(compressed, 0, destination, 1)); - assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); - - ByteBuffer movingSrc = ByteBuffer.wrap(compressed); - ByteBuffer movingDest = ByteBuffer.allocate(15); - exception = assertThrows(LZ4Exception.class, - () -> decompressor.decompress(movingSrc, movingDest)); - assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); - assertEquals(0, movingSrc.position()); - assertEquals(0, movingDest.position()); - - ByteBuffer indexedSrc = ByteBuffer.wrap(compressed); - ByteBuffer indexedDest = ByteBuffer.allocate(16); - exception = assertThrows(LZ4Exception.class, - () -> decompressor.decompress(indexedSrc, 0, indexedDest, 1)); - assertEquals("Decompressed length 16 exceeds destination length 15", exception.getMessage()); - assertEquals(0, indexedSrc.position()); - assertEquals(0, indexedDest.position()); - } - @ParameterizedTest @MethodSource("limitedDecompressorsWithLength") public void configuredMaximumOnlyLimitsAllocatedOutput(LZ4DecompressorWithLength decompressor) {