All findings · Lucene finding · LFV-327 · mathutil-inverse-hyperbolic-range-collapse

MathUtil.asinh, acosh and atanh return Infinity, 0 or the wrong infinity for very large, tiny or large negative arguments

MathUtil.asinh(1e200) returns Infinity instead of 461.21, asinh(1e-17) and atanh(1e-17) return 0, and acosh(-1e8) returns -Infinity although its javadoc promises NaN. The expression functions asinh, acosh and atanh call these methods and give the same values.

Summary

Priority
Low, rank 867MathUtil and the expression functions asinh, acosh and atanh collapse only for extreme magnitudes or near-zero values, a boundary-value error.
Impact
silent wrong results
Since
4.6.0 (LUCENE-5207, 2013)introduced 2013-09-13 · over 13 years
Affected
4.6.0 to 10.6.0, and main
Status
documented
Found by
semi-formal: code reading, then SMT bounds over IEEE doubles and a JUnit

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

MathUtil supplies the inverse hyperbolic functions that java.lang.Math lacks. It uses the schoolbook formulas, which break at both ends of the double range.

Inverse hyperbolic functions and the limits of a double

  1. asinh, acosh and atanh undo sinh, cosh and tanh. java.lang.Math has sinh, cosh and tanh but no inverses, so Lucene's MathUtil provides them, and the expressions module offers them as the default functions asinh, acosh and atanh.
  2. They grow very slowly: asinh(1e200) is about 461, and even at the largest double, about 1.8e308, asinh is only about 710.48. acosh is defined only from 1 up; below 1 the answer is NaN.
  3. A double cannot hold anything above about 1.8e308; a bigger result becomes Infinity. So squaring any number of 2^512 (about 1.34e154) or more gives Infinity.
  4. Near 1, doubles are about 2.2e-16 apart, so adding a number below about 1.1e-16 to 1 gives exactly 1.
Diagram of the bug: MathUtil.asinh computes log(sqrt(a*a + 1) + a). For a = 1e200 the square a*a is 1e400, above the largest double (about 1.8e308), so it becomes Infinity and the result is Infinity instead of 461.21.
How MathUtil.asinh(1e200) ends up as Infinity: the intermediate square 1e200 * 1e200 overflows, although the true result is 461.21. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You call MathUtil.asinh(1e200), or sort documents by the expression asinh(x) where a document's x is 1e200.
  2. You expect 461.2101657793691, that is ln(2e200). The inverse hyperbolic sine of any finite double is finite: at most 710.48, at Double.MAX_VALUE.
  3. You get Infinity. The code computes log(sqrt(a*a + 1) + a), and a*a overflows for every |a| of 2^512 (about 1.34e154) or more: just below 2^512 asinh is 355.58, at 2^512 it is Infinity. acosh(1e200) overflows the same way.
  4. At the small end, the log's argument sqrt(a*a + 1) + a is 1 + a, and 1 + a rounds to exactly 1, so asinh(1e-17) and atanh(1e-17) return 0.0 instead of 1e-17. And acosh never applies its documented rule "less than 1 gives NaN": acosh(-1e8) is -Infinity, acosh(-1e200) is Infinity.
  5. No error. Sorting by asinh(x) gives x = 1e160 and x = 1e200 the same key, Infinity, and _score + 0.000001 * asinh(x) ranks a document with x = 1e200 above better text matches. Ordinary arguments such as asinh(2.5) are correct.

Who is affected

Direct callers of MathUtil.asinh, acosh or atanh, and users of the optional expressions module whose expressions apply them to literals, doc values or _score. It needs magnitudes of 1.34e154 or more, below 1.1e-16, or acosh of about -6.7e7 or less: plausible paths are sentinels such as Double.MAX_VALUE and malformed or extreme data. An accidental hit is unlikely; low priority.

Mechanism, terms and numbers701 words

MathUtil.asinh, acosh and atanh evaluate the textbook formulas log(sqrt(a*a + 1) + a), log(sqrt(a*a - 1) + a) and 0.5 * log((1 + a) / (1 - a)) with no range reduction and no domain check. For every finite |a| of 2^512 (about 1.34e154) or more, a*a overflows, so asinh(1e200) and acosh(1e200) return Infinity instead of 461.2101657793691. For tiny arguments the log's argument (1 + a for asinh, (1 + a) / (1 - a) for atanh) rounds to exactly 1, so asinh(1e-17) and atanh(1e-17) return 0.0 instead of 1e-17, and just above that range asinh keeps only about 8 correct digits at 1e-8. acosh never checks its documented rule "If the argument is less than 1, then the result is NaN": acosh(-1e8) returns -Infinity and acosh(-1e200) Infinity. The default expression functions asinh, acosh and atanh are bound straight to these methods, so expression scores, sort keys and filters come out wrong with no error; outside tests, no other Lucene code calls them. Every release since 4.6.0 (2013) is affected; the inputs are extreme (plausible paths are sentinels and malformed or extreme data), so the realistic exposure looks low. A range-reduced implementation with an explicit NaN for acosh below 1 fixes all of it.

