All findings · Lucene finding · LFV-993 · fieldvaluehitqueue-comparators-allocated-before-size-guard

On main (11.0, unreleased), a field-sorted collector asked for Integer.MAX_VALUE hits throws OutOfMemoryError instead of IllegalArgumentException

Asking a field-sorted TopFieldCollectorManager for Integer.MAX_VALUE hits throws OutOfMemoryError on main, where every release from 7.5.0 to 10.6.0 refuses that size with a clear IllegalArgumentException: since #14873 the built-in sort comparators allocate their numHits-sized arrays before the queue's size check runs. Unreleased: main and the 11.0 release branch (branch_11x).

Summary

Priority
Low, rank 1131On unreleased main and branch_11x only, a call that was always refused now throws OOM instead of IAE, and IndexSearcher.search caps n, so it is Low.
Impact
wrong error
Since
main, #14873 (39549329509, 2025-07-03)introduced 2025-07-03 · about 1 year
Affected
main and branch_11x (unreleased); no release affected
Status
documented
Found by
non-formal: a main-vs-10.x probe difference, then code reading

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Background: field-sorted top-N collection

  1. A search sorted by fields keeps its best numHits hits in a priority queue, FieldValueHitQueue. Each sort field gets a comparator (for the built-in sort types) that stores one value per queue slot, so it allocates an array of numHits values up front.
  2. A Java array holds at most about 2^31 entries. Since 4.7.0 (SOLR-5661) the PriorityQueue constructor has checked for sizes too big for an array, "to prevent confusing OOME". Its current form, since 7.5.0, refuses a size of ArrayUtil.MAX_ARRAY_LENGTH (2,147,483,631) or more with an IllegalArgumentException.
Diagram of the bug. new TopFieldCollectorManager(sort by long field, numHits = Integer.MAX_VALUE).newCollector() calls FieldValueHitQueue.create(fields, 2147483647). On main, create() first builds the LongComparator, whose new long[2147483647] throws OutOfMemoryError: Requested array size exceeds VM limit (FieldValueHitQueue.java:146); the PriorityQueue constructor and its size check ("Throw exception to prevent confusing OOME") come later (:153) and are never reached. In 10.x the queue constructor ran super(size) first, so the check threw IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647 before any allocation.
How numHits = Integer.MAX_VALUE travels from newCollector() into FieldValueHitQueue.create, where the comparator's new long[numHits] fails before the PriorityQueue size check is reached; then 10.x's order, where the check runs first. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Sort by a long field and ask for every hit with Java's usual "no limit": new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector(). Core's SortRescorer.rescore(searcher, topDocs, Integer.MAX_VALUE) reaches the same code through new TopFieldCollectorManager(sort, topN, null, Integer.MAX_VALUE) (SortRescorer.java:49).
  2. No queue can hold 2,147,483,647 slots: Java arrays end just below 2^31. You expect the clear refusal that every release from 7.5.0 to 10.6.0 gives, here 10.5.2 and 10.6.0: IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647. Main's score-sorted collector still refuses the size too, with an IllegalArgumentException from its own copy of the guard.
  3. On main you get OutOfMemoryError: Requested array size exceeds VM limit, thrown in LongComparator's constructor, with assertions on and off.
  4. Why: the size check sits in the PriorityQueue constructor. Since #14873 (2025), FieldValueHitQueue.create first builds the sort comparators, each built-in one allocating numHits slots (new long[numHits]), and creates the queue, with its check, only afterwards (FieldValueHitQueue.java:146 runs before :153).
  5. Only the error changes: the call is refused either way, and IndexSearcher.search(query, n, sort) caps n at maxDoc, so it is not affected. But code that catches IllegalArgumentException misses it, and a JVM run with -XX:+ExitOnOutOfMemoryError exits.

Who is affected

Code on a main or 11.0 branch build that passes numHits of 2,147,483,631 or more, in practice Integer.MAX_VALUE for "all", to a field-sorted collector without capping it: direct TopFieldCollectorManager use, SortRescorer and the expressions rescorer. Any built-in sort type, default settings. IndexSearcher.search(query, n, sort) caps n and is safe. No release is affected. Rare.

Mechanism, terms and numbers584 words

On main, new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector() with a built-in sort (long, string, relevance and index order executed) throws OutOfMemoryError: Requested array size exceeds VM limit. Lucene 9.10.0 to 10.6.0 refuse the same call with IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647, releases from 7.5.0 refuse the equivalent TopFieldCollector.create call the same way, and main's score-sorted TopScoreDocCollectorManager still throws an IllegalArgumentException (with its own message). Since #14873 (39549329509, 2025-07-03), FieldValueHitQueue.create builds the sort comparators, each pre-allocating numHits slots, before it calls the constructor that reaches PriorityQueue's size check, a guard SOLR-5661 added in 4.7.0 "to prevent confusing OOME" and Lucene fixed again for exactly Integer.MAX_VALUE in 2018 (7.5.0). The call never succeeded on any version, so only the error type changes; IndexSearcher.search(query, n, sort) caps n at maxDoc and is not affected, while direct users and SortRescorer are. No release has it; the 11.0 release branch branch_11x has the same code. A small up-front size check in create() restores the old error; it passes the minimal test with assertions on and off, the closest core suites and core's static checks.

