RunAutomaton reserves one placeholder state for an input with no states, then loops over it and asks the input for state 0's transitions. Automaton.next reads states[0], but Builder.finish() allocated that array with exactly 0 entries, so it throws. With spare capacity (new Automaton() reserves 4 slots) the same read silently returns zeros, meaning "no transitions".
- An
Automatonstores each state's transitions as a pointer and a count: state i usesstates[2i]andstates[2i + 1]. An automaton with 0 states is legal;Automata.makeEmpty()returns one, the empty language. - A
RunAutomaton(CharacterRunAutomatonfor strings,ByteRunAutomatonfor bytes) copies a deterministic automaton into a flat lookup table for fast matching. Its start state is always state 0. - Queries such as
RegexpQuerybuild theirs throughCompiledAutomaton; the pattern tokenizersSimplePatternTokenizerandSimplePatternSplitTokenizerbuild aCharacterRunAutomatondirectly.
states[]; RunAutomaton adds a placeholder state 0 and reads states[0] for it, which throws. makeEmpty() has 4 spare slots, so the same read returns zeros and works. Select the still to open it at full size. Generated with AI, checked against the code.- Build an automaton that matches nothing with
new Automaton.Builder().finish(): 0 states, deterministic, the empty language. - Wrap it in
new CharacterRunAutomaton(a). You expect a matcher that rejects every string,run("")= false, which is what the same call gives forAutomata.makeEmpty(), also 0 states. - You get
ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0from the constructor, with assertions on and off. - Why:
RunAutomatongives a 0-state input one placeholder start state (size = max(1, 0)= 1) and then asks the input for state 0's transitions.Builder.finish()sized the input's state array exactly, 0 x 2 = 0 slots, so reading slot 0 fails.makeEmpty()survives only becausenew Automaton()reserves 4 spare slots. - The same crash comes from
ByteRunAutomaton,Operations.repeat(empty, 2, 2)and patterns built on a bounded repeat{n}or{n,m}(n >= 1) of something that can never match, sonew SimplePatternTokenizer("(a&b){2}")throws too (the same operand under{n,},*or+does not crash). Queries are safe:CompiledAutomatonswaps a 0-state automaton for a 1-state one first.
Code that builds a CharacterRunAutomaton or ByteRunAutomaton itself from an empty-language automaton made by Automaton.Builder, new Automaton(0, 0) or Operations.repeat, and the SimplePattern* tokenizers and factories given a pattern built on a bounded repeat {n} or {n,m} (n >= 1) of something that can never match, like (a&b){2}. Not the default; queries are guarded. Loud: an exception, no wrong results.
Mechanism, terms and numbers765 words
new CharacterRunAutomaton(new Automaton.Builder().finish()) throws ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0 instead of returning a run automaton that accepts nothing. The input is a valid deterministic automaton with 0 states, the empty language. RunAutomaton gives a 0-state input one synthetic, non-accepting start state (size = Math.max(1, a.getNumStates())), but then loops over that synthetic state and asks the source automaton for state 0's transitions; Automaton.next reads states[0] of the source. Automaton.Builder.finish() sizes states[] exactly (numStates * 2 = 0 entries), so the read fails. Automata.makeEmpty(), also 0 states, works only because new Automaton() pre-allocates 4 slots. The same crash comes from ByteRunAutomaton, new Automaton(0, 0), Operations.repeat(emptyLanguage, 2, 2) and string patterns built on a bounded repeat {n} or {n,m} (n >= 1) of an empty-language operand, so new SimplePatternTokenizer("(a&b){2}") throws too (the same operand under {n,}, *, + or plain concatenation does not crash). Queries are protected by CompiledAutomaton's 0-state guard. Every release since 5.0.0 is affected: LUCENE-5959 made finish() allocate exact-size arrays.
// RunAutomaton.java:67-83 (constructor)
points = a.getStartPoints(); // always holds MIN_CODE_POINT: points.length >= 1
size = Math.max(1, a.getNumStates()); // :68 1 for a 0-state automaton
accept = new FixedBitSet(size);
transitions = new int[size * points.length];
Arrays.fill(transitions, -1); // the synthetic state already rejects everything
Transition transition = new Transition();
for (int n = 0; n < size; n++) { // :73 n = 0, but the source has no state 0
if (a.isAccept(n)) { accept.set(n); } // :74 java.util.BitSet.get(0) is false: no crash
transition.source = n;
transition.transitionUpto = -1;
for (int c = 0; c < points.length; c++) {
int dest = a.next(transition, points[c]); // :80 -> Automaton.java:676 -> :694 states[2 * 0]Automaton.java:690-694: the privatenextreadsstates[stateIndex]withstateIndex = 2 * state, so state 0 readsstates[0].Automaton()isthis(2, 2)(Automaton.java:79-82, "creates an automaton with no states"):stateshas length 4, so the read returns leftover zeros, "0 transitions". That is whyAutomata.makeEmpty()(Automata.java:55-60) works.Automaton(int numStates, int numTransitions)allocatesstates = new int[numStates * 2](Automaton.java:91-92), andAutomaton.Builder.finish()builds its result with exactlynew Automaton(numStates, numTransitions)(Automaton.java:889-894, "Create automaton with the correct size."). A Builder with no states yieldsstates.length == 0.- Regexp route: every bounded repeat
{n}or{n,m}goes throughOperations.repeat(a, min, max)(RegExp.java:672-674,Operations.java:291-323). Formin == 1it copies the operand intonew Automaton()(:299-301), formin >= 2it concatenatesmincopies (:302-308); either way it copies the result into aBuilderand returnsremoveDeadStates(builder.finish())(:311-312,:322), which hands a 0-state input back unchanged (:1010-1011). So for an empty-language operand such as(a&b),#or[^\D\d]andmin >= 1the result has 0 states, exact-size arrays, and reports itself deterministic:(a&b){1},(a&b){1,3},(a&b){2},#{2,10}crash, while(a&b){2,},(a&b)*,(a&b)+,(a&b)(a&b),x(a&b){1}and(a&b){0,2}do not. Kleene star and intersection return a 0-state input unchanged (:202-204,:396-401), so((a&b){2})*and(a&b){2}&(a&b){2}crash too, while((a&b){2})+and(a&b){2}|(a&b){2}do not (probes on645d4fe14a20). - Tokenizer route:
SimplePatternTokenizer(String)("See RegExp for the accepted syntax") buildsnew RegExp(regexp).toAutomaton(), checks only determinism and callsnew CharacterRunAutomaton(dfa)(SimplePatternTokenizer.java:75-101); the same inSimplePatternSplitTokenizer.java:64-91.SimplePatternTokenizerFactoryparses the pattern in its constructor, which succeeds, and the crash appears whencreate()is called (SimplePatternTokenizerFactory.java:73-75, 87-88). - Why queries are safe:
CompiledAutomatonreplaces a 0-state automaton with a fresh 1-state automaton before it builds a run automaton (CompiledAutomaton.java:154-157).AutomatonQuerybuilds one (AutomatonQuery.java:99), andRegexpQuery,WildcardQuery,PrefixQueryandTermRangeQueryextend it;FuzzyQueryusesCompiledAutomatontoo (FuzzyAutomatonBuilder.java:63,:73). A search of every non-test source file for run-automaton constructions finds three other query-side places, none of which builds one from an exact-size 0-state automaton:TermInSetQuery(only for 2 or more terms,TermInSetQuery.java:185-195), the unified highlighter (Automata.makeString, ormakeStringUnion, which returnsmakeEmpty()for no terms,Automata.java:642-643) and the sandboxTermAutomatonQuery(rewrites an empty automaton toMatchNoDocsQuery,TermAutomatonQuery.java:491-493).
For new Automaton.Builder().finish(): getNumStates() = 0, states.length = 0, getStartPoints() = [0], so RunAutomaton sets size = 1 and reads states[0] once: Index 0 out of bounds for length 0. For Automata.makeEmpty(): 0 states but states.length = 4, so the read returns 0 transitions and run("") and run("a") are false. A survey of 15 ways to build a 0-state automaton crashes in exactly the cases with states.length = 0.
Exposure and likelihood170 words
- Who. (a) Code that constructs a
CharacterRunAutomatonorByteRunAutomatondirectly from an empty-language automaton built throughAutomaton.Builder,new Automaton(0, 0)orOperations.repeat(empty, min >= 1, max); (b)SimplePatternTokenizer,SimplePatternSplitTokenizerand their factories with a pattern built on a bounded repeat{n}or{n,m}(n >= 1) of an empty-language operand, such as(a&b){2},(a&b){1},#{2,10},([^\D\d]){2}or((a&b){2})*; the same operand under{n,},*,+or plain concatenation does not crash. Such patterns are validRegExpsyntax; they can never match. - Not default. It needs code that builds a run automaton itself, or a
SimplePattern*tokenizer or factory configured with such a pattern. Queries build theirs throughCompiledAutomatonor never from an exact-size 0-state automaton (source inventory);RegexpQueryon the same patterns returns 0 hits with no exception. - Effect. An unchecked
ArrayIndexOutOfBoundsExceptionfrom a constructor instead of a reject-everything matcher. Through a factory the factory constructor succeeds andcreate()throws. No wrong results, no corruption, no hang. - Scale. One call, no index.
One JUnit test, public API only, no index, deterministic: build a 0-state automaton with new Automaton.Builder().finish(), wrap it in CharacterRunAutomaton, and expect run("") to be false. The try/catch only labels the failure.
/*
* 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.automaton;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestRunAutomatonZeroStates extends LuceneTestCase {
public void testRunAutomatonFromBuilderWithNoStates() {
Automaton empty = new Automaton.Builder().finish();
CharacterRunAutomaton run;
try {
run = new CharacterRunAutomaton(empty);
} catch (ArrayIndexOutOfBoundsException e) {
// Expected: a run automaton that accepts nothing; actual: the constructor throws AIOOBE.
throw new AssertionError("new CharacterRunAutomaton(0-state automaton) threw " + e, e);
}
assertFalse(run.run(""));
}
}
cp $REPRO/TestRunAutomatonZeroStates.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/automaton/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.automaton.TestRunAutomatonZeroStates -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same exception)
- Expected
new CharacterRunAutomaton(...)returns a run automaton andrun("")is false, as forAutomata.makeEmpty().- Observed
ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0fromRunAutomaton.<init>(RunAutomaton.java:80), with assertions on and off.
More files, environment, full output258 words
- Smallest input. The Builder with no states is the smallest automaton there is; one call, one thread, no randomness. Same result with assertions on and off and with two seeds (DEADBEEF, C0FFEE).
- API. Public only (
Automaton.Builder,CharacterRunAutomaton); the packageorg.apache.lucene.util.automatonis for locality, no package-private API is used. - Releases. The same test fails on 10.5.2 and 10.6.0; a
main()form of it fails on every release from 5.0.0 to 10.6.0 (all 108) and passes on 4.10.0 to 4.10.4. - As the regression test: this same method. Upstream it can live in
TestAutomatonor a small new class; thetry/catchmay go, the AIOOBE then shows as the test error (not run in that form).
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on 10.5.2, 10.6.0 and every release from 5.0.0; 4.10.4 passes |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
cp $REPRO/TestRunAutomatonZeroStates.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/automaton/
cd $LUCENE
./gradlew :lucene:core:test --tests org.apache.lucene.util.automaton.TestRunAutomatonZeroStates -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same exception)
# gradle, main 645d4fe14a20, seed DEADBEEF, assertions on
TestRunAutomatonZeroStates > testRunAutomatonFromBuilderWithNoStates FAILED
java.lang.AssertionError: new CharacterRunAutomaton(0-state automaton) threw java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at __randomizedtesting.SeedInfo.seed([DEADBEEF:5E2D81154F1689AE]:0)
at org.apache.lucene.util.automaton.TestRunAutomatonZeroStates.testRunAutomatonFromBuilderWithNoStates(TestRunAutomatonZeroStates.java:30)
Caused by:
java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
at org.apache.lucene.util.automaton.Automaton.next(Automaton.java:694)
at org.apache.lucene.util.automaton.Automaton.next(Automaton.java:676)
at org.apache.lucene.util.automaton.RunAutomaton.<init>(RunAutomaton.java:80)
at org.apache.lucene.util.automaton.CharacterRunAutomaton.<init>(CharacterRunAutomaton.java:27)
at org.apache.lucene.util.automaton.TestRunAutomatonZeroStates.testRunAutomatonFromBuilderWithNoStates(TestRunAutomatonZeroStates.java:27)
1 test completed, 1 failed
# gradle, assertions off (-Ptests.asserts=false): the same AssertionError and cause
# 10.5.2 release jars, assertions on and off: the same exception at the same line numbers
# with the recommended fix: BUILD SUCCESSFUL, 1 test, 0 failures (assertions on and off)- The constructors document one failure only,
IllegalArgumentExceptionfor non-deterministic input; a 0-state automaton is deterministic, andAutomata.makeEmpty()returns one. RunAutomatonreserves a start state for this case (Math.max(1, a.getNumStates())); Lucene's test framework usesnew CharacterRunAutomaton(makeEmpty())as its empty stop set.- Only hidden spare capacity decides:
makeEmpty()works,Builder().finish()crashes, and 4.10.4 passes the same check (main() form).
The full argument205 words
- Contract.
RunAutomaton.java:55-60: "Constructs a newRunAutomatonfrom a deterministicAutomaton. ... @throws IllegalArgumentException if the automaton is not deterministic".CharacterRunAutomaton.java:21-25("Construct from a DFA") andByteRunAutomaton.java:22-35document only the same exception. - 0-state automata are first-class.
Automaton.java:79"creates an automaton with no states";Automata.java:55"Returns a new (deterministic) automaton with the empty language" (0 states);Operationsspecial-casesgetNumStates() == 0in many places, e.g.Operations.java:202-205, "Repeating the empty automata will still only accept the empty automata". RunAutomatonmeans to support them.size = Math.max(1, a.getNumStates())(RunAutomaton.java:68) exists only to give a 0-state input a start state; the class javadoc says "The initial state is always 0" (:38).- Lucene relies on it.
MockTokenFilter.EMPTY_STOPSET, "Empty set of stopwords", isnew CharacterRunAutomaton(makeEmpty())(test framework,MockTokenFilter.java:43-44); it works only becausemakeEmpty()carries spare capacity. - Tests.
TestRegExp.testRepeatWithEmptyLanguage(TestRegExp.java:107-117) builds#{2,10}and checks onlytoString(); nothing asserts a run automaton must refuse such input. - History. The crash appeared with LUCENE-5959 (#7021), a 5.0.0 memory optimization that made
finish()exact-size; it did not discuss 0-state automata. 4.10.4 passes the same check (itsmain()form). - Precedent. LUCENE-6054 (#7116, "RegExp.toAutomaton fails on #*"), an earlier empty-language edge case, was fixed under Bug Fixes.
The devil's-advocate check197 words
Strongest case for "not a bug": RunAutomaton and Automaton are @lucene.experimental low-level APIs, and callers should go through CompiledAutomaton, which already handles 0 states; direct construction from a 0-state automaton is unsupported. For the tokenizers, the class javadoc says "The provided regex should match valid token characters" (SimplePatternTokenizer.java:33-34), so a maintainer may see a never-matching pattern as invalid configuration and prefer a clear IllegalArgumentException.
Why it fails: @lucene.experimental is about API stability, not correctness; no javadoc states that precondition; the CompiledAutomaton guard shows the case is known but is not a contract on RunAutomaton; Lucene's own MockTokenFilter and the pattern tokenizers construct run automata directly; and SimplePatternTokenizer is a public, string-configured API that reaches the crash with valid RegExp syntax. That javadoc sentence contrasts token characters with separators; it does not say the pattern must be able to match, and an AIOOBE from inside RunAutomaton is not a validation error. A cross-model reviewer (OpenAI GPT-5.6) built the API argument, rated it weak and voted real with high confidence; a second cross-model fact check rated the intended-behaviour case moderate because of the javadoc sentence and still found fix A the most defensible default.
Recommended A. Loop only over the source's real states
In the RunAutomaton constructor, loop over the source's real states (n < a.getNumStates()), not n < size. The placeholder state 0 already rejects everything, so a 0-state input gets the table makeEmpty() gets today; other automata are unchanged. Validated with gradle: the test fails before, passes after; 404 automaton, query and tokenizer tests show 0 failures.
// RunAutomaton.java:73
- for (int n = 0; n < size; n++) {
+ // size is max(1, numStates); only query states that exist in the source automaton. A 0-state
+ // automaton keeps the synthetic state 0 as non-accepting with all transitions = -1.
+ final int numStates = a.getNumStates();
+ for (int n = 0; n < numStates; n++) {All options and trade-offs3 options ยท 443 words
One validated option and two alternatives that were not run. A is the smallest change at the place that invents the extra state; B and C only move or narrow the guard.
Keep size = Math.max(1, a.getNumStates()) for the tables, but query the source only for states it has. The synthetic state 0 is already initialised as non-accepting with every transition -1 by new FixedBitSet(size) and Arrays.fill(transitions, -1).
// RunAutomaton.java:73
- for (int n = 0; n < size; n++) {
+ // size is max(1, numStates); only query states that exist in the source automaton. A 0-state
+ // automaton keeps the synthetic state 0 as non-accepting with all transitions = -1.
+ final int numStates = a.getNumStates();
+ for (int n = 0; n < numStates; n++) {Trade-offs
- For every automaton with at least one state
numStates == size: same table, same cost; constructor only, no hot-path change. - Fixes every caller at once: direct
CharacterRunAutomaton/ByteRunAutomatonusers, both pattern tokenizers and their factories.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF: the minimal test fails before the change and passes after it, with assertions on and off. :lucene:core:testoverorg.apache.lucene.util.automaton.*plusTestRegexp*,TestAutomatonQuery*,TestWildcard*,TestTermInSetQuery,TestFuzzyQuery,TestTermsEnum*: 28 classes, 322 tests, 4 skipped, 0 failures.:lucene:analysis:common:testoverorg.apache.lucene.analysis.pattern.*(incl.TestSimplePatternTokenizer,TestSimplePatternSplitTokenizer): 11 classes, 82 tests, 2 skipped, 0 failures.:lucene:core:checkGoogleJavaFormat: clean.- Earlier and independently: the study's 14 tests and Lucene's 220 prebuilt core automaton tests pass with it; the cross-model reviewer's
SimplePatternTokenizer("(a&b){2}")test andTestRegExp(9/9) pass with it.
Allocate at least 2 states[] slots even for 0 states in Automaton(int, int) (Automaton.java:92), the only place states[] is allocated (Builder.finish() uses it at :894; elsewhere it is only grown, :209, :359). Not run.
Trade-offs
It would cover every way of building a 0-state automaton, and with a 2-slot minimum only 0-state automata get more memory. But it hides the read of a state that does not exist instead of removing it (RunAutomaton would still read state 0 of a 0-state source and rely on spare zeros), and it changes a shared core constructor for a RunAutomaton problem. Not recommended.
Replace a 0-state automaton with a 1-state non-accepting one in SimplePatternTokenizer and SimplePatternSplitTokenizer before new CharacterRunAutomaton(dfa). Not run.
Trade-offs
Fixes the tokenizer path only; direct CharacterRunAutomaton / ByteRunAutomaton users keep the crash. Not recommended alone.
Downstream products that build a SimplePatternTokenizer from a user-configured pattern get an unchecked ArrayIndexOutOfBoundsException for a degenerate pattern such as (a&b){2}, not a validation error; queries are not affected.
Non-formal. A Jazzer fuzzer that compares Lucene regular expressions with java.util.regex and checks run automata against each other crashed on a repeated empty character class. An independent verifier reduced it to the 0-state automaton, mapped the triggers and releases, and tried a fix; the hunt cut it to one test and re-ran it on current main.
The method in full109 words
- The study's fuzzing unit ran
RegexDiffFuzzer(Jazzer): random regex syntax trees rendered both as LuceneRegExpand asjava.util.regexand compared, plus self-consistency acrossOperations.run,ByteRunAutomaton,CompiledAutomatonand the automaton operations. It reported the AIOOBE with([^\D\d]){2}andBuilder().finish()as triggers. - An independent verifier wrote 14 tests from scratch (triggers, controls, a survey of 15 constructions, the tokenizer and factory paths, query and highlighter controls), probed release jars, and validated the loop-bound fix against Lucene's 220 core automaton tests.
- The hunt minimised it to one method, re-ran it on main and 10.5.2, bisected the introducing commit on both branches, swept every release, and had a cross-model review.
No report found (2026-10-22 and 2026-11-01): GitHub issues and PRs incl. the JIRA archive, commits, mailing lists, downstream projects, the web, and the changed files of all 409 open PRs. Two open PRs edit the same constructor but keep the loop.
Searches4 searches
Closest, none this bug: #7021 (LUCENE-5959, introduced it; nothing about 0 states); #7116 (LUCENE-6054, #*, different code); #8517 (LUCENE-7465, added SimplePatternTokenizer; no empty-pattern discussion); #16564 and #13072 (open PRs on the same constructor, keep n < size); #11646 (a different RunAutomaton bug); #14224, #3791, #13714, #9334 (empty-language cases in other code); a downstream simple_pattern issue with a different failure ("please determinize the incoming automaton first"). Gap: GitHub search works on tokens and cannot match stack frames exactly.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene incl. the migrated JIRA archive, apache/lucene-solr) | 2026-10-22/23 and 2026-11-01 | about 30 study phrasings; 45 scripted queries and 49 extra searches: class names with ArrayIndexOutOfBoundsException / AIOOBE, "RunAutomaton.<init>", "Index 0 out of bounds for length 0" (paged to the end), no-states and empty-language phrasings; changed files of all 409 open PRs | no report; #16564 and #13072 touch the constructor without fixing it |
| Commits and JIRA archive | 2026-11-01 | 6 commit queries, 18 JIRA queries; LUCENE-5959, 6054, 7465, 8287 and 5752 read in full | no report |
| Mailing lists (dev@, java-user@, issues@, commits@, builds@) | 2026-11-01 | 20 scripted queries plus manual crash-trace queries | no report |
| Downstream projects and the web | 2026-10-22 and 2026-11-01 | 5 downstream queries over three search projects, 6 web queries | one unrelated simple_pattern issue |
Introduced by LUCENE-5959 (#7021, 2014-09-18), a memory optimization that made Builder.finish() allocate exact-size arrays; first released in 5.0.0. All 108 releases from 5.0.0 to 10.6.0 and main fail; 4.10.0 to 4.10.4 pass. Not fixed upstream.
Versions tested7 rows
- Introduced: LUCENE-5959, "Optimized memory management in Automaton.Builder.finish()" (#7021, svn era, no PR): trunk
0bf7d74eac7d(first released in 6.0.0) and the identical branch_5x backportba9a5fb56251, which 5.0.0 ships; both are labeled "LUCENE-5960: move CHANGES entry under 5.0". Bisected on both branches: the parent passes, the commit fails, assertions on and off. - Latent since 4.10.0: LUCENE-5752 added the int[]
Automaton,Builder, theMath.max(1, ...)synthetic state with itsn < sizeloop, and theCompiledAutomatonguard; the read of state 0 was harmless while every automaton had spare capacity. - Crash frame:
Automaton.stepup to 8.4.x,Automaton.nextfrom 8.5.0; the message is the same everywhere. - Upstream: the involved files are unchanged up to upstream main
f815e679bf6cof 2026-11-01 (a snapshot); the open PRs #16564 and #13072 keep the loop.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | fails with assertions on and off (gradle and standalone); passes with fix A |
| 10.6.0 | affected | fails with assertions on and off |
| 10.5.2 | affected | fails with assertions on and off |
| 5.1.0 to 10.5.x (106 releases) | affected | fail with the same AIOOBE (the 97 tagged ones in the sweep, assertions on; the 9 untagged 8.8.2 to 8.11.4 releases in a later run, assertions on and off) |
| 5.0.0 | first release | fails with assertions on and off |
| 4.10.0 to 4.10.4 | not affected | pass: Builder wraps new Automaton() with spare capacity |
ba9a5fb56251 / 0bf7d74eac7d (LUCENE-5959) | introducing commits | fail; their parents pass |
The study verifier, two re-runs on newer main, the minimal repro, the current-main verification and a cross-model reviewer (OpenAI GPT-5.6, own tests) all call it real; this doc's gradle runs and fact checks confirm the failure and the fix.
Checks and verdicts219 words
Gap: the cross-model reviewer ran on main ea57360912ed; the automaton files are the same at 645d4fe14a20.
| Check | Verdict | What it found |
|---|---|---|
| study verifier | real | Own 14 tests, 7 fail on main; release probe 4.10.4 OK, 5 sampled releases from 5.0.0 to 10.1.0 AIOOBE; fix passes. |
| main refresh a9779ef3c193 | still reproduces | Study tests, 14 run, the same 7 fail. |
| minimal repro | verified | One test; fails with assertions on and off and two seeds; the fix passes. |
| current-main verification | real | Main 645d4fe14a20 and 10.5.2 fail with assertions on and off; the fix passes. |
| cross-model review, refute lens | real, high | OpenAI GPT-5.6: own direct and SimplePatternTokenizer tests fail, controls pass, fix passes; intended-behaviour argument weak. |
| doc gradle runs | fails before, passes after | Scratch worktree of 645d4fe14a20; 322 core and 82 pattern-tokenizer tests with fix A: 0 failures (6 skipped). |
| independent fact check | real, as documented | Own repro runs on main, 10.5.2 and 10.6.0 (assertions on and off), the gradle command before and after fix A, trigger probes; two wording errors (regexp trigger scope, fix B trade-offs), both corrected. |
| cross-model fact check | real | OpenAI GPT-5.6, read-only: found the sweep had skipped 8.8.2 to 8.11.4 (since run: all fail) and asked for narrower query and factory wording; corrected. |
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 400 KB
Finding record, Lucene and upstream sources, repro files and diagram notes445 words
- Registry title
- RunAutomaton throws ArrayIndexOutOfBoundsException on a 0-state automaton
- Area
- util / automaton
- Priority
- Low, rank 1143: RunAutomaton throws ArrayIndexOutOfBoundsException when built directly on a 0-state automaton (queries are guarded by CompiledAutomaton), loud and needing a degenerate empty-language automaton.
- Impact class
- crash
- Impact note
- Only code that builds a CharacterRunAutomaton or ByteRunAutomaton itself from a 0-state automaton with exact-size arrays (Automaton.Builder, new Automaton(0, 0), Operations.repeat), or a SimplePatternTokenizer, SimplePatternSplitTokenizer or their factories given a never-matching bounded-repeat pattern such as (a&b){2}; queries are guarded by CompiledAutomaton; released since 5.0.0.
- Summary
- You build an automaton with zero states through Automaton.Builder and create a RunAutomaton from it. You expect a run automaton that accepts nothing. Instead the constructor throws ArrayIndexOutOfBoundsException.
- Introduced
- LUCENE-5959 (0bf7d74eac7d) (2014-09-18)
- First affected version
- 5.0.0
- Affected versions
- 5.0.0 to 10.6.0 (all 108 lucene-core releases in that range) and main 645d4fe14a20 [exec]; 4.10.0 to 4.10.4 not affected [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; also a cross-model review that agreed; not yet triaged by a person.
- Found by
- non-formal baseline: fuzzing/PBT
- Upstream and prior reports
- none found (study searches 2026-10-22; public-list check 2026-10-20..23)
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test through gradle with assertions on and off, before and after the recommended fix, in a scratch worktree (2026-11-01), and standalone (2026-10-29 and 2026-11-01); 10.5.2 release jars with assertions on and off; all 108 releases 5.0.0 to 10.6.0 (every lucene-core version Maven Central lists in that range: the 99 tagged in the history clone in the sweep, the 9 untagged 8.8.2 to 8.11.4 releases in a later run with assertions on and off) and 4.10.0 to 4.10.4; the LUCENE-5959 commits and their parents on trunk and branch_5x.
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/automaton/RunAutomaton.java:37-41, :55-83 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/automaton/Automaton.java:79-95, :144-146, :626-628, :675-694, :889-894 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:55-60; Operations.java:291-323; CompiledAutomaton.java:154-157 (at 645d4fe14a20)lucene/analysis/common/src/java/org/apache/lucene/analysis/pattern/SimplePatternTokenizer.java:75-101; SimplePatternSplitTokenizer.java:64-91; SimplePatternTokenizerFactory.java:73-75, :87-88 (at 645d4fe14a20)
- Who is affected
lucene/core/src/java/org/apache/lucene/util/automaton/CompiledAutomaton.java:154-157 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java:99, TermInSetQuery.java:185-195, FuzzyAutomatonBuilder.java:63, :73; lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:550-552, :642-643; lucene/sandbox/src/java/org/apache/lucene/sandbox/search/TermAutomatonQuery.java:491-493 (at 645d4fe14a20)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/automaton/RunAutomaton.java:37-41, :55-68; CharacterRunAutomaton.java:21-28; ByteRunAutomaton.java:22-38 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/automaton/Automaton.java:79-82; Automata.java:55-60; Operations.java:201-205 (at 645d4fe14a20)lucene/test-framework/src/java/org/apache/lucene/tests/analysis/MockTokenFilter.java:43-44 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/util/automaton/TestRegExp.java:107-117 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/util/automaton/RunAutomaton.java:68-83 (at 645d4fe14a20)
- Diagrams
lucene/core/src/java/org/apache/lucene/util/automaton/RunAutomaton.java:68-80; Automaton.java:79-92, :690-694, :889-894 (at 645d4fe14a20)
- TestRunAutomatonZeroStates.java:
repro/LFV-029/src/org/apache/lucene/util/automaton/TestRunAutomatonZeroStates.java(34 lines)
assets/LFV-029/bug.png(400 KB)
![Two automata with 0 states feed new CharacterRunAutomaton. The one from new Automaton.Builder().finish() has a states[] array of length 0; RunAutomaton sets size = max(1, 0) = 1, loops over the placeholder state 0 and reads states[0], which throws ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0. The one from Automata.makeEmpty() has a states[] array of length 4, so the same read returns 0 transitions and the run automaton rejects every string.](../assets/LFV-029/bug.png)