All findings · Lucene finding · LFV-441 · decimal-interval-negative-min-garbage-language

Automata.makeDecimalInterval accepts a negative min and silently returns a wrong automaton

Automata.makeDecimalInterval(-1, 5, 0) neither throws nor returns the numbers 0 to 5: its automaton rejects "0" to "4" and accepts "12" and "-1", because the minus sign is read as a digit. Only code that calls the method directly is affected; Lucene's regexp syntax cannot pass a negative bound.

Summary

Priority
Low, rank 925Automata.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
minor
Since
4.0.0-ALPHA (2012; code imported in 2010)introduced 2010-04-06 · over 16 years
Affected
4.0.0-ALPHA to 10.6.0 and main
Status
documented
Found by
argument-check scan and boundary probe

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background: what makeDecimalInterval builds

  1. An automaton is a small state machine that accepts or rejects strings. Lucene builds them for regexp, wildcard and fuzzy queries, and AutomatonQuery matches the terms an automaton accepts.
  2. Automata.makeDecimalInterval(min, max, digits) builds one for the decimal numbers from min to max written as text: (0, 5, 0) accepts "0" to "5". With digits = 0 leading zeros are allowed ("05"); with digits > 0 every string has exactly that many digits.
  3. The regexp syntax <0-5> is built with this method.
Diagram of the bug. You call Automata.makeDecimalInterval(-1, 5, 0). Expected: an IllegalArgumentException, or the numbers 0 to 5 as makeDecimalInterval(0, 5, 0) gives. Inside Lucene main, the bound becomes the string "-1", the only argument check (min > max, digits) passes, and the digit-by-digit builder treats '-' as a digit just below '0'. The automaton accepts "-1" to "-9", "5", the two-character strings "00" to "49", and ".0" to "/9", and rejects "0" to "4". Some all-negative ranges, such as (-15, -1, 0), throw StringIndexOutOfBoundsException. RegExp, the only non-test caller in Lucene, rejects a sign. Fix direction: reject min < 0 with IllegalArgumentException.
How 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.

The finding, step by step

  1. Call Automata.makeDecimalInterval(0, 5, 0): the automaton accepts "0" to "5" (and "05") and rejects "6" and "12".
  2. Call makeDecimalInterval(-1, 5, 0). The javadoc names only two rejected inputs (min > max, too few digits), so you expect an IllegalArgumentException, or the non-negative part of [-1, 5], which is 0 to 5 again.
  3. 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.
  4. Why: min becomes 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.
  5. Some all-negative ranges, such as makeDecimalInterval(-15, -1, 0), throw StringIndexOutOfBoundsException: 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.

Who is affected

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.