Terms

  • TopFieldCollectorManager: builds the collectors for a search sorted by Sort fields. Its javadoc says the collectors "pre-allocate a full array of length numHits" (TopFieldCollectorManager.java:44-45, :96-97); its constructor rejects only numHits <= 0 (:62-64).
  • FieldValueHitQueue: the field-sorted hit queue, a PriorityQueue (@lucene.experimental), built by the static create(fields, size).
  • The guard: PriorityQueue's constructor throws IllegalArgumentException for maxSize < 0 || maxSize >= ArrayUtil.MAX_ARRAY_LENGTH, with the comment "Throw exception to prevent confusing OOME" (PriorityQueue.java:121-125). MAX_ARRAY_LENGTH is 2147483631 here, so on main and since 7.5.0 the guard covers the 17 values 2147483631 to 2147483647.

The bug

  1. newCollector() calls FieldValueHitQueue.create(sort.getSort(), numHits) (TopFieldCollectorManager.java:112-114), the only production caller of create.
  2. create() (FieldValueHitQueue.java:132-159) first loops over the sort fields and calls field.getComparator(size, ...) for each (:142-151, the call at :146). Each built-in comparator allocates size slots in its constructor: LongComparator.java:38 values = new long[numHits];, and likewise the int, float, double, relevance, string and doc comparators. A CUSTOM sort gets its comparator from user code (SortField.java:511-514); one that does not pre-allocate reaches the guard and gets the normal IllegalArgumentException.
  3. Only then does it call new FieldValueHitQueue<>(fields, size, ...) (:153-158), whose constructor calls super(size, lessThan) (:114-115) and so reaches the guard. For Integer.MAX_VALUE the first comparator's new long[2147483647] has already thrown OutOfMemoryError: Requested array size exceeds VM limit, so the guard never runs.
  4. Up to 10.6.0 the queue's constructor called super(size) first and built the comparators after it (10.5.2 and 10.6.0 FieldValueHitQueue.java:122-123). #14873 moved the comparator loop into create(), ahead of the constructor, because the new LessThan object needs the comparators.

The numbers

numHits = 2147483647 (Integer.MAX_VALUE): main throws OutOfMemoryError: Requested array size exceeds VM limit from LongComparator.<init>:38 via FieldValueHitQueue.create:146; 10.5.2 and 10.6.0 throw IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647. numHits = 2147483631 with a 1 GB heap: main OutOfMemoryError: Java heap space, 10.5.2 the IllegalArgumentException. numHits = 2147483630, one below the window: main, 10.5.2 and 10.6.0 throw OutOfMemoryError: Java heap space (4.7.0, whose older guard was wider, throws its IllegalArgumentException). The score-sorted TopScoreDocCollectorManager on main throws IllegalArgumentException: initialCapacity must be > 0 and < 2147483630 for the window, from TernaryLongHeap's copy of the guard.

Exposure and likelihood232 words
  • Who is hit. Code on a main / 11.0 build that passes a numHits of 2147483631 or more (in practice Integer.MAX_VALUE meaning "all") to a field-sorted collector without capping it: direct TopFieldCollectorManager users, and callers that forward an uncapped size, such as SortRescorer.rescore(searcher, topDocs, topN) (SortRescorer.java:49) and ExpressionRescorer, which extends it. A manager built with such a size also fails inside IndexSearcher.search(query, manager), which calls newCollector() without capping (IndexSearcher.java:750-752). Any built-in SortField type, including Sort.RELEVANCE and Sort.INDEXORDER (a CUSTOM sort depends on its comparator source), default settings.
  • Not hit. IndexSearcher.search(query, n, sort) and searchAfter cap n at max(1, maxDoc) before building the manager (IndexSearcher.java:721, :729, :733). No release: none throws this OutOfMemoryError from a comparator (sampled from 4.7.0 to 10.6.0).
  • What changes. Only the error: the call never succeeded on any version. Code that catches IllegalArgumentException to report a bad size no longer catches it. With -XX:+ExitOnOutOfMemoryError the JVM terminates on main ("Terminating due to java.lang.OutOfMemoryError: Requested array size exceeds VM limit", exit 3) where 10.5.2 throws a catchable exception and keeps running. Lucene's gradle test JVMs run with -XX:+HeapDumpOnOutOfMemoryError (TestsAndRandomizationPlugin.java:451 in build-tools/build-infra), so the recorded test run wrote a heap dump.
  • Scale and likelihood. No index or data plays a part; the first such call fails at once, so a developer meets it in testing. Likelihood rare, priority Low.

Minimal repro

One JUnit test on the public API: a collector sorted by a long field with numHits = Integer.MAX_VALUE. It expects the IllegalArgumentException that 10.x throws. No index, deterministic, single thread, about 1.2 s; the same with assertions on and off.