The bug (main 645d4fe14a20)

  1. asinh (MathUtil.java:102-113) strips the sign and returns sign * Math.log(Math.sqrt(a * a + 1.0d) + a) (:112). For every finite a >= 2^512, a * a is +Infinity, and sqrt and log keep it: the result is Infinity (-Infinity for a negative argument). For 0 < a <= 2^-53, the log's argument Math.sqrt(a * a + 1.0d) + a is 1.0 + a, which rounds to 1, and log(1) is 0. (1 + a * a is already exactly 1 for every a up to about 1.05e-8, where asinh is still nonzero, so the value is lost at 1 + a.) Both thresholds are exact: SMT over IEEE binary64 (cvc5 and z3, QF_FP) proves that the log argument is Infinity for every finite a >= 2^512 and not infinite on [1, 2^512), and that it is exactly 1 on (0, 2^-53] and not 1 above. Just above the small threshold the relative error is about 1.1e-16 / a: 6.7e7 ulp at 1e-8, 237 ulp at 1e-3.
  2. acosh (:127-129) is one line, return Math.log(Math.sqrt(a * a - 1.0d) + a); (:128), with no check of its documented domain (:124). Large positive arguments overflow like asinh. Negative arguments reach NaN only by accident (for -10, sqrt(99) - 10 is negative, and log of a negative number is NaN). From about -6.7e7 (-2^26) down, rounding makes sqrt(a * a - 1) + a come out 0 or negative (from about -1.34e8 on, a * a - 1 rounds to a * a): between about -1.34e8 and -6.7e7 the result is -Infinity for some arguments and NaN for others, from about -1.34e8 (-2^27) to -2^512 it is -Infinity, and from -2^512 down a * a overflows and the result is Infinity. No negative argument gives a finite result.
  3. atanh (:144-154) returns mult * Math.log((1.0d + a) / (1.0d - a)) (:153). For 0 < |a| <= 2^-54 (about 5.6e-17) both 1 + a and 1 - a round to 1, so the result is 0. There is no overflow case (the domain is |a| <= 1).
  4. Expressions. JavascriptCompiler.properties:23, :25, :28 bind acosh, asinh and atanh to these methods (while cosh, sinh and tanh go to java.lang.Math, :31, :43, :46); compile(String) uses this default function set (JavascriptCompiler.java:154-155), and the package javadoc lists them (js/package-info.java:31-32). Outside tests, nothing else in Lucene calls them.

The numbers (stock main 645d4fe14a20; the same on 10.6.0)

callcorrect (correctly rounded)main
asinh(1e200), acosh(1e200), expression asinh(1e200)461.2101657793691Infinity
asinh(nextDown(2^512)) / asinh(2^512)355.58450362725193 (both)355.58450362725193 / Infinity
asinh(Double.MAX_VALUE)about 710.476Infinity
asinh(1e-17), atanh(1e-17)1.0E-170.0
asinh(1e-8)1.0E-89.999999889225291E-9
acosh(-1e8), expression acosh(-1e8) / acosh(-1e200)NaN-Infinity / Infinity
controls: asinh(2.5), acosh(2.5), atanh(0.5), acosh(-10), acosh(-1e7)1.6472311463710958, 1.566799236972411, 0.5493061443340549, NaN, NaNthe same, except atanh(0.5) = 0.5493061443340548 (one ulp low; the fix gives the same)
Exposure and likelihood321 words
  • Who. Direct callers of the public static MathUtil.asinh/acosh/atanh (public final class MathUtil, no @lucene.internal), and users of the optional expressions module whose expressions call asinh, acosh or atanh with the default function set over literals or bound values such as doc values or _score. Outside tests, no Lucene code calls them.
  • Sort ties. Sorting by the expression asinh(x) over a DoubleDocValuesField gives the keys 346.0809111296668 Infinity Infinity for x = 1e150, 1e160 and 1e200, so two distinct documents tie although asinh is strictly increasing (correct: 346.08, 369.11, 461.21). Executed on main with assertions on and off, and on 10.5.2.
  • Scores. With FunctionScoreQuery and _score + 0.000001 * asinh(x) over a term query, the documents with x = 1e200 and 1e160 score Infinity and rank first and second, ahead of better text matches scoring 0.0498 and 0.0486; with correct math they score 0.0310 and 0.0309. Correct math caps the term at 710.48 times its weight.
  • Filters. FunctionMatchQuery with asinh(x) > 400 also matches x = 1e160 (true asinh 369.1), acosh(x) > 1 matches x = -1e200 (true NaN), and asinh(x) == 0 matches x = 1e-17.
  • Scope of these examples. The sort, score and filter results come from constructed public-API tests that show what can happen; no production impact was observed.
  • What it takes. Magnitudes of at least 1.34e154 (asinh, acosh), at most 1.1e-16 (asinh) or 5.6e-17 (atanh), or acosh of about -6.7e7 or less. These ranges look contrived for typical ranking inputs; sentinels and malformed or extreme data are plausible paths. The tiny-argument zero changes a result by at most about 1.11e-16 (asinh) or 5.6e-17 (atanh). No public report was found in the recorded searches. Registry likelihood contrived, priority low.
  • Not affected. Nothing is written to the index; there is no exception or hang, and no sign of resource exhaustion (30 million evaluations on collapse inputs took 111 ms in one local run; not a performance claim).

