All findings · Lucene finding · LFV-050 · flexible-parser-point-bound-wrap

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

With the flexible StandardQueryParser and an Integer PointsConfig, i:{2147483647 TO *] (larger than Integer.MAX_VALUE) should match nothing, but the builder's unchecked + 1 wraps 2147483647 to -2147483648, so the query matches every document with the field. Long fields and >/< at MAX/MIN do the same, since 6.0.0.

Summary

Priority
Low, rank 769The 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
silent wrong results
Since
6.0.0 (LUCENE-7089, 2016)introduced 2016-03-09 · over 10 years
Affected
every release 6.0.0 to 10.6.0 (93 executed), and main
Status
documented
Found by
semi-formal: Dafny proof obligation with explicit int/long wrap-around

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

Exclusive bounds and point range queries

  1. In the standard query syntax, square brackets include a bound and curly brackets exclude it: i:{5 TO *] means i > 5, and the operator form i>5 means the same.
  2. With a PointsConfig for the field (whose type matches how the field is indexed, one-dimensional IntPoint or LongPoint values), the flexible StandardQueryParser builds an IntPoint or LongPoint range query. Those take inclusive bounds only, so the parser must rewrite i > 5 as i >= 6.
  3. Java int and long arithmetic wraps silently at the limits: Integer.MAX_VALUE + 1 is Integer.MIN_VALUE (2147483647 + 1 = -2147483648).
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).
The exclusive bound 2147483647 is made inclusive by adding 1, which wraps to -2147483648, so the built range covers every int. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document with IntPoint("i", 0) and give StandardQueryParser an Integer PointsConfig for i, the setup of Lucene's own TestPointQueryParser and of Luke's search tab.
  2. Parse i:{2147483647 TO *], documented as "larger than" 2147483647. That is Integer.MAX_VALUE, so you expect 0 hits.
  3. Instead you get 1 hit. Point ranges take inclusive bounds, so the builder turns > 2147483647 into >= 2147483647 + 1, and in Java int arithmetic that is -2147483648. The query becomes i:[-2147483648 TO 2147483647], every int.
  4. 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).
  5. 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.

Who is affected

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.

What the code does

  1. The syntax parser records {/} and >/< as exclusive flags: > sets lowerInclusive = false and the upper bound to * (StandardSyntaxParser.jj:763-768; < at :751-756).
  2. OpenRangeQueryNodeProcessor, which runs first (StandardQueryNodeProcessorPipeline.java:53-55), turns an unescaped * into empty text (:44-54). PointRangeQueryNodeProcessor parses only non-empty text (:77-79, :94), so an open bound stays null, and converts the parsed numbers to Integer (:109-111) or Long (:112-114).
  3. PointRangeQueryNodeBuilder.build, Integer branch (:63-79): an open lower bound becomes Integer.MIN_VALUE (:65-67) and an open upper bound MAX_VALUE (:72-75); then if (minInclusive == false) { lower = lower + 1; } (:68-70) and if (maxInclusive == false) { upper = upper - 1; } (:76-78), with no overflow check; then IntPoint.newRangeQuery(field, lower, upper) (:79), whose bounds are inclusive. The Long branch repeats it (:85-87, :93-95, :96).
  4. For i:{2147483647 TO *] the lower step gives -2147483648 and the open upper bound 2147483647: 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.

The numbers

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. setPointsConfigMap with 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 indexed PointsConfig use 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 as Long.MAX_VALUE, or a "values after the last one seen" filter field:{<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.

Minimal repro

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.

TestExclusivePointBoundAtMaxValue.java (one test class)51 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 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")));
      }
    }
  }
}
Command
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 as i:[-2147483648 TO 2147483647]; assertions on and off.

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

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.

Environment

lucenemain 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
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; plain integer arithmetic)
Command (environment)
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

Observed output

# 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

