All findings · Lucene finding · LFV-026 · flexible-parser-star-range-cce

Flexible StandardQueryParser throws ClassCastException on a range over the field *, such as *>a or *:[a TO *]

The flexible StandardQueryParser throws an unchecked ClassCastException for a range on the field * with an open * bound, such as *>a or *:[a TO *], instead of a query or the QueryNodeException that parse() declares. Every release since 2.9.0 does this.

Summary

Priority
Low, rank 1051The flexible parser throws ClassCastException instead of a parse error on ranges over the literal field * such as *>a, a wrong error type for unusual input.
Impact
wrong error
Since
2.9.0 (LUCENE-1792, 2009)introduced 2009-08-19 · over 17 years
Affected
2.9.0 to 10.6.0 and main
Status
documented
Found by
fuzzing the query parsers (Jazzer)

AI-assisted page: this finding was first signalled by a testing baseline (randomized, fuzz, property-based or stress testing, or static analysis) 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

An open range bound is written *. On the field * that bound looks exactly like the match-all query *:*, and one processor rewrites it as if it were.

Background: open ranges and the flexible parser

  1. A range f:[a TO z] matches the terms of the field f between a and z. Writing * for a side leaves it open: f:[a TO *] means "from a up". The flexible parser also accepts the shorthand f>a, f>=a, f<z and f<=z.
  2. The flexible StandardQueryParser turns the text into a tree of nodes and then runs about twenty processors over it, one after another, before building the Query. One of them turns the query *:* into match-all. parse() declares one checked exception for bad query text: QueryNodeException.
Diagram of the bug. You do: parse *>a with the flexible StandardQueryParser and catch QueryNodeException. Expected: the open range *:{a TO *] or a QueryNodeException; on main: java.lang.ClassCastException, MatchAllDocsQueryNode cannot be cast to FieldValuePairQueryNode. Inside: the grammar builds a range node whose upper bound has field * and text *; MatchAllDocsQueryNodeProcessor sees *:* and replaces that bound with a match-all node; OpenRangeQueryNodeProcessor reads the bound back as a field node and the cast throws; parse() does not convert it. Fix direction: skip field nodes whose parent is a range.
How *>a goes through the flexible parser: the open bound * on the field *, the match-all rewrite inside the range, and the cast that throws ClassCastException. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Parse f>a with new StandardQueryParser(): you get the open range f:{a TO *]. Inside the parser, the open side is a bound whose text is *.
  2. Parse *>a, the same range on a field named *. The parser treats * as an ordinary field elsewhere: in a 2-document index, *:b and *:[a TO c] each find the 1 document that has it. You expect *:{a TO *], or a QueryNodeException.
  3. You get ClassCastException: MatchAllDocsQueryNode cannot be cast to FieldValuePairQueryNode. The open bound reads *:*, so the processor that turns the query *:* into match-all replaces the bound inside the range.
  4. The next processor reads the bound back as a field node, and that cast throws. parse() does not convert it, so code that catches QueryNodeException to report a bad query fails on this input instead.
  5. *:[a TO *], *:[* TO *], *<z and x OR *>a fail the same way, with assertions on and off. All 150 releases fail: the bracket forms since 2.9.0, *>a since 4.0.0-ALPHA added the > syntax. The classic QueryParser returns a range on the field * for the bracket forms (1 hit).

Who is affected

Applications that pass user-typed query text to the flexible StandardQueryParser (not the classic QueryParser), default settings, no analyzer needed. Only a range on the field * with a * bound triggers it, or an open range such as [a TO *] when the default field is *. Only the exception type is wrong; no result changes. Rare input.

Mechanism, terms and numbers682 words

The flexible StandardQueryParser declares QueryNodeException as the failure of parse(). Its grammar writes the open side of a range as a bound with the text * (f>a is f:{a TO *]). On the field * that bound is *:*, and MatchAllDocsQueryNodeProcessor, which turns the query *:* into match-all, rewrites every field node *:* without checking whether it is a range bound. OpenRangeQueryNodeProcessor runs next and reads the bound through an unchecked generic cast, so parse("*>a", "f") throws java.lang.ClassCastException: MatchAllDocsQueryNode cannot be cast to FieldValuePairQueryNode.

