All findings · Lucene finding · LFV-1203 · latlonpoint-contains-geometry-query-skips-argument-checks

LatLonPoint.newGeometryQuery with CONTAINS matches nothing instead of throwing for an empty or null-holding geometry array

LatLonPoint.newGeometryQuery(field, CONTAINS, ...) with no geometries, or with a null one, returns a query that silently matches nothing instead of the IllegalArgumentException its javadoc promises and the other relations throw. The defect needs invalid input; every release since 8.8.0 has it.

Summary

Priority
Low, rank 418A CONTAINS geometry query on a LatLonPoint field built from an empty array, or one holding a null, silently matches nothing instead of throwing the documented IllegalArgumentException, so the caller's mistake goes unnoticed, in every release since 8.8.0. Valid geometries get correct hits, and only callers that pass a bad array with this niche relation reach it.
Impact
minor
Since
8.8.0 (2021, LUCENE-9641)introduced 2021-01-08 · about 5 years
Affected
every release from 8.8.0 to 10.6.0 and main; not fixed upstream
Status
documented
Found by
semi-formal: a proof-obligation audit of the factories' documented @throws contract, witnessed by an executable repro

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

One relation, CONTAINS, takes a shortcut that runs before the argument checks every other relation goes through.

Background: geometry queries on a point field

  1. LatLonPoint.newGeometryQuery(field, relation, geometries...) builds a query over indexed points. For INTERSECTS, WITHIN and DISJOINT it builds one LatLonPointQuery (apart from two INTERSECTS shortcuts for a single rectangle or circle), whose constructor checks the arguments ("geometries must not be empty", "geometries[i] must not be null").
  2. CONTAINS asks for documents whose points contain every given geometry. Points can only contain points, so the factory adds one required (MUST) clause per Point, and returns MatchNoDocsQuery for a line or polygon: a choice made on purpose when CONTAINS was added in 8.8.0.
Diagram of the bug: LatLonPoint.newGeometryQuery called with an empty geometry array. With INTERSECTS or WITHIN the call reaches the query constructor, whose argument check throws IllegalArgumentException: geometries must not be empty. With CONTAINS the call takes the CONTAINS shortcut before any check; its loop over the geometries never runs and it returns ConstantScore() over a BooleanQuery with 0 clauses, which finds 0 hits on an index whose point (0,0) CONTAINS [Point(0,0)] finds. A null element under CONTAINS is not a Point and gets MatchNoDocsQuery.
INTERSECTS and WITHIN reject an empty geometry array; CONTAINS takes its shortcut first and returns a query that matches nothing. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You build a CONTAINS query on a LatLonPoint field from a list of points that turned out empty: LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS) with 0 geometries.
  2. You expect IllegalArgumentException: the javadoc promises it when the geometries are "null, empty or contain a null", and the same call with INTERSECTS or WITHIN throws "geometries must not be empty".
  3. Instead you get ConstantScore(): a ConstantScoreQuery over a BooleanQuery with 0 clauses. On an index with one point at (0,0) it finds 0 hits, where CONTAINS [Point(0,0)] finds 1. No error.
  4. A null slot does the same: CONTAINS [Point(0,0), null] returns MatchNoDocsQuery("... with non-point geometries"), 0 hits, in LatLonPoint and in LatLonDocValuesField.newSlowGeometryQuery; INTERSECTS throws "geometries[1] must not be null".
  5. Why: CONTAINS takes a shortcut before any argument check. Its loop never runs for 0 geometries, and null instanceof Point is false, so the null lands in the branch meant for lines and polygons.

Who is affected

Applications that build a CONTAINS query on a point field from a caller-built array that can be empty (no locations picked) or hold a null slot, relying on the documented check. They get zero hits and no error. No setting is involved: any index, assertions on or off. Valid input is not affected by this defect.

Mechanism, terms and numbers442 words

LatLonPoint.newGeometryQuery(field, queryRelation, latLonGeometries...) and LatLonDocValuesField.newSlowGeometryQuery document @throws IllegalArgumentException when the geometry array "is null, empty or contain a null". For INTERSECTS, WITHIN and DISJOINT the query constructors enforce it. For CONTAINS both factories take a shortcut first that checks only the geometry type (instanceof Point), not null or empty input: an empty array gives ConstantScore() over zero clauses, and a null element gives MatchNoDocsQuery("... with non-point geometries"). Both match no document, so a caller's mistake (an empty list of points, a null slot) becomes an empty result with no error. The doc-values factory still rejects the empty array; the null element is silent in both. Valid input is not affected by this defect. Introduced in 8.8.0 by LUCENE-9641; present in every release through 10.6.0 and on main.

