All findings · Lucene finding · LFV-083 · functionrangequery-exclusive-bound-saturation

FunctionRangeQuery over Int/Long/EnumFieldSource turns an exclusive bound at the type limit into an inclusive one

FunctionRangeQuery over an IntFieldSource asked for n > 2147483647 (Integer.MAX_VALUE) still returns the document whose value is 2147483647: 1 hit instead of 0. The same happens at MIN_VALUE and for long and enum sources, in every checked release from 5.4.0 to 10.6.0 and on main.

Summary

Priority
Low, rank 960FunctionRangeQuery over Int/Long/Enum sources turns an exclusive bound at the type limit into an inclusive one, letting through only a document whose value is exactly MIN or MAX, a module API and int-limit edge case.
Impact
silent wrong results
Since
5.4.0 (2015)introduced 2011-06-28 · over 15 years
Affected
5.4.0 to 10.6.0 (every checked release) and main
Status
documented
Found by
code reading while verifying a related parser bug

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

To make a bound exclusive, the code moves it one step and then tests inclusively. At the type limit it skips the step to avoid an overflow, so the bound silently stays inclusive.

Background: how a function range is matched

  1. FunctionRangeQuery(source, lower, upper, includeLower, includeUpper) matches documents whose value from a ValueSource lies in the range; includeLower=false means value > lower, includeUpper=false means value < upper.
  2. For int and long sources, Lucene rewrites a strict bound into an inclusive one on the next integer, n > 5 into n >= 6, so one inclusive test lower <= n <= upper covers all four cases.
Diagram of the bug, from the code at 645d4fe14a20 and runs on main. One document with n = 2147483647 (Integer.MAX_VALUE). The query FunctionRangeQuery(IntFieldSource n, lower 2147483647, exclusive), printed frange(int(n)):{2147483647 TO *], means n > 2147483647: expected 0 hits. IntDocValues.getRangeScorer turns an exclusive bound into an inclusive one with lower++ only when lower < Integer.MAX_VALUE; at 2147483647 the bound is left as is, the inclusive test val >= 2147483647 is true, and the query returns 1 hit. The same at Integer.MIN_VALUE, for Long and for EnumFieldSource; one step inside the limit is correct. Fix: an exclusive bound at the limit makes the range empty.
One document holds n = 2147483647. n > 2147483647 should become n >= 2147483648, which no int can hold, so nothing should match; the code skips the step at the limit and tests n >= 2147483647 instead, so the document matches. 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 NumericDocValuesField("n", Integer.MAX_VALUE), so n = 2147483647, and search new FunctionRangeQuery(new IntFieldSource("n"), "2147483647", null, false, true): n > 2147483647, printed {2147483647 TO *].
  2. You expect 0 hits: no int is greater than 2147483647. The float and double sources do give 0 hits for x > MAX_VALUE.
  3. Instead you get 1 hit. The code turns n > 5 into n >= 6 by adding 1, but skips the + 1 at 2147483647 to avoid an overflow, so n > 2147483647 stays n >= 2147483647 and the document matches.
  4. The same holds for n < Integer.MIN_VALUE, m > Long.MAX_VALUE, m < Long.MIN_VALUE and enum sources: 1 hit instead of 0 each. One step inside the limit, n > 2147483646, correctly gives 1. No error, with assertions on or off.

Who is affected

Users of the lucene/queries function-range API over int, long or enum sources, when a bound is generated exactly at MIN_VALUE or MAX_VALUE and a document holds exactly that value, for example a MAX_VALUE sentinel. Only that value is over-matched, silently. Opt-in module, rare.

Mechanism, terms and numbers572 words

FunctionRangeQuery with includeLower=false or includeUpper=false means a strict bound (> or <). For int, long and enum value sources, getRangeScorer implements it by moving the endpoint one step (lower++, upper--) and then testing inclusively, but it skips the step when the bound is already MAX_VALUE (or MIN_VALUE) to avoid an overflow. The bound then stays inclusive, so n > Integer.MAX_VALUE matches the document whose value is exactly Integer.MAX_VALUE: 1 hit instead of 0, silently, with assertions on and off.

It affects IntDocValues, LongDocValues and EnumFieldSource (six lines), every checked release with FunctionRangeQuery (5.4.0 to 10.6.0) and main, and the public FunctionValues.getRangeScorer since 4.0.0-ALPHA. Only a document whose value is exactly the limit is over-matched, so it is a rare, low-priority bug. Making the range empty when an exclusive bound sits at the limit fixes it; that fix passes the repro, the module's tests and check.