Minimal repro

One JUnit class, one method, one assertion, public API only: MathUtil.asinh(1e200) must be about 461.21. Pure arithmetic, so the seed does not matter; it fails with assertions on and off.

TestMathUtilAsinhLargeArgument.java (one test class)27 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.util;

import org.apache.lucene.tests.util.LuceneTestCase;

public class TestMathUtilAsinhLargeArgument extends LuceneTestCase {

  public void testAsinhOfLargeFiniteArgument() {
    // expected: ln(2e200) ~ 461.21; actual: Infinity, because 1e200 * 1e200 overflows
    assertEquals(461.2101657793691, MathUtil.asinh(1e200), 1e-9);
  }
}
Command
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.util.TestMathUtilAsinhLargeArgument -Ptests.seed=DEADBEEF
Expected
MathUtil.asinh(1e200) returns 461.2101657793691, that is ln(2e200).
Observed
expected:<461.2101657793691> but was:<Infinity>: the intermediate 1e200 * 1e200 overflowed.

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

More files, environment, full output217 words

The class sits in org.apache.lucene.util only to be next to TestMathUtil. It compiles on 9.1 and later (org.apache.lucene.tests.util.LuceneTestCase); on 9.0 and older the base class is org.apache.lucene.util.LuceneTestCase.

It shows the overflow only. The other symptoms (acosh overflow, the tiny-argument zeros, acosh of large negative arguments) are the executed values in the table above; a NaN assertion for acosh (for example assertTrue(Double.isNaN(MathUtil.acosh(-1e8))), not run as a test) would be a second test method with its own fix hunk.

Regression test: this same test, as written.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; 10.6.0, 9.12.3: fail; with the recommended fix: passes
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; IEEE double arithmetic)
Command (environment)
cp $REPRO/TestMathUtilAsinhLargeArgument.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.util.TestMathUtilAsinhLargeArgument -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false (the recorded runs added --offline --no-daemon)

Observed output

# gradle, scratch worktree of 645d4fe14a20, seed DEADBEEF, assertions on
TestMathUtilAsinhLargeArgument > testAsinhOfLargeFiniteArgument FAILED
    java.lang.AssertionError: expected:<461.2101657793691> but was:<Infinity>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:667FD9FA2387ECB5]:0)
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:555)
        at org.junit.Assert.assertEquals(Assert.java:685)
        at org.apache.lucene.util.TestMathUtilAsinhLargeArgument.testAsinhOfLargeFiniteArgument(TestMathUtilAsinhLargeArgument.java:25)

# assertions off (-Ptests.asserts=false): the same message
# standalone runner, seed DEADBEEF: 1 of 1 failed, same message, on main 645d4fe14a20 (-ea and -da), 10.6.0, 9.12.3
# with the recommended fix: the test passes (assertions on and off)

Why it looks unintended

  • The javadoc says the methods calculate the inverse hyperbolic sine, cosine and tangent; those are finite for every finite argument, yet asinh(1e200) is Infinity.
  • The acosh javadoc says "If the argument is less than 1, then the result is NaN"; acosh(-1e8) is -Infinity.
  • The 2013 formulas were never revised; no test or discussion limits their range.