Every range on the field * with a * bound fails: the </> shorthand, the bracket forms such as *:[a TO *] and *:[* TO *], ranges inside Boolean queries, escaped forms, and [a TO *] with the default field *. Ranges on * without a * bound, ranges on other fields and the query *:* work, and the classic QueryParser returns a TermRangeQuery on the field * for the bracket forms. The bracket forms have failed since 2.9.0 (2009), *>a since cffd01664bc9 (2011), first released in 4.0.0-ALPHA (2012). The failure happens while parsing, so nothing is searched and no result is wrong: a query string is refused with an undeclared exception type. A one-line guard, the one two sibling processors already use, fixes it and is validated with the whole queryparser test suite and check.

The bug, step by step

  1. Grammar. A field name is any term, so * is legal (StandardSyntaxParser.jj:339-344). For > and >= the grammar builds the open upper bound as new FieldQueryNode(field, "*", ...) (.jj:767, :773); for < and <= the lower bound (.jj:754, :760). The bracket form builds one FieldQueryNode per bound (.jj:870-873), so [a TO *] has a bound with the text * too. On the field *, that bound has field * and text *.
  2. Order. MatchAllDocsQueryNodeProcessor runs right before OpenRangeQueryNodeProcessor (StandardQueryNodeProcessorPipeline.java:52-53).
  3. The rewrite. if (node instanceof FieldQueryNode fqn) { if (fqn.getField().toString().equals("*") && fqn.getText().toString().equals("*")) { return new MatchAllDocsQueryNode(); } } (MatchAllDocsQueryNodeProcessor.java:44-47). It does not look at the parent, so the bound becomes a match-all node, and QueryNodeProcessorImpl writes it back into the range node (QueryNodeProcessorImpl.java:111-123). The class javadoc is narrower: it "converts every WildcardQueryNode that is "*:*"" (:28-30).
  4. The cast. OpenRangeQueryNodeProcessor reads FieldQueryNode lowerNode = rangeNode.getLowerBound(); FieldQueryNode upperNode = rangeNode.getUpperBound(); (OpenRangeQueryNodeProcessor.java:39-40). The getter is return (T) getChildren().get(1); with T extends FieldValuePairQueryNode<?> (AbstractRangeQueryNode.java:33, :96-99); its erased checkcast to FieldValuePairQueryNode throws, because a MatchAllDocsQueryNode is not one.
  5. Not converted. QueryParserHelper.parse runs the parser, the processors and the builder with no catch (QueryParserHelper.java:213-223). The grammar's own parse rethrows a ParseException (already a QueryNodeParseException) and wraps an Error into one (.jj:99-113), but this exception comes later, from the processor phase.

What fails and what works (main 645d4fe14a20)

Two documents: doc 0 has the field * = b, doc 1 has the field f = b; default field f.

InputFlexible parser todayWith the recommended fixClassic QueryParser
*>aClassCastException*:{a TO *], 1 hitits ParseException (no > syntax)
*:[a TO *]ClassCastException*:[a TO *], 1 hit*:[a TO *], 1 hit
*:[* TO *]ClassCastException*:[* TO *], 1 hit*:[* TO *], 1 hit
x OR *>aClassCastExceptionf:x *:{a TO *], 1 hitnot run
[a TO *], default field *ClassCastException*:[a TO *], 1 hitnot run
*:bTermQuery *:b, 1 hitsamesame
*:[a TO c]TermRangeQuery *:[a TO c], 1 hitsamenot run
*:*MatchAllDocsQuery, 2 hitssamesame
f>af:{a TO *], 1 hitsamen/a

The study's 14 trigger strings, re-run on 645d4fe14a20, all throw: *>a, *<a, *>=a, *<=a, *:[a TO *], *:[* TO a], *:{a TO *}, *:[* TO *], x AND *>a, *>"a b", *>5, \*>a, *:[a TO \*] and *>a*. The escaped forms fail because toString() of an escaped \* is *.

Exposure and likelihood168 words
  • Who is hit. Applications that pass query text to the flexible StandardQueryParser: default pipeline, no index and no configuration needed. Run with no analyzer and with StandardAnalyzer; the analyzer processor runs only after the one that throws.
  • What goes wrong. Code that catches QueryNodeException to answer "invalid query" gets an unexpected ClassCastException instead. What happens next depends on the application; one that catches nothing else answers that request with a generic server error.
  • When. A user who writes * to mean "any field" (the parser maps *:* to match-all) and adds a range, such as *:[a TO *] or *>5, hits it. So does any open range typed without a field, such as [a TO *], in an application whose default field is *.
  • Limits. Deterministic and immediate. The failure happens while parsing, before any search or index access: no wrong results, no hang, no resource growth. The likelihood is rated rare, and the recorded searches found no matching public report, 17 years after 2.9.0.

