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.
- The optional
lucene-expressionsmodule compiles a formula such asln(popularity) + (flags & 0xff)into a class that computes a double per document, for scores, sort keys and filters. - Hex and octal literals and the bitwise operators work on 64-bit
longvalues: the module's tests check0x1234ABCDEF= 78193085935. - Bad expression text is reported as
java.text.ParseException, a checked exception everycompilemethod declares:0y1234,500EEand500.5EEthrow it in the module's tests.
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.- Compile
0x7fffffffffffffffwithJavascriptCompiler.compile. It works and evaluates to 9.223372036854776E18: Lucene reads hex and octal literals as signed 64-bitlongvalues, and this is the largest one. - Compile
0x8000000000000000, one more (2^63), or a 64-bit mask such asx & 0xffffffffffffffff. The grammar accepts hex literals of any length. - You expect it to compile, or to fail with
ParseException: everycompilemethod documents@throws ParseException on failure to compile, and other bad literals such as0y1234do throw it. - You get
java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16.Long.parseLongcannot hold the value, nothing catches its exception, andcompilepasses it to the caller unchanged. - 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.
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 grammar accepts any length.
Javascript.g4:83OCTAL: [0] [0-7]+;and:84HEX: [0] [xX] [0-9a-fA-F]+;have no digit limit, so0x8000000000000000is a valid token. - The code generator parses with a signed
long.visitNumeric(JavascriptCompiler.java:432-445) callsLong.parseLong(ctx.HEX().getText().substring(2), 16)(:434) for hex,Long.parseLong(ctx.OCTAL().getText().substring(1), 8)(:436) for octal andDouble.parseDouble(:438) for decimal.Long.parseLongaccepts at most 2^63 - 1 and throwsNumberFormatExceptionabove it; nothing catches it. - Source errors normally leave the visitor as a wrapped
ParseException. The visitor runs inside theClassFile.buildcallback (:255-259), which cannot throw checked exceptions, so for bad source text it throwsnewWrappedParseException(:857-859), for example for wrong arity (:458) and unknown functions (:505-507). compileExpressionunwraps only that case.catch (RuntimeException re)(:260-265) rethrows the cause when it is aParseExceptionand every other runtime exception unchanged, so theNumberFormatExceptionescapes every publiccompileoverload. It happens before any class is defined (:276), so noExpressionexists.
| input | result |
|---|---|
0x7fffffffffffffff | compiles, 9.223372036854776E18 |
0777777777777777777777 | compiles, 9.223372036854776E18 |
9223372036854775808 (decimal 2^63) | compiles through the double path, 2^63 |
0y1234 | ParseException |
0x8000000000000000 | NumberFormatException (:434) |
-0x8000000000000000 | NumberFormatException: the minus is an operator, so Long.MIN_VALUE cannot be written in hex |
1 + 0xffffffffffffffff, x & 0xffffffffffffffff | NumberFormatException |
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 ofIllegalArgumentException), "For input string: \"8000000000000000\" under radix 16", with no position, instead of aParseExceptionthat names the expression. A handler that mapsParseExceptionto "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
compileoverload (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 as0xffffffffffffffffor the sign-bit mask0x8000000000000000in 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
IllegalArgumentExceptionfor a bad function signature (JavascriptCompiler.java:169,:191,:215), so a caller that catchesIllegalArgumentExceptionthere catches this by accident.compile(String)documents noIllegalArgumentException. - 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.
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.
/*
* 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);
}
}
}
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 documentedParseException.- Observed
java.lang.NumberFormatException: For input string: "8000000000000000" under radix 16fromLong.parseLonginvisitNumeric, on main (assertions on and off) and 10.5.2.
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.
| lucene | main 645d4fe14a20: affected; the same test also fails on the 10.5.2 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
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- Every
compilemethod documents@throws ParseException on failure to compile;NumberFormatExceptionis not listed. - Other bad source text throws
ParseException:0y1234and500EEin 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
compileoverload says@throws ParseException on failure to compile(JavascriptCompiler.java:150,:168,:190,:214).IllegalArgumentExceptionis documented only for a bad function signature,IllegalStateExceptiononly for link failures. The class javadoc sends the one documented parser limit, nesting, toParseException(:104-106). - The literal is legal.
Javascript.g4:83-84has no digit limit, andpackage-info.java:25lists "Integer, floating point, hex and octal literals" with no range. No javadoc, CHANGES or MIGRATE entry states a range or mentionsNumberFormatException. - Tests.
testInvalidCompiles(TestJavascriptCompiler.java:141-172) requiresParseExceptionfor0y1234,500EEand500.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
IllegalArgumentExceptiontoParseException; 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) replacedStackOverflowErrorwithParseExceptionand 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.
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 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
NumberFormatExceptionat the parse site only, notIllegalArgumentExceptionin general, in line with a committer's remark on LUCENE-6965 that something unexpected inside the visitor could throw anIllegalArgumentException. - The study's shorter sketch (a
catch (NumberFormatException nfe)that ignoresnfe) fixes the repro too, but fails Lucene'secjLintMain: "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:testwith fix A and the repro (assertions on): 14 suites, 171 tests, 0 failures, 0 skipped (the 13 upstream suites with 170 tests, includingTestJavascriptCompiler, plus the repro). - The closest suite
TestJavascriptCompilerand 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 bytidy.- A probe on the standalone runner shows the new
ParseException, its offset and the chained cause, for the hex and the octal trigger.
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.
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.
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.
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 signedLong.parseLongwith no handler. - No formal or exhaustive tool output produced the signal: the model's literal alphabets stay inside the
longrange (the largest is0x7fffffffffffffff), and the unit's grammar fuzzer skips thisNumberFormatExceptionon 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.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits and code | 2026-10-31 | the 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 searches | only the related items above |
| Open PRs and recent activity | 2026-10-31 | the changed files of all 410 open apache/lucene PRs; every PR (140) and issue (40) updated since 2026-10-20 | only #681 and #16847 touch lucene/expressions, neither JavascriptCompiler.java |
| Mailing lists, JIRA, web | 2026-10-31 | dev@, 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 queries | no report |
| Earlier pass | 2026-10-26 | the scripted prior-report search (21 + 24 queries), 64 hand GraphQL searches, 60 list/JIRA/web queries, the 413 open PRs' files and the git history | verdict new |
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.
| Version or commit | Verdict | Result |
|---|---|---|
| 4.5.1 | no module | no lucene-expressions artifact exists; the module starts with 4.6.0 |
| 4.6.0 to 10.6.0 (122 releases) | first: 4.6.0 | 122 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 | affected | the one-test repro fails unchanged, assertions on and off, at visitNumeric(JavascriptCompiler.java:389) |
main 645d4fe14a20 | affected | the one-test repro fails through gradle and the standalone runner, assertions on and off, at :434 |
fix A on 645d4fe14a20 | not affected | the repro passes (assertions on and off); 171 of 171 module tests pass; check -x test passes |
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.
| Check | Verdict | What 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 | real | Own 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 | real | Built 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 | real | The 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. |
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 511 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,278 words
- 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 wholelucene/expressionstest suite andcheck -x teston 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 122lucene-expressionsreleases from 4.6.0 to 10.6.0 (all fail). The hunt, verifiers and judge ran on mainea57360912ed;JavascriptCompiler.javais the same blob at both commits. Upstream main2f424fd428da(2026-10-30) still has the unguarded calls.
- 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-84lucene/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
- TestHexLiteralAboveLongMaxValue.java (1 test):
repro/LFV-197/src/org/apache/lucene/expressions/js/TestHexLiteralAboveLongMaxValue.java(33 lines)
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.
assets/LFV-197/bug.png(511 KB)
