Two lines: getLongBuffer and getFloatBuffer take the view's alignment from the absolute stream position ((int) pos & 0x7, & 0x3) instead of the offset inside the block. The two are the same only when blocks are at least 8 bytes.
ByteBuffersDataInputreads a stream stored in a list of equalByteBufferblocks whose size is a power of two (1 KiB and up by default). Position p is in blockp >> blockBits, at offsetp & blockMaskinside it.- Bulk
readLongscopies whole longs from aLongBufferview of the current block. A view starts at one byte offset, so the code caches up to 8 views per block, one per alignment, and picks the one for the current position. - A long that crosses into the next block is in no view: the view has nothing left, and
readLongsreads that one long the slow way,readLong(pos).
readLongs positions block 1 at 3 & 7 = 3 instead of its in-block offset 1, past the block's end. Select the still to open it at full size. Generated with AI, checked against the code.- Build a
ByteBuffersDataOutputwith 2-byte blocks, the smallest block size it allows:new ByteBuffersDataOutput(1, 1, ALLOCATE_BB_ON_HEAP, NO_REUSE). Write 3 zero bytes, then the long 42 (11 bytes), and calltoDataInput(). - Skip the 3 bytes and call
readLongs(dst, 0, 1). You expectdst = {42}, the valuereadLong()returns at the same position. - Instead it throws
IllegalArgumentException: newPosition > limit: (3 > 2), with assertions on and off. - Why: position 3 is the second byte of block 1 (bytes 2-3), in-block offset 1. To build its long view,
readLongspositions that 2-byte block at3 & 7 = 3, the absolute position modulo 8. This throws beforereadLongscan see that the view holds no whole long and take its slow path for values that span blocks, and its catch clause does not handle this exception type. - Blocks of 8 bytes or more (the default output allocates 1 KiB and up) and single-buffer inputs are unaffected.
readFloatsfails the same way on blocks under 4 bytes.
Only inputs built from blocks under 8 bytes: the expert ByteBuffersDataOutput constructor with minBitsPerBlock 1 or 2 (or a ByteBuffersDirectory on such outputs), or the List<ByteBuffer> constructor with 1-, 2- or 4-byte pages. Defaults (1 KiB blocks and up) are unaffected. With 2-byte blocks, opening a 200-document index with one deleted document fails. Always an exception, never a wrong value.
Mechanism, terms and numbers712 words
ByteBuffersDataInput reads a stream stored in equal, power-of-two-sized ByteBuffer blocks. Its bulk readLongs and readFloats read through cached LongBuffer/FloatBuffer views of a block, one per alignment, and compute that alignment from the absolute stream position ((int) pos & 0x7, (int) pos & 0x3) instead of the offset inside the block. For blocks of 8 bytes or more the two agree. For the 1-, 2- and 4-byte blocks Lucene allows (LIMIT_MIN_BITS_PER_BLOCK = 1, or the List<ByteBuffer> constructor), a bulk read at certain misaligned positions positions a block past its end and throws IllegalArgumentException, before the code can take its existing slow path for values that span blocks; the catch clause does not handle this type. Defaults (1 KiB blocks and up, or a single buffer) are not affected; this defect always throws, it never returns a wrong value. In every release since 9.0.0 and on main.
// store/ByteBuffersDataInput.java
public void readLongs(long[] arr, int off, int len) throws EOFException { // :328
try {
while (len > 0) {
LongBuffer longBuffer = getLongBuffer(pos); // :331 throws here
longBuffer.position(blockOffset(pos) >> 3); // :332 in-block offset
int chunk = Math.min(len, longBuffer.remaining());
if (chunk == 0) {
// read a single long spanning the boundary between two buffers
arr[off] = readLong(pos - offset); // :336 slow path
...
} catch (BufferUnderflowException | IndexOutOfBoundsException e) { // :350 IAE not caught
private LongBuffer getLongBuffer(long pos) { // :372
int bufferIndex = blockIndex(pos);
int alignment = (int) pos & 0x7; // :375 absolute position
int longBufferIndex = bufferIndex * Long.BYTES + alignment;
if (longBuffers[longBufferIndex] == null) {
ByteBuffer dup = blocks[bufferIndex].duplicate();
dup.position(alignment); // :379 > limit if block < 8 B
longBuffers[longBufferIndex] = dup.order(ByteOrder.LITTLE_ENDIAN).asLongBuffer();
}
return longBuffers[longBufferIndex];
}getFloatBuffer (:359-369) is the same with (int) pos & 0x3 (:362) and dup.position(alignment) (:366); readFloats (:296-325) has the same catch clause (:318).
- The 11 bytes (3 zeros, then the long 42) sit in 2-byte blocks. Position 3 is in block
3 >> 1 = 1, at in-block offset3 & 1 = 1. getLongBuffer(3)computesalignment = 3 & 7 = 3and callsposition(3)on a duplicate of block 1, whose limit is 2:IllegalArgumentException: newPosition > limit: (3 > 2).- The catch clause at
:350handles onlyBufferUnderflowExceptionandIndexOutOfBoundsException, so the exception reaches the caller. - With the in-block offset (
blockOffset(3) & 7 = 1) the view would have 1 byte left, room for 0 longs, sochunk == 0and the slow pathreadLong(3)would return 42.
- Exact condition, for full blocks of B bytes (every block but the last):
readLongsfails iff B < 8 and(pos & 7) > B;readFloatsiff B < 4 and(pos & 3) > B. It follows from:375/:379and:362/:366, and every cell of the tested matrices agrees (the study's, output blocks of 2 to 32 bytes after 0-7 prefix bytes, re-run on645d4fe14a20; the fact check's, output blocks of 2 to 16 bytes andListpages of 1 to 16 bytes). With 2-byte blocksreadLongsfails at alignments 3-7, with 4-byte blocks at 5-7;readFloatswith 2-byte blocks at alignment 3. - This defect never returns a wrong value. For B >= 8,
pos & 7equalsblockOffset(pos) & 7; single-buffer inputs haveblockMask = ~0(:65-67). For B < 8 a long view has room for 0 longs, so each read takes the slow path or throws. Every matrix cell is either correct or the exception. (Separate, and not part of this bug: the study noted, without classifying it, that theListconstructor does not check the last buffer'sposition().) - Legal inputs.
ByteBuffersDataOutputacceptsminBitsPerBlockdown toLIMIT_MIN_BITS_PER_BLOCK = 1, 2-byte blocks (ByteBuffersDataOutput.java:87-88,:154).new ByteBuffersDataInput(List<ByteBuffer>)checks for a non-empty list and, with more than one buffer, a first buffer whoseposition() + remaining()is a power of two and intermediate buffers of that size atposition() == 0(ByteBuffersDataInput.java:50-54,:448-479); nothing rejects 1-, 2- or 4-byte pages.
Exposure and likelihood331 words
- How you get tiny blocks (opt-in only):
new ByteBuffersDataOutput(minBitsPerBlock, maxBitsPerBlock, ...), the "Expert" constructor, withminBitsPerBlock1 or 2, thentoDataInput(); aByteBuffersDirectorywhose output supplier makes such outputs, as Lucene's ownTestMultiByteBuffersDirectorydoes (Math.max(LIMIT_MIN_BITS_PER_BLOCK, ...)); ornew ByteBuffersDataInput(List<ByteBuffer>)with 1-, 2- or 4-byte pages. - Defaults are safe.
ByteBuffersDirectory()usesByteBuffersDataOutput::new(ByteBuffersDirectory.java:111), whose blocks start atDEFAULT_MIN_BITS_PER_BLOCK = 10, 1 KiB;ByteBuffersDataOutput(long expectedSize)clamps to at least 10 bits (computeBlockSizeBitsFor,ByteBuffersDataOutput.java:554-565). The input's last buffer can still be shorter than 1 KiB (toBufferListflips each block,ByteBuffersDataOutput.java:236-248), and a short output gives a single-buffer input. Neither matters: with blocks of 1 KiB or more the alignment already equals the in-block offset modulo 8, and a single-buffer input hasblockMask = ~0. - With 2-byte blocks, ordinary index reads can hit it. Live docs are read with
readLongs(Lucene90LiveDocsFormat.java:133,:139), andByteBuffersIndexInput.readLongsdelegates toByteBuffersDataInput(ByteBuffersIndexInput.java:202-206). In the study class, 200 documents with one deleted in a 2-byte-blockByteBuffersDirectory:DirectoryReader.openthrowsIllegalArgumentException: newPosition > limit: (3 > 2)(re-run on645d4fe14a20). Whether an index fails depends on where its live-docs longs start: here atpos & 7 = 3, so the same index with 4- and 8-byte blocks opens (numDocs199, fact-check probe on645d4fe14a20). - Who in practice: the only known users are tests that use tiny blocks to exercise boundary handling, which is what the small limit was lowered for; downstream use was not surveyed beyond two code searches. Upstream's bulk-read tests never reach it: the aligned and unaligned
readLongs/readFloatstests inBaseDirectoryTestCase(:208-258,:320-376) run on 1 KiB to 1 MiB chunks in the chunked directories (BaseChunkedDirectoryTestCase.java:47-48), and the two cross-boundary tests use 16- and 8-byte chunks (:364-381,:383-400). - Effect: a loud unchecked exception; no corruption, no wrong values from this defect, nothing written. Likelihood rare; priority low.
One JUnit test, public API only, deterministic, about 2 s: 2-byte blocks, 3 zero bytes, the long 42, skipBytes(3), then readLongs.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.store;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestByteBuffersDataInputSmallBlockReadLongs extends LuceneTestCase {
public void testReadLongsWithTwoByteBlocks() throws Exception {
ByteBuffersDataOutput out =
new ByteBuffersDataOutput(
1, 1, ByteBuffersDataOutput.ALLOCATE_BB_ON_HEAP, ByteBuffersDataOutput.NO_REUSE);
out.writeBytes(new byte[3]);
out.writeLong(42L);
ByteBuffersDataInput in = out.toDataInput();
in.skipBytes(3);
long[] dst = new long[1];
try {
in.readLongs(dst, 0, 1);
} catch (IllegalArgumentException e) {
// expected: {42}, as readLong() returns; actual: IAE, a 2-byte block is positioned at 3
throw new AssertionError("expected readLongs to return {42}, got " + e, e);
}
assertEquals(42L, dst[0]);
}
}
cp $REPRO/TestByteBuffersDataInputSmallBlockReadLongs.java $LUCENE/lucene/core/src/test/org/apache/lucene/store/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.store.TestByteBuffersDataInputSmallBlockReadLongs -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
readLongs(dst, 0, 1)returns{42}, asreadLong()does at the same position.- Observed
IllegalArgumentException: newPosition > limit: (3 > 2)fromByteBuffersDataInput.getLongBuffer(:379), with assertions on and off.
More files, environment, full output226 words
- Public API only: the Expert
ByteBuffersDataOutputconstructor,writeBytes,writeLong,toDataInput,skipBytes,readLongs. No index, no randomness in the input, single thread. - Covers
readLongsthroughByteBuffersDataOutputonly, notreadFloatsor theList<ByteBuffer>route.readFloatshas the same defect ingetFloatBufferand is fixed by the same diff; it is shown by the study's class and by the cross-model reviewer's own test, not by this repro. A maintainer may prefer a second, float test or one small parameterized test over both methods. - As the regression test: this same method, for example added to
TestByteBuffersDataInput. It passes with the fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0, 10.5.2 and 9.0.0 |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
cp $REPRO/TestByteBuffersDataInputSmallBlockReadLongs.java $LUCENE/lucene/core/src/test/org/apache/lucene/store/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.store.TestByteBuffersDataInputSmallBlockReadLongs -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on (the same with -Ptests.asserts=false)
TestByteBuffersDataInputSmallBlockReadLongs > testReadLongsWithTwoByteBlocks FAILED
java.lang.AssertionError: expected readLongs to return {42}, got java.lang.IllegalArgumentException: newPosition > limit: (3 > 2)
at __randomizedtesting.SeedInfo.seed([DEADBEEF:74129CDD307F4B10]:0)
at org.apache.lucene.store.TestByteBuffersDataInputSmallBlockReadLongs.testReadLongsWithTwoByteBlocks(TestByteBuffersDataInputSmallBlockReadLongs.java:36)
Caused by:
java.lang.IllegalArgumentException: newPosition > limit: (3 > 2)
at java.base/java.nio.Buffer.createPositionException(Buffer.java:351)
at java.base/java.nio.Buffer.position(Buffer.java:326)
at java.base/java.nio.ByteBuffer.position(ByteBuffer.java:1561)
at org.apache.lucene.store.ByteBuffersDataInput.getLongBuffer(ByteBuffersDataInput.java:379)
at org.apache.lucene.store.ByteBuffersDataInput.readLongs(ByteBuffersDataInput.java:331)
at org.apache.lucene.store.TestByteBuffersDataInputSmallBlockReadLongs.testReadLongsWithTwoByteBlocks(TestByteBuffersDataInputSmallBlockReadLongs.java:33)
# 10.5.2 and 10.6.0, assertions on and off: the same message, at getLongBuffer:379 <- readLongs:331
# with the fix: :lucene:core:test (SUCCESS): 1 test, assertions on and off- Lucene documents 2-byte blocks as allowed (
LIMIT_MIN_BITS_PER_BLOCK = 1, "Smallest minBitsPerBlock allowed");readLongsdocuments no block-size limit, andreadLong()works on the same input. - Lowering that limit (PR #360), rmuir wrote: "the permissible values should be ones that work correctly", while calling the constructor expert.
- The older
ByteBufferIndexInputview code guarded this case; these copies dropped the guard.
The full argument416 words
- The size is documented and accepted.
LIMIT_MIN_BITS_PER_BLOCK = 1is javadoc'd "Smallest {@code minBitsPerBlock} allowed" (ByteBuffersDataOutput.java:87-88); the Expert constructor rejects only values below it (:154). TheListconstructor's javadoc asks only for "an identical remaining number of bytes in the buffer (that is a power of two)" (ByteBuffersDataInput.java:50-54), and its check (:448-479) rejects no page size under 8 bytes. - The contract has no precondition.
DataInput.readLongs: "Read a specified number of longs";readFloats: "Reads a specified number of floats into an array at the specified offset". Their base implementations loopreadLong()/readInt()(DataInput.java:150-160,:176-188). NoIllegalArgumentExceptionis documented, and the per-value reads work on the same input (study control). - Intent when the limit was lowered. In the review of PR #360 (LUCENE-10155,
6c6a3bd5bd5, minimum lowered from 10 bits to 1), in a thread on theByteBuffersDataOutputconstructor's limits (dweiss asked why small blocks were restricted), rmuir wrote on 2021-10-07: "the defaults should be fast, but the permissible values should be ones that work correctly", and "It is also not efficient to use MMapDirectory with 4-byte memory mappings: but it works correctly and we take advantage of this in testing". Later that day, in the same thread, he wrote that the min/max constructor should be labelled "expert" and that Lucene should "reserve the right to change it in the future (for example, if we aligned our index files to at least 8 bytes, we could then make1 << 3the minimum size ...". So it supports "allowed values should work", not a promise that the minimum stays at 2 bytes. - Coverage was assumed. PR #363 (LUCENE-10150,
61c15c8c10f, which addedreadLongs): "The method is tested well byBaseDirectoryTestCasesubclass already (aligned, unaligned, etc)". Those aligned and unaligned tests (BaseDirectoryTestCase.java:208-258) run on 1 KiB to 1 MiB chunks in the chunked directories (BaseChunkedDirectoryTestCase.java:47-48), and the two cross-boundary tests on 16- and 8-byte chunks (:364-400). At every block size of 8 bytes or more absolute and in-block alignment agree, so none of them can hit it. - The original guarded it. LUCENE-9027 (
7755cdf03fc, 2019) builtByteBufferIndexInput's long views withif (i < curBuf.limit()) ... else EMPTY_LONGBUFFERand took the alignment from the position inside the current buffer. - Same calls, other directory:
MMapDirectorywith 2-byte chunks returns the right values (study control, re-run on645d4fe14a20). - No javadoc, test, CHANGES entry or discussion accepts the exception.
The devil's-advocate check175 words
Strongest case for "not a bug": the trigger is an "Expert", non-default configuration (ByteBuffersDirectory and DataInput.readLongs are even @lucene.experimental); the only known users are tests; the failure is loud, not silent corruption; no report was found in about five years; and the reviewer who lowered the limit also said Lucene may raise the minimum to 8 bytes one day. "Do not use blocks that small", or a higher LIMIT_MIN_BITS_PER_BLOCK, would be an acceptable answer.
Why it does not settle it: "Expert" and @lucene.experimental warn about tuning and API stability, not about exceptions for documented values; the limit was lowered so tests could use tiny blocks, with the stated intent that permissible values work correctly; and the code already has a slow path for a value that crosses blocks, it just never gets there. The refute-lens cross-model reviewer rated this argument weak; the second cross-model reviewer, in the fact check, rated it moderate. What it does establish: the priority is low, and "won't fix" or a higher minimum would be defensible maintainer answers.
Recommended A. Align by the offset inside the block
Take the alignment from the offset inside the block: blockOffset(pos) & 0x7 in getLongBuffer and blockOffset(pos) & 0x3 in getFloatBuffer. Nothing changes for blocks of 8 bytes or more; smaller blocks fall through to the existing slow path. Validated: the test passes, as do all 777 store tests and core's format, lint, forbidden-API and license checks.
All options and trade-offs3 options ยท 562 words
Three options; option A was applied in a scratch worktree of main 645d4fe14a20 and run against the minimal test, every test class in org.apache.lucene.store and core's static checks.
Two lines in ByteBuffersDataInput.java (the study verifier's fix). blockOffset(pos) is what readLongs/readFloats already use to position the view (:332, :300).
--- a/lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java
+++ b/lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java
@@ -359,7 +359,7 @@
private FloatBuffer getFloatBuffer(long pos) {
// This creates a separate FloatBuffer for each observed combination of ByteBuffer/alignment
int bufferIndex = blockIndex(pos);
- int alignment = (int) pos & 0x3;
+ int alignment = blockOffset(pos) & 0x3;
int floatBufferIndex = bufferIndex * Float.BYTES + alignment;
if (floatBuffers[floatBufferIndex] == null) {
ByteBuffer dup = blocks[bufferIndex].duplicate();
@@ -372,7 +372,7 @@
private LongBuffer getLongBuffer(long pos) {
// This creates a separate LongBuffer for each observed combination of ByteBuffer/alignment
int bufferIndex = blockIndex(pos);
- int alignment = (int) pos & 0x7;
+ int alignment = blockOffset(pos) & 0x7;
int longBufferIndex = bufferIndex * Long.BYTES + alignment;
if (longBuffers[longBufferIndex] == null) {
ByteBuffer dup = blocks[bufferIndex].duplicate();Trade-offs
- For blocks of 8 bytes or more, and single-buffer inputs (
blockMask = ~0), the value is unchanged, so the fast path, the cached views and the results stay the same; the cost is one extra AND. - For smaller blocks the alignment is the real in-block offset, never past the block's limit; the view then has room for 0 values and every value takes the slow path. That is slow but correct, which is what tiny blocks are for.
What the checks validated
- Scratch worktree of
645d4fe14a20,./gradlew, OpenJDK 25.0.3: the minimal test fails on stock and passes with the fix, with assertions on and off. (The fact check re-ran the stock command in its own clone of645d4fe14a20with the same result.) - Every test class in
org.apache.lucene.store(seedDEADBEEF): the 28 existing classes plus the new test, 777 tests, 17 skipped, 0 failures, includingTestByteBuffersDataInput,TestByteBuffersDataOutput,TestByteBuffersDirectory,TestMultiByteBuffersDirectory,TestMultiMMapandTestMMapDirectory. :lucene:core:check -x testpasses (google-java-format, ecj lint, forbidden APIs, license checks). Not the full static-analysis suite: error-prone was skipped and eclint is not configured in that run.- Earlier, with the fix on the main jars: the study's 7-test class passes 7 of 7, including the
readFloatsmatrix, theListconstructor, theIndexInputcase and the live-docs index; the cross-model reviewer's own 5 tests and the fourTestByteBuffers*/TestMultiByteBuffersDirectorysuites pass (256 run, 0 failed).
In getLongBuffer/getFloatBuffer, if alignment is past the block's limit, use an empty view, the LUCENE-9027 pattern in ByteBufferIndexInput.
Trade-offs
Also sends the read to the slow path, but keeps an alignment that means nothing for small blocks and adds a branch. Not validated.
Catch IllegalArgumentException in readLongs/readFloats and fall back; or raise LIMIT_MIN_BITS_PER_BLOCK to 3 and reject List pages under 8 bytes.
Trade-offs
Catching hides other errors. Forbidding changes a documented public limit and removes the tiny-chunk coverage TestMultiByteBuffersDirectory relies on; rmuir floated an 8-byte minimum in the #360 thread, but conditional on index files being 8-byte aligned, which they are not today (the live-docs longs of the 2-byte-block index start at pos & 7 = 3). Not validated.
No query, document or index content can choose the block size, so a downstream service is exposed only if its own code configures blocks under 8 bytes.
Coverage-guided fuzzing with Jazzer: a typed write/read round-trip fuzzer over Lucene's DataOutput/DataInput implementations flagged the exception. An independent AI verifier then wrote its own 7-test class, derived the exact failure condition and validated the two-line fix; it was later cut to the one-test repro.
The method in full110 words
- Finder: the prior study's non-formal baseline, the Jazzer target
DataIOFuzzer: a typed write/read round trip onByteBuffersDataOutput/Input,ByteBuffersDirectory,MMapDirectory(16- and 64-byte chunks and the default) andNIOFSDirectory, with slices, clones,skipBytes, random access and checksums; 1.5 + 20 minutes, 278 k executions. - Verification: an independent AI verifier wrote a from-scratch 7-test class (block-size x alignment matrices for both methods, the
Listconstructor, aByteBuffersIndexInput, an end-to-end live-docs index, per-value andMMapDirectorycontrols), derived the exact condition from the code and validated the fix. - Minimization: cut to the one-method test (2026-10-29). Re-verified on main
645d4fe14a20and 10.5.2 (2026-11-01), with a cross-model review.
No matching report found in these searches (2026-11-01): GitHub issues, PRs and commits, the JIRA archive, four Apache mailing lists, downstream projects and the web, plus a scan of all 409 open PRs. Related: #11982 (closed; refactors this view code), #11992 (alignment tests on default blocks), #16500 (another method); none fixes this.
Searches4 searches
Closest items, none this bug: #363 / #11187 and #10691 (the introducing changes); #360 / #11192 (lowered the block limit; rmuir's review quote); PR #11982 (closed, unmerged), which refactors this view-buffer code and keeps the absolute alignment; stale open PR #11992, which refactors the view code of ByteBufferIndexInput (the MMapDirectory-side class, which keeps its limit guard) and adds per-alignment readFloats/readLongs tests for ByteBuffersDataInput on default 1 KiB blocks only, without touching ByteBuffersDataInput.java; open PR #16500 (random-access readBytes bounds in the same file, a different defect); open PRs #570 and #13981 (positional bulk reads through the same path, alignment unchanged); #10070 (LUCENE-9027, the guarded original). Searches for getLongBuffer and "newPosition > limit" return nothing. No match is a search result, not proof of absence. Gaps: GitHub search returns at most 50 results per query (the open-PR file scan covers that limit); private channels cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-11-01 | a scripted prior-report search (16 issue/PR and 3 commit queries, incl. "newPosition > limit"), 9 direct searches (e.g. asLongBuffer, minBitsPerBlock, "view buffer"), a getLongBuffer search, and the changed files of all 409 open PRs | no report; #11982 (this view code), #11992 (alignment tests on default blocks) and #16500 (another method) do not fix this |
| JIRA archive (LUCENE) | 2026-11-01 | 4 scripted queries plus 6 manual ones | no report; the introducing issues (LUCENE-9652, LUCENE-10150, LUCENE-10155), LUCENE-9027 and unrelated items such as LUCENE-10394 |
| Mailing lists (dev@, java-user@, issues@, commits@) | 2026-11-01 | 20 scripted queries, 12 more, and every ByteBuffersDataInput subject on issues@ and commits@ | no report |
| Downstream projects and the web | 2026-11-01 | 2 downstream code searches (Solr, Elasticsearch, OpenSearch) and 6 web queries | no report |
readLongs broke in 61c15c8c10f (#363, 2021-10-09), readFloats in 7b9f8751456 (2021-01-06); 6c6a3bd5bd5 (#360) let ByteBuffersDataOutput make 2-byte blocks. All three first ship in 9.0.0; 8.11 is not affected. It fails on all six releases run, 9.0.0 to 10.6.0, and on main; never fixed.
Versions tested7 rows
readLongs:61c15c8c10f"LUCENE-10150: override readLongs() in ByteBuffersDataInput (#363)" (rmuir, 2021-10-09) addedgetLongBufferwith(int) pos & 0x7.readFloats:7b9f8751456"LUCENE-9652: DataInput.readLEFloats for use by Lucene90VectorReader (#2175)" (msokolov, 2021-01-06) addedgetFloatBufferwith(int) pos & 0x3; renamedreadFloatsby LUCENE-9047 (b84e0c272b6).- Tiny output blocks:
6c6a3bd5bd5"LUCENE-10155: Refactor TestMultiMMap into a BaseChunkedDirectoryTestCase (#360)" (rmuir, 2021-10-09) lowered the minimum from 10 bits (1 KiB) toLIMIT_MIN_BITS_PER_BLOCK = 1. Before it, only theListconstructor (power-of-two pages, since 2018) could build blocks this small. - Commit-level check on the 9.0.0 jars: with the
ByteBuffersDataInputfrom before61c15c8c10f,readLongspasses andreadFloatsstill fails, so the floats half is older. - All 34 release tags from 9.0.0 to 10.6.0 contain both alignment lines and the same catch clauses (read); upstream main
f815e679bf6c(2026-11-01) still does. None of the three commits has a CHANGES entry.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | fails with assertions on and off; passes with the fix |
main a9779ef3c193, b83101cdd2 | affected | the study's 7-test class fails 5 of 7 (assertions on for a9779ef3c193; flags not recorded for b83101cdd2); no fix run |
| 10.6.0, 10.5.2 | affected | fails with assertions on and off (release jars) |
| 10.0.0, 9.12.3, 9.1.0 | affected | fails (release jars) |
| 9.0.0 | affected | fails with assertions on and off; readLongs and readFloats on 2-byte List pages both fail |
9.0.0 + ByteBuffersDataInput from before 61c15c8c10f | bisect | readLongs passes, readFloats still fails |
| 8.11.0, 8.11.4 | not affected | no readLongs/readFloats override; readLELongs on 2-byte pages returns 42 |
Reproduced by the study verifier's own tests, a current-main verification on main and 10.5.2, the history runs from 9.0.0 to 10.6.0, and this doc's gradle runs; an independent reviewer from another model family (OpenAI GPT-5.6) reproduced it with its own test and called it real, high confidence.
Checks and verdicts289 words
All checks agree: real, an exception only, fixed by the two-line change.
| Check | Verdict | What it found |
|---|---|---|
| study verifier | real | Own 7-test class on 7cecf795236c: 8 of 40 readLongs cells and 2 of 40 readFloats cells fail, exactly the condition; List constructor, IndexInput and live-docs index fail; controls pass; fix validated. |
| main refreshes | still reproduces | The study class on b83101cdd2 and a9779ef3c193: 7 run, 5 failed, the same set (assertions on for a9779ef3c193; no assertions-off or fix runs). |
| minimal repro | one test | Fails on main with assertions on and off (two seeds); passes with the fix. |
| current-main verification | real | Fails on main 645d4fe14a20 and 10.5.2, assertions on and off; the study class fails 5 of 7 and passes 7 of 7 with the fix. |
| cross-model review | real, high confidence | OpenAI GPT-5.6, refute lens: its own 5 tests, 3 fail (2-byte List pages for both methods, 4-byte output blocks for readLongs); controls pass; with the fix 5 of 5 and 256 store tests pass. |
| the version-history runs | since 9.0.0 | Release jars 9.0.0 to 10.6.0 fail; 8.11.0 control passes; commit-level class swaps separate the two halves. |
| this doc's gradle runs | fails / fix passes | Scratch worktree of 645d4fe14a20: stock fails both ways; with the fix the test, 777 store tests and core's static checks pass. |
| fact checks | real; wording corrected | An independent AI tool re-ran the repro on 645d4fe14a20 (standalone and gradle, assertions on and off), the fix, and its own block-size x alignment and live-docs probes; a second reviewer from another model family (OpenAI GPT-5.6) checked the text against the code and records. Neither changed the verdict; their corrections are applied. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 469 KB
Finding record, Lucene and upstream sources, repro files and diagram notes551 words
- Registry title
- ByteBuffersDataInput.readLongs and readFloats fail when blocks are smaller than 8 bytes
- Area
- store / ByteBuffersDataInput
- Priority
- Low, rank 1151: ByteBuffersDataInput.readLongs/readFloats fail on blocks smaller than 8 bytes, which only expert constructors and tests create, loud with no corruption.
- Impact class
- crash
- Impact note
- Only ByteBuffersDataInput instances built from blocks smaller than 8 bytes; default in-tree users are unaffected; released.
- Summary
- You read longs or floats in bulk from a ByteBuffersDataInput built from blocks smaller than 8 bytes. You expect the same values as reading them one at a time. Instead readLongs/readFloats throw IllegalArgumentException.
- Introduced
- LUCENE-10150 (#11187, PR #363, 61c15c8c10f) for readLongs and LUCENE-9652 (7b9f8751456) for readFloats; LUCENE-10155 (#360, 6c6a3bd5bd5) also let ByteBuffersDataOutput produce blocks under 8 bytes [read] (2021-01-06 (7b9f8751456); 2021-10-09 (61c15c8c10f, 6c6a3bd5bd5))
- First affected version
- 9.0.0 [exec: the minimal test fails with -ea and -da; read: git tag --contains, all three commits first ship in releases/lucene/9.0.0]; 8.11.0 and 8.11.4 not affected [exec]
- Affected versions
- 9.0.0 to 10.6.0 (all 34 releases) and main [read: every release tag has both alignment lines]; 9.0.0, 9.1.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main 645d4fe14a20 [exec]; 10.1.0 in the earlier study's release runs [exec]; 8.11.0 and 8.11.4 not affected [exec]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that agreed; not yet triaged by a person.
- Found by
- non-formal baseline: fuzzing/PBT
- Upstream and prior reports
- none found (study searches 2026-10-22; public-list check 2026-10-20..23)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test with assertions on and off, standalone (2026-10-29 and 2026-11-01) and through gradle in a scratch worktree (2026-11-01); the recommended fix in that worktree with the test, everyorg.apache.lucene.storetest class and:lucene:core:check -x test. Releases 10.5.2 and 10.6.0 (assertions on and off), 9.0.0 (on and off), 9.1.0, 9.12.3 and 10.0.0; 8.11.0 and 8.11.4 as the unaffected control. Earlier: the study's 7-test class on main7cecf795236c,b83101cdd2,a9779ef3c193and645d4fe14a20; the cross-model reviewer's own test onea57360912ed. Read: upstream mainf815e679bf6c(2026-11-01) still has both alignment lines. All runs used OpenJDK 25.0.3.
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java:50-54, :65-67, :296-325, :328-357, :359-369, :372-382, :436-438, :448-479 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataOutput.java:82, :87-88, :141-178 (at 645d4fe14a20)
- Who is affected
lucene/core/src/java/org/apache/lucene/store/ByteBuffersDirectory.java:111; lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataOutput.java:82, :141-178, :236-248, :554-565; lucene/core/src/java/org/apache/lucene/store/ByteBuffersIndexInput.java:202-206 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90LiveDocsFormat.java:131-139 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/store/TestMultiByteBuffersDirectory.java:29-49; lucene/test-framework/src/java/org/apache/lucene/tests/store/BaseChunkedDirectoryTestCase.java:47-48, :364-400; lucene/test-framework/src/java/org/apache/lucene/tests/store/BaseDirectoryTestCase.java:208-258, :320-376 (at 645d4fe14a20)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataOutput.java:87-88, :154; lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java:50-54; lucene/core/src/java/org/apache/lucene/store/DataInput.java:150-160, :176-188; lucene/core/src/java/org/apache/lucene/store/ByteBuffersDirectory.java:48 (at 645d4fe14a20)https://github.com/apache/lucene/pull/360 (rmuir review comment, 2021-10-07, re-read via the GitHub API 2026-11-01)https://github.com/apache/lucene/pull/363 (PR body)https://github.com/apache/lucene/pull/360 (rmuir's second comment in the same review thread, 2021-10-07, "expert" and the possible `1 << 3` minimum; re-read via the GitHub API 2026-11-01)lucene/test-framework/src/java/org/apache/lucene/tests/store/BaseDirectoryTestCase.java:208-258; BaseChunkedDirectoryTestCase.java:47-48, :364-400 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/store/ByteBuffersDataInput.java:65-67, :300, :332, :436-438 (at 645d4fe14a20)
- Prior reports
https://github.com/apache/lucene/pull/11992 (changed files, re-read via the GitHub API 2026-11-01: ByteBufferGuard, ByteBufferIndexInput, ByteBuffersDirectory, TestByteBuffersDataInput; not ByteBuffersDataInput.java)
- TestByteBuffersDataInputSmallBlockReadLongs.java:
repro/LFV-031/src/org/apache/lucene/store/TestByteBuffersDataInputSmallBlockReadLongs.java(40 lines)
assets/LFV-031/bug.png(469 KB)