The mechanism (main 5adff78bb168, under lucene/core/src/java/org/apache/lucene/)

  1. The contract. document/LatLonPoint.java:314-326: "@param latLonGeometries array of LatLonGeometries. must not be null or empty." and "@throws IllegalArgumentException if field is null, queryRelation is null, latLonGeometries is null, empty or contain a null" (:323-324). document/LatLonDocValuesField.java:243-246 has the same clause and adds "or line geometry"; that line part is not claimed here, because under CONTAINS a line gets the MatchNoDocsQuery the introducing PR's review settled for non-point geometries (:255-261).
  2. The checks live in the query constructors. new LatLonPointQuery(...) (LatLonPoint.java:340) goes through SpatialQuery (field and relation, document/SpatialQuery.java:74-85) and LatLonGeometry.create (geo/LatLonGeometry.java:24-45). new LatLonDocValuesQuery(...) (LatLonDocValuesField.java:263) does its own field and relation checks (document/LatLonDocValuesQuery.java:57-62) and calls LatLonGeometry.create (:87). LatLonGeometry.create rejects an empty array and a null element (:28-40); its null-array check (:25-26) is never reached from LatLonPoint, where a null array fails earlier with NullPointerException (LatLonPoint.java:329, :345 or SpatialQuery.java:83).
  3. CONTAINS returns before them. LatLonPoint.java:337-338 sends CONTAINS to makeContainsGeometryQuery (:343-355), which adds one MUST clause per Point and returns MatchNoDocsQuery for anything else (:346-348).
    • Empty array: the loop body never runs, and :354 returns new ConstantScoreQuery(builder.build()) over zero clauses, which matches nothing (it prints as ConstantScore(); at search time the empty BooleanQuery rewrites to MatchNoDocsQuery("empty BooleanQuery"), search/BooleanQuery.java:268-271).
    • Null element: (null instanceof Point) == false is true at :346, so the null takes the non-point branch.
  4. The doc-values twin. LatLonDocValuesField.java:255-261 has the same CONTAINS loop and the same null-element outcome; its empty array falls through to LatLonDocValuesQuery and is rejected there.
  5. Not claimed. MatchNoDocsQuery for a valid line or polygon under CONTAINS is intended. The null-array NullPointerException fails loudly (only the exception type differs), and a null field with an empty array needs a second invalid argument.
Exposure and likelihood128 words
  • Who. Code that passes an empty or null-holding array to LatLonPoint.newGeometryQuery (or LatLonDocValuesField.newSlowGeometryQuery, null element) with CONTAINS, for example a list of user-picked points that can be empty. Nothing tells the application its input was invalid.
  • Default vs opt-in. Query construction only: no configuration, any index and codec, assertions on and off. CONTAINS on a point field is a niche relation, and no public report was found since 8.8.0 (2021), so the likelihood is rare.
  • Needs invalid input. This defect changes nothing for valid geometries (valid multi-point CONTAINS on newSlowGeometryQuery has a separate defect, LFV-701). The documented IndexOrDocValuesQuery pairing still throws for the empty array, because the doc-values half rejects it; the null element is silent in both halves.
  • Scale. One call; no index needed; deterministic.

Minimal repro

One JUnit test method on the public API: expectThrows(IllegalArgumentException.class, () -> LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS)). No index, no random input, one thread. It fails with assertions on and off and passes with the fix, so it is also the regression test.

TestLatLonPointContainsNoGeometries.java (lucene/core, package org.apache.lucene.document, 1 test)30 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.document;

import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestLatLonPointContainsNoGeometries extends LuceneTestCase {

  public void testContainsWithNoGeometriesThrows() {
    // expected: the documented IllegalArgumentException; actual: a query that matches nothing
    expectThrows(
        IllegalArgumentException.class,
        () -> LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS));
  }
}
Command
cp $REPRO/TestLatLonPointContainsNoGeometries.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestLatLonPointContainsNoGeometries -Ptests.seed=DEADBEEF
# add -Ptests.asserts=false for production mode: the same failure
Expected
newGeometryQuery("f", CONTAINS) with no geometries throws IllegalArgumentException.
Observed
It returns ConstantScore(), which matches nothing: "Expected exception IllegalArgumentException but no exception was thrown", assertions on and off.

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