The bug, step by step (main 645d4fe14a20, under lucene/queries/src/java/org/apache/lucene/queries/function/)

  1. Entry. FunctionRangeQuery keeps the bounds as strings plus the two flags (FunctionRangeQuery.java:55-80; the Number constructor accepts Integer.MAX_VALUE directly), prints false as { / } (:102-112) and, per segment, calls functionValues.getRangeScorer(context, lowerVal, upperVal, includeLower, includeUpper) (:172). IntFieldSource returns an IntDocValues, LongFieldSource a LongDocValues.
  2. The endpoint adjustment. IntDocValues.getRangeScorer (docvalues/IntDocValues.java:81-117) says "instead of using separate comparison functions, adjust the endpoints" (:90), then runs if (!includeLower && lower < Integer.MAX_VALUE) lower++; (:96) and if (!includeUpper && upper > Integer.MIN_VALUE) upper--; (:103), and matches with val >= ll && val <= uu (:114).
  3. At the limit. The condition lower < Integer.MAX_VALUE stops lower++ from wrapping to Integer.MIN_VALUE (which would match everything). But when lower is MAX_VALUE nothing happens at all: the bound stays 2147483647, and val >= 2147483647 admits the MAX_VALUE document. n > 2147483647 becomes the same test as n >= 2147483647. The upper bound at MIN_VALUE is the mirror image.
  4. Same lines elsewhere. docvalues/LongDocValues.java:99 (comment), :105, :112 (guards with Long.MAX_VALUE / Long.MIN_VALUE), :123 (inclusive test); valuesource/EnumFieldSource.java:143, :148, :154, :165 (int guards on the enum's int value). By inheritance (read, not executed): MultiValuedIntFieldSource and MultiValuedLongFieldSource extend IntFieldSource / LongFieldSource and read real numeric doc values, so they are affected the same way. The count sources (DocFreqValueSource, JoinDocFreqValueSource, MaxDocValueSource, NumDocsValueSource, TermFreqValueSource, TotalTermFreqValueSource, SumTotalTermFreqValueSource) also return an IntDocValues or LongDocValues, but are not practical triggers: their values are never negative and cannot realistically reach MAX_VALUE (document counts are at most IndexWriter.MAX_DOCS = Integer.MAX_VALUE - 128).
  5. The contrast. The default FunctionValues.getRangeScorer (FunctionValues.java:224-292) and DoubleDocValues (docvalues/DoubleDocValues.java:86-147) honour a strict bound with strict comparisons (docVal > l, docVal < u), so the float and double sources are right at the limit.

Executed on main (one document per value, assertions off; the same with assertions on and on 10.5.2)

rangeexpectedactual
int n > 2147483647 (MAX)01
int n < -2147483648 (MIN)01
int n > 2147483646, n < -2147483647 (controls)11
long m > 9223372036854775807 (MAX)01
long m < -9223372036854775808 (MIN)01
long one step inside the limits (controls)11
enum e > top (int value MAX), e < bottom (int value MIN)01
float x > 3.4028235E38, double x > 1.7976931348623157E30800
Exposure and likelihood180 words
  • Who is hit. Users of FunctionRangeQuery (lucene/queries, @lucene.experimental) or of FunctionValues.getRangeScorer directly, over an IntFieldSource, LongFieldSource (or their multi-valued subclasses) or EnumFieldSource. Not default and not in lucene/core.
  • What it takes. Two things at once: an exclusive bound exactly at Integer/Long MIN_VALUE or MAX_VALUE, and a document whose value is exactly that limit. Nobody types n > 2147483647 by hand; such bounds come from generated queries, for example "greater than the last value seen" over a field that uses MAX_VALUE as a sentinel (an example route, not executed). One document is enough; no scale is needed.
  • What goes wrong. One extra value matches, silently: no exception and the same result with assertions on and off. Where the range is used as a filter, the limit-valued documents slip through it.
  • Enum. The EnumFieldSource case needs an enum whose int value is exactly Integer.MIN_VALUE or Integer.MAX_VALUE, which is contrived; the int and long cases are the ones that matter.
  • Likelihood: rare. No public report found since the guard entered Lucene in 2011.

Minimal repro

One JUnit test in lucene/queries, public API only: one document with n = Integer.MAX_VALUE in a ByteBuffersDirectory, one FunctionRangeQuery for n > Integer.MAX_VALUE, and a count. Fixed inputs, single thread; fails under the recorded seed with assertions on and off, and under a random seed.

TestFunctionRangeQueryExclusiveLimit.java (one test)33 lines
package org.apache.lucene.queries.function;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queries.function.valuesource.IntFieldSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestFunctionRangeQueryExclusiveLimit extends LuceneTestCase {

  public void testExclusiveLowerBoundAtIntMax() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        doc.add(new NumericDocValuesField("n", Integer.MAX_VALUE));
        w.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        Query q =
            new FunctionRangeQuery(
                new IntFieldSource("n"), String.valueOf(Integer.MAX_VALUE), null, false, true);
        // n > Integer.MAX_VALUE matches nothing: expected 0 hits, actual 1
        assertEquals(q.toString(), 0, new IndexSearcher(reader).count(q));
      }
    }
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or 10.6.0, branch_10_6, upstream main)
cp $REPRO/TestFunctionRangeQueryExclusiveLimit.java lucene/queries/src/test/org/apache/lucene/queries/function/
./gradlew :lucene:queries:test --tests org.apache.lucene.queries.function.TestFunctionRangeQueryExclusiveLimit -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
Expected
0 hits: no int is greater than Integer.MAX_VALUE.
Observed
1 hit, the document holding 2147483647: frange(int(n)):{2147483647 TO *] expected:<0> but was:<1>, with assertions on and off.

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

