- An FST is a graph: each node has outgoing arcs, and each arc has a label (here a byte) and points to a target node. An
FST.Arcobject describes one arc; the FST's read methods fill in anArcobject you pass to them. Util.readCeilArc(label, fst, follow, arc, in)looks at the arcs leavingfollowand reads the first one whose label is >=labelintoarc. The siblingFST.findTargetArc(label, follow, arc, in)is routinely called with one object for both,findTargetArc(label, arc, arc, in), to step down the FST in place.FSTCompilerstores a small node as a plain list of arcs (scanned one by one) and a node with many arcs as a fixed-length array (binary search, direct addressing or a continuous range). The compiler, not the caller, picks which; it shows only through low-level accessors such asArc.bytesPerArc().
Arc object plays both roles. Step 1 overwrites it with the start node's first arc, a. Step 2 restarts the list scan at its target, which is now the node below a, so the ceiling of 'a' comes back as b. Select the still to open it at full size. Generated with AI, checked against the code.- Build an FST with the single key
abusing the defaultFSTCompiler. The start arc leads to a node with one arc,a, andaleads to a node with one arc,b. Both nodes are small, so the compiler stores their arcs as a plain list. - Call
Util.readCeilArc('a', fst, arc, arc, in)with the start arc as bothfollowandarc: the in-place style ofFST.findTargetArcand of Lucene's ownTestUtil. - You expect the arc
a(label 97), the first arc >='a'leaving the start arc. Two separateArcobjects return exactly that. - Instead you get
b(label 98), an arc of the next node down, with no error and with assertions on or off. With keysab,ac,x, the ceiling of'w'isnullinstead ofx; if the start arc is final (the empty key is in the FST), or the root's first arc ends a key that nothing continues past (the one-key FSTx), the call throwsArrayIndexOutOfBoundsException: Index -1. - Why: the method first reads the start node's first arc,
a, intoarc, which is alsofollow. For a list node it then restarts its scan atfollow.target(), which now points to the node belowa.
Only application code that calls the public, @lucene.experimental Util.readCeilArc itself and passes one Arc object as both arguments, on a list-encoded node or a final arc: default compiler settings, any small node. Lucene's own callers, the codecs' FSTTermsReader and the suggesters' FSTUtil, pass two arcs and are unaffected. Every release from 4.1.0 to 10.6.0.
Mechanism, terms and numbers693 words
Util.readCeilArc(label, fst, follow, arc, in) reads "the first arc greater or equal than the given label into the provided arc in place" (Util.java:810 at 5adff78bb168) from the arcs leaving follow. It first calls fst.readFirstTargetArc(follow, arc, in) (:827), which writes the first arc of follow's target node into arc. When follow and arc are the same object, follow now describes that child arc. The fixed-length branches (:828-871) read only arc's node header and still answer correctly, but a list-encoded node falls through to fst.readFirstRealTargetArc(follow.target(), arc, in) (:875), and follow.target() is by then the child's target, so the scan runs over the node one level down. For the one-key FST ab, the aliased call on the start arc returns b instead of a, silently, with assertions on and off; with keys ab, ac, x the ceiling of 'w' is null instead of x; with a final start arc, or (keys x) when the root's first arc has no outgoing arcs, the call reads FST address -1 and throws ArrayIndexOutOfBoundsException. The two lines date from the method's first version (LUCENE-3846, released in 4.1.0). No Lucene production code passes the same object twice; the two test calls that do pass leaf arcs, where the method returns early.
Util.java:824-826: iffollowhas no target arcs, returnnull. Leaf arcs stop here.Util.java:827:fst.readFirstTargetArc(follow, arc, in)writes the first arc offollow's target intoarc(FST.java:687-692, "this changes the provided arc (2nd arg) in-place"). Withfollow == arc,followis now that child arc.readFirstTargetArcitself reads each field offollowbefore it overwrites that field, so it is alias-safe.- A list-encoded node gets
bytesPerArc = 0(FST.java:738-741), so theifatUtil.java:828is skipped. The fixed-length branches inside it (direct addressing, continuous, binary search) read onlyarc's node header and are unaffected. Util.java:875(the fix site):fst.readFirstRealTargetArc(follow.target(), arc, in).follow.target()is now the child's target, so the linear scan (:877-887) runs over the node one level down and returns its first arc >= label, ornull. If that child arc has no outgoing arcs (it ends a key and nothing continues past it, so itstarget()isFINAL_END_NODE, -1),:875reads address -1 and throws as in step 5, although the start arc is not final.- Final
follow:readFirstTargetArcturns the shared object into the virtual END arc withtarget = FINAL_END_NODE(FST.java:697-708;FINAL_END_NODE = -1,FST.java:132). The END label skips theifat:828, and:875reads address -1:ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 32768atReverseBytesReader.readByte:30<-FST.readFirstArcInfo:722<-FST.readFirstRealTargetArc:746<-Util.readCeilArc:875(executed on an on-heap FST with a list-encoded root, and also with a continuous and a binary-search root: a finalfollowthrows whatever the encoding).
- Keys
ab(the repro): aliased ceiling of'a'on the start arc isb(98) instead ofa(97). - Keys
ab,ac,x(root list-encoded,bytesPerArc0): distinct arcs giveafor'a'andxfor'w'; aliased givesbandnull. - Keys
"",ab(final start arc): distinct arcs givea; aliased throws the index -1 exception. - Keys
x(start arc not final; the root's one arcxends the key,target()-1): distinct arcs givexfor'a'; aliased throws the same index -1 exception. Keysa,b: the same. - Keys
ab,ac,b,c,d,e(5 root arcs): with default settings the root is fixed-length (bytesPerArc2) and the aliased call givesa; withallowFixedLengthArcs(false)it is a list (bytesPerArc0) and givesb. (The keys above give a list root with either setting.)
When a node is list-encoded: FSTCompiler uses fixed-length arcs only for nodes with >= 5 arcs at depth <= 3 or >= 10 arcs anywhere (FSTCompiler.java:77-87, :601-606), so with default settings most small nodes are lists.
Exposure and likelihood218 words
- Who. Application code that calls
Util.readCeilArcdirectly and passes oneArcobject as bothfollowandarcon a non-leaf arc whose target node is list-encoded, or that is final. DefaultFSTCompilersettings, tiny input (one key in the repro); no scale or configuration needed. - Not affected. Lucene's two non-test callers pass distinct objects:
FSTTermsReader.java:675(lucene/codecs,top.fstArcandframe.fstArc) andFSTUtil.java:105(lucene/suggest,path.fstNodeandscratchArc). The downstream callers found by code search (for example Apache Pinot'sRegexpMatcher) also pass distinct arcs. - Effect. A wrong arc, taken from the wrong level, or
null, silently: a walk built on it takes a wrong path or stops early. Or anArrayIndexOutOfBoundsException(index -1) whenfollowis final or when the first arc of its target node has no outgoing arcs (for example the one-key FSTx). Same with assertions on and off. - Opt-in part.
Utilis marked@lucene.experimental(Util.java:38-42); the trigger is the caller's own aliasing. - Likelihood: rare. No caller found does it and no prior report was found, but whoever does it gets a wrong answer for most labels on a list-encoded node: for the one-key FST
ab, 67 of the 95 printable ASCII labels (the scan of the wrong node gives the right answer only by coincidence).
One JUnit test method on the public API: a one-key FST ab, one aliased readCeilArc call, one assertion. Deterministic and single-threaded; the test itself takes about a second; it fails with assertions on and off.
package org.apache.lucene.util.fst;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.IntsRefBuilder;
public class TestReadCeilArcSameFollowAndArc extends LuceneTestCase {
public void testSameFollowAndArc() throws Exception {
FSTCompiler<Object> compiler =
new FSTCompiler.Builder<>(FST.INPUT_TYPE.BYTE1, NoOutputs.getSingleton()).build();
compiler.add(Util.toIntsRef(new BytesRef("ab"), new IntsRefBuilder()), NoOutputs.getSingleton().getNoOutput());
FST<Object> fst = FST.fromFSTReader(compiler.compile(), compiler.getFSTReader());
FST.Arc<Object> arc = fst.getFirstArc(new FST.Arc<>());
// expected: the root's arc 'a' (as with two distinct Arc objects); actual: the child's arc 'b'
assertEquals('a', Util.readCeilArc('a', fst, arc, arc, fst.getBytesReader()).label());
}
}
cp $REPRO/TestReadCeilArcSameFollowAndArc.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/fst/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.util.fst.TestReadCeilArcSameFollowAndArc -Ptests.seed=DEADBEEF
# and with -Ptests.asserts=false: the same failure
- Expected
- The arc
a(label 97), as with two distinctArcobjects. - Observed
- The arc
b(label 98):java.lang.AssertionError: expected:<97> but was:<98>.
More files, environment, full output156 words
TestReadCeilArcSameFollowAndArc (one method, extends LuceneTestCase, package org.apache.lucene.util.fst of the lucene/core tests). It builds the FST with FSTCompiler.Builder(BYTE1, NoOutputs) and default settings, takes the start arc and asserts that Util.readCeilArc('a', fst, arc, arc, in) returns label 'a'.
As the regression test, the same method fits in TestUtil. Line 12 is longer than 100 characters, which ./gradlew tidy reformats.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 9.12.3, 10.0.0, 10.6.0: this test fails; 10.5.2: an equivalent test fails; 4.1.0 to 8.11.4 (6 releases): an older-API form fails; 4.0.0: no such method |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (nothing platform-specific) |
cp $REPRO/TestReadCeilArcSameFollowAndArc.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/fst/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.util.fst.TestReadCeilArcSameFollowAndArc -Ptests.seed=DEADBEEF
# and with -Ptests.asserts=false: the same failure
TestReadCeilArcSameFollowAndArc > testSameFollowAndArc FAILED
java.lang.AssertionError: expected:<97> but was:<98>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:E0B3699405501700]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:647)
at org.junit.Assert.assertEquals(Assert.java:633)
at org.apache.lucene.util.fst.TestReadCeilArcSameFollowAndArc.testSameFollowAndArc(TestReadCeilArcSameFollowAndArc.java:16)
(gradle on main 5adff78bb168, seed DEADBEEF, assertions on; with -Ptests.asserts=false the same assertion)- The javadoc reads the ceiling arc "into the provided arc in place" and never says
followandarcmust differ. - The in-place siblings are alias-safe:
Util.getcallsfindTargetArc(label, arc, arc, in), and Lucene's ownTestUtilcallsreadCeilArc(label, fst, arc, arc, in). - Same keys: right with a fixed-length root, wrong with a list root; the compiler, not the caller, picks which.
The full argument190 words
- Contract.
Util.java:809-818: "Reads the first arc greater or equal than the given label into the provided arc in place and returns it iff found";@param follow"the arc to follow reading the label from",@param arc"the arc to read into in place". No javadoc, package doc, CHANGES or MIGRATE entry requires distinct objects. - Siblings.
FST.findTargetArc"Finds an arc leaving the incoming arc, replacing the arc in place" (FST.java:1006-1008);Util.getcallsfst.findTargetArc(label, arc, arc, fstReader)(Util.java:57,:84);FST.readFirstTargetArc"changes the provided arc (2nd arg) in-place" (FST.java:687-692). Both are alias-safe; the devil's-advocate verifier counted 18 non-test call sites in main that pass one arc twice to them. - Tests.
TestUtil.java:63and:101callUtil.readCeilArc(label, fst, arc, arc, in), but only on leaf arcs, where the early return atUtil.java:824-826hides the defect: the call shape Lucene's own test uses, not proof of a tested contract. - History. Both lines are in the method's first version (LUCENE-3846, 2012); the only later change to either is the rename
follow.target->follow.target()(760f2dbdcb2, LUCENE-8920). No commit, issue or PR found in the searches discusses aliasing.
The devil's-advocate check128 words
The strongest case for "intended": the method has two separately named parameters with different roles, Util is @lucene.experimental, no production caller aliases them, and TestUtil's aliased calls only reach the leaf early return; so the rule is "pass two arcs".
Why it fails: that rule is written nowhere; the same-object idiom is established by in-tree use of the sibling methods with the same (follow, arc) pair (18 call sites) and safe for them; Lucene's own test uses the call shape; and the aliased call is right or wrong depending only on the node encoding. The cross-model reviewer rated this case moderate and rejected it, as did the independent reproducer, the devil's-advocate verifier and the adjudication. A maintainer may still prefer to document the rule (fix B).
Recommended Save follow's target before it can be overwritten (fix A)
Read follow.target() into a local before readFirstTargetArc can overwrite it, and start the list scan from that local (2 lines in Util.readCeilArc). It also fixes both exception cases. The repro passes with assertions on and off, and 98 tests of the closest suites pass.
// follow and arc may be the same object: read follow's target before arc is overwritten
final long followTarget = follow.target();
fst.readFirstTargetArc(follow, arc, in);
...
fst.readFirstRealTargetArc(followTarget, arc, in);All options and trade-offs2 options ยท 355 words
Fix A was built and checked through gradle on 5adff78bb168; B changes only the javadoc and was not run. Open PR #12688 adds a PrimitiveLongFST copy of readCeilArc with the same call order, so the same change applies there.
In Util.readCeilArc, store follow.target() in a local before fst.readFirstTargetArc(follow, arc, in) (Util.java:827) and pass that local to readFirstRealTargetArc at :875.
// follow and arc may be the same object: read follow's target before arc is overwritten
final long followTarget = follow.target();
fst.readFirstTargetArc(follow, arc, in);
...
fst.readFirstRealTargetArc(followTarget, arc, in);Trade-offs
One local long. Callers with distinct arcs read the same node as before. It also fixes both exception cases (a final follow, and a first child arc with no outgoing arcs): the local is the real node (targetHasArcs checked it is > 0), so the scan reads the real arcs instead of address -1 (executed: every one-object answer equals the two-object answer).
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on, and-Ptests.asserts=false) and passes with it (both). - With the fix: the repro (1),
TestUtil(5),TestFSTs(22),TestFSTDirectAddressing(3),TestFuzzySuggester(14),TestAnalyzingSuggester(23) andTestFSTPostingsFormat(30,lucene/codecs): 98 tests, 0 failures, 5 skipped, assertions on.checkGoogleJavaFormaton core passes. The full core suite was not run, and only OpenJDK 25.0.3 on Linux aarch64 was used. - Earlier: the hunt's shadow-class runs of the same change pass the repro (assertions on and off), the finder's probe and
TestUtil; the cross-model reviewer's own test passes with it.
Add the rule to the readCeilArc javadoc and, for consistency, change the two aliased TestUtil calls (:63, :101), which pass today only because they hit the leaf early return.
Trade-offs
No runtime change, but anyone who uses the in-place idiom of the sibling methods still gets a silent wrong answer.
What the checks validated
Not run.
Downstream code that walks an FST with Util.readCeilArc and reuses one Arc object would silently follow wrong paths; the downstream callers that were checked pass two arcs.
A proof-obligation audit of Util.readCeilArc's contract: the ceiling arc must be the same whatever object it is read into, including the in-place call with one Arc for both arguments. The obligation fails at the second read of follow.target(), after follow was overwritten; a public-API JUnit test then witnessed the wrong arc.
The method in full129 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder checked
readCeilArcagainst the obligation that its result does not depend on whetherfollowandarcare distinct, the in-place idiom offindTargetArc,Util.getandTestUtil, and found that:875readsfollow.target()after:827may have overwrittenfollow. - The finder then wrote a public-API JUnit test (keys
ab,ac,x:binstead ofa), a probe for thenulland index -1 cases, and the 2-line fix that makes them pass; the test was later reduced to the one-key FSTab. - An independent reproducer, an executed cross-model review, a devil's-advocate verifier and an adjudication reproduced it with their own tests ("Prior reports, history and checks").
No prior report found in apache/lucene issues, PRs and commits (incl. the JIRA archive), the mailing lists, downstream trackers or the web (searched 2026-11-03). Nearest: #9825 (LUCENE-8781), a different readCeilArc defect fixed in 8.2; open PR #12688 copies the same call order without mentioning it.
Searches3 searches
No matching public report was found in the documented searches. Nearest items, none a report of this bug: #9825 (LUCENE-8781: readCeilArc lacked a direct-addressing branch, fixed in 8.2: badcc4e6c72 on branch_8x, 2e49f13aa1e on main), lucene-solr #800 (LUCENE-8920: added TestUtil with the aliased leaf-arc call; fixed a different off-by-one before merge), #12748 (continuous arcs; a second aliased leaf-arc call), #4919 (LUCENE-3846, which added the method) and open PR #12688 (adds a PrimitiveLongFST copy of readCeilArc with the same sequence). A last check read the issues and PRs updated since the previous check (none about this, none touching util/fst). Limits: keyword search; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-03 | two scripted prior-report searches (104 queries, 290 distinct hits in the second) and about 40 manual searches: method and class names, aliasing, in-place arcs, symptoms; the JIRA REST search | no report; #9825 is the only readCeilArc defect on record |
| Open and recent PRs | 2026-11-03 (twice) | the changed files of all 409 open apache/lucene PRs, of the 40 PRs updated since 2026-11-01, and of the 4 PRs updated since a recent cutoff | only #12688 touches Util.java; it copies the sequence and does not report it |
| Mailing lists, downstream, web | 2026-11-03 | dev@, java-user@, issues@, commits@ and general@; apache/lucene-solr, Solr, Elasticsearch, OpenSearch and Pinot trackers; GitHub code search; web queries | nothing about this bug |
Present since the method was added by LUCENE-3846 (66b2c78d6ba, 2012) and first released in 4.1.0; 4.0.0 has no such method. Every release run since then, through 10.6.0 and main, fails. Not fixed upstream.
Versions tested5 rows
Introduced with the method by LUCENE-3846 "Fuzzy suggester" (Michael McCandless; now #4919): 66b2c78d6ba on the feature branch, 2012-10-11; reached trunk as d8e44bd09d6 and branch_4x as 1fb15d729d4 on 2012-10-30 (Subversion era, no pull request). The first version already has readFirstTargetArc(follow, arc, in) followed by readFirstRealTargetArc(follow.target, arc, in). git blame credits line 875 to the later rename 760f2dbdcb2 (LUCENE-8920).
Releases before 9.x ran main() versions of the same aliased call with the older FST API (keys ab, ac, x); 9.x and later ran the JUnit test. By reading, no commit changed the two lines in 9.0.0 to 9.11.x or 10.1.0 to 10.5.1. Upstream main 1c2072f0284 (2026-11-02) still has both lines.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | gradle: fails with assertions on and off; passes with fix A |
| 9.12.3, 10.0.0, 10.6.0 | affected | the hunt's JUnit test (keys ab, ac, x): b instead of a |
| 10.5.2 | independent test | keys cat, cow, dog: o instead of c |
| 4.1.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4 | older-API form | b instead of a; distinct arcs give a |
| 4.0.0 | API absent | compile error: no Util.readCeilArc |
A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, real, high), a devil's-advocate verifier, an adjudication and a final repro check reproduced it. This doc's gradle runs reproduced it on main and validated fix A.
Checks and verdicts255 words
The cross-model review ran on the older main ea57360912ed, where Util.java and FST.java are byte-identical to 5adff78bb168; its test was then re-run on 5adff78bb168 (2 of 3 cases fail, assertions on and off). Every other check ran on 5adff78bb168 or release jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Keys ab, ac, x: b instead of a on main (assertions on and off), 10.6.0 and 9.12.3; probe: list-encoded root, null instead of x, index -1 exception; the 2-line fix passes. |
| independent reproducer | real | Own test (keys cat, cow, dog: o instead of c) fails on main (on, off), 10.6.0, 10.5.2 and 9.12.3; the distinct-arc control passes; its own 2-line fix passes. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own 3-case test fails 2 (null instead of c; index -1 exception); the fixed-length-root control passes; the fix passes it and TestUtil. |
| devil's-advocate verifier | real | No documented distinct-object rule; the siblings are alias-safe and called aliased at 18 sites; the same keys are right with fixed-length arcs and wrong with list arcs. |
| adjudication, separate re-runs and final repro check | confirmed (high), Low | Re-runs on main fail with assertions on and off and pass with the fix; one minimal deterministic test (one-key FST ab). |
| this doc | reproduced; fix A validated | Gradle without the fix fails (on, off); with it the repro and 97 more tests of the closest suites pass. |
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 521 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,234 words
- Registry title
- Util.readCeilArc returns a wrong arc or null, with no error, when one Arc object is passed as both follow and arc and the target node is list-encoded, and reads FST address -1 when follow is final (since 4.1.0)
- Area
- util / FST (Util.readCeilArc: follow overwritten by readFirstTargetArc before the list-node scan re-reads follow.target())
- Priority
- Low, rank 915: Util.readCeilArc silently returns a wrong arc or null when the caller passes one Arc object as both follow and arc and the target node is list-encoded, and throws when follow is a final arc, in every release since 4.1.0. Only application code that calls this expert, experimental FST helper in that in-place style is hit, and no Lucene caller does, so it is rare and Low.
- Impact class
- silent wrong results
- Impact note
- Only application code that calls the public, @lucene.experimental Util.readCeilArc directly and passes one Arc object as both follow and arc (the in-place style of FST.findTargetArc, Util.get and Lucene's own TestUtil) is hit: when the target node is list-encoded, which FSTCompiler picks for nodes with few arcs, it silently returns a wrong arc or null, and when follow is a final arc it throws ArrayIndexOutOfBoundsException, with -ea and -da; tiny input, default FSTCompiler settings, no scale needed; no Lucene code path is affected (FSTTermsReader and the suggest FSTUtil pass distinct arcs); every release since 4.1.0 and main.
- Summary
- You build a small FST (keys ab, ac, x) with the default FSTCompiler and call Util.readCeilArc('a', fst, arc, arc, in) with the start arc as both follow and arc, the same call shape Lucene's own TestUtil uses. You expect the arc 'a', the first arc >= 'a' leaving the start arc, which the same call with two distinct Arc objects returns. Instead it returns the arc 'b' from the node one level down (and null instead of 'x' for label 'w'), because readFirstTargetArc overwrites the shared object before the list-node scan re-reads follow.target(); when follow is a final arc the call reads FST address -1 and throws ArrayIndexOutOfBoundsException.
- Introduced
- 66b2c78d6bafbed21e70ab1de9fea25f35f2ca04 (LUCENE-3846 'Fuzzy suggester', now #4919, mikemccand; svn feature branch lucene3846 r1397171; no pull request, Subversion era), which reached trunk as d8e44bd09d6 (r1403779) and branch_4x as 1fb15d729d4 (r1403780) [read: git log -L on readCeilArc in the history clone; the method's first version already has readFirstTargetArc(follow, arc, in) followed by readFirstRealTargetArc(follow.target, arc, in), and the only later change to either line is the follow.target -> follow.target() rename in 760f2dbdcb2 (LUCENE-8920); exec: 4.0.0 has no Util.readCeilArc (compile error), 4.1.0 fails; the commits themselves were not built (Subversion-era builds)] (2012-10-11 (author and commit date of 66b2c78d6ba on the feature branch); trunk and branch_4x commits 2012-10-30)
- First affected version
- 4.1.0 [exec: 4.1.0 fails with -da, the aliased call gives 'b' instead of 'a' while distinct arcs give 'a'; 4.0.0 has no Util.readCeilArc (compile error), so the defect is as old as the API]
- Affected versions
- every release from 4.1.0 through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT); not fixed anywhere (upstream main 1c2072f0284, 2026-11-02, still has both lines at Util.java:827 and :875). Executed, fail [exec]: main -ea and -da (finder, the independent reproduction, the cross-model review, the intended-behaviour check, judge and the registration check), 10.6.0 (-ea and -da), 10.5.2 (-ea, the independent reproduction), 10.0.0 (-da), 9.12.3 (-ea and -da), 8.11.4, 7.7.3, 6.6.6, 5.5.5, 4.10.4 and 4.1.0 (-da; releases before 9.x through main() adapters with the same FST {ab, ac, x} and the same aliased call); 4.0.0: compile error (API absent). 9.0.0-9.11.x and 10.1.0-10.5.1 by reading (no commit changed the two lines). Not new in 10.6. All runs on Linux aarch64 with JDK 25.0.3.
- 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: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches; rechecked the same day): two scripted prior-report searches (104 queries over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; 290 distinct hits in the second run, none high), about 40 manual GitHub searches incl. apache/lucene-solr, Elasticsearch, OpenSearch, Solr and Pinot, GitHub code search, web searches, the changed files of all 409 open apache/lucene PRs and of the 40 PRs updated since 2026-11-01 (none touches util/fst), full reads of #9825 (body and all 25 comments) and lucene-solr PRs #721 and #800, the Util.java patch of open PR #12688, and scans of a dozen related issues (#4919, #6216, #6646, #9963, #12895 and others). The only readCeilArc defect on record is #9825 (LUCENE-8781, the missing direct-addressing branch, fixed in 8.2 by 2e49f13aa1e), a different root cause; lucene-solr PR #800 fixed a different off-by-one before merge. Open PR #12688 (random-access term dictionary) adds a PrimitiveLongFST copy of readCeilArc with the same sequence and neither reports nor fixes it. Not fixed anywhere: upstream main 1c2072f0284 still has both lines.
- Lucene commit tested
- main
5adff78bb168(12.0.0-SNAPSHOT), OpenJDK 25.0.3 on Linux aarch64: this doc's gradle runs (the repro without the fix, assertions on and off: fail; with fix A: pass; 98 tests of the closest suites with fix A: pass) and the hunt's runs; release jars 4.1.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (all fail) and 4.0.0 (no such method). The cross-model review ran onea57360912ed, whoseUtil.javaandFST.javaare byte-identical to5adff78bb168, and was re-run on5adff78bb168.
- Finding record
lucene/core/src/java/org/apache/lucene/util/fst/Util.java:38-42 (at 5adff78bb168) (@lucene.experimental), :809-818 (readCeilArc javadoc: 'Reads the first arc greater or equal than the given label into the provided arc in place'; @param follow 'the arc to follow reading the label from', @param arc 'the arc to read into in place'), :824-826 (targetHasArcs early return), :827 (fst.readFirstTargetArc(follow, arc, in)), :828-871 (array, direct-addressing and continuous branches read only arc's node fields), :873-887 (list scan; :875 fst.readFirstRealTargetArc(follow.target(), arc, in))lucene/core/src/java/org/apache/lucene/util/fst/FST.java:132 (at 5adff78bb168) (FINAL_END_NODE = -1), :687-716 (readFirstTargetArc 'changes the provided arc (2nd arg) in-place'; a final follow becomes the virtual END arc with target FINAL_END_NODE at :708), :1006-1010 (findTargetArc 'replacing the arc in place'); lucene/core/src/java/org/apache/lucene/util/fst/FSTCompiler.java:77-87 and :601-606 (fixed-length arcs only for nodes with >= 5 arcs at depth <= 3 or >= 10 arcs; other nodes are list-encoded)lucene/core/src/test/org/apache/lucene/util/fst/TestUtil.java:63 (at 5adff78bb168) and :101 (Util.readCeilArc(label, fst, arc, arc, in) on leaf arcs); lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java:675 and lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/FSTUtil.java:105 (in-tree callers, distinct arcs)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/fst/Util.java:809-818, (at 5adff78bb168) :824-828, :875-887lucene/core/src/java/org/apache/lucene/util/fst/FST.java:132, (at 5adff78bb168) :687-716, :738-741lucene/core/src/java/org/apache/lucene/util/fst/FSTCompiler.java:77-87, (at 5adff78bb168) :601-606
- Who is affected
lucene/codecs/src/java/org/apache/lucene/codecs/memory/FSTTermsReader.java:675 (at 5adff78bb168)lucene/suggest/src/java/org/apache/lucene/search/suggest/analyzing/FSTUtil.java:105 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/util/fst/Util.java:38-42 (at 5adff78bb168) (@lucene.experimental)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/fst/Util.java:57, (at 5adff78bb168) :84, :809-818, :824-826lucene/core/src/java/org/apache/lucene/util/fst/FST.java:687-692, (at 5adff78bb168) :1006-1008lucene/core/src/test/org/apache/lucene/util/fst/TestUtil.java:63, (at 5adff78bb168) :101
- Prior reports
https://github.com/apache/lucene/issues/9825, https://github.com/apache/lucene-solr/pull/800, https://github.com/apache/lucene/pull/12688, https://github.com/apache/lucene/pull/12748
- Diagrams
lucene/core/src/java/org/apache/lucene/util/fst/Util.java:827, (at 5adff78bb168) :875
- TestReadCeilArcSameFollowAndArc.java (lucene/core, package org.apache.lucene.util.fst, 1 test):
repro/LFV-1159/TestReadCeilArcSameFollowAndArc.java(18 lines)
The diagram illustrates the repro's FST (key ab) and the shared object before and after Util.java:827, and where :875 starts the scan.
assets/LFV-1159/bug.png(521 KB)
