- In the standard query syntax, square brackets include a bound and curly brackets exclude it:
i:{5 TO *]means i > 5, and the operator formi>5means the same. - With a
PointsConfigfor the field (whose type matches how the field is indexed, one-dimensionalIntPointorLongPointvalues), the flexibleStandardQueryParserbuilds anIntPointorLongPointrange query. Those take inclusive bounds only, so the parser must rewritei > 5asi >= 6. - Java
intandlongarithmetic wraps silently at the limits:Integer.MAX_VALUE + 1isInteger.MIN_VALUE(2147483647 + 1 = -2147483648).
- Index one document with
IntPoint("i", 0)and giveStandardQueryParseran IntegerPointsConfigfori, the setup of Lucene's ownTestPointQueryParserand of Luke's search tab. - Parse
i:{2147483647 TO *], documented as "larger than" 2147483647. That isInteger.MAX_VALUE, so you expect 0 hits. - Instead you get 1 hit. Point ranges take inclusive bounds, so the builder turns
> 2147483647into>= 2147483647 + 1, and in Java int arithmetic that is -2147483648. The query becomesi:[-2147483648 TO 2147483647], every int. - The same wrap hits
i>2147483647,i:[* TO -2147483648},i<"-2147483648"and the long forms: on 5 documents with the values MIN, -1, 0, 1, MAX each matches all 5 instead of 0. One step from the limit,i:{2147483646 TO *]is exact (1 hit). - No error is raised. Negated next to another clause, as in
*:* -l:{9223372036854775807 TO *], the clause excludes every document: 0 of 5 hits instead of 5 (the control*:* -l:{9223372036854775806 TO *]gives the correct 4). A purely negative query matches nothing with or without the bug.
Code parsing query text with the flexible StandardQueryParser and an Integer or Long PointsConfig (opt-in; Luke's search tab does this; the classic QueryParser builds no point queries), when a bound is exactly MAX or MIN with {, }, > or <, for example a generated filter on a field that uses MAX as a sentinel. Any index size.
Mechanism, terms and numbers459 words
With the flexible StandardQueryParser and a PointsConfig of type Integer or Long, an exclusive bound ({, }, >, <) exactly at the type's MAX or MIN wraps around. PointRangeQueryNodeBuilder makes the bound inclusive with a plain lower + 1 / upper - 1, so i:{2147483647 TO *] is built as i:[-2147483648 TO 2147483647] and matches every document with the field, where the documented meaning ("larger than" Integer.MAX_VALUE) matches none. It is silent, deterministic and independent of the index, and it has shipped in every release since 6.0.0, when this builder replaced the overflow-safe legacy numeric path.
- The syntax parser records
{/}and>/<as exclusive flags:>setslowerInclusive = falseand the upper bound to*(StandardSyntaxParser.jj:763-768;<at:751-756). OpenRangeQueryNodeProcessor, which runs first (StandardQueryNodeProcessorPipeline.java:53-55), turns an unescaped*into empty text (:44-54).PointRangeQueryNodeProcessorparses only non-empty text (:77-79,:94), so an open bound staysnull, and converts the parsed numbers toInteger(:109-111) orLong(:112-114).PointRangeQueryNodeBuilder.build, Integer branch (:63-79): an open lower bound becomesInteger.MIN_VALUE(:65-67) and an open upper boundMAX_VALUE(:72-75); thenif (minInclusive == false) { lower = lower + 1; }(:68-70) andif (maxInclusive == false) { upper = upper - 1; }(:76-78), with no overflow check; thenIntPoint.newRangeQuery(field, lower, upper)(:79), whose bounds are inclusive. The Long branch repeats it (:85-87,:93-95,:96).- For
i:{2147483647 TO *]the lower step gives-2147483648and the open upper bound2147483647: the full range. For[* TO MIN}the upper step wraps to MAX. For{MAX TO MAX]the lower step wraps and the upper stays MAX.
The method's only comment is :62, "TODO: push down cleaning up of crazy nulls and inclusive/exclusive elsewhere"; nothing mentions overflow. Float and Double use Math.nextUp/nextDown (:97-130) and do not wrap.
Five documents with i and l = MIN, -1, 0, 1, MAX. Each of i:{2147483647 TO *], i>2147483647, i:[* TO -2147483648}, i<"-2147483648", i:{2147483647 TO 2147483647], l:{9223372036854775807 TO *], l>9223372036854775807, l:[* TO -9223372036854775808} and l<"-9223372036854775808" builds the full range and matches 5 of 5, where 0 are expected. All 10 controls at and next to the limit are exact (7 strict bounds one step inside the limit, the inclusive i:[2147483647 TO *] and i>=2147483647, and i:{0 TO 2147483647]); for example i:{2147483646 TO *] gives 1 hit and l>9223372036854775806 gives 1. i:{2147483647 TO 2147483647} builds i:[-2147483648 TO 2147483646] and matches 4 of 5.
Exposure and likelihood164 words
- Who.
setPointsConfigMapwith an Integer or Long type. Opt-in, but it is the path a maintainer recommends for numeric fields (Uwe Schindler on java-user, 2023-11-14, "StandardQueryParser and numeric fields") and the one Luke uses when a user assigns a point type to a field (SearchImpl.java:221-240). The recorded GitHub code search found no indexedPointsConfiguse in elastic/elasticsearch or apache/solr. - Trigger. An exclusive bound or
>/<exactly at the type's MAX or MIN. People rarely type it; the plausible route is generated query text on a field that uses MAX/MIN as a sentinel ("never expires" stored asLong.MAX_VALUE, or a "values after the last one seen" filterfield:{<last> TO *]when the last value is MAX). That route is an inference; no user report was found. - Effect. Silent wrong results that fail open: a filter meant to match nothing matches every document with the field; negated, it hides everything. Deterministic, one document shows it, same with assertions on and off.
One JUnit class, one test method, public API only: one document with IntPoint("i", 0), a StandardQueryParser with an Integer PointsConfig, and the count of i:{2147483647 TO *] must be 0. Single thread, ByteBuffersDirectory, deterministic.
/*
* 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 java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.flexible.standard.config.PointsConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestExclusivePointBoundAtMaxValue extends LuceneTestCase {
public void testNothingIsGreaterThanMaxValue() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new IntPoint("i", 0));
w.addDocument(doc);
}
StandardQueryParser qp = new StandardQueryParser();
NumberFormat format = NumberFormat.getIntegerInstance(Locale.ROOT);
qp.setPointsConfigMap(Map.of("i", new PointsConfig(format, Integer.class)));
try (DirectoryReader r = DirectoryReader.open(dir)) {
// expected 0 hits (no int is > Integer.MAX_VALUE); actual 1, because MAX_VALUE + 1 wraps
assertEquals(0, new IndexSearcher(r).count(qp.parse("i:{2147483647 TO *]", "i")));
}
}
}
}
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestExclusivePointBoundAtMaxValue -Ptests.seed=DEADBEEF -Ptests.asserts=true
- Expected
- 0 hits: no int is larger than 2147483647 (
Integer.MAX_VALUE). - Observed
expected:<0> but was:<1>: the query is built asi:[-2147483648 TO 2147483647]; assertions on and off.
More files, environment, full output310 words
Oracle: the documented meaning of {x TO *], "larger than" x, with x = Integer.MAX_VALUE. LuceneTestCase is only the base class; the result does not depend on the seed (DEADBEEF and 5EED1234 give the same failure). To see the wrapped query in the message, keep the parsed query in a local and pass query.toString() to assertEquals; it then reads i:[-2147483648 TO 2147483647] expected:<0> but was:<1>.
Regression test: this same class (or its one method added to TestPointQueryParser); it passes with the recommended fix. If maintainers choose to reject such a bound instead, assert the exception.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; 10.6.0, 10.5.2, 9.12.3: fail; every release 6.0.0 to 10.6.0 fails with an equivalent probe; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; plain integer arithmetic) |
cp $REPRO/TestExclusivePointBoundAtMaxValue.java $LUCENE/lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/
cd $LUCENE && ./gradlew :lucene:queryparser:test --tests TestExclusivePointBoundAtMaxValue -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off fails the same way: -Ptests.asserts=false
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true (stack trimmed; -Ptests.asserts=false identical)
> java.lang.AssertionError: expected:<0> but was:<1>
> at __randomizedtesting.SeedInfo.seed([DEADBEEF:A3F4C59FD775AE79]:0)
> at org.apache.lucene.queryparser.flexible.standard.TestExclusivePointBoundAtMaxValue.testNothingIsGreaterThanMaxValue(TestExclusivePointBoundAtMaxValue.java:47)
:lucene:queryparser:test (FAILURE): 1 test, 1 failure
# the nine limit forms on stock main (5 docs, i and l = MIN, -1, 0, 1, MAX; independent test)
i:{2147483647 TO *] -> i:[-2147483648 TO 2147483647] hits=5 expected=0
i>2147483647 -> i:[-2147483648 TO 2147483647] hits=5 expected=0
i:[* TO -2147483648} -> i:[-2147483648 TO 2147483647] hits=5 expected=0
i<"-2147483648" -> i:[-2147483648 TO 2147483647] hits=5 expected=0
i:{2147483647 TO 2147483647] -> i:[-2147483648 TO 2147483647] hits=5 expected=0
l:{9223372036854775807 TO *] -> l:[-9223372036854775808 TO 9223372036854775807] hits=5 expected=0
l>9223372036854775807 -> l:[-9223372036854775808 TO 9223372036854775807] hits=5 expected=0
l:[* TO -9223372036854775808} -> l:[-9223372036854775808 TO 9223372036854775807] hits=5 expected=0
l<"-9223372036854775808" -> l:[-9223372036854775808 TO 9223372036854775807] hits=5 expected=0
controls at and next to the limit (e.g. i:{2147483646 TO *] 1 hit, i:[2147483647 TO *] 1 hit, l>9223372036854775806 1 hit): 10 of 10 exact- The parser javadoc:
score:{2.5 TO *]"selects documents whose score field is larger than 2.5"; no int is larger thanInteger.MAX_VALUE. IntPoint/LongPoint.newRangeQuery, which the builder calls: "For exclusive ranges, passMath.addExact(lowerValue, 1)".- The legacy numeric path this code replaced returns 0 hits here, and LUCENE-7111 fixed the same wrap elsewhere as a bug in 2016.
The full argument220 words
- Documented semantics.
StandardQueryParser.java:154-158:score:{2.5 TO 7.3}is an "exclusive range ... excluding boundaries", andscore:{2.5 TO *]"selects documents whose score field is larger than 2.5".PointsConfigdocuments no restriction on bound values, and2147483647is a valid int literal. - The API the builder calls says how.
IntPoint.java:186-187,LongPoint.java:205-206: "Ranges are inclusive. For exclusive ranges, passMath.addExact(lowerValue, 1)orMath.addExact(upperValue, -1)." When the builder was written the javadoc said "passlowerValue + 1orupperValue - 1" (IntPoint.javaat89cc676f2bf, line 177); eight days laterc1e95d7b4da(Adrien Grand, 2016-03-17) replaced exactly that advice withMath.addExact, in the same release (6.0.0). The builder still uses the plain+ 1. - Regression against the replaced code. LUCENE-7089 (#8145) added points "the exact same way legacy numerics works", but the legacy query guarded the step (
if (minBound == Long.MAX_VALUE) break;,LegacyNumericRangeQuery.javaat89cc676f2bf, lines 416-419 and 456-459). On 6.0.0 the legacy path gives 0 hits for the same query, the points path 1. - Precedent. LUCENE-7111 (#8166,
751a53fe00a, 2016, CHANGES "Bug Fixes"):DocValuesRangeQuery.newLongRange"behaves incorrectly for Long.MAX_VALUE and Long.MIN_VALUE", the same wrap; Robert Muir suggestedMath.addExact/subtractExactthere. - Tests. No upstream test asserts the wrap:
TestPointQueryParser.java:33-55checks only[1 TO 3]and exact values;TestQPHelper.java:667asserts thatfield>ameans{a TO *].
The devil's-advocate check149 words
The strongest case for "intended": the package docs call the standard package "an example Lucene query parser implementation"; LUCENE-7092 (#8148) kept point ranges inclusive-only because callers "just add/subtract 1"; {MAX TO *] asks for values outside the type; and a maintainer might prefer an exception to an empty query (the read-only OpenAI GPT-5.6 review's argument).
Why it fails: the same package docs call StandardQueryParser "the default implementation of a fully fledged query parser"; LUCENE-7092 says nothing about MAX/MIN, and eight days after the builder landed the javadoc's plain + 1 advice was replaced with an overflow-checked step; the literal is inside the configured type and "larger than MAX" gives an empty match set; and exception versus empty result is a real choice of fix that maintainers may weigh either way, while matching the whole domain is wrong under both. Upstream has fixed StandardQueryParser defects as bugs before (LUCENE-6249, LUCENE-5344).
Recommended Return MatchNoDocsQuery when the exclusive bound is already at the limit (validated)
Before the + 1 / - 1, return MatchNoDocsQuery when an exclusive lower bound is already MAX_VALUE or an exclusive upper bound is already MIN_VALUE, for int and long (+13 lines in PointRangeQueryNodeBuilder.java). The repro, an independent 9-form test with 10 controls and the whole queryparser module (536 tests, two seeds) pass.
if (minInclusive == false) {
if (lower == Integer.MAX_VALUE) {
return new MatchNoDocsQuery("exclusive lower bound is Integer.MAX_VALUE");
}
lower = lower + 1;
}
// ... and before upper = upper - 1:
if (upper == Integer.MIN_VALUE) {
return new MatchNoDocsQuery("exclusive upper bound is Integer.MIN_VALUE");
}
// the same two guards in the Long branch with Long.MAX_VALUE / Long.MIN_VALUEAll options and trade-offs3 options · 534 words
Two fixes were validated with gradle in a scratch worktree of 645d4fe14a20; a third is described. All three make i:{2147483647 TO *] match nothing or fail loudly; they differ in what else they change.
+13 lines in PointRangeQueryNodeBuilder.java: an import and four guards, one before each + 1 / - 1 in the Integer and Long branches.
if (minInclusive == false) {
if (lower == Integer.MAX_VALUE) {
return new MatchNoDocsQuery("exclusive lower bound is Integer.MAX_VALUE");
}
lower = lower + 1;
}
// ... and before upper = upper - 1:
if (upper == Integer.MIN_VALUE) {
return new MatchNoDocsQuery("exclusive upper bound is Integer.MIN_VALUE");
}
// the same two guards in the Long branch with Long.MAX_VALUE / Long.MIN_VALUETrade-offs
- Changes results only for bounds exactly at MAX/MIN, which are wrong today; an empty result is the match set the documented semantics give and what the legacy path returned.
- Leaves the open-bound sibling (
i:{* TO 0]drops aMIN_VALUEdocument) unchanged. - Backport-friendly: the file is the same blob on 9.12.3, 10.5.2, 10.6.0 and main.
- A downstream project with its own builder keyed by
PointsConfigmade the same choice (FoundationDB/fdb-record-layer#1849, 2022).
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20: the repro passes with-Ptests.asserts=trueand=false; the tie-break's independent test (9 limit forms, 10 controls) passes 2 of 2. :lucene:queryparser:test, whole module, seeds DEADBEEF and CAFEBABE: SUCCESS, 536 tests (533 + the repro + 2 independent), 1 skipped, 0 failed.checkGoogleJavaFormat,forbiddenApisMain/TestandecjLintMain/Testpass (run without the copied-in independent test, whose own formatting fails the check; it is not part of the fix). An independent fact check repeated the repro, the module (534 tests with the repro) and the three lint checks with this diff.
Also sets minInclusive = true / maxInclusive = true when a bound is null, in all four type branches (+13 lines).
Trade-offs
Also fixes i:{* TO 0] dropping the MIN_VALUE document and float/double {* TO x] dropping -Infinity. But it changes behaviour the study verifier called debatable: the legacy numeric query and TermRangeQuery ignore exclusivity on an open bound, and Uwe Schindler (LUCENE-3338, #4411, 2011) was "glad" that [x TO *] does not behave differently from [x TO *}. Better as a separate decision.
What the checks validated
Same gradle runs: the repro passes with assertions on and off, the independent test 2 of 2, the whole module with both seeds 536 tests and 0 failures, checkGoogleJavaFormat passes (without the copied-in independent test).
Follow the IntPoint/LongPoint javadoc literally and turn the ArithmeticException into a QueryNodeException.
Trade-offs
A parse error instead of an empty result for a query whose documented match set is empty; applications that generate such bounds would start failing loudly. The documentation does not rule this option out, and a maintainer may well prefer it: the bound cannot be made inclusive, and rejecting it is what the javadoc's Math.addExact advice does. The regression test would assert the exception instead of 0 hits.
Where an application turns generated or user-supplied text into a restricting filter with this parser, a bound exactly at the type limit makes that filter match every document with the field (negated, hide them all).
A study of Lucene's points query code wrote Dafny proof obligations with Java's int/long wrap-around made explicit. The obligation that the parser's inclusive bound equals the exclusive one failed in about 2 seconds, with the counterexample L = 2147483647. An independent verifier then reproduced it with its own test on main, 9.12.3 and 10.5.2.
The method in full117 words
Method: semi-formal (a code-reading hypothesis checked by a Dafny proof obligation).
- Finder (study experiment on points and BKD query evaluation, 2026-10-22, main
7cecf79523).Bounds.dfymodels Javaint/longarithmetic with explicitWrap32/Wrap64. The obligationsParserExclusiveLowerInt,ParserExclusiveUpperIntandParserExclusiveLowerLongfailed: for L = 2147483647 the real exclusive range is empty, butWrap32(L + 1) = MINmatches. Then a 3-test JUnit. - Verification. An independent study verifier wrote its own 9-case test with 7 controls (5 seeds) and a 4-guard counterfactual patch; a follow-up found the open-bound sibling; the hunt added a one-test minimal repro, a reproducer, a refute review, a second adjudication, a history and a prior-report follow-up (see the checks below).
No prior report found: 188 queries and checks over GitHub issues and PRs, the JIRA archive, dev@, java-user@, issues@, commits@, downstream trackers and the web (2026-10-31). Closest: LUCENE-7111 (#8166), the same wrap in DocValuesRangeQuery, fixed in 2016 without touching the parser.
Searches4 searches
Verdict new: no prior public report was found in the searched records. Not fixed upstream: the builder's only commits are 89cc676f2bf (2016) and 2cbf261032d (2021 reformat), and no match was found among the changed-file lists of all 409 open PRs (first 100 files of each).
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene) | 2026-10-31 | the scripted prior-report search (40 issue/PR, 3 commit queries) plus 35 manual searches (class names, StandardQueryParser with range/points/exclusive, overflow/wrap/MAX/MIN phrasings), a scan of all 106 module:queryparser issue titles the changed-file lists of all 409 open PRs (first 100 files of each), 3 commit-API queries on the builder's path and a git log -S in the history clone | context only: #8166 / LUCENE-7111 (same wrap, other class, fixed 2016), #8145 / LUCENE-7089 (introduced the code), #8148 / LUCENE-7092 (inclusive-only API), #4411 / LUCENE-3338, #4426 / LUCENE-3353, #8645 / LUCENE-7594 |
| Downstream (Elasticsearch, OpenSearch, Solr, lucene-solr, Lucene.NET, GitHub-wide) | 2026-10-31 | 2 tool and 13 manual queries | FoundationDB/fdb-record-layer#1849 (2022) special-cases MAX/MIN in its own builder; no report of Lucene's |
| Mailing lists, JIRA and the web | 2026-10-31 | 55 list queries (dev@, java-user@, issues@, commits@; 32 by the tool, 23 manual), 16 JIRA JQL queries (8 + 8), 9 web queries (6 + 3) | a 2023 java-user thread recommending setPointsConfigMap; no bound problem raised |
| Earlier searches | 2026-10-22, 2026-10-31 | the study verifier's GitHub and JIRA searches; a first scripted prior-report search run (45 queries) | nothing |
Introduced by 89cc676f2bf (LUCENE-7089, 2016-03-09), which added point support to the flexible parser to replace the overflow-safe legacy numerics; first released in 6.0.0. All 93 releases 6.0.0 to 10.6.0 and main fail; 5.5.0 has no points API and its legacy path is correct.
Versions tested6 rows
Introduced by 89cc676f2bf "LUCENE-7089, LUCENE-7075: add points to flexible queryparser to replace legacy numerics support" (Robert Muir, 2016-03-09; a JIRA patch, migrated as #8145). It reached 6.0.0 through the branch_6_0 cherry-pick 306d3fe8524. git blame -w of the method gives it for every line; the only later commit, 2cbf261032d (LUCENE-9570, 9.0.0), is formatting. The Math.addExact javadoc (c1e95d7b4da, 2016-03-17) shipped in the same release.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal repro fails with assertions on and off (gradle and standalone) |
| 10.6.0, 10.5.2, 9.12.3 | affected | the minimal JUnit test fails with assertions on and off (SHA-1-checked jars), 6 of 6 runs |
| 6.0.0 to 10.6.0 (93 releases) | affected | main() probe: i:{2147483647 TO *] parsed to i:[-2147483648 TO 2147483647], 1 hit; the control i:[0 TO 0] finds the doc |
| 6.0.0, legacy numeric path | same release | LegacyIntField + LegacyNumericConfig: 0 hits; only the new points path wraps |
| 5.5.0 | not affected | no IntPoint/PointsConfig; the legacy numeric path gives 0 hits |
main 645d4fe14a20 with the fix | not affected | repro, independent test and queryparser module pass |
The study's verifier, a reproducer, a tie-break adjudication, the version-history runs and this doc's gradle run all reproduced it; the only dissent was a procedural "not real" from a read-only OpenAI GPT-5.6 review that could not run commands and agreed on the root cause.
Checks and verdicts301 words
Every executed check ran on stock builds or SHA-1-checked release jars; the cross-model reviewer could only read code.
| Check | Verdict | What it found |
|---|---|---|
| Study finder (Dafny) | candidate | Three wrap obligations failed (int lower and upper, long lower); a 3-test JUnit. |
| Study verifier | new bug | Own 9-case test with 7 controls on main (5 seeds), plain main() on 9.12.3 and 10.5.2; a 4-guard patch makes all 9 pass. |
| Minimal repro | verified | Fails on 645d4fe14a20 with -ea and -da, seeds DEADBEEF and 5EED1234; the fix12b shadow passes. |
| Reproducer | real | Main and 10.5.2 with -ea and -da; the study's 9-form test on main: all 9 mismatch. |
| Refute lens (OpenAI GPT-5.6, read-only) | not real (procedural, low) | Could not run commands; agreed on the root cause and that the inputs are legal; rated the intended argument weak. |
| Tie-break adjudication | real | The minimal test's query-as-message variant on main, 10.6.0 and 9.12.3; own test with 10 controls and 9 limit forms over 20 seeds (controls pass, trigger fails every time). |
| History follow-up | 6.0.0, 89cc676f2bf | 93 releases fail; 5.5.0 and the 6.0.0 legacy path are correct. |
| Prior-report follow-up | new | 188 queries; closest is the LUCENE-7111 precedent. |
| This doc (gradle) | real; fixes validated | Stock fails with assertions on and off; both fixes pass the repro, the independent test and the queryparser module (536 tests, two seeds). |
| Fact check | correct in substance | Ran the minimal test on main, 10.6.0, 10.5.2 and 9.12.3 with assertions on and off (8 of 8 fail), the documented gradle command, fix A with the module and lint, and its own probe; three text fixes, applied. |
| Cross-model fact check (OpenAI GPT-5.6, read-only) | core bug real | Read the code, history and recorded runs (no Java run); wording fixes, applied. |
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 528 KB
Finding record, Lucene and upstream sources, repro files and diagram notes644 words
- Registry title
- Flexible StandardQueryParser: an exclusive int/long point bound at MAX/MIN wraps around, so the query matches every document with the field instead of none
- Area
- queryparser / flexible
- Priority
- Low, rank 769: The flexible parser's exclusive int/long bound exactly at MAX or MIN wraps so the query fails open and matches every document with the field, an int-limit edge case on a niche parser config, below LFV-082 because its trigger is narrower.
- Impact class
- silent wrong results
- Impact note
- Users of the opt-in flexible StandardQueryParser with a PointsConfig (Luke's search tab uses it) whose query has an exclusive bound exactly at the int/long limit, for example generated filters on a field that uses MAX/MIN as a sentinel; ordinary scale; released (every release 6.0.0 to 10.6.0 executed, 93 releases).
- Summary
- You parse i:{2147483647 TO *], i>2147483647, l>9223372036854775807 or i<"-2147483648" with the flexible StandardQueryParser and a PointsConfig for the field. You expect no hits, because no int is larger than Integer.MAX_VALUE (or smaller than MIN_VALUE). Instead PointRangeQueryNodeBuilder adds or subtracts 1 for the exclusive bound without an overflow check, so MAX+1 wraps to MIN, the query becomes the full range and matches every document with the field (5 of 5 in the repro; {MAX TO MAX} matches all but one).
- Introduced
- 89cc676f2bf (LUCENE-7089) (2016-03-09)
- First affected version
- 6.0.0 [exec] (89cc676f2bf, LUCENE-7089, reached 6.0.0 through the branch_6_0 cherry-pick 306d3fe8524); 5.5.0 is not affected: it has no points API and its legacy numeric path gives 0 hits [exec]
- Affected versions
- every release from 6.0.0 to 10.6.0 (93 releases) and main 645d4fe14a20 [exec: a main() probe fails on each release with -ea (10.6.0 and main also -da), and the minimal JUnit test fails on 9.12.3, 10.5.2 and 10.6.0 with -ea and -da]; never fixed
- 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
- semi-formal: Dafny, code reading
- Upstream and prior reports
- none found (study searches 2026-10-22; public-list check 2026-10-20..23)
- 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; with the recommended fix it passes, as do an independent 9-form test with 10 controls and the whole queryparser module (536 tests, seeds DEADBEEF and CAFEBABE). The version-history runs ran all 93 releases 6.0.0 to 10.6.0 (SHA-1-checked jars, all fail) and 5.5.0 (no points API; its legacy numeric path is correct). Upstream main2f424fd428da(2026-10-30) has the same code [read].
- Finding record
SECURITY.md:38, 50 (threat model)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/builders/PointRangeQueryNodeBuilder.java:62-96a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:751-756, :763-768a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/processors/PointRangeQueryNodeProcessor.java:77-79, :94, :109-114; processors/OpenRangeQueryNodeProcessor.java:44-54; processors/StandardQueryNodeProcessorPipeline.java:53-55a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/IntPoint.java:186-187; LongPoint.java:205-206
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/luke/src/java/org/apache/lucene/luke/models/search/SearchImpl.java:221-240
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/StandardQueryParser.java:154-158a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/IntPoint.java:186-187; LongPoint.java:205-206a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/package-info.java:31-32, :41-42a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestPointQueryParser.java:33-55; TestQPHelper.java:667a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:9504 (LUCENE-7111, 6.0.0 Bug Fixes; again at :9621 under 5.5.2), :10977 (LUCENE-6249), :13004 (LUCENE-5344)
- TestExclusivePointBoundAtMaxValue.java (one test class):
repro/LFV-050/src/org/apache/lucene/queryparser/flexible/standard/TestExclusivePointBoundAtMaxValue.java(51 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs (the minimal repro and the nine limit forms on stock main).
assets/LFV-050/bug.png(528 KB)
![Diagram of the bug: the query i:{2147483647 TO *] asks for ints larger than Integer.MAX_VALUE, which is none. The builder turns the exclusive bound into an inclusive one by adding 1; 2147483647 + 1 wraps to -2147483648, so the built range i:[-2147483648 TO 2147483647] covers every int and matches every document with the field (1 of 1 in the repro, 5 of 5 on the five-value index).](../assets/LFV-050/bug.png)