- Build a
BYTE2FST from 2,048 sparse keys (labels 0, 32, ..., 65,504) withByteSequenceOutputs: key 0 maps to a 1,048,570-byte value, the rest to 1 byte. The root gets fixed-length arcs (on by default); arc 0 is exactly 2^20 bytes. - You expect a correct FST or an error. One byte less gives a correct 2,147,481,607-byte FST with all 2,048 lookups right.
- Instead the padded size 2,048 × 2^20 = 2^31 wraps to -2,147,483,648 as an
int. The compiler skips the padding but still writes the binary-search header over the first 6 bytes of arc 0. - With assertions off,
compile()returns normally. EveryUtil.getthrows (on heap,Index -32704 out of bounds for length 33), and with patterned value bytes, aftersave()and an off-heap reload, enumeration returns key 17,188, never added. - At build time only
assert destPos >= srcPoscatches it, and only with assertions on. WithallowFixedLengthArcs(false)the same keys read back correctly.
Applications that build their own FST<BytesRef> or FST<CharsRef> with a wide, sparse node and one large output; in-tree, StemmerOverrideFilter.Builder with 2,048 one-code-point words and a 1,048,570-char stem. Not the default index format: no core codec on main uses util.fst, and no way in through the pre-10.3 default terms index is known. No field report; very low reach. Workaround: allowFixedLengthArcs(false).
Mechanism, terms and numbers625 words
FSTCompiler computes the padded size of a node with fixed-length arcs as a Java int. When one node has many arcs and one large output, the size reaches 2^31 and wraps; with assertions off the compiler skips the padding but still writes the header that claims it. compile() returns normally, lookups through that node throw, and enumeration can return a key that was never added. Silent in most releases since 4.2.0 (2013). This is about FSTs that applications build; the default index format on main does not use util.fst.
- An FST (finite state transducer) is Lucene's compact map from keys to values. A key is a sequence of integer labels (bytes
BYTE1, 16-bit charsBYTE2, or intsBYTE4); the values are outputs. The FST is a graph of nodes, and each arc leaving a node carries one label.FSTCompilerbuilds it from sorted keys;Util.get,FSTEnumand the arc readers inFSTread it. FSTCompilerwrites each node in two passes. Pass 1 writes the arcs back to back, each only as long as it needs (flags, label, output, target): a list node that a reader scans from the start. Pass 2 runs for nodes that qualify for fixed-length arcs (depth at most 3 with at least 5 arcs, or at least 10 arcs at any depth; on by default): it pads every arc to the size of the largest one,maxBytesPerArc, so a reader can jump to arc i and binary search the labels. A small header in front says "binary-search node,numArcsarcs,maxBytesPerArcbytes each".
The padded size is computed as an int: numArcs * maxBytesPerArc (main FSTCompiler.java:665, int destPos = headerLen + nodeIn.numArcs * maxBytesPerArc;). If one arc is big and the node has many arcs, the product reaches 2^31 and wraps. In the binary-search writer the end position destPos then comes out too small, the guard if (destPos > srcPos) (:667) is false, so the arcs are not padded, and the header is written anyway (:695) over the first 6 bytes of arc 0.
The line before the guard, assert destPos >= srcPos (:666), catches it only with assertions on (-ea), while the FST is built. Java runs with assertions off unless told otherwise. Under -da, compile() returns normally and the FST holds the unpadded list plus a header that claims padding:
- every
Util.getunder that node throws (IndexOutOfBoundsExceptionorArrayIndexOutOfBoundsExceptionon heap,IllegalArgumentException: read negative positionthroughMMapDirectory); - enumeration (
IntsRefFSTEnum.next()) can return a key that was never added, because it decodes the big output's own bytes as an arc (it depends on those bytes and the shape); FST.save()writes the bad bytes as they are, so the corruption survives a save and reload.
BYTE2, ByteSequenceOutputs, 2,048 sparse root arcs (labels 0, 32, ..., 65,504), arc 0 with a 1,048,570-byte output: arc 0 is 1 + 2 + 3 + 1,048,570 = 2^20 bytes, so 2048 * 1048576 = 2^31, which is -2,147,483,648 as an int. destPos = 6 + (-2,147,483,648) = -2,147,483,642, below srcPos = 1,058,811, so no expansion happens. Util.get(key 32) then probes arc 1,023 about 1 GB before the start of a 1 MB FST, which on heap is block -32,704: Index -32704 out of bounds for length 33.
With allowFixedLengthArcs(false) the same keys build and read back correctly. With a 1,048,569-byte output (product 2,147,481,600) the compiler builds a correct 2,147,481,607-byte FST and all 2,048 lookups are right; one byte more gives a 1,058,812-byte FST on which every lookup throws. The failure is a cliff, not a limit.
Exposure and likelihood280 words
- Who could hit it. Applications that build their own
FST<BytesRef>orFST<CharsRef>maps with a wide node and one large output; #9132 (LUCENE-8084) describes that data shape ("one of the outputs is very long, all the others are small", "root level fanout is huge") but reports only wasted space. One in-tree public API is silent at build time:StemmerOverrideFilter.Builderwith 2,048 one-code-point words and one 1,048,570-char stem builds, then every lookup throws. There is no field report. - Not the default index format. On main and
branch_10_6no core codec importsutil.fst(the default codec is Lucene104). Releases before 10.3 built the default terms index withFSTCompiler, asBYTE1FSTs with small outputs; no way to reach this bug through them is known. - Silent where it matters. Production runs with assertions off, so
compile()succeeds and the damage shows only on read. In a deep node other keys keep working and the corruption stays hidden until a key under that node is read (301 of 301 other keys correct, 4,096 of 4,096 under the bad node throw, in one repro). - Shape. It needs the binary-search layout, which the compiler picks for sparse labels: for
BYTE2, at most 4,096 sparse arcs and one arc of at least 512 KiB; forBYTE1, at most about 33 arcs and one arc of 62-64 MiB or more; forBYTE4, for example 1,048,576 arcs at stride 1024 and one arc of 2 KiB. Contiguous label sets get other layouts and fail loudly (out of memory orArrayIndexOutOfBoundsException, tried at-Xmx2gand-Xmx8g, not with larger heaps). - Reach is very low (registry: data corruption, opt-in shape, extreme scale). Workaround:
allowFixedLengthArcs(false).
The test shows that a default BYTE2 FSTCompiler with 2048 sparse root arcs, one of them 2^20 bytes (a 1,048,570-byte output), so the fixed-length root node would take 2048 x 2^20 = 2^31 bytes and the int size wraps.
/*
* 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.fst;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.IntsRef;
public class TestFSTNodeExpandedSizeOverflow extends LuceneTestCase {
public void testRootNodeOf2048ArcsWithOneMiBArc() throws Exception {
FSTCompiler<BytesRef> compiler =
new FSTCompiler.Builder<>(FST.INPUT_TYPE.BYTE2, ByteSequenceOutputs.getSingleton()).build();
BytesRef big = new BytesRef(new byte[(1 << 20) - 6]);
for (int i = 0; i < 2048; i++) {
BytesRef output = i == 0 ? big : new BytesRef(new byte[] {1});
compiler.add(new IntsRef(new int[] {i * 32}, 0, 1), output);
}
FST<BytesRef> fst = FST.fromFSTReader(compiler.compile(), compiler.getFSTReader());
// 2048 arcs x 2^20 bytes = 2^31: expected key 0's output, actual a corrupt FST that throws
assertEquals(big, Util.get(fst, new IntsRef(new int[] {0}, 0, 1)));
}
}
# in a copy of this finding's repro folder (repro/LFV-007/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.util.fst.TestFSTNodeExpandedSizeOverflow
- Expected vs actual
- Expected:
Util.getreturns key 0's output (orcompile()fails). Actual: with-da,compile()returns a corrupt FST and the lookup throwsIndexOutOfBoundsException: Index -32704 out of bounds for length 33; with-ea, Lucene'sassert destPos >= srcPos(FSTCompiler.java:666) fires insidecompile().
More files, environment, full output32 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError
org.apache.lucene.util.fst.FSTCompiler.writeNodeForBinarySearch(FSTCompiler.java:666)
org.apache.lucene.util.fst.FSTCompiler.addNode(FSTCompiler.java:553)
org.apache.lucene.util.fst.FSTSuffixNodeCache.add(FSTSuffixNodeCache.java:154)
org.apache.lucene.util.fst.FSTCompiler.compileNode(FSTCompiler.java:388)
org.apache.lucene.util.fst.FSTCompiler.compile(FSTCompiler.java:1014)
org.apache.lucene.util.fst.TestFSTNodeExpandedSizeOverflow.testRootNodeOf2048ArcsWithOneMiBArc(TestFSTNodeExpandedSizeOverflow.java:33)- The writer's own assertion states the rule that breaks:
assert destPos >= srcPos. - No size limit is documented; fixed-length arcs are documented as an optimization, and with it off ("smaller but slower to traverse") the same input is correct.
- It is a cliff, not a limit: one byte less builds a correct 2 GB FST.
The full argument141 words
- The writer's own assertion states the invariant that breaks:
assert destPos >= srcPos. - No size limit is documented, and the
FSTCompilerclass javadoc advertises "FSTs larger than 2.1GB are now possible (as of Lucene 4.2)". - Fixed-length arcs are documented as an optimization: "Pass false to disable the fixed length arc optimization ... smaller but slower to traverse". An optimization that changes the result is a bug by the API's own description, and with the optimization off the same input is correct.
- It is a cliff: one byte less builds a correct 2 GB FST.
- Precedent: FST size overflows were fixed as bugs before, for example #5804 / LUCENE-4739 (
type:bug, "FSTs more than ~1.1GB" could not be saved and loaded) and #5146 / LUCENE-4074 (type:bug, an int overflow in the FST sorter).
The devil's-advocate check201 words
The intended-behaviour checker argued every point for "not a bug": the classes are @lucene.experimental (but that means the API may change, not that output may be corrupt); #12624's review assumed single nodes are small (those comments are about shrinking a scratch buffer, and 105 review comments, 20 conversation comments and 61 reviews never mention overflow, destPos or a (long) cast); an assert guards it (FSTCompiler checks caller contracts with asserts, but this one is an internal invariant that legal input breaks, and about 25% of main's test seeds run with assertions off); it is a pathological shape and a known space limitation (#5747, #9132 are about wasted space only); the format cannot hold a 2 GiB fixed-length node (true, but the input does not need one: the list encoding is 1 MB); and nobody has hit it in about 13 years (that speaks to reach, not intent).
Verdict: not intended. Not documented, not an unsupported configuration, and no documented limit is exceeded; a real correctness bug with very low practical reach. Not a security issue: SECURITY.md says reports that feed arbitrary data into low-level APIs are generally not vulnerabilities but "are still valid bug reports", so a public issue is right.
Recommended Keep the variable-length list for nodes whose fixed-length area would exceed 1 GiB
Keep the variable-length arc list the compiler already wrote when a node's fixed-length arc area, computed as a long, would exceed 1 GiB: one guard before all three fixed-length writers, no reader or format change. The test then passes with assertions on and off. Cost: 1-2 GiB nodes stay lists, so lookups there are a linear scan.
private static final int FIXED_LENGTH_ARC_MAX_NODE_BYTES = 1 << 30;
...
// if the expanded node would be too large, keep the variable length arcs written above
if (doFixedLengthArcs
&& (long) nodeIn.numArcs * maxBytesPerArc <= FIXED_LENGTH_ARC_MAX_NODE_BYTES) {All options and trade-offs5 options · 460 words
The checks validated keeping oversized nodes as lists; the alternatives come from the brief and the fix README.
Keep the variable-length list for nodes whose fixed-length area would exceed 1 GiB (validated by the checks) (recommended)
Local branch fix-fst-expanded-size-overflow, commit 13e3e5e3f5 on main b4383f066b (3 files, +37/-2). A node whose fixed-length arc area would be larger than 1 GiB keeps the variable-length list that pass 1 already wrote, which is a valid encoding. It uses the hook the commented-out LUCENE-4682 heuristic already reserves, one guard before all three fixed-length writers (continuous, direct addressing, binary search), with no reader or format change. The cap also keeps the other size products and the reader's bytesPerArc * mid in int range.
private static final int FIXED_LENGTH_ARC_MAX_NODE_BYTES = 1 << 30;
...
// if the expanded node would be too large, keep the variable length arcs written above
if (doFixedLengthArcs
&& (long) nodeIn.numArcs * maxBytesPerArc <= FIXED_LENGTH_ARC_MAX_NODE_BYTES) {Trade-offs
- A node whose arc area is between 1 GiB and 2 GiB used to be expanded correctly; now it stays a list, so lookups there are a linear scan (slower, but it saves at least 1 GiB of build heap).
- Nodes over the cap keep pass-1 arcs without
BIT_TARGET_NEXT: valid, slightly larger than a pure list. Output is byte-identical whenever the arc area is at most 2^30. - The CHANGES entry sits under 10.7.0 with
GITHUB#TBDplaceholders, which is probably stale (10.6 is expected to be the last 10.x); let the committer choose. The commit was written by an AI tool.
What the checks validated
- The fix hunk applies cleanly to main and to
branch_10_6; the test passes 5/5 with-eaand 5/5 with-daon both. org.apache.lucene.util.fst.*: 38 tests, 0 failures;:lucene:core:check -x testand error-prone pass on main.- All probes and repros are correct on the fixed jars, including shapes that used to fail loudly. Boundary: at exactly 2^30 the node still expands (1,073,741,831 bytes, 2048/2048 correct); one byte more per arc keeps a 534,525-byte list (2048/2048 correct).
Compute the product as a long again, as 9.4.2 did.
Trade-offs
Not enough on its own: the scratch buffer is a byte[], so the bug only turns into another exception; 9.4.2-9.8.0 also show that a 2 GB node hit a second truncation.
Fail loudly in the compiler.
Trade-offs
Loud, but it rejects input that has a valid encoding (the list).
Use the largest size the scratch array can hold as the cap.
Trade-offs
More code and little headroom.
Decide fixed-length arcs by wasted space, as those issues propose.
Trade-offs
A format-affecting design change, out of scope for this bug.
SMT solving (Z3): while modelling another finding, an AI tool transcribed FSTCompiler's encoding-decision arithmetic into Z3; a second model's query for the silent path (padding skipped, header still written) was satisfiable with default settings. A concrete witness then compiled under -da on the real jar, and its lookups threw. Not a blind discovery.
The method in full284 words
Method: SMT (Z3) on the transcribed size arithmetic of FSTCompiler, then a concrete witness on the real jar (the study's "formal" tier). Run 3, experiment "fst-verification" (2026-10-22, main 7cecf79523).
- The task targeted
util/fst, namedFSTCompiler, the binary-search, direct and continuous arc encodings, fixed-length arcs and their thresholds, and asked for Alloy/Dafny models and bounded-exhaustive checks; the shared preamble named SMT (Z3/cvc5, bit-vector and floating-point theories) as a general technique. It said nothing about size arithmetic. - while modelling a different finding, the AI tool transcribed the encoding-decision arithmetic into a Z3 bit-vector and floating-point model that contains
int sizeForBinarySearch. - 06:10: a Java probe for "expanded size exceeds 2^31" gave only loud failures. 06:11: the AI tool wrote
smt/expand_overflow.py, whose query Q6 encodes the silent path: binary search chosen,destPos <= srcPos, expansion skipped, header still written. - 06:36: after bounding
srcPoscorrectly, Z3 reported Q6d satisfiable (default factor, credit 0) for 5, 10 and 64 arcs; the final log (06:47) has it unsatisfiable for 200 and 255 arcs within a 512 MB first-pass bound, where direct addressing is chosen instead. - 06:37: a concrete re-execution of the same arithmetic picked a witness (64 arcs, one 33,554,427-byte output). On the real jar under
-dait compiled and the lookups threw: the first observation of the silent corruption. 06:38: a JUnit repro. - The experiment's bounded-exhaustive harness (7.0M FSTs, no discrepancy) cannot reach nodes of 2^31 bytes. A separate AI verifier wrote its own tests and ran 9.3.0, 9.4.2 and 9.9.2.
So the AI tool formed the mechanism while reading and modelling the code, Z3 showed the silent branch is reachable with default settings, and a concrete scan picked the sizes. Not "blind".
No prior report in GitHub issues and PRs (about 110 searches; all 410 open PRs), the dev@, java-user@ and issues@ lists, or Elasticsearch, OpenSearch and Solr, searched 2026-10-23. The closest, #5747 and #9132, are about wasted space, not corruption.
Searches5 searches
Nothing reports this bug. The closest items are about wasted space in fixed-length arcs, not corruption.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs, incl. migrated JIRA (apache/lucene) | 2026-10-23 | about 75 search/issues phrasings (open and closed, titles, bodies, comments), for example FSTCompiler overflow, maxBytesPerArc, writeNodeForBinarySearch, sizeForBinarySearch, destPos, allowFixedLengthArcs, "array arcs", "never added", findTargetArc, NarrowCalculation; other AI tools ran 35 more, about 110 in all; all 95 items created since 2026-10-10 listed | no report; related, not duplicates: #5747 (LUCENE-4682, wasted bytes from array arcs, open) and #9132 (LUCENE-8084, the same shape, space only); the only "FSTCompiler overflow" hit, #11643 (LUCENE-10607), is unrelated |
| #12624 review | 2026-10-23 | 105 review comments, 20 conversation comments, 61 reviews | no mention of overflow, destPos or a (long) cast; single-node size discussed as a buffer question |
| Open PRs | 2026-10-23 | all 410 open PRs | none touches FSTCompiler.java |
| Mailing lists (lists.apache.org) | 2026-10-23 | dev@, java-user@ and issues@ archives | no report |
| Downstream repos | 2026-10-23 | Elasticsearch, OpenSearch and Solr | no report |
Silent since 4.2.0 (LUCENE-4678, 2013); the current form comes from #12624 (9.10.0). 9.4.2-9.8.0 are silent in another form; 9.9.x (by default) and 3.6.0-4.1.0 fail loudly. All 133 releases from 3.6.0 to 10.5.2 were run.
Versions tested7 rows
The current form comes from #12624 (@dungba88, merged by @mikemccand 2023-12-07, b6999818474; 9.10.0 ships the backport 193d8f14107), which replaced long destPos = startAddress + headerLen + nodeIn.numArcs * (long) maxBytesPerArc; with int destPos = headerLen + nodeIn.numArcs * maxBytesPerArc;. But it is not a 9.10.0 regression: the silent form starts in 4.2.0 with LUCENE-4678 (2220c6e8ba9, @mikemccand, 2013-01-12, paged bytes), which added the if (destPos > srcPos) skip while the product stayed int. #11923 (9.4.2) cast the product to long, but a second int truncation in BytesStore.reverse kept 9.4.2-9.8.0 wrong; #12738 (9.9.0) made the build fail loudly, but only while the suffix cache is on. The int destPos line is identical at every release tag from 9.10.0 to 10.5.2.
| Version or commit | Verdict | Result |
|---|---|---|
main b83101cdd2, branch_10_6 d04ee73177 | affected | -da: silent build, every read through the node throws; -ea: AssertionError in writeNodeForBinarySearch |
| all 19 releases 9.10.0-10.5.2 | affected | silent, the same as main (all jar sha1 = Maven Central) |
| 9.9.0-9.9.2, default settings | fails loudly | ArithmeticException: integer overflow at NodeHash.add for the test shape; silent with the non-default suffixRAMLimitMB(0) |
| 9.4.2-9.8.0 | silent, other form | compile() returns a 2,148,532,360-byte FST; with a zero-filled value 2047/2048 lookups return null and 1 an empty output, no exception; on 9.4.2 with the verifier's value bytes (big value byte k = 31k+5, small values 1 + i % 100) one lookup did not return within 120 s |
| 4.2.0-9.4.1 (101 releases) | affected | silent, the same mechanism as main |
| 3.6.0-4.1.0 | fails loudly | ArrayIndexOutOfBoundsException: arraycopy ... in FST.addNode |
fix 13e3e5e3f5 | not affected | every probe and repro correct, including the shapes that used to fail loudly |
Independent AI checkers and a claim re-check checked brief v1: the test fails without the fix and passes with it on main and branch_10_6, a run on all 133 releases moved the start back from 9.10.0 to 4.2.0, and 4 of 35 claims were refuted and corrected. The study's verifier confirmed a new bug.
Checks and verdicts273 words
Independent AI checkers each took a lens on brief v1, and a claim re-check re-checked the disputed claims, also on old releases; brief v2 applies the result.
| Check | Verdict | What it found |
|---|---|---|
| exec-current | confirmed | The test fails 8/8 under -da and 3/3 under -ea on main, and 3/3 each way on branch_10_6; the fix passes 5/5 both ways; its own BYTE4 deep-node repro is silent in three stores and returns a never-added key; a shape matrix of which layouts are silent; heap is a few MB, not 1 MB. |
| exec-versions | history wrong in two places | Ran one probe on all 133 releases 3.6.0-10.5.2: 9.4.2-9.8.0 are silent too, and the silent form goes back to 4.2.0, so it is not a 9.10.0 regression. |
| code-history | holds; AI summary wrong | Root cause, lines, stack traces and fix hold on both refs; the AI summary's last sentence was wrong (destPos was already long before #11923). |
| intended-behavior | not intended | Argued every point for "not a bug" and showed each fails; found the cliff at one byte and the precedents. |
| dups-policy | no duplicates; provenance corrected | About 75 phrasings, the lists, downstream repos and all open PRs: nothing; the task did name util/fst and fixed-length arcs, and Z3 as a general technique. |
| Claim re-check | status ready | Re-ran 4.1.0, 4.2.0, 9.4.1, 9.4.2, 9.8.0 and 9.9.2 itself; 35 claim verdicts, 4 refuted (two version-history claims, the AI summary's history sentence, the task description). |
| study verifier | new bug | FST verifier wrote 9 + 2 tests (BYTE2, BYTE4 with 1M arcs, a deep node, an off-heap reload, the analysis path). |
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 303 KB
Finding record, Lucene and upstream sources, repro files and diagram notes471 words
- Registry title
- FSTCompiler silently writes a corrupt FST when a fixed-length-arc node would expand to 2^31 bytes or more
- Area
- util / FST
- Priority
- Low, rank 990: FSTCompiler can silently write a corrupt FST when one padded fixed-length node reaches 2 GiB, which needs custom FSTs with huge outputs off the index path, unreported in about 13 years.
- Impact class
- data corruption
- Impact note
- Only applications that build FSTs with a wide node and huge outputs (e.g. a ~1 MiB output on a 2048-arc BYTE2 node; in-tree StemmerOverrideFilter.Builder with a 1 MiB stem); the default index format on main does not use util.fst; very low practical reach.
- Summary
- You build an FST in which one node has many arcs and at least one large output, so that its fixed-length arc area numArcs*maxBytesPerArc reaches 2^31 bytes. You expect a correct FST or an error. Instead, with assertions off, compile() returns an FST whose node header was written over the unexpanded arc list: lookups through that node throw, and enumeration can return a key that was never added.
- Introduced
- silent form since LUCENE-4678 (2220c6e8ba9) in 4.2.0; current form #12624 (b6999818474; 9.10.0 ships the backport 193d8f14107), which dropped the long arithmetic (2013-01-12 (LUCENE-4678); 2023-12-07 (#12624 merged))
- First affected version
- 4.2.0
- Affected versions
- silent: 4.2.0-9.4.1 (101 releases) and all 19 releases 9.10.0-10.5.2, branch_10_6 and main [exec]; 9.4.2-9.8.0: silent build, reads return null/empty or hang [exec]; 9.9.0-9.9.2: loud by default, silent with suffixRAMLimitMB(0) [exec]; 3.6.0-4.1.0 loud [exec]
- 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; not yet triaged by a person.
- Found by
- formal: Z3
- Upstream and prior reports
- none found (2026-10-23: about 110 phrasings, mailing lists, downstream repos, 410 open PRs); #9132 (LUCENE-8084) is the same shape for space only
- Lucene commit tested
- main
b83101cdd2and branch_10_6d04ee73177(both run 2026-10-23); all 133 lucene-core releases from 3.6.0 to 10.5.2 (sha1-checked, one reflection probe), the full-history clone for the code history, and fix commit13e3e5e3f5. The public-list rerun reproduced the repro on mainb83101cdd2; the hunt's mainea57360912edadds only #16883, which narrows LFV-004 alone;util/fstis unchanged between the two commits, so the line numbers cited below hold on both.
- TestFSTNodeExpandedSizeOverflow.java (JUnit test):
repro/LFV-007/src/org/apache/lucene/util/fst/TestFSTNodeExpandedSizeOverflow.java(37 lines)
The PNG is the result panel.
Current version v5 (2026-10-24): text and layout from v3, which applied every item of the v2 visual check.
assets/LFV-007/bug.png(303 KB)
