Repository navigation
Match native end-of-block checks in fast decompressors and for MF_LIMIT - #177
Merged
Merged
Conversation
…ompressors The fast decompressors pass the end of the source buffer as srcEnd, so the input-side check added in #175 is relative to the buffer end, not the block end: 1F 07 01 00 00 40 01 02 03 04 (destLen 24) is rejected from an exact-size array but accepted as soon as any byte follows it, and the deprecated LZ4BlockInputStream(InputStream) constructor, which reuses a larger buffer, never rejects it. LZ4_decompress_fast rejects such blocks with an output-side rule instead: after a match, at least LASTLITERALS bytes of output must remain. Apply the same rule in the fast decompressors. It is only needed in the branches taken near the end of the output, so the hot path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decoders treat a sequence as the last one when its literals end less than COPY_LENGTH (8) bytes before the end of the output, but the reference decoders use MFLIMIT (12): LZ4_decompress_safe rejects cpy > oend - MFLIMIT unless the literals end the block, LZ4_decompress_fast rejects oend - op < MFLIMIT unless op == oend. So with a tight destination the Java decoders accepted blocks the native ones reject, e.g. 10 41 01 00 50 42 43 44 45 46 with a destination of exactly 10 bytes (the match starts 9 bytes before the end). Use MF_LIMIT for the output-side bound. Since MF_LIMIT >= COPY_LENGTH this only routes more sequences to the checked last-literals path, so wildArraycopy keeps its headroom. Spec-compliant encoders never start a match within the last MF_LIMIT bytes, so valid blocks are unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover the fast decompressors (with and without bytes after the block, byte[] and direct ByteBuffer), a match with two length bytes, the MF_LIMIT rule with an exact-size destination, and valid twins of each block in all decompressors including the native ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-ups to #175: two more cases where the Java decompressors accept blocks that the native (lz4 1.11.0) decoders reject, plus tests. One commit per change.
1. Fast decompressors: require LAST_LITERALS after the last match
The fast decompressors pass the end of the source buffer as
srcEnd, so #175's input-side check (lenLimit = srcEnd - (1 + LAST_LITERALS)) is relative to the buffer end, not the block end.1F 07 01 00 00 40 01 02 03 04withdestLen = 24(1 literal, a 19-byte match, 4 last literals) is rejected from an exact-size array but accepted (returning 10) as soon as any byte follows it. The deprecatednew LZ4BlockInputStream(in)uses the fast decompressor with a reused, larger buffer, so it never rejects it.LZ4_decompress_fastrejects such blocks with an output-side rule: after a match,oend - op < LASTLITERALSis an error. The fast decompressors now apply the same rule (matchCopyEnd > destEnd - LAST_LITERALS) in the two branches taken near the end of the output (thematchDec == 0branch and thenotEnoughSpace(destEnd - matchCopyEnd, COPY_LENGTH)branch). The wild-copy branch already guaranteesCOPY_LENGTH >= LAST_LITERALSbytes remain, so the hot path is unchanged. The comment from #175 now says which decoder enforces what.2. Last match must start MF_LIMIT bytes before the end of the output
The decoders took the "these must be the last literals" path when the literals end less than
COPY_LENGTH(8) bytes before the destination end. The native decoders useMFLIMIT(12):LZ4_decompress_saferejectscpy > oend - MFLIMITunless the literals end the block,LZ4_decompress_fastrejectsoend - op < MFLIMITunlessop == oend. So with a tight destination,10 41 01 00 50 42 43 44 45 46(1 literal, a 4-byte match starting 9 bytes before the end, 5 last literals) with a 10-byte destination was rejected by JNI but decoded by the Java safe, unsafe and fast decompressors.The output-side operand is now
MF_LIMIT; the input-sideCOPY_LENGTH + literalLenoperand is unchanged. SinceMF_LIMIT >= COPY_LENGTH, this only routes more sequences to the checked last-literals path, sowildArraycopykeeps its headroom. Spec-compliant encoders never start a match within the last 12 bytes, so valid blocks are unaffected; a block whose last match starts exactly 12 bytes before the end still decodes with an exact-size destination.3. Tests
In
LZ4Test, for all safe and fast decompressors (Java and JNI), byte[] and direct ByteBuffer:testMatchFollowedByTooFewLastLiterals: the Reject blocks the native decoder rejects since lz4 1.11.0 #175 block and a variant with two match-length bytes (1F 07 01 00 FF 00 40 01 02 03 04) are rejected, including by the fast decompressors with 32 trailing bytes after the block. Their valid twins with 5 last literals decode to 25 and 280 bytes.testLastMatchStartsTooCloseToEnd: the MF_LIMIT block above is rejected with an exact 10-byte destination.20 41 42 01 00 80 43 ... 4A, whose last match starts exactly 12 bytes before the end, decodes to 14 bytes with an exact-size and with a larger destination.Both new tests fail on current
main(JNI passes, Java fails). The full localverifypasses.🤖 Generated with Claude Code