All findings · Lucene finding · LFV-197 · expressions-hex-octal-literal-overflow-numberformatexception

JavascriptCompiler: a hex or octal literal above Long.MAX_VALUE, such as 0x8000000000000000, throws NumberFormatException instead of ParseException

JavascriptCompiler.compile("0x8000000000000000"), a hex literal one above Long.MAX_VALUE, throws an unchecked NumberFormatException instead of the documented ParseException, so a catch (ParseException e) handler never runs. Any hex or octal literal of 2^63 or more does this, in every release since 4.6.0; the expression is still rejected, only with the wrong exception.

Summary

Priority
Low, rank 895Hex or octal expression literals above Long.MAX_VALUE throw NumberFormatException instead of ParseException, and only the error type is wrong.
Impact
wrong error
Since
4.6.0 (the module's first release, LUCENE-5207, 2013)introduced 2013-09-13 · over 13 years
Affected
4.6.0 to 10.6.0 and main
Status
documented
Found by
code reading while modelling the compiler's literals for an exhaustive check

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

Lucene expressions read hex and octal literals as signed 64-bit long values with Long.parseLong, whose maximum is 2^63 - 1. A larger literal makes it throw NumberFormatException. The compiler turns only its own wrapped ParseExceptions into ParseException and passes every other runtime exception to the caller unchanged.

Background in 3 steps

  1. The optional lucene-expressions module compiles a formula such as ln(popularity) + (flags & 0xff) into a class that computes a double per document, for scores, sort keys and filters.
  2. Hex and octal literals and the bitwise operators work on 64-bit long values: the module's tests check 0x1234ABCDEF = 78193085935.
  3. Bad expression text is reported as java.text.ParseException, a checked exception every compile method declares: 0y1234, 500EE and 500.5EE throw it in the module's tests.
Diagram of the minimal repro: compile("0x8000000000000000"). The grammar accepts the hex literal. Long.parseLong reads hex digits as a signed 64-bit long, whose largest value is 0x7fffffffffffffff, so it throws NumberFormatException. compileExpression turns only wrapped ParseExceptions into ParseException, so the caller gets the unchecked NumberFormatException instead of the documented ParseException.
0x7fffffffffffffff is the largest signed 64-bit value and compiles; one more, 0x8000000000000000, overflows Long.parseLong, and its NumberFormatException reaches the caller instead of the documented ParseException. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Compile 0x7fffffffffffffff with JavascriptCompiler.compile. It works and evaluates to 9.223372036854776E18: Lucene reads hex and octal literals as signed 64-bit long values, and this is the largest one.
  2. Compile 0x8000000000000000, one more (2^63), or a 64-bit mask such as x & 0xffffffffffffffff. The grammar accepts hex literals of any length.
  3. You expect it to compile, or to fail with ParseException: every compile method documents @throws ParseException on failure to compile, and other bad literals such as 0y1234 do throw it.
  4. You get java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16. Long.parseLong cannot hold the value, nothing catches its exception, and compile passes it to the caller unchanged.
  5. Octal literals of 2^63 or more (01000000000000000000000) fail the same way; decimal literals are not affected. Nothing is computed wrongly: only the exception type is wrong.

Who is affected

Users of the optional expressions module, any compile overload, default settings, whose expression contains a hex literal above 0x7fffffffffffffff or an octal one above 0777777777777777777777, typically a 64-bit mask such as 0xffffffffffffffff. Code that catches ParseException to report a bad expression gets an unchecked exception with no position instead.

Mechanism, terms and numbers460 words

Lucene expressions read hex and octal literals as signed 64-bit long values with Long.parseLong, and the grammar accepts them with any number of digits. A literal of 2^63 or more, such as 0x8000000000000000, the 64-bit mask 0xffffffffffffffff or the octal 01000000000000000000000, does not fit: Long.parseLong throws NumberFormatException, nothing in the code generator catches it, and compileExpression passes every runtime exception that is not a wrapped ParseException to the caller unchanged.

So compile throws an unchecked java.lang.NumberFormatException ("For input string: \"8000000000000000\" under radix 16") instead of the ParseException every compile overload documents, and a caller that maps ParseException to "invalid expression" does not handle it. The literal is still rejected: no wrong results, no index effect, no hang. The largest in-range literals, 0x7fffffffffffffff and 0777777777777777777777, compile and evaluate correctly, so the boundary is exactly Long.MAX_VALUE.

The two calls are in the module's first patch (LUCENE-5207, 2013): all 122 lucene-expressions releases from 4.6.0 to 10.6.0 fail, and so does main. Not fixed upstream. Fix: catch the NumberFormatException at the literal and throw ParseException (validated with the module's tests and checks).

