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.
- A range
f:[a TO z]matches the terms of the fieldfbetweenaandz. Writing*for a side leaves it open:f:[a TO *]means "fromaup". The flexible parser also accepts the shorthandf>a,f>=a,f<zandf<=z. - The flexible
StandardQueryParserturns the text into a tree of nodes and then runs about twenty processors over it, one after another, before building theQuery. One of them turns the query*:*into match-all.parse()declares one checked exception for bad query text:QueryNodeException.
*>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.- Parse
f>awithnew StandardQueryParser(): you get the open rangef:{a TO *]. Inside the parser, the open side is a bound whose text is*. - Parse
*>a, the same range on a field named*. The parser treats*as an ordinary field elsewhere: in a 2-document index,*:band*:[a TO c]each find the 1 document that has it. You expect*:{a TO *], or aQueryNodeException. - 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. - The next processor reads the bound back as a field node, and that cast throws.
parse()does not convert it, so code that catchesQueryNodeExceptionto report a bad query fails on this input instead. *:[a TO *],*:[* TO *],*<zandx OR *>afail the same way, with assertions on and off. All 150 releases fail: the bracket forms since 2.9.0,*>asince 4.0.0-ALPHA added the>syntax. The classicQueryParserreturns a range on the field*for the bracket forms (1 hit).
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.
- Grammar. A field name is any term, so
*is legal (StandardSyntaxParser.jj:339-344). For>and>=the grammar builds the open upper bound asnew FieldQueryNode(field, "*", ...)(.jj:767,:773); for<and<=the lower bound (.jj:754,:760). The bracket form builds oneFieldQueryNodeper bound (.jj:870-873), so[a TO *]has a bound with the text*too. On the field*, that bound has field*and text*. - Order.
MatchAllDocsQueryNodeProcessorruns right beforeOpenRangeQueryNodeProcessor(StandardQueryNodeProcessorPipeline.java:52-53). - 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, andQueryNodeProcessorImplwrites it back into the range node (QueryNodeProcessorImpl.java:111-123). The class javadoc is narrower: it "converts everyWildcardQueryNodethat is"*:*"" (:28-30). - The cast.
OpenRangeQueryNodeProcessorreadsFieldQueryNode lowerNode = rangeNode.getLowerBound(); FieldQueryNode upperNode = rangeNode.getUpperBound();(OpenRangeQueryNodeProcessor.java:39-40). The getter isreturn (T) getChildren().get(1);withT extends FieldValuePairQueryNode<?>(AbstractRangeQueryNode.java:33,:96-99); its erased checkcast toFieldValuePairQueryNodethrows, because aMatchAllDocsQueryNodeis not one. - Not converted.
QueryParserHelper.parseruns the parser, the processors and the builder with nocatch(QueryParserHelper.java:213-223). The grammar's ownparserethrows aParseException(already aQueryNodeParseException) and wraps anErrorinto one (.jj:99-113), but this exception comes later, from the processor phase.
Two documents: doc 0 has the field * = b, doc 1 has the field f = b; default field f.
| Input | Flexible parser today | With the recommended fix | Classic QueryParser |
|---|---|---|---|
*>a | ClassCastException | *:{a TO *], 1 hit | its ParseException (no > syntax) |
*:[a TO *] | ClassCastException | *:[a TO *], 1 hit | *:[a TO *], 1 hit |
*:[* TO *] | ClassCastException | *:[* TO *], 1 hit | *:[* TO *], 1 hit |
x OR *>a | ClassCastException | f:x *:{a TO *], 1 hit | not run |
[a TO *], default field * | ClassCastException | *:[a TO *], 1 hit | not run |
*:b | TermQuery *:b, 1 hit | same | same |
*:[a TO c] | TermRangeQuery *:[a TO c], 1 hit | same | not run |
*:* | MatchAllDocsQuery, 2 hits | same | same |
f>a | f:{a TO *], 1 hit | same | n/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 withStandardAnalyzer; the analyzer processor runs only after the one that throws. - What goes wrong. Code that catches
QueryNodeExceptionto answer "invalid query" gets an unexpectedClassCastExceptioninstead. 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.
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.
/*
* 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);
}
}
}
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 aQueryNodeException. - Observed
ClassCastException: MatchAllDocsQueryNode cannot be cast to FieldValuePairQueryNodefromAbstractRangeQueryNode.getUpperBound, on main, 10.6.0, 10.5.2 and 9.1.0, with assertions on and off.
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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0, 10.5.2 and 9.1.0 release jars with this test |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
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 failureparse()declares onlyQueryNodeException;*is an ordinary field elsewhere (*:band*:[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
ClassCastExceptionfrom another processor touching a node it should skip, was fixed in 2012.
The full argument303 words
- The declared failure is
QueryNodeException:StandardQueryParser#parse(:274) andQueryParserHelper#parse(:211), "if something wrong happens along the three phases". No javadoc or MIGRATE entry mentionsClassCastExceptionfor the parser, none of the fiveClassCastExceptionentries in CHANGES is about a query parser, and nothing reserves*as a field name for ranges. *is an ordinary field elsewhere.*:bgivesTermQuery *:band*:[a TO c]aTermRangeQuery, 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 whosetestStarParsingasserts that*:foois aTermQueryon the field*(TestQueryParser.java:275-278).- The processor does more than its javadoc says: "converts every
WildcardQueryNodethat is"*:*"" (MatchAllDocsQueryNodeProcessor.java:28-30), while the code matches anyFieldQueryNode(:44). Range bounds are never wildcard nodes:WildcardQueryNodeProcessorskips 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
343992fcbb4matched onlyWildcardQueryNode; 16 days later565a6d22b5f(LUCENE-1792, about rewrite methods for multi-term queries) widened the test toFieldQueryNodewithout 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.
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.
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.
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.
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.
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.
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 survivetoString,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; removingMatchAllDocsQueryNodeProcessorfrom 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
645d4fe14a20and on releases, dated the bug to 2009 by building commits and running all 150 releases, and validated the fix with gradle.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-11-01 | the 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 range | no 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 PRs | 2026-11-01 | the changed-file lists of all 410 open PRs and of the 150 most recently updated closed PRs | ten open PRs touch lucene/queryparser; none touches MatchAllDocsQueryNodeProcessor, OpenRangeQueryNodeProcessor, AbstractRangeQueryNode, the grammar or the pipeline |
| JIRA archive (LUCENE, SOLR) | 2026-11-01 | the scripted search's JIRA queries plus 11 JQL text searches, and full reads of LUCENE-3338, -3343, -4109, -5344 and -1768 | no report |
| Mailing lists | 2026-11-01 | queries over dev@, java-user@, issues@ and commits@, plus 12 direct archive searches | only commit and release mails and PR notifications |
| Downstream projects and the web | 2026-11-01 | Elasticsearch, OpenSearch, Solr, ports and parsers built on the flexible framework; 16 web queries | adsabs/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 |
| History | 2026-11-01 | git log --follow on the five fix-site files; upstream main, branch_11x and branch_10_6 through the commits API | the bug-path lines unchanged since the design commits apart from cleanups; not fixed |
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).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | ClassCastException, assertions on and off (gradle and standalone) |
| 10.6.0, 10.5.2, 9.1.0 | affected | the 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) | affected | all 4 probe triggers (*>a and the bracket forms) throw ClassCastException |
| 2.9.0 to 3.6.2 (17 releases) | first affected 2.9.0 | the bracket forms throw ClassCastException; *>a gives QueryNodeException (no > syntax yet) |
| 2.4.1 | not affected | no flexible parser (the probe does not compile) |
commit 7700fa36264 (parent of 565a6d22b5f) | not affected | the bracket forms give TermRangeQuery on field * |
commit 565a6d22b5f | introducing | 3 of 4 triggers throw; with only the widened test reverted: pass |
recommended fix on 645d4fe14a20 | not affected | the repro passes; 534 queryparser tests, 0 failures (1 skipped by @Ignore); check passes |
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).
| Check | Verdict | What it found |
|---|---|---|
| Study verifier | new bug | Own 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 reproduces | The study test fails the same 2 of 6 methods on b83101cdd2 and a9779ef3c193. |
| Noise audit | confirmed, low impact | Cut the repro to one test, one input; fails 5 of 5 random-seed iterations, assertions on and off. |
| Reproducer on 645d4fe14a20 | real | Main 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 only | Could 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, high | Re-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 validated | Scratch 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 461 KB
Finding record, Lucene and upstream sources, repro files and diagram notes656 words
- 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; amain()probe on all 150lucene-queryparserreleases from 2.9.0 to 10.6.0; commit builds of the introducing commit565a6d22b5f, its parent,cffd01664bc9and its parent; the study's runs on main7cecf79523,b83101cdd2anda9779ef3c193and on 8.11.4, 9.12.3 and 10.1.0; the recommended fix in a scratch worktree of645d4fe14a20. Upstream main2f424fd428da(2026-10-30) has the same code.
- 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-47lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/core/processors/QueryNodeProcessorImpl.java:84-92, (at 645d4fe14a20) 111-123lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/OpenRangeQueryNodeProcessor.java:31, (at 645d4fe14a20) 38-40lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/nodes/AbstractRangeQueryNode.java:33, (at 645d4fe14a20) 86-99lucene/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:211lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/MatchAllDocsQueryNodeProcessor.java:28-30, (at 645d4fe14a20) 44lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/WildcardQueryNodeProcessor.java:96-103; (at 645d4fe14a20) AnalyzerQueryNodeProcessor.java:107-110; PointQueryNodeProcessor.java:46, 63lucene/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-1264lucene/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
- TestStarFieldOpenRange.java (1 test):
repro/LFV-026/src/org/apache/lucene/queryparser/flexible/standard/TestStarFieldOpenRange.java(33 lines)
assets/LFV-026/bug.png(461 KB)
![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.](../assets/LFV-026/bug.png)