Skip to content
Merged
5 changes: 5 additions & 0 deletions VERSION.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.2.1 (not yet released)

#85: Handle malformed LZF back references
(contributed by @yawkat)

1.2.0 (02-Jan-2026)

#54: Validate arguments for `Unsafe` codecs
Expand Down
49 changes: 42 additions & 7 deletions src/main/java/com/ning/compress/lzf/ChunkDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,19 @@ public int decode(final byte[] sourceBuffer, int inPtr, int inLength,
int outPtr = 0;
int blockNr = 0;

final int endMinusOne = inPtr + inLength - 1; // -1 to offset possible end marker

final int inEnd = inPtr + inLength;
final int endMinusOne = inEnd - 1; // -1 to offset possible end marker

while (inPtr < endMinusOne) {
// let's do basic sanity checks; no point in skimping with these checks
if (sourceBuffer[inPtr] != LZFChunk.BYTE_Z || sourceBuffer[inPtr+1] != LZFChunk.BYTE_V) {
throw new LZFException("Corrupt input data, block #"+blockNr+" (at offset "+inPtr+"): did not start with 'ZV' signature bytes");
}
// Verify that the header is fully available before reading it: otherwise a truncated
// block would be reported as `ArrayIndexOutOfBoundsException` instead of corrupt input
if ((inPtr + LZFChunk.HEADER_LEN_NOT_COMPRESSED) > inEnd) {
_reportTruncatedHeader(blockNr, inPtr);
}
inPtr += 2;
int type = sourceBuffer[inPtr++];
int len = uint16(sourceBuffer, inPtr);
Expand All @@ -94,23 +100,32 @@ public int decode(final byte[] sourceBuffer, int inPtr, int inLength,
if ((outPtr + len) > targetBuffer.length) {
_reportArrayOverflow(targetBuffer, outPtr, len);
}
if ((inPtr + len) > inEnd) {
_reportIncompleteBlock(blockNr);
}
System.arraycopy(sourceBuffer, inPtr, targetBuffer, outPtr, len);
outPtr += len;
} else { // compressed
// compressed blocks have 2 more header bytes, for uncompressed length
if ((inPtr + 2) > inEnd) {
_reportTruncatedHeader(blockNr, inPtr);
}
int uncompLen = uint16(sourceBuffer, inPtr);
if ((outPtr + uncompLen) > targetBuffer.length) {
_reportArrayOverflow(targetBuffer, outPtr, uncompLen);
}
inPtr += 2;
// Content has to fit as well: passing an end offset past the end of input would
// be an invalid argument for `decodeChunk()`, not malformed content
if ((inPtr + len) > inEnd) {
_reportIncompleteBlock(blockNr);
}
decodeChunk(sourceBuffer, inPtr, inPtr + len, targetBuffer, outPtr, outPtr+uncompLen);
outPtr += uncompLen;
}
inPtr += len;

// Fail if more input than expected was consumed, respectively if `inLength` does not include full block
if (inPtr > endMinusOne + 1) {
throw new LZFException("Corrupt input data, block #" + blockNr + " is incomplete");
}
// NOTE: no need to verify that we did not consume more input than there was: checks
// above already guarantee that both the header and the content of the block fit
++blockNr;
}
return outPtr;
Expand Down Expand Up @@ -144,6 +159,11 @@ public abstract void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, i
* <p>For backward compatibility this method just delegates to {@link #decodeChunk(byte[], int, byte[], int, int)},
* ignoring the {@code inEnd} parameter. Subclasses should override it and consider the {@code inEnd} parameter.
*
* @throws LZFException If content is not valid LZF: this includes truncated content, as well as
* back references that would point before start of the chunk's output, or produce more output
* than {@code outEnd - outPos} bytes. Note that invalid arguments (as opposed to invalid
* content) are instead reported as {@link ArrayIndexOutOfBoundsException}.
*
* @since 1.2
*/
public void decodeChunk(byte[] in, int inPos, int inEnd, byte[] out, int outPos, int outEnd)
Expand Down Expand Up @@ -301,4 +321,19 @@ protected void _reportArrayOverflow(byte[] targetBuffer, int outPtr, int dataLen
throw new LZFException("Target buffer too small ("+targetBuffer.length+"): can not copy/uncompress "
+dataLen+" bytes to offset "+outPtr);
}

/**
* Helper method called when the header of a block extends past the end of available input
*/
private void _reportTruncatedHeader(int blockNr, int offset) throws LZFException {
throw new LZFException("Corrupt input data, block #"+blockNr+" (at offset "+offset
+"): truncated block header");
}

/**
* Helper method called when the content of a block extends past the end of available input
*/
private void _reportIncompleteBlock(int blockNr) throws LZFException {
throw new LZFException("Corrupt input data, block #"+blockNr+" is incomplete");
}
}
70 changes: 56 additions & 14 deletions src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.io.InputStream;
import java.lang.reflect.Field;

import sun.misc.Unsafe;

Check warning on line 7 in src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

sun.misc.Unsafe is internal proprietary API and may be removed in a future release

import com.ning.compress.lzf.*;

Expand All @@ -21,18 +21,29 @@
@SuppressWarnings("restriction")
public class UnsafeChunkDecoder extends ChunkDecoder
{
private static final Unsafe unsafe;

Check warning on line 24 in src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

sun.misc.Unsafe is internal proprietary API and may be removed in a future release
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");

Check warning on line 27 in src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

sun.misc.Unsafe is internal proprietary API and may be removed in a future release
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);

Check warning on line 29 in src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

sun.misc.Unsafe is internal proprietary API and may be removed in a future release
}
catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* Maximum offset of a back-reference: 5 bits from the control byte, a full byte,
* and the implicit 1 -- so at most 8192 bytes back
*/
private final static int MAX_BACK_REF_OFFSET = 8192;

