In sharded grouping each shard returns its top documents per group, and TopGroups.merge combines them: it merges the top docOffset + docTopN hits of each group, then drops the first docOffset.
g with one document (scores 2 and 1). Merging with docOffset 1 and docTopN Integer.MAX_VALUE adds 1 + 2147483647 in int, which wraps to -2147483648, and the merge throws instead of returning the second document. Select the still to open it at full size. Generated with AI, checked against the code.- Two shards each return group
gwith one document, scoring 2 and 1. You merge them withTopGroups.merge(shards, Sort.RELEVANCE, Sort.RELEVANCE, docOffset, docTopN, ScoreMergeMode.None). docOffset0 withdocTopN10 orInteger.MAX_VALUEgives both documents;docOffset1 withdocTopN10 gives the second one.- You ask for every document after the first:
docOffset1,docTopNInteger.MAX_VALUE. You expect that same second document. - You get
NegativeArraySizeException: -2147483648, with assertions on or off. merge computesdocOffset + docTopNinint: 1 + 2147483647 wraps to -2147483648, andTopDocs.mergesizes its result array with it. - The edge is exactly the wrap:
docOffset1 withdocTopN2147483646 (sum 2^31 - 1) works;docOffset2 with the samedocTopN(sum 2^31) throws. A field sort inside groups fails the same way.
Code that calls TopGroups.merge itself, bounds each shard's per-group window on its own (for example at maxDoc), and passes docOffset 1 or more with a "no limit" docTopN: every such request fails, loudly. Lucene's GroupingSearch and TopGroupsCollectorManager fail earlier on these values (LFV-964); Solr passes a finite value when group.limit < 0.
Mechanism, terms and numbers419 words
TopGroups.merge combines the second-pass grouping results of several shards, then skips docOffset documents per group and keeps docTopN. It adds the two ints without an overflow check before calling TopDocs.merge, so docOffset 1 with docTopN Integer.MAX_VALUE ("all after the first") throws NegativeArraySizeException: -2147483648 as soon as one group's shard results hold a document, for relevance and field order. docOffset 0, or docTopN Integer.MAX_VALUE - 1, works. It has been there since 3.3 (2011). Lucene's own GroupingSearch and TopGroupsCollectorManager fail earlier on such values (LFV-964), so the demonstrated reachable path is hand-written shard-merge code that bounds each shard's window itself.
- Two-pass grouping across shards: each shard runs the first pass,
SearchGroup.mergepicks the top groups, each shard runs the second pass for those groups, andTopGroups.mergecombines the per-group documents. The grouping package leaves this to the caller: "Sharding is not directly supported, though is not too difficult, if you can merge the top groups and top documents per group yourself" (package-info.java:76-77). docOffset,docTopN: "which document to start from within each group (for pagination)" and "how many top documents to keep within each group" (TopGroups.java:128-129). No bound is documented, andmergechecks neither.
- For each group,
mergecallsTopDocs.merge(docOffset + docTopN, shardTopDocs)for relevance order (TopGroups.java:241) orTopDocs.merge(docSort, docOffset + docTopN, ...)for a field sort (:247). The sum is anint:1 + 2147483647is-2147483648. TopDocs.merge(topN, ...)ismerge(0, topN, ...)(TopDocs.java:203-204,:216-217). When the group's shard results hold at least oneScoreDoc,mergeAuxsizes its result withnew ScoreDoc[Math.min(size, availHitCount - start)](TopDocs.java:313), and the negative size throwsNegativeArraySizeException. A group whose shards return noScoreDoctakes the empty branch (:310-311), but one group with a returnedScoreDocfails the whole call.- The design itself handles "no limit":
mergekeeps the top hits from position 0, reads the group's best score fromscoreDocs[0](TopGroups.java:244-245) and drops the firstdocOffsetitself (:250-260); coreTopDocs.mergesizes by the available hits, not bytopN. That is whydocOffset0 withInteger.MAX_VALUEworks. Only the addition breaks.
Executed on main 645d4fe14a20, two shards with one document each (scores 2 and 1), relevance and field order alike: (0, 10) and (0, 2147483647) give 2 documents; (1, 10) and (1, 2147483646) give 1; (1, 2147483647) and (2, 2147483646) throw -2147483648; (2, 2147483647) throws -2147483647.
Exposure and likelihood274 words
- Who is hit (the demonstrated reachable path). Hand-written shard merging, the recipe the package docs point to (
package-info.java:76-77), that bounds each shard's second-pass window itself and passesdocOffset >= 1withdocTopN > Integer.MAX_VALUE - docOffset(typicallyInteger.MAX_VALUE) to the merge. An independent reproducer built exactly that (two indexes, the first pass,SearchGroup.merge,TopGroupsCollectorManagerper shard with the window atmaxDoc, thenTopGroups.merge), and it fails on main and 10.5.2. - Who is not hit yet.
TopGroupsCollectorManager.newCollectoraddswithinGroupOffset + maxDocsPerGroupitself (TopGroupsCollectorManager.java:107) beforereducecallsmerge(:116-122), soGroupingSearchand the manager fail first:IllegalArgumentException: numHits must be > 0for(1, MAX),OutOfMemoryErrorfor(0, MAX)with a field sort (that is LFV-964). If that sum is fixed andreducestill passes the caller's values on, this line becomes the first failure. Lucene's own sharded test passes the same window to each shard and to the merge (TestGrouping.java:1527,:1552-1557). Solr passes a finitedocsPerGroupforgroup.limit < 0since SOLR-4164; forgroup.limit >= 0it passes the request's limit and offset tomergeunchanged (read only; whether its shard requests fail earlier was not checked). A code search found no call in Elasticsearch or OpenSearch. - Default or opt-in. No setting needed, but the call is an expert one: a static helper of an
@lucene.experimentalclass (TopGroups.java:38). - Effect. An exception instead of the merged groups whenever a group returns a document; no wrong or partial result. Workaround:
docTopN = Integer.MAX_VALUE - docOffset, or a finite cap. - Scale. None: 2 documents. Likelihood: rare; no report since 3.3 (2011).
The test shows that TopGroups.merge of two hand-built shard results with docOffset 1 and docTopN Integer.MAX_VALUE overflows the int sum docOffset + docTopN.
/*
* 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.grouping;
import java.util.List;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TotalHits;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
public class TestTopGroupsMergeOffsetOverflow extends LuceneTestCase {
@SuppressWarnings("unchecked")
private static TopGroups<BytesRef> shard(float score) {
GroupDocs<BytesRef> group =
new GroupDocs<>(
Float.NaN,
score,
new TotalHits(1, TotalHits.Relation.EQUAL_TO),
new ScoreDoc[] {new ScoreDoc(0, score)},
new BytesRef("g"),
new Object[] {score});
return new TopGroups<>(
Sort.RELEVANCE.getSort(), Sort.RELEVANCE.getSort(), 1, 1, new GroupDocs[] {group}, score);
}
public void testDocOffsetOneWithUnboundedDocTopN() {
TopGroups<BytesRef> merged;
try {
merged =
TopGroups.merge(
List.of(shard(2f), shard(1f)),
Sort.RELEVANCE,
Sort.RELEVANCE,
1,
Integer.MAX_VALUE,
TopGroups.ScoreMergeMode.None);
} catch (NegativeArraySizeException e) {
throw new AssertionError(
"expected group g's second document, but merge(docOffset=1, docTopN=MAX_VALUE) threw " + e,
e);
}
assertEquals(1, merged.groups[0].scoreDocs().length);
}
}
# in a copy of this finding's repro folder (repro/LFV-960/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/grouping/build/libs/lucene-grouping-$V.jar
CP=$CP:$LUCENE/lucene/queries/build/libs/lucene-queries-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.search.grouping.TestTopGroupsMergeOffsetOverflow
- Expected vs actual
- Expected group g's second document (1 ScoreDoc); actual
NegativeArraySizeException: -2147483648from TopDocs.mergeAux via TopGroups.merge:241 (-ea and -da).
More files, environment, full output47 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected group g's second document, but merge(docOffset=1, docTopN=MAX_VALUE) threw java.lang.NegativeArraySizeException: -2147483648
org.apache.lucene.search.grouping.TestTopGroupsMergeOffsetOverflow.testDocOffsetOneWithUnboundedDocTopN(TestTopGroupsMergeOffsetOverflow.java:54)
Caused by: java.lang.NegativeArraySizeException: -2147483648
org.apache.lucene.search.TopDocs.mergeAux(TopDocs.java:313)
org.apache.lucene.search.TopDocs.merge(TopDocs.java:217)
org.apache.lucene.search.TopDocs.merge(TopDocs.java:204)
org.apache.lucene.search.grouping.TopGroups.merge(TopGroups.java:241)
org.apache.lucene.search.grouping.TestTopGroupsMergeOffsetOverflow.testDocOffsetOneWithUnboundedDocTopN(TestTopGroupsMergeOffsetOverflow.java:46)- No documented bound:
docOffsetis "which document to start from within each group",docTopN"how many top documents to keep". - It is an
intwrap, not a limit:merge(1, 2147483646)works,merge(1, 2147483647)throws,merge(0, 2147483647)works. - The grouping module fixed the same offset + limit overflow pattern as a bug before, in its facet API (SOLR-3956, 2013).
The full argument155 words
- Contract.
TopGroups.java:128-129give the two parameters' meaning and no bound; the documented preconditions are the same sorts and the same top groups on every shard (:113-115,:124-125).mergevalidates neither value. - The method does not need a bound. It does not allocate by
docTopN; coreTopDocs.mergeacceptstopN = Integer.MAX_VALUEand sizes by the available hits (TopDocs.java:313). - The exception is accidental: a JVM allocation failure inside core, not a defined rejection like the
IllegalArgumentExceptionsmergethrows for mismatched shards (TopGroups.java:164-166,:202-210). - Precedent. SOLR-3956 (type Bug, fixed 2013, commit
1ad382f3046): for grouped facets the committer choseInteger.MAX_VALUEas "unlimited" and changedgetFacetEntries(offset, limit)"to not have int overflow if Integer.MAX_VALUE < offset+limit" (Chris M. Hostetter, JIRA comment of 2013-03-27). Main still testsgetFacetEntries(1, Integer.MAX_VALUE)(TestTermGroupFacetCollectorManager.java:106,:135). - Tests never tried it. Upstream tests merge with finite windows (
TestTopGroups.java:174-181,TestGrouping.java:1552-1557).
The devil's-advocate check206 words
The strongest case (the refute review, the intended-behaviour check): an unwritten precondition that the merge window equals the per-shard second-pass window, which is itself docOffset + docTopN (TopGroupsCollectorManager.java:107, TestGrouping.java:1527), so any pair that overflows in merge has already failed in the shard; TopGroups is @lucene.experimental; sharding is "not directly supported". The refute reviewer rated it moderate and, on that reading, judged the inputs outside the contract ((1, MAX) "is not a supported pagination request"), while noting the javadoc states no bound, "so this is not a conclusive contractual refutation".
Why it is weakened, though not conclusively disproved: nothing documents that precondition; TestTopGroups feeds merge hand-built results, so it is a standalone utility; the merge is well defined whenever each shard returned all its candidates (a window capped at maxDoc); the boundary pair shows the limit is the wrap; and the same module treated the same pattern as a bug. The works-as-designed reviewer: "experimental status is not an input-domain restriction". Against it: the standard collection recipe always computes one positive int window before the merge, and the request is extreme, though it uses the conventional "no limit" sentinel that the zero-offset call accepts. Verdict: a defect, low severity, narrow reach.
Recommended Saturate the window (validated)
Saturate the window before calling TopDocs.merge: int docWindow = (int) Math.min((long) docOffset + docTopN, Integer.MAX_VALUE); and pass docWindow at both call sites (+5/-2 in TopGroups.java). With it the test passes, and so do all 57 grouping tests and the module's checks.
// docOffset + docTopN can exceed Integer.MAX_VALUE (e.g. docTopN = Integer.MAX_VALUE for "all
// remaining documents"): saturate instead of wrapping to a negative size.
final int docWindow = (int) Math.min((long) docOffset + docTopN, Integer.MAX_VALUE);
...
mergedTopDocs = TopDocs.merge(docWindow, shardTopDocs);
...
mergedTopDocs = TopDocs.merge(docSort, docWindow, (TopFieldDocs[]) shardTopDocs);All options and trade-offs3 options · 391 words
One fix was built and validated with gradle; two alternatives are compared below.
Compute docOffset + docTopN in long, cap it at Integer.MAX_VALUE, and pass that to both TopDocs.merge calls. The finder wrote this patch; the first verifier independently chose the same saturation semantics (inlined at both calls, without the local).
// docOffset + docTopN can exceed Integer.MAX_VALUE (e.g. docTopN = Integer.MAX_VALUE for "all
// remaining documents"): saturate instead of wrapping to a negative size.
final int docWindow = (int) Math.min((long) docOffset + docTopN, Integer.MAX_VALUE);
...
mergedTopDocs = TopDocs.merge(docWindow, shardTopDocs);
...
mergedTopDocs = TopDocs.merge(docSort, docWindow, (TopFieldDocs[]) shardTopDocs);Trade-offs
Keeps the method's design (merge from position 0, take maxScore from scoreDocs[0], slice locally) and every non-wrapping result. Nothing is lost by saturating: a group's merged hits are one Java array. One long addition per call, before the per-group loop. It repairs the positive overflow only and is not input validation: for every pair whose sum is at most Integer.MAX_VALUE, negative values included, it passes exactly what main passes today, and merge still neither documents nor checks docOffset >= 0 and docTopN >= 0 (option C's checks would add that).
What the checks validated
- Scratch worktree of
645d4fe14a20, gradle, JDK 25.0.3: stock, the repro fails (assertions on and off); with the fix it passes, withTestTopGroups; all of:lucene:grouping:test(15 suites, 57 tests) passes with assertions on and off;:lucene:grouping:check -x testpasses. - Standalone runner with the class shadowed: the repro passes; on main
a9779ef3c193the finder's 2 tests, the first verifier's 5 tests and the audit's 3 tests passed with the same fix.
Let core skip the offset and drop the local slice (TopGroups.java:250-260).
Trade-offs
Core has the same addition (int requestedResultWindow = start + size, TopDocs.java:314, LFV-262: a page of null ScoreDocs), so it needs that fix first. It would also break maxScore for relevance order: TopGroups.java:244-245 reads the best score from scoreDocs[0], which would then be the first document after the offset.
Validate the two values and use Math.addExact, or throw IllegalArgumentException.
Trade-offs
A clean message, but it still refuses the "all remaining" request that docOffset 0 and core TopDocs.merge accept.
An application that forwards an uncapped, user-chosen per-group offset and page size into its own TopGroups.merge call fails that request with this exception; capping docTopN avoids it.
A z3 model of the grouping module's int offset + limit sums flagged this call: it is satisfiable at docOffset 1, docTopN Integer.MAX_VALUE. A boundary grid over the documented sharding recipe on main then threw exactly where the sum reaches 2^31, and it was reduced to one public-API test.
The method in full135 words
Method: semi-formal. A hunt unit on the grouping module's int offset + limit sums (GroupingSearch, FirstPassGroupingCollectorManager, TopGroups.merge) wrote a z3 model of each addition site. Query Q5 is satisfiable at docOffset 1, docTopN Integer.MAX_VALUE (sum -2^31); Q8 shows the wrap region is exactly docOffset + docTopN >= 2^31. The site had been noted, but never run, by an earlier unit that worked on core TopDocs.merge (LFV-262).
The unit then drove the documented sharding recipe on the stock main jars with a boundary grid of (docOffset, docTopN) up to Integer.MAX_VALUE, compared with a finite docTopN: (0, MAX) and (1, MAX-1) matched; (1, MAX), (2, MAX-1) and (2, MAX) threw, for relevance and field order. It reduced that to a public-API JUnit (2026-10-28), later minimised to the one-test repro.
No report found: GitHub issues and PRs (including every open PR's file list), the JIRA archive, four mailing lists and the web, searched 2026-10-28 and rechecked 2026-10-31. Closest: #10342 (a different overflow in the same method) and SOLR-4164 (for group.limit < 0 Solr passes a finite value).
Searches4 searches
Nobody else has reported TopGroups.merge's docOffset + docTopN sum, a NegativeArraySizeException from it, or a bound on the two values. Related items concern other code or set precedents.
| Where | When | How | Found |
|---|---|---|---|
| scripted prior-report search, two runs | 2026-10-28 | 51 queries each over apache/lucene issues and PRs, commits, downstream projects, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; 2804 and 172 distinct hits, every high and medium hit read | no report; related: #10342 / LUCENE-9302 (int total-hit counts in the same method), SOLR-4164, SOLR-3956 (a bug fix for the same offset + limit pattern in the grouped facet API), #5897 / LUCENE-4832 (a join-module enhancement, not a bug) |
| GitHub by hand | 2026-10-28 | 27 searches (recorded as 25 entries, two of which hold two code-search queries each) over apache/lucene, lucene-solr, solr, lucenenet, Elasticsearch and OpenSearch (issues, commits, code); the changed-file lists of all 414 open apache/lucene PRs; the 93 items updated since 2026-10-26 | no report; only open PR #16548 touches TopGroups.java (totalHits relation only) |
| JIRA and mailing lists | 2026-10-28 | 11 JQL queries (13 tickets read in full), 35 list queries, 7 web queries | no report |
| Recheck for this doc | 2026-10-31 | upstream main fetched; GitHub searches for TopGroups issues and PRs updated since 2026-10-27, NegativeArraySizeException issues, "TopGroups.merge" | nothing new; main 2f424fd428d still has both sums |
Born with the method: LUCENE-3191 (a1e74fd6c94 / backport c0713158f8c, 2011) added TopGroups.merge with the int sum; first release 3.3. Never fixed; later edits kept it. Executed failing on 18 releases from 3.3.0 to 10.5.2, the 10.6.0 release commit and main.
Versions tested7 rows
Introduced by LUCENE-3191, "Add TopDocs.merge to merge multiple TopDocs" (migrated issue #4264; author mikemccand; Subversion era, no PR). Its commits, both with the subject "add TopDocs.merge, TopGroups.merge, SearchGroup.merge, to make sharding easier": trunk commit a1e74fd6c94 (2011-06-15, first released in 4.0.0) and branch_3x backport c0713158f8c (2011-06-16, first released in 3.3). The parent has no TopGroups.merge. git log -S 'docOffset + docTopN' finds only that commit and e4436aa7fec (LUCENE-6220, 2015), which split the call into the two now at TopGroups.java:241 and :247. Later edits, among them LUCENE-5515 (paging TopDocs.merge, 4.8.0), LUCENE-6900, #15879, #15897, #16305 and #16516 (not a complete list), kept it.
Not a regression. All 127 release tags from 3.3 to 10.5.2 have the sum (read); the 10.6.0 tag has it at :239 and :245. 3.2.0 has no TopGroups.merge.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the one-test repro fails through gradle and the standalone runner, assertions on and off; the boundary probe throws for every sum >= 2^31 |
main a9779ef3c193 | affected | 4 wrapping cases throw, 4 controls pass (assertions on and off); the finder's, the first verifier's and the audit's JUnits fail |
10.6.0 (release commit 4ae946e5a9b) | affected | 4 of 4 throw (assertions on) |
| 10.5.2, 9.12.3, 3.3.0 | affected | 4 of 4 throw, assertions on and off |
| 10.0.0, 9.0.0, 8.11.4, 8.0.0, 7.7.3, 7.0.0, 6.6.6, 6.0.0, 5.5.0, 5.4.1, 5.0.0, 4.10.4, 4.0.0, 3.6.2, 3.4.0 | affected | 4 of 4 throw (assertions on) |
| 3.2.0 | no API | TopGroups.merge does not exist |
fix (saturated window) on 645d4fe14a20 | not affected | the repro and all 57 grouping tests pass |
A finder, an independent reproducer, reviews by another model family (reading only), an intended-behaviour devil's advocate, an adjudication and a credibility audit agree on the root cause; every executed check fails on main with assertions on and off and passes with the saturating fix.
Checks and verdicts321 words
The cross-model reviews ran reading-only (they could not run code); every claim they read was executed by the other checks. The hunt's checks ran on main a9779ef3c193, whose relevant lines equal 645d4fe14a20.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | z3 site model, boundary grid, 2-test JUnit: fails 2 of 2 on main (-ea, -da), the docOffset 0 control passes; passes with the fix, as do 29 upstream grouping tests. |
| cross-model review, refute lens | not real (low), procedural | OpenAI GPT-5.6, reading only: could not run Java, so its "not real" is procedural; root cause agrees; intended argument moderate, and on the implicit-window reading it judged the inputs outside the contract, while calling that "not a conclusive contractual refutation"; raised the reach caveat (in-tree callers fail earlier). |
| Independent reproducer | real | Own 5-test JUnit (hand-built and end-to-end sharded, plus a control): 4 of 5 fail on main and 10.5.2, 2 of 3 on 9.12.3; own saturating fix passes 5 of 5. |
| Cross-model review, works-as-designed lens | real, medium | OpenAI GPT-5.6, reading only: intended argument weak, inputs in contract; a separate run showed the standard per-shard paths fail before any merge. |
| Intended-behaviour devil's advocate | real, medium-high | Boundary probe, SOLR-3956 precedent, finder's JUnit re-run (fails; passes with the fix). |
| adjudication | confirmed, high | Re-ran the finder's and the independent reproduction's JUnits with new seeds (fail) and the independent reproduction's with the fix (pass). |
| credibility audit | keep | Finder's JUnit fails 2 of 2; an independent hand-built JUnit fails the two wrapping pairs with the (1, MAX-1) control passing, and passes with the fix. |
| this doc | confirmed on 645d4fe14a20 | The one-test repro fails through gradle and the standalone runner (assertions on and off); a 14-pair probe pins the edge for both sort kinds (four named outcomes asserted per sort, the rest observed in its output); the fix is 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 501 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,180 words
- Registry title
- TopGroups.merge throws NegativeArraySizeException when docOffset + docTopN overflows int, for example docOffset = 1 with docTopN = Integer.MAX_VALUE ('all remaining documents'), although docOffset 0 with the same docTopN works
- Area
- grouping / TopGroups.merge (merging second-pass grouped results across shards or slices)
- Priority
- Low, rank 899: TopGroups.merge overflows docOffset + docTopN and throws NegativeArraySizeException, which is the grouping-module, loud form of the core TopDocs.merge overflow LFV-262 and only hits hand-written shard-merge code.
- Impact class
- crash
- Impact note
- Only code that merges grouped shard results itself with the public, @lucene.experimental static TopGroups.merge (the package docs leave sharding to the caller), bounds each shard's second-pass window on its own (for example at maxDoc) and passes docOffset >= 1 with docTopN > Integer.MAX_VALUE - docOffset, typically Integer.MAX_VALUE as 'all remaining documents', is hit; merge then throws NegativeArraySizeException every time, for relevance and field order within groups, with no silent wrong result; Lucene's own GroupingSearch and TopGroupsCollectorManager fail earlier, in the per-shard collector, for the same values; no setting or index size is needed, and every release from 3.3 to 10.6.0 and main is affected.
- Summary
- You merge two shards' second-pass grouping results with TopGroups.merge(shardTopGroups, groupSort, docSort, 1, Integer.MAX_VALUE, ...) to get every document after the first in each group, and expect the same groups as with docTopN = 10 on the same small data, just as docOffset 0 with docTopN = Integer.MAX_VALUE already gives. Instead merge throws NegativeArraySizeException: -2147483648: it passes the int sum docOffset + docTopN to TopDocs.merge, the sum wraps negative, and TopDocs.mergeAux sizes its result array with it. Every pair with docOffset + docTopN >= 2^31 fails, for relevance and field order within groups; a sum of 2^31 - 1 still works.
- Introduced
- a1e74fd6c9463558e3c052f61dfd53c00328902f (trunk, svn r1136080, first released in 4.0.0) and its branch_3x backport c0713158f8c466146a09d2a4d974c656b379c209 (svn r1136465, first released in 3.3): LUCENE-3191 'Add TopDocs.merge to merge multiple TopDocs' (migrated issue #4264; svn era, no GitHub PR) added TopGroups.merge with the int sum docOffset + docTopN [read: git log -S 'docOffset + docTopN' finds exactly a1e74fd6c94, which adds it, and e4436aa7fec (LUCENE-6220, 2015), which splits it into the two calls now at TopGroups.java:241 and :247; it was never removed] (2011-06-15 (author date of a1e74fd6c94); 2011-06-16 (author date of the branch_3x backport c0713158f8c))
- First affected version
- 3.3 [exec: 3.3.0 throws NegativeArraySizeException: -2147483648 for docOffset 1 + docTopN Integer.MAX_VALUE with -ea and -da]; 3.2.0 has no TopGroups.merge [exec: API-presence check, the probe does not compile]
- Affected versions
- Every release from 3.3 to 10.5.2 [read: the int sum is in all 127 release tags from 3.3 to 10.5.2 in the history clone; exec on 18 releases: 3.3.0, 3.4.0, 3.6.2, 4.0.0, 4.10.4, 5.0.0, 5.4.1, 5.5.0, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 with -ea, and with -da also on 3.3.0, 9.12.3 and 10.5.2: all 4 overflow cases throw, all 4 non-wrapping controls pass], branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) [exec, array-signature probe] and main a9779ef3c193 [exec, -ea and -da]; upstream main 645d4fe14a2 still has both sums and TopGroups.java was last changed by 897625db0f9 (2026-08-20, unrelated) [read]; not fixed
- 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: Z3
- Upstream and prior reports
- none found (the prior-report record, verdict new, searched 2026-10-28, read-only, plus the finder's scripted prior-report search): two scripted prior-report searches (51 queries each over apache/lucene issues and PRs, commits, downstream projects, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; 2804 and 172 distinct hits, every high and medium hit read), 25 manual GitHub searches (apache/lucene, lucene-solr, solr, lucenenet, elasticsearch, OpenSearch; issues, commits and code), 11 JQL queries with 13 tickets read in full, 35 mailing-list queries, 7 web queries, the changed-file lists of all 414 open apache/lucene PRs and the 93 items updated since 2026-10-26; refresh 2026-10-28: no PR created since 2026-10-26 and no issue updated after a recent cutoff touches grouping, and no new commit on TopGroups.java; not fixed upstream (upstream main 645d4fe14a2 still has both sums at TopGroups.java:241 and :247); closest items, not reports: #10342 / LUCENE-9302 and lucene-solr PR #1411 (int total-hit-count accumulators in the same method, a different fix), SOLR-4164 (Solr caps the per-shard window and passes merge a finite docsPerGroup), SOLR-3956 and #5897 / LUCENE-4832 (precedents that offset + limit overflow and Integer.MAX_VALUE as 'all' are treated as bugs in grouping APIs), #15995, #15879, #15897, #16305 and #16516 (recent edits of the method, no size discussion), and #16548 (the only open PR touching TopGroups.java, totalHits relation only)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28) through gradle in a scratch worktree (removed afterwards), assertions on (seed DEADBEEF) and off (5EED0960): stock, the one-test repro fails; with the recommended fix it passes, together withTestTopGroups, all of:lucene:grouping:test(57 tests, both assertion modes) and:lucene:grouping:check -x test. Through a standalone runner on645d4fe14a20: the repro (assertions on and off; fix shadowed: passes), a 14-pair boundary probe on the repro's data (relevance and field order, assertions on and off) and the audit's 2 + 3-document boundary JUnit. On maina9779ef3c193, whereTopGroups.java,TopDocs.javaandTopGroupsCollectorManager.javahave the same lines: the finder's JUnit, an independent 5-test JUnit, a devil's-advocate probe, judge and audit re-runs. A release probe on 18 releases from 3.3.0 to 10.5.2 and on the 10.6.0 release commit4ae946e5a9b. Upstream main2f424fd428d(2026-10-30) has no change to the grouping module orTopDocs.javasince645d4fe14a20. OpenJDK 25.0.3, Linux aarch64 (the only platform run).
- Finding record
lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:38 (at a9779ef3c193) (@lucene.experimental), :128-129 (docOffset and docTopN @param, no bound), :241 and :247 (TopDocs.merge(docOffset + docTopN, ...)), :250-260 (slice from docOffset); lucene/core/src/java/org/apache/lucene/search/TopDocs.java:313 (new ScoreDoc[Math.min(size, availHitCount - start)]); lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollectorManager.java:107 (newCollector sums withinGroupOffset + maxDocsPerGroup) and :116 (reduce calls TopGroups.merge); lucene/grouping/src/java/org/apache/lucene/search/grouping/package-info.java:73-78 (sharding left to the caller)a Lucene clone tags: TopGroupsCollectorManager (#15603, backport 8e443458c2f) and #15995 (fbeed298542) are first in releases/lucene/10.5.0
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:112-141 (javadoc, signature), :183, :241, :244-245, :247, :250-260a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopDocs.java:203-204, :216-217, :310-313a Lucene main checkout (645d4fe14a20) lucene/grouping/src/java/org/apache/lucene/search/grouping/package-info.java:73-78
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollectorManager.java:101-123; lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java:1527, :1552-1557; lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:38
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:113-115, :124-125, :128-129, :164-166, :202-210; lucene/core/src/java/org/apache/lucene/search/TopDocs.java:313a Lucene main checkout (645d4fe14a20) lucene/grouping/src/test/org/apache/lucene/search/grouping/TestTermGroupFacetCollectorManager.java:106-139; TestTopGroups.java:174-181; TestGrouping.java:1527, :1552-1557
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopDocs.java:314; lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:244-245, :250-260
- History
a Lucene clone tag releases/lucene/10.6.0 (TopGroups.java:239, :245; commit 4ae946e5a9b)
- TestTopGroupsMergeOffsetOverflow.java (JUnit test):
repro/LFV-960/src/org/apache/lucene/search/grouping/TestTopGroupsMergeOffsetOverflow.java(60 lines)
It is made separately from this text; its numbers come from the executed repro on main.
assets/LFV-960/bug.png(501 KB)