More files, environment, full output268 words

TestLatLonPointContainsNoGeometries (one method, extends LuceneTestCase, package org.apache.lucene.document of the lucene/core tests, Apache license header). It is the hunt's minimal test with its comment shortened to pass checkGoogleJavaFormat. It covers the empty array, the simplest of the two silent cases; the null element follows from the same shortcut and is shown by the diagnostic below. Run with seed DEADBEEF; the hunt's equivalent tests also failed with seeds 4C0A0001, 4C0A0002, 12345678 and CAFEBABE.

Environment

lucenemain 5adff78bb168 (12.0.0-SNAPSHOT): fails; 8.8.0-8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0: fail; 8.7.0: no API
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux aarch64 (only this platform was run)
Command (environment)
cp $REPRO/TestLatLonPointContainsNoGeometries.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestLatLonPointContainsNoGeometries -Ptests.seed=DEADBEEF

Observed output

# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF (assertions on; -Ptests.asserts=false gives the same)
TestLatLonPointContainsNoGeometries > testContainsWithNoGeometriesThrows FAILED
    junit.framework.AssertionFailedError: Expected exception IllegalArgumentException but no exception was thrown
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:D113C0FEC8040294]:0)
        at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1432)
        at app//org.apache.lucene.tests.util.LuceneTestCaseParent.expectThrows(LuceneTestCaseParent.java:1418)
        at app//org.apache.lucene.document.TestLatLonPointContainsNoGeometries.testContainsWithNoGeometriesThrows(TestLatLonPointContainsNoGeometries.java:26)
1 test completed, 1 failed

# with the validate-first fix: 1 test, 0 failures (assertions on and off)

# what the factories return (public-API diagnostic on main, assertions off, one document at (0,0))
P.newGeometryQuery(f, CONTAINS, []) -> ConstantScoreQuery [ConstantScore()] hits=0
P.newGeometryQuery(f, INTERSECTS, []) -> IllegalArgumentException: geometries must not be empty
P.newGeometryQuery(f, WITHIN, []) -> IllegalArgumentException: geometries must not be empty
DV.newSlowGeometryQuery(f, CONTAINS, []) -> IllegalArgumentException: geometries must not be empty
P.newGeometryQuery(f, CONTAINS, [Point(0,0), null]) -> MatchNoDocsQuery [MatchNoDocsQuery("Contains LatLonPoint.newGeometryQuery with non-point geometries")] hits=0
P.newGeometryQuery(f, INTERSECTS, [Point(0,0), null]) -> IllegalArgumentException: geometries[1] must not be null
DV.newSlowGeometryQuery(f, CONTAINS, [null]) -> MatchNoDocsQuery [MatchNoDocsQuery("Contains LatLonDocValuesField.newSlowGeometryQuery with non-point geometries")] hits=0
P.newGeometryQuery(f, CONTAINS, [Point(0,0)]) -> ConstantScoreQuery [ConstantScore(+LatLonPointQuery: field=f:[Point(0.0,0.0),])] hits=1

Why it looks unintended

  • Both javadocs promise IllegalArgumentException for an array that is "null, empty or contain a null"; the commit that added CONTAINS rewrote and kept that clause.
  • For the same empty array, INTERSECTS, WITHIN and the doc-values twin's CONTAINS throw "geometries must not be empty".
  • The intended MatchNoDocsQuery is for valid non-point geometries; a null reaches it only by failing instanceof Point.
The full argument127 words
  • The contract is explicit and current. The @throws clause names these inputs for every relation, in both factories. LUCENE-9641 (14009f4424d), which added CONTAINS and the shortcut, rewrote the clause in the same diff and kept "null, empty or contain a null" (git blame).
  • The rest of the API enforces it. INTERSECTS, WITHIN, newPolygonQuery and newSlowGeometryQuery(CONTAINS) throw "geometries must not be empty" for the empty array; INTERSECTS throws "geometries[i] must not be null" for a null element (executed). Before LUCENE-9641, on unreleased master, the constructors of the removed LatLonPointInGeometryQuery and LatLonDocValuesPointInGeometryQuery rejected all of these inputs.
  • Tests. No upstream test passes an empty or null-holding array with any relation; BaseGeoPointTestCase.testPolygonNullField (lucene/test-framework/.../tests/geo/BaseGeoPointTestCase.java:472-483) treats these @throws clauses as tested contract for the field argument.