/**
* Maximum run length of a back-reference: 255 + 9, for the long form
*/
private final static int MAX_BACK_REF_LENGTH = 264;

private static final long BYTE_ARRAY_OFFSET = unsafe.arrayBaseOffset(byte[].class);
// private static final long SHORT_ARRAY_OFFSET = unsafe.arrayBaseOffset(short[].class);
// private static final long SHORT_ARRAY_STRIDE = unsafe.arrayIndexScale(short[].class);
Expand Down Expand Up @@ -68,7 +79,7 @@
}

@Override
public void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) throws LZFException {

Check warning on line 82 in src/main/java/com/ning/compress/lzf/impl/UnsafeChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

decodeChunk(byte[],int,byte[],int,int) in com.ning.compress.lzf.ChunkDecoder has been deprecated
decodeChunk(in, inPos, in.length, out, outPos, outEnd);
}

Expand All @@ -82,60 +93,91 @@

final int outPosStart = outPos;

// Back-reference offsets and run lengths are bounded, so checks against the start and the
// end of this chunk's output can only fail near them: in between they are provably
// redundant, and skipping them matters because this is the hot path.
// Note: distance from the start is compared as a difference, not against a precomputed
// `outPos + MAX_BACK_REF_OFFSET`, since that sum can overflow for a very large output
// buffer -- which would silently disable the check for the whole chunk
final int lengthChecksAfter = outEnd - MAX_BACK_REF_LENGTH;

// We need to take care of end condition, leave last 32 bytes out
final int inputEnd32 = inEnd - 32;
final int outputEnd8 = outEnd - 8;
final int outputEnd32 = outEnd - 32;