TestTopFieldCollectorManagerMaxNumHits.java (JUnit, one test)30 lines
/*
 * 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;

import org.apache.lucene.tests.util.LuceneTestCase;

public class TestTopFieldCollectorManagerMaxNumHits extends LuceneTestCase {

  public void testNumHitsIntegerMaxValue() {
    Sort sort = new Sort(new SortField("price", SortField.Type.LONG));
    // Expected: PriorityQueue's IllegalArgumentException (as in 10.x); actual: OutOfMemoryError.
    expectThrows(
        IllegalArgumentException.class,
        () -> new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector());
  }
}
Command
cp TestTopFieldCollectorManagerMaxNumHits.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
IllegalArgumentException: maxSize must be >= 0 and < 2147483631; got: 2147483647, as on 10.5.2 and 10.6.0.
Observed
OutOfMemoryError: Requested array size exceeds VM limit at LongComparator.<init>(LongComparator.java:38), via FieldValueHitQueue.create(FieldValueHitQueue.java:146); the same with assertions off.

All files of this repro: repro/LFV-993/

More files, environment, full output206 words

TestTopFieldCollectorManagerMaxNumHits builds new Sort(new SortField("price", SortField.Type.LONG)) and asserts expectThrows(IllegalArgumentException.class, () -> new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector()). It sits in org.apache.lucene.search like TestTopFieldCollector but uses only public API; no randomness of its own. Integer.MAX_VALUE gives the heap-independent "Requested array size exceeds VM limit"; at 2147483631, the bottom of the window, main gives the heap-dependent Java heap space (1 GB heap); the values in between were not run. It compiles unchanged against 10.5.2 and 10.6.0, where it passes with assertions on and off.

As the regression test: this same test, for example as a method in TestTopFieldCollector. It passes with the recommended fix.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); passes on 10.5.2 and 10.6.0; no release is affected
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestTopFieldCollectorManagerMaxNumHits.java $LUCENE/lucene/core/src/test/org/apache/lucene/search/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)

Observed output

java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Dumping heap to java_pid4670.hprof ...

TestTopFieldCollectorManagerMaxNumHits > testNumHitsIntegerMaxValue FAILED
    junit.framework.AssertionFailedError: Unexpected exception type, expected IllegalArgumentException but got java.lang.OutOfMemoryError: Requested array size exceeds VM limit
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:FCF900E87FF408D6]:0)
        at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1437)
        at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
        at app//org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits.testNumHitsIntegerMaxValue(TestTopFieldCollectorManagerMaxNumHits.java:26)

        Caused by:
        java.lang.OutOfMemoryError: Requested array size exceeds VM limit
            at org.apache.lucene.search.comparators.LongComparator.<init>(LongComparator.java:38)
            at org.apache.lucene.search.SortField.getComparator(SortField.java:503)
            at org.apache.lucene.search.FieldValueHitQueue.create(FieldValueHitQueue.java:146)
            at org.apache.lucene.search.TopFieldCollectorManager.newCollector(TopFieldCollectorManager.java:114)
            at org.apache.lucene.search.TestTopFieldCollectorManagerMaxNumHits.lambda$testNumHitsIntegerMaxValue$0(TestTopFieldCollectorManagerMaxNumHits.java:28)

Why it looks unintended

  • The guard says why it exists: "Throw exception to prevent confusing OOME" (PriorityQueue.java:122). It was added in 4.7.0 (SOLR-5661) and fixed again in 2018 for exactly Integer.MAX_VALUE.
  • Main keeps the guard elsewhere: its score-sorted collector still throws an IllegalArgumentException, from TernaryLongHeap's copy.
  • #14873 was an API refactor; its CHANGES entry, migration note and review never mention size checks.
The full argument246 words
  • Stated purpose. PriorityQueue.java:121-125: if ((maxSize < 0) || (maxSize >= ArrayUtil.MAX_ARRAY_LENGTH)) { // Throw exception to prevent confusing OOME:. CHANGES.txt:12883-12884, 4.7.0 "Bug fixes": "SOLR-5661: PriorityQueue now refuses to allocate itself if the incoming maxSize is too large (Raintung Li via Mike McCandless)".
  • Fixed again for exactly this value. From 4.9.1 to 7.4.0 the guard let Integer.MAX_VALUE overflow past it (LUCENE-5844, b293b947cb3, moved heapSize = maxSize + 1 ahead of the check), so the call threw NegativeArraySizeException. Michael McCandless fixed that in 2018 (4c646dab3fb, "Fix PriorityQueue constructor to throw IllegalArgumentException if you pass Integer.MAX_VALUE as maxSize (thanks rsaavedraf)", released in 7.5.0), writing today's check and message and adding TestPriorityQueue.testMaxIntSize.
  • Re-applied in newer heaps. LongHeap.java:57-58 and TernaryLongHeap.java:57-58 carry the same check and comment; main's TopScoreDocCollectorManager rejects these sizes with an IllegalArgumentException from that copy ("initialCapacity must be > 0 and < 2147483630"), so the two top-N collector families now disagree.
  • A refactor, not a decision. #14873 ("Change PriorityQueue to take a LessThan interface implementation in its constructor rather than overriding the lessThan abstract method", CHANGES.txt:75-76, 11.0.0 API Changes) moved the comparator loop for a structural reason; its MIGRATE note and review thread discuss the API and performance, not size validation.
  • Tests. TestPriorityQueue.testMaxIntSize (TestPriorityQueue.java:378-382) pins the IllegalArgumentException for the bare queue (added by the 2018 fix); no core search test gives a field-sorted collector a numHits near Integer.MAX_VALUE (those arguments are totalHitsThreshold), so nothing caught the reorder.
The devil's-advocate check190 words

The strongest case for "intended" (put forward by an independent reviewer from another model family, OpenAI GPT-5.6, and argued in full by the intended-behaviour check): no javadoc promises the IllegalArgumentException; FieldValueHitQueue is @lucene.experimental and PriorityQueue @lucene.internal; both javadocs document the numHits pre-allocation, and create() says only "Must be greater than zero"; IndexSearcher caps n, and, as Michael McCandless pointed out on SOLR-5661, callers should bound the size they pass (Adrien Grand described top-k collection as designed for "some reasonable value of k" on LUCENE-8542); and on main and in 10.x the guard covers only 17 values: at 2147483630, main, 10.5.2 and 10.6.0 throw a heap OutOfMemoryError (1 GB heap).

Why it fails: the guard is a deliberate bug fix with a stated purpose, Lucene fixed it again in 2018 for exactly Integer.MAX_VALUE, re-applies it in newer heaps, main's other top-N collector still has it, and the refactor did not mean to drop it; core's own SortRescorer forwards an uncapped topN into this path. The honest residue: the fix restores a clearer error, not a capability, and the contract rests on intent and history, not on a javadoc.

Suggested fix

Recommended A. Check the size in FieldValueHitQueue.create before building comparators

Check the size in FieldValueHitQueue.create right after its empty-sort check, before any comparator is built, and throw the same IllegalArgumentException as PriorityQueue. It restores the 10.x error for every built-in field sort. With it the test passes with assertions on and off, and the closest core suites and core's static checks pass.

All options and trade-offs3 options · 613 words

Option A was validated through gradle in a scratch worktree of 645d4fe14a20. B and C come from the adjudication's and the reviewers' notes; neither was run.

A. Check the size in FieldValueHitQueue.create before building comparators (validated by the checks) (recommended)

In FieldValueHitQueue.create, right after the empty-sort check, throw the PriorityQueue message when size < 0 || size >= ArrayUtil.MAX_ARRAY_LENGTH, and import ArrayUtil.

diff --git a/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java b/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
index 104b438a721..ff0fbcc1948 100644
--- a/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
+++ b/lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java
@@ -18,6 +18,7 @@ package org.apache.lucene.search;
 
 import java.io.IOException;
 import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.PriorityQueue;
 
 /**
@@ -135,6 +136,12 @@ public class FieldValueHitQueue<T extends FieldValueHitQueue.Entry> extends Prio
     if (fields.length == 0) {
       throw new IllegalArgumentException("Sort must contain at least one field");
     }
+    // the comparators below pre-allocate arrays of this size: reject it before they do, as
+    // PriorityQueue would (SOLR-5661), instead of failing with a confusing OutOfMemoryError
+    if (size < 0 || size >= ArrayUtil.MAX_ARRAY_LENGTH) {
+      throw new IllegalArgumentException(
+          "maxSize must be >= 0 and < " + ArrayUtil.MAX_ARRAY_LENGTH + "; got: " + size);
+    }
 
     int numComparators = fields.length;
     FieldComparator<?>[] comparators = new FieldComparator<?>[numComparators];

Trade-offs

  • Restores the 10.x behaviour exactly, with the same message, at the one place every field-sorted collector goes through, including external callers of the public create().
  • Cost: one comparison per newCollector(), not per hit.
  • It duplicates the guard's bound and message; a shared helper could avoid that.
  • It turns the main-only OutOfMemoryError of uncapped callers (the SortRescorer and grouping findings LFV-997 and LFV-964) back into this exception without fixing their own sizing.
  • The diff is against 645d4fe14a20; git apply rejects it on upstream main 2f424fd428da and on branch_11x (#16857 added an import next to its context). The same 7 lines regenerated against 2f424fd428da pass git apply --check there and on branch_11x, but were not compiled or tested on those revisions.
  • Like PriorityQueue, it accepts size == 0, while create()'s javadoc says the size "Must be greater than zero" (FieldValueHitQueue.java:130); TopFieldCollectorManager already rejects numHits <= 0, so only direct create() callers can pass 0.

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20, seed DEADBEEF, 2026-11-01: without the fix the test fails with assertions on and off; with it, it passes with assertions on and off.
  • Closest core suites with the fix: TestTopFieldCollector (13 tests), TestTopFieldCollectorEarlyTermination (5), TestSort (30), TestSortRandom (1), TestSortOptimization (31), TestSortRescorer (3), TestPriorityQueue (18) and the new test: 8 classes, 102 tests, 0 failures, 0 skipped.
  • ./gradlew :lucene:core:check -x test with the fix and the test as a new file: passes (google-java-format, ecj lint of main and test code, forbidden APIs). The full Lucene test suite was not run.
  • Earlier: the finder's 4-test JUnit passes with the fix (assertions on and off), and so do TestTopFieldCollector + TestSort (43 of 43) on a9779ef3c193; the minimal test passes with the fix shadowed into the stock jars.

B. Reject the size in the TopFieldCollectorManager constructor (not run)

Next to the existing numHits <= 0 check (TopFieldCollectorManager.java:62-64).

if (numHits >= ArrayUtil.MAX_ARRAY_LENGTH) {
  throw new IllegalArgumentException(
      "numHits must be < " + ArrayUtil.MAX_ARRAY_LENGTH + "; got: " + numHits);
}

Trade-offs

  • Fails earlier, at construction, and covers every Lucene caller today: newCollector() is the only production caller of create().
  • Misses direct callers of the public FieldValueHitQueue.create.

What the checks validated

Not run.

C. Build the PriorityQueue before the comparators (not recommended) (not run)

Restore the old order inside create().

Trade-offs

The LessThan handed to the queue's constructor needs the comparators, so this needs a late-bound comparator holder: more change for the same effect.

What the checks validated

Not run.

Downstream note

Applications that forward an unbounded size such as Integer.MAX_VALUE to a field-sorted collector and treat OutOfMemoryError as fatal exit on main where 10.x threw a catchable exception; capping numHits at maxDoc, as IndexSearcher does, avoids it.

How it was found

Non-formal: an early probe saw SortRescorer with topN = Integer.MAX_VALUE throw OutOfMemoryError on main but IllegalArgumentException on 10.5.2 and the 10.6 release candidate. An AI finder traced that difference by code reading and git history to #14873 and confirmed it with a public-API test across main and releases.

The method in full134 words
  • First signal: an executed probe on the stock jars (2026-10-28) ran SortRescorer and the expressions rescorer with topN = Integer.MAX_VALUE and saw OutOfMemoryError: Requested array size exceeds VM limit on main but the PriorityQueue IllegalArgumentException on 10.5.2 and the 10.6 release candidate.
  • Root cause: the finder, explaining that main-vs-release difference, read FieldValueHitQueue.create and its git history and found the comparator loop that #14873 moved ahead of the queue constructor.
  • Confirmation: a public-API JUnit (long, relevance and string sorts plus a score-sorted control) failed 3 of 4 on main and passed on 10.5.2 and the release candidate. No model or formal tool was involved, so the method is non-formal.
  • Later: independent verifiers, cross-model reviews (reading only), an adjudication, the registration check, a credibility audit, the one-test minimal repro (2026-10-29) and this doc's gradle run (2026-11-01).

Prior reports, history and checks

Prior reports

No matching public report found in GitHub issues, PRs and commits, the JIRA archive, the Lucene mailing lists, downstream projects or the web (searched 2026-10-29, refreshed 2026-11-01). SOLR-5661 (2014) is the fixed original whose guard this bypasses. Not fixed upstream as of 2026-10-30.

Searches3 searches

Related, none a report of this regression: SOLR-5661 (2014, reported with a patch by Raintung Li, committed by Michael McCandless; the same symptom, whose fix added the guard; its trace came from Solr's own ShardFieldSortedHitQueue), #14873 (the change), #14872 (open: remove the last PriorityQueue subclasses; no mention of the size check), #16857 (merged 2026-10-29) and #16947 (merged 2026-10-30), which edit FieldValueHitQueue.java but keep the order, and #16277 (open, 3-ary heap; constructor arity only). SOLR-5661 is the statement of intent, not a report of this bug.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits2026-10-29two scripted prior-report searches (16 issue/PR and 3 commit queries each) and 19 manual searches: FieldValueHitQueue, TopFieldCollectorManager, the OOM text, "maxSize must be", "confusing OOME", SortRescorer, 14873 regression; the changed files of 141 open PRs updated since 2026-10-01; commit histories of the touched filesno report; #14873 introduced it
Mailing lists, JIRA archive, downstream, web2026-10-29dev@, java-user@, issues@, commits@ (2 years); JIRA incl. SOLR-5661 and its comments; Elasticsearch, OpenSearch and Solr issues; web; 3,476 and 692 distinct hits, 0 high overlapno report; SOLR-5661 is the fixed original
Refresh2026-11-01by the fact check of this doc: every apache/lucene issue and PR updated since 2026-10-29, filtered for FieldValueHitQueue, TopFieldCollectorManager, "confusing OOME", "Requested array size", "maxSize must be" and 14873; upstream commits on FieldValueHitQueue.java up to 2f424fd428daonly #16857 (merged PR, keeps the order) and the unrelated closed issue #16887; commits #16857 and #16947 keep the order

History

Introduced on main by #14873 (39549329509, 2025-07-03), an API refactor of PriorityQueue; bisected by building the commit and its parent; also on branch_11x, the 11.0 release branch. No release is affected: 7.5.0 to 10.6.0 throw the quoted IllegalArgumentException, older releases fail inside PriorityQueue itself. The guard dates from SOLR-5661 (4.7.0), its current form from 7.5.0.

Versions tested9 rows

Introduced by #14873 "Make PriorityQueue use LessThan interface by default" (commit 39549329509, Simon Cooper, 2025-07-03, milestone 11.0.0; cited as the source of the change, not as blame). It turned FieldValueHitQueue from an abstract PriorityQueue subclass, whose constructor called super(size) before building the comparators, into a class built by the static create(), which builds the comparators first. git blame shows no later change to the order; upstream #16857 and #16947 edited the file without touching it. Releases before 4.7.0 throw an OutOfMemoryError from PriorityQueue itself: the original SOLR-5661 bug, long fixed. The 11.0 release branch branch_11x, cut from main at c5ab3e57b9b on 2026-10-28 (#16929), carries #14873 and the same create() with no size check (read at 603af870375; the fact check read the same on GitHub on 2026-11-01), so a fix needs main and branch_11x. branch_10_6 keeps super(size) first; branch_10x was decommissioned on 2026-10-29 (#16911). What older releases throw for Integer.MAX_VALUE depends on the guard's version: 4.7.0 to 4.9.0 an IllegalArgumentException worded "maxSize must be <= 2147483391" (a guard of Integer.MAX_VALUE - 256), 4.9.1 to 7.4.0 a NegativeArraySizeException (LUCENE-5844 let this one value overflow past the guard), and 7.5.0 on the quoted message, after Michael McCandless's 2018 fix 4c646dab3fb.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails with assertions on and off (standalone and gradle); passes with fix A
main 39549329509 (#14873) vs parent 62e02760321 bisectjavac builds of core: 0 of 3 field sorts get the IllegalArgumentException at #14873, 3 of 3 at its parent
upstream main 2f424fd428da (2026-10-30) readsame order in create() (source read, not executed)
branch_11x 603af870375 (2026-10-29) readsame order in create(), no size check (source read, not executed; the fact check read the same on GitHub on 2026-11-01)
10.6.0, 10.5.2 not affectedminimal test passes (IllegalArgumentException), SHA-1-checked jars
10.0.0, 9.12.3, 9.10.0, 8.11.4, 7.5.0 not affectedIllegalArgumentException (probes; the quoted message executed on 7.5.0, 8.11.4 and 9.10.0)
4.9.1, 4.10.0, 5.5.5, 6.6.6, 7.3.1, 7.4.0 guard overflowNegativeArraySizeException: -2147483648 from PriorityQueue itself (LUCENE-5844 let Integer.MAX_VALUE overflow past the guard; fixed in 7.5.0); not this regression
4.9.0, 4.7.0 not affectedIllegalArgumentException: maxSize must be <= 2147483391 (an older, wider guard; 4.7.0 also refuses 2147483630)
4.6.1 original SOLR-5661OutOfMemoryError from PriorityQueue itself, before the guard existed; not this regression

Independent checks

Confirmed by the finder, an independent reproducer, an intended-behaviour devil's advocate, an adjudication and a credibility audit, each re-running a test; reviewers from another model family (OpenAI GPT-5.6) read the code but could not run it. This doc's gradle run validated the fix.

Checks and verdicts363 words

Nine checks; none refuted the root cause. The cross-model reviews and the cross-model fact check were reading only.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateIts JUnit fails 3 of 4 on main with assertions on and off (control passes) and passes on 10.5.2 and the 10.6 RC; the fix passes it and TestTopFieldCollector + TestSort (43 of 43).
Independent reproducer real, low severityOwn 7-test JUnit outside org.apache.lucene (LONG, RELEVANCE, INDEXORDER, LONG+DOC, STRING at 2147483631, IndexSearcher.search with the manager, a control): 6 of 7 fail on main, 7 of 7 pass on 10.5.2 and 9.12.3.
Cross-model adversary (OpenAI GPT-5.6, refute lens, reading only) not real, by procedure onlyCould not run code, so its verdict is procedural; it agreed on the root cause and the trigger. Re-runs for it: 3 of 4 fail on main.
Intended-behaviour devil's advocate real, mediumBuilt the strongest "intended" case and found none of it holds in full; its boundary probe established the 17-value window on main and 10.5.2.
adjudication confirmed, medium confidenceRe-ran the independent reproduction's JUnit with a new seed: 6 of 7 fail on main, 7 of 7 pass on 10.5.2.
Registration check and credibility audit confirmedFresh-seed re-runs reproduce; the -XX:+ExitOnOutOfMemoryError demo exits on main and keeps running on 10.5.2.
brief validation (gradle) fix A validatedOn stock 645d4fe14a20 the test fails with assertions on and off; with fix A it passes, the 7 closest core suites pass (8 classes, 102 tests, 0 failures) and :lucene:core:check -x test passes.
fact check (2026-11-01) bug holds; 4 wording and history fixesRe-ran the minimal test (standalone and gradle, assertions on and off) and fix A with the closest suites; probed 13 releases from 4.6.1 to 10.6.0, which corrected the release history; found branch_11x affected too. All fixed in this doc.
cross-model fact check (OpenAI GPT-5.6, reading only) core bug real; wording fixesAgreed on the mechanism and the repro; asked to limit the claim to built-in sorts (CUSTOM depends on user code), to describe SortRescorer's call exactly and to scope the 17-value window. All fixed in this doc.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,520 words

Finding record

LFV-993, snapshot of 2026-11-06

Registry title
On main, a field-sorted TopFieldCollectorManager with numHits >= ArrayUtil.MAX_ARRAY_LENGTH (e.g. Integer.MAX_VALUE) throws OutOfMemoryError instead of the IllegalArgumentException that 10.x throws, because since #14873 FieldValueHitQueue.create allocates the numHits-sized sort comparators before the PriorityQueue size check runs (unreleased regression of the SOLR-5661 guard)
Area
core / search: field-sorted top-N collection (FieldValueHitQueue, TopFieldCollectorManager)
Priority
Low, rank 1131: On unreleased main and branch_11x only, a call that was always refused now throws OOM instead of IAE, and IndexSearcher.search caps n, so it is Low.
Impact class
wrong error
Impact note
Only main and the 11.0 release branch branch_11x (11.0.0-SNAPSHOT, unreleased), since #14873: an application that passes a numHits in [2147483631, 2147483647] (in practice Integer.MAX_VALUE meaning 'all') to TopFieldCollectorManager directly, or through a caller that does not cap it (SortRescorer.rescore, ExpressionRescorer), with any built-in sort and default settings, gets an OutOfMemoryError from newCollector() instead of the IllegalArgumentException that 7.5.0 to 10.6.0 throw; the call is refused either way, IndexSearcher.search(query, n, sort) is not affected because it caps n at maxDoc, and a JVM run with -XX:+ExitOnOutOfMemoryError exits instead of letting the caller catch the error.
Summary
You call new TopFieldCollectorManager(sort, Integer.MAX_VALUE, null, 1).newCollector() with any built-in sort (a long field, a string field, Sort.RELEVANCE), directly or through SortRescorer.rescore(searcher, topDocs, Integer.MAX_VALUE), and expect the IllegalArgumentException 'maxSize must be >= 0 and < 2147483631' that Lucene 7.5.0 to 10.6.0 throw (main's score-ordered TopScoreDocCollectorManager still throws an IllegalArgumentException, with its own message). On main you get OutOfMemoryError 'Requested array size exceeds VM limit' instead ('Java heap space' at 2147483631 with a 1 GB heap). Since #14873, FieldValueHitQueue.create builds the sort comparators, each of which pre-allocates a numHits-sized array, before it calls the PriorityQueue constructor, whose size check exists to prevent exactly this confusing OutOfMemoryError (SOLR-5661).
Introduced
39549329509 (#14873, PR title 'Make PriorityQueue use LessThan interface by default', commit 'Change PriorityQueue to use LessThan interface by default (#14873)', by Simon Cooper (thecoop); base main, milestone 11.0.0): it turned FieldValueHitQueue from an abstract PriorityQueue subclass, whose constructor called super(size) and so reached the SOLR-5661 guard before it built the comparators, into a class built by the static create(), which builds the comparators first and only then calls the constructor [exec: javac core builds of the parent 62e02760321 (3 of 3 field sorts get the IllegalArgumentException) and of 39549329509 (0 of 3, OutOfMemoryError from FieldValueHitQueue.create:146); read: git blame at a9779ef3c193, nothing later changed the order]. #14873 was an API refactor; its CHANGES entry (11.0.0 API Changes), MIGRATE.md and review thread say nothing about size validation. Cited as the source of the change, not as blame. (2025-07-03 (author and commit date of 39549329509, 16:04:41+01:00; PR merged 2025-07-03T15:04:41Z))
First affected version
unreleased (main [exec], 12.0.0-SNAPSHOT at e1faeba08bd, and the 11.0 release branch branch_11x [read], 11.0.0-SNAPSHOT; the first affected release would be 11.0.0); every release from 4.7.0 to 10.5.2 and 10.6.0 (released 2026-10-29 from RC 4ae946e5a9b) is unaffected
Affected versions
[exec] main from 39549329509 (2025-07-03) through a9779ef3c193 fails: the finder's JUnit 3 of 4 with -ea and -da (the control passes), the independent reproduction's JUnit 6 of 7 with -ea and -da, a history probe 0 of 3 field sorts with the IllegalArgumentException at 39549329509 and at a9779ef3c193. [exec] not affected: 4.7.0, 8.11.4, 9.10.0, 9.12.3, 10.0.0, 10.5.2, the 10.6 RC 4ae946e5a9b and main's parent commit 62e02760321 (IllegalArgumentException from PriorityQueue). [read] branch_11x 603af870375 (the 11.0 release branch, cut from main on 2026-10-28) and upstream main 2f424fd428da (2026-10-30) have the same create() order with no size check; branch_10x 657014b3c57 (decommissioned on 2026-10-29) and branch_10_6 still call super(size) first. Releases before 4.7.0 throw the OutOfMemoryError from PriorityQueue itself: that is the original SOLR-5661 bug (executed on 4.6.1), long fixed and not this regression. Not fixed upstream: through main e1faeba08bd, FieldValueHitQueue.java was changed after 39549329509 only by #16857 (70a428b5ba9) and #16947 (f6e64aa0db3), and neither changes the order in create() [read 2026-11-02]. All runs on Linux aarch64 with JDK 25.0.3. [exec 2026-11-02] main e1faeba08bd still fails (minimal repro 1 of 1 with -ea and -da; the OutOfMemoryError is now raised under FieldValueHitQueue.create:147). [read 2026-11-02] branch_11x da3ea39fa59 has the same FieldValueHitQueue.java blob as main (affected); branch_10_6 91eaab4fb29 (the 10.6.1 work) keeps the 10.6.0 file, which calls super(size) first (not affected).
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 “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading
Upstream and prior reports
none found (searched 2026-10-29, read-only; the finder's 51-query run 2026-10-28; judge refresh 2026-10-29; a further refresh 2026-10-29): two scripted prior-report searches (GitHub 16, commits 3, downstream 2, lists 20, JIRA 4, web 6 queries each; 3,476 and 692 distinct hits, 0 high), 19 manual GitHub searches, the file lists of the 141 open apache/lucene PRs updated since 2026-10-01 and of the closed PRs updated since 2026-10-26, the commit history of FieldValueHitQueue.java, TopFieldCollectorManager.java, PriorityQueue.java and the comparators, lists.apache.org (dev, java-user, issues), SOLR-5661 on JIRA, and web searches. Nothing reports or fixes a field-sorted collector throwing OutOfMemoryError instead of the IllegalArgumentException on main. Related, not reports of this bug: SOLR-5661 (2014, the fixed original whose guard this regresses: same symptom, different root cause and code), #14873 (the change; its review discussed performance and making PriorityQueue final, not the size check), #14872 (open issue to remove the last PriorityQueue subclasses), #16857 ('Consolidate Sort pruning logic', merged 2026-10-29 as 70a428b5ba9, adds 71 lines to FieldValueHitQueue.java and deletes none, so it leaves the ordering alone; fix A's diff against 645d4fe14a20 does not apply on top of it unchanged) and open #16277 (3-ary queue, constructor arity only). Refresh: upstream main 645d4fe14a20, no commit on FieldValueHitQueue.java after 39549329509; none of the 60 issues and PRs updated since 2026-10-27 is about it; GitHub search 'FieldValueHitQueue updated:>=2026-10-27' finds only #16857; 'Requested array size exceeds VM limit' (updated since 2026-10-01), 'TopFieldCollectorManager OutOfMemoryError' and '"numHits" "MAX_ARRAY_LENGTH"' find nothing.
Lucene commit tested
main 645d4fe14a20: the minimal test with assertions on and off, standalone and through gradle; the recommended fix in a scratch worktree with the closest core suites and :lucene:core:check -x test. SHA-1-checked release jars: the minimal test on 10.5.2 and 10.6.0 (passes); earlier probes on 4.6.1, 4.7.0, 8.11.4, 9.10.0, 9.12.3, 10.0.0, 10.5.2 and the 10.6 RC; javac builds of the introducing commit 39549329509 and its parent 62e02760321. Upstream main 2f424fd428da (2026-10-30) read: same order in create(). branch_11x at 603af870375 read: same order. Fact check (2026-11-01): the minimal test again on main (standalone and gradle, assertions on and off), fix A through gradle, and a release probe through FieldValueHitQueue.create and TopFieldCollector.create on 4.6.1, 4.7.0, 4.9.0, 4.9.1, 4.10.0, 5.5.5, 6.6.6, 7.3.1, 7.4.0, 7.5.0, 8.11.4, 9.10.0 and 10.6.0.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26 (at a9779ef3c193) (@lucene.experimental), :125-130 (create javadoc: 'pre-allocate a full array of length numHits'; size 'Must be greater than zero'), :132-159 (create: field.getComparator(size, ...) at :146 in the comparator loop, before new FieldValueHitQueue<>(fields, size, ...) at :153 reaches the PriorityQueue constructor)
  • lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39 (at a9779ef3c193) (@lucene.internal), :79 (PriorityQueue(int maxSize, LessThan)), :121-125 ('Throw exception to prevent confusing OOME', maxSize >= ArrayUtil.MAX_ARRAY_LENGTH -> IllegalArgumentException)
  • lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:38 (at a9779ef3c193) (values = new long[numHits])
  • lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45 (at a9779ef3c193) and :96-97 (pre-allocation NOTE), :62-64 (only numHits <= 0 rejected), :112-114 (newCollector -> FieldValueHitQueue.create(sort.getSort(), numHits))
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:588-597 (at a9779ef3c193) and :721-729 (n capped at max(1, maxDoc) before the collector manager is built)
  • lucene/core/src/java/org/apache/lucene/search/SortRescorer.java:38 (at a9779ef3c193) and :49 (rescore passes topN unchanged to new TopFieldCollectorManager)
  • lucene/core/src/java/org/apache/lucene/util/LongHeap.java:57 (at a9779ef3c193) and TernaryLongHeap.java:57 (the same guard re-applied in newer heaps; source of the score-ordered control's IllegalArgumentException)
  • lucene/CHANGES.txt:12866-12867 (at a9779ef3c193) (4.7.0 Bug fixes: 'SOLR-5661: PriorityQueue now refuses to allocate itself if the incoming maxSize is too large (Raintung Li via Mike McCandless)') and :75 (11.0.0 API Changes: GITHUB#14873)
  • Lucene main a9779ef3c193 SECURITY.md lines 28, 38, 46, 50 (threat model)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:44-45, :62-64, :96-97, :112-114 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26, :114-115, :125-130, :132-159 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39, :79, :121-125 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:38 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/util/TernaryLongHeap.java:57-61 (at 645d4fe14a20)
  • a Lucene clone releases/lucene/10.5.2 and releases/lucene/10.6.0: lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:122-123
  • lucene/core/src/java/org/apache/lucene/search/SortField.java:481-530 (getComparator; CUSTOM at :511-514) (at 645d4fe14a20)
Who is affected
  • lucene/core/src/java/org/apache/lucene/search/SortRescorer.java:38, :49 (at 645d4fe14a20)
  • lucene/expressions/src/java/org/apache/lucene/expressions/ExpressionRescorer.java:37 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:721, :729, :733 (at 645d4fe14a20)
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java:451; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:750-752 (at 645d4fe14a20)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java:39, :121-125 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/util/LongHeap.java:57-58; util/TernaryLongHeap.java:57-58 (at 645d4fe14a20)
  • lucene/CHANGES.txt:75-76 (11.0.0 API Changes, GITHUB#14873), :12883-12884 (4.7.0 Bug fixes, SOLR-5661) (at 645d4fe14a20)
  • lucene/MIGRATE.md:414-421 (PriorityQueue now takes a LessThan) (at 645d4fe14a20)
  • lucene/core/src/test/org/apache/lucene/util/TestPriorityQueue.java:378-382 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java:26, :130 (at 645d4fe14a20)
Suggested fix
  • lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java:62-64, :112-114 (at 645d4fe14a20)
History
  • a Lucene clone origin/branch_11x (603af870375), origin/branch_10_6 (7e5aec4370e), 48cf3f515d6 (#16911), c5ab3e57b9b: lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the mechanism in the brief and re-rendered after the fact checks (wording: built-in sorts, main and the 11.0 branch, the window's scope); the PNG is the summary frame.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.