Minimal repro

One JUnit test in lucene/queryparser, public API only: the flexible parser parses *>a. It accepts a query or a QueryNodeException; any other runtime exception fails it as an AssertionError with that cause. No analyzer, no index; deterministic; fails with assertions on and off.

TestStarFieldOpenRange.java (1 test)33 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.flexible.standard;

import org.apache.lucene.queryparser.flexible.core.QueryNodeException;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestStarFieldOpenRange extends LuceneTestCase {

  public void testOpenRangeOnStarField() {
    try {
      new StandardQueryParser().parse("*>a", "f");
    } catch (QueryNodeException e) {
      // expected: a Query or the documented QueryNodeException; actual: ClassCastException
    } catch (RuntimeException e) {
      throw new AssertionError("expected a Query or QueryNodeException, but parse threw " + e, e);
    }
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or any commit with the same flexible parser)
cp $REPRO/TestStarFieldOpenRange.java lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
./gradlew :lucene:queryparser:test --tests org.apache.lucene.queryparser.flexible.standard.TestStarFieldOpenRange -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
Expected
A Query (the open range *:{a TO *]) or a QueryNodeException.
Observed
ClassCastException: MatchAllDocsQueryNode cannot be cast to FieldValuePairQueryNode from AbstractRangeQueryNode.getUpperBound, on main, 10.6.0, 10.5.2 and 9.1.0, with assertions on and off.

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

More files, environment, full output223 words

The test sits in the flexible parser's test package, as Lucene's own parser tests do, but uses no package-private member. Nothing in it is randomized; 5 random-seed iterations fail 5 of 5 with the same message.

As the regression test it needs one lint change: ecjLintTest rejects the unused catch parameter e ("The value of the exception parameter e is not used"), so the in-tree copy uses catch (@SuppressWarnings("unused") QueryNodeException e), as QueryParserBase.java:803-805 does. With that change it fails on stock, passes with the recommended fix and passes :lucene:queryparser:check -x test.

The other shapes of the same root cause (*:[a TO *], *:[* TO *], [a TO *] with the default field *) are covered by the table above, not by the test.

Environment

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

Observed output

TestStarFieldOpenRange > testOpenRangeOnStarField FAILED
    java.lang.AssertionError: expected a Query or QueryNodeException, but parse threw java.lang.ClassCastException: class org.apache.lucene.queryparser.flexible.core.nodes.MatchAllDocsQueryNode cannot be cast to class org.apache.lucene.queryparser.flexible.core.nodes.FieldValuePairQueryNode (org.apache.lucene.queryparser.flexible.core.nodes.MatchAllDocsQueryNode and org.apache.lucene.queryparser.flexible.core.nodes.FieldValuePairQueryNode are in unnamed module of loader 'app')
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:3A3036066A125B29]:0)
        at org.apache.lucene.queryparser.flexible.standard.TestStarFieldOpenRange.testOpenRangeOnStarField(TestStarFieldOpenRange.java:30)

        Caused by:
        java.lang.ClassCastException: class org.apache.lucene.queryparser.flexible.core.nodes.MatchAllDocsQueryNode cannot be cast to class org.apache.lucene.queryparser.flexible.core.nodes.FieldValuePairQueryNode (...)
            at org.apache.lucene.queryparser.flexible.standard.nodes.AbstractRangeQueryNode.getUpperBound(AbstractRangeQueryNode.java:98)
            at org.apache.lucene.queryparser.flexible.standard.processors.OpenRangeQueryNodeProcessor.postProcessNode(OpenRangeQueryNodeProcessor.java:40)
            at org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessorImpl.processIteration(QueryNodeProcessorImpl.java:89)
            at org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessorImpl.process(QueryNodeProcessorImpl.java:81)
            at org.apache.lucene.queryparser.flexible.core.processors.QueryNodeProcessorPipeline.process(QueryNodeProcessorPipeline.java:78)
            at org.apache.lucene.queryparser.flexible.core.QueryParserHelper.parse(QueryParserHelper.java:219)
            at org.apache.lucene.queryparser.flexible.standard.StandardQueryParser.parse(StandardQueryParser.java:279)
            at org.apache.lucene.queryparser.flexible.standard.TestStarFieldOpenRange.testOpenRangeOnStarField(TestStarFieldOpenRange.java:26)
