All findings · Lucene finding · LFV-043 · timsorter-run-stack-overflow

TimSorter overflows its 49-entry run stack on a legal input of 1,093,938,049 elements, because it checks only the top three runs

Sorting 1,093,938,049 elements arranged as a specific pattern of 1,061 sorted runs, with CollectionUtil.timSort, ArrayUtil.timSort or index sorting, throws ArrayIndexOutOfBoundsException: TimSorter checks its stack rule only on the top three runs, so its 49-slot run stack overflows. OpenJDK fixed the same flaw in JDK 11 with a fourth-run check.

Summary

Priority
Low, rank 1194TimSorter overflows its run stack only on at least 1.09 billion elements in an LP-solved run pattern, loud and practically unreachable.
Impact
crash
Since
5.1.0 (2015); the root defect since 4.3.0 (2013)introduced 2015-02-26 · over 11 years
Affected
5.1.0 to 10.6.0, and main
Status
documented
Found by
non-formal: a replay of a published TimSort counterexample, then a linear program

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

One missing comparison: TimSorter's merge rule looks at the top three runs only, so the run stack can outgrow its 49 slots.

Background: how TimSort keeps its run stack small

  1. TimSort splits the input into runs: stretches that are already sorted. Short ones are extended to a minimum length, 32 to 64 elements in Lucene.
  2. It pushes each run onto a stack and merges neighbours so that every run is longer than the two above it together. Then run lengths grow at least like Fibonacci numbers, and 40 entries are enough for any int-sized input.
  3. Lucene's TimSorter reserves 49 entries and does not check the bound when it pushes a run.
Summary card in three panels. 1, what you do: CollectionUtil.timSort on 1,093,938,049 Booleans in 1,061 ascending runs, every run but the last at least 38 elements (minRun(n) = 33), a legal input; expected: the list comes back sorted; actual: ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50 at TimSorter.pushRunLen(TimSorter.java:98); the list is left unsorted, no element lost. 2, where it goes wrong, the run stack before push #6 of the real input: after merging 433,663 + 433,659 the stack holds D 71,762,141, C 71,328,474, B 867,322 and A 867,319 (A on top); the checks C > B + A and B > A hold, but D <= C + B (71,762,141 <= 71,328,474 + 867,322) is never checked, and ensureInvariants stops. 3, at scale: the stack size at each of the 1,061 pushes; on main it climbs past 35, the most if the rule held everywhere, and reaches 49 at push #1,061, where it throws; with the fix it stays at most 18 runs at a push and the list sorts. Below: the fix line in ensureInvariants at TimSorter.java:135, if (runLen2 <= runLen1 + runLen0 || (stackSize > 3 && runLen(3) <= runLen2 + runLen1)).
TimSorter's run stack: a merge breaks the length rule one level below the three runs it checks, and with the 1,061-run input the stack fills all 49 slots. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You sort 1,093,938,049 elements with CollectionUtil.timSort (or ArrayUtil.timSort, or index sorting of a segment with that many documents), laid out as 1,061 ascending runs of 38 to 113,968,681 elements. The input is legal: every run but the one-element last run is longer than the minimum run length of 33. A linear program found the lengths.
  2. You expect a sorted result. If every run on the stack were longer than the two above it together, 40 entries would cover any int-sized input, and Lucene reserves 49.
  3. But ensureInvariants checks that rule only for the top three runs, so a merge can break it one level down unnoticed: stack 1,200 | 800 | 250 | 200, a run of 300 arrives, 250 and 200 merge, and now 1,200 < 800 + 450.
  4. In this input such breaks pile up: after 1,060 runs, 49 runs wait on the stack, and pushing the last run writes slot 50 of a 50-slot array: ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50. The data is left unsorted but intact.
  5. Through index sorting, IndexWriter.commit() throws the same exception and the writer closes with it as its tragic exception: the 1,093,938,049 buffered documents are lost.

Who is affected

Code that sorts 1.09 billion or more items with CollectionUtil.timSort, ArrayUtil.timSort or a TimSorter subclass, and index sorting of a segment that large. The run lengths must follow a pattern a linear program found; no natural data is known to produce it. A plain sort fails loudly, data intact; an index-sorting commit closes the writer and loses uncommitted documents.

Mechanism, terms and numbers995 words

Lucene's TimSorter keeps the sorted stretches (runs) it has found but not yet merged on a stack of 49 entries (runEnds has 1 + STACKSIZE = 50 slots, TimSorter.java:45, :62). The stack stays that small only if every run on it is longer than the two above it together; ensureInvariants (:127-153) checks that only for the top three runs, so a merge can break the rule one level deeper without anyone noticing. In 2015 LUCENE-6293 (#7355) met this flaw and raised the stack from 40 to 49 entries; in 2018 Auger, Jugé, Nicaud and Pivoteau showed for Java's identical check that such a bound still fails, and OpenJDK fixed its TimSort in JDK 11 (JDK-8203864) by also checking the fourth run. Lucene's TimSorter never got that check.

A legal input of 1,093,938,049 elements in 1,061 runs (every run but the one-element last run is at least 38, above minRun(n) = 33, so sort() detects exactly these runs; the lengths were solved for with a linear program) fills all 49 entries, and the 1,061st push throws ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50 at TimSorter.pushRunLen(TimSorter.java:98). It is reached through the Java-public CollectionUtil.timSort and ArrayUtil.timSort (methods of @lucene.internal classes), any TimSorter subclass, and index sorting, which is supported public API: IndexWriterConfig.setIndexSort plus IndexWriter.commit() on a 1,093,938,049-document in-memory segment (the writer is then closed and the buffered documents are lost) and SortingCodecReader.wrap on a segment that large, both run with a deliberately extreme setup. A plain sort leaves its data unsorted but still a permutation of the input. Every release from 5.1.0 to 10.6.0 and main is affected; the one-line fourth-run check fixes it.

The code

static final int STACKSIZE = 49; // depends on MINRUN                       // TimSorter.java:45
runEnds = new int[1 + STACKSIZE];                                           // :62, 50 slots, up to 49 runs