The full argument236 words
  • The contract. "Calculates inverse hyperbolic sine / cosine / tangent of a double value" (MathUtil.java:91, :116, :132), followed by special cases: an infinite result for an infinite argument (asinh), for +Infinity (acosh) or for +-1 (atanh), and a zero for a zero argument (asinh, atanh) or for +1 (acosh) (:93-100, :118-125, :134-142). The true values here are finite and nonzero, so Infinity at 1e200 or 0 at 1e-17 is the loss of the whole value, not rounding. (The list has no "only"; the argument rests on what the functions are.)
  • The acosh rule. "If the argument is less than 1, then the result is NaN" (:124). TestMathUtil.testAcoshMethod asserts NaN for 0.9, 0, -0, -0.9, -1, -10 and -Infinity (TestMathUtil.java:105-121).
  • Monotonicity. asinh(nextDown(2^512)) is 355.58450362725193; the next double up gives Infinity.
  • History. The formula lines blame to c73f175c71ee (LUCENE-5207, 2013) at 645d4fe14a20; the migrated LUCENE-5207 discussion (#6271, 89 JIRA comments) never discusses the range or accuracy of these functions. The upstream tests stop at |x| = 1234567.89 (TestMathUtil.java:123-126, :146-153, :177-182; TestJavascriptFunction.java:54-57, :74-81, :108-113), so nothing pins the current behaviour.
  • Precedent. In LUCENE-4162 (#5234, 2012), about another public MathUtil method, Uwe Schindler wrote: "Otherwise this class must be hidden, we should not offer any public method to the users that can loop infinite without parameter checking." It was fixed.
The devil's-advocate check207 words

The strongest case for "intended" (a devil's-advocate verifier, the cross-model reviews and the adjudication): the util package javadoc says "APIs in this package are primarily internal and may change between releases" (util/package-info.java:35); JavascriptCompiler is @lucene.experimental (JavascriptCompiler.java:111); no ulp bound is promised, unlike java.lang.Math's functions; the upstream tests accept an absolute error of 1e-6 (1e-7 in TestJavascriptFunction) at moderate magnitudes; and the code does exactly what its author wrote.

Why it fails: the first two are about API stability, not permission to return wrong values. The package sentence was added on 2026-02-18 by a docs-only change (#15719), MathUtil itself has no @lucene.internal, and the expression functions are documented for users. No tolerance accepts Infinity for a finite argument, and the acosh NaN rule is explicit. Where the defence is moderate: the tiny-argument zero (an absolute error of at most about 1.11e-16, inside the tests' tolerance) and the graded precision loss above it; for those, a fix is an accuracy improvement rather than a bug fix. The independent reproducer, the devil's-advocate verifier and the intended-lens review said real; the refute-lens review's "not real" was procedural (it could not run code and agreed with the root cause); the adjudication confirmed with high confidence.

Suggested fix

Recommended Range reduction, log1p near 0, and NaN for acosh below 1

Use fdlibm-style range reduction: log(|a|) + ln 2 above 2^28, log1p forms near 0 (and near 1 for acosh), and an explicit NaN for acosh below 1 (+29/-3 lines). Validated: the repro passes with assertions on and off, and so do TestMathUtil, the whole expressions module and the core format, ECJ and forbidden-apis checks.

// asinh, after the sign is stripped
final double r;
if (a > 0x1p28) {
  r = Math.log(a) + LN2; // a * a may overflow
} else if (a > 2) {
  r = Math.log(2 * a + 1 / (Math.sqrt(a * a + 1) + a));
} else {
  final double t = a * a; // log1p avoids log(1 + tiny)
  r = Math.log1p(a + t / (1 + Math.sqrt(1 + t)));
}
return sign * r;

// acosh: if (!(a >= 1)) return Double.NaN; then the same three ranges
// atanh: return mult * Math.log1p(2 * a / (1 - a));
All options and trade-offs3 options · 608 words

The recommended fix was validated through gradle in a scratch worktree of 645d4fe14a20; the smaller guard-only option and documenting the limits were not built.

Range reduction, log1p near 0, and NaN for acosh below 1 (validated by the checks) (recommended)

For large arguments use log(a) + ln 2 (above 2^28, sqrt(a * a +- 1) equals a to double precision); between 2 and 2^28 an equivalent form without overflow; near 0 (asinh, atanh) and near 1 (acosh) Math.log1p forms that avoid the cancellation; acosh returns NaN for any argument below 1 or NaN. The same reductions as fdlibm.

// asinh, after the sign is stripped
final double r;
if (a > 0x1p28) {
  r = Math.log(a) + LN2; // a * a may overflow
} else if (a > 2) {
  r = Math.log(2 * a + 1 / (Math.sqrt(a * a + 1) + a));
} else {
  final double t = a * a; // log1p avoids log(1 + tiny)
  r = Math.log1p(a + t / (1 + Math.sqrt(1 + t)));
}
return sign * r;

// acosh: if (!(a >= 1)) return Double.NaN; then the same three ranges
// atanh: return mult * Math.log1p(2 * a / (1 - a));

Trade-offs

  • Last-bit changes. The formulas are rewritten for ordinary arguments too, so results can move by an ulp and an expression score with them; the upstream tests (absolute tolerance) still pass.
  • Cost. One or two extra comparisons, and Math.log1p instead of Math.log for |a| <= 2 (asinh, acosh) and in every atanh call. Not benchmarked.
  • Accuracy. Maximum error 1.04 ulp (asinh), 0.98 ulp (acosh) and 1.19 ulp (atanh) over 6,898 points covering every binade, against a 200-digit reference (a sample, not a proof of a global error bound), with 0 collapses (stock: 5,028, 1,682 and 3,342 collapses on the same points). fdlibm's split for atanh below 0.5 (0.5 * log1p(2a + 2a*a / (1 - a))) could bring atanh under 1 ulp; not built.
  • Design support. Elasticsearch's own 2026 implementations of these functions use the same reductions.

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31). Stock: the repro fails with assertions on and off. With the fix: it passes with assertions on and off.
  • The repro plus TestMathUtil (its acosh test asserts the NaN cases): 7 tests, 0 failures. :lucene:expressions:test, the whole module including TestJavascriptFunction: 13 suites, 170 tests, 0 failures.
  • :lucene:core checkGoogleJavaFormat, ecjLintMain, ecjLintTest (with the repro test in place), forbiddenApisMain, forbiddenApisTest: pass. The finder's original comment on the atanh line was 101 columns and failed the format check, so the diff shortens it; the code is unchanged.
  • Earlier: the finder's 7 tests, an independent reproducer's 12 tests (including the acosh NaN cases) and TestMathUtil plus TestJavascriptFunction (35 tests) pass with it; the value probe gives the correct value for every row of the table above.

Guard only the overflow and the acosh domain (not built)

if (a > 0x1p28) return sign * (Math.log(a) + LN2); in asinh (the same without the sign in acosh), and if (!(a >= 1)) return Double.NaN; first in acosh; the formulas stay as they are otherwise.

Trade-offs

Fixes the Infinity collapse and the documented NaN rule. It leaves every asinh result for |a| <= 2^28 and every acosh result for 1 <= a <= 2^28 bit-for-bit unchanged (below 1, only the wrong acosh results change, to NaN), but keeps the tiny-argument zeros and the graded precision loss.

Document the limits (not recommended)

State in the javadoc that the functions are accurate only for moderate arguments.

Trade-offs

Leaves Infinity for finite input, and contradicts the acosh special case unless that line changes too.

Downstream note

An application that binds untrusted numeric values into an expression using asinh or acosh lets a finite value of 1.34e154 or more score Infinity and outrank every real match; validation that rejects only Infinity and NaN does not stop it.

How it was found

Semi-formal. A hunt unit reviewing numeric utilities read the three formulas and suspected overflow and cancellation; SMT solving over exact IEEE double semantics then proved the exact thresholds, and a public-API JUnit on the stock jars confirmed them. An independent reproducer added the acosh NaN violation.

The method in full171 words
  • Code reading first. The hunt unit for numeric, string and version utilities chose the MathUtil methods an earlier study had not covered and read the formulas.
  • SMT. cvc5 1.4.2 and z3 4.12.2, QF_FP (exact IEEE binary64 semantics with round-to-nearest-even): 6 UNSAT queries prove the exact collapse regions over all doubles (the log argument is +Infinity for every finite a >= 2^512 and not infinite on [1, 2^512); it is exactly 1 for every a in (0, 2^-53] for asinh and (0, 2^-54] for atanh, and not 1 above), with 3 SAT vacuity probes; both solvers agree.
  • Measurement. An error table of the stock jar over 6,898 points covering every binade, against a 200-digit decimal reference.
  • JUnit. Public-API tests on the stock main jars, direct and through JavascriptCompiler: 7 of 7 fail with assertions on and off.
  • The acosh negative-argument symptom was found by the independent reproducer, who wrote their own tests from the claim only. So the label is semi-formal: the hypothesis came from reading, the solvers bounded it.

Prior reports, history and checks

Prior reports

No public report found: 88 logged queries on 2026-10-26 over apache/lucene issues and PRs (the migrated JIRA archive included), open PRs, JIRA, the mailing lists, downstream projects and the web. The bare terms asinh, acosh and atanh match no apache/lucene issue or PR; a recheck on 2026-10-31 found none either.

Searches5 searches

No upstream report or fix was found (searches cannot prove that none exists). The nearest items are the change that introduced the code, a 2012 fix of another MathUtil method, a speed study of the .NET port, and Elasticsearch's own range-reduced functions, none of which mentions this behaviour of Lucene's MathUtil.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene, migrated JIRA included)2026-10-26asinh, acosh, atanh alone; hyperbolic, "inverse hyperbolic", MathUtil, MathUtil.java, titles with MathUtil or JavascriptCompiler, log1p, fdlibm, StrictMath, expressions with Infinity, NaN or precision, symptom phrases0 hits for asinh, acosh and atanh; no relevant hit for the rest
Open PRs and commits2026-10-26changed files of all 411 open PRs; GitHub commit search; local history with git log -S on the three formula stringsno open PR touches MathUtil.java or the function bindings; the formulas were introduced once (2013) and never changed
JIRA, mailing lists, downstream projects, web2026-10-26LUCENE, SOLR and LUCENENET JIRA; dev@, java-user@, issues@ and commits@ (Lucene), the Lucene.NET and Solr lists; Lucene.NET, Elasticsearch, OpenSearch and Solr on GitHub; 12 web queriesno report; apache/lucenenet#297 measured only the speed of the ported functions; Elasticsearch #141128, #142184 and #142213 implement their own functions and never mention Lucene's MathUtil
Recheck (GitHub API)2026-10-31asinh, acosh, atanh, "MathUtil hyperbolic", "inverse hyperbolic", "asinh Infinity", "acosh NaN" in apache/lucene issues and PRs0 hits each
Closest related items (read)2026-10-26LUCENE-5207 / #6271 (89 JIRA comments), LUCENE-4162 / #5234#6271 introduced the code and never discusses range or accuracy; #5234 fixed the argument check of another MathUtil method

History

Introduced in 4.6.0 by LUCENE-5207 (c73f175c71ee, 2013), which added the expressions module and these three methods; the formulas never changed. Every release from 4.6.0 to 10.6.0 and main is affected, and it is not fixed upstream.

Versions tested6 rows

Introduced by c73f175c71ee (2013-09-13, "LUCENE-5207: initial patch from ryan", committed by Robert Muir, patch by Ryan Ernst), on the SVN feature branch that added the expressions module; it landed on 2013-09-15 as eecd78f37a12 on trunk (the lineage of 6.0.0 and later) and as 08df432f8ba2 on branch_4x (4.6.0 to 5.5.5), with identical MathUtil.java. SVN era, no pull request. Not a regression: the values were wrong from the start.

Upstream (2026-10-31): main 2f424fd428da, branch_10_6 and branch_9_12 still contain the three formula lines; the last commit on MathUtil.java in main (#14750, 2025-06-03) is already in 645d4fe14a20. branch_10x was decommissioned on 2026-10-29 (#16911).

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe minimal test fails (assertions on and off, gradle and the standalone runner); every wrong value of the table reproduced
10.6.0 affectedthe minimal test fails; the value probe gives identical values to main
9.12.3, 10.5.2 affectedthe minimal test fails on 9.12.3; an independent 12-test JUnit fails 10 of 12 on both (assertions on and off); the acosh negative-argument program fails on both
4.6.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4 affectedthe version-neutral program gives the same four wrong values; on 4.6.0 also through expressions and for acosh of negative arguments
4.5.1 API absentno MathUtil.asinh/acosh/atanh and no expressions module (compile error)
fix (scratch worktree of 645d4fe14a20) not affectedthe minimal test passes; TestMathUtil and the expressions module pass

Independent checks

An independent reproducer, a devil's-advocate verifier and a cross-model review said real; the adjudication confirmed with high confidence. A second cross-model review returned a procedural "not real" because, reading only, it could not run code, while agreeing with the root cause. A credibility audit lowered the likelihood to contrived.

Checks and verdicts361 words

Every executing check ran on stock builds of main (ea57360912ed, a9779ef3c193 or 645d4fe14a20, with identical files involved) or on releases.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (SMT, error table, JUnit) candidateSMT thresholds (6 UNSAT, 3 SAT probes, two solvers); 6,898-point error table; JUnit 7 of 7 fail on main with assertions on and off; a version-neutral program fails on 4.10.4 to 10.5.2; the fix passes 7 of 7 and 35 upstream tests.
Cross-model review, intended lens (OpenAI GPT-5.6, reading only) real (medium)Rated the intended-behaviour case moderate and rebutted it; a separate run re-ran the finder's test: 7 of 7 fail.
Independent reproducer real (high)Own 12-test JUnit from the claim only, a 150-digit reference and two controls: 10 of 12 fail on main, 10.5.2 and 9.12.3 with assertions on and off. Found the acosh negative-argument case and confirmed the thresholds; the fix passes 12 of 12.
Cross-model review, refute lens (OpenAI GPT-5.6, reading only) not real (low), proceduralCould not run any command, so "not reproduced" by construction; agreed with the root cause and that the inputs are legal, and noted the static evidence "otherwise strongly favors a genuine defect". Separate re-runs of the finder's test failed 7 of 7.
Devil's advocate realBuilt the intended-behaviour case: weak for Infinity, moderate for the tiny-argument zero. Own doc-values sort test fails on main and 10.5.2.
Adjudication confirmed (high)Own 7-test JUnit: 6 of 7 fail with assertions on and off; re-ran the independent reproduction's test: 10 of 12. Rated the acosh NaN rule and the Infinity collapse the strongest parts.
Follow-up and credibility audit reproduces; likelihood contrivedHistory runs (4.5.1 absent, 4.6.0 to main fail), a ranking and filter probe, the prior-art search (new) and re-runs; the audit's fresh-seed re-run and direct probe confirmed the thresholds and lowered the likelihood from rare to contrived.
This doc reproduced, fix validatedGradle on 645d4fe14a20; the 10.6.0 and 9.12.3 jars; the value probe on main, 10.6.0 and the fix; the recommended fix passes the repro, TestMathUtil, the expressions module and the core lint checks.

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,267 words

Finding record

LFV-327, snapshot of 2026-11-06

Registry title
MathUtil.asinh, acosh and atanh use the textbook log formulas without range reduction: asinh and acosh return Infinity for every finite |x| >= 2^512, asinh and atanh return 0 for tiny nonzero x, and acosh of a large negative x returns an infinity instead of the documented NaN, also through the expression functions asinh, acosh and atanh
Area
util / MathUtil inverse hyperbolic functions (asinh, acosh, atanh), reached through the expressions module's default functions asinh, acosh and atanh
Priority
Low, rank 867: MathUtil and the expression functions asinh, acosh and atanh collapse only for extreme magnitudes or near-zero values, a boundary-value error.
Impact class
silent wrong results
Impact note
Direct callers of MathUtil.asinh/acosh/atanh and users of the optional expressions module whose expressions call asinh, acosh or atanh (default function set, over literals or bound values such as doc values) get Infinity, 0 or an infinity of the wrong kind with no error when the argument is a finite |x| >= 2^512 (about 1.34e154; asinh, acosh), 0 < |x| <= 2^-53 (asinh) or 2^-54 (atanh), or x <= about -6.7e7 (acosh, documented NaN), which skews expression scores, sort keys (distinct documents tie at Infinity) and threshold filters at query time; no default code path calls these functions, the util package is described as 'primarily internal' and JavascriptCompiler is @lucene.experimental, every release since 4.6.0 is affected, and the adjudication and all verifiers rate the realistic exposure low.
Summary
You call MathUtil.asinh(1e200), or evaluate the expression asinh(x) for a document whose x is 1e200, and expect about 461.21, because the inverse hyperbolic sine of a finite number is finite (at most about 710.5). Instead you get Infinity, because the code computes log(sqrt(x*x + 1) + x) and x*x overflows for every |x| >= 2^512; acosh does the same, asinh(1e-17) and atanh(1e-17) return 0 instead of 1e-17 because 1 + x*x (or (1+x)/(1-x)) rounds to exactly 1, and acosh(-1e8) returns -Infinity (acosh(-1e200) +Infinity) although its javadoc says that any argument below 1 gives NaN.
Introduced
c73f175c71ee02d057819037105b4e55d4099567 ('LUCENE-5207: initial patch from ryan', committed by rmuir, patch by rjernst, on the SVN feature branch lucene5207, r1522767), landed on trunk as eecd78f37a128dac326294ebf550eae0bbb04434 (r1523462, lineage of 6.0.0 and later) and on branch_4x as 08df432f8ba20370ed18b8933603ceba4007437b (r1523470, lineage of 4.6.0-5.5.5), MathUtil.java identical in all three; JIRA LUCENE-5207 'lucene expressions module' (migrated as apache/lucene#6271), SVN era, no pull request. It added asinh/acosh/atanh together with the expressions module and bound them as expression functions. [read: git blame of the formula lines and git log -S on each of the three formula strings return only c73f175c71ee; the only later commit in these lines is 2d6ad2fee6d (LUCENE-9570, javadoc reformatting); exec: 4.5.1 lacks the API (compile error), 4.6.0 fails]; not a regression: the values have been wrong since the API was added (2013-09-13 (c73f175c71ee; trunk merge and branch_4x backport 2013-09-15))
First affected version
4.6.0 [exec] (released 2013-11-22; 4.5.1 has no MathUtil.asinh/acosh/atanh and no expressions module, compile error [exec])
Affected versions
every release from 4.6.0 through 10.5.2 (the latest 9.x 9.12.3 and the latest 10.x before 10.6.0, 10.5.2) and main ea57360912ed; not fixed upstream (upstream main e1c4184cb983, branch_10x, branch_10_6 and branch_9_12 still have the unguarded formulas, read 2026-10-26). [exec] the version-neutral main() MathUtilHyperbolicMain fails with identical values on 4.6.0 (-ea and -da), 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.12.3, 10.5.2 and main (-ea and -da); the expressions path fails on 4.6.0 (4.x Expression API) and main; the acosh negative-argument probe fails on 4.6.0, 9.12.3, 10.5.2 and main; the independent reproduction's JUnit fails 10 of 12 on main, 10.5.2 and 9.12.3 (-ea and -da). [read] blame and pickaxe show no change to the formula lines since c73f175c71ee, so the releases between the executed ones are covered by reading only; 10.6.0 (released 2026-10-29) fails too [exec: the minimal test and the value probe on the 10.6.0 jars, 2026-10-31]. All runs on JDK 25.0.3, aarch64 Linux, not each release's own JDK.
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: code reading; also reached by Z3/cvc5
Upstream and prior reports
none found (2026-10-26, follow-up prior-report record verdict new, 88 logged queries and reads): a scripted prior-report search over apache/lucene issues and PRs (the bare terms asinh, acosh and atanh return 0 items; hyperbolic, MathUtil, MathUtil.java and symptom phrases give no relevant hit), commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists and the JIRA archive; further GitHub API searches (log1p, fdlibm, StrictMath, JavascriptCompiler titles, expressions with Infinity, NaN or precision); a changed-file scan of all 411 open apache/lucene PRs (none touches MathUtil.java or JavascriptCompiler.properties); LUCENE, SOLR and LUCENENET JIRA searches; the Lucene.NET and Solr lists; 12 web queries; and a full read of the LUCENE-5207 discussion (#6271, 89 JIRA comments). Closest items, none a report: #6271 / LUCENE-5207 (introduced the code; nothing on range or accuracy), #5234 / LUCENE-4162 (the MathUtil.log argument check, another method), apache/lucenenet#297 (speed of the ported functions only; accuracy never came up), and Elasticsearch's own range-reduced asinh/acosh/atanh of 2026 (elastic/elasticsearch#141128, #142184, #142213; they never mention Lucene's MathUtil). Not fixed upstream (main e1c4184cb983, branch_10x, branch_10_6, branch_9_12). Gaps: GitHub's search rate limit cut a few restrictive queries (for example 'MathUtil.asinh', subsumed by the bare-term searches); private reports to security@lucene.apache.org are not visible.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails through gradle in a scratch worktree (assertions on and off) and through a standalone runner, which also fails it on 10.6.0 and 9.12.3; a value probe gives the same wrong values on main and 10.6.0. With the recommended fix the test passes (assertions on and off), and so do TestMathUtil, the whole expressions module and the core format, ECJ and forbidden-apis checks. Earlier runs: 4.5.1 (API absent) and 4.6.0 to 10.5.2 and main ea57360912ed (version-neutral program, identical values). The hunt, the verifiers and the adjudication ran main ea57360912ed, a refresh re-ran the repros on a9779ef3c193; the files involved are identical in all three.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/util/MathUtil.java:90-154 (javadoc special cases; formulas at :112, :128, :153)
  • lucene/core/src/test/org/apache/lucene/util/TestMathUtil.java:105-127 (acosh NaN asserted for 0.9, 0, -0, -0.9, -1, -10 and -Infinity; epsilon 1e-6; largest argument 1234567.89)
  • lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptCompiler.properties:23,25,28 (acosh, asinh, atanh bound to org.apache.lucene.util.MathUtil)
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/package-info.java:31-32 (asinh, acosh, atanh listed as expression functions); JavascriptCompiler.java:111 (@lucene.experimental), :155 (compile(String) uses DEFAULT_FUNCTIONS)
  • lucene/core/src/java/org/apache/lucene/util/package-info.java:35 ('APIs in this package are primarily internal and may change between releases')
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/util/MathUtil.java:90-154 (javadoc :91, :116, :124, :132; formulas :112, :128, :153) at 645d4fe14a20
  • lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptCompiler.properties:23, :25, :28, :31, :43, :46; lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:154-155, :866; lucene/expressions/src/java/org/apache/lucene/expressions/js/package-info.java:31-32
  • JDK 25 `javap java.lang.Math`: sinh, cosh, tanh, log1p, no asinh/acosh/atanh (run for this doc)
Who is affected
  • lucene/core/src/java/org/apache/lucene/util/MathUtil.java:22 (public final class, no @lucene.internal)
  • grep over lucene/ at 645d4fe14a20: asinh/acosh/atanh only in MathUtil.java, expressions js/package-info.java and JavascriptCompiler.properties outside tests (run for this doc)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/util/MathUtil.java:90-143 (javadoc); lucene/core/src/test/org/apache/lucene/util/TestMathUtil.java:105-126, :146-153, :177-182; lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptFunction.java:23, :54-57, :74-81, :108-113
  • https://github.com/apache/lucene/issues/5234 (LUCENE-4162 body, Uwe Schindler; read 2026-10-31)
History
  • a Lucene clone release tags releases/lucene/9.12.3 and releases/lucene/10.6.0 (MathUtil.java:112, :128, :153 unchanged)

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

Repro files

About the diagrams

Made in parallel with this doc from the minimal repro and the executed values.

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.