:lucene:queryparser:test (FAILURE): 1 test, 1  failure

Why it looks unintended

  • parse() declares only QueryNodeException; * is an ordinary field elsewhere (*:b and *:[a TO c] work), and the classic parser builds *:[a TO *].
  • Three sibling processors skip range bounds on purpose; this one's own javadoc limits it to wildcard nodes.
  • Precedent: #5175, a ClassCastException from another processor touching a node it should skip, was fixed in 2012.
The full argument303 words
  • The declared failure is QueryNodeException: StandardQueryParser#parse (:274) and QueryParserHelper#parse (:211), "if something wrong happens along the three phases". No javadoc or MIGRATE entry mentions ClassCastException for the parser, none of the five ClassCastException entries in CHANGES is about a query parser, and nothing reserves * as a field name for ranges.
  • * is an ordinary field elsewhere. *:b gives TermQuery *:b and *:[a TO c] a TermRangeQuery, 1 hit each. The class javadoc says the syntax is borrowed from the classic parser (StandardQueryParser.java:63-65), which builds working range queries for *:[a TO *], *:[* TO z] and *:[* TO *], and whose testStarParsing asserts that *:foo is a TermQuery on the field * (TestQueryParser.java:275-278).
  • The processor does more than its javadoc says: "converts every WildcardQueryNode that is "*:*"" (MatchAllDocsQueryNodeProcessor.java:28-30), while the code matches any FieldQueryNode (:44). Range bounds are never wildcard nodes: WildcardQueryNodeProcessor skips them (:96-103).
  • Sibling guards. WildcardQueryNodeProcessor: "do not process wildcards for TermRangeQueryNode children ... to reproduce the old parser behavior" (:96-98). AnalyzerQueryNodeProcessor: !(node.getParent() instanceof RangeQueryNode) (:110). PointQueryNodeProcessor: "FieldQueryNodes children of a RangeQueryNode are ignored" (:46, guard :63).
  • The node's own type. TermRangeQueryNode extends AbstractRangeQueryNode<FieldQueryNode>, "a range query composed by FieldQueryNode bounds" (TermRangeQueryNode.java:21-28).
  • History. The 2009 framework commit 343992fcbb4 matched only WildcardQueryNode; 16 days later 565a6d22b5f (LUCENE-1792, about rewrite methods for multi-term queries) widened the test to FieldQueryNode without updating the javadoc, and its 25-comment discussion (#2867) never mentions match-all. Its parent commit passes the probe; the commit fails; the commit with only that hunk reverted passes.
  • Precedent. #5175 (LUCENE-4103), "AnalyzerQueryNodeProcessor throws a class cast exception with the RegexpQueryNode", was fixed in 2012 with a one-line node-type guard (5fb2e4a751e).
The devil's-advocate check130 words

The strongest case for "intended" (the cross-model reviewer's, which it rated weak itself): * in the field position may be a match-all sentinel, so a range over "all fields" is meaningless input, and @throws QueryNodeException does not promise that no unchecked exception escapes.

Why it fails: the parser treats * as a field name everywhere else (*:b, *:[a TO c]), and so do the classic parser and its tests; the documented match-all special case is the whole query *:* (CHANGES, LUCENE-723; TestQPHelper.testMatchAllDocs), which the fix keeps; even for invalid input the answer would be a QueryNodeException, not a ClassCastException from an erased generic cast; and the sibling guards and the 2012 precedent show upstream treats this kind of defect as a bug. Verdict: not intended, a defect.

Suggested fix

Recommended MatchAllDocsQueryNodeProcessor leaves range bounds alone (one line plus an import)

Let MatchAllDocsQueryNodeProcessor skip a field node whose parent is a range, the guard its sibling processors already use. *>a then gives *:{a TO *], and *:* stays match-all. Validated: the repro passes, the whole queryparser suite gives 534 tests, 0 failures (1 skipped by @Ignore), and check passes.

+import org.apache.lucene.queryparser.flexible.core.nodes.RangeQueryNode;
 ...
-    if (node instanceof FieldQueryNode fqn) {
+    if (node instanceof FieldQueryNode fqn && !(node.getParent() instanceof RangeQueryNode)) {
       if (fqn.getField().toString().equals("*") && fqn.getText().toString().equals("*")) {
         return new MatchAllDocsQueryNode();
All options and trade-offs4 options · 545 words

The recommended fix is the study verifier's; it touches one file and needs no grammar regeneration. The open choice for maintainers is what a range on the field * should give: a range on a field literally named * (the recommended fix, as in the classic parser) or a QueryNodeException (the third option). The repro test accepts either.

MatchAllDocsQueryNodeProcessor leaves range bounds alone (one line plus an import) (validated by the checks) (recommended)

In MatchAllDocsQueryNodeProcessor.postProcessNode (:44), the same guard AnalyzerQueryNodeProcessor (:110) and PointQueryNodeProcessor (:63) use. A bound * on the field * stays a field node, so OpenRangeQueryNodeProcessor makes it an open side as for any other field: *>a gives TermRangeQuery *:{a TO *], the bracket forms give the classic parser's range and hit count, and *:* stays MatchAllDocsQuery (2 hits of 2).

+import org.apache.lucene.queryparser.flexible.core.nodes.RangeQueryNode;
 ...
-    if (node instanceof FieldQueryNode fqn) {
+    if (node instanceof FieldQueryNode fqn && !(node.getParent() instanceof RangeQueryNode)) {
       if (fqn.getField().toString().equals("*") && fqn.getText().toString().equals("*")) {
         return new MatchAllDocsQueryNode();

Trade-offs

  • Ranges on the field * now parse as ranges on a field literally named *, as in the classic parser. They failed before, so no working behaviour is lost; a user who meant "any field" gets a range on *, which usually matches nothing.
  • Optional and cosmetic: make the class javadoc say what the code does (every FieldQueryNode *:* except range bounds). Not part of the validated diff.
  • A CHANGES entry under Bug Fixes would fit.

What the checks validated

Scratch worktree of 645d4fe14a20, gradle, seed DEADBEEF: the repro fails on stock and passes with the fix, with assertions on and off; the whole :lucene:queryparser:test, 534 tests, 1 skipped, 0 failures; with the lint-clean regression test, :lucene:queryparser:check -x test passes. With the patched classes placed ahead of the release jars on the classpath, a probe of 17 trigger calls (and 10 controls) gives 0 undeclared exceptions, and 13 upstream flexible-parser test classes (154 tests) are unchanged.

Narrow the processor to what its javadoc says, instanceof WildcardQueryNode (not run)

Range bounds are never wildcard nodes (WildcardQueryNodeProcessor.java:96-103 skips them), so this keeps them out too.

-    if (node instanceof FieldQueryNode fqn) {
+    if (node instanceof WildcardQueryNode fqn) {

Trade-offs

It changes more than the bug: by reading the code, an escaped *:\* or a quoted *:"*" is a plain or quoted field node that the current test turns into match-all, and would not be after this change. It needs its own tests and a decision on those inputs.

What the checks validated

Not run.

Reject the input with the declared exception (not run)

Check the bound types in OpenRangeQueryNodeProcessor (or in the AbstractRangeQueryNode getters) and throw a QueryNodeException when a bound is not a field node.

Trade-offs

Gives the declared exception, but rejects input the classic parser accepts, and leaves the broken range node for later processors and for custom pipelines that reorder them.

What the checks validated

Not run.

Not recommended: wrap every RuntimeException in QueryParserHelper.parse (not run)

Catch RuntimeException around the three phases (QueryParserHelper.java:213-223) and rethrow it as a QueryNodeException.

Trade-offs

Hides this and other separate defects of the same parser behind one generic message, changes behaviour for every caller, and leaves the broken tree in place.

What the checks validated

Not run.

Downstream note

Not a Lucene security issue; downstream, a service that catches only QueryNodeException to answer "invalid query" returns a generic server error for such input instead.

How it was found

An AI tool wrote and ran a Jazzer fuzzer over Lucene's query parsers that checks each parser throws only its documented exception; it flagged this ClassCastException without a hint. An independent verifier then reproduced it and traced it to the match-all processor by removing that processor from the pipeline and by swapping in one that skips range bounds.

The method in full163 words

Method: non-formal baseline of the study (fuzzing).

  • The fuzzer, QueryParserFuzzer, fed generated strings to the classic, flexible and simple query parsers. Its oracle: each parser may only throw its documented exception, and a parsed query must survive toString, equals, count and rewrite. It ran for 1.5 + 7 + 20 minutes, about 470,000 executions, and produced five query-parser findings; this is one of them.
  • An independent AI verifier (same model family) wrote its own 6-test JUnit class and a version probe: 14 trigger strings failed on main 7cecf79523, and 6 of them on 8.11.4, 9.12.3 and 10.1.0, while the controls parsed; removing MatchAllDocsQueryNodeProcessor from the pipeline removed every exception, and a range-aware replacement made all 14 parse while keeping *:* as match-all.
  • Later checks cut the repro to one test (2026-10-29 noise audit), re-ran it on 645d4fe14a20 and on releases, dated the bug to 2009 by building commits and running all 150 releases, and validated the fix with gradle.

Prior reports, history and checks

Prior reports

No prior report found: 158 queries over GitHub issues and PRs, the JIRA archive, the mailing lists, downstream projects and the web, plus the changed files of all 410 open PRs (2026-11-01). The issues that added the > shorthand never discuss the field *.

Searches6 searches

Verdict new. The closest items are the design origin of *>a, a downstream parser with the same failing cast from its own grammar, the 2012 precedent at another processor, and other unchecked exceptions from parse(); none reports this bug. GitHub search drops *, >, [ and :, so class, method and message terms were searched instead.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene)2026-11-01the scripted search's GitHub queries plus 12 extra phrasings and 6 manual global searches, e.g. MatchAllDocsQueryNode ClassCastException, "MatchAllDocsQueryNode cannot be cast", AbstractRangeQueryNode getLowerBound ClassCastException, FieldValuePairQueryNode, StandardQueryParser star field rangeno report; closest: #4411 / LUCENE-3338 and #4416 / LUCENE-3343 (added the > shorthand; 35 comments, nobody mentions the field *), #5175 / LUCENE-4103 (fixed 2012, the precedent), #8975, #6554, #6938, #7761 (other unchecked exceptions or casts), PR #14697 (toQueryString only), #16820 (proposed deprecation of the flexible parser)
Open and recent PRs2026-11-01the changed-file lists of all 410 open PRs and of the 150 most recently updated closed PRsten open PRs touch lucene/queryparser; none touches MatchAllDocsQueryNodeProcessor, OpenRangeQueryNodeProcessor, AbstractRangeQueryNode, the grammar or the pipeline
JIRA archive (LUCENE, SOLR)2026-11-01the scripted search's JIRA queries plus 11 JQL text searches, and full reads of LUCENE-3338, -3343, -4109, -5344 and -1768no report
Mailing lists2026-11-01queries over dev@, java-user@, issues@ and commits@, plus 12 direct archive searchesonly commit and release mails and PR notifications
Downstream projects and the web2026-11-01Elasticsearch, OpenSearch, Solr, ports and parsers built on the flexible framework; 16 web queriesadsabs/montysolr#159 (open since 2019): the same failing cast from that project's own grammar node, a different root cause, not reported to Lucene; nothing else
History2026-11-01git log --follow on the five fix-site files; upstream main, branch_11x and branch_10_6 through the commits APIthe bug-path lines unchanged since the design commits apart from cleanups; not fixed

History

Introduced in 2.9.0 by 565a6d22b5f (LUCENE-1792, 2009), which widened the match-all test from wildcard nodes to all field nodes. The > shorthand of cffd01664bc9 (2011, first released in 4.0.0-ALPHA, 2012) made *>a a trigger. All 150 releases from 2.9.0 to 10.6.0 were run, and all fail; main fails.

Versions tested8 rows

Introduced by 565a6d22b5f "LUCENE-1792: Fix new query parser to set rewrite method for multi-term queries." (2009-08-19, svn r805633; issue #2867, a JIRA-era patch with no PR). It moved wildcard detection out of the grammar and changed MatchAllDocsQueryNodeProcessor's test from instanceof WildcardQueryNode to instanceof FieldQueryNode. Range bounds were ParametricQueryNode then, a FieldQueryNode subclass, so from 2.9.0 to 3.6.2 the bracket forms fail at ParametricRangeQueryNode.getUpperBound / getLowerBound and *>a gives the declared QueryNodeException (no > syntax yet).

cffd01664bc9 (2011-08-01, LUCENE-3343 and LUCENE-3338) added the <, >, <=, >= shorthand and OpenRangeQueryNodeProcessor right after the match-all processor; from 4.0.0-ALPHA all four probe triggers fail at AbstractRangeQueryNode. The parent of 565a6d22b5f passes, the commit fails, and the commit with only the widened test reverted passes. Not a regression: no release ever shipped without it. The lines on the bug path have changed since only for cleanups; other commits to these files (#14697's toQueryString fix, the 2021-22 interval-function additions) do not touch them. Upstream main 2f424fd428da and branch_11x have the same code; branch_10_6 has the same bug path (its AbstractRangeQueryNode lacks #14697).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedClassCastException, assertions on and off (gradle and standalone)
10.6.0, 10.5.2, 9.1.0 affectedthe minimal test fails, assertions on and off (release jars, SHA-1 checked)
4.0.0-ALPHA to 10.6.0 (133 releases incl. ALPHA and BETA) affectedall 4 probe triggers (*>a and the bracket forms) throw ClassCastException
2.9.0 to 3.6.2 (17 releases) first affected 2.9.0the bracket forms throw ClassCastException; *>a gives QueryNodeException (no > syntax yet)
2.4.1 not affectedno flexible parser (the probe does not compile)
commit 7700fa36264 (parent of 565a6d22b5f) not affectedthe bracket forms give TermRangeQuery on field *
commit 565a6d22b5f introducing3 of 4 triggers throw; with only the widened test reverted: pass
recommended fix on 645d4fe14a20 not affectedthe repro passes; 534 queryparser tests, 0 failures (1 skipped by @Ignore); check passes

Independent checks

An independent verifier reproduced it in the study; a 2026-11-01 reproducer and a tie-breaker re-ran it on main, 10.6.0 and 10.5.2 and rated it real (high). The review by another model family (OpenAI GPT-5.6) could not run code, so its verdict is procedural; a separate run re-ran the test, which failed. No independently written cross-model reproduction exists.

Checks and verdicts276 words

The cross-model reviewer agreed on the root cause and that the input is in contract, and offered the "* is a match-all sentinel" argument, which it rated weak; the tie-breaker answered it (see the devil's advocate above).

Each independent check and its verdict
CheckVerdictWhat it found
Study verifier new bugOwn 6-test JUnit class and version probe: 14 triggers fail on main 7cecf79523, 6 of them on 8.11.4, 9.12.3 and 10.1.0; removing or replacing the match-all processor removes every exception.
Re-runs on later main commits still reproducesThe study test fails the same 2 of 6 methods on b83101cdd2 and a9779ef3c193.
Noise audit confirmed, low impactCut the repro to one test, one input; fails 5 of 5 random-seed iterations, assertions on and off.
Reproducer on 645d4fe14a20 realMain and 10.5.2 fail with assertions on and off, 10.6.0 with assertions off; the shadowed fix passes; not fixed on main.
Cross-model review, refute lens (OpenAI GPT-5.6) not real (low), reading onlyCould not run code; agreed on the root cause and that the input is in contract; its verdict is procedural. A separate run re-ran the supplied test on 10.6.0 with assertions off, and it failed.
Second adjudication real, highRe-ran main, 10.6.0 and 10.5.2, the 14 study triggers, a test-framework-free probe on main and ten older releases, the shadowed fix and 13 upstream test classes.
Gradle validation of the fix (this doc) fix validatedScratch worktree of 645d4fe14a20: stock fails, fix passes (assertions on and off); 534 queryparser tests, 0 failures (1 skipped by @Ignore); check -x test passes with the lint-clean test.

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 notes656 words

Finding record

LFV-026, snapshot of 2026-11-06

Registry title
Flexible StandardQueryParser throws ClassCastException on the input *>a
Area
queryparser / flexible
Priority
Low, rank 1051: The flexible parser throws ClassCastException instead of a parse error on ranges over the literal field * such as *>a, a wrong error type for unusual input.
Impact class
wrong error
Impact note
Applications that parse untrusted query strings with the flexible StandardQueryParser; one unusual input; in every release since 2.9.0 (all 150 releases executed).
Summary
You parse the query string *>a with the flexible StandardQueryParser. You expect a query or a QueryNodeException for bad syntax. Instead parse() throws an unchecked ClassCastException.
Introduced
565a6d22b5f0f6be56bc1542796ac7c831f838e4 (LUCENE-1792 'Fix new query parser to set rewrite method for multi-term queries', svn r805633, issue #2867; a JIRA-era patch, no PR): it widened MatchAllDocsQueryNodeProcessor's test from instanceof WildcardQueryNode to instanceof FieldQueryNode, so a range bound * on the field * is rewritten to a match-all node [exec: its parent 7700fa36264 passes the probe, the commit fails 3 of 4, and the commit with only that hunk reverted passes]; cffd01664bc9 (2011-08-01, LUCENE-3343 and LUCENE-3338, first released in 4.0.0-ALPHA) added the < and > shorthand, which made *>a a trigger (2009-08-19)
First affected version
2.9.0 [exec, -ea and -da: the bracket forms such as *:[a TO *] throw ClassCastException; *>a fails from 4.0.0-ALPHA, which added the > syntax], the first release with the flexible parser; 2.4.1 has no flexible parser [exec: the probe does not compile]
Affected versions
every release from 2.9.0 to 10.6.0 and main 645d4fe14a20, never fixed [exec: a main() probe on all 150 lucene-queryparser releases on Maven Central (SHA-1-checked jars) fails on each; the minimal JUnit fails on 9.1.0, 10.6.0 and main with -ea and -da]; upstream main 2f424fd428da has the same code [read]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed in the earlier formal verification study by independent AI checks and a rerun on Lucene main; also a cross-model review that could only read the code (its low-confidence “not real” is procedural); not yet triaged by a person.
Found by
non-formal baseline: fuzzing/PBT
Upstream and prior reports
none found (study searches 2026-10-22; a prior-report search on 2026-11-01, read-only: 158 queries over GitHub issues and PRs, the JIRA archive, the mailing lists, downstream projects and the web, plus the changed files of all 410 open PRs; closest, not this bug: #5175/LUCENE-4103, fixed 2012, a ClassCastException from another processor of the same pipeline, and adsabs/montysolr#159, the same failing cast from that project's own grammar)
Lucene commit tested
main 645d4fe14a20 (gradle and standalone, assertions on and off); the minimal test on the 10.6.0, 10.5.2 and 9.1.0 release jars; a main() probe on all 150 lucene-queryparser releases from 2.9.0 to 10.6.0; commit builds of the introducing commit 565a6d22b5f, its parent, cffd01664bc9 and its parent; the study's runs on main 7cecf79523, b83101cdd2 and a9779ef3c193 and on 8.11.4, 9.12.3 and 10.1.0; the recommended fix in a scratch worktree of 645d4fe14a20. Upstream main 2f424fd428da (2026-10-30) has the same code.

Lucene and upstream sources

The finding in 30 seconds
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:99-113, (at 645d4fe14a20) 339-344, 732-779 (754, 760, 767, 773), 846-876 (870-873)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/StandardQueryNodeProcessorPipeline.java:52-53 (at 645d4fe14a20)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MatchAllDocsQueryNodeProcessor.java:28-30, (at 645d4fe14a20) 44-47
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/processors/QueryNodeProcessorImpl.java:84-92, (at 645d4fe14a20) 111-123
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/OpenRangeQueryNodeProcessor.java:31, (at 645d4fe14a20) 38-40
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/AbstractRangeQueryNode.java:33, (at 645d4fe14a20) 86-99
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/QueryParserHelper.java:211-223 (at 645d4fe14a20)
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:145-159 (at 645d4fe14a20) (background: range operators)
Minimal repro
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/classic/QueryParserBase.java:803-805 (at 645d4fe14a20)
Why it looks unintended
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:63-65, (at 645d4fe14a20) 274, 277-280; core/QueryParserHelper.java:211
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MatchAllDocsQueryNodeProcessor.java:28-30, (at 645d4fe14a20) 44
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/WildcardQueryNodeProcessor.java:96-103; (at 645d4fe14a20) AnalyzerQueryNodeProcessor.java:107-110; PointQueryNodeProcessor.java:46, 63
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/TermRangeQueryNode.java:21-28 (at 645d4fe14a20)
  • lucene/queryparser/src/test/org/apache/lucene/queryparser/classic/TestQueryParser.java:275-278; (at 645d4fe14a20) flexible/standard/TestQPHelper.java:1131-1134, 1254-1264
  • lucene/CHANGES.txt:21146 (at 645d4fe14a20) (LUCENE-723)
Suggested fix
  • lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MatchAllDocsQueryNodeProcessor.java:28-30, (at 645d4fe14a20) 44; WildcardQueryNodeProcessor.java:96-103; AnalyzerQueryNodeProcessor.java:110; PointQueryNodeProcessor.java:63; OpenRangeQueryNodeProcessor.java:38-40; core/QueryParserHelper.java:213-223

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

Repro files

About the diagrams

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.