One line of integer arithmetic: the method adds the modelled array, 16 + n × 4 bytes, as an int, which is wrong once n reaches 536,870,908.
RamUsageEstimatoris Lucene's helper for estimating how many bytes a Java object takes on the heap. Lucene's query cache sizes queries with it, and downstream memory budgets add up its estimates.- For a collection,
sizeOfCollectionassumes it is backed by an array, likeArrayList: the collection object, plus one array of references (a 16-byte header and 4 bytes per element with compressed references; 24 and 8 without), plus each element's own estimate. - A Java
intholds at most 2,147,483,647. Arithmetic past that silently wraps around to negative numbers; alongholds up to about 9.2 × 10^18.
16 + n × 4, computed in a 32-bit int, wraps to a negative number at n = 536,870,908, while the same sum in long gives the right size. Select the still to open it at full size. Generated with AI, checked against the code.- Make a list of 536,870,908 elements, for example
Arrays.asList(new Object[536870908]), and askRamUsageEstimator.sizeOfCollection(list)how many bytes it uses. - You expect at least its backing array: a 16-byte header plus 536,870,908 references of 4 bytes (compressed references, the usual case), 2,147,483,648 bytes, which
RamUsageEstimator.shallowSizeOf(array)reports correctly. With the 24-byte list object: 2,147,483,672. - The method adds
16 + 536,870,908 × 4in anint. The largestintis 2,147,483,647, so 2,147,483,648 wraps to -2,147,483,648; with the 24-byte list object and rounding to 8 bytes you get -2,147,483,616. - One element fewer gives the right 2,147,483,672. From 536,870,908 elements on (268,435,453 with 8-byte references) every estimate is 4 GiB or more too small; with real elements it usually stays positive, for example 40% too small for
Integerelements at the threshold.
Only code that sizes one collection of 536,870,908 or more elements (2 GiB of references) with this @lucene.internal estimator, typically downstream memory accounting. By default no Lucene code path does (the sandbox CoveringQuery could, with a clause limit raised far above 1024). A budget that sums the estimates under-counts by 4 GiB or more.
Mechanism, terms and numbers793 words
RamUsageEstimator.sizeOfCollection estimates a collection's heap size as the collection object, plus one array of references, plus each element's estimate. It adds the array part as NUM_BYTES_ARRAY_HEADER + collection.size() * NUM_BYTES_OBJECT_REF (RamUsageEstimator.java:307), where both constants and Collection.size() are int, so the sum is computed in 32-bit arithmetic and only then widened to long. From 536,870,908 elements on (2^29 - 4, with 4-byte references and a 16-byte array header; 268,435,453 with 8-byte references and a 24-byte header; both with the default 8-byte object alignment) it wraps. sizeOfCollection(Arrays.asList(new Object[536870908])) returns -2,147,483,616 bytes, while shallowSizeOf of the backing array correctly returns 2,147,483,648.
The estimate is then always too small by a multiple of 4 GiB (with 4-byte references 8 GiB from 1,610,612,732 elements on; up to 16 GiB with 8-byte references), and it is negative only when the elements add little: with Integer elements it is positive but 40% low at the threshold. sizeOfObject on a collection and sizeOfMap on a map with collection values reach the same line. Every other array formula in the class widens to long first. With default settings no Lucene code path sizes a collection that large (the sandbox CoveringQuery would, but only after the application raises the clause limit from 1024 to more than 536,870,907), so in practice only callers of this @lucene.internal helper, typically downstream memory accounting, are affected. It is in every release since 8.2.0 (2019) and on main; the one-line cast (long) collection.size() * NUM_BYTES_OBJECT_REF fixes it and passes Lucene's core tests.
private static long sizeOfCollection(Collection<?> collection, int depth, long defSize) { // RamUsageEstimator.java:298
if (collection == null) {
return 0;
}
long size = shallowSizeOf(collection); // :302
if (depth > MAX_DEPTH) {
return size;
}
// assume array-backed collection and add per-object references // :306
size += NUM_BYTES_ARRAY_HEADER + collection.size() * NUM_BYTES_OBJECT_REF; // :307, all int
for (Object o : collection) {
size += sizeOfObject(o, depth, defSize);
}
return alignObjectSize(size); // :311
}The same class sizes the same array correctly in shallowSizeOf(Object[]):
return alignObjectSize(
(long) NUM_BYTES_ARRAY_HEADER + (long) NUM_BYTES_OBJECT_REF * arr.length); // :512-513shallowSizeOf(list)= 24 bytes (:302).:307, inint: 16 + 536,870,908 × 4 = 2,147,483,648. That is one more thanInteger.MAX_VALUE, so it wraps to -2,147,483,648, andsize= 24 - 2,147,483,648 = -2,147,483,624.- The elements are null and add 0.
alignObjectSizerounds with Java's%, which keeps the sign, so the negative sum moves 8 bytes toward zero: -2,147,483,616.
With 536,870,907 elements the sum is 2,147,483,644, which fits, and the result is the correct 2,147,483,672. Adding one null element drops the estimate by 4,294,967,288 bytes (2^32 - 8).
- Threshold. (2^31 - 1 - header) / ref + 1: 536,870,908 = 2^29 - 4 with 4-byte references and a 16-byte header, 268,435,453 with 8-byte references and a 24-byte header (
:129,:133; both with the default 8-byte object alignment). The header addition overflows first. For these two layouts a Z3 bit-vector model of the line found these minimal sizes and proved that no smaller size overflows. - Size of the error. Always a multiple of 2^32 bytes before alignment (2^32 - 8 after it where the result is negative). With 4-byte references it is 4 GiB from 536,870,908 to 1,610,612,731 elements and 8 GiB from 1,610,612,732 on (
nCopies(Integer.MAX_VALUE, null)gives 40 bytes instead of 8,589,934,632). With 8-byte references it reaches 16 GiB (48 bytes instead of 17,179,869,232 atInteger.MAX_VALUE). - Negative only sometimes. With null elements (4-byte references) the result is negative in two ranges: from 536,870,908 to 1,073,741,810 elements, and again from 1,610,612,732 to 2,147,483,634, where the
intsum wraps a second time. In between it is positive but 4 GiB low (it climbs from 0 back to 2,147,483,672); above the second range it is 0 to 40 bytes, 8 GiB low. With real elements it is usually positive but 4 GiB low: about 3% of the total with the default 256-byte size for unknown objects, 40-50% with sharedIntegerelements at the threshold (20-25% further up). A "not negative" check alone misses most cases. - Entry points. Both public
sizeOfCollectionoverloads (:285,:294);sizeOfObjecton aCollection(:393,:403, theCollectioncase at:434); a collection value in a map passed to the publicsizeOfMap(:247,:256, via:275). The hppc*ObjectHashMap.ramBytesUsedmethods do not reach it: they pass the iteration cursor, not the value, tosizeOfObject(IntObjectHashMap.java:446-447). - Not affected.
sizeOfMap's own sums (:269-277),shallowSizeOf(Object[])(:511-514) andshallowSizeOfArray(:561-574) add inlong. Of the file's 16 multiplications (on 15 lines),:307is the only one done inint.
Exposure and likelihood359 words
- Who. Code that passes one huge collection to
sizeOfCollection,sizeOfObjector (as a map value)sizeOfMap.RamUsageEstimatoris public and@lucene.internal; downstream projects use it for memory accounting. A read-only code search foundsizeOfCollectioncall sites in 26 files of one downstream search engine, among them a query circuit breaker (read, not run). - Not Lucene itself, by default. No in-tree production code calls
sizeOfCollectiondirectly. The only in-tree caller that passes aCollectionis the sandboxCoveringQuery(sizeOfObject(queries, ...),CoveringQuery.java:82-85), whose constructor rejects more thanIndexSearcher.getMaxClauseCount()sub-queries (:70-72; default 1024). The limit is the application's to set:IndexSearcher.setMaxClauseCount(int)is public and accepts any value of at least 1, so an application that raises it above 536,870,907 and passes that many sub-queries does reach the line. No query parser builds aCoveringQuery. The other callers pass arrays, maps whose values are not collections, or scalars. - Default vs opt-in. No setting is involved, apart from that clause limit. The threshold depends only on the JVM's object layout: the reference size and the array header, which the class derives from the reference size and the object alignment. With the default 8-byte alignment it is 536,870,908 with compressed references and 268,435,453 with 8-byte references (
-XX:-UseCompressedOops, or heaps too large for compressed references). A non-default-XX:ObjectAlignmentInBytescan change the header (for example 32 bytes with 8-byte references and 16-byte alignment, which gives 268,435,452, by arithmetic); the test computes n from the runtime constants. - Scale. Either threshold is 2 GiB of references in one collection.
- Effect. A memory budget that sums these estimates under-counts by 4 GiB (at most 8 or 16 GiB) and trips later than intended. Nothing in Lucene uses the value to decide matching, filtering, deletions or what is written. With compressed references, elements of 28 bytes or more push the correct estimate to about 16 GiB before the threshold, so a budget of half the heap trips first (executed probe).
- Likelihood: contrived (a judgement). It takes one collection with 2 GiB of references, sized by this estimator, and no default Lucene path builds or sizes one; accidental triggering looks unlikely.
One JUnit test, public API only, deterministic and heap-free: Collections.nCopies makes a virtual list of 536,870,908 nulls (the size is computed from the JVM's constants), and the test asserts that its estimated size is not negative. It runs in about 5 s.
/*
* 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.util;
import java.util.Collections;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRamUsageEstimatorLargeCollection extends LuceneTestCase {
public void testSizeOfLargeCollectionIsNotNegative() {
// n: smallest size whose header + n * ref bytes exceed an int; expected >= 0 bytes, actual < 0
int n =
(Integer.MAX_VALUE - RamUsageEstimator.NUM_BYTES_ARRAY_HEADER)
/ RamUsageEstimator.NUM_BYTES_OBJECT_REF
+ 1;
long bytes = RamUsageEstimator.sizeOfCollection(Collections.nCopies(n, null));
assertTrue("sizeOfCollection of " + n + " elements: expected >= 0, got " + bytes, bytes >= 0);
}
}
cp $REPRO/TestRamUsageEstimatorLargeCollection.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.TestRamUsageEstimatorLargeCollection -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
sizeOfCollection(Collections.nCopies(536870908, null))returns a size of at least 0 bytes (2,147,483,672 with the fix).- Observed
AssertionError: sizeOfCollection of 536870908 elements: expected >= 0, got -2147483616, with assertions on and off.
More files, environment, full output302 words
- Why
nCopies. It is a virtual list, so the test needs no heap beyond the default; the method still iterates the 536,870,908 nulls (about 5 s). The stronger, contract-based form comparessizeOfCollection(Arrays.asList(backing))withshallowSizeOf(backing)(-2,147,483,616 against 2,147,483,648), but it needs a 2 GiB array (-Xmx4g). The virtual list holds no backing array, so fornCopiesthe "expected" size is the method's own array model, not memory the list retains; theArrays.asListcomparison is the stronger demonstration. - Smallest input. n is computed from the runtime constants, so it is the smallest failing size on any JVM layout: with the default 8-byte alignment 536,870,908 with compressed references, 268,435,453 with
-XX:-UseCompressedOops(then:expected >= 0, got -2147483608). 536,870,907 gives the correct 2,147,483,672. - As the regression test: this same test, for example as a method of
TestRamUsageEstimator. It passes with the fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp $REPRO/TestRamUsageEstimatorLargeCollection.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.TestRamUsageEstimatorLargeCollection -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on (the same with -Ptests.asserts=false)
TestRamUsageEstimatorLargeCollection > testSizeOfLargeCollectionIsNotNegative FAILED
java.lang.AssertionError: sizeOfCollection of 536870908 elements: expected >= 0, got -2147483616
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C88601A39A7659BE]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.assertTrue(Assert.java:42)
at org.apache.lucene.util.TestRamUsageEstimatorLargeCollection.testSizeOfLargeCollectionIsNotNegative(TestRamUsageEstimatorLargeCollection.java:31)
:lucene:core:test (FAILURE): 1 test, 1 failure
# 10.6.0 (Maven Central jar), assertions on and off
java.lang.AssertionError: sizeOfCollection of 536870908 elements: expected >= 0, got -2147483616
# main 645d4fe14a20, -XX:-UseCompressedOops (8-byte references), assertions off
java.lang.AssertionError: sizeOfCollection of 268435453 elements: expected >= 0, got -2147483608
# with the fix, assertions on and off
:lucene:core:test (SUCCESS): 1 test
# the finder's array-backed tests, main ea57360912ed, -Xmx4g, assertions off
sizeOfCollection(536870908 elements) = -2147483616, but the backing Object[] alone is 2147483648 bytes
sizeOfObject(list of 536870908) = -2147483616 < backing array 2147483648- The javadoc promises "the size in bytes of a Collection object, including sizes of its values", with no size limit.
- The class's other array formulas widen to
longfirst, a rule adopted in 2012 for arrays above 2 GB. - A negative or 4 GiB-low byte count is no "best effort" estimate: the method gets its own formula wrong.
The full argument175 words
- The contract. Both public overloads: "Returns the size in bytes of a Collection object, including sizes of its values" (
RamUsageEstimator.java:281,:290), with no size limit. The comment above the line says the method models an array-backed collection (:306). 536,870,908 is a legal array length; the finder's test allocated it. - The class gets the same array right elsewhere.
shallowSizeOf(Object[])(:511-514) andshallowSizeOfArray(:561-574) cast tolongbefore multiplying. Those casts came in with LUCENE-3867 (#4940, 2012), because the array formulas "can overflow if arrays are greater than 2 GB". - History shows no intent. The line came in with the LUCENE-8855 follow-up
7989a863faad"Fix some size estimates and relax test assertions to work under different JVMs." (2019), which replaced the constantNUM_BYTES_ARRAY_HEADER * 2with the element-count term. Neither the commit nor the LUCENE-8855 discussion (#9898) mentions a size limit. - Tests. The only collection test,
TestRamUsageEstimator.testCollection(:165-181), uses a 102-element list and accepts a 10% error (:174). No test or javadoc sets a size limit.
The devil's-advocate check195 words
The intended-behaviour checker built the strongest case for "not a bug": RamUsageEstimator is @lucene.internal; its class javadoc says the measurements "may be slightly wrong" and sizeOfObject is a "Best effort attempt"; the array-backed model is itself a heuristic; no Lucene code reaches the size; and with the default 256-byte element size the error is only about 3%.
Why it fails: @lucene.internal is about API stability ("might change in incompatible ways in the next release"), not a waiver for wrong results, and the methods are public and documented without a size limit. "May be slightly wrong" is the class javadoc's caveat for a non-OpenJDK/Oracle-based JVM, that is, for layout assumptions, and "Best effort attempt" describes sizing objects of undetermined type, which fall back to a default size; neither covers wraparound. Here the method gets its own array formula wrong, on HotSpot, by a multiple of 2^32. A negative byte count is never a valid estimate, and with shared Integer elements the error is 40-50% at the threshold, outside the 10% that testCollection accepts. What the argument does establish: priority is low, and "no Lucene code reaches the size" holds only with the default clause limit.
Recommended A. Cast the element count to long
Widen before multiplying: NUM_BYTES_ARRAY_HEADER + (long) collection.size() * NUM_BYTES_OBJECT_REF, as the class's other array formulas already do. Results below the threshold do not change. Validated: the test passes, and so do the closest suites and all of :lucene:core:test.
// assume array-backed collection and add per-object references
- size += NUM_BYTES_ARRAY_HEADER + collection.size() * NUM_BYTES_OBJECT_REF;
+ size += NUM_BYTES_ARRAY_HEADER + (long) collection.size() * NUM_BYTES_OBJECT_REF;All options and trade-offs3 options · 347 words
Three options; option A was applied in a scratch worktree of main 645d4fe14a20 and run against the minimal test, the closest suites, all core tests and core's static checks.
One line in sizeOfCollection (RamUsageEstimator.java:307). (long) collection.size() * NUM_BYTES_OBJECT_REF is at most (2^31 - 1) × 8, far inside long, and the header is then added in long. Below the threshold the int expression was exact, so no existing estimate moves.
// assume array-backed collection and add per-object references
- size += NUM_BYTES_ARRAY_HEADER + collection.size() * NUM_BYTES_OBJECT_REF;
+ size += NUM_BYTES_ARRAY_HEADER + (long) collection.size() * NUM_BYTES_OBJECT_REF;Trade-offs
None found: sizeOfCollection is not on a search hot path, and one widening costs nothing next to the per-element loop.
What the checks validated
- The minimal test passes with assertions on and off (gradle, scratch worktree of
645d4fe14a20; also standalone). - Closest suites, seed
DEADBEEF:TestRamUsageEstimator, the minimal test, the 15 hppc collection and map test classes andTestLRUQueryCache, 482 tests (4 skipped), 0 failures; sandboxTestCoveringQuery, 5 tests (2 skipped), 0 failures. - All of
:lucene:core:testpasses (random seed, 8,898 tests, 321 skipped, 0 failures, the minimal test included). :lucene:core:check -x testpasses (formatting, ecj lint, forbidden APIs, license checks).- Earlier, on main
ea57360912ed: the finder's 3 tests, an independent verifier's 4 tests (with 4-byte and 8-byte references) and a boundary check at every size pass with the same cast.
size += alignObjectSize((long) NUM_BYTES_ARRAY_HEADER + (long) NUM_BYTES_OBJECT_REF * collection.size()); also aligns the modelled array on its own, as a real array is.
Trade-offs
More faithful, but with 4-byte references it can raise the estimate of a collection with an odd number of elements by up to 8 bytes: a behaviour change beyond the bug. Not validated.
Clamp a negative estimate, or use Math.multiplyExact / Math.addExact.
Trade-offs
Not recommended: a clamp misses the more common positive-but-4 GiB-low case, and an exception from an estimator breaks callers that only want a number. Not validated.
Downstream memory accounting, such as a cache or a circuit breaker, that sizes a collection of 536,870,908 or more elements with this estimator under-counts it by 4 GiB or more and so trips later than intended.
Semi-formal. While modelling RamUsageEstimator, an AI tool listed the file's 16 multiplications and found only this one computed in int. A Z3 bit-vector model of the line for the default layouts gave the smallest overflowing sizes and proved that no smaller one overflows; a JUnit test on the stock build then reproduced it.
The method in full142 words
- The hunt unit covered numeric, string and version utilities; the finder was modelling
RamUsageEstimatorfor a no-throw property. - Its inventory of the file's 16 multiplications (on 15 lines) showed
:307as the only one evaluated inint; the others multiply inlong. - A Z3 32-bit bit-vector model of
NUM_BYTES_ARRAY_HEADER + n * NUM_BYTES_OBJECT_REFgave the minimal overflowing n: 536,870,908 for 4-byte references and a 16-byte header, 268,435,453 for 8-byte references and a 24-byte header; below each, the expression is exact (unsat). Both sizes are legal array lengths. - A public-API JUnit test on the stock main build then failed:
Arrays.asList(new Object[536870908])sized at -2,147,483,616 bytes against 2,147,483,648 for its backing array. Independent verifiers reproduced it with their own tests, corrected the wording (the threshold is 2^29 - 4; the result is not always negative) and found no sign that it is intended.
No report found: 119 queries (2026-10-26) over GitHub issues, PRs and commits, the JIRA archive, the mailing lists, downstream code and the web, and a GitHub re-check on 2026-11-01. sizeOfCollection matches no Lucene issue or PR. Upstream main, branch_11x and branch_10_6 still have the line.
Searches5 searches
Closest items, none the same bug: #9898 (LUCENE-8855), which introduced the method; #4940 (LUCENE-3867, 2012), which added the long casts to the array formulas; the 2022 error-prone int-overflow sweep #11910, #11921 and #11923, whose lists do not include this file; #15026 (sizeOf(Query) accuracy) and #16113/#16114 (Accountable ownership); the merged accuracy fixes #16743 and #16296, which do not touch the line; elastic/elasticsearch#149697, a downstream circuit breaker that calls sizeOfCollection and reports no overflow. Gaps: GitHub search returns at most 50 results per query; web search used one engine; private security reports cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-26 | three scripted searches plus manual searches, for example sizeOfCollection, "RamUsageEstimator overflow", "RamUsageEstimator negative", "sizeOfObject overflow"; the changed files of all 411 open PRs | no report; 0 issues or PRs mention sizeOfCollection; open #12295 and #11873 touch the file but not the line |
| GitHub re-check (apache/lucene) | 2026-11-01 | sizeOfCollection, "RamUsageEstimator overflow", "RamUsageEstimator negative"; the 15 newest issues and PRs that mention RamUsageEstimator; open PRs | no report; nothing new since 2026-10-26 |
| JIRA archive | 2026-10-26 | text ~ "sizeOfCollection" and the scripted JIRA queries | 0 results |
| Mailing lists (dev@, java-user@, issues@, commits@; Solr lists) | 2026-10-26 | scripted list queries plus manual searches | no report |
| Downstream and web (Solr, Elasticsearch, other GitHub projects) | 2026-10-26 | GitHub searches and 6 web queries | callers only; elastic/elasticsearch#149697 reports no overflow |
Introduced on 2019-06-28 by the LUCENE-8855 follow-up 7989a863faad, which replaced a constant with the element-count term; first released in 8.2.0. It fails on every release run from 8.2.0 to 10.6.0 and on main; 8.1.1 has no such method.
Versions tested5 rows
- Introduced:
7989a863faad"LUCENE-8855: Fix some size estimates and relax test assertions to work under different JVMs." (authored 2019-06-27, committed 2019-06-28; branch_8x cherry-pick84db2b274286), through the JIRA patch workflow, no PR; the issue is LUCENE-8855, migrated as #9898. It replacedsize += NUM_BYTES_ARRAY_HEADER * 2;, the form in whicha76c962ee61fhad added the method one day earlier and which was never released. - Bisect on the 8.2.0 jars, shadowing only
RamUsageEstimator: the source of84db2b274286overflows (2,147,483,672 at n - 1, -2,147,483,616 at n); its parent does not (a constant 56, an undercount that was never released). - The identical line is in all 48 release tags from 8.2.0 to 10.5.2 in the history clone and in the 10.6.0 tag; upstream main (2026-10-30),
branch_11xandbranch_10_6still have it. Not executed: 8.3.0-8.11.3, 9.1.0-9.12.2 and 10.1.0-10.5.1. Their tagged source has the identical line, except 8.8.2-8.11.3, which have no tags in the history clone; those lie between 8.8.1 (line present) and 8.11.4 (executed, fails).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20, a9779ef3c193, ea57360912ed | affected | fails with assertions on and off (8-byte references too, on 645d4fe14a20 and ea57360912ed); passes with the fix |
| 10.6.0 | affected | fails with assertions on and off (minimal test) |
| 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4, 8.2.0 | affected | fails with the same values (release jars, assertions off) |
84db2b274286 vs its parent, on the 8.2.0 jars | bisect | the commit overflows at n = 536,870,908; its parent gives a constant 56 |
| 8.1.1 and earlier | no API | sizeOfCollection does not exist (compile error) |
The finder, an independent reproducer, a cross-model adversary, an intended-behaviour devil's advocate, an adjudication and a credibility audit all call it real; the adjudication confirmed it with high confidence. Re-run on three main commits, with 8-byte references and on 10.6.0.
Checks and verdicts330 words
The cross-model reviewer (OpenAI GPT-5.6) gave no verdict in the first review; in a later review it could not run code and agreed on the root cause, while the verifying AI tool's own boundary check supplied the executed evidence.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Multiplication inventory, Z3 bit-vector model (thresholds, exact below them), a 3-test public-API JUnit failing on main with assertions on and off; the cast passes it and the upstream tests. |
| cross-model review (first review) | no verdict | OpenAI GPT-5.6: no answer, which is not a refutation. |
| independent reproducer | real | Own 4-test JUnit from the claim only: 3 of 4 fail, the control below the threshold passes, on main (also with 8-byte references), 10.5.2 and 9.12.3; with the cast 4 of 4 pass. |
| cross-model adversary | real, medium | OpenAI GPT-5.6, could not run code: agrees on the root cause. The verifying AI tool's boundary check matches the long formula up to 536,870,907 and is 4,294,967,288 off at 536,870,908; with the fix it matches at every size. |
| devil's advocate | real, minor | No intended, documented, misuse or unreachable argument holds; Integer elements 40% low (50% at the 8-byte threshold). |
| adjudication | confirmed, high | Re-ran the independent reproduction's test (3 of 4 fail, control passes) and the fix check (4 of 4 pass); wording corrections. |
| credibility audit | reproduces | Fresh seed on main: 3 of 3 fail with the same values; impact unchanged (minor). |
| re-runs | reproduces | The finder's test on main a9779ef3c193 (main refresh); the minimal repro on 645d4fe14a20 (standalone and gradle, assertions on and off, 8-byte references) and on 10.6.0; passes with the fix. |
| fact checks | reproduces | An independent fact check re-ran the minimal test on main 645d4fe14a20 (assertions on and off, and with 8-byte references: fails; with the fix: passes) and probed the null-list result up to Integer.MAX_VALUE. A read-only cross-model fact check (OpenAI GPT-5.6) reviewed the text. Their corrections are adopted in this doc. |
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 412 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,074 words
- Registry title
- RamUsageEstimator.sizeOfCollection adds the assumed backing-array bytes in int arithmetic, so a collection of 536,870,908 or more elements (268,435,453 with 8-byte references) gets a size estimate that is a multiple of 4 GiB too low, and negative when the elements add little
- Area
- util / RamUsageEstimator
- Priority
- Low, rank 1196: RamUsageEstimator.sizeOfCollection overflows only for collections of 536M+ elements and has no Lucene caller.
- Impact class
- minor
- Impact note
- Only code that passes one Collection of 536,870,908 or more elements (268,435,453 with 8-byte references, that is at least 2 GiB of references) to the @lucene.internal RamUsageEstimator.sizeOfCollection or sizeOfObject (directly, or as a value inside sizeOfMap or an hppc *ObjectHashMap) gets an estimate that is 4 GiB too low at the threshold (up to 8 GiB, or 16 GiB with 8-byte references, near Integer.MAX_VALUE), negative only when the elements add little; no Lucene code path reaches it (the only in-tree caller that passes a Collection is the sandbox CoveringQuery, capped by the default clause limit of 1024), so it matters only to downstream memory accounting; any configuration, released since 8.2.0.
- Summary
- You ask RamUsageEstimator.sizeOfCollection (or sizeOfObject) for the size of a very large list, for example Arrays.asList(new Object[536870908]). You expect a size in bytes that is at least the 2,147,483,648 bytes of its backing array, which shallowSizeOf(Object[]) reports correctly. Instead the method adds the array header and the per-element references in int arithmetic, which wraps, so it returns -2,147,483,616; in general the estimate is too low by a multiple of 2^32 bytes from 536,870,908 elements on (268,435,453 with 8-byte references), and it is negative only when the elements' own sizes add little.
- Introduced
- 7989a863faad (master, now main; branch_8x cherry-pick 84db2b274286), the LUCENE-8855 follow-up 'Fix some size estimates and relax test assertions to work under different JVMs.' by sigram (GitHub), no PR (2019 JIRA patch workflow; issue LUCENE-8855, migrated as apache/lucene#9898) [read: git blame (also -w -M -C) at the history head 7cecf795236 gives RamUsageEstimator.java:307 to 7989a863faad, git log -L lists only a76c962ee61f and 7989a863faad, git log -S/-G on the expression finds only 7989a863faad and 84db2b274286; exec: on the 8.2.0 jars with only RamUsageEstimator shadowed, the source of 84db2b274286 overflows (2147483672 at n-1, -2147483616 at n) and the source of its parent 84db2b274286^ does not (constant 56)]; it replaced 'size += NUM_BYTES_ARRAY_HEADER * 2', the form with which a76c962ee61f (LUCENE-8855, 2019-06-26) had added the method one day earlier and which was never released (2019-06-27 (authored; committed and pushed 2019-06-28))
- First affected version
- 8.2.0 [exec]; 8.1.1 and earlier have no sizeOfCollection (compile error) [exec]
- Affected versions
- 8.2.0-8.11.4, 9.0.0-9.12.3, 10.0.0-10.6.0 and main (ea57360912ed, a9779ef3c193 and 645d4fe14a20) [exec: the minimal test fails on 10.6.0 and main 645d4fe14a20 with -ea and -da (on main also with 8-byte references); the finder repro fails with -da on 8.2.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main, and with -ea on main; the independent reproduction's test and the verifying AI tool's boundary check also fail on 10.5.2 and 9.12.3]; the identical line is in all 48 release tags from 8.2.0 to 10.5.2 that the history clone has (8.8.2-8.11.3 are not in the clone and are bracketed by the 8.8.1 source and the 8.11.4 run) [read]; upstream main e1c4184cb983 (line 307), branch_10x and branch_10_6 (line 309; 10.6.0 was released from branch_10_6 on 2026-10-29) still have it (2026-10-26) [read]; 8.1.1 and earlier: API absent [exec]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: code reading; also reached by Z3
- Upstream and prior reports
- none found (2026-10-26: three scripted searches with 119 queries over apache/lucene GitHub issues and PRs, commits, downstream repositories, dev@, java-user@, issues@ and commits@, the JIRA archive and 6 web queries; 8 manual GitHub searches across apache/lucene, apache/lucene-solr, apache/solr, elastic/elasticsearch and opensearch-project/OpenSearch; manual lists.apache.org searches of the Lucene and Solr lists; the file lists of all 411 open apache/lucene PRs (2 touch RamUsageEstimator.java, #12295 and #11873, neither line 307); GitHub issue and PR search for 'sizeOfCollection' in apache/lucene and JIRA text~'sizeOfCollection' both return 0; the 2022 error-prone int-overflow sweep #11910/#11921/#11923 lists no RamUsageEstimator.java site); not fixed upstream: main e1c4184cb983 line 307, branch_10x and branch_10_6 line 309 still have the int expression and CHANGES.txt has no entry; related, not reports of this bug: #9898 (LUCENE-8855, introduced the method), #4940 (LUCENE-3867, 2012, added the long casts to the array formulas for arrays above 2 GB), #15026 (sizeOf(Query) accuracy), #16113/#16114 (Accountable ownership), #16743 and #16296 (merged RamUsageEstimator accuracy fixes), elastic/elasticsearch#149697 (a downstream circuit breaker that calls sizeOfCollection and reports no overflow)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test with assertions on and off standalone (2026-10-29) and through gradle in a scratch worktree (2026-11-01), and with 8-byte references standalone (2026-11-01); the recommended fix in that worktree with the test,TestRamUsageEstimator, the hppc map tests,TestLRUQueryCache, sandboxTestCoveringQuery, all of:lucene:core:testand:lucene:core:check -x test. Release 10.6.0 (Maven Central jar, SHA-1 checked), assertions on and off. Earlier runs: mainea57360912ed(also with 8-byte references) anda9779ef3c193; the 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4 and 8.2.0 jars; 8.1.1 (no such method); a commit-level check of84db2b274286and its parent on the 8.2.0 jars. Read: upstream main2f424fd428d(2026-10-30),branch_11xandbranch_10_6still have the line. All runs used OpenJDK 25.0.3 on Linux aarch64.
- Finding record
lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:280-312 (sizeOfCollection javadoc 'Returns the size in bytes of a Collection object, including sizes of its values'; :306 comment 'assume array-backed collection and add per-object references'; :307 the int expression), :511-514 (shallowSizeOf(Object[]) widens to long)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:34-43, :62, :68, :124-133, :166-169, :240, :247-312, :389-444, :511-514, :561-574 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:470 (sizeOf(query, 32)) (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/internal/hppc/IntObjectHashMap.java:444-450, LongObjectHashMap.java:444-450, CharObjectHashMap.java:446-452, ObjectCursor.java:24-37 (at 645d4fe14a20)
- Who is affected
lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:42 (@lucene.internal), :124-133 (at 645d4fe14a20)lucene/sandbox/src/java/org/apache/lucene/sandbox/search/CoveringQuery.java:69-86; lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:80, :255-261 (setMaxClauseCount); search/Multiset.java:31 (at 645d4fe14a20)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/RamUsageEstimator.java:34-43, :280-312, :389-404, :511-514, :561-574 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/util/TestRamUsageEstimator.java:165-181 (at 645d4fe14a20)
- History
a Lucene clone: origin/main 2f424fd428d, origin/branch_11x 603af870375, origin/branch_10_6 7e5aec4370e, releases/lucene/10.6.0 (RamUsageEstimator.java read)
- TestRamUsageEstimatorLargeCollection.java:
repro/LFV-323/src/org/apache/lucene/util/TestRamUsageEstimatorLargeCollection.java(33 lines)
assets/LFV-323/bug.png(412 KB)