The devil's-advocate check200 words

The case for intended (the devil's-advocate verifier and the cross-model refute lens): (a) @param says the array "must not be null or empty", so the caller misuses the API; (b) in the review of lucene-solr#2155 a reviewer (nknize) suggested throwing for non-point geometries under CONTAINS, and the author (iverase) answered "Still I return a MatchNoDocs query for the Query builders"; (c) LatLonShape.newGeometryQuery(f, CONTAINS) with an empty array also returns ConstantScore(); (d) an empty set matching nothing is common in Lucene.

Why it fails. (a) The javadoc does not stop at the precondition: it promises the exception for exactly these inputs. (b) The review covered valid non-point geometries, a relation points can never satisfy, not invalid arrays. (c) LatLonShape's empty case appears to be a historical consequence of LUCENE-9225 (286d22717b2, 8.5.0), which dropped a CONTAINS && length > 1 guard that XYShape.newGeometryQuery still has (XYShape throws), and for a null element LatLonShape throws "geometries[0] must not be null" while LatLonPoint returns MatchNoDocsQuery (executed). (d) Here the API documents the exception and enforces it on every other path. The cross-model reviewer rated the misuse defence weak against the explicit @throws; it does hold for impact, hence Low.

Suggested fix

Recommended Validate every argument before any shortcut, in both factories (fix A)

Check every argument first: a small package-private helper, called as the first line of both LatLonPoint.newGeometryQuery and LatLonDocValuesField.newSlowGeometryQuery, throws IllegalArgumentException for a null field or relation, a null or empty array and a null element, before any shortcut. The repro passes, and the 236 TestLatLon* tests run with 0 failures (19 skipped).

All options and trade-offs3 options ยท 501 words

Fix A was built and checked through gradle on 5adff78bb168 in a scratch worktree: the repro with assertions on and off, then the repro with every org.apache.lucene.document.TestLatLon* and org.apache.lucene.search.TestLatLon* suite, seed DEADBEEF, and checkGoogleJavaFormat. Option B was not run. The regression test is the one repro test, unchanged; it covers only the empty LatLonPoint case, while the null element and the doc-values factory, which fix A also changes, are shown by the diagnostic in the repro section, not by a test.

Validate every argument before any shortcut, in both factories (fix A) (validated by the checks) (recommended)

Add LatLonPoint.checkGeometryQueryArguments(field, queryRelation, latLonGeometries) (package-private) and call it as the first statement of newGeometryQuery (LatLonPoint.java:327-328) and of newSlowGeometryQuery (LatLonDocValuesField.java:248-249); +27/-0, messages as in LatLonGeometry.create.

  static void checkGeometryQueryArguments(
      String field, ShapeField.QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
    if (field == null) {
      throw new IllegalArgumentException("field must not be null");
    }
    if (queryRelation == null) {
      throw new IllegalArgumentException("queryRelation must not be null");
    }
    if (latLonGeometries == null) {
      throw new IllegalArgumentException("geometries must not be null");
    }
    if (latLonGeometries.length == 0) {
      throw new IllegalArgumentException("geometries must not be empty");
    }
    for (int i = 0; i < latLonGeometries.length; i++) {
      if (latLonGeometries[i] == null) {
        throw new IllegalArgumentException("geometries[" + i + "] must not be null");
      }
    }
  }

Trade-offs

Implements the null, empty and null-element part of both @throws clauses in one place, before every shortcut; the doc-values clause's "or line geometry" is left as it is. Side effects, all towards the documented behaviour: a null array gives IllegalArgumentException instead of NullPointerException (as newPolygonQuery did in 8.7.0), and so does a null field with an empty CONTAINS array. Callers that pass an empty CONTAINS array today get an exception instead of zero hits: a behaviour change, so consider a CHANGES entry. Cost: one linear null scan per query construction, run first on every path (the single-geometry INTERSECTS shortcuts included); the paths that reach LatLonGeometry.create scan the array again there.

What the checks validated

The repro passes with assertions on and off; the repro plus every document.TestLatLon* and search.TestLatLon* suite (22 classes): 236 tests, 0 failures, 19 skipped; :lucene:core:checkGoogleJavaFormat passes with the fix and the test in place. The hunt validated the LatLonPoint half alone the same way (repro passes; 41 TestLatLonPointQueries tests pass).

Check only inside the CONTAINS shortcuts (option B) (not run)

Throw for length == 0 before the loop in makeContainsGeometryQuery, and for geometry == null inside both CONTAINS loops (LatLonPoint.java:345-346, LatLonDocValuesField.java:256-257).

Trade-offs

Smallest change, leaves the other paths untouched. Keeps the undocumented NullPointerException for a null array and a second copy of the checks.

What the checks validated

Not run.

Document match-nothing for CONTAINS instead (not recommended) (not run)

Change the javadoc so that an empty or null-holding array under CONTAINS is documented to match nothing.

Trade-offs

Keeps the empty array inconsistent with the other relations and with the doc-values twin, and keeps hiding the caller's mistake.

What the checks validated

Not run.

Downstream note

Downstream, a product that forwards a user's list of locations into a CONTAINS query gets zero hits for an empty list (and, under MUST_NOT, excludes nothing) instead of an error it could report.

How it was found

A proof-obligation audit of the documented @throws contract of LatLonPoint.newGeometryQuery and LatLonDocValuesField.newSlowGeometryQuery: every path that returns a query must first reject a null, empty or null-holding array. The CONTAINS shortcut does not; a one-test public-API JUnit witnessed it.

The method in full107 words

Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).

  • The audit checked the paths of the two factories (LatLonPoint.java:314-355, LatLonDocValuesField.java:232-264) against that @throws clause. The INTERSECTS, WITHIN and DISJOINT paths reach the checks in the query constructors and LatLonGeometry.create; the CONTAINS shortcut returns before them.
  • The finder then wrote a one-test public-API JUnit that fails on main with assertions on and off and on 10.6.0 and 9.12.3, and a probe for the null element in both factories.
  • An independent reproducer, an executed cross-model review, a devil's-advocate verifier and an adjudication reproduced it ("Prior reports, history and checks").

