Two getField calls and two reads on one document are enough: the first read trips an assertion whose condition is the wrong way round.
LazyDocument(modulelucene/misc) wraps one document of a reader.getField(fieldInfo)returns a placeholder field whose value is read from the index only when you first ask for it, for example withstringValue().- A field can store several values. You call
getFieldonce per stored value, in stored order: the first placeholder stands for the first value, the second for the second. - The first read of a field loads the document and fills every placeholder of that field created so far. If you then ask for another placeholder,
getFielddrops the loaded copy, so the next read loads it again and fills the new placeholder too.
f = v0, v1: after the first getField there is 1 placeholder and 2 stored values, so the check 2 <= 1 fails on the first read; after the second getField it is 2 <= 2. The over-request (1 <= 2) passes and overruns the array. Select the still to open it at full size. Generated with AI, checked against the code.- Store one document with two values in field
f,v0andv1, and wrap it innew LazyDocument(reader, 0)(modulelucene/misc).getField(f)returns a placeholder whose value is loaded on first read; you call it once per stored value. - Call
getField(f)and read it, then callgetField(f)again and read that. You expectv0, thenv1:getFieldre-fetches the document when more placeholders are requested after a read, as its own comment says. - With assertions on, the first read throws
AssertionError: More lazy values then real values for field: f. The check isrealValues.length <= lazyValues.size(): 2 stored values, 1 placeholder so far, and 2 <= 1 is false. - With assertions off the same calls return
v0andv1. The case the message names, moregetFieldcalls than stored values (1 value, 2 calls), passes the check (1 <= 2) and fails in the loop right after it withArrayIndexOutOfBoundsException. - Every release from 4.2.1 (2013) to 10.6.0 and main is affected, with assertions on only. Requesting every value first and reading afterwards, as Solr does, works.
Code that uses the opt-in lucene/misc LazyDocument, runs with assertions on (typically test runs), and reads a value of a multi-valued field before requesting all its values, directly or during a StoredFieldVisitor visit. No other Lucene code uses the class. With assertions off the values are right.
Mechanism, terms and numbers643 words
LazyDocument.getField(FieldInfo) (module lucene/misc) returns a new placeholder on every call; its NOTE asks callers to call it once per stored value of the field, in stored order. The first read of a placeholder calls fetchRealValues, which loads the document and fills every placeholder of that field created so far. Before filling them it asserts realValues.length <= lazyValues.size() with the message "More lazy values then real values for field: " (LazyDocument.java:113-114), but the loop right after it reads realValues[i] for every placeholder (:116-121); since getField only ever adds non-null placeholders (:77-78), the loop is safe exactly when lazyValues.size() <= realValues.length: the condition is the reverse of what the message and the loop need. With assertions on, reading the first value of a two-valued field before the second getField call throws AssertionError, although getField resets the cached document whenever more placeholders are requested after a read (:80-86) and with assertions off the same calls return the right values. The case the message names, more getField calls than stored values, passes the assertion and fails in the loop with ArrayIndexOutOfBoundsException. It came with SOLR-4589 (2013, first released in 4.2.1) and is unchanged up to 10.6.0 and main. Production runs (assertions off) are never wrong; the fix is to swap the two sides.
- Placeholder (lazy field): the
LazyDocument.LazyFieldthatgetFieldreturns.lazyValuesis the list of placeholders created so far for one field;realValuesis every stored value of that field in the loaded document. - Over-request: calling
getFieldmore often than the field has stored values. ThegetFieldNOTE forbids it ("must be called once for each value of the field name specified in sequence that the values exist",LazyDocument.java:61-63).
getField(:68-88) appends a new placeholder to the field's list (:71-78) and setsdoc = null(:85), with the comment "edge case: if someone asks this LazyDoc for more LazyFields after other LazyFields from the same LazyDoc have been actuallized, we need to force the doc to be re-fetched so the new LazyFields are also populated" (:81-84).- A read such as
stringValue()(:170-172) callsgetRealValue()(:148-157), which callsfetchRealValueswhile the value is unset (:149-150).fetchRealValues(:107-122) loads the document if needed (getDocument(),:95-104), takeslazyValues(:110) andrealValues = d.getFields(name)(:111). - It asserts
realValues.length <= lazyValues.size()with the message "More lazy values then real values for field: " (:113-114), then setsrealValue = realValues[i]for everyi < lazyValues.size()(:116-121). The loop needslazyValues.size() <= realValues.length(it skips null placeholders, butgetFieldnever adds one,:77-78); the assertion checks the reverse. - So with assertions on it rejects a read made while fewer placeholders than stored values exist, which the loop handles fine, and it lets through the over-request, which the loop does not handle:
ArrayIndexOutOfBoundsExceptionat:119, with assertions on as well as off.
Stored f = v0, v1. After the first getField(f): 1 placeholder, 2 stored values; the first read asserts 2 <= 1 and fails. With assertions off the loop fills placeholder 0 with v0. The second getField(f) sets doc = null; its read reloads the document, asserts 2 <= 2 and fills both placeholders: v1. Only the first read ever fails, because after the field's last getField call the two counts are equal. Over-request: 1 stored value, 2 calls: 1 <= 2 holds and the loop throws Index 1 out of bounds for length 1.
The same assertion fires when the read happens inside a StoredFieldVisitor visit, before the visitor has requested the field's later values (an independent probe, on main and 10.5.2).
Exposure and likelihood191 words
- Who is hit. Code that uses
LazyDocument(no other Lucene class does: in the tracked sources onlyLazyDocument.javaandTestLazyDocument.javamention it) and runs with assertions enabled and reads a lazy value of a multi-valued field before the field's lastgetFieldcall, directly or from inside aStoredFieldVisitor. Assertions are typically enabled in test runs; Lucene's own build enables them for about 75% of test seeds. - What happens. A deterministic
AssertionErrorat that read. With assertions off the values are right: the hunt's bounded-exhaustive harness found 0 wrong values and 0 exceptions in 2,088,699 checks (1,986,359 value reads plus one whole-document check per schedule). - The guard does not guard. The misuse its message names (more
getFieldcalls than stored values) reaches anArrayIndexOutOfBoundsExceptioninstead of the intended assertion message, with assertions on. - Default vs opt-in. An opt-in module; assertions are off by default in the JVM. No scale is needed: two stored values.
- How likely. Rare. The usual flow, and Solr's (its stored-field visitor requests every value during the visit and reads afterwards, by reading its source), requests every value first. No public report in the 13 years since 4.2.1.
One JUnit test, one method: one document with f = v0, v1, then getField and a read, twice. Public API only, ByteBuffersDirectory, deterministic, single thread, about 2 s. It fails with assertions on and passes with them off, which is the bug.
/*
* 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.misc.document;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.FieldInfo;
import org.apache.lucene.index.FieldInfos;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestLazyDocumentReadBeforeLastValue extends LuceneTestCase {
public void testReadFirstValueBeforeRequestingSecond() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new StoredField("f", "v0"));
doc.add(new StoredField("f", "v1"));
w.addDocument(doc);
}
try (IndexReader reader = DirectoryReader.open(dir)) {
FieldInfo fi = FieldInfos.getMergedFieldInfos(reader).fieldInfo("f");
LazyDocument lazy = new LazyDocument(reader, 0);
// expected v0 then v1; actual (-ea): AssertionError from fetchRealValues on the first read
assertEquals("v0", lazy.getField(fi).stringValue());
assertEquals("v1", lazy.getField(fi).stringValue());
}
}
}
}
cp TestLazyDocumentReadBeforeLastValue.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/document/
cd $LUCENE && ./gradlew :lucene:misc:test --tests org.apache.lucene.misc.document.TestLazyDocumentReadBeforeLastValue -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false instead (the test then passes: v0, v1)
- Expected
v0from the first read andv1from the second, as with assertions off.- Observed
AssertionError: More lazy values then real values for field: fatLazyDocument.java:113, on the first read, with assertions on.
More files, environment, full output242 words
The test sits in org.apache.lucene.misc.document, LazyDocument's package, by Lucene test convention; it uses only public API (LazyDocument(IndexReader, int), getField(FieldInfo) and stringValue() on the IndexableField it returns; it never names LazyDocument.LazyField or getDocument(), which are marked @lucene.internal). The outcome does not depend on the seed or on the test framework's random codec.
Not shown by this one test, all from the same assertion and executed elsewhere: a read in the middle of a StoredFieldVisitor visit, and the over-request that reaches ArrayIndexOutOfBoundsException.
As the regression test: the same test unchanged, for example as a new method of TestLazyDocument. It fails on stock main with assertions on and passes with fix A with assertions on and off (gradle). :lucene:misc:check -x test passes with fix A and this test in place.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); this test also fails on 10.6.0; every release from 4.2.1 to 10.6.0 is affected with assertions on; 4.0.0 and 4.2.0 are not |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestLazyDocumentReadBeforeLastValue.java $LUCENE/lucene/misc/src/test/org/apache/lucene/misc/document/
cd $LUCENE && ./gradlew :lucene:misc:test --tests org.apache.lucene.misc.document.TestLazyDocumentReadBeforeLastValue -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false instead (the test then passes: v0, v1)
# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (with =false the test passes; the same on 10.6.0)
TestLazyDocumentReadBeforeLastValue > testReadFirstValueBeforeRequestingSecond FAILED
java.lang.AssertionError: More lazy values then real values for field: f
at __randomizedtesting.SeedInfo.seed([DEADBEEF:73F8C0A28644819C]:0)
at org.apache.lucene.misc.document.LazyDocument.fetchRealValues(LazyDocument.java:113)
at org.apache.lucene.misc.document.LazyDocument$LazyField.getRealValue(LazyDocument.java:150)
at org.apache.lucene.misc.document.LazyDocument$LazyField.stringValue(LazyDocument.java:171)
at org.apache.lucene.misc.document.TestLazyDocumentReadBeforeLastValue.testReadFirstValueBeforeRequestingSecond(TestLazyDocumentReadBeforeLastValue.java:45)
1 test completed, 1 failed- The message names the unsafe case, more placeholders than stored values; the condition rejects the opposite, harmless one.
getFieldsupports this order on purpose: it drops the cached document so later placeholders get filled (LazyDocument.java:81-85).- SOLR-4589, which added the assertion, named over-requesting as the hazard; before it, the same calls passed with assertions on (4.0.0, 4.2.0).
The full argument308 words
- Message vs condition. "More lazy values then real values" means
lazyValues.size() > realValues.length, the only state in which the loop at:116-121can index pastrealValues. The condition rejectsrealValues.length > lazyValues.size()instead, which the loop handles fine. - The code supports the order.
getFieldresets the cached document for exactly this "edge case" (:81-85), and the next read fills the new placeholder; with assertions off the values are correct. - Same commit, same style. The two assertions added beside it word their messages as the failure:
assert hasBeenLoaded() : "field value was not lazy loaded"andassert realValue.name().equals(name()) : "realvalue name != name: ..."(:152-154). - The author's stated intent (SOLR-4589). On 2013-03-16, before the patch, they wrote that a client app generating LazyField instances for the same field twice "will most likeley get an ArrayIndexOutOfBoundsError". On 2013-03-19, asked whether the re-fetch edge case happens in Solr: "no, but the way the public API for LazyDocument is defined, it is possible ... so i wanted to protect against it -- i included test randomization to verify that the edge case works." Nobody in the thread discusses the condition itself; reading it as reversed is an interpretation of the code. The quotes show that the author meant later
getFieldcalls after a read to work; they do not say whether such a call was expected for the same field or for another one. - Never tested. Upstream
TestLazyDocumentrequests every value inside the visitor before reading (TestLazyDocument.java:101-104,:203-215), and its step commented "use the same LazyDoc to ask for one more lazy field" builds a newLazyDocumentfor a different field (:161-165). - History. 4.0.0 and 4.2.0 pass the same read order with assertions on; 4.2.1, whose only
LazyDocumentchange is the SOLR-4589 backport, fails; 4.2.1 with only the condition flipped passes.
The devil's-advocate check181 words
The strongest case for "intended" (the cross-model reviews and the adjudication rate it moderate; the independent reproducer, the devil's advocate and the audit rebut it without rating it): the getField NOTE ("must be called once for each value of the field name specified in sequence that the values exist", :61-63) can be read as "request every value before reading any"; the author called the API "really only usable from a StoredFieldVisitor"; Solr and the upstream test always request everything first. On that reading the condition is deliberate and only the message is wrong.
Why it fails: no javadoc, CHANGES.txt or MIGRATE.md text states a timing rule (none explicitly allows early reads either), and getField explicitly handles requests after a read, which the author said they wanted to protect. Even on the strict reading the assertion is wrong, because it lets through the over-request its message describes. Verdict: not intended; an inverted assertion of low impact. Residual risk: a maintainer may read the API as meant only for visitors; the re-fetch branch, the message and the AIOOBE argue against that reading.
Recommended A. Swap the two sides of the assertion
Swap the two sides: assert lazyValues.size() <= realValues.length, and "then" to "than" in the message. Nothing changes with assertions off, and an over-request now gets the assertion message. Validated: the repro passes with assertions on and off, and the whole misc test suite and its static checks pass.
- assert realValues.length <= lazyValues.size()
- : "More lazy values then real values for field: " + name;
+ assert lazyValues.size() <= realValues.length
+ : "More lazy values than real values for field: " + name;All options and trade-offs3 options ยท 409 words
Fix A was run through gradle in a scratch worktree of 645d4fe14a20; B and C are alternatives, not tested.
One line in fetchRealValues (LazyDocument.java:113-114), plus the message typo.
- assert realValues.length <= lazyValues.size()
- : "More lazy values then real values for field: " + name;
+ assert lazyValues.size() <= realValues.length
+ : "More lazy values than real values for field: " + name;Trade-offs
- For every state
getFieldcan create (non-null placeholders only,LazyDocument.java:77-78) it accepts exactly the states the loop can handle and rejects the one it cannot, so the over-request now gets the assertion message instead of anArrayIndexOutOfBoundsExceptionwhen assertions are on (an independent reproducer's fix check). - No change at all with assertions off. Under the strict reading of the
getFieldNOTE, a read before the lastgetFieldcall is no longer flagged; it returns the right values, so nothing is lost.
What the checks validated
- Gradle in a scratch worktree of
645d4fe14a20(2026-11-01): the repro fails on stock main with-Ptests.asserts=trueand passes with=false; with A it passes with both. - With A: upstream
TestLazyDocument5 of 5 iterations with assertions on; the wholelucene/misctest suite with assertions on, 24 classes including the repro, 317 tests, 0 failures, 9 skipped (by the test sources 3@Nightlyand 6 that need anFSDirectory; the gradle log records only the count);:lucene:misc:check -x test(google-java-format, ecj lint of main and test, forbidden APIs) passes with A and the test in place. - Earlier, the same fix: the finder's repro and upstream
TestLazyDocument; an independent 3-test class 3/0 and upstreamTestLazyDocument5/5; the cross-model reviewers' own tests; the bounded-exhaustive harness with assertions on, 0 wrong values and 0 exceptions in 2,088,699 checks; 4.2.1 with the same flip.
Replace the assertion with if (lazyValues.size() > realValues.length) throw new IllegalStateException(...), so a caller that breaks the NOTE gets a clear message in production too.
Trade-offs
Changes behaviour with assertions off (an IllegalStateException instead of the ArrayIndexOutOfBoundsException), so it is a slightly bigger decision than a bug fix. Not tested.
assert lazyValues.size() == realValues.length.
Trade-offs
Fixes the over-request message but keeps rejecting the read-before-last-getField order that getField's re-fetch branch supports and that works with assertions off. Not recommended; not tested.
Solr's lazy field loading, the known downstream user, requests every value before reading and is not hit; other downstream code that reads early sees only a spurious AssertionError, and only in runs with assertions on.
Semi-formal: a bounded-exhaustive harness compared LazyDocument with the stored document over 5,117 small field layouts and random read orders that obey the getField contract. With assertions off it found 0 wrong values in 2,088,699 checks; with assertions on, 99,858 of 102,340 read orders threw this AssertionError.
The method in full196 words
Method: semi-formal (bounded-exhaustive check of the stock jar). A hunt unit auditing the lucene/misc collectors, selectors and document helpers wrote a harness that builds every small document shape (1-3 stored fields of 1-3 values each and every interleaving of the values: 5,117 shapes; each field's value type is drawn at random from six stored types), calls getField once per stored value in stored order (the documented contract), and after every getField call reads a random subset of the placeholders created so far (20 schedules per shape), comparing each value with storedFields().document().
With assertions off: 2,088,699 checks (1,986,359 value reads plus one whole-document check per schedule, 102,340), 0 wrong values, 0 exceptions. With assertions on: 99,858 of the 102,340 schedules threw AssertionError: More lazy values then real values; the shortest was get(f#0) get(f#1) read(f@0) on a three-valued field. With the condition flipped, assertions on: 2,088,699 checks, 0 wrong values, 0 exceptions.
The finder reduced it to a public-API JUnit test. It was then reviewed by a reviewer from another model family (OpenAI GPT-5.6), reproduced independently, argued against by an intended-behaviour devil's advocate, judged, re-run and audited, and cut to the one-test repro on 645d4fe14a20.
No prior report found in GitHub issues, PRs and commits (including all 409 open PRs), the JIRA archive (SOLR-4589 read in full), the Lucene and Solr lists, downstream code or the web (2026-10-27); a refresh on 2026-11-01 found nothing new. Not fixed upstream as of 2026-11-01.
Searches4 searches
Closest items, none a report of this bug: SOLR-4589 (2013, the change that added the assertion; its discussion never touches the condition); Solr-side LazyField issues about other behaviour (SOLR-13034, SOLR-15192, SOLR-15443, SOLR-7953, SOLR-10117 and related); the pre-4.0 FieldsReader lazy-field issues (LUCENE-690 and others, a different class); open PR #16847 (a 438-file PR for long doc ids whose only LazyDocument-related edit is a Math.toIntExact change in TestLazyDocument.java; it does not touch LazyDocument.java) and merged #16255 (a comment typo in LazyDocument.java). The Lucene.NET port carries the same reversed check, unreported; one unofficial Go port writes the corrected direction without any report or discussion. Limits: inline PR review comments are not searched; JIRA only through text queries; GitHub code search covers default branches only; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; downstream repositories; all of GitHub | 2026-10-27 | four scripted prior-report searches (45 queries each); manual GitHub searches on apache/lucene, apache/solr, apache/lucene-solr, apache/lucenenet, Elasticsearch and OpenSearch; a code search for the message "More lazy values" (80 distinct files compared) | no report; the Lucene.NET port has the same check; one unofficial Go port silently corrects it |
| Open PRs | 2026-10-27 | changed files of all 409 open apache/lucene PRs | none touches LazyDocument.java; #16847, a 438-file PR for long doc ids, has one LazyDocument-related edit, a Math.toIntExact change in TestLazyDocument.java |
| JIRA, mailing lists, web | 2026-10-27 | JIRA text queries on all projects plus SOLR-4589 read in full (21 comments); the Lucene and Solr lists; 3 web queries | only related Solr LazyField issues and the origin discussion |
| Refresh and upstream branches | 2026-11-01 | apache/lucene issues and PRs mentioning LazyDocument updated since 2026-10-26, fetchRealValues and "lazy values"; commits touching the file on main and branch_10_6 | nothing new; on main the file last changed 2026-06-14 (#16255); main f815e679bf6c and branch_10_6 still have the reversed condition |
Introduced in 4.2.1 by SOLR-4589 (ad81236c7d5, 2013, a performance fix for lazy loading of multi-valued fields), which added fetchRealValues with the reversed assertion; never changed since. Executed on 13 releases from 4.2.1 to 10.6.0 and on main: all fail with assertions on; 4.0.0 and 4.2.0 pass.
Versions tested6 rows
Introduced by ad81236c7d5 "SOLR-4589: Fixed CPU spikes and poor performance in lazy field loading of multivalued fields" (by Chris M. Hostetter, 2013-03-20, svn r1458887 on trunk; Subversion era, no pull request). It replaced the per-value ordinal lookup with fetchRealValues, added the assertion with the reversed condition, the getField NOTE and TestLazyDocument. Backports 29756e22101 (lucene_solr_4_2, first release 4.2.1, tagged 2013-04-03) and dfd2d1f49cd (branch_4x, 4.3.0). On every released line git log -S on the condition finds only these three commits (the other hits, with --all, are side-branch deletions or rewrites of the file), and the condition is present verbatim in all 119 release tags from 4.2.1 to 10.6.0 (absent at 4.2.0); later edits renamed types (2016), reformatted (2021) and moved the package (9.0.0). Commit isolation: the backport is the only LazyDocument change between 4.2.0 and 4.2.1, and 4.2.1 with only the condition flipped passes.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and passes with them off (gradle and standalone); passes with fix A |
| 10.6.0 | affected | minimal repro fails with assertions on, passes with them off (release jars) |
| 4.2.1 | first affected | fails with assertions on (LazyDocument.java:115), passes with them off; with only the condition flipped it passes |
4.3.0, 4.10.4, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2; main a9779ef3c193 | affected | fail with assertions on; the request-all-then-read control passes wherever it was run (all but 4.3.0) |
| 4.0.0, 4.2.0 | before SOLR-4589 | the same read order passes with assertions on (no fetchRealValues, no assertion) |
upstream main f815e679bf6c, branch_10_6 | read | the same assertion; on main the file last changed 2026-06-14 (#16255, a comment typo) |
Confirmed with high confidence: the finder, an independent reproducer, reviews by a reviewer from another model family (OpenAI GPT-5.6), an intended-behaviour devil's advocate, an adjudication and a credibility audit all reproduced it. All but the finder, which reported it, and the audit, which confirmed it, called it real; the cross-model reviews and the adjudication rated the intended argument moderate.
Checks and verdicts281 words
Everyone agrees on the root cause, the one-line fix and the low impact: an assertion-only failure, correct values with assertions off, an opt-in module.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Bounded-exhaustive harness on the stock jar (above); JUnit fails with assertions on and passes with them off on main, fails on 10.5.2 and 9.12.3; the fix passes the repro and upstream TestLazyDocument. |
| cross-model review, refute lens (OpenAI GPT-5.6) | real, high | Own test fails with assertions on and passes with them off; its request-all-then-read control passes; the fix passes upstream TestLazyDocument; intended argument moderate. |
| independent reproducer | real | Own 3-test class (3 stored values plus a single-valued field) fails 1/3 with assertions on on main, 10.5.2, 9.12.3 and 8.11.4, passes 3/3 with them off; its over-request probe throws ArrayIndexOutOfBoundsException either way; its fix passes 3/3. |
| cross-model review, intended lens (OpenAI GPT-5.6) | real, high | Own interleaved test fails with assertions on and passes with them off, re-run on a9779ef3c193; intended argument moderate. |
| intended-behaviour devil's advocate | real, high | Read SOLR-4589 and all six patch revisions; its mid-visit StoredFieldVisitor probe fails with assertions on on main and 10.5.2 and returns v0, v1, v2 with them off. |
| adjudication | confirmed, high | New seeds: the finder's repro fails with assertions on and passes with them off; the independent test fails 1/3; the fix passes. |
| re-run and credibility audit | confirmed | Re-runs on a9779ef3c193 with new seeds reproduce it; impact minor confirmed. |
| minimal repro and this doc | still reproduces | The one-test repro fails on 645d4fe14a20 and on 10.6.0 with assertions on and passes with them off; fix A validated with gradle. |
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 391 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,215 words
- Registry title
- LazyDocument.fetchRealValues checks its value-count assertion in the wrong direction: with assertions enabled, reading a lazy value of a multi-valued field before that field's last getField call throws AssertionError ('More lazy values then real values'), while the over-request case the message names is not caught and fails with ArrayIndexOutOfBoundsException
- Area
- misc / document: LazyDocument (lazy loading of stored fields, multi-valued fields)
- Priority
- Low, rank 1177: A released wrong-direction assertion in the opt-in misc LazyDocument throws only under -ea and returns correct values with -da.
- Impact class
- minor
- Impact note
- Hits only JVMs run with assertions enabled (test suites, or applications started with -ea) that use the opt-in lucene/misc LazyDocument and read a lazy value of a multi-valued stored field before making the last getField call for that field (directly, or by reading during a StoredFieldVisitor visit): the read throws AssertionError although with -da the same calls return the right values; the usual flow (request every value, then read) and upstream's TestLazyDocument are not affected, no scale is needed, and the reversed check also lets the forbidden over-request through as ArrayIndexOutOfBoundsException instead of the assertion message.
- Summary
- For a document whose stored field f has the values v0 and v1, you call LazyDocument.getField(f), read the returned field, then call getField(f) again and read the second one; getField's own comment says the document is re-fetched when more lazy fields are requested after others were loaded, so you expect v0 and then v1. With assertions enabled the first read instead throws AssertionError 'More lazy values then real values for field: f', because fetchRealValues asserts realValues.length <= lazyValues.size() (LazyDocument.java:113-114), the reverse of what its message and the loop right after it (:116-121, which reads realValues[i] once per lazy value) need. With -da the same calls return v0 and v1, and the case the message describes (more getField calls than stored values) passes the assertion and fails with ArrayIndexOutOfBoundsException in both modes.
- Introduced
- ad81236c7d52fac6f681b670055a22d3e13ab3ee (SOLR-4589, svn r1458887 on trunk, by hossman, 'Fixed CPU spikes and poor performance in lazy field loading of multivalued fields'; Subversion era, no pull request): it replaced the per-value ordinal lookup with fetchRealValues and added the assertion with the reversed condition; backports 29756e22101 (lucene_solr_4_2, r1458959, first release 4.2.1) and dfd2d1f49cd (branch_4x, r1458938, 4.3.0). The condition and message never changed afterwards (later commits only renamed types, reformatted, and moved the package in 9.0.0) [read: git log -S and git log -L in the full-history clone; commit isolation executed: the backport is the only LazyDocument change between 4.2.0 and 4.2.1, 4.2.0 passes the probe with -ea, 4.2.1 fails it, and 4.2.1 with only the condition flipped passes] (2013-03-20)
- First affected version
- 4.2.1 [exec] (4.2.0 and 4.0.0 not affected [exec]; LazyDocument first shipped in 4.0.0-alpha with an ordinal-based implementation that had neither fetchRealValues nor the assertion)
- Affected versions
- Every release from 4.2.1 to 10.6.0 and main (a9779ef3c193, 645d4fe14a20), only with assertions enabled [exec on 4.2.1, 4.3.0, 4.10.4, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main with era-specific probes; the minimal repro fails with -ea and passes with -da on the 10.6.0 release jars and on main 645d4fe14a20 (2026-11-01); the finder's JUnit fails with -ea on main, 10.5.2 and 9.12.3 and passes with -da; the independent reproduction's test fails with -ea on main, 10.5.2, 9.12.3 and 8.11.4 and passes with -da; the assertion line is unchanged in all 118 release tags from 4.2.1 to 10.5.2 in the history clone (8.8.2-8.11.3 not tagged there) [read]]. With -da every executed version returns the right values. Not fixed: upstream main a95c754ee003 (2026-10-26), branch_10x and branch_10_6 still have the reversed condition, and no open PR touches LazyDocument.java [read].
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (searched 2026-10-27, read-only): four scripted prior-report searches (45 queries each over GitHub issues and PRs, commits, downstream repositories, the dev, java-user, issues and commits lists and the JIRA archive), manual GitHub searches on apache/lucene, apache/solr, apache/lucene-solr, apache/lucenenet, Elasticsearch and OpenSearch, a GitHub code search for the message ('More lazy values': 80 distinct files compared), the file lists of all 409 open apache/lucene PRs, JIRA text queries plus SOLR-4589 read in full (description and 21 comments), list searches on the Lucene and Solr lists, and 3 web queries. No issue, PR, JIRA ticket, list post or downstream report mentions the reversed assertion, its message, an AssertionError from fetchRealValues, or reading before the last getField call. Related only: SOLR-4589 (the change that added the assertion; its author warned before the patch that over-requesting gives ArrayIndexOutOfBoundsException and said they wanted to support requesting more lazy fields after others were loaded), Solr-side LazyField issues (SOLR-13034, SOLR-15192, SOLR-15443, SOLR-7953, SOLR-10117 and related), the pre-4.0 FieldsReader lazy-field issues (LUCENE-690 and others, a different class), #16847 (open; touches only TestLazyDocument.java) and #16255 (merged; fixed a comment typo in LazyDocument.java). The Lucene.NET port carries the same reversed check; one unofficial Go port silently uses the corrected direction, with no report or discussion. Not fixed: upstream main a95c754ee003 (2026-10-26). Gaps: inline PR review comments are not searched, JIRA only through text queries, GitHub code search covers default branches of public repositories only, private reports are not searchable.
- Lucene commit tested
- main
645d4fe14a20(the minimal repro through gradle with-Ptests.asserts=trueand=false, stock and with fix A; fix A with upstreamTestLazyDocument(5 iterations), the wholelucene/misctest suite and:lucene:misc:check -x test; standalone runs with assertions on and off and a second seed), the 10.6.0 release jars (SHA-1 checked; assertions on and off), and, by the hunt and its verifiers, maina9779ef3c193andea57360912edand the releases 4.0.0, 4.2.0, 4.2.1, 4.3.0, 4.10.4, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2.LazyDocument.javaandTestLazyDocument.javaare byte-identical atea57360912ed,a9779ef3c193and645d4fe14a20; upstream mainf815e679bf6c(2026-11-01) has the same file andbranch_10_6the same assertion (read).
- Finding record
a Lucene main checkout lucene/misc/src/java/org/apache/lucene/misc/document/LazyDocument.java:38-39 (class javadoc), :61-66 (getField NOTE 'must be called once for each value of the field name specified in sequence that the values exist'), :68 (getField), :81-85 (re-fetch comment, doc = null), :113-114 (assert realValues.length <= lazyValues.size() : 'More lazy values then real values for field: '), :116-121 (loop setting f.realValue = realValues[i] for i < lazyValues.size()), :152-153 (adjacent assertions from the same commit whose messages state the failure); identical in Lucene main ea57360912ed
- The finding in 30 seconds
lucene/misc/src/java/org/apache/lucene/misc/document/LazyDocument.java:61-63, :68-88, :95-104, :107-122, :148-157, :170-172 (at 645d4fe14a20)
- Who is affected
build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:240-248 (at 645d4fe14a20)git grep -l LazyDocument (at 645d4fe14a20)
- Why it looks unintended
lucene/misc/src/java/org/apache/lucene/misc/document/LazyDocument.java:61-63, :80-86, :113-121, :152-154 (at 645d4fe14a20)lucene/misc/src/test/org/apache/lucene/misc/document/TestLazyDocument.java:101-104, :161-165, :203-215 (at 645d4fe14a20)
- TestLazyDocumentReadBeforeLastValue.java (JUnit, one test):
repro/LFV-697/src/org/apache/lucene/misc/document/TestLazyDocumentReadBeforeLastValue.java(50 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.
assets/LFV-697/bug.png(391 KB)