void pushRunLen(int len) {                                                  // :97
  runEnds[stackSize + 1] = runEnds[stackSize] + len;                        // :98, no bound check
  ++stackSize;
}

void ensureInvariants() {                                                   // :127
  while (stackSize > 1) {
    final int runLen0 = runLen(0);                                          // top run
    final int runLen1 = runLen(1);
    if (stackSize > 2) {
      final int runLen2 = runLen(2);
      if (runLen2 <= runLen1 + runLen0) {                                   // :135, top three runs only
        if (runLen2 < runLen0) { mergeAt(1); } else { mergeAt(0); }
        continue;
      }
    }
    if (runLen1 <= runLen0) { mergeAt(0); continue; }                       // :146
    break;
  }
}

// sort(from, to), :202-205
do {
  ensureInvariants();                                                       // :203
  pushRunLen(nextRun());                                                    // :204
} while (runEnd(0) < to);

nextRun() (:103-125) takes the next maximal non-descending stretch (or a strictly descending one, reversed) and extends it to at least minRun elements with a binary sort; minRun(length) is 32 to 64 (:67-78).

The rule, and why 40 or 49 entries were thought enough

TimSort wants every run on the stack to be longer than the two runs above it together, and longer than the one directly above it. Then lengths grow at least like Fibonacci numbers from the top down; with runs of at least 32 elements, 40 entries already reach Integer.MAX_VALUE. The stack was sized at 40 on that basis in 2013, and LUCENE-6293 (2015) raised it to 49 after the first counterexample to the three-run check, keeping the check. For this input's n and minRun(n) = 33 the rule would allow at most 35 runs (35 such runs hold at least 836,296,060 elements; 36 would need 1,353,155,492, more than n), the bound the diagram draws.

A small walk through the three-run check (merge decisions depend only on run lengths)

  1. Stack, bottom to top: 1,200 | 800 | 250 | 200. Every rule holds: 1,200 > 800 + 250, 800 > 250 + 200, 250 > 200.
  2. A run of 300 is pushed. The top three: 250 <= 200 + 300, and 250 < 300, so 250 and 200 merge (mergeAt(1)): 1,200 | 800 | 450 | 300.
  3. The top three again: 800 > 450 + 300 and 450 > 300, so the loop stops.
  4. One level deeper the rule is now broken: 1,200 < 800 + 450 = 1,250, and nothing checks it again. With the fourth-run check, step 3 sees 1,200 <= 800 + 450 and keeps merging.

