The highlighter keeps the best passages in a java.util.PriorityQueue whose starting size is Math.min(64, maxPassages + 1). The 64 keeps big limits cheap; the + 1 matches the one extra passage it holds for a moment. For Integer.MAX_VALUE the + 1 overflows to a negative size, and the JDK refuses it.
- The UnifiedHighlighter cuts a field's text into passages (by default, sentences), scores each passage that contains a query match, and joins the best ones into the snippet.
maxPassagesis how many of those top-scoring passages a snippet may use.highlight(field, query, topDocs, maxPassages),highlightFields(fields, query, topDocs, int[] maxPassages)andhighlightWithoutSearcher(field, query, content, maxPassages)take it as a plainintwith no documented range.
maxPassages = Integer.MAX_VALUE, how line 143 turns 2,147,483,647 + 1 into -2,147,483,648, the JDK's size check, expected vs actual for each call, and why it is a bug. Select the still to open it at full size. Generated with AI, checked against the code.- You highlight the text
a testfor the querybody:testwithhighlightWithoutSearcher("body", query, "a test", 1000)and geta <b>test</b>. - You pass
Integer.MAX_VALUE(2,147,483,647) instead, to mean "no limit". You expect the samea <b>test</b>: the javadoc sets no upper bound, and 1, 3, 64, 1000 and 2,147,483,646 all give it. - You get an
IllegalArgumentExceptionwith no message, thrown by thejava.util.PriorityQueueconstructor. WithhighlightFieldsand per-field limits{1, Integer.MAX_VALUE}the whole call fails, so the field that asked for one passage loses its snippet too. - Why: the queue's starting size is
Math.min(64, maxPassages + 1). 2,147,483,647 + 1 wraps to -2,147,483,648,Math.minpicks it, and the JDK rejects a size below 1. A document without a match never builds the queue (since 7.3.0), so it still gets its summary.
Anyone who passes Integer.MAX_VALUE as "no limit", directly or by forwarding a client's snippet count. Every document with a match fails, and a multi-field call loses all its snippets. The error has no message and is not the documented one. It fails loudly on the first matching document; no wrong results, hang or index damage was observed.
Mechanism, terms and numbers605 words
The UnifiedHighlighter keeps a field's best maxPassages passages in a java.util.PriorityQueue, created in FieldHighlighter.highlightOffsetsEnums with the initial capacity Math.min(64, maxPassages + 1) (FieldHighlighter.java:141-143 on main 645d4fe14a20). The 64 cap keeps the eagerly allocated initial array small for large limits; the + 1 matches the queue briefly holding one passage more than the limit (why it was written this way is not recorded). For maxPassages = Integer.MAX_VALUE, a conventional "no limit" value (Lucene's own javadocs use it that way, in this module too: MatchRegionRetriever, FastVectorHighlighter), the + 1 wraps to Integer.MIN_VALUE, Math.min returns it, and the JDK constructor throws IllegalArgumentException with no message.
With the stock highlighter that happens for every document whose field has a match; it was executed through highlight, highlightFields and highlightWithoutSearcher, with offsets in postings and with re-analysis, with assertions on and off; in a multi-field call the other fields lose their snippets too. Every limit from 1 to Integer.MAX_VALUE - 1 gives a valid capacity and every one tested works; since 7.3.0, documents without a match are summarized normally. The expression has been there since the UnifiedHighlighter was added (LUCENE-7438, 2016); every release from 6.3.0 to 10.6.0 and main is affected. A one-line overflow-safe clamp fixes it.
- Entry points.
highlight(field, query, topDocs, maxPassages)wraps the value in an array and callshighlightFields(UnifiedHighlighter.java:766-770), which ends inhighlightFieldsAsObjects(:858). That method checks only "fieldsIn must not be empty" and "invalid number of maxPassagesIn" (:892-897), plus that a searcher is set (:898-902), never the values, and builds oneFieldHighlighterper field with its own count (:922-924).highlightWithoutSearcherdoes the same for one string (:1096-1107). - No match, no queue.
highlightFieldForDoccallshighlightOffsetsEnums(FieldHighlighter.java:86), which returnsnew Passage[0]when there is no term occurrence (:137-139); the summary path then sizes its list withMath.min(maxPassages, 10)(:112). SoInteger.MAX_VALUEworks for documents without a match. - The wrap. With a match,
:141-143buildsnew PriorityQueue<>(Math.min(64, maxPassages + 1), comparator).2147483647 + 1 = -2147483648andMath.min(64, -2147483648) = -2147483648; for2147483646the expression isMath.min(64, 2147483647) = 64. - The throw. JDK 25
PriorityQueue(int, Comparator):if (initialCapacity < 1) throw new IllegalArgumentException();, thennew Object[initialCapacity](PriorityQueue.java:173-175). The queue grows on demand, so 64 is only a starting size. - The
+ 1.maybeAddPassage(FieldHighlighter.java:201-224) offers the new passage and polls the lowest whensize() > maxPassages(:215-217), so the queue briefly holdsmaxPassages + 1entries, which the+ 1matches (the original rationale is not recorded).
On 645d4fe14a20: the minimal test and a probe (highlightWithoutSearcher with 1, 3, 64, 1000, MAX_VALUE - 1, MAX_VALUE; a text without a match; highlightFields with {1, MAX_VALUE}). On ea57360912ed, whose highlighter tree is identical: an independent reproducer's six tests (offsets in postings, re-analysis, highlightWithoutSearcher, {1, MAX_VALUE}, two controls with 3, 63, 64, 1000, MAX_VALUE / 2, MAX_VALUE - 1).
| call | main today | expected |
|---|---|---|
maxPassages 1, 63, 64, 1000, MAX_VALUE / 2, MAX_VALUE - 1, document with a match | the snippet | same |
maxPassages = MAX_VALUE, document without a match | the summary | same |
maxPassages = MAX_VALUE, document with a match (highlight with offsets in postings or re-analysis, highlightWithoutSearcher) | IllegalArgumentException, no message | the same snippet, or a clear rejection |
highlightFields with {1, MAX_VALUE} | IllegalArgumentException; both fields' snippets lost | both fields highlighted, or a clear rejection |
Exposure and likelihood207 words
- Who is hit. Code that calls
highlight,highlightFieldsorhighlightWithoutSearcher(or a subclass throughhighlightFieldsAsObjects) withmaxPassages = Integer.MAX_VALUE, directly or by forwarding a client's snippet count without an upper bound. - Default vs opt-in. Default highlighter settings; the trigger is the single argument value
Integer.MAX_VALUE. Any field with a match: by reading, every offset source reaches the same line, and offsets from postings and from re-analysis were executed. - What goes wrong. The call throws on the first matching document, so the request gets no snippets; in a multi-field call all fields lose theirs. The exception has no message and is not the documented one (the only documented
IllegalArgumentExceptionis for fields indexed without offsets), so it points the caller at the wrong cause. - Scale. None needed: one document, one sentence. It failed with every recorded seed, assertions on and off.
- Workaround. Pass
Integer.MAX_VALUE - 1or any realistic bound. - How likely. Rare:
Integer.MAX_VALUEis a conventional "no limit" value (in the same module,MatchRegionRetrieverasks forInteger#MAX_VALUE"for all hits to be considered" andFastVectorHighlighterdocuments its default phrase limit as "unlimited (Integer.MAX_VALUE)"), but the failure is loud on the first matching document, and the recorded searches found no report in ten years.
One JUnit method on the public API: highlight a test for body:test with maxPassages = 1000, then with Integer.MAX_VALUE, and compare. No index, no searcher, no random() calls; it fails with assertions on and off and with every recorded seed.
/*
* 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.search.uhighlight;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestUnifiedHighlighterMaxValueMaxPassages extends LuceneTestCase {
public void testMaxValueMaxPassages() throws Exception {
UnifiedHighlighter highlighter =
UnifiedHighlighter.builderWithoutSearcher(new StandardAnalyzer()).build();
Query query = new TermQuery(new Term("body", "test"));
Object expected = highlighter.highlightWithoutSearcher("body", query, "a test", 1000);
// expected: the same snippet as maxPassages = 1000; actual: a message-less IllegalArgumentException
try {
assertEquals(
expected,
highlighter.highlightWithoutSearcher("body", query, "a test", Integer.MAX_VALUE));
} catch (IllegalArgumentException e) {
throw new AssertionError("expected " + expected + " for maxPassages = MAX_VALUE, got " + e, e);
}
}
}
cp $REPRO/TestUnifiedHighlighterMaxValueMaxPassages.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests org.apache.lucene.search.uhighlight.TestUnifiedHighlighterMaxValueMaxPassages -Ptests.seed=DEADBEEF
- Expected
a <b>test</b>, the same snippet thatmaxPassages = 1000gives.- Observed
IllegalArgumentExceptionwith no message, from thejava.util.PriorityQueueconstructor atFieldHighlighter.java:143.
More files, environment, full output238 words
Copy the test into lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/ of a Lucene checkout and run it with gradle (command below). The IllegalArgumentException is caught only to rethrow it as the cause of an AssertionError that states expected vs actual; the cause keeps the PriorityQueue and FieldHighlighter frames. With the clamp fix it passes unchanged, so it doubles as the regression test.
Before using it in a PR, run ./gradlew tidy: google-java-format re-wraps the comment and the throw line (both over 100 columns); the code does not change, and the re-wrapped file passes checkGoogleJavaFormat.
Executed on main 645d4fe14a20: gradle in a clean scratch worktree (fails), and the standalone runner on the stock main jars with assertions on and off (fails); it also fails on the 10.6.0, 10.5.2 and 9.12.3 release jars.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; also fails on the 10.6.0, 10.5.2 and 9.12.3 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (plain int arithmetic, nothing platform-specific) |
cp $REPRO/TestUnifiedHighlighterMaxValueMaxPassages.java $LUCENE/lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/
cd $LUCENE && ./gradlew :lucene:highlighter:test --tests org.apache.lucene.search.uhighlight.TestUnifiedHighlighterMaxValueMaxPassages -Ptests.seed=DEADBEEF
# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF
TestUnifiedHighlighterMaxValueMaxPassages > testMaxValueMaxPassages FAILED
java.lang.AssertionError: expected a <b>test</b> for maxPassages = MAX_VALUE, got java.lang.IllegalArgumentException
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C5AAB44B573F209A]:0)
at org.apache.lucene.search.uhighlight.TestUnifiedHighlighterMaxValueMaxPassages.testMaxValueMaxPassages(TestUnifiedHighlighterMaxValueMaxPassages.java:38)
Caused by:
java.lang.IllegalArgumentException
at java.base/java.util.PriorityQueue.<init>(PriorityQueue.java:174)
at org.apache.lucene.search.uhighlight.FieldHighlighter.highlightOffsetsEnums(FieldHighlighter.java:143)
at org.apache.lucene.search.uhighlight.FieldHighlighter.highlightFieldForDoc(FieldHighlighter.java:86)
at org.apache.lucene.search.uhighlight.UnifiedHighlighter.highlightWithoutSearcher(UnifiedHighlighter.java:1107)
at org.apache.lucene.search.uhighlight.TestUnifiedHighlighterMaxValueMaxPassages.testMaxValueMaxPassages(TestUnifiedHighlighterMaxValueMaxPassages.java:36)
# standalone runner on the built or released jars, seed DEADBEEF
main 645d4fe14a20 -ea / -da tests_run=1 failures=1 same cause
10.6.0, 10.5.2, 9.12.3 -ea tests_run=1 failures=1 same cause- The javadoc sets no upper bound on
maxPassages; the only documentedIllegalArgumentExceptionis for fields without offsets. - The 64 cap keeps big limits cheap: every value from 1 to 2,147,483,646 gets a valid size, and
Integer.MAX_VALUEalready works for documents without a match. - Intended rejection looks different: in the same class,
withMaxLengthrejectsInteger.MAX_VALUEup front, with a message.
The full argument229 words
- Contract. Every overload documents
maxPassagesonly as "The maximum number of top-N ranked passages [per-field] used to form the highlighted snippets" (UnifiedHighlighter.java:757-758,:819-820,:845-846,:881-882,:1089-1090), with no range, and nothing validates the values (:892-897). The only documentedIllegalArgumentExceptionis for fields indexed without offsets (:744-745,:763-764,:793-794,:825-826,:851-852,:887-888). - Built for large limits. The cap of 64 keeps a large count from allocating a large queue; every value from 64 to
MAX_VALUE - 1gets capacity 64. The predecessorPostingsHighlightersized its queue from the raw count and threwOutOfMemoryError("Requested array size exceeds VM limit") for bothMAX_VALUEandMAX_VALUE - 1(executed on 6.2.1); the cap avoids that for every value except the one where+ 1wraps. - Intended rejection looks different.
setMaxLengthandBuilder.withMaxLengthrejectInteger.MAX_VALUEup front with "maxLength must be < Integer.MAX_VALUE" (:192-196,:355-360); their comment names a different+ 1hazard (BreakIterator.preceding(offset+1)and the offsets-queue sentinel).maxPassagesnever had such a guard. - Tests. No upstream test passes
maxPassages = Integer.MAX_VALUE; theuhighlighttests usewithMaxLength(Integer.MAX_VALUE - 1)only. - History and precedent. LUCENE-7438 (#8490) and its 28 comments never discuss the queue capacity or
Integer.MAX_VALUE. LUCENE-10135 (#11172, PR #334, 9.0.0) fixed a similarMAX_VALUEoverflow ofmaxPassageWindowin MatchHighlighter'sPassageSelector.
The devil's-advocate check201 words
The strongest case for "works as designed": no contract promises Integer.MAX_VALUE; UnifiedHighlighter is @lucene.experimental and FieldHighlighter @lucene.internal; 2^31 - 1 passages is unattainable (by default a stored value is truncated to maxLength, 10,000 characters, when it is loaded, so a field read from the index has at most 10,000 passages); the classic Highlighter in the same module also rejects maxNumFragments MAX_VALUE and MAX_VALUE - 1 ("maxSize must be >= 0 and < 2147483631", executed); and Elasticsearch and OpenSearch pass MAX_VALUE - 1 as their "all fragments" value.
Why it fails: the classic Highlighter has no bounds check of its own (only Math.max(1, maxNumFragments)); its rejection comes from the range check of Lucene's generic org.apache.lucene.util.PriorityQueue (commented "Throw exception to prevent confusing OOME"), a stable limit with a message that names it. Here every value but one works, and that one fails inside a JDK collection, without a message and only for documents with a match. The experimental tag allows API changes, not an undocumented error from an accepted argument, and the downstream MAX_VALUE - 1 is an unexplained value choice, not a documented limit. Verdict: a defect, low severity; clamping or rejecting with a message both fix it.
Recommended Overflow-safe cap on the initial queue capacity (one line)
Make the cap overflow-safe: maxPassages >= 64 ? 64 : maxPassages + 1 at FieldHighlighter.java:143, one line. Every value below Integer.MAX_VALUE keeps today's starting size, and Integer.MAX_VALUE gets 64. With it, the minimal test and all 723 highlighter tests pass.
PriorityQueue<Passage> passageQueue =
new PriorityQueue<>(
maxPassages >= 64 ? 64 : maxPassages + 1, // no int overflow for Integer.MAX_VALUE
(left, right) -> {All options and trade-offs2 options · 504 words
Two options; the clamp is preferred here for compatibility, and rejecting is also defensible. The clamp was validated on 645d4fe14a20; rejecting the value with a message was not executed. Either way, do not size the queue from maxPassages itself (for example new PriorityQueue<>(maxPassages + 1) or long arithmetic without the cap): java.util.PriorityQueue allocates its initial array eagerly, the predecessor PostingsHighlighter threw OutOfMemoryError that way for both MAX_VALUE and MAX_VALUE - 1, and a downstream engine capped huge fragment counts after the classic Highlighter allocated gigabytes (elastic/elasticsearch#155507). A natural combination: the clamp for this bug, plus a maxPassages < 1 check for the maxPassages = 0 failure (a separate bug, a NullPointerException at FieldHighlighter.java:212).
In FieldHighlighter.highlightOffsetsEnums, compute the initial capacity without overflow. (int) Math.min(64L, maxPassages + 1L) is equivalent.
PriorityQueue<Passage> passageQueue =
new PriorityQueue<>(
maxPassages >= 64 ? 64 : maxPassages + 1, // no int overflow for Integer.MAX_VALUE
(left, right) -> {Trade-offs
- The same initial capacity as today for every value below
Integer.MAX_VALUE:maxPassages + 1up to 63, 64 from 64 toInteger.MAX_VALUE - 1;Integer.MAX_VALUEnow gets 64 as well. The initial allocation stays bounded. - It does not change
maxPassages = 0(a separateNullPointerExceptionatFieldHighlighter.java:212) or negative values, which stay invalid and are not claimed: the new expression givesn + 1for them, like the old one. On main (executed, -1, -5 andInteger.MIN_VALUE), a document with a match gets the same message-less exception at line 143, and a document without a match fails earlier in the summary path (new ArrayList<>(Math.min(maxPassages, 10)), line 112) with "Illegal Capacity: <n>". - What the validation covers: the positive boundary (
Integer.MAX_VALUE) and the existing highlighter suites; no negative input was run with the fix, and belowInteger.MAX_VALUEthe new expression equals the old one.
What the checks validated
On a clean scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF): the minimal test fails unmodified and passes with this line; the closest suite TestUnifiedHighlighter (156 tests) and the whole :lucene:highlighter:test (35 suites, 723 tests, including the minimal test) pass with 0 failures (the suite and skip counts read from the JUnit XML of the fact check's identical re-run); checkGoogleJavaFormat passes.
Earlier runs agree: the minimal test passes with the fix shadowed (-ea and -da); the finder's 2 tests and the independent reproducer's 6 tests pass with it, the reproducer's also with the long-arithmetic form.
Check the counts in highlightFieldsAsObjects and highlightWithoutSearcher, as withMaxLength does for maxLength: == Integer.MAX_VALUE covers this bug; < 1 would also turn the maxPassages = 0 failure into a clear error. Not executed.
Trade-offs
Clear error instead of a JDK one, consistent with withMaxLength. But the conventional "no limit" value becomes an error that callers must work around with Integer.MAX_VALUE - 1, while every other large value stays legal.
By code reading only (nothing run downstream): Solr passes hl.snippets to maxPassages unchecked, so hl.snippets=2147483647 should fail the same way. Elasticsearch rejects counts above 10,000 by default and maps "all fragments" to 2,147,483,646, which works. OpenSearch maps it the same way, passes other counts through (no upper bound found, not fully traced) and keeps its own copy of the line.
A static scan listed, for every public method in the core and module jars, which argument checks it makes: highlightFields checks the field list and the array length but never the maxPassages values. A search of such unchecked parameters for + 1 arithmetic found the wrap, and a public-API test confirmed it.
The method in full133 words
Method: a static guard inventory, then a guided search (the finder labelled it semi-formal; the last step was a text search, so it sits at the boundary with non-formal), in a hunt unit on accepted-parameter boundaries, run on the stock main jars at ea57360912ed.
- A bytecode pass (the JDK ClassFile API) over 27 core and non-sandbox module jars listed 1,231 public or protected entry points whose code reaches an
IllegalArgumentException, with each guard's parameters and constants. - For
UnifiedHighlighter.highlightFields/highlightFieldsAsObjectsthe only guards are "fieldsIn must not be empty" and "invalid number of maxPassagesIn": the counts themselves are accepted unbounded. - Searching the accepted-unbounded parameters for
+ 1and* 2arithmetic foundMath.min(64, maxPassages + 1); a hand-written public-API JUnit witnessed the failure, later reduced to the single test shown here.
No earlier report found (searched 2026-10-26, re-checked 2026-10-30) in GitHub issues and PRs including the JIRA archive, the mailing lists, downstream projects or the web. Closest, not this bug: #8895 (the shape of the maxPassages API) and LUCENE-10135 (#11172, a different highlighter overflow, fixed in 9.0.0).
Searches4 searches
Related, not this bug: #8895 / LUCENE-7844 (open since 2017: simplify the maxPassages input API; nothing about limits or overflow), #8490 / LUCENE-7438 (the introducing change), #11172 / LUCENE-10135 (a MAX_VALUE overflow of maxPassageWindow in MatchHighlighter's PassageSelector, fixed in 9.0.0), #13103 (open: a different IllegalArgumentException, "indexed without offsets", for interval queries), elastic/elasticsearch#61860 (2020: passes Integer.MAX_VALUE - 1 for "all fragments", with no stated reason and no mention of an error) and elastic/elasticsearch#155507 (2026: caps large fragment counts because of the classic Highlighter's allocations; does not mention the UnifiedHighlighter or the wrap). Coverage limits: GitHub code search was rate-limited, list search matches subjects and leading text only, and private reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| Search tool (GitHub issues/PRs/commits incl. the JIRA archive, dev@, java-user@, issues@, commits@, downstream, web) | 2026-10-26 | two runs: maxPassages with MAX_VALUE, overflow, + 1, initialCapacity; FieldHighlighter, highlightOffsetsEnums, passageQueue; stack-frame strings; hl.snippets and number_of_fragments | 20 hits classified; none about this bug |
| GitHub (apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch, other downstream users) and JIRA | 2026-10-26 | 71 read-only GitHub searches, 24 JQL queries (LUCENE and SOLR), 34 mailing-list queries; 79 items read | #8895, #8490, #11172, #13103, elastic/elasticsearch#61860 and #155507: related, none the same bug |
| Open PRs | 2026-10-26 | changed files of all 411 open apache/lucene PRs | no PR touches FieldHighlighter.java |
| GitHub, re-checks | 2026-10-30 | issue/PR search for maxPassages, maxPassages MAX_VALUE, maxPassages overflow, FieldHighlighter PriorityQueue, highlightOffsetsEnums, hl.snippets; open PRs mentioning FieldHighlighter; commits on FieldHighlighter.java | only older issues, none about this; 0 open PRs; line 143 unchanged on upstream main 2f424fd428da |
Present since the UnifiedHighlighter was added (LUCENE-7438, 722e8271243 on the default branch and 4b6794368df on branch_6x, 2016-10-04), first released in 6.3.0. Every release from 6.3.0 to 10.6.0 and main fails; before 7.3.0 even documents without a match failed.
Versions tested7 rows
The first highlightOffsetsEnums already had Math.min(64, maxPassages + 1). Later commits did not change the expression: LUCENE-7559 (6.4.0) changed only the comparator body, LUCENE-8145 (9422609a5d4 on the default branch, first released in 8.0.0; its branch_7x twin aa5c3a5c222 in 7.3.0) added the early return for documents without a match before the queue, which narrowed the bug, and LUCENE-9570 (6faa4f98e04, 9.0.0) only reformatted it, which is why plain git blame on main names that commit. Not a regression: no UnifiedHighlighter release ever accepted MAX_VALUE for a matching document, and the predecessor PostingsHighlighter failed for MAX_VALUE and MAX_VALUE - 1 with OutOfMemoryError. Old releases ran on JDK 25.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle; standalone -ea and -da) |
| 10.6.0, 10.5.2, 9.12.3 | affected | the minimal test fails |
| 9.1.0, 9.12.3, 10.0.0, 10.5.2 | affected | the finder's JUnit fails (-ea, and -da on 9.12.3 and 10.5.2) |
| 7.3.0 to 10.5.2 | 69 releases | a portable main() probe fails for the document with a match; the document without a match is summarized |
| 6.3.0 to 7.2.1 | 18 releases | the probe fails for both documents, with and without a match |
| 6.2.1 and older | not affected | no UnifiedHighlighter (the probe does not compile) |
clamp fix on 645d4fe14a20 | not affected | the minimal test passes, and so do all 723 highlighter tests |
An independent reproducer with its own six tests, a devil's advocate, an adjudication and a fresh-seed re-run confirm it; a reviewer from another model family, reading only, agreed on the root cause. The history sweep ran all 87 releases from 6.3.0, and the fix passes the highlighter tests.
Checks and verdicts475 words
The cross-model reviewer could not run Java, so its refute-lens review voted not real at low confidence, on procedure only, while agreeing with the root cause and rating the intended-behaviour argument weak; a re-run of the repro failed. Its intended-behaviour review returned real (medium). The Solr reach is by code reading only.
| Check | Verdict | What it found |
|---|---|---|
| finder (static guard inventory) | candidate | Its 2-test JUnit fails 1 of 2 on main (-ea, -da), 10.5.2 and 9.12.3; the control (1000) passes; with the fix it passes 2 of 2. |
| cross-model review (OpenAI GPT-5.6, refute and intended lenses, reading only) | refute: procedural "not real" (low); intended: real (medium) | Could not run Java; the lenses agreed on the root cause, found the inputs in contract and rated the intended-behaviour argument weak; a separate run re-ran the repro for each lens (fails). |
| independent reproducer | real | Own 6-test public-API JUnit fails 4 of 6 on main (-ea, -da), 10.5.2 (-ea, -da) and 9.12.3; both controls pass; with the long-arithmetic clamp it passes 6 of 6. |
| intended-behaviour devil's advocate | real (low severity) | Built the works-as-designed case and rejected it; its probe records 1, 63, 64, 1000 and 2,147,483,646 working, 2,147,483,647 throwing with a null message, and the classic Highlighter's rejection (from Lucene's util.PriorityQueue, with a message). |
| adjudication | confirmed (high) | Re-ran the reproducer's JUnit (-ea, -da: 4 of 6 fail, controls pass); impact class wrong error; security none. |
| registry re-run | keep | Fresh seeds: the finder's JUnit fails on main (-ea, -da) and passes with the fix; the reproducer's fails 4 of 6 and passes 6 of 6 with the fix. |
| history sweep and security probe | affected since 6.3.0; security none | Portable probe on all 87 releases 6.3.0 to 10.5.2 and main; 1,000 failing calls leave no residue on the highlighter. |
| minimal-repro audit | verified | The one test fails on main 645d4fe14a20 (-ea, -da, two seeds) and passes with the fix shadowed. |
| doc fact checks and diagram check | corrections applied | Two rounds of independent fact checks (one by OpenAI GPT-5.6 each round) and two diagram checks. Round 1 tightened the wording of intent, tested ranges, version qualifiers and the classic-Highlighter argument (one finding rejected with a measurement); round 2 scoped the negative-value remark (now executed), added the stock-highlighter qualifier, cited the "no limit" convention, the default maxLength bound and the kept downstream files, and corrected the seed wording. |
| this doc | reproduced, fix validated | Re-ran the minimal test on main 645d4fe14a20 (standalone runner -ea and -da, and gradle in a clean scratch worktree: fails), validated the clamp in that worktree (the test, TestUnifiedHighlighter and all 723 highlighter tests pass; the format check passes) and re-checked GitHub read-only (nothing new). |
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 537 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,521 words
- Registry title
- UnifiedHighlighter with maxPassages = Integer.MAX_VALUE throws a message-less IllegalArgumentException for every document that has a match (FieldHighlighter sizes its passage queue with Math.min(64, maxPassages + 1), which wraps)
- Area
- highlighter / UnifiedHighlighter passage selection (FieldHighlighter.highlightOffsetsEnums passage-queue sizing; maxPassages is never range-checked by UnifiedHighlighter.highlightFieldsAsObjects)
- Priority
- Low, rank 639: UnifiedHighlighter throws a message-less IAE on every matching document for maxPassages = Integer.MAX_VALUE, which is loud and caught in testing but uses the usual no-limit idiom.
- Impact class
- wrong error
- Impact note
- Applications on any release from 6.3.0 through 10.5.2 or on main that call UnifiedHighlighter (highlight, highlightFields, highlightWithoutSearcher; offsets in postings and re-analysis were run; default settings) with maxPassages = Integer.MAX_VALUE, the usual Java 'no limit' value, get a message-less IllegalArgumentException from inside java.util.PriorityQueue for every document that matches (the javadoc documents an IllegalArgumentException only for fields without offsets), and in a multi-field call such as {1, MAX_VALUE} the whole call fails, so the other fields lose their snippets too; every value up to Integer.MAX_VALUE - 1 works, documents without a match are summarized normally, there are no wrong results, no corruption and no hang, no index size is needed, and by code reading (not executed) Solr's hl.method=unified with hl.snippets=2147483647 reaches it, while default Elasticsearch caps number_of_fragments at 10000 and both Elasticsearch and OpenSearch map 0 ('all') to Integer.MAX_VALUE - 1.
- Summary
- You ask the UnifiedHighlighter for at most Integer.MAX_VALUE passages, meaning 'no limit', and expect the same snippet that 1000 or Integer.MAX_VALUE - 1 gives (or, if the value is not allowed, an IllegalArgumentException that says so). Instead, for every document that has a match, FieldHighlighter.highlightOffsetsEnums sizes its passage queue with Math.min(64, maxPassages + 1); the + 1 wraps to Integer.MIN_VALUE, Math.min returns it, and the java.util.PriorityQueue constructor throws an IllegalArgumentException with no message. With per-field counts such as {1, MAX_VALUE} the field that asked for one passage loses its snippet too, because the whole call throws.
- Introduced
- 722e82712435ecf46c9868137d885484152f749b on master and its branch_6x twin 4b6794368df373df1f68ccf27f7556914efeb95e (LUCENE-7438 'New UnifiedHighlighter' by dsmiley, 2016-10-04; JIRA issue now GitHub #8490, reviewed in apache/lucene-solr#79, which was closed without a GitHub merge; the commits were made directly; FieldHighlighter.java blob 7c8f048d171e is identical in both) [read: git log -L on the line, git log -S'maxPassages + 1', git log -G'Math\.min\(64' and git blame -w -M -C --ignore-rev 6faa4f98e048 in the full-history clone; exec: the history probe fails on 6.3.0, the first release with the API, and does not compile on 6.2.1, where the API is absent]. The first FieldHighlighter.highlightOffsetsEnums already wrote 'new PriorityQueue<>(Math.min(64, maxPassages + 1), ...)'. Later commits did not change the expression: LUCENE-7559 (c51e89014ac / 23b8bb66935, 6.4.0) changed only the comparator body, LUCENE-8145 (9422609a5d4 / aa5c3a5c222, 7.3.0) added an early return for documents without a match before the queue (narrowing the bug), and LUCENE-9570 (6faa4f98e04, 9.0.0) only reformatted it, which is why plain git blame on main names that commit; the predecessor PostingsHighlighter sized the queue new PriorityQueue<>(n, ...) without a cap and failed for both Integer.MAX_VALUE and Integer.MAX_VALUE - 1 with OutOfMemoryError [exec on 6.2.1], so the wrap is new with the UnifiedHighlighter's cap; not a regression within the UnifiedHighlighter (2016-10-04)
- First affected version
- 6.3.0 [exec] (first release with the UnifiedHighlighter, from branch_6x commit 4b6794368df; 7.0.0 is the first release of the master commit and is affected [exec]); 6.2.1 has no org.apache.lucene.search.uhighlight package (the probe does not compile there) [exec]
- Affected versions
- every release that has the UnifiedHighlighter: all 87 Maven Central releases from 6.3.0 to 10.5.2 (13 in 6.x, 14 in 7.x, 27 in 8.x including 8.8.2-8.11.4, which have no tag in the history clone and are covered by execution only, 21 in 9.x, 12 in 10.x) and main ea57360912ed [exec: the history probe, a public-API main(), fails on every one with -ea, and with -da on 6.3.0, 7.3.0, 9.12.3, 10.5.2 and main; the finder's JUnit fails on 9.1.0, 9.12.3 (-ea and -da), 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da); the independent reproduction's test fails on 9.12.3, 10.5.2 (-ea and -da) and main (-ea and -da)]; scope differs by release [exec]: in 6.3.0-7.2.1 every highlighted document with non-empty content throws, with or without a match, and from 7.3.0 on (LUCENE-8145) only documents with at least one match throw; not affected: releases up to 6.2.1, which have no UnifiedHighlighter; not fixed upstream (FieldHighlighter.java:143 unchanged on upstream main cf398bfb142c and on branch_10x as of 2026-10-26, no open PR touches FieldHighlighter.java) [read]; release branches after 10.5.2 other than branch_10x were not checked
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (2026-10-26, the prior-report record plus the finder's run: 289 recorded queries, namely the scripted prior-report search (34 GitHub issue and PR, 6 commit, 5 downstream, 32 mailing-list queries over dev@, java-user@, issues@ and commits@, 8 JIRA-archive and 8 web queries), the finder's earlier run of the same tool (51 queries), 71 read-only GitHub GraphQL searches over apache/lucene, apache/lucene-solr, apache/solr, elastic/elasticsearch, opensearch-project/OpenSearch, dbmdz/solr-ocrhighlighting, hibernate/hibernate-search, apache/jackrabbit-oak, crate/crate and all of GitHub for exact identifiers and stack-frame strings, 24 JIRA queries (12 SOLR, 12 LUCENE), 34 lists.apache.org queries over the Lucene and Solr lists, and 16 manual reads (GitHub core API, local git history, the 6.2.1 and 6.3.0 sources); the changed files of all 411 open apache/lucene PRs; 20 hits classified and 79 items read, none the same bug); not fixed upstream (FieldHighlighter.java:143 still reads 'Math.min(64, maxPassages + 1),' on upstream main cf398bfb142c, 3 commits after the pinned commit with no highlighter change; the newest commit touching FieldHighlighter.java is dbf0d1426f13 (#16056, 2026-05-14, a toArray change); no open PR touches the file); closest items, not reports: #8895 / LUCENE-7844 (open since 2017; the shape of the maxPassages API, nothing about MAX_VALUE, overflow or limits), #8490 / LUCENE-7438 (the introducing change), #11172 / LUCENE-10135 with PR #334 (fixed 2021, 9.0.0: a MAX_VALUE overflow of maxPassageWindow in the separate MatchHighlighter PassageSelector), elastic/elasticsearch#61860 (2020: passes Integer.MAX_VALUE - 1 for number_of_fragments 0, with no stated reason) and elastic/elasticsearch#155507 (2026-08: caps number_of_fragments because the classic Highlighter allocated gigabytes; does not mention the UnifiedHighlighter or the wrap)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails under gradle in a clean scratch worktree and with the standalone runner (-eaand-da), and passes with the clamp fix, together with the whole highlighter module (35 suites, 723 tests); the 10.6.0, 10.5.2 and 9.12.3 release jars (the minimal test fails); the hunt's runs onea57360912ed, whose highlighter tree is identical; and the history sweep over all 87 releases from 6.3.0 to 10.5.2.
- Finding record
lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:32 (at ea57360912ed) (@lucene.internal), :83 (try-with-resources OffsetsEnum), :86 (highlightOffsetsEnums call), :109-112 (getSummaryPassagesNoHighlight sizes its list Math.min(maxPassages, 10), so the no-match path works for MAX_VALUE), :137-139 (no term occurrence -> new Passage[0] before the queue exists), :141-143 ('new PriorityQueue<>(Math.min(64, maxPassages + 1), ...)'), :205-217 (maybeAddPassage: offer, then poll when size() > maxPassages, the reason for the + 1)lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:94 (at ea57360912ed) (@lucene.experimental), :355-360 (Builder.withMaxLength rejects Integer.MAX_VALUE with 'maxLength must be < Integer.MAX_VALUE'; its comment names BreakIterator.preceding(offset+1) and the offsets-queue sentinel, a different quantity), :752-770 (highlight(field, query, topDocs, maxPassages): '@param maxPassages The maximum number of top-N ranked passages used to form the highlighted snippets', no range; the only documented IllegalArgumentException is for fields without offsets), :891-897 (highlightFieldsAsObjects checks 'fieldsIn must not be empty' and 'invalid number of maxPassagesIn', never the values), :919-924 (one new FieldHighlighter per field and call)JDK 25.0.3 java.base/java/util/PriorityQueue.java:169-177 ('if (initialCapacity < 1) throw new IllegalArgumentException()'; eager Object[initialCapacity])
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:32, :83, :86, :109-127, :137-139, :141-152, :201-224a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:757-770, :819-860, :881-902, :922-924, :1089-1107, :1152-1160JDK 25.0.3 lib/src.zip java.base/java/util/PriorityQueue.java:166-177
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:744-745, :763-764, :1096-1107a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/matchhighlight/MatchRegionRetriever.java:230-232 and .../vectorhighlight/FastVectorHighlighter.java:326-327 (Integer.MAX_VALUE documented as "all hits" / "unlimited"); lucene/core/.../search/TopScoreDocCollectorManager.java:53-54
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:94, :192-196, :355-360, :744-745, :757-758, :763-764, :819-820, :845-846, :881-897, :1089-1090a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:32, :141-143a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/test/org/apache/lucene/search/uhighlight/ (grep MAX_VALUE: only withMaxLength(Integer.MAX_VALUE - 1) in TestUnifiedHighlighter.java:566, :631, TestUnifiedHighlighterRanking.java:119, :128, TestUnifiedHighlighterTermIntervals.java:413, :471)a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/highlight/Highlighter.java:138, :182-186 and lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:121-124 (the classic Highlighter's rejection)a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/UnifiedHighlighter.java:100 (DEFAULT_MAX_LENGTH = 10000), :1410, :1447, :1450-1464 (stored values truncated to maxLength when loaded)
- Diagrams
a Lucene main checkout (645d4fe14a20) lucene/highlighter/src/java/org/apache/lucene/search/uhighlight/FieldHighlighter.java:137-143
- TestUnifiedHighlighterMaxValueMaxPassages.java (lucene/highlighter, package org.apache.lucene.search.uhighlight, 1 test):
repro/LFV-438/src/org/apache/lucene/search/uhighlight/TestUnifiedHighlighterMaxValueMaxPassages.java(41 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and an executed probe on the stock main jars (assertions on and off) and with the fix.
assets/LFV-438/bug.png(537 KB)
