All findings · Lucene finding · LFV-965 · xml-queryparser-unbounded-nesting-recursion

XML query parser throws StackOverflowError instead of ParserException on deeply nested queries

Query XML nested a few thousand levels deep makes the XML query parser throw StackOverflowError instead of its declared ParserException: each level adds two Java calls and nothing counts them. On 10.6.0 with Java 21, parse(InputStream) fails at 3,000 nested BooleanQuery levels (about 180 KB) on a 1 MB stack.

Summary

Priority
Low, rank 820The XML query parser lets a StackOverflowError escape on deep nesting, the same class as LFV-926 and LFV-924, but the parser is niche.
Impact
wrong error
Since
1.9 (2006; the 1.9 tag already has the parser, first published jar 1.9.1)introduced 2006-02-25 · over 20 years
Affected
every release 1.9 to 10.6.0, and main (executed from 1.9.1; which entry fails depends on the JDK)
Status
documented
Found by
non-formal: a planning probe, then a bytecode recursion model and a 152-JVM depth sweep

AI-assisted page: this finding was first signalled by code reading or testing 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

One nesting level is <BooleanQuery><Clause occurs="must">...</Clause></BooleanQuery>, 60 bytes of XML; the repro wraps that around one <TermQuery> 100,000 times.

How the XML query parser builds a query

  1. CoreParser.parse(InputStream) reads the XML text with the JDK's XML parser into a DOM tree, then calls getQuery(Element) on the root. Applications may also call getQuery(Element) with a DOM they built.
  2. QueryBuilderFactory looks up a builder by element name (BooleanQuery, TermQuery, SpanOr, ...) and calls it.
  3. A compound builder such as BooleanQueryBuilder builds each child by calling the factory again. So the depth of the Java call stack grows with the depth of the XML.
Diagram of the bug: nested <BooleanQuery><Clause occurs="must"> XML is built into a query by BooleanQueryBuilder and QueryBuilderFactory calling each other once per level; the Java call stack fills and java.lang.StackOverflowError escapes CoreParser instead of a Query or the declared ParserException. On Java 21 the plain parse(InputStream) fails this way; on Java 25 XML text deeper than 100 elements gets a ParserException from the JDK, while getQuery(Element) on a DOM failed on every JDK tested.
Each nesting level of the query XML adds a builder call and a factory call to the Java stack, until StackOverflowError escapes where a Query or ParserException is expected. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You parse a query with CoreParser.parse(InputStream): <BooleanQuery><Clause occurs="must"> repeated 3,000 times around one <TermQuery>, about 180 KB of well-formed XML. The shipped DTD says elements nest to "an arbitrary depth and complexity", and 40 levels parse fine.
  2. You expect a Query, or the ParserException the method declares, which parseXML already throws for every Exception from the XML parser.
  3. Instead, on 10.6.0 with Java 21 and a 1 MB thread stack, java.lang.StackOverflowError escapes (2,000 levels still parse; on the 2 MB default stack it usually takes 5,000, about 300 KB). catch (ParserException e) and catch (Exception e) miss it.
  4. Why: BooleanQueryBuilder builds its child by calling QueryBuilderFactory.getQuery, which calls the next builder: two Java frames per level, never counted. The DisjunctionMax, ConstantScore and span builders do the same.
  5. The JDK decides where it shows: Java 25, main's minimum, rejects XML text deeper than 100 elements with a ParserException, but no JDK XML setting applies to getQuery(Element) on a DOM the application builds: it overflowed on every JDK tested (3,000 levels at -Xss1m on main).

Who is affected

Applications using the opt-in XML query parser (CoreParser and subclasses, default settings) that rely on ParserException for bad input: on 10.x with Java 21 through plain parse(InputStream); on any JDK through getQuery(Element) on an application-built DOM. Real queries nest a handful of levels; this needs thousands. Only the error type is wrong: no query is returned, the JVM continues.

Mechanism, terms and numbers797 words

CoreParser turns query XML into a Lucene Query by recursion: each compound builder (BooleanQuery, DisjunctionMaxQuery, ConstantScoreQuery and the span builders) calls back into QueryBuilderFactory or SpanQueryBuilderFactory for its children, so every nesting level adds two Java frames, and nothing counts them. Well-formed XML from the documented vocabulary, nested a few thousand levels deep, therefore throws java.lang.StackOverflowError out of parse(InputStream), getQuery(Element) and getSpanQuery(Element), which declare only ParserException. Whether the text entry gets that far depends on the JDK's XML parser: Java 21, the 10.x minimum, has no element-depth limit by default, so on 10.6.0 plain parse(InputStream) overflows at 3,000 nested BooleanQuery levels (about 180 KB) on a 1 MB stack and 5,000 (about 300 KB) on the 2 MB default; Java 25, main's minimum, rejects text deeper than 100 elements with a ParserException (tested builds: OpenJDK 21.0.10 and 25.0.3). No JDK XML setting applies to the DOM entries; they overflowed on every JDK tested. Nothing else goes wrong: no query is returned, no index effect, and the JVM continues. The recursion dates from the parser's original contribution (2006; in the 1.9 release tag, first published as a jar in 1.9.1). A nesting limit in CoreParser, the approach #16423 took for the expressions parser in 10.6.0, fixes it; the repro and the 69 existing XML-parser tests pass with it.

Terms

  • CoreParser: the XML query parser in lucene-queryparser (org.apache.lucene.queryparser.xml), with the subclasses CorePlusQueriesParser and CorePlusExtensionsParser. Its element vocabulary is documented in LuceneCoreQuery.dtd.
  • Entry points: parse(InputStream) (XML text), and the public getQuery(Element) / getSpanQuery(Element) (a DOM element the caller supplies). All three declare only ParserException.
  • Element-depth limit: the JDK's XML parser setting jdk.xml.maxElementDepth. In the tested OpenJDK 25.0.3 build, conf/jaxp.properties:184 sets it to 100; in the tested 21.0.10 build, line 163 is commented out (# jdk.xml.maxElementDepth=0), so there is no limit (other builds were not checked).