The bug, step by step (main 645d4fe14a20, Automata.java)

  1. 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.
  2. Padding keeps the sign. Both strings are left-padded with '0' to d characters, d = digits, or y.length() when digits <= 0 (:509-523). Here d = 1 and x is already two characters long, so nothing changes. Padding only lengthens; it never removes the -.
  3. The builder assumes two digit strings of equal length. between(builder, x, y, 0, initials, digits <= 0) (:534); its javadoc says the length of x.substring(n) "must be equal to y.substring(n).length()" (:232-236).
  4. '-' becomes a digit. At position 0, cx = '-' (0x2D, just below '0') and cy = '5' (:251-252). They differ, so between() adds (:256-261): a transition on '-' into atLeast("-1", 1), which accepts "-1" to "-9"; a transition on '5', which accepts "5"; and a transition on the range '.' to '4' into anyOfRightLength("-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.
  5. Leading zeros. With digits <= 0 a '0' loop is put in front (:538-544), so "05" is accepted too.
  6. Some all-negative ranges throw. For (-15, -1, 0): x = "-15", y = "-1", d = 2, and x stays three characters long. between() matches '-' and '1', then reads y.charAt(2) (:252) on a two-character string: StringIndexOutOfBoundsException: Index 2 out of bounds for length 2. This happens only when the padded y is a proper prefix of x: between() stops at the end of x (:245) and reads y only 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".

The numbers (executed)

  • (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.makeDecimalInterval directly with a negative min: an "all ints" range, or a lower bound computed from data. The class is @lucene.experimental.
  • Not reachable from queries. RegExp.toAutomaton is 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 a NumberFormatException into IllegalArgumentException (:1489-1490), so it never passes a negative bound: new RegExp("<-1-5>") throws IllegalArgumentException: interval syntax error at position 5. RegexpQuery and the query parsers go through RegExp.
  • 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 above max and 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 an AutomatonQuery, uses that language. Some all-negative ranges, such as (-15, -1, 0), fail loudly with an undocumented StringIndexOutOfBoundsException instead; 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 = 0 control: in the probe (digits up to 1,000) the largest negative-min automaton had 1,079 states against 1,003 for the control with the same digits, and every call took under 10 ms. Both grow with digits, 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.

Minimal repro

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.

TestMakeDecimalIntervalNegativeMin.java (1 test)39 lines
/*
 * 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());
  }
}
Command
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", as makeDecimalInterval(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.

All files of this repro: repro/LFV-441/

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.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0 release jars with this test
jdkOpenJDK 25.0.3
osLinux, aarch64

Observed output

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

Why it looks unintended

  • The method checks its arguments and documents exactly two rejected inputs, min > max and too few digits; min < 0 is 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 min is 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.testIntervalRandom draws min from 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#makeStringUnion now "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.

Suggested fix

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 &gt; max or if numbers in the interval cannot be
-   *     expressed with the given fixed number of digits
+   * @exception IllegalArgumentException if min &lt; 0, if min &gt; 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 &gt; max or if numbers in the interval cannot be
-   *     expressed with the given fixed number of digits
+   * @exception IllegalArgumentException if min &lt; 0, if min &gt; 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 min today 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 " + min would help but is optional.
  • RegExp is 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 local check rendered no javadoc; :lucene:core:renderJavadoc run separately with the fix passes.
  • Probe: (-1, 5, 0), (-15, -1, 0) and (Integer.MIN_VALUE, Integer.MAX_VALUE, 0) throw IllegalArgumentException; (0, 5, 0) and <0-5> are unchanged.

Treat a negative min as 0 (validated by the checks)

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 throws IllegalArgumentException while (-15, -1, 2) returns the empty automaton.

What the checks validated

  • The repro passes and the automaton tests pass (258 tests, 2 skipped); check was not run for this option.
  • Probe: the languages listed above, and RegExp unchanged.

Document min >= 0 only (not run)

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).

Downstream note

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].

How it was found

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. For makeDecimalInterval the only guards are min > max and 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 makeDecimalInterval accepts min = -1 and Integer.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 -./0123456789 with the documented language and found the StringIndexOutOfBoundsException.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene)2026-10-2630 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-check2026-10-31gh search issues and prs: makeDecimalInterval; decimal interval negative; Automata negative minonly #14218
Open PRs2026-10-26changed files of the 15 open PRs returned by the Automata/automaton searchesonly draft #14350 touches Automata.java and RegExp.java, not this method
JIRA archive (LUCENE)2026-10-26text ~ makeDecimalInterval, makeInterval, decimal interval, numeric interval + RegExp, Automata + StringIndexOutOfBoundsExceptionLUCENE-5879 (rename), LUCENE-10010 (unrelated)
Mailing lists2026-10-2620 queries over dev@, java-user@, issues@ and commits@no report
Downstream, origin library and web2026-10-26Elasticsearch, OpenSearch, Solr, Lucene.NET and a GitHub-wide makeDecimalInterval search; all 55 issues and PRs of dk.brics.automaton; 19 web queriesno report; one unrelated review comment (elastic/detection-rules#6924, descending regexp bounds)

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (gradle and the standalone runner, assertions on and off); probe as above
10.6.0 affectedthe minimal test fails (release jars)
10.5.2, 9.12.3 affectedfails, assertions on and off; same symptoms
10.2.0, 10.1.0, 8.11.4, 5.2.0, 5.1.0, 5.0.0 affectedsame symptoms (history probe)
4.10.4, 4.10.0, 4.9.1, 4.0.0 affectedsame symptoms (history probe, older API names)
4.0.0-ALPHA first affectedsame symptoms, assertions on and off (BasicAutomata.makeInterval)
3.6.2, 3.0.1 API absentno automaton package
4.0.0-alpha to 10.5.2, the 123 tags in the clone readthe 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 affectedreject: IllegalArgumentException; clamp: the language of (0, 5, 0); the repro and the automaton tests pass with each

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Independent reproducer realOwn 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), proceduralCould 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 impactThe implied-precondition case does not survive; RegExp rejects <-1-5> and <5--1>.
Adjudication confirmed (medium), minorRe-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; keptImpact already rated minor; the repro cut to one test with a reading-independent assertion.
Main refresh and this doc still reproducesa9779ef3c193; then 645d4fe14a20 (gradle) and 10.6.0.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,309 words

Finding record

LFV-441, snapshot of 2026-11-06

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 main ea57360912ed (byte-identical Automata.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 on a9779ef3c193; both fix options in a scratch worktree of 645d4fe14a20. Upstream main 2f424fd428da (2026-10-30) has the same check line.

Lucene and upstream sources

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-546
  • lucene/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-1490
  • lucene/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, 506
  • lucene/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, 138
  • lucene/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 min
  • gh 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)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

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").

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.