The study kept the merge decisions of the published Java crash sequence (Auger et al., ESA 2018) fixed and re-solved its run lengths as a linear program so that every run is at least minRun(n); 17 such sequences were found, from 1,093,938,049 to 1,719,699,526 elements; the three run on the real jar (1,093,938,049, 1,285,317,058 and 1,719,699,526) all crash it. With the smallest, before the 1,061st push the stack holds 49 runs, from 299,699,572 elements at the bottom down to 38 at the top, and the rule is broken at 32 of its levels, for example 299,699,572 < 227,937,415 + 113,968,730 at the bottom (computed with the study's mirror of ensureInvariants; the diagram's probe, which records the real TimSorter's stack, shows the same 49 runs). The push then writes runEnds[50] and throws.

Lucene's larger minRun (32 to 64; the JDK uses 16 to 32) does not protect it: merge decisions depend only on ratios of run lengths, this pattern needs n/minRun of about 2^24.54, and for n between 2^30 and 2^31 Lucene allows up to 2^25. That is also why no input below 2^30 elements is known; smaller inputs with other patterns were not searched.

How index sorting reaches it

  • Flush. IndexingChain.flush calls maybeSortSegment first (IndexingChain.java:305), which calls Sorter.sortAndLeaveUnpacked (:297); that builds a doc comparator with a doc-ID tie-break (index/Sorter.java:225-238) and sorts all maxDoc doc IDs with DocValueSorter extends TimSorter (index/Sorter.java:85, :132-154). DocumentsWriterPerThread.flush turns any exception into an aborting one and fires a tragic event (DocumentsWriterPerThread.java:585-602).
  • Re-sort. SortingCodecReader.wrap(reader, sort) calls new Sorter(sort).sort(reader) (SortingCodecReader.java:445-446), the same path. The sort is skipped only when the segment is already sorted (index/Sorter.java:133-143).
Exposure and likelihood283 words
  • Who. Callers of CollectionUtil.timSort and ArrayUtil.timSort (public methods of @lucene.internal classes) or of a TimSorter subclass, on 1,093,938,049 or more elements in a run pattern like this one; and applications that use index sorting, the supported public route, on segments of that many documents. FreqProxTermsWriter's DocOffsetSorter (postings of index-sorted segments) is another in-tree TimSorter subclass; it was not assessed.
  • Default vs opt-in. The utility path needs no setting. Index sorting is opt-in (IndexWriterConfig.setIndexSort).
  • Scale. 1,093,938,049 elements, the smallest overflowing input the study found: below Integer.MAX_VALUE and below IndexWriter.MAX_DOCS (IndexWriter.java:208). The executed index routes were deliberately extreme: NoMergePolicy, a 1024 MB RAM buffer, one reused document holding only the sort field, a single segment of 1,093,938,049 documents, a 16 GB heap and about 5 minutes each. With real documents the per-thread hard RAM limit (default 1945 MB, IndexWriterConfig.java:93) makes a single flushed segment that large much less likely. The re-sort route is not bounded by that setting, but it still needs an existing segment of about 1.09 billion documents, an int[maxDoc] doc map (index/Sorter.java:146) and a large heap. The default TieredMergePolicy caps merged segments at 5 GB (TieredMergePolicy.java:77); how a normal application would come to hold such a segment was not studied.
  • Effect. A loud ArrayIndexOutOfBoundsException. A plain sort leaves its data unsorted but still a permutation of the input. On the flush route the IndexWriter is closed with the exception as its tragic exception, so the uncommitted documents are lost.
  • Likelihood: contrived. The run lengths must follow a pattern solved for with a linear program; the study's search, which kept one merge pattern fixed, found no smaller legal input. Priority low.

Minimal repro

One JUnit test through Java-public methods of @lucene.internal classes: CollectionUtil.timSort on a List<Boolean> view of a FixedBitSet that holds the 1,061 runs (each run is false values ending in one true). Deterministic and single-threaded, about 205 MB of heap, about a minute under Gradle. The array of run lengths is the input itself, the smallest the study found.

TestTimSorterRunStackOverflow.java140 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.util;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.RandomAccess;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestTimSorterRunStackOverflow extends LuceneTestCase {

  private static final int[] RUNS = {
      71762141, 71328474, 433663, 433659, 867319, 1734637, 3469274, 4625699, 2312850, 2312846,
      15146674, 1043270, 521635, 521631, 2086531, 2782041, 1391021, 1391017, 5409525, 4327615,
      1081906, 1081902, 2163804, 16189952, 1952643, 38, 1952677, 1952685, 5858002, 38, 27232480,
      673551, 673547, 1347094, 1347102, 1347086, 4310699, 1077677, 1077673, 2155346, 13222701,
      787070, 787066, 1574131, 2098842, 1049421, 1049417, 5876752, 1469190, 1469186, 2938372,
      71762145, 54762751, 2974702, 38, 2974736, 5949471, 5949479, 84, 42, 38, 17737952, 110634,
      110630, 110638, 331899, 331907, 165930, 38, 165934, 17848594, 202827, 202823, 202831, 608438,
      38, 17848598, 1216949, 1216907, 38, 2433889, 2433910, 2433868, 38, 8112996, 1622598, 1622594,
      1622602, 3245197, 1622594, 38, 71762149, 42206581, 113968681, 45, 41, 82, 165, 329, 659,
      1317, 2635, 5269, 10539, 21078, 42156, 84312, 168624, 337247, 674494, 1348988, 2697976,
      5395952, 10791905, 21456976, 126834, 126830, 253659, 507318, 1014636, 1217565, 811707,
      405854, 405850, 4556410, 313838, 156919, 156915, 627630, 38, 836891, 418445, 418441, 1627288,
      1301826, 325458, 325454, 650909, 4870256, 190615, 190611, 381223, 762445, 1016593, 508297,
      508293, 8158379, 269950, 269946, 539892, 539900, 539884, 1919614, 239955, 239951, 479902,
      959803, 2159577, 1057090, 42, 38, 2782046, 434697, 434655, 38, 434701, 434685, 1043265,
      695508, 347754, 347750, 21583817, 21207364, 376449, 376445, 376453, 1129343, 2258644, 42, 38,
      46, 4065710, 451745, 451741, 451749, 903490, 451741, 1355239, 677582, 38, 677616, 4517464,
      621146, 621142, 1242284, 1242305, 1242263, 38, 4517468, 49, 38, 53, 136, 144, 128, 462, 84,
      42, 38, 88, 38, 168, 84, 42, 38, 21583821, 13816717, 35400493, 42, 38, 76, 152, 304, 608,
      1216, 2432, 4864, 9728, 19456, 38912, 77824, 155648, 311296, 622592, 1245184, 1245192,
      3042559, 6738578, 39540, 39536, 79072, 158145, 316290, 421719, 210860, 210856, 1428263,
      47751, 47747, 95494, 190988, 381976, 664303, 99648, 99644, 199289, 265718, 132859, 132855,
      1476022, 169594, 38, 169628, 169640, 339264, 169632, 169628, 2599103, 64338, 64334, 128669,
      257300, 38, 411739, 102937, 102933, 205866, 1260945, 77205, 77201, 154401, 205868, 102934,
      102930, 540402, 180133, 90066, 90062, 180137, 180121, 6778127, 6690893, 87229, 87225, 174450,
      174458, 174442, 697718, 84, 42, 38, 1196332, 199352, 38, 199386, 398734, 38, 398780, 398734,
      42, 38, 1395692, 232586, 38, 232620, 232628, 38, 2093555, 42, 38, 46, 84, 130, 38, 218, 42,
      38, 50, 84, 42, 38, 6778131, 4144004, 10922089, 42, 38, 76, 152, 304, 608, 1216, 2432, 4864,
      9728, 19456, 38912, 77824, 155648, 311296, 622592, 862803, 2089918, 18070, 18066, 36131,
      48175, 24088, 24084, 126458, 42151, 21076, 21072, 42155, 42139, 438525, 25136, 25132, 50263,
      67017, 33509, 33505, 203960, 30597, 30593, 61186, 81581, 40790, 40786, 463669, 22468, 22464,
      44929, 89857, 179676, 38, 803139, 19958, 19954, 39909, 79817, 127706, 31929, 31925, 63849,
      388089, 26954, 26950, 53900, 107763, 38, 172479, 43122, 43118, 86236, 2107995, 1664713,
      33027, 33023, 33031, 99077, 99085, 99039, 42, 38, 356723, 39636, 39632, 39640, 79272, 158539,
      39637, 39633, 79266, 396367, 132093, 38, 132097, 132135, 79235, 396371, 19825, 19821, 39642,
      39650, 79291, 198140, 84, 42, 38, 88, 42, 38, 2107999, 1245065, 3353017, 43, 39, 79, 158,
      316, 632, 1264, 2527, 5055, 10109, 20218, 40437, 80873, 161747, 323493, 641437, 5550, 5546,
      11091, 14788, 7394, 7390, 38817, 12938, 12896, 38, 12942, 12926, 134592, 7717, 7713, 15427,
      20569, 10285, 10281, 62596, 9392, 9388, 18776, 25035, 12518, 12514, 142317, 11030, 11026,
      11034, 22059, 33093, 22051, 246503, 6101, 6097, 12194, 24388, 39020, 9757, 9753, 19506,
      119684, 7128, 7124, 14248, 18998, 9499, 9495, 53188, 13299, 13295, 26590, 646993, 631094,
      15896, 38, 15930, 31860, 42468, 21253, 21211, 38, 146736, 1955, 38, 1959, 3948, 6315, 1581,
      1577, 3153, 148699, 4603, 38, 4637, 9235, 38, 148703, 18543, 42, 38, 18547, 18627, 18539, 38,
      74326, 42, 38, 76, 84, 42, 38, 646999, 382530, 1029483, 42, 38, 76, 152, 304, 608, 1216,
      2432, 4864, 5398, 9736, 24862, 49725, 99450, 197693, 1206, 1202, 2404, 4808, 9617, 12822,
      6411, 6407, 41980, 2895, 1448, 1444, 5782, 7710, 3855, 3851, 14992, 11988, 2999, 2995, 5990,
      44883, 5372, 38, 5406, 5414, 10820, 5406, 75464, 1871, 1867, 3733, 3741, 3725, 11945, 2988,
      2984, 5969, 36636, 2185, 2181, 4361, 5815, 2908, 2904, 16278, 4072, 2036, 2032, 8135, 198907,
      152323, 3950, 3946, 3954, 7899, 19661, 84, 42, 38, 36523, 3046, 3042, 6085, 12131, 38, 12177,
      42, 38, 46, 39577, 3084, 3080, 3088, 9211, 38, 54658, 3418, 3414, 3422, 6836, 10259, 27304,
      42, 38, 50, 84, 42, 38, 198911, 116938, 315803, 42, 38, 76, 152, 304, 608, 1216, 2432, 4864,
      9728, 11056, 30512, 60652, 373, 369, 738, 1476, 2951, 3935, 1968, 1964, 12879, 891, 445, 441,
      1773, 2365, 1182, 1178, 4598, 3674, 920, 916, 1833, 13777, 830, 826, 834, 2486, 4933, 38,
      23143, 577, 573, 1145, 2253, 38, 3663, 918, 914, 1828, 11231, 673, 669, 1337, 1783, 891, 887,
      4987, 1249, 1245, 2490, 61033, 60449, 580, 576, 1151, 2272, 2310, 2264, 42, 38, 8389, 840,
      836, 1673, 1681, 3354, 1654, 38, 1658, 9237, 1363, 1359, 2717, 2725, 2709, 9241, 772, 386,
      382, 1535, 1543, 772, 768, 3079, 614, 610, 622, 1228, 614, 610, 61037, 35847, 96838, 42, 38,
      76, 152, 304, 608, 1216, 2432, 4864, 8978, 18541, 165, 161, 323, 430, 215, 211, 1128, 375,
      333, 38, 379, 363, 3890, 227, 223, 446, 595, 297, 293, 1804, 273, 269, 539, 718, 359, 355,
      4125, 362, 38, 396, 792, 800, 784, 7111, 181, 177, 354, 708, 1131, 285, 281, 561, 3431, 242,
      238, 476, 913, 38, 1520, 382, 378, 756, 18714, 13817, 435, 38, 439, 881, 915, 873, 38, 3226,
      388, 38, 392, 815, 1588, 42, 38, 46, 3622, 288, 284, 292, 822, 38, 4842, 500, 38, 534, 542,
      1609, 1617, 747, 42, 38, 785, 38, 18718, 11005, 29671, 48, 44, 87, 175, 350, 700, 1399, 2799,
      5555, 42, 38, 75, 151, 302, 310, 102, 98, 106, 1177, 43, 39, 78, 157, 313, 543, 84, 80, 160,
      214, 107, 103, 1228, 91, 38, 95, 221, 358, 84, 42, 38, 2123, 69, 65, 130, 138, 122, 450, 70,
      66, 132, 177, 88, 84, 528, 246, 42, 38, 733, 117, 59, 55, 188, 38, 273, 179, 90, 86, 5605,
      4450, 197, 38, 231, 239, 38, 471, 42, 38, 46, 766, 61, 57, 113, 151, 75, 71, 234, 77, 73, 81,
      1345, 42, 38, 46, 84, 38, 1503, 86, 82, 90, 229, 263, 221, 38, 491, 84, 42, 38, 88, 59, 176,
      130, 42, 38, 5609, 3636, 9199, 42, 38, 76, 152, 304, 386, 994, 1936, 52, 48, 96, 193, 306,
      79, 75, 149, 934, 60, 56, 111, 148, 74, 70, 411, 105, 101, 201, 1997, 1314, 109, 105, 210,
      378, 42, 38, 428, 99, 38, 133, 154, 112, 38, 2001, 803, 2386, 414, 368, 42, 38, 46, 84, 154,
      50, 46, 54, 38, 1048, 264, 218, 42, 38, 50, 84, 42, 38, 268, 75, 272, 67, 63, 84, 42, 38, 1
  };

  public void testTimSortLegalInput() {
    int size = Arrays.stream(RUNS).sum();
    FixedBitSet bits = new FixedBitSet(size);
    int end = -1;
    for (int i = 0; i < RUNS.length - 1; i++) {
      end += RUNS[i];
      bits.set(end);
    }
    class BitList extends AbstractList<Boolean> implements RandomAccess {
      @Override
      public Boolean get(int i) {
        return bits.get(i);
      }

      @Override
      public Boolean set(int i, Boolean value) {
        boolean old = bits.get(i);
        if (value) {
          bits.set(i);
        } else {
          bits.clear(i);
        }
        return old;
      }

      @Override
      public int size() {
        return size;
      }
    }
    try {
      CollectionUtil.timSort(new BitList(), Comparator.<Boolean>naturalOrder());
    } catch (ArrayIndexOutOfBoundsException e) {
      // Expected: the list is sorted; actual: TimSorter's 49-entry run stack overflows.
      throw new AssertionError("timSort of " + size + " elements threw " + e, e);
    }
    assertEquals(size - (RUNS.length - 1), bits.nextSetBit(0));
  }
}
Command
cp $REPRO/TestTimSorterRunStackOverflow.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.TestTimSorterRunStackOverflow -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
Expected
The list comes back sorted: 1,093,936,989 falses, then 1,060 trues.
Observed
AssertionError: timSort of 1093938049 elements threw java.lang.ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50, from TimSorter.pushRunLen(TimSorter.java:98), with assertions on and off.

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

More files, environment, full output458 words
  • The input. Run i is RUNS[i] - 1 false values followed by one true, so it is ascending and ends at a descent; the last run is a single false. n = 1,093,938,049, minRun(n) = 33, and every run except the last is at least 38, so sort() detects exactly these runs. It is the smallest legal overflowing input the study found; its search kept one merge pattern fixed.
  • Why a bit set. A RandomAccess list view of a FixedBitSet keeps 1.09 billion elements in about 137 MB. With the sorter's temp array (Object[n / 64], about 68 MB, CollectionUtil.java:86, :166) the test needs about 205 MB of heap: it ran out of memory with -Xmx160m, and fails the same way with -Xmx224m and with 512 MB, the Gradle test default. It takes about 10 s with a plain JVM and about a minute under Gradle's default test JVM flags.
  • What it asserts. That the sort does not throw, and that the result is sorted: the first set bit is at n - 1,060.
  • Index-sorting route (prose only). The same runs as NumericDocValuesField values in doc-ID order, with setIndexSort(new Sort(new SortField("v", SortField.Type.INT))) and setRAMBufferSizeMB(1024): commit() of 1,093,938,049 buffered documents throws the same exception and closes the writer; SortingCodecReader.wrap on a committed unsorted segment of that size throws too. That program needs a 16 GB heap and about 5 minutes, so it is supporting evidence, not a second repro.
  • Regression test. The same test; given its size it may belong under @Nightly, like TestTimSorterWorstCase. ./gradlew tidy (google-java-format) re-indents the RUNS array and changes nothing else, but the file gets one line shorter, so the test frames then read :132 and :135.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.6.0, 10.5.2 and 9.12.3
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp $REPRO/TestTimSorterRunStackOverflow.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.TestTimSorterRunStackOverflow -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)