More files, environment, full output149 words

Plain IndexWriterConfig and IndexSearcher. The test's own inputs are fixed (it calls no random()); the test framework still picks a seeded random codec, similarity, locale and time zone, and the result is the same under any seed tried. The package is org.apache.lucene.queries.function only for locality.

As the regression test it needs only the ASF license header; with it, :lucene:queries:check -x test passes, and it fails on stock and passes with the recommended fix.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): affected; the same test also fails on the 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0 jars
jdkOpenJDK 25.0.3
osLinux, aarch64

Observed output

TestFunctionRangeQueryExclusiveLimit > testExclusiveLowerBoundAtIntMax FAILED
    java.lang.AssertionError: frange(int(n)):{2147483647 TO *] expected:<0> but was:<1>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:25D5DFBD05250EC1]:0)
        at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
        at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:647)
        at org.apache.lucene.queries.function.TestFunctionRangeQueryExclusiveLimit.testExclusiveLowerBoundAtIntMax(TestFunctionRangeQueryExclusiveLimit.java:29)

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

# the same failure with -Ptests.asserts=false, and with the standalone runner on main and the 10.5.2 and 9.12.3 jars

Why it looks unintended

  • The code comment says the step replaces strict comparisons, and the float, double and default implementations compare strictly: 0 hits.
  • The test that came with the guard in 2009 asserts that exclusive frange matches the same documents as a standard exclusive range query.
  • Lucene made the same corner case an empty result in NumericRangeQuery (2009) and DocValuesRangeQuery (2016).