main_loop:
do {
if (inPos >= inEnd) {
throw new LZFException("Corrupt data: truncated block");
}
int ctrl = in[inPos++] & 255;
while (ctrl < LZFChunk.MAX_LITERAL) { // literal run(s)
final int literalLength = ctrl + 1;
if (outPos > outputEnd32 || inPos > inputEnd32) {
System.arraycopy(in, inPos, out, outPos, ctrl+1);
// Near the end of input or output: a literal run is at most 32 bytes long, so
// before these bounds it provably fits and does not need to be checked
if (inPos > inEnd - literalLength || outPos > outEnd - literalLength) {
throw new LZFException("Corrupt data: truncated block");
}
System.arraycopy(in, inPos, out, outPos, literalLength);
} else {
copyUpTo32(in, inPos, out, outPos, ctrl);
}
++ctrl;
inPos += ctrl;
outPos += ctrl;
inPos += literalLength;
outPos += literalLength;
if (outPos >= outEnd) {
break main_loop;
}
// The literal run may end exactly at inEnd, but another byte is required for the next control token.
if (inPos >= inEnd) {
throw new LZFException("Corrupt data: truncated block");
}
ctrl = in[inPos++] & 255;
}
// back reference
int len = ctrl >> 5;
ctrl = -((ctrl & 0x1f) << 8) - 1;
// short back reference? 2 bytes; run lengths of 2 - 8 bytes
// short back reference? 2 bytes; run lengths of 3 - 8 bytes
if (len < 7) {
if (inPos >= inEnd) {
throw new LZFException("Corrupt data: truncated block");
}
ctrl -= in[inPos++] & 255;
if (ctrl < -7 && outPos < outputEnd8) { // non-overlapping? can use efficient bulk copy
if (outPos + ctrl < outPosStart) {
final int copyLength = len + 2;
if ((outPos - outPosStart) < MAX_BACK_REF_OFFSET || outPos > lengthChecksAfter) {
if (outPos > outEnd - copyLength || outPos + ctrl < outPosStart) {
throw new LZFException("Invalid back reference");
}
}
if (ctrl < -7 && outPos < outputEnd8) { // non-overlapping? can use efficient bulk copy
final long rawOffset = BYTE_ARRAY_OFFSET + outPos;
unsafe.putLong(out, rawOffset, unsafe.getLong(out, rawOffset + ctrl));
// moveLong(out, outPos, outEnd, ctrl);
outPos += len+2;
outPos += copyLength;
continue;
}
// otherwise, byte-by-byte
outPos = copyOverlappingShort(out, outPos, ctrl, len);
continue;
}
// long back reference: 3 bytes, length of up to 264 bytes
if (inPos > inEnd - 2) {
throw new LZFException("Corrupt data: truncated block");
}
len = (in[inPos++] & 255) + 9;
ctrl -= in[inPos++] & 255;
// First: ovelapping case can't use default handling, off line.
if ((outPos - outPosStart) < MAX_BACK_REF_OFFSET || outPos > lengthChecksAfter) {
if (outPos > outEnd - len || outPos + ctrl < outPosStart) {
throw new LZFException("Invalid back reference");
}
}
// First: overlapping case can't use default handling, handled off-line.
if ((ctrl > -9) || (outPos > outputEnd32)) {
outPos = copyOverlappingLong(out, outPos, ctrl, len-9);
continue;
}
// but non-overlapping is simple
if (outPos + ctrl < outPosStart) {
throw new LZFException("Invalid back reference");
}
if (len <= 32) {
copyUpTo32(out, outPos+ctrl, outPos, len-1);
outPos += len;
Expand Down Expand Up @@ -310,9 +352,9 @@
}
}
private final static void copyLong(byte[] buffer, int inputIndex, int outputIndex, int length,
int outputEnd8)
int outputEnd32)
{
if ((outputIndex + length) > outputEnd8) {
if ((outputIndex + length) > outputEnd32) {
copyLongTail(buffer, inputIndex,outputIndex, length);
return;
}
Expand Down
96 changes: 29 additions & 67 deletions src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
}

@Override
public void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd) throws LZFException {

Check warning on line 45 in src/main/java/com/ning/compress/lzf/impl/VanillaChunkDecoder.java

View workflow job for this annotation

GitHub Actions / fuzz

decodeChunk(byte[],int,byte[],int,int) in com.ning.compress.lzf.ChunkDecoder has been deprecated
decodeChunk(in, inPos, in.length, out, outPos, outEnd);
}

Expand All @@ -50,82 +50,37 @@
public final void decodeChunk(byte[] in, int inPos, int inEnd, byte[] out, int outPos, int outEnd)
throws LZFException
{
final int outStart = outPos;
// NOTE: loop must be entered at least once, same as UnsafeChunkDecoder: a compressed
// chunk always has at least one control byte, so a chunk with no content at all
// (declared lengths of 0) is corrupt and must be reported as such by both decoders
do {
if (inPos >= inEnd) {
throw new LZFException("Corrupt data: truncated block");
}
int ctrl = in[inPos++] & 255;
if (ctrl < LZFChunk.MAX_LITERAL) { // literal run
switch (ctrl) {
case 31:
out[outPos++] = in[inPos++];
case 30:
out[outPos++] = in[inPos++];
case 29:
out[outPos++] = in[inPos++];
case 28:
out[outPos++] = in[inPos++];
case 27:
out[outPos++] = in[inPos++];
case 26:
out[outPos++] = in[inPos++];
case 25:
out[outPos++] = in[inPos++];
case 24:
out[outPos++] = in[inPos++];
case 23:
out[outPos++] = in[inPos++];
case 22:
out[outPos++] = in[inPos++];
case 21:
out[outPos++] = in[inPos++];
case 20:
out[outPos++] = in[inPos++];
case 19:
out[outPos++] = in[inPos++];
case 18:
out[outPos++] = in[inPos++];
case 17:
out[outPos++] = in[inPos++];
case 16:
out[outPos++] = in[inPos++];
case 15:
out[outPos++] = in[inPos++];
case 14:
out[outPos++] = in[inPos++];
case 13:
out[outPos++] = in[inPos++];
case 12:
out[outPos++] = in[inPos++];
case 11:
out[outPos++] = in[inPos++];
case 10:
out[outPos++] = in[inPos++];
case 9:
out[outPos++] = in[inPos++];
case 8:
out[outPos++] = in[inPos++];
case 7:
out[outPos++] = in[inPos++];
case 6:
out[outPos++] = in[inPos++];
case 5:
out[outPos++] = in[inPos++];
case 4:
out[outPos++] = in[inPos++];
case 3:
out[outPos++] = in[inPos++];
case 2:
out[outPos++] = in[inPos++];
case 1:
out[outPos++] = in[inPos++];
case 0:
out[outPos++] = in[inPos++];
int literalLen = ctrl + 1;
if (inPos > inEnd - literalLen || outPos > outEnd - literalLen) {
throw new LZFException("Corrupt data: truncated block");
}
System.arraycopy(in, inPos, out, outPos, literalLen);
Comment thread
cowtowncoder marked this conversation as resolved.
inPos += literalLen;
outPos += literalLen;
continue;
}
// back reference
int len = ctrl >> 5;
ctrl = -((ctrl & 0x1f) << 8) - 1;
if (len < 7) { // 2 bytes; length of 3 - 8 bytes
if (inPos >= inEnd) {
throw new LZFException("Corrupt data: truncated block");
}
ctrl -= in[inPos++] & 255;
final int copyLength = len + 2;
if (outPos > outEnd - copyLength || outPos + ctrl < outStart) {
throw new LZFException("Invalid back reference");
}
out[outPos] = out[outPos++ + ctrl];
out[outPos] = out[outPos++ + ctrl];
switch (len) {
Expand All @@ -146,10 +101,17 @@
}

// long version (3 bytes, length of up to 264 bytes)
if (inPos > inEnd - 2) {
throw new LZFException("Corrupt data: truncated block");
}
len = in[inPos++] & 255;
ctrl -= in[inPos++] & 255;

// First: if there is no overlap, can just use arraycopy:
final int copyLength = len + 9;
if (outPos > outEnd - copyLength || outPos + ctrl < outStart) {
throw new LZFException("Invalid back reference");
}

// First: if there is no overlap, can just use bulk copy:
if ((ctrl + len) < -9) {
len += 9;
if (len <= 32) {
Expand Down
Loading
Loading