Prior reports, history and checks

Prior reports

No prior report found in apache/lucene issues, PRs and commits (with the JIRA archive and the archived lucene-solr PRs), the dev@, java-user@, issues@ and commits@ lists, downstream trackers or the web (searched 2026-11-03). Nearest: lucene-solr#2155, the introducing PR, whose review covered only valid non-point geometries.

Searches3 searches

No matching public report was found in the documented searches. Related, not reports: lucene-solr#2155 (LUCENE-9641, the introducing PR) and its issue #10680; lucene-solr#1940 / #10591 (LUCENE-9552, added newGeometryQuery); the open, stale #12162 (a LatLonField that delegates to the same factory and would inherit the gap); #16847 and #709 (other code in the same files). Limits: keyword search; GitHub search does not index PR diffs or every review comment; private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive; archived apache/lucene-solr PRs)2026-11-03, re-checked the same daytwo scripted prior-report searches plus the finder's own run; 26 manual and 12 GraphQL searches (newGeometryQuery, makeContainsGeometryQuery, newSlowGeometryQuery, "geometries must not be empty", "non-point geometries", LUCENE-9641, LUCENE-9552); git log -S on the method and the messageno report; lucene-solr#2155, #10680, #10591 and #12162 are the nearest items
Open PRs2026-11-03the changed files of all 409 open apache/lucene PRs; every issue and PR created since 2026-11-01#16847, #12162 and #709 touch the files, not these code paths; nothing new is geo-related
Downstream code, mailing lists, web2026-11-03Elasticsearch, OpenSearch and Solr trackers; dev@, java-user@, issues@ and commits@; 13 web queriesno report

History

Introduced in 8.8.0 by LUCENE-9641 (14009f4424d, 2021-01-08), which added the relation parameter and the CONTAINS shortcut before the checks, and removed the query classes that had rejected these inputs. Every release run from 8.8.0 to 10.6.0 fails; 8.7.0 has no such API. Not fixed upstream.

Versions tested4 rows