Observed output

# Gradle, main 645d4fe14a20, seed DEADBEEF, assertions on, default 512 MB test heap
TestTimSorterRunStackOverflow > testTimSortLegalInput FAILED
    java.lang.AssertionError: timSort of 1093938049 elements threw java.lang.ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:202A3451691B6A42]:0)
        at org.apache.lucene.util.TestTimSorterRunStackOverflow.testTimSortLegalInput(TestTimSorterRunStackOverflow.java:136)

        Caused by:
        java.lang.ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50
            at org.apache.lucene.util.TimSorter.pushRunLen(TimSorter.java:98)
            at org.apache.lucene.util.TimSorter.sort(TimSorter.java:204)
            at org.apache.lucene.util.CollectionUtil.timSort(CollectionUtil.java:166)
            at org.apache.lucene.util.TestTimSorterRunStackOverflow.testTimSortLegalInput(TestTimSorterRunStackOverflow.java:133)

# index-sorting flush route, stock main, assertions off (plain main(), 16 GB heap)
route=flush buffered docs=1093938049
java.lang.AssertionError: IndexWriter.commit() with index sort <int: "v"> and 1093938049 buffered docs threw
  java.lang.ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50; writer open: false,
  tragic exception: java.lang.ArrayIndexOutOfBoundsException: Index 50 out of bounds for length 50
