X* means "zero or more repetitions of X". Zero repetitions is the empty string, so X* always accepts it, even when X accepts nothing.
RegExp and RegexpQuery build a(a&b)*: the intersection comes out with 0 states, repeat returns it as is, and the concatenation and the query match nothing. Select the still to open it at full size. Generated with AI, checked against the code.- Index two documents with a
StringField"f": one with the empty term"", one witha. Search withRegexpQuery(default flags) for(a&b)*and fora(a&b)*. - No string is both
aandb, so(a&b)matches nothing and(a&b)*is zero repetitions: the empty string. You expect 1 hit for each query (""anda). - You get 0 hits for both, and no error.
#*and(#){0,}also reject"", while(a&b)?and(a&b){0,5}accept it. - Why: Lucene usually stores "matches nothing" as an automaton with 0 states, and
Operations.repeatreturns such an input unchanged instead of adding the empty string (Operations.java:202-205). A concatenation with an empty part is empty, soa(a&b)*matches nothing too. - The same empty language built with 1 state gives the right answer. The shortcut came from a 2014 crash fix (LUCENE-6054); 4.9.1 was correct.
Anyone whose regex or automaton stars (or {0,}s) a sub-expression that matches nothing and has 0 states, which is how #, an impossible & such as (a&b), an empty character class and Automata.makeEmpty() are built. Default flags; both query parsers too (/a(a&b)*/ gives 0 hits). Rare, but then the whole enclosing pattern silently matches nothing.
Mechanism, terms and numbers710 words
X* means zero or more repetitions of X, and zero repetitions is the empty string, so X* accepts "" even when X accepts nothing. Lucene usually represents "accepts nothing" as an automaton with 0 states (Automata.makeEmpty(), #, an impossible intersection such as (a&b), an empty character class), and Operations.repeat(Automaton) returns such an input unchanged. So #*, (a&b)* and (#){0,} accept nothing, and because a concatenation with an empty part is empty, a(a&b)* matches nothing at all: RegexpQuery and both query parsers' /.../ syntax silently return 0 hits where 1 is expected. The same empty language built with 1 state gives the correct {""}. The 0-state shortcut came from the 2014 LUCENE-6054 crash fix; every release from 4.10.3 to 10.5.2 and main is affected, and 4.9.1 was correct.
- Language: the set of strings an automaton or regex accepts. The empty language
{ }accepts nothing; the empty-string language{""}accepts exactly one string, the empty one. They are different. - Kleene star
X*: zero or more repetitions ofX. Zero repetitions always gives"", so for everyX,X*contains"". In particular{ }* = {""}. - How
Xcan be empty in Lucene's regex syntax (all enabled by default,RegExp.ALL):#("the empty language" in theRegExpgrammar), an impossible intersection such as(a&b)or([a-c]&[x-z]), an empty character class such as[^\s\S]. In code:Automata.makeEmpty(), an intersection with no common string,Automata.makeStringUnionof an empty list.
- Lucene usually represents the empty language as an automaton with 0 states:
Automata.makeEmpty()(Automata.java:55-60, also used for#),Operations.intersection(it returns a 0-state operand as is and ends withremoveDeadStates, which drops every state when nothing can reach an accept state;Operations.java:396-401, 436, 1007-1043),makeCharClasswith no ranges,makeStringUnionof an empty list. Operations.repeat(Automaton)starts with a shortcut (Operations.java:202-205atea57360912ed):
if (a.getNumStates() == 0) {
// Repeating the empty automata will still only accept the empty automata.
return a;
}The general path below it starts with a new accepting initial state (:212-215), so it accepts "". A second shortcut (:207-210) returns an input whose only accept state is its initial state; such an input already accepts "", so that shortcut is correct too. Only the 0-state shortcut returns an automaton that rejects "".
{0,}takes the same path:repeat(a, 0)callsrepeat(a)(:273-276), andRegExpmaps*and{n,}to these two methods (RegExp.java:665-671).?and{0,k}are correct, becauseoptionaladds a state when there is none (:177-180) andrepeat(a, 0, max)starts frommakeEmptyString()(:297-298).concatenatereturns the empty automaton as soon as one part has 0 states (:79-81). So the error spreads to the whole pattern:a(a&b)*=afollowed by "nothing" = nothing.RegexpQueryhands the automaton toCompiledAutomaton(AutomatonQuery.java:99), which replaces a 0-state automaton with a 1-state one that accepts nothing (CompiledAutomaton.java:154-157) and classifies it asAUTOMATON_TYPE.NONE(:166-168). The wrong language is kept, no term matches, and no error is raised.
| expression | should accept | main and 10.5.2 | states |
|---|---|---|---|
Operations.repeat(Automata.makeEmpty()) | "" | rejects | 0 |
repeat of a 1-state, non-accepting automaton (same empty language) | "" | accepts | 1 |
RegExp #*, (a&b)*, (#){0,}, [^\s\S]* | "" | reject | 0 |
RegExp (a&b)?, (a&b){0,5} (controls) | "" | accept | 1 |
RegExp a(a&b)* | "a" | rejects | 0 |
RegExp x([a-c]&[x-z])*y | "xy" | rejects | 0 |
RegExp a(a&b)? (control) | "a" | accepts | 2 |
RegexpQuery over the terms "" and a: (a&b)* and a(a&b)* count 0 hits each where 1 is expected; the controls a* = 2, (a&b)? = 1 and a(a&b)? = 1 are right. #+, (a&b)+ and (#){1,} correctly accept nothing.
Same language, different answer. An independent test compared repeat() with the textbook definition of L* on all 31 words over {a,b} of length 0 to 4, for 2,000 random automata with 0 to 4 states: all 384 of 384 0-state inputs were wrong, none of the 1,616 inputs with at least one state were, and 775 of those also had the empty language. A second seed gave 418 of 418 against 0 of 1,582.
Exposure and likelihood304 words
- Who is hit. Any code that stars an empty operand with 0 states. That is how
#, impossible intersections, empty character classes andAutomata.makeEmpty()represent the empty language; an empty operand with 1 or more states is handled correctly:new RegExp(String)(defaultRegExp.ALL, which enables#and&;RegExp.java:512-513) andRegexpQuery(Term)(RegexpQuery.java:54-55);- the classic and the flexible query parser, which both build
RegexpQuerywithRegExp.ALL(QueryParserBase.java:511-513,RegexpQueryNodeBuilder.java:45-50). Over the termsaandab,/a(a&b)*/counts 0 hits in both, where 1 is expected;/a(b)*/= 2 and/a(a&b)?/= 1 are right; - code that composes automata with the public
OperationsandAutomataAPI, where an operand can come out empty on its own: an intersection with no common string, a string union of an empty word list, an empty character class, or a difference whose left language is contained in the right.Operations.repeatof each of these rejects"".
- Default, not opt-in. No flag or setting is involved.
- Silent, and wider than the empty string. No exception, with assertions on and off. Because a concatenation with an empty part is empty, a whole pattern such as
prefix(<empty>)*suffixmatches no term at all. As a standalone positive query it only under-matches (no crash, no index damage); under a negation (aMUST_NOTclause,Operations.complement, a deny filter) the miss turns into an over-match. - How likely. Rare.
#*or(a&b)*is unlikely in a hand-written query; generated patterns and composed automata are the more plausible path. No report from any downstream user is known. - Scale. None needed:
#*is three characters. - Long-standing. Every release from 4.10.3 (2014) to 10.5.2, and main.
- Workaround. Write
(X)?or(X){0,k}instead of(X)*whenXmay be empty; in code, checkOperations.isEmpty(x)before callingrepeatand useAutomata.makeEmptyString().
The test shows that Operations.repeat(Automata.makeEmpty()), the Kleene star of the empty language, returns the empty language, so it rejects "".
/*
* 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 TestKleeneStarOfEmptyLanguage extends LuceneTestCase {
public void testStarOfEmptyLanguageAcceptsEmptyString() {
Automaton star = Operations.repeat(Automata.makeEmpty());
// expected: the star (zero or more repetitions) accepts "", i.e. true; actual: false
assertEquals("repeat(makeEmpty()) accepts \"\"", true, Operations.run(star, ""));
}
}
# in a copy of this finding's repro folder (repro/LFV-020/, 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.automaton.TestKleeneStarOfEmptyLanguage
- Expected vs actual
- Expected: the star (zero or more repetitions) accepts
""(true). Actual:false(expected:<true> but was:<false>).
More files, environment, full output36 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: repeat(makeEmpty()) accepts "" expected:<true> but was:<false>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.apache.lucene.util.automaton.TestKleeneStarOfEmptyLanguage.testStarOfEmptyLanguageAcceptsEmptyString(TestKleeneStarOfEmptyLanguage.java:26)- The javadoc promises "the Kleene star (zero or more concatenated repetitions)"; the
RegExpgrammar says*is "zero or more occurrences". - The shortcut came from a 2014 crash fix that never discussed the result; 4.9.1 returned the correct
{""}. - The answer depends on the state count, not the language: 384 of 384 0-state inputs are wrong, 0 of 1,616 others.
The full argument498 words
The contract, in its own words (at ea57360912ed):
Operations.repeat(Automaton)(Operations.java:196-197): "Returns an automaton that accepts the Kleene star (zero or more concatenated repetitions) of the language of the given automaton."repeat(Automaton, int)(:268-269): "acceptsminor more concatenated repetitions"; formin= 0 that includes zero repetitions.RegExpgrammar:*"(zero or more occurrences)" (RegExp.java:121),{n,}"(n or more occurrences)" (:142),?"(zero or one occurrence)" (:114),#"(the empty language)" (:230).- The package doc promises "all standard (and a number of non-standard) regular expression operations" and points to dk.brics (
package-info.java:33-34, 40-42). In #15182 a committer (rmuir) wrote that the syntax "should continue to match https://www.brics.dk/automaton/". Lucene 4.9.1'srepeat, which came from dk.brics, returns{""}here. optionalandrepeat(a, 0, max)in the same class already return{""}for the empty language.- No javadoc,
CHANGES.txtorMIGRATE.mdtext says the star of the empty language is empty.CHANGES.txtlists the change only as "LUCENE-6054: Allow repeating the empty automaton", under bug fixes (lines 11525 and 11719).
History.
- LUCENE-6054 (GitHub #7116, reported by nik9000) is a crash report: "RegExp.toAutomaton fails on #*", body "This throws an assertion error:
new RegExp("#*").toAutomaton(1000);". The fix (nik9000's patch, committed by mikemccand as54156c042d4on 2014-11-07, about 9 minutes after the issue was filed) added thereturn abranch. The comments are thanks, commit notices and a bulk close; nobody discussed what#*should accept. Its test (TestRegExp.testRepeatWithEmptyLanguage,TestRegExp.java:107-117) only checks thattoString()is non-empty ("paranoia"). - Before the fix, in 4.10.0-4.10.2, a 0-state input went down the general path: with assertions on it threw
AssertionError: state=0 nextState=0(the reported crash); with assertions off it returned the correct{""}. So the crash fix silently changed production results. Reverting only that hunk inside 4.10.3 restores the 4.10.2 result on all five bug checks (they throw again with assertions on; with them off they return{""}and the whole output is identical to 4.10.2's). With assertions on, one control differs: 4.10.2 also throws onrepeat(makeEmpty(), 0, 3)and the reverted 4.10.3 does not, because LUCENE-6046 rewrote the boundedrepeatin 4.10.3. - The tests that pin today's result came later, from optimisation work.
TestOperations.testRepeatEmptyLanguage(TestOperations.java:304-309) assertsassertSame(input, Operations.repeat(input))formakeEmpty(): jpountz added the assertion in #13714 (2024-09-05, "Make Operations#repeat create simpler automata"), and rmuir moved it into its own test in #14218 (2025-02-13, dead-state clean-up), addingAutomatonTestUtil.assertMinimalDFA(actual), which treats the 0-state result as the expected minimal DFA.testDuelRepeat's oraclenaiveRepeat, labelled "the original implementation of Operations#repeat" (:487-491), copies the samereturn a, so it cannot catch this. The study read both PRs and their review comments: nobody discusses what the star of the empty language should be.
Tests. With the fix, the only test failures in all of :lucene:core:test and six modules that use RegExp are those two pinning tests; with them updated, everything passes (see the fix section).
The devil's-advocate check332 words
An independent reviewer from another model family (OpenAI GPT-5.6), asked to refute the claim, made the strongest case for "intended" (verdict not real, high confidence): the 0-state branch has an explanatory comment; it was added on purpose, with a CHANGES entry; a dedicated assertSame test (with assertMinimalDFA on the result) and a copied oracle pin it; two later reviewed PRs (2024, 2025) kept and strengthened it; and these APIs are @lucene.experimental. It called the behaviour "conclusively intentional in the current implementation". The reviewer itself reproduced the bug, agreed on the root cause and that the inputs are valid, and conceded that the history "does not articulate why empty-language star should depart from the documented operation" and that #13714 and #14218 "were not semantic discussions".
Why it fails (the second adjudication, verdict real):
- The artifacts show that the branch and its result were chosen and kept on purpose: the comment states the result, and
assertSame,assertMinimalDFAand the copied oracle preserve it. None of them explains the result or reconciles it with the documented Kleene star; there is no "by design" or "won't fix" anywhere. - The branch comes from a crash fix committed minutes after the report, whose own test never checks what is accepted; the pinning tests come from an optimisation and a clean-up, and the oracle copies "the original implementation".
- The code comment is not API documentation, gives no reason, and contradicts the javadoc six lines above it; all public docs say the opposite.
- The result depends on the number of states, not on the language (384 of 384 against 0 of 1,616). A rule about languages cannot split results that way; if the split is deliberate, it is a rule about representations, and the javadoc defines
repeatby the language alone. @lucene.experimentalallows API changes, not results that contradict the API's own javadoc.- Before 2014, production code returned the correct
{""}; the change was announced only as "Allow repeating the empty automaton".
Verdict: not intended, a defect.
Recommended Return {""} for a 0-state input
In Operations.repeat(Automaton), return Automata.makeEmptyString() instead of the 0-state input, and update the two tests that pin the old result (testRepeatEmptyLanguage and the naiveRepeat oracle). With it the repro passes 7 of 7, the whole :lucene:core suite runs with 0 failures (8,873 tests, 322 of them skipped), and six modules that use RegExp pass.
public static Automaton repeat(Automaton a) {
if (a.getNumStates() == 0) {
// Zero repetitions are always allowed: the Kleene star of the empty language is {""}.
return Automata.makeEmptyString();
}All options and trade-offs3 options ยท 507 words
One option was validated with gradle in a scratch worktree at ea57360912ed; the other two are alternatives that were not run. The scratch worktree was removed after the runs.
One line in Operations.repeat(Automaton) plus the two pinning tests in TestOperations. testRepeatEmptyLanguage then asserts that the result is a minimal DFA with the same language as Automata.makeEmptyString(), and naiveRepeat gets the same one-line change. A regression test for TestRegExp (#*, (a&b)*, (#){0,} accept only ""; a(a&b)* accepts a) is in the repro section.
public static Automaton repeat(Automaton a) {
if (a.getNumStates() == 0) {
// Zero repetitions are always allowed: the Kleene star of the empty language is {""}.
return Automata.makeEmptyString();
}Trade-offs
- Matches the javadoc, the
RegExpgrammar,optional,repeat(a, 0, max), the 1-state empty automaton and 4.9.1.makeEmptyString()is a 1-state minimal DFA. Cost: one new 1-state automaton per call on a 0-state input instead of returning the input, a small constant; not benchmarked. #+,(a&b)+and(#){1,}stay empty, as they should.- Query compilation changes with it:
(a&b)*anda(a&b)*compile toAUTOMATON_TYPE.SINGLE(the term""and the terma) instead ofNONE(executed). - Visible change: patterns with a starred empty sub-expression match more afterwards (the empty term, or the whole enclosing concatenation). Worth a CHANGES line.
repeat()no longer returns the same instance for an empty input; in-tree only theassertSametest relied on that.
What the checks validated
- The repro: stock 7 tests, 6 failed; with the fix 7 of 7 pass. The
TestRegExpregression test fails on stock (assertions off) and passes with the fix. - 34 core classes (
util.automaton.*,TestRegexp*,TestAutomatonQuery*,TestWildcard*,TestPrefix*,TestTermsEnum*, the DocValues and FieldCache rewrite methods,TestQueryBuilder,TestMatchesIterator,TestDisjunctionMaxQuery), seedDEADBEEF: stock 388 tests, 6 failures (all in the repro); code fix only: exactly 2 failures,testRepeatEmptyLanguageandtestDuelRepeat; with the 2-test update: 0 failures. - Assertions off, seed
1234ABCD(util.automaton.*,TestRegexp*,TestAutomatonQuery*): 267 tests, 0 failures. - The whole
:lucene:core:test(fix, both test changes and the repro): 748 classes, 8,873 tests, 0 failures, 322 skipped. - queryparser 533, highlighter 722, monitor 181, queries 512, memory 51 and suggest 236 tests: 0 failures. Not run:
analysis.testsand the other modules. :lucene:core:checkGoogleJavaFormatpasses; theTestRegExpmethod was formatted withtidy.
Map * and {0,} of an empty operand to makeEmptyString() in RegExp.
Trade-offs
Fixes regex users but leaves the public Operations.repeat wrong, and dependent on the state count, for code that composes automata. Not recommended.
Amend the javadoc and the RegExp grammar.
Trade-offs
The text would have to say that the result depends on the number of states (0 states: empty; 1 dead state: {""}) and make an exception for * and {n,}. It would contradict optional, repeat(a, 0, max), dk.brics and 4.9.1.
Differential fuzzing with Jazzer: an AI tool ran a target that renders random regex syntax trees both as a Lucene RegExp and as a java.util.regex pattern and compares them, plus consistency checks across Operations. A separate AI verifier wrote its own tests and traced the cause to the LUCENE-6054 fix.
The method in full158 words
Method: differential fuzzing (Jazzer 0.30.0), the study's non-formal baseline tier, 2026-10-22, main 7cecf79523.
- An AI tool ran the
RegexDiffFuzzertarget: one random regex syntax tree is rendered both as a LuceneRegExpand as ajava.util.regexpattern and the two are compared, with self-consistency checks acrossOperations(run, complement, intersection, union, reverse, minimize). About 145 k executions in 5 runs produced this bug and three other automaton and regex findings. - A separate AI verifier wrote its own 10-test JUnit (9 fail; the in-test controls pass), traced the history to LUCENE-6054, ran 4.9.1 (correct) and 4.10.4, 5.0.0, 10.1.0, 10.5.2 and main (wrong), and tested the fix against 210 Lucene tests.
- On 2026-10-26 the hunt re-ran it on
ea57360912ed(-ea,-da, 10.5.2, 9.12.3), obtained a cross-model refute review and a second adjudication, built the release matrix with-eaand-da, and searched for prior reports. For this page the fix was validated with gradle.
No report found in GitHub issues and PRs, the JIRA archive, the dev@, java-user@ and issues@ lists, downstream trackers or the web (71 tool queries plus manual searches, 2026-10-26). Not fixed upstream. The closest item is LUCENE-6054 (#7116), the crash whose fix caused it.
Searches5 searches
No report of this bug was found, and upstream has not fixed it. Nothing says that #*, (a&b)* or (#){0,} should accept "", or that a(a&b)* or RegexpQuery("(a&b)*") matches nothing. Closest items, none of them this bug: #7116/LUCENE-6054 (the crash whose fix caused it), #13714 and #14218 (pinned the result in tests), #15182 (open: empty alternatives in parsing, and the dk.brics statement), #14224 (empty [] class parsing), #16816 (open: rewrites a NONE-type AutomatonQuery to MatchNoDocsQuery and leaves repeat alone), #13706/#13707 (isTotal on repeat), #8970, #3791 (LUCENE-2717) and #2680 (LUCENE-1606).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene), via a scripted prior-report search | 2026-10-26 | 30 queries: Operations, RegExp, Automata, RegexpQuery, repeat, makeEmpty, makeEmptyString; "Kleene star", "empty language", "empty automaton", "repeating the empty", testRepeatEmptyLanguage, naiveRepeat; "star of the empty language", "accepts the empty string", "matches nothing" | no report; LUCENE-6054/#7116 came back at rank 5 |
| Commits, downstream, mailing lists, JIRA, web (same tool) | 2026-10-26 | 4 commit queries, 3 downstream, 24 on dev@, java-user@, issues@ and commits@, 4 in the JIRA archive, 6 web; 71 queries in total, 0 errors, 2,813 distinct hits, comments of the top 20 read | nothing about this bug |
| Manual searches | 2026-10-26 | GitHub ("empty language", Kleene, repeat empty in:title, "empty automaton" in:title, intersection) on apache/lucene, two downstream search-engine trackers and apache/lucenenet; 11 JQL queries in the JIRA archive; lists.apache.org, all dates; 10 web searches | unrelated items and textbook material only |
| Open PRs and branches | 2026-10-26 | changed files of the 412 open PRs updated since 2025-06-01; Operations.java history on main, branch_10x, branch_10_6 and branch_10_5; CHANGES.txt and MIGRATE.md at ea57360912ed | the 0-state branch is on all four branches; only #14389, #14350 and #14360 touch the automaton classes, none of them repeat |
| Earlier searches | 2026-10-22 and 2026-10-26 | the study (about 22 GitHub queries, dev@, web) and the candidate step | no report |
Introduced by the LUCENE-6054 crash fix (2014-11-07, 54156c042d4 and its backports): wrong in every release run from 4.10.3 to 10.5.2 and in main, with assertions on and off. 4.9.1 was correct; 4.10.0-4.10.2 threw with assertions on and were correct with them off.
Versions tested6 rows
LUCENE-6054 ("allow repeating the empty automaton") added the same 4-line hunk on three branches on 2014-11-07: fed5251becd (lucene_solr_4_10, first released in 4.10.3), c349c30655d (branch_5x, 5.0.0) and 54156c042d4 (trunk, 6.0.0). Its precondition was LUCENE-5752 (46132142cc2 on branch_4x, first released in 4.10.0; trunk ec2eaaafad8), which made makeEmpty() a 0-state automaton. The branch is present at all 100 release tags from 4.10.3 to 10.5.2; git blame on main still attributes it to 54156c042d4.
Every release row ran a main() probe (5 checks; 4 controls, 3 on 4.9.1, whose makeEmpty() already has 1 state) on the release's Maven Central jars, SHA-1 checked, with -ea and -da. The main row ran the same probe on the locally built jar; the fix row is the JUnit and gradle runs.
| Version or commit | Verdict | Result |
|---|---|---|
main ea57360912ed (unreleased 11.0) | affected | wrong with -ea and -da; the JUnit repro fails 6 of 7 |
| 4.10.3 to 10.5.2 (20 releases run: 4.10.3, 4.10.4, 5.0.0, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0-8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) | affected | wrong with -ea and -da: all 5 checks false, all 4 controls pass |
| 4.10.0, 4.10.1, 4.10.2 | crash with -ea | -ea: AssertionError state=0 nextState=0 (the LUCENE-6054 crash); -da: correct |
| 4.9.1 | not affected | correct with -ea and -da |
| 4.10.3 with only the LUCENE-6054 hunk reverted | revert check | the 5 bug checks match 4.10.2 again (throw with -ea, correct with -da); the -da output is identical |
main ea57360912ed with the fix | fix | the repro passes 7 of 7; all core tests pass with the 2-test update |
The study's verifier, the public-list rerun, the hunt's re-verification and a second adjudication confirmed it. A cross-model refute review reproduced it but argued it is intended; the second adjudication weighed that case and ruled it a real bug.
Checks and verdicts416 words
Each check worked from the code or its own runs. The only disagreement was on intent, and it is shown in full in "Why it is a bug".
| Check | Verdict | What it found |
|---|---|---|
| Study verifier (fuzzing/PBT) | new bug | Its own 10 tests: 9 fail, the in-test controls pass. Traced it to LUCENE-6054, ran 4.9.1 (correct) and 4.10.4, 5.0.0, 10.1.0, 10.5.2 and main (wrong), and tested the fix against 210 Lucene tests. |
| Public-list rerun | reproduces on b83101cdd2 | 10 tests, 9 failures, the same tests as the study. |
| Hunt verification | real | The 7-test JUnit fails 6 of 7 on main with -ea and -da, on 10.5.2 with both, and on 9.12.3; the one-line fix makes it pass. |
| Cross-model refute review | not real (intent only) | An independent reviewer from another model family (OpenAI GPT-5.6) wrote and ran its own tests: they fail on main with -ea and -da and with three seeds, and pass with the fix. It agreed on the root cause and that the inputs are valid, but judged the result intended because of the code comment and the pinning tests. The repro driver re-ran its tests with the same result. |
| Second adjudication | real, high confidence | Wrote an independent reference-semantics test (384 of 384 0-state inputs wrong, 0 of 1,616 others), ran both query parsers (0 hits instead of 1), ran 4.9.1, 4.10.0, 4.10.2 and 4.10.3 with -ea and -da, read LUCENE-6054, and ran 24 Lucene test classes with the fix (277 of 277 with the 2-test update). |
| History follow-up | introduced by LUCENE-6054, first released in 4.10.3 | 24 releases plus main with -ea and -da, and a single-hunk revert inside 4.10.3. |
| Prior-report follow-up | new | 71 tool queries plus manual searches; nothing found. |
| This page's own runs | reproduced; fix validated | Gradle in a scratch worktree: stock 6 of 7 fail (also with assertions off); with the fix and the 2-test update the whole core suite (8,873 tests, 0 failures, 322 skipped) and six RegExp-using modules pass. The plain program gives the same table on main, with -da and on 10.5.2. |
| Fact checks of this page | bug stands; wording fixed | An independent fact check (re-ran every repro), a cross-model fact check (OpenAI GPT-5.6) and a diagram check. None changed a conclusion; their wording findings (the revert check, the LUCENE-5752 commit, test-control and skipped-test counts, the trigger condition, the LFV-029 note, the cost of the fix) are fixed here, as listed in the hunt's record. |
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 491 KB
Finding record, Lucene and upstream sources, repro files and diagram notes470 words
- Registry title
- The Kleene star of the empty language returns the empty language instead of {""}
- Area
- util / automaton
- Priority
- Low, rank 918: The Kleene star of the empty language returns the empty language instead of the empty string, so patterns like (a&b)* match nothing, a near-contrived input with a trivial fix.
- Impact class
- silent wrong results
- Impact note
- Default configuration; only regexes or automata whose repeated operand is the empty language, such as (a&b)*; released since 4.10.3.
- Summary
- You build a RegExp or call Operations.repeat on an operand whose language is empty, such as (a&b)*. You expect the result to accept the empty string, since the star of any language contains it. Instead the result accepts nothing.
- Introduced
- LUCENE-6054 fix 54156c042d4 (#7116) added the
return athat makes the star of the empty language empty; LUCENE-5752 (ec2eaaafad8) had made makeEmpty() 0-state [read] (2014-11-07 (54156c042d4)) - First affected version
- 4.10.3
- Affected versions
- 4.10.3+ [read: LUCENE-6054 fixVersions]; 4.10.4, 5.0.0, 10.1.0, 10.5.2 and main [exec]; 4.9.1 correct [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 disputed it (the study's other checks rated it real); 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
ea57360912ed(commit 2026-10-24; tested 2026-10-26): the JUnit repro through the repro runner with-eaand-daand through gradle in a scratch worktree, stock and with the fix (the worktree was removed after the runs); a plain-Java probe on main (-ea,-da), 10.5.2 and the fix; the Maven Central jars of 24 releases from 4.9.1 to 10.5.2 with-eaand-da(SHA-1 checked). The study reproduced it on7cecf79523and the public-list rerun onb83101cdd2; upstream main HEAD was stillea57360912edon 2026-10-26.
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java at ea57360912ed (lines 79-81, 177-180, 196-215, 268-276, 297-298, 396-401, 436, 1007-1043)lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java at ea57360912ed (lines 55-60, 140-142, 159-166, 641-643); RegExp.java (lines 114, 121, 142, 230, 665-671, 698-699)
- Who is affected
lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:512-513; lucene/core/src/java/org/apache/lucene/search/RegexpQuery.java:54-55 (at ea57360912ed)lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:511-513; lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/RegexpQueryNodeBuilder.java:45-50lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java:377-383 at ea57360912ed (the deprecated minus: makeEmpty() or intersection(a1, complement(a2)))
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/automaton/Operations.java:196-205, 268-269 at ea57360912ed; RegExp.java:114, 121, 142, 230; package-info.java:33-34, 40-42lucene/core/src/test/org/apache/lucene/util/automaton/TestOperations.java:304-309, 477-491; TestRegExp.java:107-117 (at ea57360912ed)lucene/CHANGES.txt:11525, 11719 (at ea57360912ed)
- TestKleeneStarOfEmptyLanguage.java (JUnit test):
repro/LFV-020/src/org/apache/lucene/util/automaton/TestKleeneStarOfEmptyLanguage.java(28 lines)
assets/LFV-020/bug.png(491 KB)