What happens (file:line at 645d4fe14a20)

  1. CoreParser.java:141-143: parse(InputStream) returns getQuery(parseXML(xmlStream).getDocumentElement()). parseXML (:182-206) turns off validation (:184), enables FEATURE_SECURE_PROCESSING (:186; a ParserConfigurationException there is ignored, :187-192) and wraps every Exception from creating the parser or parsing the text in a ParserException (:196-198, :203-205); it catches Exception, not Error. Lucene sets no depth limit of its own.
  2. CoreParser.java:208-216: getQuery(Element) and getSpanQuery(Element) hand the element straight to queryFactory / spanFactory, with no depth state.
  3. QueryBuilderFactory.java:29-35 looks up the builder by element name and calls it (:34). BooleanQueryBuilder.java:57 calls factory.getQuery(clauseQuery) for each clause; DisjunctionMaxQueryBuilder.java:54 and ConstantScoreQueryBuilder.java:41 do the same, and the span builders call SpanQueryBuilderFactory.java:46 (SpanOrBuilder.java:41, SpanNearBuilder.java:45, SpanFirstBuilder.java:38, SpanNotBuilder.java:42-43, SpanPositionRangeBuilder.java:39).
  4. Each nesting level adds a builder frame and a factory frame. The executed trace cycles BooleanQueryBuilder.getQuery(BooleanQueryBuilder.java:57) and QueryBuilderFactory.getQuery(QueryBuilderFactory.java:34) until StackOverflowError.
  5. Two more recursions, found by the hunt's static model and exercised only in its sweep: DOMUtils.getTextBuffer (DOMUtils.java:157-177, once per nested element at :167 and per entity reference at :172) and DOMUtils.getAttributeWithInheritance (:88-101, once per ancestor). These four cycles are the XML module's own; the classic QueryParser that UserInputQueryBuilder calls (UserInputQueryBuilder.java:59-73) and custom builders are outside the model.

Thresholds (last grid point that parsed, first that overflowed; Linux aarch64)

Lucene, JDK, entry-Xss1mdefault stack (2 MB)
10.6.0 commit, Java 21, parse(InputStream), nested BooleanQuery2,000 parse, 3,000 (about 180 KB) overflow3,000 parse, 5,000 (about 300 KB) overflow (7 of 10 runs in the fact check; 7,500 in 4 of 4)
same, nested <SpanOr>2,000 parse, 3,000 (about 51 KB) overflow5,000 parse, 7,500 overflow
main, Java 25, getQuery(Element), nested BooleanQuery2,000 parse, 3,000 overflow5,000 parse, 7,500 overflow
main, Java 25, parse(InputStream)ParserException from 101 elements (the JDK's limit)same
main, Java 25, parse(InputStream), -Djdk.xml.maxElementDepth=05,000 parse, 10,000 overflow

Rows 1-4 come from the hunt's 152-JVM sweep: one JVM per shape, entry, stack and JDK, trying depths 50, 100, ..., 2,000, 3,000, 5,000, 7,500, ... in ascending order until the first overflow. The 1,000 / 3,000 / 5,000 / 10,000-level points of rows 1 and 3 were also run as cold single calls, one call per fresh JVM, with the same outcome, and row 5 comes from those cold single calls only; the audit probe (ascending, one JVM) agrees on row 3. The fact check re-ran row 1 on the 10.6.0 release jars: at 1 MB, 2,000 levels parsed and 3,000 overflowed 6 of 6 times each. The main rows ran on a9779ef3c193; no commit touches the xml module between it and 645d4fe14a20. Warm JVMs need several times more levels (the independent reproduction: 1,933 to 24,872 on the 10.6.0 commit's jars with Java 21, 256 KB to 2 MB stacks). x86_64 was not measured.

Exposure and likelihood255 words
  • 10.x and 9.x on Java 21: the plain parse(InputStream) of well-formed XML, default settings (executed on the 10.6.0 commit, 10.5.2 and 9.12.3; 9.12.3 also on Java 17).
  • Any JDK (no JDK XML setting applies to a DOM): getQuery(Element) (executed on Java 17, 21 and 25) / getSpanQuery(Element) (21 and 25) with a DOM the application builds (a 2019 java-user@ thread shows applications doing this).
  • Main: also with another JAXP implementation on the classpath (xercesImpl 2.12.2, executed) or a relaxed jdk.xml.maxElementDepth (executed with 0).
  • Default vs opt-in: the XML query parser is an opt-in module class; the trigger needs no non-default setting.
  • Likelihood: contrived. On 1-2 MB stacks it takes about 3,000-5,000 levels; only an adversary or a broken generator produces that.
  • Effect: the calling thread gets java.lang.StackOverflowError, which catch (ParserException) and catch (Exception) handlers miss. No query is returned, no index or data is touched, and the JVM and later parses continue (CoreParser keeps no per-parse state; QueryBuilder.java:22-26 requires thread-safe builders).
  • The parser fails first: a nest of one clause per level around one term still searches where the parser already overflows (1 MB stack: 2,000 levels parse and IndexSearcher.count returns 1, 3,000 overflow in the parser; 2 MB: 5,000 parse and search, 10,000 overflow in the parser). Core search is not unbounded either: IndexSearcher.count and toString of the same tree built in Java overflowed at 5,000 and 7,500 levels on a 1 MB stack (JIT-dependent), so the parser is the earliest limit, not the only one.

Minimal repro

One JUnit class, one test method, queryparser module, public API: it builds 100,000 nested <BooleanQuery><Clause occurs="must"> levels as a DOM around one <TermQuery> and calls CoreParser.getQuery(Element). A Query or ParserException passes; StackOverflowError fails. Deterministic, single thread, no index; fails on main (Java 25), and no JDK XML setting protects the DOM entry.

TestCoreParserDeepNesting.java (one test class)48 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.queryparser.xml;

import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

public class TestCoreParserDeepNesting extends LuceneTestCase {