Introduced by 14009f4424d "LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155)" (Ignacio Vera, 2021-01-08; apache/lucene-solr#2155, issue #10680; branch_8x copy f73f6b11f0e, lucene-solr#2188). It added the queryRelation parameter and the CONTAINS shortcuts in front of the validating constructors, removed LatLonPointInGeometryQuery and LatLonDocValuesPointInGeometryQuery, whose constructors had rejected these inputs, and kept "null, empty or contain a null" in the rewritten @throws (git blame, git log -S makeContainsGeometryQuery). First release 8.8.0, which is also the first release with newGeometryQuery at all.

All 34 main-line release tags 9.0.0-10.6.0 have the same unchecked bodies (read). The null-element case comes from the same commit and was executed on main only. Not new in 10.6. Upstream main 1c2072f0284 (2026-11-02) still dispatches CONTAINS before any check.

Versions and branches executed, with results
Version or commitVerdictResult
main 5adff78bb168 affectedgradle and a standalone JUnit run: fails with assertions on and off; passes with the validate-first fix
8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0-8.11.4 affectedthe hunt's one-test JUnit: "expected IllegalArgumentException, got ConstantScoreQuery ConstantScore()"
9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 affectedthe same failure (10.6.0 with assertions on and off; 10.5.2 from the independent reproducer's test)
8.7.0 not affectedthe API does not exist (compile error)

Independent checks

A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, refute lens), a devil's-advocate verifier with an intended-lens cross-model review, an adjudication, a separate re-run and an audit re-run reproduced it and agree on the root cause; a final repro check confirmed it at Low. This doc's gradle runs reproduced it and validated fix A.

Checks and verdicts266 words

The cross-model refute review ran on main ea57360912ed, whose six relevant files are byte-identical to 5adff78bb168; its test was re-run on 5adff78bb168 with the same result. All other main runs used 5adff78bb168; the release runs used each release's own jars.

Each independent check and its verdict
CheckVerdictWhat it found
finder reproducedOwn one-test JUnit fails on main (assertions on and off), 10.6.0 and 9.12.3; passes with the LatLonPoint fix; a probe shows the null element in both factories.
independent reproducer real, LowOwn expectThrows test fails on main, 10.6.0, 10.5.2 and 9.12.3; the diagnostic records the returned queries and hits; its own fix passes with 41 + 12 point-query tests.
cross-model review, refute lens (OpenAI GPT-5.6, executed) real (high)Own 3-method test fails 1 of 3, the empty CONTAINS (assertions on and off); that method alone also fails with two more seeds; its controls pass; the fix passes; the misuse defence rated weak.
devil's-advocate verifier real, Low (medium risk that maintainers see it as noise)Built the intended case (precondition, the LUCENE-9641 review, LatLonShape parity) and showed why it fails; an intended-lens cross-model review agreed and advised dropping the null-array and null-field sub-cases.
adjudication confirmed (high), LowRe-ran the repro on main with assertions off: fails; limited the claim to the empty array and the null element.
separate re-run, audit re-run and final repro check reproduced; confirmed, LowFresh seeds on main (assertions on and off) and 10.6.0 fail; passes with the LatLonPoint fix shadowed in; the final check confirmed the minimal repro at Low.

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 notes1,348 words

Finding record

LFV-1203, snapshot of 2026-11-06

Registry title
LatLonPoint.newGeometryQuery with QueryRelation.CONTAINS returns a query that matches nothing instead of the documented IllegalArgumentException when the geometry array is empty or holds a null (LatLonDocValuesField.newSlowGeometryQuery does the same for a null element), because the CONTAINS shortcut runs before any argument check (since 8.8.0, LUCENE-9641)
Area
core / document: LatLon point geometry query factories (LatLonPoint.newGeometryQuery CONTAINS shortcut, LatLonDocValuesField.newSlowGeometryQuery)
Priority
Low, rank 418: A CONTAINS geometry query on a LatLonPoint field built from an empty array, or one holding a null, silently matches nothing instead of throwing the documented IllegalArgumentException, so the caller's mistake goes unnoticed, in every release since 8.8.0. Valid geometries get correct hits, and only callers that pass a bad array with this niche relation reach it.
Impact class
minor
Impact note
Applications that build a CONTAINS geometry query on a LatLonPoint field from a caller-built array that turns out empty, or that holds a null slot, get no IllegalArgumentException but a query that silently matches no document, so the caller's mistake goes unnoticed; query construction only, so any index configuration, -ea and -da, every release from 8.8.0 to 10.6.0 and main; valid geometry arrays get correct hits, the doc-values twin newSlowGeometryQuery still rejects the empty array (but not a null element), and nothing is persisted.
Summary
You call LatLonPoint.newGeometryQuery("f", QueryRelation.CONTAINS) with no geometries (an empty array), or with an array that holds a null, and expect the IllegalArgumentException its javadoc promises ('if latLonGeometries is null, empty or contain a null'), which INTERSECTS and WITHIN and the doc-values twin LatLonDocValuesField.newSlowGeometryQuery("f", CONTAINS) throw for the empty array ('geometries must not be empty'). Instead you get a query that matches no document: a ConstantScoreQuery over an empty BooleanQuery for the empty array, and MatchNoDocsQuery('Contains LatLonPoint.newGeometryQuery with non-point geometries') for a null element, which newSlowGeometryQuery also returns for a null element. The CONTAINS shortcut runs before any argument check: its loop never runs for an empty array, and a null element fails 'instanceof Point' and lands in the branch meant for valid non-point geometries.
Introduced
14009f4424d847ce84fa23a71d8066dfe6c70da0 'LUCENE-9641: Support for spatial relationships in LatLonPoint (#2155)' by Ignacio Vera (GitHub iverase), apache/lucene-solr#2155, issue LUCENE-9641 = #10680; branch_8x copy f73f6b11f0ed4bcbfce445152d813adb3327d249 (lucene-solr#2188), same day [read: git blame -w -M -C -C on the CONTAINS dispatch, makeContainsGeometryQuery, the @throws text and the doc-values CONTAINS loop; git log -S 'makeContainsGeometryQuery' returns only this commit; exec: 8.7.0 lacks the API, 8.8.0 fails]. It added the queryRelation parameter and dispatched CONTAINS to new shortcuts before any validation, removed LatLonPointInGeometryQuery and LatLonDocValuesPointInGeometryQuery (whose constructors had rejected a null or empty array and null elements with IllegalArgumentException), and rewrote the @throws clause, keeping 'latLonGeometries is null, empty or contain a null' (2021-01-08 (author and commit date 2021-01-08T08:16:58+01:00; branch_8x copy the same day); first release 8.8.0 (tag 2021-01-29))
First affected version
8.8.0 [exec] (8.7.0 has no relation-taking newGeometryQuery, so the repro does not compile there [exec]; LUCENE-9552, which added newGeometryQuery itself, also first shipped in 8.8.0, so no release has the relation-taking factories without the defect)
Affected versions
Every release from 8.8.0 through 10.6.0 (4ae946e5a9b) and main. Executed and reproduced (the empty-array repro): 8.8.0, 8.8.1, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0, 8.11.1, 8.11.2, 8.11.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 (-ea and -da) and main 5adff78bb168 (-ea and -da) [exec]; all 34 main-line release tags 9.0.0-10.6.0 and 8.8.0/8.8.1 have the same validation-free newGeometryQuery and makeContainsGeometryQuery bodies, and newSlowGeometryQuery keeps the unchecked CONTAINS loop in every tag from 8.8.0 [read]. The null-element sub-case comes from the same commit [read] and was executed on main only (both factories). Not affected: 8.7.0 and older (no such API; 8.7.0 executed: compile error). Not fixed upstream: upstream main 1c2072f0284 (author date 2026-11-02) still dispatches CONTAINS to makeContainsGeometryQuery before any check [read, 2026-11-03]. Not new in 10.6.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: proof-obligation audit
Upstream and prior reports
none found (2026-11-03: two scripted prior-report searches over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web (122 and 174 hits, the one high-rated hit #12162 by term overlap only), the finder's own 51-query run (287 hits, 0 high), 26 manual GitHub searches incl. Elasticsearch, OpenSearch and Solr and items created or updated since 2026-10-30, 12 GitHub GraphQL searches incl. the archived apache/lucene-solr PRs, the changed files of all 409 open apache/lucene PRs (#16847, #12162 and #709 touch the files, not the factory code paths), every issue and PR created since 2026-11-01 (#16959-#16966, none geo), commits by author date on main, branch_10x and branch_10_6 (newest on the files 1518164afa3, author date 2026-04-01, #15874, instanceof patterns, unrelated), git log -S on makeContainsGeometryQuery and the MatchNoDocsQuery message, and 13 web queries; the independent reproduction's and the intended-behaviour check's searches agree). Re-checked 2026-11-03: newGeometryQuery still matches only #12162; makeContainsGeometryQuery, newSlowGeometryQuery, 'geometries must not be empty' and 'non-point geometries' match nothing; issues and PRs created since 2026-11-02 are #16962-#16966, none geo (#16962, a MatchNoDocsQuery constructor cleanup, changes no file under document/ or geo/); upstream main 1c2072f0284 still dispatches CONTAINS at LatLonPoint.java:337-338. Nearest items, none of them this bug: lucene-solr#2155 (LUCENE-9641, the introducing PR; the review settled MatchNoDocsQuery for valid non-point CONTAINS geometries, nobody raised empty or null input), #10680 (its issue), lucene-solr#1940 / #10591 (LUCENE-9552, added newGeometryQuery and its @throws), open and stale #12162 (LatLonField: keeps the @throws and delegates to the same factory, so it would inherit the gap; no report of it), #16847 and #709 (other code paths in the same files).
Lucene commit tested
main 5adff78bb168 (2026-11-02) through gradle on 2026-11-03 in a scratch worktree: the one-test repro with assertions on and off without a fix (fails) and with the validate-first fix (passes), plus every org.apache.lucene.document.TestLatLon* and org.apache.lucene.search.TestLatLon* suite with the fix (236 tests, 0 failures, 19 skipped) and checkGoogleJavaFormat. The hunt's equivalent one-test JUnit failed on 8.8.0 through 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and 10.6.0 (release jars) and on main with assertions on and off; 8.7.0 has no such API. A public-API diagnostic on main recorded the returned queries and hit counts. Only Linux aarch64 with OpenJDK 25.0.3 was run.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:302-312 (at 5adff78bb168) (newPolygonQuery delegates to newGeometryQuery), :314-326 (javadoc: '@param latLonGeometries ... must not be null or empty', '@throws IllegalArgumentException if field is null, queryRelation is null, latLonGeometries is null, empty or contain a null' at :323-324), :327-341 (newGeometryQuery: INTERSECTS single-geometry shortcut :329-336, CONTAINS dispatch to makeContainsGeometryQuery :337-338 before any check, LatLonPointQuery :340), :343-355 (makeContainsGeometryQuery: non-Point -> MatchNoDocsQuery :346-348, which a null element also takes; an empty array skips the loop and :354 returns ConstantScoreQuery over a zero-clause BooleanQuery)
  • lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-264 (at 5adff78bb168) (newSlowGeometryQuery: the same @throws at :245-246; CONTAINS loop :255-261 returns MatchNoDocsQuery for a non-Point or null element; the empty array reaches LatLonDocValuesQuery and is rejected)
  • lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45 (at 5adff78bb168) (create: 'geometries must not be null', 'geometries must not be empty', 'geometries[i] must not be null'), lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java:74-85 (field and queryRelation checks; geometries.clone() at :83), reached only on the non-shortcut paths
  • two Lucene main checkouts: sha1 of LatLonPoint.java (d4a52ea99997), LatLonDocValuesField.java (4fd2180c0080), SpatialQuery.java (cabef6daeb93), LatLonPointQuery.java (b54a847ca846), LatLonDocValuesQuery.java (ad0c238b14c0) and LatLonGeometry.java (bcf8e96637eb) identical at ea57360912ed and 5adff78bb168 (checked 2026-11-03)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:314-355 (at 5adff78bb168)
  • lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-264 (at 5adff78bb168)
  • lucene/core/src/java/org/apache/lucene/document/SpatialQuery.java:74-85; (at 5adff78bb168) lucene/core/src/java/org/apache/lucene/document/LatLonPointQuery.java:43-50; lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesQuery.java:48, :55-87; lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45; lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java:268-271
Who is affected
  • lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:232-238 (at 5adff78bb168) (IndexOrDocValuesQuery pairing)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:323-324; (at 5adff78bb168) LatLonDocValuesField.java:245-246
  • lucene/test-framework/src/java/org/apache/lucene/tests/geo/BaseGeoPointTestCase.java:472-483 (at 5adff78bb168)
  • lucene/core/src/java/org/apache/lucene/document/LatLonShape.java:325-342, (at 5adff78bb168) :354-370; XYShape.java:263
Diagrams
  • lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:337-355; (at 5adff78bb168) lucene/core/src/java/org/apache/lucene/geo/LatLonGeometry.java:24-45

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

Repro files

About the diagrams

The diagram follows the repro and the public-API diagnostic on main 5adff78bb168 (LatLonPoint.java:337-355, LatLonGeometry.java:24-45).

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.