The parse-time nesting guard adds one level for the root of the parse tree. At Integer.MAX_VALUE that + 1 wraps to the most negative int, so the guard rejects the root itself and nothing compiles.
- The expressions module compiles JavaScript-like formulas such as
cbrt(score)+ln(popularity)into a Java class whoseevaluatereturns adouble. - Since #16423 (10.6) the compiler rejects too deeply nested input with a
ParseExceptioninstead of failing with aStackOverflowError. The default limit is 1024 levels; a new overload lets the caller choose it. - Two guards enforce it: one while parsing and one while generating code. The parsing one also counts the root of the parse tree, so it is given one extra level.
2147483647 + 1 wraps to -2147483648, so the first grammar rule already fails; 1 + 2, 1, sqrt(16) and a 1,100-deep nest all throw ParseException, while Integer.MAX_VALUE - 1 compiles them. Select the still to open it at full size. Generated with AI, checked against the code.- You compile an expression with the three-argument
JavascriptCompiler.compile(source, functions, maxNestingDepth), new in 10.6, and passInteger.MAX_VALUE(2,147,483,647) to ask for no practical nesting limit. The one- and two-argument overloads use a limit of 1024. - You expect
compile("1 + 2", DEFAULT_FUNCTIONS, Integer.MAX_VALUE)to compile and evaluate to 3.0, as it does withInteger.MAX_VALUE - 1: a higher limit should accept at least as much. - Instead it throws
ParseException: Invalid expression '1 + 2': Nesting level too deep (>-2147483648). Every expression fails, even the literal1, with assertions on or off. - Why: the parse-time guard counts the root of the parse tree as one level, so it is created with the limit
maxNestingDepth + 1. Inint, 2,147,483,647 + 1 wraps to -2,147,483,648, and its checkdepth > limitis already true at depth 1, the root. - Only this one value is hit:
1compiles at every sampled limit from 1 to 2,147,483,646 (for example 1, 16, 1024 and 2,147,483,646) and fails at 2,147,483,647; the default overloads are unaffected. Workaround: passInteger.MAX_VALUE - 1.
Only code that passes exactly Integer.MAX_VALUE to the new three-argument compile (or to the package-private form it delegates to): every compile then fails at once, with assertions on or off. No wrong value, no hang, no index effect. The default overloads (limit 1024) and every smaller positive limit work. New in 10.6; no release through 10.5.2 has the overload.
Mechanism, terms and numbers541 words
#16423 added a public overload JavascriptCompiler.compile(String, Map<String, MethodHandle>, int maxNestingDepth) (JavascriptCompiler.java:195-199 on main 645d4fe14a20), documented only as "the maximum depth of nesting (function calls, precedence - also implicit)" (:187-188). Its parse-time guard is installed as new JavascriptNestingDepthListener(sourceText, maxNestingDepth + 1) (:319-320), because the listener "will count root node" (:317-318). For Integer.MAX_VALUE the int addition wraps to Integer.MIN_VALUE, and the listener's depth > maxNestingDepth (JavascriptNestingDepthListener.java:44-45) is true on the first rule the parser enters, the root: every expression, even 1, fails with ParseException: Invalid expression '1': Nesting level too deep (>-2147483648). Integer.MAX_VALUE - 1 works, and the one- and two-argument overloads (limit 1024) cannot be hit. The failure is loud and the same with and without assertions; no value is ever wrong. The overload is on main, branch_11x, branch_10x and branch_10_6 (10.6.0, released 2026-10-29, fails too); no release through 10.5.2 has it. Not passing the extra level when the limit is already Integer.MAX_VALUE fixes it.
- maxNestingDepth: the limit argument of the public overload
compile(String, Map, int)(JavascriptCompiler.java:195-199), "the maximum depth of nesting (function calls, precedence - also implicit)" (:187-188). The one- and two-argument overloads passDEFAULT_MAX_NESTING_DEPTH = 1024(:138,:154-176). The value is stored as given (:219-240). - Parse-time guard: a
JavascriptNestingDepthListenerattached to the ANTLR parser (:319-320).enterEveryRuledoesdepth++and throws whendepth > maxNestingDepth(JavascriptNestingDepthListener.java:43-53);exitEveryRuledoesdepth--. - Visitor-side guard: a second listener instance with the raw
maxNestingDepth(:404), driven by the code generator as it walks the tree.
- The + 1.
getAntlrParseTreecreates the parse-time guard withmaxNestingDepth + 1; the comment says it "will count root node, so add one recursion extra" (:317-320). - The wrap. For
maxNestingDepth = Integer.MAX_VALUE= 2,147,483,647 the sum is 2,147,483,648, which does not fit anintand wraps toInteger.MIN_VALUE= -2,147,483,648. - The first rule fails.
javascriptParser.compile()(:321) enters the root rule first (JavascriptParser.java:217).depthbecomes 1 and1 > -2147483648is true, so the listener throws before the text of the expression matters. The message prints the wrapped limit: "Nesting level too deep (>-2147483648)". - Only this value. Every other
intplus 1 is representable. Limits 0 and below reject every expression, which matches a maximum depth of 0 or less. The visitor-side guard (:404) has no + 1 and is never reached here.
compile("1", DEFAULT_FUNCTIONS, Integer.MAX_VALUE - 1).evaluate(null)returns 1.0; withInteger.MAX_VALUE:ParseException: Invalid expression '1': Nesting level too deep (>-2147483648), with assertions on and off.- The same for
1 + 2(3.0 atInteger.MAX_VALUE - 1),sqrt(16),1 ? 2 : 3and a 1,100-deep nest of parentheses. By the code, any source that reaches the parser's root rule is rejected there, whatever its text. 1compiles at every sampled limit from 1 to 2,147,483,646 (1, 2, 3, 4, 8, 16, 1024, 2048, 1,048,576, 2,147,483,645, 2,147,483,646) and, of the positive limits, fails only at 2,147,483,647.- A 1,500-deep expression fails at the default 1024, compiles at 4,096 and at
Integer.MAX_VALUE - 1(on a 256 MB stack) and fails atInteger.MAX_VALUE.
Exposure and likelihood235 words
- Who is exposed. Callers of
compile(String, Map, int)who passInteger.MAX_VALUE, a natural value for "no practical limit" (Lucene's ownJoinUtildocuments it as the value that disables its bound), for example after the default 1024 rejected a deep generated expression. The PR author made the limit configurable "so people can tune it". The package-privatecompile(String, Map, int, boolean)(JavascriptCompiler.java:219-227) takes the same path but is reachable only from inside the package; in the tree onlyCompilerTestCasecalls it, with the default limit. - Opt-in. The one- and two-argument overloads use 1024 (listener limit 1025) and cannot hit it. No index, codec or data is involved; single thread, deterministic.
- Effect. The expression feature is unusable for that caller until the value changes; it shows on the first compile. No wrong value, no hang, no lasting state.
- Raising the limit is meaningful. A 1,500-deep expression that the default rejects compiles at 4,096 and at
Integer.MAX_VALUE - 1. WithInteger.MAX_VALUE - 1and very deep input (a 20,000-deep nest on the default stack) the existingStackOverflowErrorfallback gives a cleanParseException. - How likely. Plausible but narrow: a natural value, a new API, no known caller yet. Workaround: pass
Integer.MAX_VALUE - 1or any large finite limit. - Timing. 10.6.0 RC1 contains it; the vote passed and 10.6.0 was released from it on 2026-10-29, so a fix can come in a 10.6.x release or 11.0.
The test shows that JavascriptCompiler.compile("1", DEFAULT_FUNCTIONS, Integer.MAX_VALUE) rejects even the trivial expression 1, because the parse-time nesting listener is created with maxNestingDepth + 1, which wraps to Integer.MIN_VALUE.
/*
* 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.expressions.js;
import java.text.ParseException;
import org.apache.lucene.tests.util.LuceneTestCaseJupiter;
import org.junit.jupiter.api.Test;
public class TestMaxNestingDepthMaxValue extends LuceneTestCaseJupiter {
@Test
public void testMaxValueLimitCompilesTrivialExpression() {
// expected: "1" compiles; actual: ParseException because maxNestingDepth + 1 wraps around
try {
JavascriptCompiler.compile("1", JavascriptCompiler.DEFAULT_FUNCTIONS, Integer.MAX_VALUE);
} catch (ParseException e) {
throw new AssertionError(
"expected \"1\" to compile with maxNestingDepth=Integer.MAX_VALUE, but got: " + e, e);
}
}
}
# in a copy of this finding's repro folder (repro/LFV-182/, 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:$LUCENE/lucene/expressions/build/libs/lucene-expressions-$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.expressions.js.TestMaxNestingDepthMaxValue
# the module(s) expressions also need their third-party runtime jars
- Expected vs actual
- Expected:
1compiles. Actual:ParseException: Invalid expression '1': Nesting level too deep (>-2147483648), with-eaand-da.
More files, environment, full output62 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 4 test(s) run, 1 failed
=> java.lang.AssertionError: expected "1" to compile with maxNestingDepth=Integer.MAX_VALUE, but got: java.text.ParseException: Invalid expression '1': Nesting level too deep (>-2147483648)
__randomizedtesting.SeedChain.seed([DEADBEEF:90DC4BAD2263C875:A1AD4638E010A3D3])
org.apache.lucene.expressions.js.TestMaxNestingDepthMaxValue.testMaxValueLimitCompilesTrivialExpression(TestMaxNestingDepthMaxValue.java:31)
Caused by: java.text.ParseException: Invalid expression '1': Nesting level too deep (>-2147483648)
org.apache.lucene.expressions.js.JavascriptCompiler.newWrappedParseException(JavascriptCompiler.java:858)
org.apache.lucene.expressions.js.JavascriptNestingDepthListener.enterEveryRule(JavascriptNestingDepthListener.java:46)
org.antlr.v4.runtime.Parser.triggerEnterRuleEvent(Parser.java:396)
org.antlr.v4.runtime.Parser.enterRule(Parser.java:630)
org.apache.lucene.expressions.js.JavascriptParser.compile(JavascriptParser.java:217)
[...]- The limit is documented only as "the maximum depth of nesting", with no range or check;
Integer.MAX_VALUE - 1accepts1,Integer.MAX_VALUEaccepts nothing. - The + 1 is commented as room for the root node, not a reserved value, and the code keeps a fallback for "too high limits".
IndexSearcher.setMaxClauseCount, a comparable limit, acceptsInteger.MAX_VALUE.
The full argument297 words
- Contract.
@param maxNestingDepth"the maximum depth of nesting (function calls, precedence - also implicit)" (JavascriptCompiler.java:187-188); no range, no reserved value, and nothing validates it (:195-199,:219-240). The class javadoc only says the default of 1024 is enforced with aParseException(:104-109). CHANGES (10.6.0,lucene/CHANGES.txt:551-555): "The new code allows to add a hard limit for nesting (default is 1024) and no longer throws virtual machine errors." - Monotonicity. A larger limit cannot rightly accept less:
Integer.MAX_VALUE - 1accepts1,Integer.MAX_VALUEaccepts nothing. - The + 1 is root-node compensation. The comment says so (
:317-318), and the visitor-side guard uses the raw value (:404). - Too-high limits are expected. The
StackOverflowErrorfallback (:266-272) carries the comment "we should catch this before in the visitor, but too high limits may cause this." The #16423 body calls it a "last safety"; in review uschindler wrote "I am not sure if 250 is enough for all cases, so I'd like to have it configurable (at least for a while, so people can tune it)". In #16549 (dweiss, test-only) the quoted fix note says "the SOE fallback in production code is intentional and unchanged". - Precedent. In review msokolov compared the limit with "BooleanQuery's 1024 limit"; that limit, now
IndexSearcher.setMaxClauseCount, accepts any value >= 1,Integer.MAX_VALUEincluded (IndexSearcher.java:256-261).JoinUtil.createJoinQuerydocuments that min 0 and maxInteger.MAX_VALUE"disables the min and max 'from' documents filtering" (JoinUtil.java:512-517). - History and tests. The overload, the listener and the + 1 arrived together in
b6c2c31e225(#16423); its body, 3 comments, 9 reviews and 7 inline comments never mentionInteger.MAX_VALUE, the + 1 or argument checks. No upstream test passes a non-default limit (CompilerTestCase.java:37-38and thetestRecursionDepth*tests use 1024).
The devil's-advocate check161 words
The strongest case for "works as designed" (built by the intended-behaviour checker and the reviews by another model family): the class is @lucene.experimental (JavascriptCompiler.java:111); the limit is a resource guard; Integer.MAX_VALUE levels can never be reached before the JVM stack runs out (uschindler in review: "In my testing 2048 sometimes causes stack overflows already"), so a maintainer could say "pass a large finite number", or prefer to reject the value.
Why it fails: that argues how useful the value is, not what it should do. Nothing documents or rejects it, Integer.MAX_VALUE - 1 (just as unreachable) works, and the code keeps an intentional StackOverflowError-to-ParseException fallback for limits too high to stop the recursion first (:266-272). What remains is severity (one value, a loud failure), which the Low priority reflects. Nothing documents Integer.MAX_VALUE as a "no limit" value either. Verdict: a defect; rejecting the value with a clear IllegalArgumentException resolves it as well as accepting it.
Recommended Do not add the root level at Integer.MAX_VALUE
Skip the extra root level when the limit is already Integer.MAX_VALUE: pass maxNestingDepth == Integer.MAX_VALUE ? maxNestingDepth : maxNestingDepth + 1 to the parse-time guard (one statement). Every other limit behaves as before; the new test and all 170 upstream expressions tests pass with assertions on and off.
// add listener to detect too deep nesting early, this may not catch all occurrences... (this
// will count root node, so add one recursion extra, without overflowing Integer.MAX_VALUE).
javascriptParser.addParseListener(
new JavascriptNestingDepthListener(
sourceText,
maxNestingDepth == Integer.MAX_VALUE ? maxNestingDepth : maxNestingDepth + 1));All options and trade-offs4 options ยท 474 words
One fix was built and validated with gradle; the alternatives were not built. Rejecting the value instead (option B) is an equally valid answer for maintainers.
JavascriptCompiler.java:317-320, one file, +4/-2 with the comment (after tidy):
// add listener to detect too deep nesting early, this may not catch all occurrences... (this
// will count root node, so add one recursion extra, without overflowing Integer.MAX_VALUE).
javascriptParser.addParseListener(
new JavascriptNestingDepthListener(
sourceText,
maxNestingDepth == Integer.MAX_VALUE ? maxNestingDepth : maxNestingDepth + 1));Trade-offs
- Nothing changes for any other limit.
- At
Integer.MAX_VALUEthe call then behaves likeInteger.MAX_VALUE - 1today: the parse-time guard can never fire (nointdepth exceedsInteger.MAX_VALUE), and very deep input is stopped by the existingStackOverflowErrorfallback with aParseException(with the same expression shadowed in: a 200,000-deep nest in 6.8 ms and a 1,000,001-character chain in 167 ms, each aParseExceptioncaused by aStackOverflowError). That is the cost of the huge limit the caller chose. - Not covered: limits of 0 and below, which reject every expression today; a lower-bound check (option B) is complementary.
What the checks validated
- Scratch worktree of main
645d4fe14a20, gradle: the new test fails on the stock tree (the only failure among the module's 174 tests) and passes with the fix, with assertions on and off. The wholelucene/expressionsmodule passes with assertions on and off: 14 suites (13 upstream plus the new test), 174 tests (170 upstream), 0 failures;TestJavascriptCompiler20 of 20.:lucene:expressions:check -x test(google-java-format, ecjLint, forbiddenApis) passes. - Standalone runner with the fixed class shadowed in: the new test passes. Earlier, with the finder's identical expression: the finder's test, the independent reproducer's 5 tests and the cross-model reviewers' tests pass, and the 170 upstream expressions tests pass.
Document a range and throw IllegalArgumentException outside it, for example for maxNestingDepth < 1 || maxNestingDepth == Integer.MAX_VALUE.
Trade-offs
Loud at the call site and explicit, but it rejects a "no limit" value that comparable Lucene limits accept (JoinUtil, IndexSearcher.setMaxClauseCount), and it changes today's behaviour for 0 and negative limits (they reject every expression now). Can be combined with the first option for the lower bound. Not built.
Make JavascriptNestingDepthListener's limit a long and pass maxNestingDepth + 1L (two files).
Trade-offs
Same effect as the first option; the message would then print 2147483648. Not built.
Let the parse-time listener skip the root rule, so both guards take the caller's limit unchanged.
Trade-offs
More code. It would also change a side note: today the parse-time message prints the internal limit, for example "Nesting level too deep (>1025)" for the default 1024 (not claimed as a separate bug). Not built.
Elasticsearch, OpenSearch and Solr call the one- or two-argument compile (limit 1024), so they are not affected; no downstream caller of the new overload was found (code search of those projects, not exhaustive).
Code reading. While writing a boundary check for the new nesting guard around its default of 1024, the finder noticed the unguarded maxNestingDepth + 1; a small program and then a public-API JUnit test on the stock main jars confirmed it.
The method in full81 words
Method: non-formal (the hunt's expressions-compiler hunt unit, 2026-10-26, main ea57360912ed). The unit's boundary harness checks depths around the default limit (1022 to 1026 and beyond) and could not reach this value; the signal came from reading getAntlrParseTree while writing it. The finder's JUnit test (1 + 2, with the Integer.MAX_VALUE - 1 control) failed with assertions on and off, a main() form failed too, and a one-line shadow fix made it pass with the 170 upstream expressions tests.
No report found: a search on 2026-10-26 (85 logged queries over GitHub issues, PRs, code and history, four Lucene mailing lists, JIRA, downstream projects and the web) and a recheck on 2026-10-29. Related only: #16423, which added the code, and #16549, a test-only follow-up.
Searches5 searches
Closest items, none a report of this bug: #16423 (uschindler, "Add (configurable) guards for complexity of expressions", merged 2026-07-27) introduced the overload, the listener and the + 1; its body, comments, reviews and inline comments never mention the + 1, Integer.MAX_VALUE or argument checks. #16549 (dweiss, merged 2026-08-24) is a test-only stack-size fix for the recursion tests and keeps the StackOverflowError fallback.
Gaps: on 2026-10-26 GitHub's search rate limit starved the tool's GitHub, commit and downstream sources, which were replaced by GraphQL search and git history; GitHub search is lexical. On 2026-10-29 one recheck query hit the rate limit and was not retried.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene, Elasticsearch, OpenSearch, Solr, all of GitHub) | 2026-10-26 | 40 GraphQL issue/PR searches, open and closed, with comments (22 in apache/lucene, 7 downstream, 11 date-bounded or global); #16423 and #16549 read in full | only #16423 and #16549, neither a report |
| Open and recent PRs | 2026-10-26 | the changed files of all 413 open apache/lucene PRs and of the 291 closed or merged PRs created since 2026-07-20 | none touches JavascriptCompiler.java or JavascriptNestingDepthListener.java apart from #16423 and #16549 |
| Git history | 2026-10-26 | full-history git log -S 'maxNestingDepth + 1', -S 'Nesting level too deep', --grep nesting / NestingDepth / JavascriptCompiler | only b6c2c31e225 (#16423) added both strings |
| Mailing lists, JIRA and web | 2026-10-26 | a scripted prior-report search: dev@, java-user@, issues@ and commits@ (20 queries), JIRA (4), web (6, including the exact message) | only notification mails for #16423 and #16549; unrelated JIRA and web pages |
| GitHub (recheck) | 2026-10-29 | commits on both files; the line on branch_10_6, branch_11x and branch_10x; 6 issue/PR searches | nothing new; not fixed |
Introduced with the API by #16423 (b6c2c31e225, uschindler, merged 2026-07-27, milestone 10.6.0) and backported the same day. Present on main, branch_11x, branch_10x and branch_10_6, and in 10.6.0 (released 2026-10-29); every release through 10.5.2 lacks the overload. Not fixed upstream.
Versions tested7 rows
Introduced by b6c2c31e22518503a8da0cd26b196217b30f4186, #16423 "Add (configurable) guards for complexity of expressions" (author and merger uschindler, merged 2026-07-27 17:14 UTC, milestone 10.6.0; CHANGES under 10.6.0, credited to Uwe Schindler and Seth Kraft). It added the overload, DEFAULT_MAX_NESTING_DEPTH, the listener and the + 1 in one change (git log -S, git blame). Backported to branch_10x the same day as 1dc74b98be6; branch_10_6 forks from branch_10x after it. No later commit changed either file; on 2026-10-29 the line is at :320 on main and branch_11x and at :358 on branch_10_6 and branch_10x.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | this doc's test fails (gradle and the standalone runner, assertions on and off) |
main ea57360912ed | affected | every hunt repro fails with assertions on and off; the expressions module is identical to 645d4fe14a20 |
10.6.0 RC1 4ae946e5a9b (branch_10_6) | affected | jars built at the RC revision: the main() repro fails 1 of 1 (assertions on); a JUnit 4 test fails 3 of 5 with assertions on and with them off |
branch_10x backport 1dc74b98be6, branch_10_6 2763db11b0a | hybrid | the 10.5.2 jars with that commit's two expressions classes: same ParseException |
| branch_11x, branch_10x heads | read only | the + 1 line is present (contents API, 2026-10-29) |
9.12.3, 10.5.0, 10.5.1, 10.5.2; main before b6c2c31e225 | overload absent | no compile(String, Map, int) (a reflection probe and a direct call that does not compile) |
| main + fix | not affected | the new test passes; the whole expressions module, 174 tests, 0 failures |
An independent reproducer, executing reviews by another model family (OpenAI GPT-5.6), an intended-behaviour devil's advocate and an adjudication re-ran repros and agree. A credibility audit rated it a loud validation gap; a no-noise review first flagged it, then kept it as Low. This doc's test fails on main 645d4fe14a20 with gradle; the fix passes the whole module.
Checks and verdicts488 words
Every check below ran code, except the second-opinion review in the last row, which read the saved outputs. The credibility audit lowered the impact class from crash to wrong error.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | JUnit (1 + 2) fails on main with -ea and -da; the main() form fails; with the fix it passes and the 170 upstream expressions tests pass. |
| cross-model review, refute lens | real, high (executed) | OpenAI GPT-5.6: its own test fails on main with -ea and -da (two seeds), passes with the fix, 170 of 170 upstream tests; intended argument weak. |
| Independent reproducer | real | Own public-API JUnit in another package: 3 of 5 fail with -ea and -da; a 20-deep nest is accepted at 1024, 2^20, MAX - 2 and MAX - 1 and rejected only at MAX; a one-line fix passes 5 of 5; the overload is absent in 10.5.2 and 9.12.3. |
| Cross-model review, intended lens | real, high (executed) | OpenAI GPT-5.6: own test 1 of 5 fails, 5 of 5 pass with the fix; 1 accepted at each tested limit (1, 2, 3, 4, 16, 1024, 2147483646), rejected at 2147483647 (and at 0, -1, -2). |
| Intended-behaviour devil's advocate | real, intended argument weak | 5 of 5 monotonicity violations, all at Integer.MAX_VALUE; read the #16423 and #16549 discussions and the Lucene precedents. |
| adjudication | confirmed, high | Re-ran the independent reproduction's JUnit with -ea and -da: 5 run, 3 failed each, same exception and stack. |
| history step and security screen | 21 runs; security none | Fails exactly where the overload exists; the saturating control passes. Security screen: 1,408 checks over 16 limits, 0 safety violations; the rejection is cheaper than a normal compile. |
| credibility audit | downgrade (wrong error) | Reproduced; a validation gap on a configuration argument that fails loudly; keep it a small report. |
| no-noise review | noise candidate, then kept (Low) | Triage (2026-10-29) first classed it as a noise candidate ("absurd parameter, loud failure": one extreme value on an unreleased overload, a loud failure, never a wrong value). An independent challenger kept it as a Low claim (no documented range, JoinUtil documents Integer.MAX_VALUE as its "disable" value, the message prints a negative limit) and re-ran the finder's test on 645d4fe14a20: it fails. |
| this doc's runs | reproduced, fix validated | The one-method test fails on 645d4fe14a20 with gradle and the standalone runner, assertions on and off; with the fix it passes and the whole expressions module and check -x test pass. |
| fact checks of this doc | no substantive error | A separate AI tool re-ran the doc's gradle command (fails as shown with assertions on and off; passes with the fix; the module passes 174 of 174) and its own probe. A second review by another model family (OpenAI GPT-5.6) read the saved outputs only. Their wording points are applied; see an unpublished 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.
- PNG 496 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,178 words
- Registry title
- JavascriptCompiler.compile(source, functions, Integer.MAX_VALUE) rejects every expression, even '1', because maxNestingDepth + 1 overflows to Integer.MIN_VALUE
- Area
- expressions / JavascriptCompiler nesting-depth guard (compile(String, Map, int maxNestingDepth), new in #16423, released in 10.6.0)
- Priority
- Low, rank 896: The new compile overload loudly rejects every expression only at the maxNestingDepth Integer.MAX_VALUE boundary and has a one-line fix.
- Impact class
- wrong error
- Impact note
- Only callers of the new public overload JavascriptCompiler.compile(String, Map, int maxNestingDepth) (expressions module, @lucene.experimental; new in 10.6.0, released 2026-10-29, and on main and branch_10x) who pass exactly Integer.MAX_VALUE are hit, on every compile, with -ea and -da: each expression, even '1', throws ParseException 'Nesting level too deep (>-2147483648)'; the failure is loud and immediate, with no wrong value, no index effect and no hang, and the default overloads (limit 1024) and every limit up to Integer.MAX_VALUE - 1 work.
- Summary
- You compile an expression with the new overload JavascriptCompiler.compile(source, functions, maxNestingDepth) and pass Integer.MAX_VALUE to ask for no practical nesting limit, and you expect it to accept at least everything that Integer.MAX_VALUE - 1 accepts, because the parameter is documented only as 'the maximum depth of nesting', with no bound. Instead every expression, even '1', fails with ParseException 'Nesting level too deep (>-2147483648)': the parse-time depth listener is created with maxNestingDepth + 1 to account for the root node, that int addition wraps to Integer.MIN_VALUE, and the depth check fails on the first parser rule. Integer.MAX_VALUE - 1 and the default limit of 1024 work, and the overload first shipped in 10.6.0 (released 2026-10-29).
- Introduced
- b6c2c31e22518503a8da0cd26b196217b30f4186 (main) and its branch_10x backport 1dc74b98be6d4a1b8b09c89ecd23fb404f759011 (branch_10_6 forks from branch_10x after it and contains it), PR #16423 'Add (configurable) guards for complexity of expressions' (author and merger uschindler; milestone 10.6.0; CHANGES entry under 10.6.0 Improvements), which added the compile(String, Map, int) overload, DEFAULT_MAX_NESTING_DEPTH, JavascriptNestingDepthListener and the 'maxNestingDepth + 1' line in one change [read: git log -S 'maxNestingDepth + 1' and git log -G 'maxNestingDepth' on both files return only this commit, and git blame attributes JavascriptCompiler.java:320 and JavascriptNestingDepthListener.java:45 to it; exec: main with the parent 96ff912d99d's JavascriptCompiler.java shadowed has no such overload, and lucene/expressions/src/java on main ea57360912ed is byte-identical to the commit] (2026-07-27 (PR #16423 merged 17:14:20Z; the branch_10x backport was committed the same day))
- First affected version
- 10.6.0 (released 2026-10-29), the first affected release (main, branch_10_6 and branch_10x since 2026-07-27); every release through 10.5.2 lacks the overload [exec: 9.12.3, 10.5.0, 10.5.1, 10.5.2]
- Affected versions
- main ea57360912ed (11.0.0-SNAPSHOT) [exec, -ea and -da: finder, the cross-model reviews, the independent reproduction, the intended-behaviour check, adjudication, history step and follow-up re-run]; the branch_10x backport 1dc74b98be6 and branch_10_6 head 2763db11b0a [exec as hybrids: the 10.5.2 jars plus that commit's JavascriptCompiler.java and JavascriptNestingDepthListener.java fail with the same ParseException (-ea and -da for the backport, -ea for branch_10_6), and pass when only the + 1 is saturated] [read: the line is unchanged at :358 on both branches upstream on 2026-10-26]. Not affected, because the overload does not exist [exec]: 9.12.3, 10.5.0, 10.5.1, 10.5.2, and main just before the commit (parent 96ff912d99d, hybrid). 10.6.0 (released 2026-10-29) is the first release that contains it. By code reading, the package-private compile(String, Map, int, boolean picky) takes the same path, and the default overloads (limit 1024, listener limit 1025) cannot overflow.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, follow-up prior-report record verdict new, 85 logged query lines: a scripted prior-report search over the dev@, java-user@, issues@ and commits@ lists (20 queries), JIRA (4) and the web (6); 40 GitHub GraphQL issue/PR searches, open and closed, including comments (22 in apache/lucene, 7 in Elasticsearch, OpenSearch and Solr, 11 date-bounded or across all of GitHub); #16423 and #16549 read in full (bodies, comments, reviews, inline review comments); the changed files of all 413 open apache/lucene PRs and of the 291 closed or merged PRs created since 2026-07-20; full-history git log -S and --grep; lists.apache.org and JIRA reads; downstream code search; earlier passes by the finder, the independent reproduction and a scripted prior-report search, 29 queries): no issue, PR, review comment, list post, JIRA entry, downstream report or web page describes it; not fixed: upstream main (:320), branch_10x and branch_10_6 (:358) still have the '+ 1', and no open PR touches JavascriptCompiler.java or JavascriptNestingDepthListener.java; related, not reports of this bug: #16423 (introduced the code; its review never mentions the + 1, overflow, Integer.MAX_VALUE or argument validation) and #16549 (test-only stack-size fix that keeps the StackOverflowError fallback);apache.org are not visible
- Lucene commit tested
- main
645d4fe14a20(tested 2026-10-29) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off. Earlier hunt runs: mainea57360912ed(the expressions module is identical), jars built at the 10.6.0 RC1 revision4ae946e5a9b, hybrids of the 10.5.2 jars with the expressions classes of the branch_10x backport1dc74b98be6and of branch_10_62763db11b0a, and the 9.12.3, 10.5.0, 10.5.1 and 10.5.2 release jars (overload absent). The fix was validated in the scratch worktree: the new test, the wholelucene/expressionsmodule with assertions on and off, and:lucene:expressions:check -x test.
- Finding record
lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:104-111 (class javadoc: the default limit of 1024 is enforced with a ParseException; @lucene.experimental at :111), :138 (DEFAULT_MAX_NESTING_DEPTH = 1024), :178-199 (the public overload; @param at :187 'the maximum depth of nesting (function calls, precedence - also implicit)'; no range, no validation), :266-271 (StackOverflowError fallback, comment 'too high limits may cause this'), :317-320 (listener created with maxNestingDepth + 1; comment 'will count root node, so add one recursion extra'), :404 (visitor-side checker with maxNestingDepth, no + 1)lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptNestingDepthListener.java:43-53 (enterEveryRule: depth++; if (depth > maxNestingDepth) throw 'Nesting level too deep (>' + maxNestingDepth + ')')read-only upstream check 2026-10-26; a re-run of the maxNestingDepth and JavascriptCompiler searches hit the GitHub search rate limit and was not retried
- The finding in 30 seconds
lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:104-111, (at 645d4fe14a20) :138, :154-176, :187-188, :195-199, :219-240, :306-322, :404lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptNestingDepthListener.java:24-59 (at 645d4fe14a20)lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptParser.java:215-217 (at 645d4fe14a20) (compile(): enterRule(_localctx, 0, RULE_compile))
- Who is affected
lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:219-227; (at 645d4fe14a20) lucene/expressions/src/test/org/apache/lucene/expressions/js/CompilerTestCase.java:37-38; lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java:512-517
- Why it looks unintended
lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:104-111, (at 645d4fe14a20) :187-188, :195-199, :219-240, :266-272, :317-320, :404lucene/CHANGES.txt:551-555 (at 645d4fe14a20) (GITHUB#16423, under Lucene 10.6.0)lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:256-261; (at 645d4fe14a20) lucene/join/src/java/org/apache/lucene/search/join/JoinUtil.java:512-517lucene/expressions/src/test/org/apache/lucene/expressions/js/CompilerTestCase.java:37-38, (at 645d4fe14a20) TestJavascriptCompiler.java:276-351
- History
Lucene main 645d4fe14a20 git log -- JavascriptCompiler.java JavascriptNestingDepthListener.java (b6c2c31e225 newest)lucene/CHANGES.txt:551-555 (at 645d4fe14a20)
- TestMaxNestingDepthMaxValue.java (JUnit test):
repro/LFV-182/src/org/apache/lucene/expressions/js/TestMaxNestingDepthMaxValue.java(35 lines)
It is made in parallel with this doc; its code rows are checked against main 645d4fe14a20 and its values come from its own probe on main with assertions on and off and with the fix.
assets/LFV-182/bug.png(496 KB)