Why it looks unintended

  • The parser javadoc: score:{2.5 TO *] "selects documents whose score field is larger than 2.5"; no int is larger than Integer.MAX_VALUE.
  • IntPoint/LongPoint.newRangeQuery, which the builder calls: "For exclusive ranges, pass Math.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", and score:{2.5 TO *] "selects documents whose score field is larger than 2.5". PointsConfig documents no restriction on bound values, and 2147483647 is 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, pass Math.addExact(lowerValue, 1) or Math.addExact(upperValue, -1)." When the builder was written the javadoc said "pass lowerValue + 1 or upperValue - 1" (IntPoint.java at 89cc676f2bf, line 177); eight days later c1e95d7b4da (Adrien Grand, 2016-03-17) replaced exactly that advice with Math.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.java at 89cc676f2bf, 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 suggested Math.addExact/subtractExact there.
  • Tests. No upstream test asserts the wrap: TestPointQueryParser.java:33-55 checks only [1 TO 3] and exact values; TestQPHelper.java:667 asserts that field>a means {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).

Suggested fix

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_VALUE
All 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.

Return MatchNoDocsQuery when the exclusive bound is already at the limit (validated) (recommended)

+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_VALUE

Trade-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 a MIN_VALUE document) 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 PointsConfig made the same choice (FoundationDB/fdb-record-layer#1849, 2022).

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20: the repro passes with -Ptests.asserts=true and =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/Test and ecjLintMain/Test pass (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.

The same guards plus "an open bound is inclusive" (the study's fix12b; validated)

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).

Math.addExact / subtractExact, rejecting the query (not run)

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.

Downstream note

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).

How it was found

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.dfy models Java int/long arithmetic with explicit Wrap32/Wrap64. The obligations ParserExclusiveLowerInt, ParserExclusiveUpperInt and ParserExclusiveLowerLong failed: for L = 2147483647 the real exclusive range is empty, but Wrap32(L + 1) = MIN matches. 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).

Prior reports, history and checks

Prior reports

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).

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene)2026-10-31the 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 clonecontext 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-312 tool and 13 manual queriesFoundationDB/fdb-record-layer#1849 (2022) special-cases MAX/MIN in its own builder; no report of Lucene's
Mailing lists, JIRA and the web2026-10-3155 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 searches2026-10-22, 2026-10-31the study verifier's GitHub and JIRA searches; a first scripted prior-report search run (45 queries)nothing

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and off (gradle and standalone)
10.6.0, 10.5.2, 9.12.3 affectedthe 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) affectedmain() 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 releaseLegacyIntField + LegacyNumericConfig: 0 hits; only the new points path wraps
5.5.0 not affectedno IntPoint/PointsConfig; the legacy numeric path gives 0 hits
main 645d4fe14a20 with the fix not affectedrepro, independent test and queryparser module pass

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
Study finder (Dafny) candidateThree wrap obligations failed (int lower and upper, long lower); a 3-test JUnit.
Study verifier new bugOwn 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 verifiedFails on 645d4fe14a20 with -ea and -da, seeds DEADBEEF and 5EED1234; the fix12b shadow passes.
Reproducer realMain 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 realThe 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, 89cc676f2bf93 releases fail; 5.5.0 and the 6.0.0 legacy path are correct.
Prior-report follow-up new188 queries; closest is the LUCENE-7111 precedent.
This doc (gradle) real; fixes validatedStock 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 substanceRan 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 realRead the code, history and recorded runs (no Java run); wording fixes, applied.

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

Finding record

LFV-050, snapshot of 2026-11-06

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 main 2f424fd428da (2026-10-30) has the same code [read].

Lucene and upstream sources

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-96
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj:751-756, :763-768
  • a 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-55
  • a 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-158
  • a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/document/IntPoint.java:186-187; LongPoint.java:205-206
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/java/org/apache/lucene/queryparser/flexible/standard/package-info.java:31-32, :41-42
  • a Lucene main checkout (645d4fe14a20) lucene/queryparser/src/test/org/apache/lucene/queryparser/flexible/standard/TestPointQueryParser.java:33-55; TestQPHelper.java:667
  • a 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)

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

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the executed runs (the minimal repro and the nine limit forms on stock main).

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.