The mechanism (main 645d4fe14a20)

  1. The grammar accepts any length. Javascript.g4:83 OCTAL: [0] [0-7]+; and :84 HEX: [0] [xX] [0-9a-fA-F]+; have no digit limit, so 0x8000000000000000 is a valid token.
  2. The code generator parses with a signed long. visitNumeric (JavascriptCompiler.java:432-445) calls Long.parseLong(ctx.HEX().getText().substring(2), 16) (:434) for hex, Long.parseLong(ctx.OCTAL().getText().substring(1), 8) (:436) for octal and Double.parseDouble (:438) for decimal. Long.parseLong accepts at most 2^63 - 1 and throws NumberFormatException above it; nothing catches it.
  3. Source errors normally leave the visitor as a wrapped ParseException. The visitor runs inside the ClassFile.build callback (:255-259), which cannot throw checked exceptions, so for bad source text it throws newWrappedParseException (:857-859), for example for wrong arity (:458) and unknown functions (:505-507).
  4. compileExpression unwraps only that case. catch (RuntimeException re) (:260-265) rethrows the cause when it is a ParseException and every other runtime exception unchanged, so the NumberFormatException escapes every public compile overload. It happens before any class is defined (:276), so no Expression exists.

Executed forms (main 645d4fe14a20, assertions on and off)

inputresult
0x7fffffffffffffffcompiles, 9.223372036854776E18
0777777777777777777777compiles, 9.223372036854776E18
9223372036854775808 (decimal 2^63)compiles through the double path, 2^63
0y1234ParseException
0x8000000000000000NumberFormatException (:434)
-0x8000000000000000NumberFormatException: the minus is an operator, so Long.MIN_VALUE cannot be written in hex
1 + 0xffffffffffffffff, x & 0xffffffffffffffffNumberFormatException
compile("0x8000000000000000", DEFAULT_FUNCTIONS)NumberFormatException
01000000000000000000000 (octal 2^63)NumberFormatException (:436, "under radix 8")

Each failure is a bare NumberFormatException (getCause() is null). Throwing ParseException at the parse site makes the repro pass, so the two unguarded calls are the whole cause.

Exposure and likelihood246 words
  • What goes wrong. The caller sees an unchecked java.lang.NumberFormatException (a subclass of IllegalArgumentException), "For input string: \"8000000000000000\" under radix 16", with no position, instead of a ParseException that names the expression. A handler that maps ParseException to "invalid expression" (an error shown to whoever typed it) does not run; the exception surfaces as a generic failure or propagates.
  • Who is exposed. Any public compile overload (given a valid function map, for the overloads that take one), default settings, assertions on or off, no index needed. In practice a 64-bit mask such as 0xffffffffffffffff or the sign-bit mask 0x8000000000000000 in a bitwise expression. On #3534 (an open request for bitwise filtering on numeric fields), gsmiller suggested expressions for such tests in 2024.
  • Partly masked on the overloads with a function map. They document IllegalArgumentException for a bad function signature (JavascriptCompiler.java:169, :191, :215), so a caller that catches IllegalArgumentException there catches this by accident. compile(String) documents no IllegalArgumentException.
  • Not affected. Decimal literals, in-range hex and octal literals, results (nothing is computed wrongly) and the index. The recorded security probe found no hang and no residual state for its inputs: a 1 MiB trigger literal costs 9-14 ms, about as much as a valid literal of the same length.
  • How likely. Rare: it needs a literal of 2^63 or more, the failure is loud at compile time, and no prior public report was found in the recorded searches, over 13 years of releases.

Minimal repro

One JUnit test in lucene/expressions, public API only: compile 0x8000000000000000 and fail on any unchecked exception. It accepts both outcomes the contract allows (compiles, or ParseException). No index and no randomness; it fails with assertions on and off.

TestHexLiteralAboveLongMaxValue.java (1 test)33 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.expressions.js;

