The circle's bounding box takes asin of a ratio of two fast cosines; at one of SloppyMath's table switches the ratio rounds just above 1, and asin of more than 1 is NaN.
- A LatLon distance query first draws a latitude/longitude box around the circle (
Rectangle.fromPointDistance) and uses it to skip index cells and points far away; candidates inside the box are then checked by their real distance. - If the circle reaches a pole, the box spans every longitude. Otherwise its half-width in longitude is
asin(sin(r) / cos(lat)), where r is the radius as an angle and lat the centre latitude. In exact arithmetic that ratio is at most 1 whenever the circle reaches neither pole. - Lucene computes
sin,cosandasinwithSloppyMath, fast approximations (the cosine is documented as accurate to about 1e-15), not exact.asinof anything above 1 is NaN, as inMath.asin.
asin gives NaN. Select the still to open it at full size. Generated with AI, checked against the code.- Index one point at (0.43945312499999994, 0) and count
LatLonPoint.newDistanceQuery("p", 0.43945312499999994, 0, 9958692.080819247). Centre and radius are valid: the pole is 9,958,692.15 m away, so the circle stops 7 cm short of it. You expect 1 hit, the point itself. - Before searching, Lucene draws a lat/lon box around the circle. It pads the radius by 7 cm, so the box's top edge is 47 nanometres below the pole: no pole is inside, and the box's half-width in longitude is
asin(sin(r) / cos(lat)). In exact arithmetic that ratio is 0.99999999999999994, just below 1. - Lucene's fast cosine (a table with a step of 2pi/2048 plus a short polynomial) is not monotone at some of the points where it switches table points, here at 0.439453125 degrees.
sin(r), computed ascos(r - pi/2), comes out one rounding step abovecos(lat): 0.9999705864309741 vs 0.999970586430974. - The quotient is 1.0000000000000002,
asinreturns NaN, the box's longitudes are NaN, and the search throwsIllegalArgumentException: invalid longitude NaN; must be between -180.0 and 180.0. The same centre with the radius 1 m smaller or larger works.
Any application running a LatLon circle query on user-supplied coordinates: LatLonPoint.newDistanceQuery (default configuration, not experimental), newGeometryQuery with a Circle, LatLonDocValuesField.newSlowDistanceQuery. Only for a centre within about 1e-13 degree below one of five latitudes (0.44, 0.79, 2.02, 2.55, 12.22 degrees, or their southern mirrors) and a radius within about 1e-7 m of "distance to the pole minus 7 cm". Contrived.
Mechanism, terms and numbers547 words
Before a LatLon distance query runs, Rectangle.fromPointDistance draws a latitude/longitude box around the circle. When the circle, padded by 7 cm, reaches neither pole, it computes the box's half-width in longitude as asin(sin(radDistance) / cos(radLat)) (Rectangle.java:157) with no clamp. The quotient is at most 1 in exact arithmetic, but sin, cos and asin are SloppyMath's fast versions, and SloppyMath.cos is not monotone where it switches table entries. For the circle centred at (0.43945312499999994, 0) with radius 9,958,692.080819247 m, which stops 7 cm short of the north pole, the quotient comes out 1.0000000000000002 (StrictMath: 0.9999999999999999), asin returns NaN, and the Rectangle constructor throws IllegalArgumentException: invalid longitude NaN; must be between -180.0 and 180.0. LatLonPoint.newDistanceQuery therefore fails at search time, and LatLonDocValuesField.newSlowDistanceQuery when built, although their javadoc accepts every valid centre and finite, non-negative radius. No wrong hits and nothing corrupted; the inputs are contrived. It dates from LUCENE-6908 (5.5.0, 2015). Fix: asin(Math.min(1, ...)), the clamp SloppyMath.haversinMeters already uses.
LatLonPoint.newDistanceQueryaccepts the query: the constructor checks only a non-null field, a finite non-negative radius and the centre (LatLonPointDistanceQuery.java:57-71). At search,createWeightcallsRectangle.fromPointDistance(latitude, longitude, radiusMeters)(:83).fromPointDistancepads the radius by 7 cm,radDistance = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS(Rectangle.java:150), and takes the non-pole branch whenminLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS(:156). For the repro the padded top edge is 7.3e-15 rad (47 nm) below the pole, so this branch runs.double deltaLon = asin(sin(radDistance) / cos(radLat));(:157), withasin,sinandcosimported fromSloppyMath(:31-33).SloppyMath.sin(a)iscos(a - PIO2)(SloppyMath.java:203-205), so the ratio iscos(0.007669903939435452) / cos(0.007669903939428205). The numerator's angle is 7.2e-15 rad larger, so exactly the ratio is below 1.SloppyMath.cos(SloppyMath.java:92-114) rounds the angle to the nearest table point (a step of 2pi/2048; 2049 entries),index = (int) (a * SIN_COS_INDEXER + 0.5)(:100), and adds a 4th-order Taylor correction. The two angles straddle the switch between points 2 and 3, at 2.5 x 2pi/2048 = 0.007669903939428206 rad (0.439453125 degrees). The correction from point 3 lands one ulp higher: 0.9999705864309741 against 0.999970586430974. The documented error, "around 1E-15" (:80), still holds; monotonicity is not promised.- The ratio is
1.0000000000000002;SloppyMath.asinreturns NaN above 1, as documented (:124-125, code:171-176).minLonandmaxLonbecome NaN, andnew Rectangle(...)(Rectangle.java:174-178) rejects them inGeoUtils.checkLongitude(Rectangle.java:88,GeoUtils.java:98-100).
| value | SloppyMath | exact / StrictMath |
|---|---|---|
sin(radDistance) | 0.9999705864309741 | |
cos(radLat) | 0.999970586430974 | |
| ratio | 1.0000000000000002 | 0.99999999999999994 exact; 0.9999999999999999 StrictMath |
| half-width in longitude | NaN | 89.99999914622636 degrees |
Neighbours pass: radius 1 m smaller gives longitudes -89.997 to 89.997; 1 m larger crosses the pole (-180 to 180); the exact knot latitude 0.439453125 gives -90 to 90; a centre 1e-9 degree north or south also gives a box.
LatLonPoint.newGeometryQuery(INTERSECTS, Circle) becomes a distance query (LatLonPoint.java:333-334) and fails at search. LatLonDocValuesField.newSlowDistanceQuery and LatLonShape.newGeometryQuery with a Circle fail when built (Circle2D.java:417). Read only, not executed: the same call sits behind GeoEncodingUtils.createDistancePredicate (GeoEncodingUtils.java:174), distance sort (LatLonPointDistanceComparator.java:94), NearestNeighbor (NearestNeighbor.java:131) and distance-feature pruning (LatLonPointDistanceFeatureQuery.java:375), there with computed radii.
Exposure and likelihood323 words
- Who is hit. Applications that run a LatLon circle query on a user-supplied centre and radius:
LatLonPoint.newDistanceQuery,LatLonPoint.newGeometryQuery(INTERSECTS, Circle),LatLonDocValuesField.newSlowDistanceQueryandLatLonShape.newGeometryQuerywith aCircle. Default codec, writer and searcher. - What it takes. A centre latitude within about 1e-14 to 1e-13 degree below one of five latitudes, k x 360/4096 degrees for k = 5, 9, 23, 29, 139 (0.44, 0.79, 2.02, 2.55, 12.22 degrees), together with a radius within about 1e-7 m of "distance to the pole minus 7 cm". The repro's centre is one double (5.6e-17 degree) below 0.439453125; in a scan of the 4,097 doubles at and below each latitude the throwing centres reach at most 5.6e-14, 1.45e-13, 9.7e-14, 9.3e-15 and 1.07e-14 degree below. The southern mirrors fail too (the repro's centre negated throws the same). 0 of 2,000,000 random legal circles throw, drawn as
TestGeoUtilsdoes (latitude in [-90, 90], longitude in [-180, 180], radius in [0, 50,000 km)); an independent run with radius in [0, 20,000 km) also found 0 of 2,000,000. The five latitudes lie on LatLonPoint's 32-bit encoding grid (step 4.2e-8 degree) and the exact knot passes, so a centre decoded from an indexedLatLonPointcannot trigger it (the repro's indexed point decodes to 0.43945308309048414, which gives a box); the repro itself passes the raw double as the query centre. - What happens. That one query fails with an
IllegalArgumentExceptionthat blames a longitude the caller never passed, before any index data is read. - What does not happen. No wrong hits, no corruption, no hang, no effect on other searches or on the index (a larger query that contains such a clause, for example a
BooleanQuery, fails as a whole). - Why fix it anyway. It is a one-line guard, and the unclamped ratio is fragile in general: with
Math.sin/Math.cosinstead of SloppyMath the same formula goes above 1 in 450 of 1,280,000 near-pole test cases (OpenJDK 25, Linux aarch64, JIT intrinsics).
One JUnit test on the public API: index one LatLonPoint at the centre, count LatLonPoint.newDistanceQuery with the radius that stops 7 cm short of the pole, and expect 1 hit. Deterministic, single thread; the same with assertions on and off. Keep the two double literals unchanged.
/*
* 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.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
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 TestDistanceQueryNearPoleNaN extends LuceneTestCase {
public void testLegalCircleEndingJustShortOfPole() throws Exception {
double lat = 0.43945312499999994;
double radiusMeters = 9958692.080819247;
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new LatLonPoint("p", lat, 0));
w.addDocument(doc);
try (DirectoryReader r = DirectoryReader.open(w)) {
Query q = LatLonPoint.newDistanceQuery("p", lat, 0, radiusMeters);
try {
// expected: the centre point matches (1 hit); actual: IllegalArgumentException
assertEquals(1, new IndexSearcher(r).count(q));
} catch (IllegalArgumentException e) {
throw new AssertionError("expected 1 hit for a legal circle, got " + e, e);
}
}
}
}
}
cp TestDistanceQueryNearPoleNaN.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDistanceQueryNearPoleNaN -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
- Expected
- 1 hit: the indexed centre point lies inside the valid circle.
- Observed
IllegalArgumentException: invalid longitude NaN; must be between -180.0 and 180.0fromRectangle.fromPointDistance, with assertions on and off.
More files, environment, full output219 words
Public API only: IndexWriter with the default IndexWriterConfig, ByteBuffersDirectory, LatLonPoint, IndexSearcher.count. The inputs are exact doubles (bits 0x3fdc1fffffffffff and 0x4162fea48296123f); the seed does not matter (10 of 10 random-seed iterations fail). The code is pure Java double arithmetic; a run in the interpreter with SVE off (-Xint -XX:UseSVE=0) fails the same way; x86 was not run.
As the regression test: this same test, for example as a method in TestLatLonPoint.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); the same test also fails on 10.6.0; every release from 5.5.0 is affected |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp TestDistanceQueryNearPoleNaN.java $LUCENE/lucene/core/src/test/org/apache/lucene/document/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.document.TestDistanceQueryNearPoleNaN -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same result)
TestDistanceQueryNearPoleNaN > testLegalCircleEndingJustShortOfPole FAILED
java.lang.AssertionError: expected 1 hit for a legal circle, got java.lang.IllegalArgumentException: invalid longitude NaN; must be between -180.0 and 180.0
at __randomizedtesting.SeedInfo.seed([DEADBEEF:B3F9BEAC2208C74F]:0)
at org.apache.lucene.document.TestDistanceQueryNearPoleNaN.testLegalCircleEndingJustShortOfPole(TestDistanceQueryNearPoleNaN.java:44)
Caused by:
java.lang.IllegalArgumentException: invalid longitude NaN; must be between -180.0 and 180.0
at org.apache.lucene.geo.GeoUtils.checkLongitude(GeoUtils.java:100)
at org.apache.lucene.geo.Rectangle.<init>(Rectangle.java:88)
at org.apache.lucene.geo.Rectangle.fromPointDistance(Rectangle.java:178)
at org.apache.lucene.document.LatLonPointDistanceQuery.createWeight(LatLonPointDistanceQuery.java:83)
at org.apache.lucene.search.IndexSearcher.createWeight(IndexSearcher.java:978)
at org.apache.lucene.search.ConstantScoreQuery.createWeight(ConstantScoreQuery.java:146)
at org.apache.lucene.search.IndexSearcher.createWeight(IndexSearcher.java:978)
at org.apache.lucene.search.IndexSearcher.count(IndexSearcher.java:527)
# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF, assertions on; identical with -Ptests.asserts=false
# the arithmetic (stock 645d4fe14a20 core jar)
SloppyMath.sin(rd) = 0.9999705864309741
SloppyMath.cos(radLat) = 0.999970586430974
sloppy quotient = 1.0000000000000002 asin -> NaN
StrictMath quotient = 0.9999999999999999 asin(deg) -> 89.99999914622636newDistanceQuerydocumentsIllegalArgumentExceptiononly for a null field, invalid coordinates or an invalid radius (LatLonPoint.java:293-294); none holds here.- The non-pole branch appears intended to keep the
asinargument at most 1;SloppyMath.haversinMetersclamps the same hazard withasin(Math.min(1, ...))(SloppyMath.java:58). TestGeoUtilsbuilds boxes for random legal circles and expects a box every time.
The full argument228 words
- Contract.
LatLonPoint.newDistanceQuery: "@param radiusMeters maximum distance from the center in meters: must be non-negative and finite", "@throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid." (LatLonPoint.java:290-294); the same forLatLonDocValuesField.newSlowDistanceQuery(LatLonDocValuesField.java:204-208).fromPointDistancechecks only the centre (Rectangle.java:145-146): "Compute Bounding Box for a circle using WGS-84 parameters" (:142). - The guard shows the intent. The non-pole branch (
:156) appears intended to keepasin's argument at most 1 (no comment says so, but in exact arithmetic that is what the guard ensures); rounding breaks that at the boundary. - Lucene clamps the same hazard elsewhere.
SloppyMath.haversinMeters:asin(Math.min(1, Math.sqrt(sortKey * 0.5)))(SloppyMath.java:58). spatial3dTools.safeAcos: "Java acos yields a NAN if you take an arc-cos of an angle that's just a tiny bit greater than 1.0, so here's a more resilient version" (Tools.java:27-38).XYRectangle.fromPointDistancerounds its box up "to avoid numerical errors" (LUCENE-9243,XYRectangle.java:79-81). The 7 cm pad in this method (Rectangle.java:149-150, comment "LUCENE-7143") is the same kind of guard. - Tests.
TestGeoUtils.testRandomCircleToBBoxcallsfromPointDistanceon random legal circles with radii up to 50,000 km and expects a box every time (TestGeoUtils.java:34-51); it even carries "TODO: randomly quantize radius too, to provoke exact math errors?" (:49). No test expects an exception for a valid circle.
The devil's-advocate check179 words
The strongest case for "intended" (the study's intended-behaviour verifier and cross-model reviewers): SloppyMath trades accuracy for speed on purpose ("This is performance sensitive, check SloppySinBenchmark and RectangleBenchmark", SloppyMath.java:183); its asin is specified to return NaN above 1 and TestSloppyMath asserts asin(2) is NaN (TestSloppyMath.java:95); Circle is @lucene.experimental and says "For very large circles or circles near poles, consider using polygons instead" (Circle.java:33); the Rectangle class notes that rectangles near poles are distorted (Rectangle.java:63-64); and the witness needs nanometre-scale tuning.
Why it fails: SloppyMath keeps its own contract (one ulp is within "around 1E-15"), which is exactly why the caller must keep asin's argument in range, as haversinMeters does. The pole notes come from a docs-only change (#14534) and are accuracy advice, not a precondition that permits an exception. LatLonPoint.newDistanceQuery is not experimental and documents no such case. A Math.min leaves the speed trade-off untouched. The argument supports low priority, not intended behaviour; every verifier rated it weak. Verdict: not intended, a defect. Not security: a clean exception on one query.
Recommended A. Clamp the asin argument to 1 (one line)
Clamp the ratio: asin(Math.min(1, sin(radDistance) / cos(radLat))) at Rectangle.java:157, the idiom SloppyMath.haversinMeters already uses. It is conservative (asin(1) = pi/2 is at least the exact half-width) and the box only pre-filters, so hits do not change. With it the repro passes (assertions on and off), as do 433 geo and LatLon tests and the core module's static checks.
--- a/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java
@@ -154,7 +154,9 @@ public class Rectangle extends LatLonGeometry {
double maxLon;
if (minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS) {
- double deltaLon = asin(sin(radDistance) / cos(radLat));
+ // the quotient is <= 1 in exact arithmetic, but rounding can push it just above 1 (asin would
+ // return NaN); clamp it, as SloppyMath.haversinMeters does
+ double deltaLon = asin(Math.min(1, sin(radDistance) / cos(radLat)));
minLon = radLon - deltaLon;
if (minLon < MIN_LON_RADIANS) {
minLon += 2d * PI;All options and trade-offs3 options · 497 words
Fix A was validated through gradle in a scratch worktree of 645d4fe14a20. B is an alternative that was not run; C is not recommended.
Same idiom as SloppyMath.haversinMeters (SloppyMath.java:58). It covers every caller of fromPointDistance: distance and Circle queries, the distance predicate, distance sort, nearest-neighbour and distance-feature pruning.
--- a/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java
+++ b/lucene/core/src/java/org/apache/lucene/geo/Rectangle.java
@@ -154,7 +154,9 @@ public class Rectangle extends LatLonGeometry {
double maxLon;
if (minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS) {
- double deltaLon = asin(sin(radDistance) / cos(radLat));
+ // the quotient is <= 1 in exact arithmetic, but rounding can push it just above 1 (asin would
+ // return NaN); clamp it, as SloppyMath.haversinMeters does
+ double deltaLon = asin(Math.min(1, sin(radDistance) / cos(radLat)));
minLon = radLon - deltaLon;
if (minLon < MIN_LON_RADIANS) {
minLon += 2d * PI;Trade-offs
- Conservative:
SloppyMath.asin(1)is exactly pi/2 (SloppyMath.java:172-173), at least the exact half-width (89.99999914622636 degrees here, 90.0 with the clamp), so the box grows by a hair and still contains the circle. - The box is only a pre-filter;
LatLonPointDistanceQuerythen checks each candidate's distance (LatLonPointDistanceQuery.java:115,:187), so hits do not change. - Cost: one
Math.minper box (not measured). Low-risk rather than proven risk-free: no regression in the validation suites listed under "What the checks validated", which are not the full test suite.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, seedDEADBEEF, 2026-11-01: without the fix the repro fails with assertions on and off; with it, it passes with assertions on and off. ./gradlew :lucene:core:test --tests 'org.apache.lucene.geo.*' --tests 'org.apache.lucene.document.TestLatLon*' --tests 'org.apache.lucene.search.TestLatLon*' --tests org.apache.lucene.search.TestNearest: 39 classes (all 17geotest classes includingTestGeoUtils,TestCircle,TestCircle2D; 19TestLatLon*document classes;TestLatLonPointQueries,TestLatLonDocValuesQueries,TestNearest), 433 tests, 0 failures, 26 skipped../gradlew :lucene:core:check -x testwith the fix and the repro as a new test file: passes (google-java-format, ecj lint, forbidden APIs). The full Lucene test suite was not run.- Earlier: with the clamp shadowed into the stock jars, the finder's and the independent verifier's tests (including a 49M-pair scan) pass, the history probe finds 0 failures, and eight upstream geo test classes pass as without the fix (116 tests).
Compute q = sin(radDistance) / cos(radLat); if q >= 1, use the full longitude range as the pole branch does (Rectangle.java:166-172).
Trade-offs
- Also safe, but the box spans every longitude instead of a half-width of 90 degrees, so a few more candidates are checked.
- Slightly more code than A.
Replace the SloppyMath calls with StrictMath or Math.
Trade-offs
Math.sin/Math.cosalso push the same ratio above 1 (450 of 1,280,000 near-pole cases on Linux aarch64), so a clamp is still needed.- The sloppy versions are kept here for speed (
SloppyMath.java:181-183).
A service that forwards user-supplied distance-query parameters returns an error for such a request instead of results; other requests and the index are not affected.
Semi-formal: while checking SloppyMath's documented error bounds, an AI tool found the table switches of SloppyMath.cos where a larger angle gets a larger cosine, evaluated the box's asin argument for every input pair in a bounded window around each switch, and replayed the first hit at each switch through Rectangle.fromPointDistance. SMT solvers timed out on the same question.
The method in full194 words
Method: bounded-exhaustive translation validation of the stock main jar (study unit on SloppyMath and geo distance error bounds, main a9779ef3c193, 2026-10-27).
- Step 1: a scan of every
SloppyMath.costable switch in [0, pi] for the two doubles on either side, looking for a cosine that goes up as the angle grows: 12 such switches. - Step 2: around each of the 512 switches in [0, pi/2], an exhaustive window scan of the ratio
sin(radDistance) / cos(radLat)atRectangle.java:157for circles that stay in the non-pole branch, evaluated with the same publicSloppyMath.sin/SloppyMath.coscalls; any ratio above 1 is a witness, and the first one at each switch was replayed throughRectangle.fromPointDistance. The south side was not scanned (it is the mirror image). - SMT encodings of the same branch (QF_FP, with Z3 4.12.2 and cvc5 1.4.2) returned unknown or timed out, so the witness comes from the executed scans, not from a solver model.
- The finder then wrote a public-API JUnit with controls and the clamp check; an independent verifier found its own witness with its own scan (487 failing legal pairs in 49M). The one-test minimal repro was reduced from them.
No prior report found in GitHub issues, PRs and commits, the JIRA archive, the Lucene mailing lists, downstream projects or the web (searched 2026-10-27, refreshed 2026-11-01). Closest: #8198 (the same box too small for tiny radii) and #8178 (created haversinMeters with the clamp haversin had since 2013; not the box). Not fixed upstream as of 2026-10-30.
Searches4 searches
Closest items, none a report of this bug: #8198 (LUCENE-7143, open: the same function's box was too small for tiny radii, the origin of the 7 cm pad; a different defect and line); #14516 (merged 2025: moved sloppySin into SloppyMath.sin unchanged; review about speed and accuracy); #7966 (LUCENE-6908, the introducing change; its 19 comments are about pole-crossing relations); #8178 (LUCENE-7123: created haversinMeters, keeping the asin(Math.min(1, ...)) clamp haversin had since LUCENE-5258 in 2013; it did not clamp the box); elastic/elasticsearch#55851 and #56755 (the same message from an unrelated encodeLongitude call in a downstream test). No open PR touches Rectangle.java, SloppyMath.java or Circle2D.java (2026-10-27). Gaps: keyword search only; GitHub search does not index PR diffs (the open-PR file scan covers that); private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits | 2026-10-27 | two scripted prior-report searches (45 and 51 queries: fromPointDistance, circleToBBox, "invalid longitude" NaN, asin NaN, SloppyMath monotonic, sloppySin, 1.0000000000000002, ...); 25 manual searches (open and closed, migrated JIRA issues); the changed files of all 408 open PRs; git log -S on the formula and the clamp | no report; #8198, #14516, #7966 and #8178 are context only |
| JIRA archive, mailing lists, downstream, web | 2026-10-27 | 6 JIRA queries; 33 searches of dev@, java-user@, issues@, builds@ and commits@; 10 Elasticsearch, OpenSearch and Solr searches; 3 GitHub-wide searches; 7 web searches | no report; elastic/elasticsearch#55851 and #56755 show the same message from unrelated code |
| apache/lucene issues (refresh) | 2026-11-01 | fromPointDistance and "invalid longitude NaN" | only the unrelated #8267 and #8207; nothing new |
| Upstream code | 2026-11-01 | Rectangle.java on upstream main 2f424fd428da (2026-10-30) | line 157 still unclamped |
Introduced by LUCENE-6908 (92b659ab9e1a / 3a189ff68dec, 2015-12-07, first released in 5.5.0), which switched this formula from StrictMath to SloppyMath trig. There never was a clamp here; the failure came in with that change (bounded scans of 5.4.1 found no failure); 18 releases from 5.5.0 to 10.6.0 and main were executed and all fail.
Versions tested6 rows
Introduced by LUCENE-6908 (migrated issue #7966, Nick Knize, JIRA patch era): 92b659ab9e1a on trunk, 3a189ff68dec on branch_5x, both 2015-12-07. Its diff replaces StrictMath.asin(StrictMath.sin(radDistance) / StrictMath.cos(radLat)) with the SloppyMath versions and adds sin(a) = cos(a - PIO2). The unclamped ratio itself is older (d369057766ae, LUCENE-6780, 2015-10, sandbox, StrictMath), but that form threw for no input in the scans.
Later commits moved the code and kept the formula: LUCENE-6997 (to lucene-spatial), LUCENE-7123 (GeoUtils.sloppySin), LUCENE-7147 (the 7 cm pad, 6.1.0; it only moves the witness radius), LUCENE-7163 (core Rectangle.fromPointDistance, 6.1.0), LUCENE-7314 (LatLonPoint in core, 7.5.0), #14516 (renamed to SloppyMath.sin, 10.3.0). SloppyMath.cos and asin have the same bodies from 5.5.0 to main.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | the minimal repro fails through gradle and standalone, assertions on and off; upstream main 2f424fd428da still has the line |
| 10.6.0 | affected | the minimal repro fails, assertions on and off |
| 9.12.3, 10.0.0, 10.2.2, 10.3.0, 10.5.2 | affected | the study's JUnit repro fails 3 of 3 |
| 5.5.0, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 6.6.6, 7.4.0, 7.5.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0 | affected | a version-adapted public-API probe finds 17,844 throwing legal circles in each line (2,359,296 scanned); 5.5.x and 6.0.x need other witness radii |
| 5.4.1 | StrictMath trig | no failure found: 0 throws in a bounded scan of 100.7M circles next to the knots (sandbox code only) |
| main with the clamp | fix | the repro and the probe pass |
Confirmed by the finder's controls, independent verifiers, an adjudication, a credibility audit and registry re-runs (assertions on and off, interpreter with SVE off), and by this doc's gradle run on main. Cross-model reviews (OpenAI GPT-5.6) could only read code; both found the intended-behaviour case weak. The clamp passed every check it was given.
Checks and verdicts346 words
The study's checks: finder, a read-only cross-model refute pass, the independent reproduction, a read-only cross-model review, the intended-behaviour check, adjudication; then registry re-runs, a credibility audit, the minimal repro and this doc's gradle run on 645d4fe14a20.
| Check | Verdict | What it found |
|---|---|---|
| finder | fails 3 of 3 on main, 10.5.2, 9.12.3 | Public-API JUnit with controls (radius +-1 m, centre +-1e-9 degree, the exact knot: all pass); passes with the clamp. |
| Independent reproducer | real | Own scan and own witness (centre 0.4394531249999943): 5 of 6 tests fail on main, 10.5.2 and 9.12.3; 487 failing legal pairs among 49M; exact arithmetic puts the padded circle 4.7e-8 m inside the non-pole branch; 6 of 6 pass with its own clamp. |
| Intended-behaviour devil's advocate | real, high | Built the strongest "intended" case and rejected it; 0 of 2,000,000 random circles; Math.sin/Math.cos overshoot in 450 of 1,280,000 near-pole cases; corrected the entry points. |
| cross-model passes (OpenAI GPT-5.6, read-only) | real (medium); not real (low, procedural) | Could not run code. The intended lens agreed on the root cause and rated the intended case weak; the refute pass's "not real" is procedural ("this is not a substantive refutation"), and of the evidence items it listed one is neutral, one (the experimental Circle and its near-pole advice) supports not real, and the rest support real. |
| adjudication | confirmed, high | Re-ran the independent reproduction's test with a fresh seed (5 of 6 fail, 6 of 6 with the clamp) and narrowed the claims: entry points, five knots, aarch64 only. |
| registry re-runs and audit | still reproduces; confirmed | Fails with -ea, -da and -Xint -XX:UseSVE=0; eight upstream geo test classes pass with the fix as without (116 tests); the audit's own probe and rarity scan agree. |
| this doc's gradle run | fails stock, passes fixed | Gradle in a scratch worktree of 645d4fe14a20: the repro fails with assertions on and off and passes with fix A; 39 geo and LatLon test classes (433 tests) and :lucene:core:check -x test pass with it. The repro also fails on 10.6.0. |
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 494 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,435 words
- Registry title
- Rectangle.fromPointDistance throws 'invalid longitude NaN' for some legal circles whose edge stops just short of a pole, so LatLon distance queries on them fail with IllegalArgumentException
- Area
- core / geo (Rectangle.fromPointDistance circle bounding box, used by LatLon distance and Circle queries)
- Priority
- Low, rank 1135: It is a loud IllegalArgumentException for contrived, ulp-precise legal circles in core LatLon distance queries (0 of 2M random circles hit it), the same valid-input-rejected boundary profile as LFV-219, in core code rather than spatial3d like LFV-742.
- Impact class
- crash
- Impact note
- Hits applications that run a LatLon circle query with the default configuration (LatLonPoint.newDistanceQuery and LatLonPoint.newGeometryQuery(INTERSECTS, Circle) fail at search time in LatLonPointDistanceQuery.createWeight; LatLonDocValuesField.newSlowDistanceQuery and LatLonShape.newGeometryQuery with a Circle fail when the query is built), but only for contrived legal circles: a centre latitude about 1e-14 to 1e-13 degree below one of five dyadic latitudes (0.44, 0.79, 2.02, 2.55 or 12.22 degrees, or their southern mirrors) together with a radius within about 1e-7 m of 'distance to the pole minus 7 cm'; the valid query is then rejected with IllegalArgumentException 'invalid longitude NaN' (no wrong hits, nothing corrupted; 0 of 2,000,000 random legal circles hit it, and a centre decoded from an indexed point cannot), in every release from 5.5.0 to 10.6.0 and on main [exec, -ea and -da].
- Summary
- You run LatLonPoint.newDistanceQuery("p", 0.43945312499999994, 0, 9958692.080819247), a valid centre and a finite, non-negative radius whose circle stops about 7 cm short of the north pole, and expect it to match the two indexed points inside the circle. Instead IndexSearcher.count throws IllegalArgumentException 'invalid longitude NaN; must be between -180.0 and 180.0' from Rectangle.fromPointDistance, and LatLonDocValuesField.newSlowDistanceQuery throws the same when it is built. The bounding box computes asin(sin(r) / cos(lat)) without a clamp, and SloppyMath.cos, which is not monotone across its lookup-table switches, makes the quotient 1.0000000000000002 although its exact value is below 1, so asin returns NaN.
- Introduced
- 92b659ab9e1a (trunk) / 3a189ff68dec (branch_5x, shipped in 5.5.0): LUCENE-6908 (migrated as #7966) by nknize, which replaced StrictMath.asin/sin/cos with SloppyMath in the non-pole branch deltaLon = asin(sin(radDistance) / cos(radLat)) and added sin(a) = cos(a - pi/2) [read: git log -S and the blame chain of Rectangle.java:157 in the history clone; exec: 5.4.1 passes, 5.5.0 fails]. The unclamped quotient itself is older (d369057766ae, LUCENE-6780, 2015-10-21, sandbox, StrictMath trig), but that form threw for no input in the bounded scans. Later commits moved or renamed the code and kept the formula: 665041c52f08 (LUCENE-6997, to lucene-spatial), ce3114233bdc (LUCENE-7123, GeoUtils.sloppySin), 6c219e99e4b / 951563aa427 (LUCENE-7163, core Rectangle.fromPointDistance, 6.1.0) and 3fcc87752a42 (#14516, renamed to SloppyMath.sin, 10.3.0); 045659533cd (LUCENE-7147, the 7 cm pad, 6.1.0) only moved the witness radius. Never clamped: not a regression (2015-12-07)
- First affected version
- 5.5.0 [exec: lucene-spatial GeoUtils.circleToBBox and the GeoPointDistanceQuery constructor throw 'invalid minLon NaN' for 17,844 of 2,359,296 scanned legal circles next to the knots; 5.4.1, StrictMath trig in the sandbox module only, 0 of 100,663,296]. The API named in the title, core Rectangle.fromPointDistance, exists from 6.1.0; LatLonPoint.newDistanceQuery from 6.0.0 (sandbox module) and in core from 7.5.0
- Affected versions
- Every release from 5.5.0 to 10.6.0, and main a9779ef3c193; never fixed (10.6.0: the one-test minimal repro fails on the release jars, SHA-1 checked against Maven Central, -ea and -da, 2026-11-01 [exec]). [exec] 17 releases and main fail, 5.4.1 passes: the version-adapted public-API probe throws for 17,844 legal circles in each line on 5.5.0, 5.5.5, 6.0.0, 6.0.1, 6.1.0, 6.6.6, 7.4.0, 7.5.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 10.2.2, 10.5.2 and main (-da also on 5.5.0, 8.11.4 and main); the finder's JUnit fails 3 of 3 on 9.12.3, 10.0.0, 10.2.2, 10.3.0, 10.5.2 (-ea and -da) and main (-ea and -da); the independent reproduction's JUnit fails 5 of 6 on 9.12.3, 10.5.2 and main; the independent reproduction's main() probe fails on 8.11.4 and 7.7.3. The exact repro radius 9958692.080819247 m applies from 6.1.0 (5.5.x and 6.0.x converted radius to angle differently, so the probe finds other radii there). [read] every release tag 5.5.0..10.5.2 in the history clone contains the unclamped line; it is unchanged on upstream main f7bcc0abd816, branch_10x and branch_10_5 (2026-10-27), and no open PR touches Rectangle.java, SloppyMath.java or Circle2D.java. All runs with JDK 25.0.3 on aarch64; the code is pure Java double arithmetic, and a re-run in the interpreter with SVE off (-Xint -XX:UseSVE=0) fails the same way; x86 was not run
- 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
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (2026-10-27, read-only): two scripted prior-report searches (the finder's 45 queries, 51 hits; a second search's 51 queries over GitHub issues/PRs, commits, downstream repositories, dev@, java-user@, issues@ and commits@, JIRA and the web, 60 hits; 0 high overlap), 25 manual apache/lucene searches (issues, PRs, migrated JIRA issues), 10 Elasticsearch, OpenSearch, Solr and lucene-solr searches, 3 searches of all of GitHub, 6 JIRA and 33 mailing-list searches (builds@ included), 7 web searches, the changed files of all 408 open apache/lucene PRs and the upstream state of Rectangle.java (line 157 unchanged on main f7bcc0abd816, branch_10x and branch_10_5); the intended-behaviour check's own searches agree. 23 hits read, none the same bug. Closest items, context not reports: #8198 (LUCENE-7143, open: the same function's box is too small for tiny radii, the origin of the 7 cm pad; a different defect, line and fix); #14516 (moved sloppySin into SloppyMath.sin unchanged; its review discusses speed and accuracy only); #7966 (LUCENE-6908, the introducing change, about pole-crossing relations); #8178 (LUCENE-7123, added the asin clamp to haversinMeters but not to the box); elastic/elasticsearch#55851 and #56755 (the same message from an unrelated encodeLongitude call in a downstream test). Gaps: keyword search only, GitHub search rate limits, one open PR's file list capped by the API, private channels and security@ not visible
- Lucene commit tested
- main
645d4fe14a20: the minimal repro with assertions on and off through gradle and standalone; the clamp fix in a scratch worktree with the geo and LatLon test suites and:lucene:core:check -x test; the arithmetic probe on the stock core jar. Release jars (SHA-1-checked): the minimal repro on 10.6.0; the study's JUnit repros and version-adapted probes on 5.4.1 (no failure found in a bounded scan) and on 17 releases from 5.5.0 to 10.5.2, and on maina9779ef3c193, whose geo,SloppyMathand LatLon sources are identical to645d4fe14a20. Upstream main2f424fd428da(2026-10-30) still has the unclamped line (read).
- Finding record
lucene/core/src/java/org/apache/lucene/geo/Rectangle.java:142-178 (at a9779ef3c193) (javadoc 'Compute Bounding Box for a circle using WGS-84 parameters'; :150 the 7 cm pad '(radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS' (LUCENE-7143); :156 the non-pole guard 'minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS'; :157 'double deltaLon = asin(sin(radDistance) / cos(radLat));' with no clamp; :174-178 new Rectangle(...)), :84-90 (constructor: checkLatitude and checkLongitude of every bound, which rejects NaN), :63-64 (class note: rectangles near poles are distorted, about accuracy)lucene/core/src/java/org/apache/lucene/util/SloppyMath.java:58 (at a9779ef3c193) (haversinMeters: 'asin(Math.min(1, Math.sqrt(sortKey * 0.5)))', the clamp idiom), :80 and :92-114 (cos, 'Error is around 1E-15', table plus Taylor polynomial), :119-125 and :136-178 (asin: NaN when the absolute argument is greater than 1), :189-205 (sin(a) = cos(a - pi/2), 'Error is around 1E-12')lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:284-299 (at a9779ef3c193) (newDistanceQuery: radius 'must be non-negative and finite'; '@throws IllegalArgumentException if field is null, location has invalid coordinates, or radius is invalid')lucene/core/src/java/org/apache/lucene/document/LatLonDocValuesField.java:195-209 (at a9779ef3c193) (newSlowDistanceQuery, the same @throws at :207)lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java:83 (at a9779ef3c193) (createWeight builds the box) and lucene/core/src/java/org/apache/lucene/geo/Circle2D.java:412-418 (HaversinDistance builds the box at :417 when a Circle query is constructed); lucene/core/src/java/org/apache/lucene/geo/Circle.java:33 (@lucene.experimental note 'For very large circles or circles near poles, consider using polygons instead', accuracy advice from the docs-only #14534)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/geo/Rectangle.java:31-33, :88, :142-179 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/util/SloppyMath.java:80, :92-114, :100, :124-125, :171-176, :203-205, :222 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/document/LatLonPointDistanceQuery.java:57-71, :83; document/LatLonPoint.java:333-334; geo/Circle2D.java:417; geo/GeoUtils.java:98-100 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/geo/GeoEncodingUtils.java:174; document/LatLonPointDistanceComparator.java:94; document/NearestNeighbor.java:131; document/LatLonPointDistanceFeatureQuery.java:375 (at 645d4fe14a20)
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/document/LatLonPoint.java:290-294; document/LatLonDocValuesField.java:204-208; geo/Rectangle.java:63-64, :142, :145-146, :149-150, :156; util/SloppyMath.java:58, :183; geo/Circle.java:33, :47; geo/XYRectangle.java:79-81 (at 645d4fe14a20)lucene/spatial3d/src/java/org/apache/lucene/spatial3d/geom/Tools.java:27-38 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java:34-51, :49; lucene/core/src/test/org/apache/lucene/util/TestSloppyMath.java:95 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/util/SloppyMath.java:58, :172-173, :181-183; document/LatLonPointDistanceQuery.java:115, :187; geo/Rectangle.java:166-172 (at 645d4fe14a20)
- TestDistanceQueryNearPoleNaN.java (JUnit, one test):
repro/LFV-771/src/org/apache/lucene/document/TestDistanceQueryNearPoleNaN.java(49 lines)
Made in parallel with this doc from the mechanism in the brief; the PNG is the summary frame.
assets/LFV-771/bug.png(494 KB)