The full argument430 words
  • The comment. "instead of using separate comparison functions, adjust the endpoints" (IntDocValues.java:90, LongDocValues.java:99, EnumFieldSource.java:143): the adjustment must give the same answer as docVal > lower, which is no match for lower = MAX_VALUE. The default FunctionValues.getRangeScorer and DoubleDocValues do exactly that.
  • The author's own test. The guard was written in SOLR-939 (6bd49091023, 2009-03-23, Solr's frange). The same commit's TestRangeQuery.testRandomRangeQueries builds, for random bounds with incl=false incu=false, both field:{lower TO upper} and the frange query and asserts they match the same documents (sameDocs). It indexes values 5 to 24 and draws bounds from 0 to 29, so it never reaches the type limit. The same commit's testRangeQueries also asserts that an exclusive frange lower bound at the largest indexed value matches 0 documents.
  • The same corner case, fixed as a bug. Commit d820e34a260 (2009-06-26, NumericRangeQuery): "Fix the special corner case, that an exclusive range starting/ending with MAX_VALUE/MIN_VALUE overflows the bounds. Return a empty TermEnum in this case", with tests asserting EMPTY_DOCIDSET for an exclusive range starting at Integer.MAX_VALUE or ending at Integer.MIN_VALUE. That class was then one week old and unreleased, and there minBound++ wrapped around rather than saturating; the fix made the range empty. LUCENE-7111 (#8166, 2016) fixed the same corner case in the sandbox DocValuesRangeQuery (there the +1 overflowed and the query matched every document) with a patch that returns no documents, which Yonik Seeley, the author of this guard, +1'd. FunctionRangeQuery.java:38 still names DocValuesRangeQuery as similar; that javadoc reference is stale, as the class no longer exists on main.
  • The other range code we checked on main. Facet LongRange rejects such a range as matching nothing (IllegalArgumentException, LongRange.java:58-76, Range.java:37-40); the float and double function sources return 0 hits (executed).
  • Not documented or tested where we looked. No javadoc, CHANGES.txt or MIGRATE.md entry says an exclusive bound at the limit is inclusive; TestFunctionRangeQuery uses bounds between 2 and 10 and TestValueSources uses MIN..MAX inclusive. No javadoc sentence defines the limit case the other way either; the case for "empty" rests on the comment, the default implementation and the tests above.
  • Legal input. Public constructors, a valid int or long value, no documented precondition. Out-of-range literals are rejected (Integer.parseInt / Long.parseLong throw NumberFormatException), so the exact limit values are the valid boundary inputs. EnumFieldSource resolves a bound through its label map (unknown labels fall back to its default value), so the enum case needs an enum that maps a label to Integer.MIN_VALUE or Integer.MAX_VALUE, as the probe's enum does.
The devil's-advocate check152 words

The strongest case: the guard is deliberate overflow protection and saturating at the limit is a conscious trade-off; n > Integer.MAX_VALUE is a meaningless query, so garbage in, garbage out; the class is @lucene.experimental, and open issue #16820 lists ValueSource among deprecation candidates for Lucene 11. The cross-model review rated this argument weak.

Why it fails: preventing the overflow is right, but keeping the bound inclusive contradicts the comment above it, the author's tests, the 2009 NumericRangeQuery and 2016 LUCENE-7111 fixes, and the comparators we checked on main (the default FunctionValues scorer and DoubleDocValues match nothing; facet LongRange rejects the range). No javadoc sentence defines the limit case, so this rests on that code and test evidence. A generated "after the last value" bound is a valid question whose answer is empty. @lucene.experimental allows API changes, not wrong results. It is a reason for low priority, not for keeping the behaviour.

Suggested fix

Recommended An exclusive bound at the limit makes the range empty (+63/-6 in three files)

When an exclusive bound sits exactly at the limit, use an empty range instead of keeping the bound, in IntDocValues, LongDocValues and EnumFieldSource; the overflow protection stays. Validated on main: the repro passes with assertions on and off, the closest suites (62 tests) and the whole lucene/queries test task (512 tests) pass, and check passes.

if (!includeLower) {
  if (lower == Integer.MAX_VALUE) {
    matchesNothing = true; // nothing is greater than Integer.MAX_VALUE
  } else {
    lower++;
  }
}
// ... the same for the upper bound at Integer.MIN_VALUE, then:
if (matchesNothing) {
  lower = 1; // an empty range (lower > upper)
  upper = 0;
}
All options and trade-offs5 options · 644 words

The recommended fix was run in a scratch worktree created at 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF) and removed afterwards; the script does not record the worktree's revision, which is corroborated by the diff applying cleanly to the 645d4fe14a20 files. An independent fact check re-ran the key steps in its own worktree with the revision recorded (645d4fe14a20): the repro fails on stock (assertions on and off, and a random seed) and passes with the fix (on and off); TestFunctionRangeQuery and TestValueSources (50 tests) and checkGoogleJavaFormat pass with the fix. "Closest suites" means TestFunctionRangeQuery, TestValueSources, TestDocValuesFieldSources, TestSortedSetFieldSource, TestFunctionQuerySort and TestFieldScoreQuery in lucene/queries.

An exclusive bound at the limit makes the range empty (+63/-6 in three files) (validated by the checks) (recommended)

In each of the three getRangeScorer overrides, replace the saturating guard with an explicit check; when an exclusive bound is at the limit, set an empty range (lower > upper), for which the existing inclusive test matches nothing. The upper bound at MIN_VALUE, LongDocValues and EnumFieldSource get the same change. It is the outcome Lucene already chose for the same corner case in NumericRangeQuery (d820e34a260, 2009) and in Ishan Chattopadhyaya's LUCENE-7111 patch (#8166, 2016).

if (!includeLower) {
  if (lower == Integer.MAX_VALUE) {
    matchesNothing = true; // nothing is greater than Integer.MAX_VALUE
  } else {
    lower++;
  }
}
// ... the same for the upper bound at Integer.MIN_VALUE, then:
if (matchesNothing) {
  lower = 1; // an empty range (lower > upper)
  upper = 0;
}