import java.text.ParseException;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestHexLiteralAboveLongMaxValue extends LuceneTestCase {

  public void testCompileHexLiteralAboveLongMaxValue() {
    try {
      JavascriptCompiler.compile("0x8000000000000000");
    } catch (ParseException _) {
    } catch (RuntimeException e) {
      // Expected: compiles, or fails with the documented ParseException; actual: it throws an unchecked exception.
      throw new AssertionError("compile(\"0x8000000000000000\") threw " + e, e);
    }
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or a later commit where JavascriptCompiler.java is unchanged)
cp $REPRO/TestHexLiteralAboveLongMaxValue.java lucene/expressions/src/test/org/apache/lucene/expressions/js/
./gradlew :lucene:expressions:test --tests org.apache.lucene.expressions.js.TestHexLiteralAboveLongMaxValue -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false
Expected
compile("0x8000000000000000") compiles, or fails with the documented ParseException.
Observed
java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16 from Long.parseLong in visitNumeric, on main (assertions on and off) and 10.5.2.

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

More files, environment, full output165 words

The test sits in org.apache.lucene.expressions.js next to the compiler's other tests but uses only JavascriptCompiler.compile(String) with the default functions. Both tested seeds, DEADBEEF and 5EED1234, fail the same way (the test uses no randomness), and the same file fails unchanged against the 10.5.2 release jars.

As a regression test: ./gradlew tidy wraps its long comment line (nothing else changes), and catch (ParseException _) is Java 22+ syntax, fine on main; a Java 21 backport needs a named variable. The octal sibling (01000000000000000000000, :436) has the same symptom and fix and is left out on purpose.

Environment

lucenemain 645d4fe14a20: affected; the same test also fails on the 10.5.2 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64

Observed output

TestHexLiteralAboveLongMaxValue > testCompileHexLiteralAboveLongMaxValue FAILED
    java.lang.AssertionError: compile("0x8000000000000000") threw java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:3A1B5B8F06FCBC08]:0)
        at org.apache.lucene.expressions.js.TestHexLiteralAboveLongMaxValue.testCompileHexLiteralAboveLongMaxValue(TestHexLiteralAboveLongMaxValue.java:30)

        Caused by:
        java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16
            at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
            at java.base/java.lang.Long.parseLong(Long.java:594)
            at org.apache.lucene.expressions.js.JavascriptCompiler$2.visitNumeric(JavascriptCompiler.java:434)
            ...
:lucene:expressions:test (FAILURE): 1 test, 1  failure

Why it looks unintended

  • Every compile method documents @throws ParseException on failure to compile; NumberFormatException is not listed.
  • Other bad source text throws ParseException: 0y1234 and 500EE in the tests, bad function names and arity since LUCENE-6965.
  • The grammar accepts hex and octal literals of any length, and the docs list them with no range.
