newDistanceSort passes x and y to a sort field whose constructor checks only the field name. Every computed distance then contains the bad coordinate: NaN for a NaN origin, Infinity for an infinite one, while documents without a location keep their fixed distance, Infinity.
XYDocValuesFieldstores a point on a flat x/y plane per document.XYDocValuesField.newDistanceSort(field, x, y)returns aSortFieldthat orders hits by straight-line distance from the origin (x, y), nearest first.- A document without the field gets distance Infinity, so it sorts after every document that has a location ("missing values sort last").
- For XY, only finite floats are valid coordinates: a NaN or Infinity in an indexed point, an
XYPoint, anXYCircleor a distance query throwsIllegalArgumentException.
- Index three documents: A at (1, 1), B with no location, C at (2, 2). Sorting with
XYDocValuesField.newDistanceSort("p", 0, 0)gives A (1.41), C (2.83), then B (Infinity): missing values sort last, as documented. - Now the origin is
(NaN, 0), for example a centroid computed as 0/0 or a request value "NaN" read withFloat.parseFloat. You expectIllegalArgumentException: the javadoc promises it for invalid coordinates, andnew XYDocValuesField("p", NaN, 0),XYPointField.newDistanceQueryand the lat/lon twin all throw it. - You get no error: the sort field's constructor only checks that the field name is not null. Every located document's distance becomes NaN, and
Double.compareputs NaN after Infinity. - So the order is B, A, C: the document with no location comes first, and A and C report distance NaN. An infinite origin makes every distance Infinity and returns A, B, C in doc-id order.
- Only requests that use this sort are affected: in the probe the matching documents, hit count and paging were unchanged, and the bad sort does not change the index.
Applications that sort by x/y distance and let a NaN or infinite origin through, such as a computed 0/0 or "NaN" or "Infinity" parsed with Float.parseFloat. They get no error and a meaningless order for those requests. Rare: a GitHub code search found no caller in Elasticsearch, OpenSearch or Solr, and no public report since 2020.
Mechanism, terms and numbers596 words
XYDocValuesField.newDistanceSort(field, x, y) documents "@throws IllegalArgumentException if {@code field} is null or location has invalid coordinates", word for word like its lat/lon twin. It returns new XYPointSortField(field, x, y), whose constructor checks only field != null, so a NaN or infinite x or y is accepted. The XY APIs around it that take coordinates (the XYDocValuesField constructor, XYPointField.newDistanceQuery, and the XYPoint, XYCircle, XYRectangle, XYLine and XYPolygon constructors) reject exactly these values through XYEncodingUtils.checkVal, and the lat/lon sort field validates its origin.
The search then runs. A document without the field has distance +Infinity; a located one has sqrt((x - docX)^2 + (y - docY)^2), which is NaN for a NaN origin and +Infinity for an infinite one. Double.compare orders NaN after +Infinity, so with a NaN origin documents without a location come first, against the documented "missing values sort last"; with an infinite origin everything ties and comes back in doc-id order. Only requests that use this sort are affected. It came with the XY sort in 8.5.0 (LUCENE-9238, a port of the lat/lon code that kept the @throws line but not the check) and is in every release through 10.6.0 and on main. Two XYEncodingUtils.encode calls in the constructor fix it; the fix passes the repro, the closest XY suites and check.
- No check.
XYDocValuesField.newDistanceSortisreturn new XYPointSortField(field, x, y);(document/XYDocValuesField.java:151-152). The constructor callssuper(field, Type.CUSTOM, false, Double.POSITIVE_INFINITY)(the missing value), throws only whenfield == null, and storesxandyas given (document/XYPointSortField.java:28-35). The twinLatLonPointSortFieldcallsGeoUtils.checkLatitude(latitude); GeoUtils.checkLongitude(longitude);at the same place (document/LatLonPointSortField.java:34-35), andcheckLatituderejects NaN explicitly (geo/GeoUtils.java:85-86). - The sort key.
XYPointDistanceComparator.sortKeystarts fromDouble.POSITIVE_INFINITY, which a document without the field keeps, and takesMath.minoverMath.sqrt(diffX * diffX + diffY * diffY)withdiffX = x - docX(document/XYPointDistanceComparator.java:190-209). For a NaN origin the distance is NaN andMath.min(Infinity, NaN)is NaN. For an infinite origin every distance is +Infinity, equal to the missing value. - The order. Slots are compared with
Double.compare(:77-79), which orders NaN after +Infinity; a missing document is compared as +Infinity incompareBottom(:126-128). Ascending order therefore puts missing documents first under a NaN origin and ties everything under an infinite origin (ties go by doc id). - No crash, no hang.
setBottombuilds its pruning box only whenbottom < Float.MAX_VALUE(:87-97), which is false for NaN and +Infinity, so nothing throws at search time and nothing loops. The box keeps its initial full range (:55-58), socompareBottomnever skips a value by the box and computes the distance of every value it sees (:122-160): possibly more work than with a finite origin. The probe saw no timing blow-up; memory was not measured.
Documents: A at (1, 1), B without a location, C at (2, 2).
| origin | accepted? | order (distance) |
|---|---|---|
| (0, 0) | yes | A 1.4142, C 2.8284, B Infinity |
| (NaN, 0), (0, NaN) | yes | B Infinity, A NaN, C NaN |
| (Infinity, 0), (0, -Infinity) | yes | A, B, C, all Infinity (doc-id order) |
| (-Float.MAX_VALUE, 0) | yes | A, C (3.4028E38 each), B Infinity |
Controls in the same run: new XYDocValuesField("p", NaN, 0) and XYPointField.newDistanceQuery("p", NaN, 0, 1) throw "invalid value NaN; must be between -3.4028234663852886E38 and 3.4028234663852886E38"; LatLonDocValuesField.newDistanceSort("p", NaN, 0) throws "invalid latitude NaN; must be between -90.0 and 90.0".
Exposure and likelihood278 words
- Who is hit. Applications that sort with
XYDocValuesField.newDistanceSortand let a non-finite origin through: a reference point computed as0/0(for example the centroid of an empty set), or a request parameter "NaN" or "Infinity" parsed withFloat.parseFloatand passed on unchecked. DefaultIndexWriterConfig, codec and searcher; the missing check is in the sort field's constructor, so it does not depend on the index; query-time only. - What goes wrong. No exception, and the results come back in a meaningless order: documents without a location first (NaN origin) or plain doc-id order (infinite origin), with NaN or Infinity as the reported distances. An application that relies on the documented
IllegalArgumentExceptionto reject bad input never sees it. - Limits. The sort field belongs to the request that builds it, so only requests that use it are affected. In the study's probe (20,000 documents, 8 segments, 7 origins; main with assertions on and off, 10.5.2 with assertions on) the matched set and hit count equalled a filter's count,
searchAfterpaging returned every match exactly once, a sliced concurrent search returned the same top 50 hits as the sequential one, and search time did not blow up. These are the tested workloads, not a proof for every index. The bad sort runs only at query time and does not change the index; index-time XY fields still reject non-finite values. - Reach. A GitHub code search found no caller in Elasticsearch, OpenSearch or Solr (default branches, 2026-10-26), only copies of Lucene's own test elsewhere. That bounds the search; it does not prove there are no callers.
- Likelihood: rare. Normal code passes finite coordinates; the recorded searches found no public report since 8.5.0 (2020).
One JUnit test class in lucene/core, one line: newDistanceSort("p", Float.NaN, 0f) must throw IllegalArgumentException. Public API, no index, deterministic; fails with assertions on and off, on main and 10.6.0.
/*
* 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.tests.util.LuceneTestCase;
public class TestXYDistanceSortNaNOrigin extends LuceneTestCase {
public void testNaNOriginRejected() {
// javadoc: throws IllegalArgumentException for invalid coordinates; actual: no exception
expectThrows(
IllegalArgumentException.class, () -> XYDocValuesField.newDistanceSort("p", Float.NaN, 0f));
}
}
cd $LUCENE # an apache/lucene checkout at 645d4fe14a20 (or 10.6.0, branch_10_6, upstream main)
cp $REPRO/TestXYDistanceSortNaNOrigin.java lucene/core/src/test/org/apache/lucene/document/
./gradlew :lucene:core:test --tests org.apache.lucene.document.TestXYDistanceSortNaNOrigin -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
- Expected
newDistanceSort("p", Float.NaN, 0f)throwsIllegalArgumentException, as its javadoc promises.- Observed
- No exception ("Expected exception IllegalArgumentException but no exception was thrown"), on main and 10.6.0, assertions on and off.
More files, environment, full output233 words
The test has the ASF license header, so it is also the regression test unchanged; with it in place, :lucene:core:check -x test passes. It fails on stock and passes with the recommended fix.
The test covers only (NaN, 0); (0, NaN) and the infinite origins are covered by the order probe below. The recommended fix rejects all four non-finite cases.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): affected; also the 10.6.0 release jars with this test |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux, aarch64 |
TestXYDistanceSortNaNOrigin > testNaNOriginRejected FAILED
junit.framework.AssertionFailedError: Expected exception IllegalArgumentException but no exception was thrown
at __randomizedtesting.SeedInfo.seed([DEADBEEF:E2DEC189C27FB499]: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.TestXYDistanceSortNaNOrigin.testNaNOriginRejected(TestXYDistanceSortNaNOrigin.java:25)
:lucene:core:test (FAILURE): 1 test, 1 failure
# the same failure with -Ptests.asserts=false, and on the 10.6.0 release jars (assertions on and off)
# order probe on the same main, assertions on (identical with assertions off and on 10.6.0):
origin=(0.0, 0.0) accepted, order: A=1.4142135623730951 C=2.8284271247461903 B(no location)=Infinity
origin=(NaN, 0.0) accepted, order: B(no location)=Infinity A=NaN C=NaN
origin=(0.0, NaN) accepted, order: B(no location)=Infinity A=NaN C=NaN
origin=(Infinity, 0.0) accepted, order: A=Infinity B(no location)=Infinity C=Infinity
origin=(0.0, -Infinity) accepted, order: A=Infinity B(no location)=Infinity C=Infinity
origin=(-3.4028235E38, 0.0) accepted, order: A=3.4028234663852886E38 C=3.4028234663852886E38 B(no location)=Infinity
control new XYDocValuesField("p", NaN, 0): rejected: invalid value NaN; must be between -3.4028234663852886E38 and 3.4028234663852886E38
control XYPointField.newDistanceQuery("p", NaN, 0, 1): rejected: invalid value NaN; must be between -3.4028234663852886E38 and 3.4028234663852886E38
control LatLonDocValuesField.newDistanceSort("p", NaN, 0): rejected: invalid latitude NaN; must be between -90.0 and 90.0- The javadoc promises
IllegalArgumentExceptionwhen the location has invalid coordinates, and says missing values sort last. - For XY, invalid means non-finite: the same class's constructor,
XYPoint,XYCircleandXYPointField.newDistanceQueryall reject NaN and Infinity. - The lat/lon twin, from which the XY code was ported, validates its origin; the port kept the
@throwsline but not the check.
The full argument306 words
- The method's own contract. "@throws IllegalArgumentException if {@code field} is null or location has invalid coordinates." (
XYDocValuesField.java:149) and "missing values sort last" (:139-140). - What "invalid" means for XY. The same class's constructor and
setLocationValue: "@throws IllegalArgumentException if ... x or y are infinite or NaN" (:79,:91), enforced throughXYEncodingUtils.encode(:94-95), which callscheckVal, "validates value is a number and finite" (XYEncodingUtils.java:34-41).XYPointandXYCirclecallcheckValtoo. - The sibling query enforces the identical wording, and a test checks it.
XYPointField.newDistanceQuery: "@throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid" (XYPointField.java:174-175), enforced bynew XYCircle(:178). UpstreamBaseXYPointTestCase.testDistanceIllegal, "distance query should not accept invalid x/y as origin", passes a NaN origin. No upstream test passes a non-finite origin to the XY sort. - The twins validate.
LatLonPointSortFieldhas checked its origin since its first commit (LUCENE-7099, 2016), and the Geo3D distance sort goes throughGeo3DUtil.fromDistance, which callsGeoUtils.checkLatitude/checkLongitude. - A port that lost the check. LUCENE-9238 (PR apache/lucene-solr#1272, whose description reads "Adaptation of LatLonPoint to generic cartesian space.") copied the
@throwsline word for word but not the check. That the check was lost in the port is an inference from the identical javadoc and the validating twin, not a stated intent; the PR has no discussion of validation. - The finite-only rule predates the sort.
XYEncodingUtils.checkValhas rejected NaN and values beyond +/-Float.MAX_VALUE(so both infinities) since it was added withXYEncodingUtilsin LUCENE-8632 (0c09481374c, 2019-07-08). One week before the sort, LUCENE-9218 (apache/lucene-solr#1252, merged 2020-02-14) rewrote it for floats asFloat.isFinite(x) == false, "validates value is a number and finite"; in that PR the author had writtenFloat.isNaN(x) || Float.isInfinite(x)and a reviewer suggested the single test.
The devil's-advocate check174 words
The strongest case: the XY javadoc is lat/lon boilerplate (it still says the sort value is a "distance in meters", XYDocValuesField.java:137, although x/y coordinates have no unit), XYPointSortField is package-private, NaN in means garbage out, and Double.compare is a total order, so the result is at least deterministic. Finiteness is an XY-family rule, not a Lucene-wide one: the generic FloatPoint deliberately accepts NaN and the infinities (TestPointQueries.testCrazyFloats), and a caller could be using a NaN origin as a sentinel today. The cross-model reviews rated this case weak.
Why it fails: the @throws clause is exactly what the siblings enforce (the lat/lon and Geo3D sorts, the XY field, point, circle and distance-query APIs, the same class's constructor). The fix rejects only non-finite origins; the finite origins probed are unchanged (an origin at -Float.MAX_VALUE still sorts with missing last, with and without the fix). A caller that relies on a non-finite origin would now get the exception, a compatibility change. It is a good reason for low priority, not for keeping the gap.
Recommended Validate x and y in the XYPointSortField constructor (+4 lines)
Validate the origin in the XYPointSortField constructor with the XY family's finite check: XYEncodingUtils.encode(x); XYEncodingUtils.encode(y); plus an import. Validated on main: the repro passes with assertions on and off, the closest XY suites (90 tests) pass, and check passes.
All options and trade-offs4 options · 532 words
The recommended fix was run in a scratch worktree of 645d4fe14a20 (OpenJDK 25.0.3, seed DEADBEEF), removed afterwards. "Closest XY suites" means TestXYPointDistanceSort, TestXYDocValuesQueries, TestXYPointQueries, TestXYPoint and TestXYCircle in lucene/core.
Validate x and y in the XYPointSortField constructor (+4 lines) (validated by the checks) (recommended)
XYEncodingUtils.checkVal is package-private in org.apache.lucene.geo; encode is public and calls it, exactly as XYDocValuesField.setLocationValue does. The error is the one new XYDocValuesField gives: "invalid value NaN; must be between -3.4028234663852886E38 and 3.4028234663852886E38". This is the finder's fix with a shorter comment. The diff shown is complete; its comment was reworded after the fact check, and it differs from the gradle-validated diff only in that comment line.
--- a/lucene/core/src/java/org/apache/lucene/document/XYPointSortField.java
+++ b/lucene/core/src/java/org/apache/lucene/document/XYPointSortField.java
@@ -16,6 +16,7 @@
*/
package org.apache.lucene.document;
+import org.apache.lucene.geo.XYEncodingUtils;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.Pruning;
import org.apache.lucene.search.SortField;
@@ -30,6 +31,9 @@
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
+ // reject NaN and infinite x/y, as XYDocValuesField does (XYEncodingUtils.checkVal)
+ XYEncodingUtils.encode(x);
+ XYEncodingUtils.encode(y);
this.x = x;
this.y = y;
}Trade-offs
- A caller that passes a non-finite origin now gets the documented exception instead of a meaningless order: a visible behaviour change worth a CHANGES entry, and a break for any caller that used a NaN or infinite origin on purpose (say, as a sentinel). The finite origins probed give the same results: an origin at -Float.MAX_VALUE still sorts, missing last. Constructor-time cost only.
- Calling
encodefor its side effect reads slightly odd; the alternatives below avoid that. - Open PR #14775 ("Add the ability to inverse a Sort") also edits
XYPointSortField.java, outside the constructor, so a fix PR conflicts at most trivially.
What the checks validated
- Stock
645d4fe14a20: the repro fails (1 of 1) with assertions on and off, through gradle and the standalone runner; also on the 10.6.0 jars. - With the fix: the repro passes with assertions on and off; the 5 closest XY suites pass (90 tests, 5 skipped, 0 failures);
:lucene:core:check -x test(google-java-format, ecj lint main and test, forbidden APIs, dependency license check) passes with the repro test in place. These are targeted runs, not the full Lucene test suite. - Probe with the fixed file shadowed: (NaN, 0), (0, NaN), (Infinity, 0) and (0, -Infinity) are rejected; (0, 0) and (-Float.MAX_VALUE, 0) give the same order and distances as stock.
- The diff shown (reworded comment) applies to
645d4fe14a20withgit apply --check, is google-java-format clean, and passes the minimal repro with assertions on and off (stock still fails both).
new XYPoint(x, y); validates both values with the same checkVal and the same message.
Trade-offs
Reads as "the origin must be a valid point", at the cost of one small allocation per sort field.
if (Float.isFinite(x) == false || Float.isFinite(y) == false) throw new IllegalArgumentException(...).
Trade-offs
Clearest intent, but a new message unlike the rest of the XY family.
Would keep missing documents last for a NaN origin.
Trade-offs
Not enough: it hides the bad input instead of rejecting it, and the javadoc promises an exception.
A GitHub code search found no caller of this API in Elasticsearch, OpenSearch or Solr; an application that forwards a user-supplied origin unchecked lets that user scramble only the order of their own results.
A normalised static diff of sibling classes compared LatLonPointSortField with its x/y port XYPointSortField: the lat/lon origin checks have no counterpart in the XY class. A public-API JUnit with NaN and Infinity then showed that newDistanceSort accepts them.
The method in full128 words
Method: non-formal (the diff is an automated code-reading aid, not a model, a proof or a bounded-exhaustive check).
- The unit's twin-diff tool normalised the lat/lon and x/y sibling classes of the geo family and listed every member that differs beyond the expected type and name changes. In
LatLonPointSortFieldvsXYPointSortField, theGeoUtils.checkLatitude/checkLongitudelines exist only in the lat/lon twin. - The finder then ran a targeted public-API JUnit on the stock jar with the non-finite values (NaN, +Infinity, -Infinity):
newDistanceSortaccepted all of them, and a NaN origin put the document without a location first. An independent reproducer wrote its own test (all six non-finite origins accepted, controls passing), a devil's advocate and an adjudication confirmed it, and the repro was later cut to the one-line test above.
No prior report found: 75 tool queries plus manual GitHub, JIRA, mailing-list and web searches and the changed files of all 411 open PRs (2026-10-26), refreshed on GitHub 2026-10-31. Not fixed upstream.
Searches3 searches
Verdict new. Related, not reports: #10278 / apache/lucene-solr#1272 (the introduction, no validation discussion); apache/lucene-solr#1252 (LUCENE-9218, the finite-only checkVal, intent evidence); #8157 / #8159 (LUCENE-7102/7104, LatLon missing values in 2016, a different defect, not precedent); #13301 (signed-zero equals in geo classes); open #14775 (edits the same file, constructor untouched). Limits: GitHub issue search does not index code, diffs or review comments (covered in part by the open-PR file scan); list and JIRA search are word-based; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| a scripted prior-report search: apache/lucene issues, PRs and commits, downstream repositories, mailing lists (dev@, java-user@, issues@, commits@), JIRA, web | 2026-10-26 | two runs, 75 distinct queries plus 8 web queries; 120 and 194 distinct hits, every medium hit read; the finder's earlier run (15 queries, 16 hits) | no report; 0 high-overlap hits |
| Manual: GitHub (Elasticsearch, OpenSearch and its geospatial plugin, Solr, Lucene), JIRA (LUCENE, SOLR), mailing lists | 2026-10-26 | 9 GitHub searches incl. a code search, 14 JQL queries, 24 list queries; the changed files of all 411 open apache/lucene PRs | no report; #14775 is the only open PR touching XYPointSortField (constructor untouched) |
| GitHub issues and PRs (apache/lucene), refresh | 2026-10-31 | "XYPointSortField", "newDistanceSort NaN", "XYDocValuesField newDistanceSort", "XY distance sort origin", "XYDocValuesField NaN", "cartesian distance sort", "invalid coordinates newDistanceSort"; PRs mentioning XY updated since 2026-10-26; upstream commits on the two files since 2026-10-20 | nothing new (a control search for newDistanceSort returns ten older items on LatLon, GeoPoint, Geo3D and spatial-extras distance features, such as #10728 on PrefixTreeStrategy, none about XY) |
Born with the feature: LUCENE-9238 (88dd1c3f3d4, 2020-02-21) ported the lat/lon distance sort to x/y without its origin check; first released in 8.5.0. It fails the same way on all 52 releases 8.5.0 to 10.5.2, on 10.6.0 and on main; never fixed.
Versions tested8 rows
Introduced by 88dd1c3f3d4 (master, now main) and the branch_8x backport 53cb6acf912, both "LUCENE-9238: Add new XYPointField, queries and sorting capabilities (#1272)" (Ignacio Vera, 2020-02-21; PR apache/lucene-solr#1272, issue now #10278), which created XYDocValuesField, XYPointSortField and XYPointDistanceComparator. The constructor has checked only field != null ever since; later edits (LUCENE-9570, LUCENE-10377, #12405, #15483, #15486) never touched validation. Not a regression, so there is no commit-level A/B: the parents do not have the classes.
Before 8.5.0 the API does not exist (8.4.1: the study's JUnit does not compile, failing only at the uses of XYDocValuesField). The constructor is unchanged on 10.6.0, branch_10_6 and upstream main 2f424fd428d (2026-10-30).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal test fails (gradle and the standalone runner, assertions on and off); NaN origin order B, A, C in the probe |
| 10.6.0 | affected | the minimal test fails (assertions on and off); the same probe output |
| 10.0.0 to 10.5.2 (12 releases) | affected | the study's JUnit fails the same way on each (10.5.2 also with assertions off) |
| 9.0.0 to 9.12.3 (21 releases) | affected | the study's JUnit fails the same way on each |
| 8.5.0 to 8.11.4 (19 releases) | 8.5.0: first affected | the study's JUnit fails the same way on each (8.5.0 also with assertions off) |
| 8.4.1 and older | API absent | the study's JUnit does not compile (no XY point classes in lucene-core) |
branch_10_6, upstream main 2f424fd428d | read | the same constructor; no upstream commit on the file since 2026-10-20 |
fix on 645d4fe14a20 | not affected | all four non-finite origins rejected; the repro, the closest XY suites and check pass |
An independent reproducer, a devil's advocate and an adjudication (all Claude) rated it real; cross-model reviews (OpenAI GPT-5.6, reading only) split, one procedural "not real" and one real, both agreeing on the root cause. It still reproduces on main and 10.6.0.
Checks and verdicts332 words
The OpenAI GPT-5.6 reviewers could only read code; separate runs re-ran the finder's repro for both reviews, and one of them also wrote and ran its own JUnit.
| Check | Verdict | What it found |
|---|---|---|
| Finder (twin diff, public-API JUnit) | candidate | JUnit fails on main (assertions on and off), 10.5.2 and 9.12.3; the fix passes and upstream TestXYPointDistanceSort still passes. |
| Cross-model review, refute lens (OpenAI GPT-5.6) | not real (low), procedural | Could not run code; agreed on the root cause and that the inputs are in contract; rated the intended argument weak. |
| Independent reproducer | real | Own JUnit: none of six non-finite origins throws on main, 10.5.2 and 9.12.3; controls pass; passes with the fix. |
| Cross-model adversary, intended lens (OpenAI GPT-5.6), and a separate run | real (medium) | The reviewer read code only and rated the best works-as-designed case weak; a separate run wrote and ran its own JUnit, which fails on main (4 tests, 2 failures: the NaN and the Infinity origin, assertions on and off). |
| Devil's advocate | real (high), low impact | The lat/lon boilerplate argument fails; origins at +/-Float.MAX_VALUE still sort with missing last. |
| Adjudication | confirmed, high | Re-ran the independent reproduction's and the finder's tests with assertions on and off. |
| Credibility audit, current-main check and minimal repro | keep; still reproduces | Audit re-run on main; main a9779ef3c193 and 645d4fe14a20; the repro cut to one one-line test. |
| This doc | still reproduces; fix validated | Gradle on a scratch worktree of 645d4fe14a20; the 10.6.0 jars. |
| Independent fact and diagram checks | real; corrections applied | A fact check re-ran the repro on main and 10.6.0, the documented gradle command, the fix and the XY suites; a cross-model fact check (OpenAI GPT-5.6, reading only) re-read every citation; a diagram check reviewed the frames. Their errors (the checkVal history, the printed diff, the list of XY APIs that validate, the probe's limits, the cross-model review attribution) were fixed; the diagram was re-rendered with the narrower wording. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 460 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,531 words
- Registry title
- XYDocValuesField.newDistanceSort accepts a NaN or infinite origin although its javadoc promises IllegalArgumentException for invalid coordinates (the LatLon twin and every other XY entry point reject such values); with a NaN origin, documents without a location sort first instead of last
- Area
- core / document: XY distance sort (XYDocValuesField.newDistanceSort, XYPointSortField, XYPointDistanceComparator)
- Priority
- Low, rank 886: XYDocValuesField.newDistanceSort accepts a NaN or infinite origin instead of rejecting it, with a minor missing-last sort side effect, the same accepts-NaN-parameter profile as LFV-304.
- Impact class
- wrong error
- Impact note
- Applications that sort by cartesian distance with XYDocValuesField.newDistanceSort and let a NaN or +/-Infinity origin coordinate through (for example a value computed as 0/0, or request text parsed with Float.parseFloat, which accepts 'NaN' and 'Infinity') get no IllegalArgumentException, and the search runs with a meaningless order: with a NaN origin every distance is NaN and documents without the field (distance +Infinity) come first, against the documented 'missing values sort last'; with an infinite origin every distance is +Infinity and the hits come back in doc-id order; default IndexWriterConfig, codec and searcher, any index size; query-time only, so the match set, the hit count, other requests and the index are unaffected; released since 8.5.0.
- Summary
- You build a distance sort with XYDocValuesField.newDistanceSort("p", Float.NaN, 0f), or with an infinite x or y, and expect the IllegalArgumentException that its javadoc promises when the 'location has invalid coordinates', as LatLonDocValuesField.newDistanceSort throws for NaN and as the XYDocValuesField constructor, XYPointField.newDistanceQuery and XYCircle throw for the same values. Instead the sort is built and the search runs, because the XYPointSortField constructor checks only that the field name is not null. With a NaN origin every distance is NaN, which Double.compare orders after +Infinity, so a document without a location (distance +Infinity, documented as 'missing values sort last') is returned before the documents that have one; with an infinite origin all distances tie at +Infinity.
- Introduced
- 88dd1c3f3d4eb37c522b9b00a5ccce979e6efad6 (master, now main), backported to branch_8x as 53cb6acf912f54a07b870fe6726bfee329b62d06; both 'LUCENE-9238: Add new XYPointField, queries and sorting capabilities (#1272)' by iverase (PR apache/lucene-solr#1272, 'Adaptation of LatLonPoint to generic cartesian space', one approval and one comment asking for a CHANGES entry, no inline review comments; migrated issue apache/lucene#10278) [read: git log and blame in the full-history clone; the commit creates XYDocValuesField, XYPointSortField and XYPointDistanceComparator, the XYPointSortField constructor has checked only field != null ever since, and neither parent (8ea947eeb2e, cb68d7d2c57) has the classes]. Born with the feature, not a regression: the LatLon twin LatLonPointSortField validated its origin from its first commit (LUCENE-7099, b3ee746a8b1, 2016-03-11; GeoUtils.checkLatitude/checkLongitude since 6f0488f7302, 2016-03-27), and XYEncodingUtils.checkVal already required finite values one week before (LUCENE-9218, 4a54ffb553ad, 2020-02-14). No commit-level A/B: the introducing commit creates the affected classes, so its parent cannot compile the repro (2020-02-21 (author and commit date of 88dd1c3f3d4 and 53cb6acf912))
- First affected version
- 8.5.0 [exec] (the first release with the XY point classes; 8.4.1 has no such API [exec: the repro fails to compile only at the four uses of XYDocValuesField; lucene-core-8.4.1 has no XY point classes])
- Affected versions
- every release from 8.5.0 to 10.6.0, 53 releases (19 on 8.x: 8.5.0-8.11.4; 21 on 9.x: 9.0.0-9.12.3; 13 on 10.x: 10.0.0-10.6.0), and main ea57360912ed and 645d4fe14a20 (unreleased 11.0) [exec 2026-10-31: the minimal test fails on the 10.6.0 release jars (SHA-1 checked) with -ea and -da, and the order probe gives the same order there; the minimal test fails on main 645d4fe14a20 with -ea and -da; exec: the finder's JUnit, in three version-adapted sources with identical assertions, fails 3 run / 2 failed on all 52 releases and on main, with the same two messages, also with -da on 8.5.0, 10.5.2 and main (follow-up, 57 failing runs); the independent reproduction's JUnit fails 5 run / 2 failed on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check's fails 3 run / 1 failed on main (-ea and -da) and 10.5.2; 8.8.2 to 8.11.4 were covered by executing their SHA-1-checked release jars only, because the history clone's 8.x tags stop at 8.8.1]. Not affected: 8.4.1 and earlier (API absent) [exec]. Not fixed upstream: upstream main cf398bfb142 (2026-10-26) has a byte-identical XYPointSortField.java and branch_10x the same constructor; no open PR adds origin validation [read: GitHub contents and commits API, scan of the changed files of all 411 open apache/lucene PRs]. All runs on aarch64 Linux with JDK 25
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26, read-only; the prior-report record verdict new, high confidence): two scripted prior-report searches (75 distinct queries over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive, plus 8 web queries; 120 and 194 distinct hits, 0 high overlap, every medium hit read), the finder's run (15 queries, 16 hits, all LatLon, GeoPoint or Geo3D distance-sort history), 9 manual GitHub searches (Elasticsearch, OpenSearch and its geospatial plugin, Solr; a code search for 'XYDocValuesField.newDistanceSort' finds only copies of Lucene's own test), 14 JIRA text searches (LUCENE and SOLR), 24 list searches, the changed files of all 411 open apache/lucene PRs and 22 issue, PR and JIRA reads; the intended-behaviour check's own search (24 list and JIRA queries plus GitHub searches) found nothing either. Context, not this bug: LUCENE-9238 / #10278 and apache/lucene-solr#1272 (the introduction; nobody discussed validation), apache/lucene-solr#1252 (LUCENE-9218: a reviewer asked XYEncodingUtils.checkVal to reject non-finite values, one week before the XY sort was added without that check; intent evidence), LUCENE-7102 / #8157 and LUCENE-7104 / #8159 (infinite missing values in the LatLon comparator, 2016; a removed option, not precedent), #13301 (signed-zero equals in geo classes), open #14775 (adds inverseSort() to XYPointSortField, constructor untouched; a fix PR would conflict only trivially). Not fixed: upstream main cf398bfb142 (2026-10-26) has a byte-identical XYPointSortField.java. Gaps: GitHub issue search does not index code, diffs or review comments (covered in part by the open-PR file scan and the reads); list and JIRA search are word-based; private security reports are not visible
- Lucene commit tested
- main
645d4fe14a20(gradle in a scratch worktree and the standalone runner, assertions on and off; an order probe on the jars); the 10.6.0 release jars (the minimal test and the probe, assertions on and off); the study's runs on mainea57360912edanda9779ef3c193(the files involved are identical) and on all 52 releases 8.5.0 to 10.5.2; the recommended fix in a scratch worktree of645d4fe14a20. Upstream main2f424fd428d(2026-10-30) has an identicalXYPointSortField.java; 10.6.0 andbranch_10_6have the same constructor.
- Finding record
lucene/core/src/java/org/apache/lucene/document/XYPointSortField.java:28-35 (at ea57360912ed) (constructor: super(field, Type.CUSTOM, false, Double.POSITIVE_INFINITY), checks only field != null, stores x and y unchecked), :60-67 (equals compares x != other.x)lucene/core/src/java/org/apache/lucene/document/XYDocValuesField.java:79 (at ea57360912ed) and :91 (constructor and setLocationValue: '@throws IllegalArgumentException if ... x or y are infinite or NaN'), :94-95 (XYEncodingUtils.encode on x and y), :133-153 (newDistanceSort javadoc: 'distance in meters', 'missing values sort last' at :140, '@throws IllegalArgumentException if {@code field} is null or location has invalid coordinates.' at :149; the factory at :151 returns new XYPointSortField(field, x, y))lucene/core/src/java/org/apache/lucene/document/XYPointDistanceComparator.java:82-98 (at ea57360912ed) (setBottom builds the competitive box only when bottom < Float.MAX_VALUE), :122-160 (compareBottom: missing document compared as +Infinity, Math.sqrt distance otherwise), :190-208 (sortKey: +Infinity for a missing document, Math.min over sqrt((x-docX)^2+(y-docY)^2) otherwise, NaN for a NaN origin)lucene/core/src/java/org/apache/lucene/geo/XYEncodingUtils.java:34-41 (at ea57360912ed) (checkVal 'validates value is a number and finite': Float.isFinite(x) == false throws IllegalArgumentException)lucene/core/src/java/org/apache/lucene/document/LatLonPointSortField.java:29-37 (at ea57360912ed) (the twin constructor calls GeoUtils.checkLatitude and checkLongitude)lucene/core/src/java/org/apache/lucene/document/XYPointField.java:170-179 (at ea57360912ed) (newDistanceQuery: same 'location has invalid coordinates' @throws, enforced through new XYCircle); lucene/core/src/java/org/apache/lucene/geo/XYPoint.java:44-45 and XYCircle.java:51-52 (checkVal on x and y); lucene/test-framework/src/java/org/apache/lucene/tests/geo/BaseXYPointTestCase.java:256-263 (testDistanceIllegal, 'distance query should not accept invalid x/y as origin', NaN origin)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/document/XYDocValuesField.java:133-153 (at 645d4fe14a20) (newDistanceSort javadoc and factory)lucene/core/src/java/org/apache/lucene/document/XYPointSortField.java:28-35 (at 645d4fe14a20) (constructor)lucene/core/src/java/org/apache/lucene/document/LatLonPointSortField.java:29-38; (at 645d4fe14a20) geo/GeoUtils.java:85-86lucene/core/src/java/org/apache/lucene/document/XYPointDistanceComparator.java:55-58, (at 645d4fe14a20) 77-79, 87-97, 122-160, 190-209lucene/core/src/java/org/apache/lucene/geo/XYEncodingUtils.java:34-41, (at 645d4fe14a20) 50-52, 60-89; geo/XYPoint.java:43-46; geo/XYCircle.java:44-54; geo/XYRectangle.java:45-48; geo/XYLine.java:67-68; geo/XYPolygon.java:93-103
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/XYDocValuesField.java:79, (at 645d4fe14a20) 91, 94-95, 137, 139-140, 149lucene/core/src/java/org/apache/lucene/geo/XYEncodingUtils.java:34-41, (at 645d4fe14a20) 50-52; geo/XYPoint.java:43-46; geo/XYCircle.java:51-52; geo/XYRectangle.java:45-48; geo/XYLine.java:67-68; geo/XYPolygon.java:93-103; geo/XYEncodingUtils.java:60-89 (decode, floatArrayToDoubleArray: no check)lucene/core/src/java/org/apache/lucene/document/XYPointField.java:165-180 (at 645d4fe14a20)lucene/test-framework/src/java/org/apache/lucene/tests/geo/BaseXYPointTestCase.java:256-263; (at 645d4fe14a20) lucene/core/src/test/org/apache/lucene/search/TestXYPointDistanceSort.java:86lucene/core/src/java/org/apache/lucene/document/LatLonPointSortField.java:34-35; (at 645d4fe14a20) lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DDocValuesField.java:183-192; lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DUtil.java:152-158gh api repos/apache/lucene-solr/pulls/1252/comments (2026-10-31, read-only: "This could be a single test." on XYEncodingUtils.checkVal)lucene/core/src/test/org/apache/lucene/search/TestPointQueries.java:286-320 (at 645d4fe14a20) (testCrazyFloats: FloatPoint with NaN and both infinities)
- Suggested fix
lucene/core/src/java/org/apache/lucene/document/XYDocValuesField.java:93-97; (at 645d4fe14a20) geo/XYEncodingUtils.java:34-52; geo/XYPoint.java:43-46gh pr view 14775 --repo apache/lucene (2026-10-31, read-only: open, files include XYPointSortField.java)
- History
a Lucene clone releases/lucene/10.6.0, origin/branch_10_6, 2f424fd428d: XYPointSortField.java constructor (main identical, 10.6.0 and branch_10_6 differ only by the setMissingValue override)
- TestXYDistanceSortNaNOrigin.java (1 one-line test):
repro/LFV-426/src/org/apache/lucene/document/TestXYDistanceSortNaNOrigin.java(28 lines)
Made separately from this text, from the code at 645d4fe14a20 and runs executed for the diagrams; the PNG is the summary still.
assets/LFV-426/bug.png(460 KB)