Trade-offs

  • Setup-time only, no per-document cost; inclusive bounds and bounds inside the range behave as before.
  • The sentinel range 1..0 needs its comment to read clearly; returning a scorer that matches nothing would state the intent more directly but repeats code in three places (not run).
  • Open ends are deliberately left alone: a null bound still becomes MIN_VALUE / MAX_VALUE, inclusive, whatever its include flag (an open end has no value to exclude); only explicit bounds change.

What the checks validated

  • Stock 645d4fe14a20: the repro fails (1 of 1) with assertions on and off; the closest suites pass (62 tests).
  • With the fix: the repro passes with assertions on and off; the closest suites pass (62 tests); the whole :lucene:queries:test task passes (512 tests, 5 skipped, 0 failures); :lucene:queries:check -x test (google-java-format, ecj lint, forbidden APIs, license checks) passes with the license-headed regression test in place, which passes with the fix and fails on stock. Targeted runs, not the full Lucene test suite.
  • The all-sites probe with the three fixed files shadowed gives all 13 rows correct (int, long, enum at MIN and MAX: 0 hits; controls 1; float and double 0).

A match-nothing flag checked in matches()

The tie-breaker's verification sketch for IntDocValues and LongDocValues: the same check, then if (matchNone) return false; before the value test. With it shadowed, the tie-breaker's own int/long/double probe passes all its 13 rows, the minimal test passes, and 54 upstream tests pass.

Trade-offs

Same result, one extra branch per document; EnumFieldSource needs the same change.

Math.addExact / Math.subtractExact, with an empty range on ArithmeticException