# frames: TimSorter.pushRunLen(:98) <- TimSorter.sort(:204) <- index.Sorter.sort(Sorter.java:154)
#   <- Sorter.sortAndLeaveUnpacked(:238) <- IndexingChain.maybeSortSegment(:297) <- IndexingChain.flush(:305)
#   <- DocumentsWriterPerThread.flush(:492) <- ... <- IndexWriter.commit(:4215)

Why it looks unintended

  • Sorter.sort promises to "Sort the slice", with no size limit or documented exception; setIndexSort accepts segments up to MAX_DOCS.
  • LUCENE-6293 fixed this exact failure as a bug in 2015 by enlarging the stack; the nightly TestTimSorterWorstCase expects sort() to finish.
  • OpenJDK fixed the same residual in JDK 11 with the fourth-run check.
The full argument207 words
  • Contract. Sorter.sort(from, to): "Sort the slice which starts at from (inclusive) and ends at to (exclusive)." (util/Sorter.java:75-79). CollectionUtil.timSort documents only IllegalArgumentException for lists without random access (CollectionUtil.java:155-162); ArrayUtil.timSort documents no exception (ArrayUtil.java:558-600); the TimSorter class javadoc names no size limit (TimSorter.java:21-40). IndexWriterConfig.setIndexSort: "Set the Sort order to use for all (flushed and merged) segments." (IndexWriterConfig.java:504).
  • Intent. STACKSIZE = 49; // depends on MINRUN says the stack is meant to be large enough. LUCENE-6293 is listed under Bug Fixes in CHANGES 5.1.0 ("Fixed TimSorter bug.") and added the worst-case test that is now the @Nightly TestTimSorterWorstCase, which drives a TimSorter with an adversarial run pattern and expects sort() to finish.
  • The 2015 discussion named both fixes. Adrien Grand on #7355: "There are two suggested ways to fix the issue, either change the max stack size or change the condition upon which consecutive runs are collapsed. I opted for the first one which is easier to implement." This bug report is about the second one.
  • Precedent. OpenJDK treated the same residual as a bug (JDK-8203864, fixed in JDK 11). JDK 25's java.util.TimSort.mergeCollapse has the fourth-run check and credits the Auger et al. analysis in its comment.
The devil's-advocate check210 words

The strongest case for "not a bug": the classes are @lucene.internal; the input is absurd, 1.09 billion elements in an order solved for with a linear program, and the failure is loud. In 2015 Adrien Grand wrote "This bug cannot corrupt data, it can just raise an ArrayIndexOutOfBoundsException if you happen to have a large array which has a very specific pattern", and Robert Muir asked for "no high-risk fixes for low-risk bugs". The cross-model adversary refuted on the @lucene.internal ground alone, while agreeing on the reproduction, the root cause and the fix.

