Reject LZ4 match offsets that point before the start of the output - #16478
Open
serhiy-bzhezytskyy wants to merge 2 commits into
Open
Reject LZ4 match offsets that point before the start of the output#16478serhiy-bzhezytskyy wants to merge 2 commits into
serhiy-bzhezytskyy wants to merge 2 commits into
Conversation
LZ4#decompress read a match offset without checking it against the number of bytes written so far. An offset larger than dOff makes the match reference bytes before dest[0], which this call never wrote, so corrupt input failed with ArrayIndexOutOfBoundsException from System.arraycopy or from the incremental copy loop rather than with a checked IOException. The bound is dOff and not the number of bytes decompressed by this call, because a preset dictionary is placed in dest[dOff-dictLen:dOff] and a match may legitimately reference it. The compressor already asserts the same invariant when writing a match (matchDec > 0 && matchDec < 1 << 16 at LZ4.java:183), so this only affects input that was not produced by LZ4#compress.
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.
Description
LZ4#decompressreads a match offset and checks only that it is not zero. It does not check it against the number of bytes written so far, so an offset larger thandOffmakes the match reference bytes beforedest[0]— bytes this call never wrote:Corrupt input therefore fails with
ArrayIndexOutOfBoundsException: arraycopy: source index -8 out of bounds for byte[18]rather than a checkedIOException.The LZ4 block format leaves this to the implementation, and @jpountz described the same condition on LUCENE-5267 in 2013 while diagnosing an
AIOOBEfrom a corrupt.fdt:The compressor already asserts the invariant when writing a match —
assert matchDec > 0 && matchDec < 1 << 16atLZ4.java:188— so this only affects input that was not produced byLZ4#compress, which is to say corrupt or hostile input.What this does and does not fix
It makes the failure legible. It does not improve corruption detection, and it is worth being explicit about that, because the two are easy to conflate.
Measured on
main, flipping one byte at a time across the.fdtof a 2,000-document index (codec and mode pinned toLucene104/BEST_SPEED, every 37th byte, all documents read back and compared):mainArrayIndexOutOfBoundsExceptionfrom LZ4IOExceptionCorruptIndexExceptionNullPointerException(header or index corruption)So the
AIOOBEbecomes a checked exception almost one-for-one, and the largest outcome — a wrong document returned silently — is untouched. A validity check cannot detect a valid-but-wrong LZ4 stream; most flipped bytes still decode to something, just not to what was compressed. Only a checksum can catch that, which is what LUCENE-5267 proposed and what the frame format specifies as an optional per-block checksum.That sampling is coarse: 2.7% of positions, all eight bits inverted rather than one, only the first exception classified, one corpus and one compression mode. It is offered as the shape of the change, not as a precise figure.
Relationship to #15570
matchDec == 0was closed there in January. This is the other unchecked path in the same method.lucene-coreis not affected by CVE-2025-66566 — that is alz4-javaadvisory, and Lucene has its own vendored LZ4 — but the advisory describes the general shape as "an attacker can direct such a copy at a region that hasn't been filled with decompressed bytes yet", and cites it as prior art for why the bound matters. Whether Lucene's stored-fields path reuses output buffers such that another document's bytes could surface is not something I measured, so this is proposed as format conformance and error legibility, not as a security fix.Verification
AIOOBE: source index -8andsource index -1dOffrather than the number of bytes this call decompressed, since a match may legitimately reference the dictionary the caller placed indest[dOff-dictLen:dOff]:lucene:core:testfor*LZ4*,*Compressing*and*StoredFields*passes with-Ptests.nightly=true -Ptests.iters=5(1,375 tests):lucene:core:checkandtidypassThe
CHANGES.txtentry usesGITHUB#PENDING; I will replace it with this PR's number.