The full argument166 words
  • The contract. Every compile overload says @throws ParseException on failure to compile (JavascriptCompiler.java:150, :168, :190, :214). IllegalArgumentException is documented only for a bad function signature, IllegalStateException only for link failures. The class javadoc sends the one documented parser limit, nesting, to ParseException (:104-106).
  • The literal is legal. Javascript.g4:83-84 has no digit limit, and package-info.java:25 lists "Integer, floating point, hex and octal literals" with no range. No javadoc, CHANGES or MIGRATE entry states a range or mentions NumberFormatException.
  • Tests. testInvalidCompiles (TestJavascriptCompiler.java:141-172) requires ParseException for 0y1234, 500EE and 500.5EE. No test uses a literal of 2^63 or more.
  • Precedent. LUCENE-6965 (#8022, 5.5.0) moved bad function names and arity from IllegalArgumentException to ParseException; its description says that "would be better since that's the exception clients will be prepared to receive". #16423 (10.6.0, Uwe Schindler and Seth Kraft) replaced StackOverflowError with ParseException and clarified the documented exceptions, without this case. No commit or discussion mentions literal range.
The devil's-advocate check239 words

The strongest case for "intended" (built by the devil's-advocate verifier and both executed cross-model reviews): JavascriptCompiler is @lucene.experimental (:111), the syntax is only "based on" JavaScript, hex and octal literals have been signed 64-bit values since 2013, NumberFormatException is an IllegalArgumentException, the usual Java signal for a bad argument, and @throws lists for unchecked exceptions are often incomplete.

Why it fails: that justifies rejecting such literals, not the exception type. The bad input is expression source text, the class of error LUCENE-6965 moved to ParseException; the compiler already has a helper for it and uses it for other source errors; compile(String) documents no IllegalArgumentException; and the exception list was re-stated in 2026 without this case. Verdict: a bug in the exception type. The independent reproduction, the intended-behaviour check and both executed cross-model reviews rated the intended argument weak; the adjudication gave no rating but found that it "justifies rejecting such literals but not the exception type". A later fact check by another model family (OpenAI GPT-5.6) rated it moderate: keeping hex and octal literals in the signed long range is a plausible, long-standing design choice, and a grammar that accepts any length is not a promise that any magnitude is supported. So the evidence argues only for the exception type: it is inconsistent with the documented failure channel and with LUCENE-6965, but it does not show what the original authors intended for out-of-range literals.

Suggested fix

Recommended A: throw ParseException at the literal

Parse hex and octal literals through a small helper that catches NumberFormatException and throws ParseException ("integer literal out of range", at the literal's position, with the original exception as cause). Nothing that compiles today changes. The repro passes with assertions on and off; all 170 expressions tests and the module's format and lint checks pass.

  // visitNumeric, for HEX (OCTAL the same with substring(1) and radix 8):
- pushLong(Long.parseLong(ctx.HEX().getText().substring(2), 16));
+ pushLong(parseIntegerLiteral(ctx, ctx.HEX().getText().substring(2), 16));

  private long parseIntegerLiteral(NumericContext ctx, String digits, int radix) {
    try {
      return Long.parseLong(digits, radix);
    } catch (NumberFormatException e) {
      var pe = new ParseException("Invalid expression '" + sourceText
          + "': integer literal out of range (" + ctx.getText() + ")", ctx.start.getStartIndex());
      pe.initCause(e);
      throw new RuntimeException(pe);
    }
  }
All options and trade-offs3 options · 570 words

One option was built and validated through gradle on 645d4fe14a20; a second is a behaviour change left to maintainers; a third is not a fix.

A: throw ParseException at the literal (validated by the checks) (recommended)

A parseIntegerLiteral helper in the visitor replaces the two Long.parseLong calls in visitNumeric (+20/-2 in JavascriptCompiler.java). It throws ParseException: Invalid expression '0x8000000000000000': integer literal out of range (0x8000000000000000) with error offset 0 (4 for 1 + 01000000000000000000000), wrapped in a RuntimeException that the existing code at :260-265 unwraps. Chaining the cause follows the StackOverflowError catch at :266-271. The snippet is condensed.

  // visitNumeric, for HEX (OCTAL the same with substring(1) and radix 8):
- pushLong(Long.parseLong(ctx.HEX().getText().substring(2), 16));
+ pushLong(parseIntegerLiteral(ctx, ctx.HEX().getText().substring(2), 16));

  private long parseIntegerLiteral(NumericContext ctx, String digits, int radix) {
    try {
      return Long.parseLong(digits, radix);
    } catch (NumberFormatException e) {
      var pe = new ParseException("Invalid expression '" + sourceText
          + "': integer literal out of range (" + ctx.getText() + ")", ctx.start.getStartIndex());
      pe.initCause(e);
      throw new RuntimeException(pe);
    }
  }

Trade-offs

  • Only the failing case changes. Every literal that compiles today parses exactly as before.
  • The message embeds the whole source like the other Invalid expression '...' messages (:458, :505-507), and then the literal: a 1 MiB literal gives a message of about 2 MiB (measured with the study's sketch, whose message is the same). Quoting only the literal would keep it short.
  • It catches NumberFormatException at the parse site only, not IllegalArgumentException in general, in line with a committer's remark on LUCENE-6965 that something unexpected inside the visitor could throw an IllegalArgumentException.
  • The study's shorter sketch (a catch (NumberFormatException nfe) that ignores nfe) fixes the repro too, but fails Lucene's ecjLintMain: "The value of the exception parameter nfe is not used". Chaining the cause, as here, avoids that.

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20 (one checkout, OpenJDK 25.0.3, Linux aarch64), seed DEADBEEF: the repro fails on stock and passes with fix A, assertions on and off.
  • The whole :lucene:expressions:test with fix A and the repro (assertions on): 14 suites, 171 tests, 0 failures, 0 skipped (the 13 upstream suites with 170 tests, including TestJavascriptCompiler, plus the repro).
  • The closest suite TestJavascriptCompiler and the repro with -Ptests.asserts=false: 21 tests pass.
  • :lucene:expressions:check -x test (ANTLR checksum, google-java-format, ecj lint, forbidden APIs): passes, with the repro's comment wrapped by tidy.
  • A probe on the standalone runner shows the new ParseException, its offset and the chained cause, for the hex and the octal trigger.

B: accept the literal as its exact value, as a double

Parse the digits with new BigInteger(digits, radix) and, when the value does not fit a long, load BigInteger.doubleValue(), the way JavaScript reads 0x8000000000000000 as 2^63.

Trade-offs

A behaviour change (today's refusals start to compile) that needs a CHANGES entry and a rule for bitwise positions: there the operand becomes a long again, and the JVM's double-to-long conversion saturates at Long.MAX_VALUE, so x & 0xffffffffffffffff would silently mean x & 0x7fffffffffffffff unless such literals are rejected there. Not built or run.

C: not a fix: Java-style wrapping (Long.parseUnsignedLong or a narrowing cast)

Read the 64 bits as a two's-complement long, as Java's 0xffffffffffffffffL does.

Trade-offs

0xffffffffffffffff would become -1: 1 + 0xffffffffffffffff would be 0, and a filter such as x > 0xffffffffffffffff would turn today's refusal into a silent match of every non-negative value.

Downstream note

Servers that compile client-supplied expression scripts and catch only ParseException report such a request as a generic compile failure instead of a script syntax error; the expression is still rejected, before any class is generated.

How it was found

Found by reading the compiler while writing the literal rules of a bounded-exhaustive translation-validation model of the expressions compiler: the finder noticed the signed Long.parseLong. No tool output produced it; the exhaustive check's literals stay inside the long range. A probe and a public-API JUnit on the stock jar then confirmed it.

The method in full131 words

Method: semi-formal (a hunt unit, the expressions compiler).

  • The finder, an AI tool, was writing the literal semantics of an exact reference model of JavascriptCompiler, used to check every small expression tree against the compiled code, and noticed that hex and octal tokens go through signed Long.parseLong with no handler.
  • No formal or exhaustive tool output produced the signal: the model's literal alphabets stay inside the long range (the largest is 0x7fffffffffffffff), and the unit's grammar fuzzer skips this NumberFormatException on purpose as already known, so it neither confirms nor adds to it.
  • The finder then confirmed it with a probe and a public-API JUnit on the stock jar; independent reproducers wrote their own tests from the claim alone, and the one-test repro was cut from those on 2026-10-29.

Prior reports, history and checks

Prior reports

No prior report: 215 query lines over GitHub, the mailing lists, JIRA, downstream repositories and the web, plus 10 hand JQL and 10 hand web queries and the files of all 410 open PRs (2026-10-31; an earlier pass on 2026-10-26 agrees). Closest: #16423 and LUCENE-6965, which fixed other unchecked exceptions escaping compile.

Searches4 searches

Verdict new: no searched source reports or discusses hex or octal literals above Long.MAX_VALUE or NumberFormatException from JavascriptCompiler, and no fix exists. Related, not reports of this bug: #16423 (same class: StackOverflowError and LinkageError escaping compile, now ParseException and IllegalStateException); #8022 / LUCENE-6965 (same class: bad function names and arity); #3534 (open, bitwise filtering on numeric fields: gsmiller suggests expressions, the use where large hex masks appear, nothing on literal range); #6271 / LUCENE-5207 (the origin; its comments never discuss literal range); #12873, #14597 and #7476 / LUCENE-6417 (rewrites that carried visitNumeric forward). The public bug list mentions it. Gaps: GitHub search is lexical, and private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits and code2026-10-31the scripted prior-report search (102 queries over GitHub, commits, downstream projects, mailing lists and JIRA); 102 hand GraphQL searches: the error text under radix 16 / under radix 8, 0x8000000000000000, 0xffffffffffffffff, visitNumeric, pushLong, hex/octal/integer literal, label:module:expressions, in apache/lucene, Elasticsearch, Solr, Lucene.NET and all of GitHub; 11 REST searchesonly the related items above
Open PRs and recent activity2026-10-31the changed files of all 410 open apache/lucene PRs; every PR (140) and issue (40) updated since 2026-10-20only #681 and #16847 touch lucene/expressions, neither JavascriptCompiler.java
Mailing lists, JIRA, web2026-10-31dev@, java-user@, issues@ and commits@; 10 JQL queries and 8 grammar-related issues searched for hex, octal, radix, parseLong, 0x, NumberFormat; the LUCENE-5207 thread; 10 web queriesno report
Earlier pass2026-10-26the scripted prior-report search (21 + 24 queries), 64 hand GraphQL searches, 60 list/JIRA/web queries, the 413 open PRs' files and the git historyverdict new

History

Introduced with the module by LUCENE-5207 (initial patch c73f175c71ee, 2013; first release 4.6.0). Executed: all 122 lucene-expressions releases from 4.6.0 to 10.6.0 fail, and so does main. 4.5.1 has no expressions module. Not fixed upstream.

Versions tested5 rows

Introduced by LUCENE-5207 "lucene expressions module" (#6271; SVN era, no PR): the initial patch c73f175c71ee "LUCENE-5207: initial patch from ryan" (2013-09-13, feature branch lucene5207) wrote the two Long.parseLong calls with no NumberFormatException handling while compile already documented @throws ParseException. It reached trunk as eecd78f37a1 and branch_4x as 08df432f8ba (2013-09-15). 7b444287278 moved the calls into pushLong(...) before the merge; LUCENE-6417, the ANTLR 4.5 port (7c9d78a2c45, 2015, 5.4.0), moved them unchanged into visitNumeric, where git blame still points. LUCENE-6965 (2016) and #16423 (2026) changed other error paths only. Not a regression: no release ever had the API without the bug. Upstream main 2f424fd428da (2026-10-30) still has the calls at :434/:436, branch_10_6 at :459/:461.

Versions and branches executed, with results
Version or commitVerdictResult
4.5.1 no moduleno lucene-expressions artifact exists; the module starts with 4.6.0
4.6.0 to 10.6.0 (122 releases) first: 4.6.0122 of 122 fail with assertions on and off (for 4.x-8.x with assertions off through a small main() probe that compiles the same literal plus the octal case, 1 + 0xffffffffffffffff and two in-range controls); 246 runs (two modes for each release and main) plus one extra 4.6.0 main() control: 247 failing runs, the same NumberFormatException each time, all on OpenJDK 25.0.3, Linux aarch64
10.5.2 affectedthe one-test repro fails unchanged, assertions on and off, at visitNumeric(JavascriptCompiler.java:389)
main 645d4fe14a20 affectedthe one-test repro fails through gradle and the standalone runner, assertions on and off, at :434
fix A on 645d4fe14a20 not affectedthe repro passes (assertions on and off); 171 of 171 module tests pass; check -x test passes

Independent checks

An independent reproducer, an intended-behaviour devil's advocate, two executed reviews by another model family (OpenAI GPT-5.6) and an adjudication all rated it real; the adjudication confirmed it with high confidence. A later review that could not run Java returned a procedural "not real"; a tie-breaker re-ran everything on 645d4fe14a20 and confirmed it.

Checks and verdicts320 words

The second refute review (2026-10-31) reviewed by reading only, because it could not run code; its own record calls its verdict procedural, "not a substantive disproof", and a separate run of the test on 10.5.2 failed with the same exception. The tie-breaker re-ran the repro and its own 10-test probe on main and 10.5.2 with assertions on and off, then the fix.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (a hunt unit) candidate (semi-formal)Own JUnit fails 3 of 7 on main (assertions on and off): hex, octal and 1 + 0xffffffffffffffff; in-range controls pass; its main() form fails on 10.5.2, 9.12.3 and 8.11.4; the study fix passes it and the 170 expressions tests.
Refute review (OpenAI GPT-5.6, executed) real (high)Root cause agreed, fix fixes it, inputs in contract, intended argument weak.
Independent reproducer realOwn JUnit fails 6 of 8 on main and 10.5.2 (assertions on and off), 5 of 7 on 9.12.3; controls pass (Long.MAX_VALUE literals evaluate, 0y1234 throws ParseException).
Works-as-designed lens (OpenAI GPT-5.6, executed) real (high)Own contract test fails 3 of 7 on main and passes 7 of 7 with the fix; the same leak on 10.5.2; 170 expressions tests pass with the fix.
Devil's advocate realBuilt the intended-behaviour case and rated it weak; read the javadoc, package docs, tests, history, LUCENE-6965 and #16423.
Adjudication confirmed (high)Re-ran the independent reproduction's test: 6 of 8 fail with assertions on and off.
Second refute review (OpenAI GPT-5.6, 2026-10-31) not real, low (procedural)Could not run Java; reading only; its record calls the verdict not a substantive disproof.
Re-verification and tie-breaker on 645d4fe14a20 realThe repro fails on main and 10.5.2 (assertions on and off); a 10-test probe: 6 trigger shapes fail, 4 controls pass; with the fix 1/1 and 10/10; 170 expressions tests pass with and without the fix.

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

Finding record

LFV-197, snapshot of 2026-11-06

Registry title
JavascriptCompiler.compile throws NumberFormatException instead of the documented ParseException for a hex or octal literal above Long.MAX_VALUE, such as 0x8000000000000000
Area
expressions / JavascriptCompiler literal parsing (hex and octal literals in visitNumeric)
Priority
Low, rank 895: Hex or octal expression literals above Long.MAX_VALUE throw NumberFormatException instead of ParseException, and only the error type is wrong.
Impact class
wrong error
Impact note
Anyone who compiles expression text with the optional expressions module (any public compile overload, default settings, every release since 4.6.0, with -ea and -da, at any index size) and writes a hex literal above 0x7fffffffffffffff or an octal literal above 0777777777777777777777 gets an unchecked java.lang.NumberFormatException at compile time instead of the documented ParseException, so a caller that treats ParseException as the 'bad expression' signal does not handle it; the literal is still rejected, so there are no wrong results, no index effect and no hang, and decimal literals (Double.parseDouble) are not affected.
Summary
You compile an expression such as '0x8000000000000000', '01000000000000000000000' or '1 + 0xffffffffffffffff' with JavascriptCompiler.compile, and you expect it either to compile (JavaScript reads 0x8000000000000000 as 2^63) or to fail with ParseException, because every compile overload documents '@throws ParseException on failure to compile' and the grammar accepts hex and octal literals of any length. Instead compile throws java.lang.NumberFormatException ('For input string: "8000000000000000" under radix 16'): visitNumeric parses hex and octal tokens with signed Long.parseLong, and compileExpression rethrows every RuntimeException that does not wrap a ParseException. The largest in-range literals, 0x7fffffffffffffff and 0777777777777777777777, compile and evaluate correctly, so the boundary is exactly Long.MAX_VALUE.
Introduced
c73f175c71ee02d057819037105b4e55d4099567 ('LUCENE-5207: initial patch from ryan', committed by rmuir on the lucene5207 feature branch, svn r1522767), which first wrote Long.parseLong(text.substring(2), 16) for HEX and Long.parseLong(text.substring(1), 8) for OCTAL with no NumberFormatException handling; it reached trunk through merge eecd78f37a128dac326294ebf550eae0bbb04434 and branch_4x through 08df432f8ba20370ed18b8933603ceba4007437b (both 2013-09-15, 'LUCENE-5207: lucene expressions module'); no GitHub PR (svn era), JIRA LUCENE-5207 [read: git log -S parseLong -- lucene/expressions returns only this commit; the current lines blame to 7c9d78a2c45 (LUCENE-6417 ANTLR 4.5 port, 2015-08-07), which moved the calls unchanged into visitNumeric; exec: the first release, 4.6.0, is affected] (2013-09-13 (feature-branch commit; merged to trunk and branch_4x on 2013-09-15))
First affected version
4.6.0 (the first release with the expressions module, tag dated 2013-11-22) [exec, -ea and -da]; 4.5.1 and earlier have no expressions module (Maven Central has no lucene-expressions-4.5.1.jar), so the bug is as old as the API and is not a regression
Affected versions
every release that ships lucene-expressions, 4.6.0 through 10.6.0, and main ea57360912ed (11.0.0-SNAPSHOT); the follow-up's sweep fails on all 122 lucene-expressions releases from 4.6.0 to 10.6.0 with -ea and -da [exec: this doc's one-test repro, ported for releases before 9.1.0; for 4.x-8.x with -da a small main() probe]. [exec] 14 versions in the history step (a version-agnostic main() with in-range controls, release jars SHA-1-checked against Maven Central, seed DEADBEEF): 4.6.0, 4.10.4, 5.5.5, 6.0.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main, all affected, with identical -ea and -da results on 4.6.0, 9.12.3, 10.5.2 and main; independently by the verifiers: main (-ea and -da; finder, the independent reproduction, the cross-model review, the intended-behaviour check, adjudication, follow-up re-run), 10.5.2 (-ea and -da; the independent reproduction, the cross-model review) and 9.12.3 (-ea; the independent reproduction); 8.11.4 by the finder and the history step only, not by an independent verifier. 4.x-6.x ran on JDK 25, newer than the JDKs they support; their in-range controls passed. [read] all 112 release tags from 4.6.0 to 10.5.2 in the history clone have the two unguarded Long.parseLong calls and no NumberFormatException handling (8.8.2, 8.9.0, 8.10.x and 8.11.x are not tags there); branch_10_6 (2763db11b0a, the 10.6.0 release branch) and the unreleased branch_10x (1309534296e) have them too (upstream JavascriptCompiler.java:459/461), not executed.
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 adjudication that ruled it real, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: code reading
Upstream and prior reports
none found (2026-10-26, follow-up prior-report record verdict new): a scripted prior-report search over GitHub issues and PRs, commits and downstream projects (21 queries), the dev@, java-user@, issues@ and commits@ lists, JIRA and the web (60 queries over two specs), plus the intended-behaviour check's earlier run (24 queries), 0 high-overlap hits; 64 hand GitHub GraphQL searches over apache/lucene, Elasticsearch, OpenSearch, Solr and Lucene.NET (the exact error text 'under radix 16', the triggers 0x8000000000000000 and 0xffffffffffffffff, visitNumeric, pushLong, 'hex/octal literal' and 'hex/octal constant': 0 relevant); manual list and JIRA queries; the LUCENE-5207 description and its 89 comments grepped for hex, octal, radix, parseLong and 0x; the changed files of all 413 open apache/lucene PRs (none touches JavascriptCompiler.java, Javascript.g4 or the generated parser); git history (no commit message in the module mentions hex, octal, radix or NumberFormatException). Not fixed: upstream main, branch_10x and branch_10_6 still call Long.parseLong unguarded. Related, not reports of this bug: LUCENE-6965 / #8022 (2016, moved bad function names and arity from IllegalArgumentException to ParseException) and #16423 (2026, maps StackOverflowError to ParseException), the same bug class with different triggers and fix sites. Gaps: GitHub's search rate limit was saturated, so spec B's GitHub phrasings ran as hand GraphQL searches; a scripted prior-report search does not read inline PR review comments (read by hand for #16423 and #12873 only); lists.apache.org search is token-based; private reports to security@lucene.apache.org are not visible
Lucene commit tested
main 645d4fe14a20 (commit of 2026-10-28): this doc's one-test repro through gradle in a scratch worktree with assertions on and off (seed DEADBEEF) and on the standalone runner (seed DEADBEEF with assertions on and off, 5EED1234 with assertions on); the recommended fix through gradle with the whole lucene/expressions test suite and check -x test on 2026-10-31. The repro also fails on the 10.5.2 release jars (assertions on and off), and the follow-up executed it on all 122 lucene-expressions releases from 4.6.0 to 10.6.0 (all fail). The hunt, verifiers and judge ran on main ea57360912ed; JavascriptCompiler.java is the same blob at both commits. Upstream main 2f424fd428da (2026-10-30) still has the unguarded calls.

Lucene and upstream sources

Finding record
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:146-155 (compile(String) javadoc: '@throws ParseException on failure to compile'; IllegalStateException for link failures), :164-172 (compile(String, Map): IllegalArgumentException only 'if any of the functions does not have correct signature'), :250-265 (ClassFile.build; RuntimeException rethrown unless its cause is a ParseException), :432-437 (visitNumeric: Long.parseLong(..., 16) for HEX, Long.parseLong(..., 8) for OCTAL, Double.parseDouble for DECIMAL)
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/Javascript.g4:83-84 (OCTAL: [0] [0-7]+; HEX: [0] [xX] [0-9a-fA-F]+; no length limit) and package-info.java:25 ('Integer, floating point, hex and octal literals', no range)
The finding in 30 seconds
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/Javascript.g4:83-84 (at 645d4fe14a20)
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:249-276 (at 645d4fe14a20) (compileExpression; ClassFile.build :255-259; catch RuntimeException :260-265; defineHiddenClassWithClassData :276), :432-445 (visitNumeric), :458, :505-507, :857-859 (newWrappedParseException)
  • lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptCompiler.java:141-172 (at 645d4fe14a20) (testInvalidCompiles); TestJavascriptOperations.java:331 (0x1234ABCDEF)
Who is affected
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:146-155, (at 645d4fe14a20) :164-172, :190-191, :214-215
Why it looks unintended
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:104-106, (at 645d4fe14a20) :111, :150, :168-169, :190-191, :214-215; package-info.java:25; Javascript.g4:83-84
  • lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptCompiler.java:141-172 (at 645d4fe14a20)
  • lucene/CHANGES.txt:551-555 (at 645d4fe14a20) (#16423), :9835-9837 (LUCENE-6965)
Suggested fix
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:260-271, (at 645d4fe14a20) :432-445, :458, :505-507
Diagrams
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:260-265, (at 645d4fe14a20) :432-436

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

Repro files

About the diagrams

A picture of the minimal repro and the path of the exception, made in parallel with this doc from the code at 645d4fe14a20; the PNG is the summary still.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.