Why it fails: @lucene.internal is a compatibility notice ("might change in incompatible ways in the next release"), not permission to crash on valid input. The objection does apply to the minimal test, which goes through internal classes; but the same overflow is reached through supported public API, IndexWriterConfig.setIndexSort plus IndexWriter.commit(), where it closes the writer, although only with a deliberately extreme setup (one in-memory segment of 1.09 billion one-field documents). The 2015 maintainers held the same "very specific pattern, no corruption" view and still fixed it, believing 49 was enough; it is not. The condition fix that looked riskier in 2015 is now the JDK's own code. What the argument does establish: priority is low.

Suggested fix

Recommended A. Also check the fourth run (the JDK-8203864 condition)

Add the fourth-run check to ensureInvariants, the condition OpenJDK adopted in JDK 11: also merge when stackSize > 3 && runLen(3) <= runLen2 + runLen1. One line; the stack can stay at 49 entries. Validated: the test passes with assertions on and off, and so do the closest sorter and index-sorting suites and all 8,898 core tests.

       if (stackSize > 2) {
         final int runLen2 = runLen(2);
 
-        if (runLen2 <= runLen1 + runLen0) {
+        // also check the fourth run from the top (as in OpenJDK's JDK-8203864 fix and CPython's
+        // listsort), otherwise the invariant can break deeper in the stack and overflow runEnds
+        if (runLen2 <= runLen1 + runLen0 || (stackSize > 3 && runLen(3) <= runLen2 + runLen1)) {
           // merge the smaller of 0 and 2 with 1
           if (runLen2 < runLen0) {
             mergeAt(1);
All options and trade-offs3 options · 556 words

Three options. Option A was applied in a scratch worktree of main 645d4fe14a20 and run with Gradle against the minimal test, the closest suites, all core tests and core's static checks; earlier runs also tested it, with TimSorter shadowed, against the index-sorting re-sort route.

A. Also check the fourth run (the JDK-8203864 condition) (validated by the checks) (recommended)

One line in ensureInvariants (TimSorter.java:135). It is the JDK's mergeCollapse condition n > 1 && runLen[n-2] <= runLen[n] + runLen[n-1] in TimSorter's top-of-stack indexing, with the same choice of which run to merge. The JDK's comment says the invariants then hold upon entry to its method; that is about the JDK's loop, not a proof for Lucene's, and no general proof for TimSorter was written. What was checked: with the fix the stack peaks at 19 runs on this input and on each of the study's 17 legal overflowing sequences (a run-length mirror), and the diagram's probe on the real code sees at most 18 runs before a push; both are far below 49. The diff's comment names CPython's listsort for its 2015 change (bpo-23515); current CPython was not checked.

       if (stackSize > 2) {
         final int runLen2 = runLen(2);
 
-        if (runLen2 <= runLen1 + runLen0) {
+        // also check the fourth run from the top (as in OpenJDK's JDK-8203864 fix and CPython's
+        // listsort), otherwise the invariant can break deeper in the stack and overflow runEnds
+        if (runLen2 <= runLen1 + runLen0 || (stackSize > 3 && runLen(3) <= runLen2 + runLen1)) {
           // merge the smaller of 0 and 2 with 1
           if (runLen2 < runLen0) {
             mergeAt(1);

Trade-offs

Changes the merge order only when the fourth-run rule is broken (the small walk above is such a state); how often that happens on real data, and any performance effect, were not measured. One extra comparison per loop iteration. In 2015 this was the option judged harder to implement; it is now the JDK's own code.

What the checks validated

  • The minimal test fails on stock main under Gradle and passes with the fix, with assertions on (73.6 s) and off (69.6 s), in a scratch worktree of main 645d4fe14a20, seed DEADBEEF.
  • Closest suites: TestTimSorter, TestCollectionUtil, TestArrayUtil, TestIndexSorting and TestSortingCodecReader, 97 tests, 0 failures; the @Nightly TestTimSorterWorstCase with -Ptests.nightly=true passes.
  • All of :lucene:core:test: 8,898 tests, 322 skipped, 0 failures, the minimal test included.
  • :lucene:core:check -x test passes once ./gradlew tidy has re-indented the test's RUNS array; the fix itself needed no reformatting.
  • Earlier, with TimSorter shadowed: the index-sorting re-sort route on the 1,093,938,049-document segment passes (it fails on stock main), and 32 upstream sorter tests pass.

B. Raise STACKSIZE again

Size the stack from Auger et al.'s height bound for the three-run check, which the study reads as at most 86 entries for n < 2^31.

Trade-offs

Keeps every merge decision unchanged and costs 37 more ints per sort, but keeps a broken invariant and relies on a published bound once more, which is what failed after 2015. Not validated.

C. Use the JDK sort instead

Open #10671 (LUCENE-9632) discusses replacing Lucene's TimSort with the JDK's.

Trade-offs

Not a fix for this bug: maintainers there note that TimSorter must stay for the Sorter API (parallel arrays), and index sorting's DocValueSorter would keep using it.

Downstream note

A service that sorts, or index-sorts, more than a billion items in an order an outside party controls could see that sort or the commit running it fail loudly; a failed commit closes the writer and loses its uncommitted documents, but nothing is silently wrong and no memory is corrupted.

How it was found

Non-formal. An AI tool replayed the published 2018 Java TimSort crash sequence through TimSorter's internals, but that sequence is not a legal input for Lucene (its runs are too short). An independent verifier kept its merge decisions, re-solved the run lengths with a linear program so that every run is legal, and crashed the stock jar.

The method in full176 words
  • An AI tool replayed the crash sequence that Auger, Jugé, Nicaud and Pivoteau published for Java's TimSort (ESA 2018) through TimSorter's package-private internals, and the stack overflowed. That sequence has runs of 17 elements, below Lucene's minRun(n) = 33, and the real sort() of that exact array succeeds (maximum stack depth 44). So the finder concluded, wrongly, that no reachable overflow was known.
  • An independent verifier recorded the 4,138 comparisons ensureInvariants makes on that trace as linear constraints, minimised n with every run at least m (HiGHS), and rounded the solution run by run while keeping every merge decision. That gave 17 legal sequences from 1,093,938,049 to 1,719,699,526 elements; the three run on the stock jar crashed it, through CollectionUtil.timSort, a TimSorter subclass and, at 1,285,317,058, ArrayUtil.timSort(Boolean[]). The Python mirror and the linear program only found inputs; every behavioural claim comes from the real jar.
  • It was later reduced to one test with the smallest input found (Java-public methods of @lucene.internal classes), and a tie-break executed the index-sorting routes, which are supported public API.

Prior reports, history and checks

Prior reports

No public report of the residual: 159 scripted queries plus manual GitHub, JIRA and mailing-list searches (2026-11-01). The closed parent #7355 (LUCENE-6293, 2015) is the same code and symptom, fixed incompletely; OpenJDK's JDK-8203864 is the same residual in another project.

Searches4 searches

Closest items, none a report of this bug: #7355 (LUCENE-6293 "TimSort bug", closed 2015-02-26), the parent: same code, root cause and symptom, fixed by 40 -> 49, all 14 comments from 2015; JDK-8203864 (OpenJDK, fixed in JDK 11), the same residual in another project, not linked from any Lucene item; #10671 (LUCENE-9632, open), replacing Lucene's TimSort with the JDK's, where the run stack never comes up; PR #12800 (open, stale), the only open PR touching TimSorter.java, which adds a minRunLength hook and does not discuss the stack. Not a report but worth knowing: the Lucene.NET port still has STACKSIZE = 40. Gaps: searches can miss items with other wording; only the first 400 characters of each list mail were scored mechanically.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-22, 2026-11-01scripted searches (60 issue/PR and 3 commit queries, e.g. TimSorter, ensureInvariants, pushRunLen, mergeCollapse, STACKSIZE, JDK-8203864, "TimSort ArrayIndexOutOfBoundsException") and 13 manual searches; 30 items read in full; all 409 open PRs listed with their filesno report; #7355 is the closed parent; PR #12800 is the only open PR on the file
JIRA archive (LUCENE, SOLR)2026-10-22, 2026-11-0110 scripted queries and 7 manual ones, e.g. TimSort, TimSorter, mergeCollapse, pushRunLen, runEnds, Augeronly LUCENE-6293 and unrelated items
Mailing lists (dev@, java-user@, issues@, commits@)2026-11-0176 scripted queries and 15 manual ones, e.g. LUCENE-6293, Auger, Jugé, Nicaud, 8203864only the 2015 LUCENE-6293 notifications
Downstream and web2026-11-01TimSorter in Elasticsearch, OpenSearch, Solr and Lucene.NET; 10 web searchesno report; JDK-8203864 and the ESA 2018 paper, neither mentioning Lucene

History

The 49-entry stack comes from LUCENE-6293 (3ec9440e945, 2015-02-26), first released in 5.1.0; every sampled release from 5.1.0 to 10.6.0 and main fails at index 50. The three-run check itself dates from LUCENE-4839 (2013, 4.3.0), where the same input overflows the old 40-entry stack at index 41.

Versions tested5 rows
  • The residual (this bug): 3ec9440e945 (trunk) / da5f4038b88 (branch_5x), "LUCENE-6293: Fixed TimSorter bug." by Adrien Grand, 2015-02-26 (#7355): its only production change was STACKSIZE from 40 to 49 (it also added a worst-case test and a CHANGES entry); first released in 5.1.0.
  • The root defect: LUCENE-4839 "Sorter API: Use TimSort to sort doc IDs and postings lists" (migrated as #5904; 8613df447f0 on trunk, backport 51b3f46aadc, 2013-03-16) added the three-run check and a 40-entry stack; first released in 4.3.0. In 4.3.0 to 5.0.0 the same input overflows at index 41, which is the original LUCENE-6293 bug (that one also hits much smaller inputs). The finding record's introducing commit, 3ec9440e94, is the incomplete fix (the 49-entry stack), not the origin.
  • ensureInvariants is textually identical (whitespace aside) in 4.3.0's SorterTemplate and main's TimSorter; TimSorter.java is the same blob in 9.12.3, 10.0.0, 10.6.0, main and upstream main f815e679bf6c (2026-11-01), whose last commit to the file is 0fcf9c825f1f (#426, 2021, javadocs only).
Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedfails at index 50 with assertions on and off (Gradle and standalone); passes with fix A
10.6.0 affectedfails at index 50 with assertions on and off; the minimal test fails too
5.1.0, 5.5.5, 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, 10.5.2 affectedfail at index 50 (release jars, same input)
4.3.0, 4.3.1, 4.4.0, 4.10.4, 5.0.0 40-entry stackfail at index 41: the original LUCENE-6293 form of the same defect
4.2.1 and earlier no TimSortCollectionUtil.timSort does not exist (compile error)

Independent checks

The study verifier, the repro minimization, a re-verification and a tie-break all call it real; a cross-model adversary reproduced it and agreed on the root cause and the fix, and refuted only because the entry point is @lucene.internal, which the tie-break answered by running the index-sorting routes.

Checks and verdicts319 words

The cross-model reviewer was OpenAI GPT-5.6; it ran its own test (a JDK BitSet with a reverse comparator and a small control) with assertions on and off.

Each independent check and its verdict
CheckVerdictWhat it found
study verifier realBuilt 17 legal overflowing inputs with a linear program; the three run on the real jar (1,093,938,049 to 1,719,699,526 elements) crash it through TimSorter.sort, CollectionUtil.timSort and ArrayUtil.timSort(Boolean[]).
minimisation verifiedOne test with the smallest input found (1,093,938,049 elements), through Java-public methods of @lucene.internal classes; fails with assertions on and off, also with a 512 MB heap; passes with the fix.
re-verification realRe-ran the test on main (on and off), 10.5.2 (on and off) and 9.12.3; the fix passes it and 32 upstream sorter tests.
cross-model adversary not real (contract only)Reproduced the overflow with its own test, agreed on the root cause and the fix, and refuted only because CollectionUtil.timSort is @lucene.internal.
Second adjudication realRan the flush route (setIndexSort + commit()) and the re-sort route (SortingCodecReader.wrap) on 1,093,938,049 documents: both fail; the re-sort route and the nightly worst-case test pass with the fix.
history origin correctedThe three-run check dates from 2013 (4.3.0); the 49-entry residual fails on every sampled release from 5.1.0 to 10.6.0.
fact checks confirmed; wording correctedIndependent fact checks of this doc, one by OpenAI GPT-5.6, re-ran the repro and fix A and recomputed the input facts with their own mirrors. They found no error in the bug, the repro or the fix; the wording corrections they asked for (heap, internal-API scope, effect on the writer, evidence) are applied.
this doc's runs reproduces; fix validatedGradle: the test fails on stock main and passes with fix A, assertions on and off; 97 closest-suite tests, the nightly worst-case test and all 8,898 core tests pass with the fix.

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 notes625 words

Finding record

LFV-043, snapshot of 2026-11-06

Registry title
TimSorter overflows its 49-entry run stack on legal inputs of about 1.1 billion elements
Area
util / sorters
Priority
Low, rank 1194: TimSorter overflows its run stack only on at least 1.09 billion elements in an LP-solved run pattern, loud and practically unreachable.
Impact class
crash
Impact note
Only sorts of n >= 1,093,938,049 elements with a specific 1,061-run pattern through ArrayUtil.timSort, CollectionUtil.timSort, another TimSorter subclass or index sorting of a segment that large (IndexWriter.commit() with an index sort, SortingCodecReader.wrap), both index routes executed; no smaller legal input is known; low likelihood.
Summary
You sort about 1.09 billion or more elements with ArrayUtil.timSort or CollectionUtil.timSort, arranged in a specific pattern of 1,061 runs. You expect a sorted result. Instead TimSorter's fixed 49-entry run stack overflows and the sort throws ArrayIndexOutOfBoundsException.
Introduced
3ec9440e94 (LUCENE-6293) raised STACKSIZE from 40 to 49 without the fourth-run check (2015-02-26)
First affected version
5.1.0 [exec] (the 49-entry stack of LUCENE-6293, 3ec9440e94, 2015); the root defect, the three-run check of LUCENE-4839 (2013), since 4.3.0 [exec: 4.3.0 to 5.0.0 overflow their 40-entry stack at index 41]
Affected versions
5.1.0 through 10.6.0 and main [exec: overflow at index 50 on 5.1.0, 5.5.5, 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, 10.5.2, 10.6.0 and main 645d4fe14a20; the minimal test also on 10.6.0, 10.5.2 and 9.12.3; main also at n = 1,285,317,058 and 1,719,699,526]; the same root defect with the older 40-entry stack in 4.3.0 through 5.0.0 [exec: overflow at index 41 on 4.3.0, 4.3.1, 4.4.0, 4.10.4 and 5.0.0]; 4.2.1 and earlier have no TimSort [exec]; TimSorter.java is the same blob in 9.12.3, 10.0.0, 10.6.0, main and upstream main f815e679bf6c [read]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that disputed it (the study's other checks rated it real); not yet triaged by a person.
Found by
non-formal: code reading
Upstream and prior reports
none found as a report of this residual (study searches 2026-10-22); parent LUCENE-6293 (#7355) is closed; cf. JDK-8203864
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test with assertions on and off standalone and through Gradle in a scratch worktree (2026-11-01), and the recommended fix in that worktree with the test and the closest suites; the index-sorting flush and re-sort routes on a 1,093,938,049-document segment (both fail; the re-sort route passes with the fix). Releases: the minimal test on 10.6.0, 10.5.2 and 9.12.3; a plain-Java probe with the same input on sampled releases: 4.2.1 (no TimSort), 4.3.0 to 5.0.0 (overflow at index 41) and 5.1.0 to 10.6.0 (overflow at index 50). Read: upstream main f815e679bf6c (2026-11-01) has the same TimSorter.java. All runs used OpenJDK 25.0.3 on Linux aarch64.

Lucene and upstream sources

The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/util/TimSorter.java:43-46, :62, :67-78, :97-100, :103-125, :127-153, :196-208 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/index/Sorter.java:85-129, :132-171, :220-238; IndexingChain.java:291-305; DocumentsWriterPerThread.java:485-492, :585-605; SortingCodecReader.java:441-447 (at 645d4fe14a20)
Who is affected
  • lucene/core/src/java/org/apache/lucene/util/CollectionUtil.java:29, :155-167; ArrayUtil.java:26, :558-602 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:208; IndexWriterConfig.java:93, :504-505; TieredMergePolicy.java:77; Sorter.java:146; FreqProxTermsWriter.java:313-316, :403 (at 645d4fe14a20)
Minimal repro
  • lucene/core/src/java/org/apache/lucene/util/CollectionUtil.java:78-90, :163-167 (ListTimSorter temp array of size n / 64) (at 645d4fe14a20)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/util/Sorter.java:75-79; CollectionUtil.java:155-162; ArrayUtil.java:558-600; TimSorter.java:21-45 (at 645d4fe14a20)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:504-505 (at 645d4fe14a20)
  • lucene/core/src/test/org/apache/lucene/util/TestTimSorterWorstCase.java:26-73 (@Nightly, testWorstCaseStackSize, sort at :72) (at 645d4fe14a20)
  • JDK 25 src.zip java.base/java/util/TimSort.java:420-450 (mergeCollapse and its comment)
Suggested fix
  • JDK 25 src.zip java.base/java/util/TimSort.java:420-450 (mergeCollapse), :182-184 (stack length 49)

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

Repro files

About the diagrams

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.