A negative min passes the method's only argument check, keeps its minus sign as text, and the builder reads that sign as a digit.
- An automaton is a small state machine that accepts or rejects strings. Lucene builds them for regexp, wildcard and fuzzy queries, and
AutomatonQuerymatches the terms an automaton accepts. Automata.makeDecimalInterval(min, max, digits)builds one for the decimal numbers frommintomaxwritten as text:(0, 5, 0)accepts "0" to "5". Withdigits = 0leading zeros are allowed ("05"); withdigits > 0every string has exactly that many digits.- The regexp syntax
<0-5>is built with this method.
makeDecimalInterval(-1, 5, 0) reads the minus sign of "-1" as a digit, and which strings the resulting automaton accepts and rejects compared with (0, 5, 0). Select the still to open it at full size. Generated with AI, checked against the code.- Call
Automata.makeDecimalInterval(0, 5, 0): the automaton accepts "0" to "5" (and "05") and rejects "6" and "12". - Call
makeDecimalInterval(-1, 5, 0). The javadoc names only two rejected inputs (min > max, too few digits), so you expect anIllegalArgumentException, or the non-negative part of [-1, 5], which is 0 to 5 again. - You get no exception and an automaton that rejects "0", "1" and "4" and accepts "12", "49", "-1" and "-5" (of the numbers 0 to 5, only "5" comes out right): wrong under both expectations.
- Why:
minbecomes the string "-1", and the digit-by-digit builder treats '-' as a digit just below '0'. So it builds the two-character strings "00" to "49" where "0" to "4" belong. - Some all-negative ranges, such as
makeDecimalInterval(-15, -1, 0), throwStringIndexOutOfBoundsException: Index 2 out of bounds for length 2; others, such as(-5, -1, 0), return without error an automaton that accepts "-1", "-5" and "0-1". Every release since 4.0.0-ALPHA behaves this way.
Only application code that calls the experimental Automata.makeDecimalInterval itself with a negative min, for example an "all ints" range (Integer.MIN_VALUE, Integer.MAX_VALUE, 0), which then rejects every number up to five digits. Query text cannot reach it. The output is silently wrong; no index data, no resource growth. Rare.
Mechanism, terms and numbers715 words
Automata.makeDecimalInterval(min, max, digits) builds an automaton that accepts the decimal numbers from min to max written as text; its javadoc says it accepts "non-negative integers in the given interval" and names two cases that throw IllegalArgumentException: min > max, and max not fitting in digits. A negative min is in neither list and is not checked. The code turns it into a string with Integer.toString, keeps the -, and builds the automaton digit by digit as if - were a digit just below 0.
For (-1, 5, 0) the call returns without error an automaton that rejects "0" to "4" and accepts "12", "49", "-1" and "-5". For an "all ints" range (Integer.MIN_VALUE, Integer.MAX_VALUE, 0) it rejects every digit string of up to five characters. Some all-negative ranges, such as (-15, -1, 0), throw StringIndexOutOfBoundsException instead; others, such as (-5, -1, 0), silently accept "-1", "-5" and "0-1". RegExp, the only non-test caller inside Lucene, cannot pass a negative bound, so only applications that call the experimental Automata API directly are affected. The check condition has not changed since the code was imported in 2010. A one-condition fix (reject min < 0) passes the repro, the automaton tests and check -x test.
- No check for a negative
min.String x = Integer.toString(min); String y = Integer.toString(max);(:504-505), then the only check,if (min > max || (digits > 0 && y.length() > digits)) throw new IllegalArgumentException();(:506-508). For(-1, 5, 0):x = "-1",y = "5", no exception. - Padding keeps the sign. Both strings are left-padded with
'0'todcharacters,d = digits, ory.length()whendigits <= 0(:509-523). Hered = 1andxis already two characters long, so nothing changes. Padding only lengthens; it never removes the-. - The builder assumes two digit strings of equal length.
between(builder, x, y, 0, initials, digits <= 0)(:534); its javadoc says the length ofx.substring(n)"must be equal to y.substring(n).length()" (:232-236). - '-' becomes a digit. At position 0,
cx = '-'(0x2D, just below'0') andcy = '5'(:251-252). They differ, sobetween()adds (:256-261): a transition on'-'intoatLeast("-1", 1), which accepts "-1" to "-9"; a transition on'5', which accepts "5"; and a transition on the range'.'to'4'intoanyOfRightLength("-1", 1)(:182-190), which wants one more digit. So "00" to "49", and "." or "/" followed by one digit (".0" to "/9"), are accepted, and "0" to "4" alone are not. - Leading zeros. With
digits <= 0a'0'loop is put in front (:538-544), so "05" is accepted too. - Some all-negative ranges throw. For
(-15, -1, 0):x = "-15",y = "-1",d = 2, andxstays three characters long.between()matches'-'and'1', then readsy.charAt(2)(:252) on a two-character string:StringIndexOutOfBoundsException: Index 2 out of bounds for length 2. This happens only when the paddedyis a proper prefix ofx:between()stops at the end ofx(:245) and readsyonly while the two agree (:251-254). Other all-negative ranges return without error:(-5, -1, 0)accepts "-1", "-5" and "0-1";(-1, -1, 0)and(Integer.MIN_VALUE, -1, 0)accept "-1" and "0-1".
(0, 5, 0): accepts 0, 1, 4, 5, 05; rejects 6, 12, 49, -1, -5, 12345 (the control).(-1, 5, 0): accepts 5, 05, 12, 49, -1, -5; rejects 0, 1, 4. Exhaustively over all strings of length 0 to 5 on-./0123456789, it accepts 292 strings outside the documented language. With a fixed width,(-1, 5, 2)accepts "00" to "05" correctly, but also "-1" to "-9" and ".0" to "/9".(Integer.MIN_VALUE, Integer.MAX_VALUE, 0): rejects all 111,110 digit strings of length 1 to 5, "0" and "5" included.(-15, -1, 0)and(-100, -1, 0):StringIndexOutOfBoundsException.(-5, -1, 0): no exception; accepts "-1", "-5" and "0-1".- Assertions on or off make no difference.
Exposure and likelihood306 words
- Who is hit. Application code that calls the public static
Automata.makeDecimalIntervaldirectly with a negativemin: an "all ints" range, or a lower bound computed from data. The class is@lucene.experimental. - Not reachable from queries.
RegExp.toAutomatonis the only production caller (RegExp.java:728-729). Its<n-m>parser rejects a-at the start, at the end, or a second-(RegExp.java:1474) and turns aNumberFormatExceptionintoIllegalArgumentException(:1489-1490), so it never passes a negative bound:new RegExp("<-1-5>")throwsIllegalArgumentException: interval syntax error at position 5.RegexpQueryand the query parsers go throughRegExp. - What goes wrong. Mostly no exception, and an automaton with the wrong language. For
(-1, 5, 0)it is wrong in both directions: in-range values are missing, values abovemaxand strings with-are present; for(-1, 5, 2)nothing in range is missing, but strings with-,.or/are added. Whatever the caller builds from it, for example anAutomatonQuery, uses that language. Some all-negative ranges, such as(-15, -1, 0), fail loudly with an undocumentedStringIndexOutOfBoundsExceptioninstead; others, such as(-5, -1, 0), silently accept "-1", "-5" and "0-1". - Limits. Deterministic, no index data, no shared state. No resource blow-up relative to the
min = 0control: in the probe (digitsup to 1,000) the largest negative-minautomaton had 1,079 states against 1,003 for the control with the samedigits, and every call took under 10 ms. Both grow withdigits, which the caller chooses, so these are not general bounds. - Likelihood: rare. The javadoc's "non-negative integers" steers careful callers away, nothing in Lucene passes a negative
min, and no earlier report was found. One downstream direct caller is known, but whether it can pass a negative value was not checked.
One JUnit test in lucene/core, public API only: it builds makeDecimalInterval(-1, 5, 0) and passes if the call throws IllegalArgumentException, or if the automaton accepts "0" and rejects "12". Deterministic; fails with assertions on and off.
/*
* 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 java.util.List;
import java.util.stream.Stream;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestMakeDecimalIntervalNegativeMin extends LuceneTestCase {
public void testNegativeMin() {
Automaton a;
try {
a = Automata.makeDecimalInterval(-1, 5, 0);
} catch (IllegalArgumentException acceptable) {
return;
}
CharacterRunAutomaton run =
new CharacterRunAutomaton(
Operations.determinize(a, Operations.DEFAULT_DETERMINIZE_WORK_LIMIT));
// expected IllegalArgumentException, or "0" (in [-1, 5]) accepted and "12" (> max) rejected
assertEquals(
"accepted among [0, 12]", List.of("0"), Stream.of("0", "12").filter(run::run).toList());
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20 (or any commit with the same Automata.java)
cp $REPRO/TestMakeDecimalIntervalNegativeMin.java lucene/core/src/test/org/apache/lucene/util/automaton/
./gradlew :lucene:core:test --tests org.apache.lucene.util.automaton.TestMakeDecimalIntervalNegativeMin -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
- Expected
- An
IllegalArgumentException, or an automaton that accepts "0" and rejects "12", asmakeDecimalInterval(0, 5, 0)does. - Observed
- No exception; the automaton rejects "0" and accepts "12" (
expected:<[0]> but was:<[12]>), on main with assertions on and off, and on 10.6.0.
More files, environment, full output315 words
The test sits in the automaton test package, as Lucene's own automaton tests do, but uses no package-private member. Nothing in it is randomized; seeds DEADBEEF and 5EED1234 give the same failure. Its assertion holds for both expectations (an exception, or the non-negative part of the interval), so it fits either fix.
As the regression test it needs one lint change: Lucene's ecj settings make an unused catch parameter an error (gradle/validation/ecj-lint/ecj.javadocs.prefs:136), so it has to be annotated as QueryParserBase.java:803-805 does, in the layout google-java-format requires:
} catch (
@SuppressWarnings("unused")
IllegalArgumentException acceptable) {With that change it fails on stock, passes with the recommended fix, and :lucene:core:check -x test (google-java-format, ecj lint including its javadoc rules, forbidden APIs) passes with it in place.
The second symptom, StringIndexOutOfBoundsException for (-15, -1, 0), has the same root cause and the same fix; it is left out of the test.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0 release jars with this test |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
TestMakeDecimalIntervalNegativeMin > testNegativeMin FAILED
java.lang.AssertionError: accepted among [0, 12] expected:<[0]> but was:<[12]>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:70C4BD88EB8BECE2]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:120)
at org.apache.lucene.util.automaton.TestMakeDecimalIntervalNegativeMin.testNegativeMin(TestMakeDecimalIntervalNegativeMin.java:36)
:lucene:core:test (FAILURE): 1 test, 1 failure
# the same message with -Ptests.asserts=false, and on the 10.6.0 release jars
# probe on the same main (assertions on and off give the same lines):
makeDecimalInterval(0, 5, 0): accepts [0, 1, 4, 5, 05], rejects [6, 12, 49, -1, -5, 12345]
makeDecimalInterval(-1, 5, 0): accepts [5, 05, 12, 49, -1, -5], rejects [0, 1, 4, 6, 12345], same language as (0, 5, 0): false
makeDecimalInterval(-15, -1, 0): throws java.lang.StringIndexOutOfBoundsException: Index 2 out of bounds for length 2
makeDecimalInterval(-2147483648, 2147483647, 0): accepts [], rejects [0, 1, 4, 5, 6, 05, 12, 49, -1, -5, 12345], same language as (0, 2147483647, 0): false
new RegExp("<0-5>").toAutomaton(): ok
new RegExp("<-1-5>").toAutomaton(): throws java.lang.IllegalArgumentException: interval syntax error at position 5- The method checks its arguments and documents exactly two rejected inputs,
min > maxand too few digits;min < 0is not one of them. - Unless negative input is outside the contract, the output is wrong: for [-1, 5] it rejects "0" and accepts "12", above
max. - It breaks the builder's own precondition (equal-length digit strings), hence the
StringIndexOutOfBoundsException.
The full argument186 words
- The documented contract. "Returns a new automaton that accepts strings representing decimal (base 10) non-negative integers in the given interval."
@param min minimal value of interval.@exception IllegalArgumentException if min > max or if numbers in the interval cannot be expressed with the given fixed number of digits(Automata.java:490-499). - Wrong unless a negative
minis outside the contract. The non-negative integers in [-1, 5] are 0 to 5, which is what(0, 5, 0)returns. The automaton rejects 0 to 4 and accepts 12, so even the most lenient reading ("only the non-negative part counts") gets a wrong answer, not a narrower one. - Tests cover only
min >= 0.TestAutomaton.testIntervalRandomdrawsminfrom 0 to 100,000 (TestAutomaton.java:594), the random-operations test from 0 to 999 (:1105), and every other test call uses fixed non-negative bounds (TestAutomaton.java:342, 577, 586,TestRegExpParsing.java:474, 487,TestAutomatonQuery.java:134, 138). No test, comment or discussion says negative bounds are unsupported. - Precedent. GITHUB#12427 (9.9.0):
Automata#makeStringUnionnow "explicitly throw[s] IllegalArgumentException if input data is not properly sorted instead of relying on assert" (lucene/CHANGES.txt:3121-3123).
The devil's-advocate check147 words
The strongest case (rated moderate by the cross-model reviews): "non-negative integers" in the summary is an implied precondition; RegExp documents numeric intervals as "specified by non-negative decimal integers" (RegExp.java:347-348) and enforces it; every caller and test obeys it; the class is experimental; the code is an unchanged port of dk.brics.automaton, whose makeInterval has the same check. So a negative min is garbage in, garbage out.
Why it fails: the method does validate its arguments, and its documented list of rejected inputs leaves min < 0 out, so a caller gets no signal. An input that passes the method's own check should not yield an automaton that rejects "0" for [-1, 5], or a StringIndexOutOfBoundsException. The argument is a good reason to treat this as low-impact missing validation and to let maintainers choose between rejecting and clamping; it does not make the current output correct.
Recommended Reject a negative min with the existing IllegalArgumentException
Add min < 0 || to the existing IllegalArgumentException check and name the case in the @exception javadoc (one code line). Validated on main: the repro passes with assertions on and off, the 258 automaton tests pass, and check -x test passes. Alternative, also run: treat a negative min as 0 (empty automaton when max < 0).
- * @exception IllegalArgumentException if min > max or if numbers in the interval cannot be
- * expressed with the given fixed number of digits
+ * @exception IllegalArgumentException if min < 0, if min > max or if numbers in the
+ * interval cannot be expressed with the given fixed number of digits
@@
- if (min > max || (digits > 0 && y.length() > digits)) {
+ if (min < 0 || min > max || (digits > 0 && y.length() > digits)) {
throw new IllegalArgumentException();All options and trade-offs3 options · 659 words
Both options were run in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF), removed afterwards. "Automaton tests" means :lucene:core:test --tests 'org.apache.lucene.util.automaton.*' --tests org.apache.lucene.search.TestAutomatonQuery --tests org.apache.lucene.search.TestRegexpQuery. The choice is the maintainers': reject a negative min, or accept it as 0.
Reject a negative min with the existing IllegalArgumentException (validated by the checks) (recommended)
In Automata.makeDecimalInterval (Automata.java:498-499, 506): one code line and two javadoc lines. It matches the summary sentence ("non-negative integers") and the RegExp interval syntax, and covers both symptoms: (-1, 5, 0), (-15, -1, 0) and (Integer.MIN_VALUE, Integer.MAX_VALUE, 0) all throw IllegalArgumentException.
- * @exception IllegalArgumentException if min > max or if numbers in the interval cannot be
- * expressed with the given fixed number of digits
+ * @exception IllegalArgumentException if min < 0, if min > max or if numbers in the
+ * interval cannot be expressed with the given fixed number of digits
@@
- if (min > max || (digits > 0 && y.length() > digits)) {
+ if (min < 0 || min > max || (digits > 0 && y.length() > digits)) {
throw new IllegalArgumentException();Trade-offs
- A caller that passes a negative
mintoday gets an exception instead of a wrong automaton: a behaviour change; whether it needs a CHANGES entry is the maintainers' call. - The exception stays the method's bare
new IllegalArgumentException(); a message such as"min must be >= 0, got " + minwould help but is optional. RegExpis unaffected: it never passes a negative bound.
What the checks validated
- Stock
645d4fe14a20: the repro fails with assertions on and off. - With the fix: the repro passes with assertions on and off; the automaton tests pass (258 tests, 2 skipped);
:lucene:core:check -x test(google-java-format, ecj lint including its javadoc rules, forbidden APIs) passes with the lint-ready regression test in place, which passes too and fails on stock. That localcheckrendered no javadoc;:lucene:core:renderJavadocrun separately with the fix passes. - Probe:
(-1, 5, 0),(-15, -1, 0)and(Integer.MIN_VALUE, Integer.MAX_VALUE, 0)throwIllegalArgumentException;(0, 5, 0)and<0-5>are unchanged.
After the existing check, return makeEmpty() when max < 0, and use "0" as the lower bound when min < 0 (+6 lines), with the javadoc @param min minimal value of interval; a negative value is treated as 0. This is the "non-negative part of the interval" reading: (-1, 5, 0) has the same language as (0, 5, 0), (Integer.MIN_VALUE, Integer.MAX_VALUE, 0) the same as (0, Integer.MAX_VALUE, 0), and (-15, -1, 0) is empty (language equality checked with Operations.minus both ways).
if (min > max || (digits > 0 && y.length() > digits)) {
throw new IllegalArgumentException();
}
+ if (max < 0) {
+ return makeEmpty(); // no non-negative integer lies in the interval
+ }
+ if (min < 0) {
+ x = "0";
+ }Trade-offs
- It accepts what is arguably a caller mistake without any signal.
- It adds a new documented behaviour to an experimental API, in more lines than rejecting.
- "Empty when
max < 0" depends on order: the clamp runs after the existing width check, which counts the-, so(-15, -1, 1)still throwsIllegalArgumentExceptionwhile(-15, -1, 2)returns the empty automaton.
What the checks validated
- The repro passes and the automaton tests pass (258 tests, 2 skipped);
checkwas not run for this option. - Probe: the languages listed above, and
RegExpunchanged.
A javadoc-only change (@param min minimal value of interval, must be non-negative). It closes the contract gap on paper but leaves the silent wrong automaton and the StringIndexOutOfBoundsException; the repro still fails. It fits naturally into the reject option.
Trade-offs
- No behaviour change, so no protection for callers.
What the checks validated
Not run (a javadoc change cannot make the repro pass).
Not a Lucene security issue, as query text cannot reach it; downstream, an application that builds a numeric-range restriction with this method from an unchecked negative lower bound would also match values above max, such as "12" for [-1, 5].
A static scan listed the argument checks of Lucene's public methods; a probe then called each one with boundary values and recorded that makeDecimalInterval accepts min = -1 and Integer.MIN_VALUE. The finder checked the returned automaton against the javadoc, and an independent reproducer made that check exhaustive.
The method in full138 words
Method: semi-formal (the finder's label; it sits at the boundary with non-formal testing).
- A pass over the real jars' bytecode with the JDK ClassFile API listed, for 27 core and module jars, 1,231 public entry points that can throw
IllegalArgumentException, with the parameters and constants each guard tests. FormakeDecimalIntervalthe only guards aremin > maxand the digit count. - A dynamic probe called 134 of these methods 2,851 times with values derived from the guard constants, on the real jar, and recorded that
makeDecimalIntervalacceptsmin = -1andInteger.MIN_VALUE. - The tool's own output was only "accepted". The wrong-language oracle was the finder's public-API JUnit test against the javadoc's interval semantics; an independent reproducer then compared every string of length 0 to 5 over
-./0123456789with the documented language and found theStringIndexOutOfBoundsException.
No prior report found: 109 queries over GitHub issues and PRs, the JIRA archive, the mailing lists, downstream projects, the origin library dk.brics.automaton and the web (2026-10-26), re-checked on GitHub on 2026-10-31. Upstream main still has the same check.
Searches6 searches
Verdict new. The closest items are a dead-state cleanup of this method, the rename that gave it its name, RegExp parsing issues and a review comment about descending regexp bounds; none reports this bug. dk.brics.automaton, where the code comes from, has the identical check and no report either.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26 | 30 tool queries and 14 manual ones: makeDecimalInterval, makeInterval, BasicAutomata.makeInterval, Automata, RegExp, negative min, StringIndexOutOfBoundsException, Integer.MIN_VALUE, decimal interval, numeric interval, non-negative integers, <n-m> | no report; closest: #14218 (dead-state cleanup in this method, check untouched), #6941 / LUCENE-5879 (the rename), #12251 / #12277 (RegExp repeat parsing), #225 (unrelated) |
| GitHub issues and PRs (apache/lucene), re-check | 2026-10-31 | gh search issues and prs: makeDecimalInterval; decimal interval negative; Automata negative min | only #14218 |
| Open PRs | 2026-10-26 | changed files of the 15 open PRs returned by the Automata/automaton searches | only draft #14350 touches Automata.java and RegExp.java, not this method |
| JIRA archive (LUCENE) | 2026-10-26 | text ~ makeDecimalInterval, makeInterval, decimal interval, numeric interval + RegExp, Automata + StringIndexOutOfBoundsException | LUCENE-5879 (rename), LUCENE-10010 (unrelated) |
| Mailing lists | 2026-10-26 | 20 queries over dev@, java-user@, issues@ and commits@ | no report |
| Downstream, origin library and web | 2026-10-26 | Elasticsearch, OpenSearch, Solr, Lucene.NET and a GitHub-wide makeDecimalInterval search; all 55 issues and PRs of dk.brics.automaton; 19 web queries | no report; one unrelated review comment (elastic/detection-rules#6924, descending regexp bounds) |
Since the 2010 import of the dk.brics.automaton code (b679816a7001), first released in 4.0.0-ALPHA (2012); the check condition never changed. 14 releases from 4.0.0-ALPHA to 10.6.0, and main, were run and fail the same way; 3.x has no such API.
Versions tested9 rows
Introduced by b679816a7001 (2010-04-06, svn r931278, "LUCENE-2370: Reintegrate flex_1458 branch into trunk (revision 931101)"; feature issues LUCENE-1606 and LUCENE-2089). It added BasicAutomata.makeInterval(int min, int max, int digits) with the same check, if (min > max || (digits > 0 && y.length() > digits)) throw new IllegalArgumentException();, in a port of dk.brics.automaton that has the identical check. No GitHub PR (SVN era).
Not a regression. LUCENE-5752 renamed the class to Automata (4.10.0), LUCENE-5879 renamed the method to makeDecimalInterval (5.2.0), and #14218 added dead-state removal (10.2.0); none changed the check condition (LUCENE-5752 only put the throw in braces). A static sweep finds the condition unchanged in all 123 release tags of the history clone from 4.0.0-alpha to 10.5.2. The clone has no 8.8.2 to 8.11.x tags, so 8.8.2 to 8.11.3 were neither swept nor run; they lie between 8.8.1 (swept) and 8.11.4 (run) and are affected by inference only. Upstream main 2f424fd428da (2026-10-30) still has it.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle and the standalone runner, assertions on and off); probe as above |
| 10.6.0 | affected | the minimal test fails (release jars) |
| 10.5.2, 9.12.3 | affected | fails, assertions on and off; same symptoms |
| 10.2.0, 10.1.0, 8.11.4, 5.2.0, 5.1.0, 5.0.0 | affected | same symptoms (history probe) |
| 4.10.4, 4.10.0, 4.9.1, 4.0.0 | affected | same symptoms (history probe, older API names) |
| 4.0.0-ALPHA | first affected | same symptoms, assertions on and off (BasicAutomata.makeInterval) |
| 3.6.2, 3.0.1 | API absent | no automaton package |
| 4.0.0-alpha to 10.5.2, the 123 tags in the clone | read | the check condition is present and unchanged; 8.8.2 to 8.11.3 are not in the clone (inferred, between 8.8.1 swept and 8.11.4 run) |
fix options on 645d4fe14a20 | not affected | reject: IllegalArgumentException; clamp: the language of (0, 5, 0); the repro and the automaton tests pass with each |
An independent reproducer, a devil's advocate and an adjudication rated it real; a cross-model review (OpenAI GPT-5.6, reading only) rated it real, the other returned a procedural "not real". Credibility and noise audits kept it; it still reproduces on later mains and 10.6.0.
Checks and verdicts225 words
The cross-model reviews agreed on the root cause and rated the works-as-designed argument moderate; the devil's advocate above answers it. There is no executing cross-model review: both reviews by the other model family read the code only.
| Check | Verdict | What it found |
|---|---|---|
| Independent reproducer | real | Own exhaustive test against the javadoc language (8 non-negative controls, 0 mismatches) fails on main, 10.5.2, 9.12.3 and, as main(), 8.11.4; found the StringIndexOutOfBoundsException; passes with the fix. |
| Cross-model review, refute lens (OpenAI GPT-5.6) | not real (low), procedural | Could not run Java, so it voted not real on procedure only; agreed on the root cause and that the inputs are in contract. |
| Cross-model review, works-as-designed lens (OpenAI GPT-5.6) | real (medium) | Intended-behaviour argument moderate; a probe run separately (the reviewer itself ran no Java) fails on main and passes with the fix. |
| Devil's advocate | real, low impact | The implied-precondition case does not survive; RegExp rejects <-1-5> and <5--1>. |
| Adjudication | confirmed (medium), minor | Re-ran the reproducer's test (seed C0FFEE01) with assertions on and off, and the finder repro with assertions off. |
| Credibility audit and noise audit | keep; kept | Impact already rated minor; the repro cut to one test with a reading-independent assertion. |
| Main refresh and this doc | still reproduces | a9779ef3c193; then 645d4fe14a20 (gradle) and 10.6.0. |
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 605 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,309 words
- Registry title
- Automata.makeDecimalInterval accepts a negative min without error and returns an automaton with the wrong language (for [-1, 5] it rejects "0" to "4" and accepts "12" and "-1"), or for some inputs throws StringIndexOutOfBoundsException
- Area
- util / automaton
- Priority
- Low, rank 925: Automata.makeDecimalInterval accepts a negative min that its javadoc excludes and returns a wrong-language automaton or an undocumented exception, a rare direct-API misuse with only one known downstream caller.
- Impact class
- minor
- Impact note
- Hits only application code that calls the public static Automata.makeDecimalInterval (class @lucene.experimental) directly with a negative min, for example makeDecimalInterval(Integer.MIN_VALUE, Integer.MAX_VALUE, 0) as an 'all ints' range; no in-tree caller can pass one (RegExp's <n-m> syntax rejects a leading or second '-', and RegExp is the only production caller), no query text can reach it, no index data is involved, any scale, every release 4.0.0-ALPHA to 10.6.0 and main.
- Summary
- You build a numeric-interval automaton with Automata.makeDecimalInterval(min, max, digits) and pass a negative min, for example (-1, 5, 0). You expect either the documented IllegalArgumentException or an automaton for the non-negative integers in the interval (here 0 to 5, as makeDecimalInterval(0, 5, 0) gives). Instead the call returns without error an automaton that rejects "0" to "4" and accepts "12", "49", "-1" and "-5", and some all-negative ranges such as (-15, -1, 0) throw an undocumented StringIndexOutOfBoundsException.
- Introduced
- b679816a7001ba986618f103a9a9c67ececc7ff8 (svn trunk r931278, 'LUCENE-2370: Reintegrate flex_1458 branch into trunk (revision 931101)'; feature issues LUCENE-1606 and LUCENE-2089 per the CHANGES.txt entry in that commit; no GitHub PR, SVN era): it added BasicAutomata.makeInterval with the check 'if (min > max || (digits > 0 && y.length() > digits)) throw new IllegalArgumentException();', a port of dk.brics.automaton, whose BasicAutomata.makeInterval has the same check [read: history clone; the merge has a single parent, so the flex-branch commit that first brought the code in cannot be identified locally]. Not a regression: the check line is unchanged since then (LUCENE-5512 diamond edit, LUCENE-5752 class rename and rewrite to Automata.makeInterval, LUCENE-5879 method rename to makeDecimalInterval in 5.2.0, #14218 removeDeadStates in 10.2.0 left it untouched) [read: git log -L; exec: same symptom on all 14 versions run] (2010-04-06)
- First affected version
- 4.0.0-ALPHA [exec: plain classpath run, jar SHA-1 matches Maven Central]; first GA release 4.0.0 [exec]; 3.0.1 and 3.6.2 have no automaton package (API absent) [exec]
- Affected versions
- every release that has the API, 4.0.0-ALPHA to 10.6.0, and main ea57360912ed [exec: the minimal test fails on the 10.6.0 jars with -ea (2026-10-31); with -ea on 4.0.0-ALPHA, 4.0.0, 4.9.1, 4.10.0, 4.10.4, 5.0.0, 5.1.0, 5.2.0, 8.11.4, 9.12.3, 10.1.0, 10.2.0, 10.5.2 and main, and with -da on 4.0.0-ALPHA, 9.12.3, 10.5.2 and main: on each, (-1, 5, 0) rejects "0", "1", "4" and accepts "12", "49", "-1", "-5", (-15, -1, 0) throws StringIndexOutOfBoundsException, and the (0, 5, 0) control passes; the finder's JUnit fails on 9.12.3, 10.5.2 and main]; the other releases [read: the check line appears unchanged in all 123 release tags 4.0.0-alpha to 10.5.2 of the history clone; 8.8.2 to 8.11.3 have no tag there and sit between swept 8.8.1 and executed 8.11.4]. API names: BasicAutomata.makeInterval (4.0.0-ALPHA to 4.9.1), Automata.makeInterval (4.10.0 to 5.1.0), Automata.makeDecimalInterval (5.2.0 on). Not fixed upstream: upstream main 49483e82b357 (read 2026-10-26) has the same check and no open PR changes the method [read]
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal; also reached by exhaustive check
- Upstream and prior reports
- none found (2026-10-26, the prior-report record: 109 recorded queries, namely the scripted prior-report search (30 GitHub issue and PR queries on apache/lucene, 3 commit, 2 downstream, 20 mailing-list queries over dev@, java-user@, issues@ and commits@, 4 JIRA-archive and 10 web queries), 25 manual GitHub issue and PR searches including apache/lucenenet, elastic/elasticsearch, opensearch-project/OpenSearch, apache/solr and an unscoped GitHub-wide search, 5 other read-only GitHub API reads (one code search, the full issue list of the origin library cs-au-dk/dk.brics.automaton (55 items) and its current BasicAutomata.java, the changed files of the 15 open apache/lucene PRs from the Automata/automaton searches, the upstream main Automata.java), git log -L in the history clone and 9 extra web variants; 10 hits classified, none the same bug; the finder's earlier run of the same tool agrees). Not fixed upstream: upstream main 49483e82b357 (read 2026-10-26) still checks only 'min > max || (digits > 0 && y.length() > digits)', and of the open PRs checked only draft #14350 touches Automata.java and RegExp.java, not this method. Closest items, not reports: #14218 (merged 2025-02-13, removed dead states in this method; argument check untouched, comments silent on bounds), #6941 / LUCENE-5879 (renamed the method; its 'negative numbers' is about sign-bit flipping in binary intervals), #12251 (open, RegExp {n,m} repeat parsing exceptions), elastic/detection-rules#6924 (a review comment that '<9-1>' makes makeDecimalInterval throw for min > max; descending regexp bounds, not a negative min). The identical check in dk.brics.automaton's BasicAutomata.makeInterval is unreported there too.
- Lucene commit tested
- main
645d4fe14a20(gradle in a scratch worktree, assertions on and off; the standalone runner; a probe on the core jar); the 10.6.0 release jars; the hunt's runs on mainea57360912ed(byte-identicalAutomata.java) and on 4.0.0-ALPHA, 4.0.0, 4.9.1, 4.10.0, 4.10.4, 5.0.0, 5.1.0, 5.2.0, 8.11.4, 9.12.3, 10.1.0, 10.2.0 and 10.5.2; the main refresh ona9779ef3c193; both fix options in a scratch worktree of645d4fe14a20. Upstream main2f424fd428da(2026-10-30) has the same check line.
- Finding record
lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:44 (at ea57360912ed) (@lucene.experimental), :232-236 (between() javadoc: x.substring(n) and y.substring(n) must have equal length), :489-500 (makeDecimalInterval javadoc: 'accepts strings representing decimal (base 10) non-negative integers in the given interval'; '@param min minimal value of interval' with no range; '@exception IllegalArgumentException if min > max or if numbers in the interval cannot be expressed with the given fixed number of digits'), :501-547 (x = Integer.toString(min) keeps the '-'; the only check is 'min > max || (digits > 0 && y.length() > digits)'; zero padding to d = digits or y.length() never strips the sign; between(builder, x, y, 0, ...))lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:345-349 (at ea57360912ed) (class javadoc: 'Numerical intervals are specified by non-negative decimal integers'), :728-729 (REGEXP_INTERVAL -> Automata.makeDecimalInterval, the only production caller), :1460-1491 (the <n-m> parser rejects a '-' at position 0, at the end or a second '-', and turns NumberFormatException into IllegalArgumentException)lucene/CHANGES.txt:3088-3090 (at ea57360912ed) (GITHUB#12427: Automata#makeStringUnion now explicitly throws IllegalArgumentException for unsorted input instead of relying on assert; the closest precedent for checking an Automata factory precondition)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:182-190, (at 645d4fe14a20) 232-266, 490-499, 501-546lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:344-350, (at 645d4fe14a20) 728-729 (background: the <n-m> syntax)lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:44 (at 645d4fe14a20) (@lucene.experimental)
- Who is affected
lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:728-729, (at 645d4fe14a20) 1474, 1489-1490lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:44, (at 645d4fe14a20) 490-491
- Minimal repro
lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:803-805 (at 645d4fe14a20)Lucene main 645d4fe14a20 gradle/validation/ecj-lint/ecj.javadocs.prefs:136
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/automaton/Automata.java:232-236, (at 645d4fe14a20) 490-499, 506lucene/core/src/java/org/apache/lucene/util/automaton/RegExp.java:347-348 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/util/automaton/TestAutomaton.java:342, (at 645d4fe14a20) 577, 586, 591-594, 1105, 1113; util/automaton/TestRegExpParsing.java:474, 487; search/TestAutomatonQuery.java:134, 138lucene/CHANGES.txt:3121-3123 (at 645d4fe14a20) (GITHUB#12427)
- Prior reports
gh search issues/prs --repo apache/lucene (read-only, 2026-10-31): makeDecimalInterval; decimal interval negative; Automata negative mingh api repos/apache/lucene (read-only, 2026-10-31): main 2f424fd428da, Automata.java:506 unchanged
- History
a Lucene clone releases/lucene/10.6.0 Automata.java:506 (same check)
- TestMakeDecimalIntervalNegativeMin.java (1 test):
repro/LFV-441/src/org/apache/lucene/util/automaton/TestMakeDecimalIntervalNegativeMin.java(39 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs; an independent visual check compared every value, code row and line number with them, and the card was rendered again after its one wording fix ("the only non-test caller").
assets/LFV-441/bug.png(605 KB)