On LUCENE-7111 (#8166), Robert Muir suggested Math.addExact / Math.subtractExact for the old DocValuesRangeQuery, so that an overflow throws instead of passing silently. Catching the exception to return an empty range is our own variation; the patch that landed there checked the limit explicitly and returned no documents.

Trade-offs

Same semantics, but uses an exception for control flow; not run.

Drop the endpoint adjustment and compare strictly, as FunctionValues and DoubleDocValues do

Four scorer variants per class.

Trade-offs

A larger change in three classes for the same result; not run.

Throw IllegalArgumentException, as facet LongRange does

"matches nothing".

Trade-offs

Not recommended: turns a valid empty question into an error, unlike the float and double sources, and breaks callers that generate bounds.

Downstream note

Where an application uses such a range as a restriction filter, documents holding exactly MIN_VALUE or MAX_VALUE slip through it; Solr's {!frange ... incl=false} reaches the same getRangeScorer code (SOLR-8867 fixed another over-match there; read, not tested).

How it was found

Not a formal finding. While verifying a related bug in the flexible query parser (an exclusive point bound at the limit wraps around), an AI verifier read this endpoint adjustment, noticed the opposite failure and ran a small test. A follow-up AI tool confirmed it with its own test; later AI tools reproduced it on main and old releases.

The method in full130 words

Method: non-formal (code reading, then executed tests).

  • During the 2026-10-22 study, an AI verifier checking the flexible StandardQueryParser bug where an exclusive int/long point bound at the limit wraps around and matches everything (LFV-050) read IntDocValues.getRangeScorer and saw that its guard avoids the wrap but keeps the bound inclusive. Its sibling test gave 1 hit for n > MAX32, n < MIN32 and m > MAX64.
  • A follow-up of the study executed it with its own test (testFunctionRangeAtLimits, with the control n > MAX32 - 1) and rated it new and low, with a separate root cause.
  • The hunt then cut the repro to one test, re-ran it on each new main, and an independent tie-breaker wrote its own probe and checked the intent evidence.

Prior reports, history and checks

Prior reports

No prior public report found: 105 tool queries (6 of them web searches run by hand) and 89 further manual ones over GitHub (incl. all 410 open PRs' files), the JIRA archive, the mailing lists, downstream projects and the web (2026-10-31). Not fixed upstream. The closest item, LUCENE-7111 (2016), fixed the same corner case in another, since-removed query.

Searches4 searches

Verdict new. LUCENE-7111 (#8166) is the same class of mistake in the sandbox DocValuesRangeQuery (there the +1 overflowed and the query matched everything), fixed there only. SOLR-8867 fixed a different over-match (missing values) in the same getRangeScorer methods. Context only: #7778 (LUCENE-6720, added FunctionRangeQuery), #5321 (LUCENE-4251), #7785 (LUCENE-6727, open), #13383 (facet LongRange excludes the limit correctly), #16820 (open, possible ValueSource deprecation). Gaps: GitHub search reads at most 50 results per query; 3 queries hit the rate limit and were re-run; private reports cannot be searched.

What was searched, when, and what turned up
WhereWhenHowFound
a scripted prior-report search: GitHub (apache/lucene issues, PRs, commits), downstream repos, mailing lists, JIRA, web2026-10-31105 queries (40 GitHub issue/PR, 6 commit, 5 downstream, 40 list on dev@, java-user@, issues@, commits@, 8 LUCENE JIRA, 6 web)669 distinct hits, 0 high; no report
GitHub (manual): apache/lucene, apache/solr, Elasticsearch, OpenSearch, all of GitHub; open PRs2026-10-3130 searches (FunctionRangeQuery, getRangeScorer, frange, IntDocValues, LongDocValues, EnumFieldSource, MAX_VALUE exclusive, ...); the changed files of all 410 open apache/lucene PRsno report; no open PR touches the guards
JIRA archive (SOLR, LUCENE) and mailing lists (manual)2026-10-3122 JIRA queries with descriptions and comments read; 36 list searches on 6 Lucene and Solr listsSOLR-8867 (different over-match), SOLR-5084 (no limit discussion); no report
Earlier searches2026-10-22, 2026-10-31the study follow-up's read-only GitHub searches; an earlier scripted prior-report search (62 hits, 0 high)no report

History

The guard was written for Solr in 2009 (SOLR-939) and entered Lucene's queries module in 2011 (LUCENE-3249, first in 4.0.0-ALPHA); FunctionRangeQuery arrived in 5.4.0 (LUCENE-6720). Executed on 19 releases from 4.0.0-ALPHA to 10.6.0 and on main; not fixed upstream.

Versions tested6 rows

Introduced into Lucene by a993ae90323 LUCENE-3249 (#4322, "Moved FunctionQuery impls to new Queries module", 2011-06-28), which moved Solr's IntFieldSource and LongFieldSource with the guard into the new queries module; first release 4.0.0-ALPHA. The guard itself was written for Solr's frange in 6bd49091023 (SOLR-939, 2009-03-23), not shipped by Lucene before 4.0. FunctionRangeQuery came with LUCENE-6720 (#7778, 13f8ab93a97, 2015-08-06), first released in 5.4.0, and did not touch the guard.

Later moves kept the text: LUCENE-5247 (#6311, 9db7442d648, 2013-10-01) moved the int and long getRangeScorer into IntDocValues / LongDocValues (where git blame stops), and SOLR-5084 copied the int guard into EnumFieldSource (2013-11-05: a9f8bf654c2 on trunk, first released in 6.0.0, and the backport 49ed368ae72, first released in 4.6.0). The int and long guards are in all 124 release tags of the history clone from 4.0.0-alpha to 10.6.0, and FunctionRangeQuery in all 92 from 5.4.0; 8.9.0 to 8.11.x are not tagged in the clone, and of those only 8.11.4 was checked (executed). No commit in the clone changed the guards' logic. Upstream main 2f424fd428da (2026-10-30) still has all six lines.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (gradle and the standalone runner, assertions on and off); all six sites wrong in the probe
10.6.0, 10.5.2, 10.0.0, 9.12.3, 9.1.0 affectedthe minimal test, unchanged, fails (10.5.2 also with assertions on; all six sites wrong on 10.5.2)
5.4.0, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0 5.4.0: first with FunctionRangeQueryFunctionRangeQuery probe: n > MAX and m > MAX give 1 hit, the control n > MAX-1 is correct
4.0.0-ALPHA, 4.0.0, 4.10.4, 5.0.0, 5.3.2 through getRangeScorerno FunctionRangeQuery yet; through the public FunctionValues.getRangeScorer, n > MAX and m > MAX give 1 hit, the control is correct
3.6.2 and earlier not affectedno org.apache.lucene.queries.function package (the guard lived only in Solr)
fix on 645d4fe14a20 not affectedthe repro, the closest suites, the lucene/queries tests and check pass; all 13 probe rows correct

Independent checks

Five AI tools reproduced or confirmed it (the study verifier and follow-up, a candidate reproduction, the current-main reproducer and a tie-breaker with its own probe); a reading-only reviewer from another model family (OpenAI GPT-5.6) gave a procedural "not real" but agreed on the root cause. It still reproduces on main and 10.5.2.

Checks and verdicts222 words

There is no executing cross-model review: the review by the other model family could read the code only; a separate run re-ran the repro on 10.5.2 (fails).

Each independent check and its verdict
CheckVerdictWhat it found
Study verifier, side observation lead, executedSibling test: n > MAX32, n < MIN32, m > MAX64 give 1 hit each.
Study follow-up new, lowOwn test with a control one step inside the limit.
Candidate reproduction reproducedMinimal test on main, 10.5.2 and 9.12.3; all-sites probe; 8.11.4 and 5.4.0 probes; fix sketch.
Cross-model review, refute lens (OpenAI GPT-5.6, reading only) not real (low), proceduralCould not run code; agreed on the root cause and that the inputs are in contract; rated the intended argument weak.
Current-main reproducer realMain 645d4fe14a20 with assertions on and off; 10.5.2.
Tie-breaker real, highOwn probe on main, 10.5.2 and 9.12.3; pristine sources from git; found the SOLR-939 equivalence test and the 2009 NumericRangeQuery fix.
This doc still reproduces; fix validatedGradle on a scratch worktree of 645d4fe14a20.
Fact checks of this doc real; wording fixedAn independent re-run (repro, all-sites probe and fix through gradle, revision recorded) and a reviewer from another model family (OpenAI GPT-5.6, reading only) checked every claim; their wording fixes are 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 notes864 words

Finding record

LFV-083, snapshot of 2026-11-06

Registry title
FunctionRangeQuery over Int/Long/EnumFieldSource turns an exclusive bound at the type limit into an inclusive one
Area
queries / function
Priority
Low, rank 960: FunctionRangeQuery over Int/Long/Enum sources turns an exclusive bound at the type limit into an inclusive one, letting through only a document whose value is exactly MIN or MAX, a module API and int-limit edge case.
Impact class
silent wrong results
Impact note
Users of lucene/queries FunctionRangeQuery over an IntFieldSource, LongFieldSource or EnumFieldSource with an exclusive bound at MIN or MAX; only documents whose value is exactly that limit are wrongly included; ordinary scale; every release from 5.4.0 to 10.6.0 and main (executed; the underlying public getRangeScorer since 4.0.0-ALPHA).
Summary
You run a FunctionRangeQuery over an IntFieldSource, LongFieldSource or EnumFieldSource with an exclusive bound exactly at the type limit, such as n > Integer.MAX_VALUE. You expect no document to match. Instead getRangeScorer adjusts an exclusive bound only when it is not already at the limit (if (!includeLower && lower < Integer.MAX_VALUE) lower++), so at the limit the bound stays inclusive and the document whose value is exactly MAX (or MIN) matches: 1 hit instead of 0.
Introduced
a993ae9032337ca8d5b2c7a4d58bf41a6f5053f5 (LUCENE-3249 'Moved FunctionQuery impls to new Queries module', #4322, svn trunk r1140906, 2011-06-28), which moved Solr's IntFieldSource and LongFieldSource with the saturating guard 'if (!includeLower && lower < Integer.MAX_VALUE) lower++' (and its upper-bound and Long twins) into Lucene's new queries module; the guard was written for Solr's frange by 6bd49091023 (SOLR-939, 2009-03-23), and FunctionRangeQuery came with LUCENE-6720 (#7778; 13f8ab93a97 on trunk, 88d137632d38 on branch_5x, 2015-08-06) without touching it; later moves kept the text: LUCENE-5247 (#6311, 9db7442d648, 2013-10-01, into IntDocValues/LongDocValues, where git blame stops) and SOLR-5084 (a9f8bf654c2 / 49ed368ae72, 2013-11-05, the int guard copied into EnumFieldSource) [read: git log -S, git blame -C -C -M and git show in the history clone; exec: 3.6.2 has no org.apache.lucene.queries.function package, 4.0.0-ALPHA fails] (2011-06-28 (a993ae90323, the guard enters Lucene's queries module; first released in 4.0.0-ALPHA); the guard itself dates from 2009-03-23 (SOLR-939, Solr only) and FunctionRangeQuery from 2015-08-06 (LUCENE-6720, first released in 5.4.0))
First affected version
5.4.0 [exec, -ea and -da] for FunctionRangeQuery, the first release with the class (5.3.2 has no FunctionRangeQuery [exec]); the same wrong range scorer is reachable through the public FunctionValues.getRangeScorer of IntFieldSource and LongFieldSource since 4.0.0-ALPHA [exec, -ea and -da]; 3.6.2 has no org.apache.lucene.queries.function package [exec: compile error]; the EnumFieldSource site from 4.6.0 [read]
Affected versions
FunctionRangeQuery: every release from 5.4.0 to 10.6.0 and main 645d4fe14a20 [exec: the FunctionRangeQuery probe fails on 5.4.0 (also -ea), 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main (also -ea), with -da; the minimal test, unchanged, fails on 9.1.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 and main (10.5.2, 10.6.0 and main also with -ea)]; getRangeScorer level, before FunctionRangeQuery existed: 4.0.0-ALPHA, 4.0.0, 4.10.4, 5.0.0 and 5.3.2 [exec]; [read] the int and long guards are in all 124 release tags of the history clone from 4.0.0-alpha to 10.6.0 (8.9.0-8.11.x are not tagged there; 8.11.4 was executed) and FunctionRangeQuery.java in all 92 tags from 5.4.0; not fixed: upstream main 2f424fd428da (2026-10-30) still has all six guard lines [read]
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: a cross-model review that could only read the code (its low-confidence “not real” is procedural), an adjudication that ruled it real and a prior-report search; not yet triaged by a person.
Found by
non-formal: code reading
Upstream and prior reports
none found (the study follow-up's read-only GitHub searches, 2026-10-22; a prior-report search on 2026-10-31 found none either)
Lucene commit tested
main 645d4fe14a20 (gradle in a scratch worktree and the standalone runner, assertions on and off); the 10.5.2 and 9.12.3 release jars with the minimal test (9.1.0, 10.0.0 and 10.6.0 in the history sweep); FunctionRangeQuery probes on 5.4.0 to 10.6.0 and getRangeScorer probes on 4.0.0-ALPHA to 5.3.2; the recommended fix in a scratch worktree of 645d4fe14a20. Upstream main 2f424fd428da (2026-10-30) has the same six guard lines.

Lucene and upstream sources

Finding record
  • a Lucene clone tag releases/lucene/10.5.2: lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:95-104
  • SECURITY.md:38, 50 (threat model)
The finding in 30 seconds
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:31-43, (at 645d4fe14a20) 55-80, 102-112, 172
  • lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:81-117 (at 645d4fe14a20) (90, 96, 103, 114)
  • lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/LongDocValues.java:99, (at 645d4fe14a20) 105, 112, 123
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/EnumFieldSource.java:143, (at 645d4fe14a20) 148, 154, 165
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionValues.java:224-292; (at 645d4fe14a20) docvalues/DoubleDocValues.java:86-147
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/ (at 645d4fe14a20) (MultiValuedIntFieldSource.java:37 and MultiValuedLongFieldSource.java:37 extend IntFieldSource/LongFieldSource; DocFreqValueSource, JoinDocFreqValueSource, MaxDocValueSource, NumDocsValueSource, TermFreqValueSource, TotalTermFreqValueSource, SumTotalTermFreqValueSource return IntDocValues/LongDocValues; grep, read); lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:208 (MAX_DOCS)
Who is affected
  • lucene/queries/src/java/org/apache/lucene/queries/function/FunctionRangeQuery.java:42 (at 645d4fe14a20) (@lucene.experimental)
Why it looks unintended
  • lucene/queries/src/java/org/apache/lucene/queries/function/docvalues/IntDocValues.java:90; (at 645d4fe14a20) docvalues/LongDocValues.java:99; valuesource/EnumFieldSource.java:143; FunctionValues.java:224-292; docvalues/DoubleDocValues.java:86-147; FunctionRangeQuery.java:38
  • lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java:58-76; (at 645d4fe14a20) Range.java:37-40
  • lucene/queries/src/test/org/apache/lucene/queries/function/TestFunctionRangeQuery.java:75-140; (at 645d4fe14a20) TestValueSources.java:497-500
  • lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/EnumFieldSource.java:70-88 (at 645d4fe14a20) (label resolution, default value); docvalues/IntDocValues.java:95, 102 (Integer.parseInt); docvalues/LongDocValues.java:87 (Long.parseLong)
Suggested fix
  • lucene/facet/src/java/org/apache/lucene/facet/range/LongRange.java:58-76 (at 645d4fe14a20)

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

Repro files

About the diagrams

Made separately from this text, from the code at 645d4fe14a20 and runs executed for the diagrams; the PNG is the summary still.

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.