  public void testDeeplyNestedBooleanQuery() throws Exception {
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
    Element query = doc.createElement("TermQuery");
    query.setAttribute("fieldName", "f");
    query.setTextContent("a");
    for (int i = 0; i < 100_000; i++) {
      Element clause = doc.createElement("Clause");
      clause.setAttribute("occurs", "must");
      clause.appendChild(query);
      query = doc.createElement("BooleanQuery");
      query.appendChild(clause);
    }
    CoreParser parser = new CoreParser("f", new MockAnalyzer(random()));
    // expected: a Query or the declared ParserException; actual: StackOverflowError
    try {
      parser.getQuery(query);
    } catch (@SuppressWarnings("unused") ParserException expected) {
    } catch (StackOverflowError e) {
      throw new AssertionError("expected a Query or ParserException, got StackOverflowError", e);
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestCoreParserDeepNesting -Ptests.seed=DEADBEEF -Ptests.asserts=true
Expected
getQuery(Element) returns a Query or throws the declared ParserException.
Observed
expected a Query or ParserException, got StackOverflowError, the stack cycling BooleanQueryBuilder.java:57 and QueryBuilderFactory.java:34, with assertions on and off.

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

More files, environment, full output407 words

It uses the DOM entry because that fails on main, where Java 25's XML depth limit guards the text path. 100,000 levels give a wide margin over the 3,000-10,000 needed on 1-2 MB stacks; on a much larger thread stack the stock code could return a Query and the test would pass (not tried). The whole test method, DOM build and failing call, runs in about 0.1-0.3 s (gradle test reports). ./gradlew tidy re-wraps the one-line catch (@SuppressWarnings("unused") ParserException expected) onto three lines; nothing else changes. The line numbers in the observed trace are those of the file as shipped.

On 10.x the text path behaves as follows (no second test needed): on 10.6.0 with Java 21 and -Xss1m, parse(InputStream) of 3,000 nested BooleanQuery levels (about 180 KB) throws StackOverflowError, while Java 25 gives ParserException from the JDK's depth limit. The hunt's and the independent reproduction's runs swapped only the JDK binaries in the harness's generated command (the harness is pinned to Java 25); the fact check re-ran it with its own probe on the 10.6.0 release jars and OpenJDK 21.0.10: 2,000 levels parse and 3,000 overflow, 6 of 6 times each.

Regression test: this same class (or its one method added to TestCoreParser). It passes with the recommended fix and asserts only the contract (a Query or ParserException), not a particular limit. On its own it does not show whether the fix's depth check or its fallback fired; the validation-only 511/512 method covers the boundary.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; 10.6.0 and 9.12.3 release jars: fail; with the fix: passes; upstream main 2f424fd428da has the same code
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run)
Command (environment)
cp $REPRO/TestCoreParserDeepNesting.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/xml/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestCoreParserDeepNesting -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=false (test-result XML; stack trimmed)
java.lang.AssertionError: expected a Query or ParserException, got StackOverflowError
	at __randomizedtesting.SeedInfo.seed([DEADBEEF:176E3F6D1EC607DC]:0)
	at org.apache.lucene.queryparser.xml.TestCoreParserDeepNesting.testDeeplyNestedBooleanQuery(TestCoreParserDeepNesting.java:45)
Caused by: java.lang.StackOverflowError
	at java.base/java.lang.StringLatin1.compareTo(StringLatin1.java:114)
	...
	at org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder.getOccursValue(BooleanQueryBuilder.java:71)
	at org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder.getQuery(BooleanQueryBuilder.java:54)
	at org.apache.lucene.queryparser.xml.QueryBuilderFactory.getQuery(QueryBuilderFactory.java:34)
	at org.apache.lucene.queryparser.xml.builders.BooleanQueryBuilder.getQuery(BooleanQueryBuilder.java:57)
	at org.apache.lucene.queryparser.xml.QueryBuilderFactory.getQuery(QueryBuilderFactory.java:34)
	... (the last two frames repeat to the end of the recorded trace)

:lucene:queryparser:test (FAILURE): 1 test, 1  failure

# -Ptests.asserts=true: the same failure (1 test completed, 1 failed). The frame where the stack runs out varies
# between runs. The standalone runner gives the same message on main (-ea, -da) and on the 10.6.0 and 9.12.3 jars (-da).

Why it looks unintended

  • The three entries declare only ParserException; parseXML turns every Exception of the XML parser into one.
  • The DTD says elements nest "to produce queries/filters of an arbitrary depth and complexity"; nothing documents a limit.
  • One input and the same 10.6.0 jars give ParserException on Java 25 and StackOverflowError on Java 21. #16423 fixed the same defect in the expressions parser.
The full argument254 words
  • Declared exception. parse(InputStream), getQuery(Element) and getSpanQuery(Element) declare only ParserException (CoreParser.java:141, :209, :214), documented as "Thrown when the xml queryparser encounters invalid syntax/configuration" (ParserException.java:19); parseXML wraps every Exception from the XML parser (CoreParser.java:196-205; it catches Exception, so an Error is not wrapped). This is an expectation from the throws clause, not a @throws guarantee: parse has no @throws text (CoreParser.java:137-140), and a checked throws clause promises nothing about Errors.
  • Documented input. LuceneCoreQuery.dtd:54-57: "Many of the elements can nest other elements to produce queries/filters of an arbitrary depth and complexity. The BooleanQuery element is one such example ... (including other BooleanQueries)". The DTD documents the vocabulary; it is not a runtime contract, since CoreParser does not validate against it (dbf.setValidating(false), CoreParser.java:184). The upstream XML tests check only shallow nesting.
  • JDK-dependent outcome. For one input and one Lucene release, the result flips between ParserException (Java 25) and StackOverflowError (Java 21). That points to an accident, not a design.
  • Precedent. #16423 (uschindler, approved by rmuir and msokolov; 10.6.0) added a nesting limit of 1,024 and a StackOverflowError fallback to the expressions parser; lucene/CHANGES.txt:551-555 says it "no longer throws virtual machine errors" (JavascriptCompiler.java:104-105, :138, :266-271). On #11687 (SimpleQueryParser stack overflow, open since 2022) mikemccand asked "I wonder if our other query parsers have this problem too?"; nobody answered.
  • SECURITY.md:46: issues outside the security scope "are still valid bug reports and hardening suggestions" that can be filed publicly.
The devil's-advocate check250 words

The strongest case for "intended" (rated strong by reading-only cross-model lenses, moderate by the intended-behaviour check): (1) a throws clause says nothing about VirtualMachineErrors, and deep nesting is valid syntax, so "invalid syntax/configuration" arguably does not apply; (2) SECURITY.md:38 names "deeply nested queries" as input the application must bound, and SECURITY.md:42 trusts caller-supplied data structures such as a DOM; (3) on #752 rmuir wrote "If you hit StackOverFlowError, really you should let the VM exit", and on #16423 uschindler wrote "The stopped JVM is actually not a bug in Lucene" (#16423 is labelled an enhancement); (4) on main the stock text path is already guarded by the JDK 25 limit; (5) core search also overflows on deep trees; (6) #16820 proposes deprecating the XML parser for Lucene 11 ("mainly a source of security issues", romseygeek).

Why it fails: none of this makes the Error the documented behaviour of a method whose only declared failure is ParserException, for well-formed input the DTD invites; the result for one input flips with the JDK; and the same reviewers approved a nesting limit for the expressions parser in #16423, whose description says "this should be handled". On #752 msokolov wrote "we should not be catching Error" but "If we could somehow detect the problem before it gets to that, then throwing IAE would make sense", which is what the recommended fix does with the parser's own exception. Points (2) and (6) settle the security rating (none), not whether it is a bug.

Suggested fix

Recommended A deterministic nesting limit in CoreParser, with a StackOverflowError fallback

In CoreParser.getQuery(Element) and getSpanQuery(Element), first walk the element's ancestors and subtree without recursion and throw ParserException past a depth of 1,024 DOM nodes, about 511 BooleanQuery levels (overridable, as #16423 did for expressions), then convert any StackOverflowError from the builders into ParserException. The repro and the 69 existing XML-parser tests pass.

  @Override
  public Query getQuery(Element e) throws ParserException {
    checkNestingDepth(e);
    try {
      return queryFactory.getQuery(e);
    } catch (StackOverflowError soe) {
      // the depth check should catch this before, but a large limit or a small stack may not
      throw new ParserException("Query nesting level too deep", soe);
    }
  }
All options and trade-offs4 options ยท 825 words

One fix was validated in a scratch worktree of 645d4fe14a20; the alternatives were not run on their own. Whether a fix is wanted at all, given the proposal to deprecate the XML parser for Lucene 11 (#16820), is the maintainers' call; 10.6 is the last 10.x minor, so a 10.x fix would go into a 10.6.x bugfix release.

A deterministic nesting limit in CoreParser, with a StackOverflowError fallback (validated by the checks) (recommended)

The change is +73/-3 lines in CoreParser.java and LuceneCoreQuery.dtd (the hunt's sketch with wording edits, formatted with ./gradlew tidy): a public DEFAULT_MAX_NESTING_DEPTH = 1024 and a protected getMaxNestingDepth(); getQuery(Element) / getSpanQuery(Element) first run an iterative check of the element's ancestors and subtree that throws ParserException("Query nesting level too deep (maximum 1024 levels)"), then call the factory inside a StackOverflowError catch that throws ParserException. The DTD's "arbitrary depth" sentence now names the limit. The same shape uschindler used in #16423.

  @Override
  public Query getQuery(Element e) throws ParserException {
    checkNestingDepth(e);
    try {
      return queryFactory.getQuery(e);
    } catch (StackOverflowError soe) {
      // the depth check should catch this before, but a large limit or a small stack may not
      throw new ParserException("Query nesting level too deep", soe);
    }
  }

Trade-offs

  • Deterministic once a DOM reaches getQuery(Element) / getSpanQuery(Element), whatever the JDK or JAXP implementation, with the parser's own exception; overridable like the expressions limit. On the text path the JAXP parser still builds the DOM first, and an Error thrown there (an OutOfMemoryError, say) is not converted. The fallback turns any StackOverflowError from a builder, a custom builder's included, into "nesting too deep".
  • The check counts the depth in DOM nodes (elements, text and comment nodes, and the ancestors of the element passed in; not attributes), a leaf's text node included: 511 nested BooleanQuery levels (1,024 nodes) are accepted, 512 rejected. So the "1024 levels" in the message and the DTD sentence means nodes. On Java 25 the JDK already rejects text deeper than 100 elements, so main's text path does not change; on Java 21, queries from 512 levels up to today's overflow point (2,000-5,000 levels cold on 1-2 MB stacks, more when warm: the independent reproduction measured up to about 24,000) would now be rejected. Counting builder levels instead is a small change.
  • One extra iterative pass over the whole DOM per parse, even for a large flat query; its cost was not measured.
  • 1,024 nodes can still overflow a very small stack (500 BooleanQuery levels overflow at -Xss256k in the hunt's sweep), hence the fallback. On #752 rmuir and msokolov objected to catching StackOverflowError; #16423 later kept such a catch behind its limit. The catch can be dropped.
  • Covers CoreParser and its subclasses, not direct use of the public QueryBuilderFactory, the builders or the internal DOMUtils.
  • New public API (a constant and a protected method), which could stay private; the change also needs a CHANGES entry.
  • The regression test asserts only the contract, so it does not show which of the two mechanisms fired; the validation-only 511/512 method covers the boundary.

What the checks validated

  • Gradle --no-daemon, scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, 2026-10-31. Stock: the repro fails with assertions on and off.
  • With the fix: the repro passes with assertions on and off; :lucene:queryparser:test --tests 'org.apache.lucene.queryparser.xml.*' passes 73 tests, 0 failures, with seeds DEADBEEF and CAFEBABE: the 69 upstream tests (TestCoreParser 22, TestCorePlusQueriesParser 23, TestCorePlusExtensionsParser 24), the repro, and 3 validation-only methods (511 levels parse, 512 get the depth ParserException; 100,000 nested SpanOr through getSpanQuery get it too; with the limit raised to Integer.MAX_VALUE, 100,000 levels get a ParserException caused by StackOverflowError).
  • checkGoogleJavaFormat, forbiddenApisMain, ecjLintMain and renderJavadoc of :lucene:queryparser pass.
  • Earlier, the hunt's sketch passed the hunt's tests on main (-ea, -da, with xercesImpl) and on the 10.6.0 commit with Java 25 and 21, and the independent reproduction's 4-test class. Not run: the whole :lucene:queryparser:test.

Only catch StackOverflowError in getQuery / getSpanQuery

Wrap the factory calls in the catch above, without a limit.

Trade-offs

Smallest change, but where it fails still depends on stack size and JIT state, and it is what rmuir and msokolov objected to on #752. Not run on its own (the third validation-only method exercises the same catch).

Count depth in QueryBuilderFactory and SpanQueryBuilderFactory

Track the depth per call and throw ParserException past the limit.

Trade-offs

Also covers direct use of the factories, but needs per-call state (one CoreParser may parse on many threads at once, QueryBuilder.java:22-26), for example a ThreadLocal or a changed builder signature, and adds work to every builder call. Not run.

Document the limit only

Say in the javadoc and the DTD that callers must bound nesting (for text, with jdk.xml.maxElementDepth).

Trade-offs

No code change, but it does not help the DOM entries or other JAXP implementations, and the result for one input still depends on the JDK. Not run.

Downstream note

Solr's xmlparser query parser passes the request's query text to SolrCoreParser.parse(InputStream) and catches only ParserException (code read, not run).

How it was found

A hunt planning probe printed StackOverflowError for parse(InputStream) on the 10.6.0 jars with Java 21; an earlier hunt had tested Java 25 only. A static recursion model over the parser's bytecode then found the unbounded recursive cycles, a 152-JVM depth sweep mapped the failing depth, and a public-API JUnit followed.

The method in full255 words

Method: non-formal (the first signal was an executed probe; the model and the sweep came after, to characterise it).

  • Lead. The hunt planners asked whether the XML query parser is a sibling of the expressions nesting fix (#16423) once the Java 25 XML limit is out of the way. Their main() probes printed StackOverflowError for CoreParser.parse(InputStream) at 5,000 and 20,000 nested BooleanQuery levels on the 10.6.0 commit's jars with Java 21 (1,000 parsed), for parse on main with xercesImpl on the classpath, and for getQuery(Element) on main at 20,000. That overturned an earlier hunt's negative, which had tested Java 25 only.
  • Model. A static recursion model over the real bytecode (class-hierarchy call graph, Tarjan strongly connected components, guard ranges and a depth-state detector) found four stateless, unguarded recursive cycles in the XML module's own code, reachable from parse, getQuery and getSpanQuery (the classic QueryParser that UserInputQueryBuilder calls, and custom builders, are outside it); its vacuity probes were classified correctly, and the CoreParser entries flip to safe with the fix.
  • Sweep. 19 shape and entry pairs (10 shapes x 2 entries, minus the DOM-only inherit shape's text entry) x 4 stack sizes x {Java 21 + 10.6.0 commit jars, Java 25 + main jars} = 152 JVMs. Each tried depths from 50 upward in ascending order and stopped at the first overflow, giving the last passing and first overflowing grid point per combination.
  • Checked by an independent reproducer, a devil's-advocate check, reading-only cross-model reviews, an adjudication, a credibility audit and a noise audit (see below).

Prior reports, history and checks

Prior reports

No prior report found: 513 query records on 2026-10-28 (GitHub incl. the JIRA archive and downstream trackers, the mailing lists, advisories, the web) and the file lists of all 414 open PRs, re-checked 2026-10-31. Closest: #11687 (SimpleQueryParser overflow), #16423 (expressions limit, fixed) and #16820 (deprecation proposal).

Searches3 searches

Verdict new, meaning the searches below found no public report (keyword search cannot prove absence). Not fixed upstream: no commit touches the module between 645d4fe14a20 and upstream main 2f424fd428da. Related hunt rows, kept separate (other parsers, other fix sites): the surround, simple and classic parser rows (LFV-919, LFV-924, LFV-926); the flexible parser overflows the same way in its processor phase.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; Solr, Lucene.NET, Elasticsearch, OpenSearch, all of GitHub)2026-10-28five runs of a scripted prior-report search (16 GitHub, 3 commit, 2 downstream queries each), the finder's 51-query run re-read, 55 manual GitHub searches, 4 code searches, and the changed files of all 414 open apache/lucene PRsbackground only: #11687, #16423, #752 / #11510, #16820, the external-entity items #15234, #16135, #9338 and SOLR-11477; stale open PRs #15053 and #12295 touch DOMUtils and keep the recursion
Mailing lists, JIRA, advisories and the web2026-10-2820 list and 4 JIRA-archive queries per tool run, 41 direct list queries plus every dev@ mail of the last 40 days, 15 LUCENE and SOLR JIRA queries, GitHub security advisories and project security pages, 33 web queriesno report; a 2019 java-user@ thread shows applications calling getQuery(Element) on their own DOM
Refresh2026-10-31read-only gh searches (CoreParser, xml query parser, queryparser xml nesting, CoreParser StackOverflowError, nesting depth query parser) and every issue and PR created since 2026-10-28nothing new; #16949 touches only a surround-parser javadoc in lucene/queryparser

History

Present since the parser's original contribution (87768c51c632, 2006-02-25), in the 1.9 release tag and first published as a jar in 1.9.1; never bounded. Executed as failing on 1.9.1, 2.1.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main.

Versions tested7 rows

Introduced by 87768c51c632 (Subversion r380874, 2006-02-25, Mark Harwood, "Added XML-query-parser module for new extensible query parser that handles queries expressed as XML"), the original contribution; no JIRA issue or PR. It already has the four original recursion families (the query and span builders calling back into their factories, DOMUtils.getTextBuffer, DOMUtils.getAttributeWithInheritance). With git blame -w -M -C -C at 645d4fe14a20, BooleanQueryBuilder.java:57, QueryBuilderFactory.java:34, SpanQueryBuilderFactory.java:46, the SpanOr, SpanNear, SpanFirst and SpanNot builder lines, DOMUtils.java:96, :167, :172 and CoreParser.java:142, :210 resolve to it; plain git blame stops them at e227a64b9d73 (LUCENE-3311, 2011, a cleanup). Later commits widened the recursion, and the other call sites blame to them: the DisjunctionMaxQuery builder (LUCENE-4004, 36bc3d70bb37, 4.0.0; DisjunctionMaxQueryBuilder.java:54), nested ConstantScoreQuery (LUCENE-6300, 544f5bf1e7b7, 5.1.0; ConstantScoreQueryBuilder.java:41), the public getSpanQuery(Element) (LUCENE-7658, 61ab4e338e35, 6.5.0; CoreParser.java:215, so earlier releases have two of the three entry points), SpanPositionRange (70162d3fb1a0, 8.3.0; SpanPositionRangeBuilder.java:39). git log -S depth / -S StackOverflowError over the module's three historical locations find no bound ever added. SOLR-11477 (2017) added FEATURE_SECURE_PROCESSING, which does not bound depth on Java 21. First release: the releases/lucene/1.9 tag (2006-02-27, "Tagged 1.9 final release") already has contrib/xml-query-parser, with no difference from 1.9.1 (read); 1.9.1 (2006-03-02) is the first version with a published lucene-xml-query-parser jar and the first one executed. On 1.9.1-2.0.0 the BooleanQuery shape runs out of heap first (a separate getElementsByTagName defect fixed for 2.1.0 in 6c53bc3c497), so 1.9.1 was run with SpanOr. Which JDK release first shipped the element-depth default of 100 was not determined.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (DOM entry, Java 25); text entry gets ParserException from the JDK limit
10.6.0 (release jars; commit 4ae946e5a9b9) affectedminimal repro fails (Java 25); parse(InputStream) overflows on Java 21 (3,000 levels at -Xss1m)
10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4 affectedDOM entry overflows on Java 25 and 21; parse(InputStream) overflows on Java 21 (9.12.3 also on Java 17)
4.0.0, 3.6.2, 2.1.0 affectedthe same pattern (history probe, depth 100,000)
1.9.1 (first published jar) SpanOr shapethe same pattern with nested SpanOr
1.9 (release tag) readthe same contrib/xml-query-parser source as 1.9.1 (read, not run)
main with the recommended fix fixrepro passes; 69 upstream XML-parser tests pass

Independent checks

The finder, an independent reproducer and a devil's-advocate check found it real; an adjudication confirmed it (medium confidence, about acceptance); two audits kept it; fact checks re-ran it. The cross-model fact check (OpenAI GPT-5.6) ran a probe and agrees it is real, but calls it a plausible Low hardening issue of uncertain acceptance.

Checks and verdicts534 words

Every executed check ran on stock builds or harness-checked release jars and reproduced the failure; the Java 21 legs rest on the finder's, the independent reproduction's and separate runs and on the fact checks' own probes on the 10.6.0 release jars.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (planning probe, recursion model, depth sweep) candidateIts JUnit fails on main (-ea, -da; with xercesImpl both deep tests), on the 10.6.0 commit, 10.5.2 and 9.12.3 with Java 25 (DOM test) and with Java 21 (both deep tests); the model finds four unguarded recursive cycles in the XML module's own code.
Refute lens (OpenAI GPT-5.6, reading only) not real, mediumAgreed on the root cause and the inputs; rated the throws-clause / SECURITY.md argument strong. Could not run Java.
Intended lens (OpenAI GPT-5.6, reading only) real, mediumIntended argument weak: the DTD documents arbitrary nesting and nothing states a limit.
Independent reproducer realOwn 4-test class fails 2 of 4 on Java 25 (main, 10.6.0 commit, 10.5.2, 9.12.3) and 3 of 4 on Java 21; 4 of 4 pass with the fix; measured the cold and warm thresholds.
Root-cause lens (OpenAI GPT-5.6, reading only) and separate re-runs uncertain, medium; re-runs failRoot cause agrees; the re-runs fail on main and on the 10.6.0 commit (Java 25 and 21) and pass with the fix.
Intended-behaviour devil's advocate realIntended case moderate and rejected; fails on main, 10.5.2, and main with -Djdk.xml.maxElementDepth=0 (both entries).
Adjudication confirmed, mediumRe-ran the independent reproduction's class (2 of 4 fail) and the finder's (1 of 3) on main; the doubt is about acceptance, not the facts.
Credibility audit keepRe-ran on main; its probe shows the parser overflows before search does.
Noise audit (2026-10-29) kept as LowChallenged as an absurd-parameter loud failure and kept; noted that the parser-nesting rows need one consistent rule.
This doc (2026-10-31) real; fix validatedGradle in a scratch worktree: stock fails -ea and -da, the fix passes with the module's XML tests and checks; the repro fails on the 10.6.0 and 9.12.3 jars.
Independent fact check (2026-10-31) real; 3 wording errors, fixedRan the documented gradle command in its own clean scratch worktree (fails with assertions on and off), the standalone runner on main, 10.6.0 and 9.12.3, the recommended fix (73 of 73 XML tests pass) and its own Java 21 probe on the 10.6.0 release jars.
Cross-model fact check (OpenAI GPT-5.6) real and reproducible; 5 errors, fixedRead the code and the follow-up, checked that the fix applies, and ran a small probe (one thread with a 1 MB stack per parse, in one JVM) on main with Java 25 and on the 10.6.0 jars with Java 21 and 25: 50 levels give the JDK's ParserException on Java 25, 3,000 overflow on Java 21, later shallow parses work. Rates the case for "acceptable hardening" strong and would treat it as a plausible Low hardening issue whose acceptance is uncertain.
Diagram check (2026-10-31) passStills of the PNG checked against the code at 645d4fe14a20 and the executed runs; no wrong fact, four wording nits.

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,806 words

Finding record

LFV-965, snapshot of 2026-11-06

Registry title
XML query parser: CoreParser.getQuery(Element) and getSpanQuery(Element) on any JDK, and plain CoreParser.parse(InputStream) on JDK 21 (the 10.x minimum, including the 10.6.0 RC), throw StackOverflowError instead of the declared ParserException on deeply nested queries (on a 1 MB thread stack about 3,000 nesting levels: about 180 KB of nested BooleanQuery XML or 51 KB of nested SpanOr; every release since 1.9.1)
Area
queryparser / xml (CoreParser, QueryBuilderFactory and SpanQueryBuilderFactory with the compound and span builders, DOMUtils)
Priority
Low, rank 820: The XML query parser lets a StackOverflowError escape on deep nesting, the same class as LFV-926 and LFV-924, but the parser is niche.
Impact class
wrong error
Impact note
Hits applications that use the opt-in XML query parser (CoreParser and its CorePlusQueriesParser and CorePlusExtensionsParser subclasses, default settings) on deeply nested query XML and rely on its declared ParserException: on the 10.x line running on JDK 21 (the 10.x minimum; executed on the 10.6.0 RC and 10.5.2, and on 9.12.3 with JDK 17 and 21) the plain parse(InputStream) of well-formed XML overflows at about 3,000 nested BooleanQuery levels (about 180 KB) on a 1 MB thread stack and 5,000 (about 300 KB) on the 2 MB aarch64 default, while on main (JDK 25 only) the JDK XML parser's default element-depth limit of 100 turns the same text into ParserException, so main is reached only through the public getQuery(Element)/getSpanQuery(Element) on an application-built DOM (any JDK), another JAXP implementation such as xercesImpl, or a relaxed jdk.xml.maxElementDepth; java.lang.StackOverflowError escapes with -ea and -da in every release since 1.9.1, the depth needed depends on stack size and JIT state (only aarch64 measured), and nothing else goes wrong (no query is built, no wrong hits, no index effect, the JVM and later parses continue).
Summary
You give the XML query parser a well-formed query whose elements nest a few thousand levels deep, for example <BooleanQuery><Clause occurs="must"> repeated 3,000 times around one <TermQuery> (about 180 KB; enough on a 1 MB thread stack, while the 2 MB aarch64 default needs 5,000 or more levels), which the shipped DTD allows ('queries/filters of an arbitrary depth and complexity') and which parses fine at 40 levels, and you expect a Query or the ParserException that CoreParser.parse(InputStream), getQuery(Element) and getSpanQuery(Element) declare. Instead java.lang.StackOverflowError escapes, because each compound builder (BooleanQuery, DisjunctionMaxQuery, ConstantScoreQuery and the span builders) calls back into QueryBuilderFactory or SpanQueryBuilderFactory once per nesting level and nothing counts the depth or turns the Error into a ParserException. Whether the plain text path fails depends on the JDK, not on Lucene: JDK 21, the minimum for 10.x including the 10.6.0 RC, has no XML element-depth limit by default and overflows, while JDK 25 (main's minimum) rejects more than 100 nested elements with a ParserException; the DOM entries overflow on every JDK.
Introduced
87768c51c632a3eefd0e78836d900ced46f0e10e (Subversion r380874, 2006-02-25, 'Added XML-query-parser module for new extensible query parser that handles queries expressed as XML ...', the module's original contribution; the message names no JIRA issue and there is no pull request). The unbounded recursion was born with the parser and is not a regression [read: git blame of the recursive call sites on main (BooleanQueryBuilder.java:57, QueryBuilderFactory.java:34, SpanQueryBuilderFactory.java:46, the SpanOr/SpanNear/SpanFirst/SpanNot builders, DOMUtils.java:96, :167 and :172, CoreParser.java:142 and :210) resolves them to 87768c51c632; git log -S depth and -S StackOverflowError over contrib/, lucene/contrib/ and lucene/queryparser/.../xml find no bound ever added; later commits only widened the recursion: DisjunctionMaxQuery builder (LUCENE-4004, 36bc3d70bb3, 4.0.0), nested ConstantScoreQuery (LUCENE-6300, 544f5bf1e7b, 5.1.0), the public getSpanQuery(Element) (LUCENE-7658, 61ab4e338e3, 6.5.0), SpanPositionRange (70162d3fb1a, 8.3.0); SOLR-11477 (926cc4d65b6, 2017) added FEATURE_SECURE_PROCESSING, which does not bound depth] (2006-02-25 (author date of 87768c51c632, r380874))
First affected version
1.9.1 [exec] (2006-03-02, the first published artifact of the XML query parser, executed with the nested SpanOr shape); the 1.9 final source tag (2006-02-27) already contains the recursion [read, no artifact]; 1.9-rc1 has no XML query parser
Affected versions
Every release from 1.9.1 to 10.6.0 (branch_10_6 head 4ae946e5a9b9, the RC1 commit) and main a9779ef3c193; which entry fails depends on the JDK's XML parser, not on the Lucene version. [exec] the probe of the version-history runs (depth 100,000, -da) on 1.9.1 (SpanOr), 2.1.0, 3.6.2, 4.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 (BooleanQuery and SpanOr), the RC and main (BooleanQuery and SpanOr): getQuery(Element) overflows on JDK 25.0.3 and JDK 21.0.10; parse(InputStream) overflows on JDK 21.0.10 and gives ParserException on JDK 25.0.3 (main was run on JDK 25 only). The finder's JUnit fails on main (-ea and -da: DOM leg; with xercesImpl 2.12.2 as the JAXP implementation also the XML leg), on the RC and 10.5.2 (JDK 25 -ea/-da: DOM leg; JDK 21 -ea/-da: both legs) and on 9.12.3 (JDK 25 -da: DOM leg; JDK 17.0.21 and 21.0.10 -da: both legs); the independent reproduction's own JUnit agrees on main, the RC, 10.5.2 and 9.12.3; the intended-behaviour check re-ran main (-ea, -da, and -Djdk.xml.maxElementDepth=0: both legs) and 10.5.2 (JDK 25 -da); the adjudication re-ran main; the registration check re-ran main and the RC (JDK 25 -da). The JDK 21 and 17 legs were run by re-running repro.sh's generated command with only the JDK binaries swapped (repro.sh is pinned to JDK 25). [read] the recursion is unchanged since 87768c51c632; releases not listed were not executed; on 1.9.1-2.0.0 the BooleanQuery shape is not usable as evidence (a separate getElementsByTagName defect, fixed in 6c53bc3c497 for 2.1.0, runs out of heap first), so 1.9.1 was run with SpanOr. Which JDK release first shipped the element-depth default of 100 was not determined. Not fixed upstream: the queryparser/xml directory is unchanged since dbf0d1426f13 (#16056, 2026-05-14) on upstream main 645d4fe14a20 (2026-10-28), branch_10_6 4ae946e5a9b9, branch_10x and branch_11x. All runs on Linux aarch64; x86_64 thresholds not measured.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that disputed it (the adjudication ruled it real), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal
Upstream and prior reports
none found (the prior-report record, verdict new, searched 2026-10-28, read-only, 513 query records): five scripted prior-report searches (394 records: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list and 4 JIRA-archive queries each, plus 33 web queries) and a re-read of the finder's 51-query run (1,108 hits, 0 high); 55 manual GitHub searches (apache/lucene, all of GitHub, apache/solr, Lucene.NET, Elasticsearch, OpenSearch), 4 code searches, 15 LUCENE and SOLR JIRA queries, 41 list queries plus the 94 dev@ mails of the last 40 days, GitHub security advisories and project security pages, and the file lists of all 414 open apache/lucene PRs: nothing reports or discusses the XML query parser overflowing on deep nesting or asks for a nesting limit there. Closest items, not reports of this bug: #11687 (SimpleQueryParser stack overflow, open since 2022, where a maintainer asked whether the other query parsers have the problem, unanswered); #16423 (merged 2026-07-27, 10.6.0), the same defect class fixed with a nesting limit in lucene/expressions only; #752 and #11510 (RegExp), where maintainers preferred an up-front check over catching StackOverflowError; #16820 (open, 2026-10-12), a proposal to deprecate the XML query parser for 11.0; the external-entity items #15234, #16135, #9338 (LUCENE-8291) and SOLR-11477 in the same package, with no depth discussion; #15053 and #12295, stale open PRs that touch DOMUtils and keep the recursion. Not fixed: the queryparser/xml directory is unchanged since dbf0d1426f13 (#16056, 2026-05-14) on upstream main 645d4fe14a20, branch_10_6 4ae946e5a9b9, branch_10x and branch_11x. Refresh 2026-10-28 (GitHub search and REST API, read-only): none of the 20 PRs created since 2026-10-26 or the newest items through #16937 touches the XML query parser (#16919, which turns some AssertionErrors into exceptions, changes other files; #16937 is the tracking issue and does not mention it), the issue and PR searches 'CoreParser StackOverflowError', 'xml query parser nesting', 'CoreParser depth' and 'XML queryparser stack overflow' return nothing, and neither upstream main 645d4fe14a20 nor branch_10_6 4ae946e5a9b9 has a newer commit on the directory.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off through gradle in a scratch worktree and through a standalone runner; the same test fails on the 10.6.0 and 9.12.3 release jars (Java 25). With the recommended fix it passes, as do the 69 upstream XML-parser tests with two seeds and the module's format, lint and javadoc checks. Java 21 legs (10.6.0's commit, 10.5.2, 9.12.3) come from the hunt's, the independent reproduction's and separate runs, and the fact check re-ran the 10.x text path on the 10.6.0 release jars with Java 21 in its own probe; the history probe ran 1.9.1 to 10.6.0 and main. Upstream main 2f424fd428da (2026-10-30) has the same code [read].

Lucene and upstream sources

Finding record
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:137-143 (at a9779ef3c193) (parse(InputStream): javadoc mentions only external entities, throws ParserException, delegates to getQuery(Element)), :182-205 (parseXML: DocumentBuilderFactory.newInstance(), FEATURE_SECURE_PROCESSING at :186, every XML-parser exception wrapped in ParserException), :208-216 (public getQuery(Element)/getSpanQuery(Element) delegate to the factories with no depth state)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/QueryBuilderFactory.java:29-35 (at a9779ef3c193) (dispatch, builder.getQuery at :34), builders/BooleanQueryBuilder.java:44-57 (factory.getQuery(clauseQuery) at :57), builders/DisjunctionMaxQueryBuilder.java:54, builders/ConstantScoreQueryBuilder.java:41, builders/SpanQueryBuilderFactory.java:46, builders/SpanOrBuilder.java:41 (and SpanNearBuilder.java:45, SpanFirstBuilder.java:38, SpanNotBuilder.java:42-43, SpanPositionRangeBuilder.java:39), DOMUtils.java:96 (getAttributeWithInheritance) and :157-175 (getTextBuffer)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/ParserException.java:19 (at a9779ef3c193) ('Thrown when the xml queryparser encounters invalid syntax/configuration.'); lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:54-56 ('Nesting elements': elements nest 'to produce queries/filters of an arbitrary depth and complexity', BooleanQuery inside BooleanQuery)
  • lucene/CHANGES.txt:541-545 (at a9779ef3c193) (GITHUB#16423, expressions nesting guard, 10.6.0 Improvements); SECURITY.md:28, :36-38 ('Untrusted query and analysis input', deeply nested queries), :40-42, :44-46, :48-50; gradle/libs.versions.toml:41 (minJava = '25')
  • JDK facts read by the independent reproduction and the intended-behaviour check and re-read in the registration check: JDK 25.0.3 conf/jaxp.properties:184 'jdk.xml.maxElementDepth=100'; JDK 21.0.10 conf/jaxp.properties:163 '# jdk.xml.maxElementDepth=0' (commented out, so no limit)
The finding in 30 seconds
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:137-143, :182-206, :208-216
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/QueryBuilderFactory.java:29-35; builders/BooleanQueryBuilder.java:44-68; builders/DisjunctionMaxQueryBuilder.java:54; builders/ConstantScoreQueryBuilder.java:41
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/builders/SpanQueryBuilderFactory.java:41-47; builders/SpanOrBuilder.java:41; SpanNearBuilder.java:45; SpanFirstBuilder.java:38; SpanNotBuilder.java:42-43; SpanPositionRangeBuilder.java:39; DOMUtils.java:88-101, :157-177
  • JDK 25.0.3 conf/jaxp.properties:184 (jdk.xml.maxElementDepth=100); JDK 21.0.10 conf/jaxp.properties:163 (commented out) [read]
Who is affected
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/QueryBuilder.java:22-26
Why it looks unintended
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/CoreParser.java:137-143, :184, :196-205, :208-216; ParserException.java:19
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/resources/org/apache/lucene/queryparser/xml/LuceneCoreQuery.dtd:54-57
  • a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:551-555 (10.6.0 Improvements); lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:104-105, :138, :266-271
  • GitHub apache/lucene#16423 reviews (msokolov, rmuir: APPROVED; label type:enhancement), read 2026-10-31
  • SECURITY.md:38, :42, :46
Suggested fix
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/xml/QueryBuilder.java:22-26
Prior reports
  • GitHub apache/lucene read-only searches, 2026-10-31 (this doc)
History
  • a Lucene clone tag releases/lucene/10.6.0 -> 4ae946e5a9b9; gradle/libs.versions.toml:42 minJava = "21" [read]

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 cited in the sections above; re-rendered on 2026-10-31 after the fact checks, with wording fixes only (no new numbers).

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.