All findings · Lucene finding · LFV-390 · native-sve-dot-int32-loop-index-overflow

Opt-in native SVE dot product crashes the JVM for byte arrays near 2^31 elements: its int32 loop index overflows

With Lucene's opt-in native dot product (a library you build with SVE from the example dotProduct.c) on an SVE CPU, VectorUtil.dotProduct on byte arrays of about 2^31 elements crashes the JVM: a 32-bit loop index overflows and the kernel reads past both arrays. Unreleased main only; default Lucene is unaffected.

Summary

Priority
Low, rank 988The JVM crashes only with the unreleased opt-in example native library on SVE and byte arrays of about 2^31 elements, an extreme scale that indexed vectors cannot reach.
Impact
crash
Since
main 50dac906310 (#15508, 2026-02-17); unreleasedintroduced 2026-02-17
Affected
main and branch_11x, only with the opt-in native library built with SVE, on an SVE CPU; no release
Status
documented
Found by
formal: CBMC bounded model checking of a harness around dotProduct.c, then an executed repro

AI-assisted page: this finding was first signalled by a formal tool's own output 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

The SVE kernel adds 4 vectors' worth of bytes to its 32-bit loop index before comparing it with the length. Near 2^31 that sum overflows, the compiled 32-bit test wraps negative and stays true, and the 64-bit load address keeps running past both arrays until the process faults.

Background: Lucene's opt-in native dot product

  1. On Lucene main, VectorUtil.dotProduct(byte[], byte[]) can call C code instead of Java. It is opt-in: you compile lucene/misc/src/c/dotProduct.c, which the file itself calls example code, into libdotProduct.so and start the JVM with -Dlucene.useNativeDotProduct=true.
  2. When the library loads, it picks a kernel for the CPU: vdot8s_sve on CPUs with SVE if the library was compiled with SVE enabled (as -march=native does on such a CPU; otherwise there is no SVE kernel), vdot8s_neon on other Arm CPUs with the dot-product extension, else a scalar loop.
  3. An SVE vector holds 16 to 256 bytes, depending on the CPU (VL). The SVE kernel handles 4 vectors per loop step: 128 bytes on a 256-bit SVE CPU, 64 bytes on a 128-bit one.
Diagram of the bug: the SVE kernel steps its loop index i by 128 bytes (4 vectors of 32 bytes) and tests i + 128 <= length in 32-bit int. For a byte array of 2,147,483,584 elements, at i = 2,147,483,520 the sum 2^31 does not fit and wraps negative, so the test stays true; the 64-bit load offset keeps growing past the end of the array until an unmapped page, and the JVM dies with SIGSEGV. With a 64-bit index the loop stops at the length and returns the exact result.
How the SVE kernel's 32-bit loop bound wraps negative at 2^31 while the 64-bit load offset keeps running past the end of the arrays. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. On a CPU with 256-bit SVE, where one vector is 32 bytes, compile the example native library from dotProduct.c with SVE enabled (-march=native does that there), put it on java.library.path and start the JVM with -Dlucene.useNativeDotProduct=true.
  2. Call VectorUtil.dotProduct(a, a) on an all-zero byte[] of 2,147,483,584 elements (2^31 - 64, about 2 GiB). You expect 0, which Lucene returns without the native flag.
  3. The SVE kernel steps i by 4 vectors, 128 bytes, and tests i + 128 <= length in 32-bit int. At i = 2,147,483,520 the sum is 2^31, one more than an int holds; in the compiled code it wraps negative, so the test stays true and the loop never stops.
  4. The loads use a 64-bit offset and run on past the array: at the fault the offset is 1,069,547,456 bytes past its end, and the whole JVM dies with SIGSEGV in vdot8s_sve, with assertions on and off.
  5. At the two vector lengths tested, every length from 2^31 - 4 x vector bytes crashes (2,147,483,520 at 256-bit SVE, 2,147,483,584 at 128-bit); one less returns the exact result. A 64-bit loop index fixes it.

Who is affected

Only applications on Lucene main (unreleased 11.0) that enable the opt-in native library (built with SVE), run on a CPU with SVE and themselves pass byte arrays of 2,147,483,520 or more elements (about 2 GiB) to VectorUtil.dotProduct or a byte similarity compare. Indexed vectors (at most 1,024 dimensions) and kNN cannot reach it. The whole JVM dies.

Mechanism, terms and numbers760 words

Lucene main can compute the byte dot product in C instead of Java: you compile the example lucene/misc/src/c/dotProduct.c into a library and pass -Dlucene.useNativeDotProduct=true. On a CPU with SVE, a library compiled with SVE enabled runs vdot8s_sve, which keeps its loop index i and the vector length in int32_t. Its loop test i + 4 * vec_length <= limit overflows once i + 4*VL passes INT32_MAX (VL = SVE vector length in bytes; at the tested VL = 32 and 16 that is at i = 2^31 - 4*VL): signed overflow, undefined behaviour in C.

In the GCC-built library the 32-bit bound wraps negative, so the test stays true, while the loads use a 64-bit offset that runs past the end of both arrays until an unmapped page. VectorUtil.dotProduct(byte[], byte[]) on an all-zero array of 2^31 - 64 elements, expected 0, kills the JVM with SIGSEGV every time in the GCC 10.5 builds tested, with assertions on and off. At the two vector lengths tested, every length from 2,147,483,520 (256-bit SVE) or 2,147,483,584 (128-bit SVE) up to the JVM maximum crashes; one less returns the exact result.

Only applications that enable the opt-in library on Lucene main (unreleased 11.0) and pass byte arrays of about 2 GiB themselves are hit; indexed vectors (at most 1,024 dimensions) and kNN cannot reach it, and default Lucene and every release are unaffected. Widening the index to int64_t and using svwhilelt_b8_s64 fixes it; the NEON kernel has the same source pattern, compiled correctly by GCC 10.5, and the same fix covers it.

The bug, step by step (main 645d4fe14a20)

  1. Java side (correct). VectorUtil.dotProduct (VectorUtil.java:251-256) checks only that the lengths are equal. With the flag, the provider is NativeVectorizationProvider (Constants.java:100-104, VectorizationProvider.java:168-170), and NativeVectorUtilSupport.dotProduct(byte[], byte[]) (:419-423, :303-306) passes the element count as the C limit (invokeIntMethodHandle, :246-252).
  2. Dispatch. resolve_dot8s (dotProduct.c:181-196) returns vdot8s_sve when the CPU reports SVE and the library was compiled with SVE enabled (#if defined(__ARM_FEATURE_SVE), :32, :185); lucene_dotProduct is bound to it through ifunc (:199-201). The resolver exists only on Linux aarch64; elsewhere lucene_dotProduct is the scalar loop (:202-206).
  3. The defect. vdot8s_sve(int8_t vec1[], int8_t vec2[], int32_t limit) (:50) declares int32_t i (:52) and int32_t vec_length = svcntb() (:63), and loops for (i = 0; i + 4 * vec_length <= limit; i += 4 * vec_length) (:66) with eight full-vector loads per step (:70-80); the tail uses svwhilelt_b8_s32 (:90, :96).
  4. The numbers (VL = 32 bytes, length 2,147,483,584). i runs 0, 128, 256, ...; at i = 2,147,483,520 the test computes i + 128 = 2^31, one more than the largest int32_t. Correct code would leave the main loop here and finish in the tail.
  5. The compiled loop (GCC 10.5 -O3). The bound sits in a 32-bit register with a signed compare (cmp w2, w5 / b.ge), the loads use a 64-bit offset (ld1b ... [x0, x9], add x9, x9, x17). The bound wraps to a negative number, which is <= limit. When 4*VL divides 2^31 (any power-of-two VL, including the tested 32 and 16) every later wrapped bound is <= limit too (negative until the offset reaches 2^32 - 4*VL, then 0, 4*VL, ...), so the loop can end only by faulting.
  6. At the fault: limit R2 = 0x7fffffc0 (2,147,483,584), load offset R9 = 0xbfbfff80 (1,069,547,456 bytes past the end), the 32-bit bound R5 = 0xbfc00080 (negative as int32_t), SEGV_MAPERR at 0x800000000, the end of the 3 GiB heap reservation.

The trigger window

At the tested vector lengths, every length n >= 2^31 - 4*VL up to the JVM's largest byte[] (2,147,483,645 on JDK 25): 2,147,483,520 to 2,147,483,645 at 256-bit SVE (126 lengths) and 2,147,483,584 to 2,147,483,645 at 128-bit SVE (62 lengths); 2,147,483,519 and 2,147,483,583 return the exact value. Both windows start below Lucene's own ArrayUtil.MAX_ARRAY_LENGTH (2,147,483,631 with compressed oops). The formula holds whenever 4*VL divides 2^31; for any other VL the first overflowing length would be ceil(2^31 / (4*VL)) * 4*VL - 4*VL (arithmetic, not run).

NEON

vdot8s_neon has int32_t i (:123) and i + 64 <= limit (:127): the same signed overflow in the source for lengths from 2^31 - 64. GCC 10.5 compiles it to a counted loop, and a NEON-only build returned the exact value at 2,147,483,631, so nothing was observed there. The scalar fallback tests i < limit and cannot overflow.

Exposure and likelihood265 words
  • Reachable through VectorUtil.dotProduct(byte[], byte[]) and its callers: VectorUtil.dotProductScore, byte VectorSimilarityFunction.DOT_PRODUCT.compare (through dotProductScore) and MAXIMUM_INNER_PRODUCT.compare, and the queries module's ByteVectorSimilarityFunction over two constant vectors. Use dotProduct itself as the oracle: dotProductScore is already wrong at these lengths for a separate reason (the hunt's LFV-143).
  • Not reachable with default Lucene (no flag), on any release (none has the native provider), on CPUs without SVE (the NEON build was correct with GCC 10.5), or through indexing and kNN: every stock vectors format caps dimensions at 1,024, about two million times below the trigger. Lucene's own off-heap byte scorers do call the native kernel, but one stored vector per call (getSegment slices one vector), so they do not widen the reach.
  • Effect: a deterministic process crash (SIGSEGV), with assertions on and off; not a catchable exception, no wrong result, no write. How far past the arrays the kernel reads before it faults depends on the memory layout (about 1 GiB in this doc's run, about 80 MB in a run with a 6 GB heap). The minimal repro passes one array twice; the independent reproducer's and the devil's advocate's programs pass two distinct arrays and crash the same way.
  • Why fix it anyway: it is the example the #15508 discussion tells users to compile, the only implementation of lucene_dotProduct in the repository, and undefined behaviour that another compiler may treat differently; the fix leaves results unchanged below the threshold (350 tests), a quick C microbenchmark showed no measurable speed difference at 64 to 4,096 bytes (not benchmarked through Lucene), and it can land before 11.0.0.

Minimal repro

One main() program on the public API: VectorUtil.dotProduct(a, a) on one all-zero byte[] of 2^31 - 64 elements, expecting 0. A main() rather than a JUnit test, because the JVM dies and a test runner could never report it. Needs -Xmx3g, a CPU with SVE and the library compiled from dotProduct.c; about 1.5 s.

NativeSveDotProductOverflow.java (one main(), public API)29 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.
 */

import org.apache.lucene.util.VectorUtil;

public class NativeSveDotProductOverflow {
  public static void main(String[] args) {
    byte[] a = new byte[Integer.MAX_VALUE - 63];
    // Expected 0 (2^31 - 64 zeros); actual with the native SVE kernel: SIGSEGV in vdot8s_sve.
    int dot = VectorUtil.dotProduct(a, a);
    if (dot != 0) {
      throw new AssertionError("expected 0, got " + dot);
    }
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20, lucene-core built (./gradlew :lucene:core:jar)
mkdir -p $REPRO/lib $REPRO/classes
gcc -shared -fPIC -O3 -march=native -funroll-loops -o $REPRO/lib/libdotProduct.so lucene/misc/src/c/dotProduct.c   # GCC 10.5.0 in every run, on an SVE CPU
CORE=lucene/core/build/libs/lucene-core-11.0.0-SNAPSHOT.jar
javac -cp $CORE -d $REPRO/classes $REPRO/NativeSveDotProductOverflow.java
java -ea -Xmx3g --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dlucene.useNativeDotProduct=true -Djava.library.path=$REPRO/lib -cp $REPRO/classes:$CORE NativeSveDotProductOverflow
# -da: the same crash; without -Dlucene.useNativeDotProduct=true the Panama provider returns 0
Expected
VectorUtil.dotProduct returns 0 and the program exits normally, as it does with the Panama provider.
Observed
The JVM dies: SIGSEGV (SEGV_MAPERR) in libdotProduct.so vdot8s_sve+0x80, with assertions on and off.

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

More files, environment, full output242 words

2^31 - 64 is the smallest length that crashes at both 256-bit and 128-bit SVE; one all-zero array passed twice keeps the memory at 2 GiB and the exact result at 0, so no accumulator overflow is involved. Deterministic, single thread, no fill loop. Every run built the library with GCC 10.5.0 and -march=native on an SVE CPU, which enables SVE (GCC 7.3 rejects the file's NEON target attribute; other compilers not checked). As a regression test it is the same program; Lucene's CI does not build the native library, so it fits a manual or native-mode check.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); the same at the introducing commit 50dac906310
jdkOpenJDK 25.0.3
osLinux aarch64, CPU with 256-bit SVE (128-bit SVE also checked); GCC 10.5.0

Observed output

# stock library, main 645d4fe14a20, -da (-ea identical apart from pc and pid)
INFO: Native vectorization enabled; uses preferredBitSize=256; FMA enabled; strict mode=false; native library loaded=true, using NativeVectorizationProvider;
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x0000ffff261ee920, pid=684, tid=707
#
# Problematic frame:
# C  [libdotProduct.so+0x920]  vdot8s_sve+0x80
#
# hs_err: si_code: 1 (SEGV_MAPERR), si_addr: 0x0000000800000000
#         R2=0x7fffffc0 (limit)  R9=0xbfbfff80 (64-bit load offset)  R5=0xbfc00080 (32-bit bound)
# Java frames: VectorUtil.dotProduct([B[B) <- NativeVectorUtilSupport.dotProduct([B[B) <- invokeIntMethodHandle <- downcall
# exit status 134

# fixed library (int64_t loop index), same program, -ea and -da
INFO: Native vectorization enabled; ... native library loaded=true, using NativeVectorizationProvider;
# exit status 0

Why it looks unintended

  • dotProduct.h requires each native function to return what the Java method it replaces returns, with no length bound; VectorUtil.dotProduct only requires equal lengths.
  • For the repro the Java providers return the exact 0; the C kernel commits undefined behaviour and kills the process.
  • No javadoc, test or discussion sets a length limit; #16812 fixed another crash in this path.
The full argument235 words
  • The contract. VectorUtil.dotProduct(byte[], byte[]): "Dot product computed over signed bytes", "@return the value of the dot product of the two vectors"; the only check is equal lengths (VectorUtil.java:244-256). dotProduct.h:18-23: "Each function must return what the VectorUtilSupport method it replaces returns"; n "is the number of elements of its FIRST array argument", with no upper bound. That header text came with #16812 (2026-10-12); #15508 added the header with only the bare declaration, so it is the contract of the current code, not one stated when the feature landed.
  • It is the default users are pointed to. In #15508 the PR author, shubhamvishu, proposed that users "optionally generate the binary on their end using gcc -shared -O3 -march=native -funroll-loops ..." and "could either use the default dotProduct.c file" or write their own. #16812 calls it "the example library in lucene/misc/src/c".
  • Legal input. Two equal-length arrays below Lucene's own ArrayUtil.MAX_ARRAY_LENGTH, passed to a public method.
  • Nothing chooses it. No javadoc, header comment, CHANGES or MIGRATE entry, test or commit message limits the length; the review threads of #15508 and #13572 never mention overflow, 2^31 or a 64-bit index. No Lucene test runs the native path in CI (#15840), and TestVectorUtilSupport stops at 4,098 elements; it passes with the stock library.
  • Precedent. #16812 (merged 2026-10-12) fixed a different SIGSEGV in this native path, on the Java side, with no "example code" objection.
The devil's-advocate check187 words

The strongest case for "not a bug": dotProduct.c:18 says "EXAMPLE CODE FOR DOT PRODUCT IMPLEMENTATION IN C"; Lucene does not build, ship or CI-test it; the provider is opt-in and @lucene.experimental; the org.apache.lucene.util package says its APIs are "primarily internal"; every in-tree caller passes dimensions capped at 1,024; and the Java byte path itself goes wrong far below this length in other ways (its int accumulator from about 2^17 saturated dimensions, dotProductScore from 65,536).

Why it fails: the header contract is explicit and unbounded; the input is legal and within ArrayUtil.MAX_ARRAY_LENGTH; in the repro the exact result is 0 and the Java providers return it, so no accumulator wrap is involved; Java wraps in a defined, memory-safe way, while the C kernel's undefined behaviour reads past both arrays and kills the JVM; the file is presented as the default users compile; and upstream fixed the same kind of failure here before. The example-code and scale points make it a low-priority hardening fix; they do not change the verdict. The independent reproducer, the devil's advocate itself, the adjudication and the cross-model reviewer all rejected the argument.

Suggested fix

Recommended 64-bit loop index in both kernels

Widen the SVE loop index and vector length to int64_t and use svwhilelt_b8_s64 in the tail; widen vdot8s_neon's index too (5 changed lines in dotProduct.c, no API change). Validated: the repro passes with assertions on and off, and 350 native-mode tests of TestVectorUtilSupport and TestVectorUtil pass.

-    int32_t i = 0;
+    int64_t i = 0;
-    int32_t vec_length = svcntb();
+    int64_t vec_length = svcntb();
-    svbool_t pred = svwhilelt_b8_s32(i, limit);
+    svbool_t pred = svwhilelt_b8_s64(i, limit);
-        pred = svwhilelt_b8_s32(i, limit);
+        pred = svwhilelt_b8_s64(i, limit);
     // vdot8s_neon
-    int32_t i = 0;
+    int64_t i = 0;
All options and trade-offs4 options ยท 515 words

The hunt's candidate fix, unchanged, applied with git apply in a scratch worktree of 645d4fe14a20 and built with GCC 10.5.0; the repro ran on the 645d4fe14a20 jars (the fix touches only C) and the closest suites through gradle with the native provider.

64-bit loop index in both kernels (validated by the checks) (recommended)

In vdot8s_sve: int64_t i (:52), int64_t vec_length (:63), svwhilelt_b8_s64 (:90, :96). In vdot8s_neon: int64_t i (:123). limit stays int32_t, as declared in dotProduct.h:28; the compiled bound compare becomes 64-bit (cmp x2, x6) and the tail uses whilelt p1.b, x3, x2.

-    int32_t i = 0;
+    int64_t i = 0;
-    int32_t vec_length = svcntb();
+    int64_t vec_length = svcntb();
-    svbool_t pred = svwhilelt_b8_s32(i, limit);
+    svbool_t pred = svwhilelt_b8_s64(i, limit);
-        pred = svwhilelt_b8_s32(i, limit);
+        pred = svwhilelt_b8_s64(i, limit);
     // vdot8s_neon
-    int32_t i = 0;
+    int64_t i = 0;

Trade-offs

  • No API or ABI change; results unchanged below the threshold (the 350 tests).
  • Not benchmarked through Lucene; a quick C microbenchmark of the stock and fixed libraries showed no measurable difference at 64 to 4,096 bytes (fix/stock 0.985 to 1.030). Only 256-bit SVE was run for this doc; 128-bit rests on the earlier run.

What the checks validated

Scratch worktree of 645d4fe14a20, OpenJDK 25.0.3, GCC 10.5.0, 256-bit SVE, seed DEADBEEF:

  • Minimal repro: stock library crashes with -ea and -da; fixed library passes with -ea and -da, native provider logged (repro harness and plain java).
  • Closest suites through gradle with -Dlucene.useNativeDotProduct=true and the fixed library: TestVectorUtilSupport (native kernel against the scalar default at 22 sizes up to 4,098) and TestVectorUtil: 350 tests, 0 failures, 16 skipped, assertions on and off; both classes log using NativeVectorizationProvider.
  • Vacuity: a mutant library returning result + 1 fails 47 of those tests, so they do run the native kernel; the stock library passes all 350, so they cannot see the bug.
  • Earlier: an independent reproducer's equivalent patch passes at 2,147,483,631 with 256-bit and 128-bit SVE; CBMC verifies the fixed source.

Keep int32_t and test i <= limit - 4 * vec_length (not run)

Removes the main-loop overflow, since limit - 4*VL cannot overflow for limit >= 0.

Trade-offs

The SVE tail's i += vec_length would then overflow for lengths within one vector of 2^31 (2,147,483,617 to 2,147,483,645 at 256-bit SVE, by arithmetic), so the tail needs the same care; more subtle than the type change for no gain.

Fall back to Java for very long arrays (not run)

NativeVectorUtilSupport could use the Panama path above some length.

Trade-offs

Protects against any user-built library with the same flaw, but adds a branch to a hot call and leaves the example's undefined behaviour in place; a complement, not a replacement.

Document a length limit in dotProduct.h (not recommended)

State that n must stay below 2^31 - 4*VL.

Trade-offs

The Java API and the other providers accept every legal length, and the header's contract says each function returns what the Java method returns.

Downstream note

Downstream relevance only: an application that enables the native library and builds byte vectors from untrusted input with no length limit would let a request carrying a vector of about 2 GiB crash the JVM; no known downstream does this.

How it was found

Bounded model checking (CBMC 6.11) of the real dotProduct.c, with models of the SVE and NEON intrinsics and the main loop allowed to start at any reachable iteration near 2^31: the signed-overflow check failed at line 66. An AI tool then reproduced the crash through VectorUtil; independent checkers confirmed it, most with their own programs and library builds.

The method in full197 words

Method: formal (the tool's own counterexample), hunt unit on the native dot product.

  • An AI tool wrote CBMC harnesses for the unmodified dotProduct.c, with semantic models of the SVE and NEON intrinsics. Its bounded matrix over ordinary lengths (2,089 runs, lengths up to 384) and a differential test of the real library against Java (0 mismatches; a deliberately wrong library gave 435,918) found nothing.
  • To cover lengths near 2^31, it replaced only the loop index's initial value so the main loop could start at any reachable iteration. CBMC's signed-overflow check then failed at dotProduct.c:66 (for example limit = i = 2,147,483,520 at 32-byte vectors) and at the NEON loop (:127); the fixed source verifies. Choosing to probe near 2^31 was the AI tool's decision, so if that counts as a hypothesis the tier is semi-formal.
  • The AI tool then ran the crash through the public VectorUtil.dotProduct; an independent reproducer, a devil's advocate and a reviewer from another model family (OpenAI GPT-5.6) each reproduced it with their own programs, and an adjudication re-ran the reproducer's program; all but the cross-model reviewer built their own library (it ran the supplied one, whose source matches main).

Prior reports, history and checks

Prior reports

No prior report found in apache/lucene issues, PRs, review comments, commits and open PRs' files, the mailing lists, JIRA, downstream projects and the web (2026-10-26 and 2026-10-31). Closest: #16812, a different crash in the same native path, fixed on the Java side.

Searches5 searches

Closest items: #16812 (merged 2026-10-12) fixed a different SIGSEGV in this path (float lengths passed as byte counts) and only renamed the symbol in dotProduct.c; #15508 (the introducing PR) and #13572 (the original kernels, closed unmerged) never discuss the index width; open #16907 (a scalar C test fixture, lengths up to 257), #16795 (Java-side tests), #16799 (new int4 kernels, vdot8s_* untouched) and #16790; issue #15840 (no CI coverage of the native path). elastic/elasticsearch#143232 is the same bug class in another project's own native code.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, review comments and commits (apache/lucene)2026-10-26; repeated 2026-10-31the hunt's search tool plus GraphQL searches: vdot8s*, svwhilelt*, svcntb, lucene_dotProduct, useNativeDotProduct, NativeVector*, misc/src/c, libdotProduct, int32 / 2^31 / signed overflow, SIGSEGV; key threads downloaded and greppedno report; related #16812, #15508, #13572, #16907, #16795, #16799, #16790, #15840
Open PRs2026-10-31the files touched by all 410 open PRs; every PR touching the native files read with its patchnone changes the vdot8s_* loop index
Mailing lists (dev@, java-user@, issues@)2026-10-26; 2026-10-31tool queries and direct lists.apache.org searches (SVE, SIGSEGV, native vectorization, int32 overflow)no report
JIRA archive2026-10-31tool queriesno report (the native code postdates the JIRA migration)
Downstream, all of GitHub and the web2026-10-26; 2026-10-31Elasticsearch, OpenSearch, Solr, GitHub-wide code search for vdot8s_sve, a sample of web queriesno report; the only copy of vdot8s_sve is Lucene's file; elastic/elasticsearch#143232 is the same class elsewhere

History

Introduced with the feature itself by #15508 (50dac906310, merged 2026-02-17); the only later change to the file renames the exported symbol (#16812). On main and branch_11x, unreleased; no release has the native provider. Not fixed upstream.

Versions tested5 rows

Introduced by 50dac9063104, "Use native vectorization in Lucene (#15508)" (shubhamvishu; PR opened 2025-12-17, merged 2026-02-17; CHANGES lists it under 11.0.0, crediting Ankur Goel, Shubham Chaudhary, Dawid Weiss). It added dotProduct.c with the 32-bit index, loop test and tail, together with the provider and the flag; git blame attributes every defect line to it. 54c6dc8becd (#16812) only renamed the exported symbol to lucene_dotProduct.

Not fixed upstream (2026-10-31): main 2f424fd428da and branch_11x have the same dotProduct.c blob. First affected release would be 11.0.0.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedthe repro crashes with -ea and -da; 2,147,483,519 exact, 2,147,483,520 crashes (re-run separately; earlier on ea57360912ed, same dotProduct.c); the fix passes
50dac906310 (introducing commit) affectedthe repro crashes with -ea and -da; same boundary
main ea57360912ed, a9779ef3c193 affectedstill reproduces (verification runs)
parent d02fb2a4cc7 not affectedno native provider; the flag is ignored and Panama returns 0
10.6.0, 10.5.2, 9.12.3 not affectedno native provider; the repros pass

Independent checks

Confirmed with high confidence by the adjudication after an independent reproducer, an intended-behaviour devil's advocate and a review by another model family (OpenAI GPT-5.6), none dissenting; re-run on three mains, and the fix validated here with the repro and 350 native-mode tests.

Checks and verdicts250 words

The finder, the independent reproducer, the devil's advocate and the cross-model reviewer each wrote their own program; the adjudication re-ran the independent reproducer's program. All but the cross-model reviewer built their own library from the unmodified dotProduct.c (it ran the supplied library, whose source matches main).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateCBMC counterexample at line 66; repro at 2,147,483,645 crashes with -ea and -da; Panama and default providers exact; boundaries at 256-bit and 128-bit SVE; fixed source verifies.
Independent reproducer realOwn program at 2,147,483,631 with its own -O3 and -O2 libraries crashes; registers and disassembly; exact boundaries at both vector lengths; NEON-only build exact; own fix passes; 10.5.2 and 9.12.3 pass.
Intended-behaviour devil's advocate real, low impactOwn random-data program crashes with -ea and -da; the strongest example-code argument was built and rejected.
adjudication confirmed, highRe-ran the independent reproduction's program with its own library: 2,147,483,631 and 2,147,483,520 crash, 2,147,483,519 returns the exact value.
refute review (OpenAI GPT-5.6) real, highOwn program, with the supplied library, crashes at 2,147,483,584 with -ea and -da, passes at 2,147,483,519, with the flag off and with the fixed library; intended argument moderate.
re-verification still reproducesMain a9779ef3c193 (2026-10-26) and 645d4fe14a20 (2026-10-29, 2026-10-31); 10.5.2 passes.
this doc (2026-10-31) real, fix validatedStock library crashes, fixed library passes (repro harness and plain java, -ea and -da); 350 native-mode tests pass with the fix, 47 fail with a mutant library.

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

Finding record

LFV-390, snapshot of 2026-11-06

Registry title
The example native int8 dot product (lucene/misc/src/c/dotProduct.c, opt-in via -Dlucene.useNativeDotProduct=true) keeps its SVE loop index in int32, so VectorUtil.dotProduct(byte[], byte[]) on two arrays of at least 2^31 - 4*VL elements (2,147,483,520 at 256-bit SVE, 2,147,483,584 at 128-bit SVE) overflows the loop bound, reads past the end of both arrays and kills the JVM with SIGSEGV in vdot8s_sve
Area
core vectorization + misc native C / opt-in native int8 dot product (NativeVectorUtilSupport, lucene/misc/src/c/dotProduct.c vdot8s_sve)
Priority
Low, rank 988: The JVM crashes only with the unreleased opt-in example native library on SVE and byte arrays of about 2^31 elements, an extreme scale that indexed vectors cannot reach.
Impact class
crash
Impact note
Only an application that runs Lucene main (unreleased 11.0.0) with -Dlucene.useNativeDotProduct=true and the example library it compiled itself from lucene/misc/src/c/dotProduct.c, on a CPU with SVE, and that itself passes two equal-length byte arrays of 2,147,483,520 to 2,147,483,645 elements (256-bit SVE; 2,147,483,584 to 2,147,483,645 at 128-bit SVE), about 2 GiB each, to VectorUtil.dotProduct; the whole JVM then dies with SIGSEGV every time (-ea and -da), not a catchable exception; indexed vector fields and kNN queries cannot reach it (dimensions are capped at 1024 in every stock format), default Lucene (no native flag) and every release are unaffected, and the NEON kernel's identical source-level overflow (line 127) compiles to a correct loop with GCC 10.5, so no misbehaviour was observed there.
Summary
You enable Lucene's opt-in native dot product (-Dlucene.useNativeDotProduct=true plus a library built from the example lucene/misc/src/c/dotProduct.c) on an SVE machine and call VectorUtil.dotProduct(byte[], byte[]) on two legal arrays of 2,147,483,645 bytes whose exact dot product is 3, expecting 3 as the Panama and default providers return. Instead the SVE kernel vdot8s_sve keeps its loop index and vector length in int32_t, so its loop test 'i + 4 * vec_length <= limit' (dotProduct.c:66) overflows once i reaches 2^31 - 4*VL (VL = SVE vector length in bytes); in the compiled code the bound wraps negative while the load offset is a 64-bit register, so the loop keeps loading past the end of both arrays until it hits an unmapped page and the JVM dies with SIGSEGV in vdot8s_sve. Every length from 2^31 - 4*VL up to the JVM maximum crashes (2,147,483,520 at 256-bit SVE, 2,147,483,584 at 128-bit SVE), while one element less returns the exact result; a 64-bit loop index with svwhilelt_b8_s64 fixes it.
Introduced
50dac9063104d798c795f4ab23de44c871272bad on main (apache/lucene#15508 'Use native vectorization in Lucene', opened and merged by GitHub shubhamvishu) [read: git blame attributes every defect line (dotProduct.c 52, 63, 66, 90-96 for SVE; 123, 127 for NEON) to it; git log --follow finds only 50dac906310 (file added) and 54c6dc8becd (#16812, which only renames the exported symbol dotProduct to lucene_dotProduct); pickaxe over all refs for 'vdot8s', 'i + 4 * vec_length <= limit' and 'useNativeDotProduct' finds only 50dac906310] [exec: the commit fails with -ea and -da and has the same 2^31 - 4*VL boundary; its parent d02fb2a4cc7 passes because the native path does not exist there]. The commit added the whole native path (provider, flag, C file), so the bug is as old as the feature, not a later regression (2026-02-17 (merged 2026-02-17T13:17:54Z; PR opened 2025-12-17))
First affected version
unreleased (11.0.0 would be the first release: CHANGES.txt lists GITHUB#15508 under Lucene 11.0.0; main / 11.0.0-SNAPSHOT since 50dac906310)
Affected versions
main from 50dac906310 (2026-02-17) to 645d4fe14a2 [exec: -ea and -da at both ends and at 645d4fe14a2 (2026-10-29); exact boundary at 256-bit SVE: 2147483519 returns the exact value, 2147483520 = 2^31-128 crashes, at the introducing commit and on main; at 128-bit SVE: 2147483583 returns the exact value, 2147483584 = 2^31-64 crashes, on a second CPU with 128-bit SVE2 (the finder with main jars, the follow-up with the introducing commit) and on the 256-bit CPU with the vector length lowered by prctl (main)]; upstream main cf398bfb142 (checked 2026-10-26) has a byte-identical dotProduct.c and no later commit touches it [read]. Not affected: every 9.x and 10.x release up to 10.6.0, and the branch_10x, branch_10_6 and branch_10_5 tips of 2026-10-24, which have no native provider [exec: 10.6.0 and 10.5.2 (Panama; 10.6.0 on the release jars, SHA-1 checked, -ea and -da, 2026-10-31) and 9.12.3 (default provider) return the exact value; read: release tags and branch tips]; the parent commit d02fb2a4cc7 [exec: pass]. NEON (vdot8s_neon, dotProduct.c:127): the same signed overflow in the source for lengths >= 2^31-64, but GCC 10.5 -O3 compiles it to a counted loop and a NEON-only build returns the exact value at 2147483631 [exec]. All runs: OpenJDK 25.0.3, Linux aarch64, libraries built with GCC 10.5.0 -shared -O3 -march=native -funroll-loops (the independent reproduction: an -O2 build also crashes); other compilers and flags not checked
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
formal: CBMC
Upstream and prior reports
none found (2026-10-26, follow-up search plus the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches; verdict new; all read-only): 204 queries over apache/lucene issues, PRs and commits (a scripted prior-report search and GitHub GraphQL, identifiers searched alone too), Elasticsearch, OpenSearch, OpenSearch k-NN, Solr and all of GitHub, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web; 17 hits read, none the same bug. Closest: #16812 (merged 2026-10-12), a different SIGSEGV in the same native path (the Java side passed byte counts as element counts for the float functions; its only dotProduct.c change renames the symbol); #15508 (introducing PR) and #13572 (closed unmerged original vdot8s kernels), whose review threads never mention overflow, 2^31 or a 64-bit index; open #16799 (adds int4 kernels, leaves vdot8s_* unchanged), #16907 (opened 2026-10-26, a C test fixture with lengths <= 257, no production change), #16795 (native smoke check at lengths 0, 1, 3, 128, 768) and issue #15840 (native path has no CI coverage). elastic/elasticsearch#143232 is the same bug class in another project's own native scorer. All 411 open apache/lucene PRs were checked for files touched; none changes the vdot8s loop index. Not fixed on upstream main cf398bfb142 (byte-identical dotProduct.c). Gaps: web search is a 7-query sample, and private security@ reports cannot be searched; upstream main 645d4fe14a2 still has a byte-identical dotProduct.c (sha1 f8c9d4083dd5) [exec 2026-10-29]
Lucene commit tested
main 645d4fe14a20 (this doc, 2026-10-31): the minimal program with libraries built by GCC 10.5.0 from the stock and the fixed dotProduct.c on a 256-bit SVE CPU, assertions on and off, through the repro harness and plain java; gradle TestVectorUtilSupport and TestVectorUtil with the native provider and the fixed, stock and a mutant library, in a scratch worktree. Earlier checks: the introducing commit 50dac906310 and its parent, main ea57360912ed and a9779ef3c193, 10.6.0, 10.5.2 and 9.12.3, the exact boundaries at 256-bit and 128-bit SVE. All runs on Linux aarch64 with OpenJDK 25.0.3. Upstream main 2f424fd428da (2026-10-30) and branch_11x have the same dotProduct.c.

Lucene and upstream sources

Finding record
  • lucene/misc/src/c/dotProduct.c:18 (at ea57360912ed) ('EXAMPLE CODE FOR DOT PRODUCT IMPLEMENTATION IN C'), :50-108 (vdot8s_sve: int32_t i :52, int32_t vec_length = svcntb() :63, loop test 'i + 4 * vec_length <= limit' :66, tail svwhilelt_b8_s32 / i += vec_length :90-96), :117-166 (vdot8s_neon: int32_t i :123, 'i + 64 <= limit' :127), :181-207 (ifunc dispatch, lucene_dotProduct)
  • lucene/misc/src/c/dotProduct.h:18-23 (at ea57360912ed) ('Each function must return what the VectorUtilSupport method it replaces returns'; n = element count, no upper bound)
  • lucene/core/src/java25/org/apache/lucene/internal/vectorization/NativeVectorUtilSupport.java:57 (at ea57360912ed) (@lucene.experimental), :246-252 (invokeIntMethodHandle passes (int) a.byteSize()), :418-423 (dotProduct(byte[], byte[]) goes native when lucene_dotProduct is bound)
  • lucene/core/src/java/org/apache/lucene/util/VectorUtil.java:244-256 (at ea57360912ed) (javadoc 'Dot product computed over signed bytes'; only an equal-length check)
  • lucene/core/src/java/org/apache/lucene/util/Constants.java:96-100 (at ea57360912ed) and lucene/core/src/java/org/apache/lucene/internal/vectorization/VectorizationProvider.java:168-170 (native provider only with -Dlucene.useNativeDotProduct=true); lucene/CHANGES.txt:164 (GITHUB#15508 under 11.0.0)
  • lucene/core/src/java/org/apache/lucene/util/Constants.java:100-104 (NATIVE_DOT_PRODUCT_ENABLED; moved by 4 lines when #16723 added HAS_AVX512 and made HAS_AVX2/HAS_SVE public)
The finding in 30 seconds
  • lucene/misc/src/c/dotProduct.c:18, (at 645d4fe14a20) :50-108 (int32_t i :52, vec_length :63, loop test :66, loads :70-80, tail :90-96), :117-166 (NEON :123, :127), :170-179 (scalar), :23, :32, :181-206 (SVE compiled only with __ARM_FEATURE_SVE, dispatch, ifunc, non-Linux scalar fallback)
  • lucene/core/src/java/org/apache/lucene/util/VectorUtil.java:244-256; (at 645d4fe14a20) lucene/core/src/java25/org/apache/lucene/internal/vectorization/NativeVectorUtilSupport.java:85, :246-252, :303-306, :419-423; lucene/core/src/java/org/apache/lucene/util/Constants.java:100-104; lucene/core/src/java/org/apache/lucene/internal/vectorization/VectorizationProvider.java:168-170; lucene/core/src/java/org/apache/lucene/util/ArrayUtil.java:33-34
Who is affected
  • lucene/core/src/java/org/apache/lucene/util/VectorUtil.java:434-437; (at 645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/VectorSimilarityFunction.java:65-68, :110-113; lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/ByteVectorSimilarityFunction.java:47
  • lucene/core/src/java25/org/apache/lucene/internal/vectorization/Lucene99MemorySegmentByteVectorScorer.java:68-76, (at 645d4fe14a20) :123, :159; Lucene99MemorySegmentByteVectorScorerSupplier.java:81-96, :162, :244 (native calls on one stored vector)
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/util/VectorUtil.java:244-256; (at 645d4fe14a20) lucene/misc/src/c/dotProduct.h:18-23, :28; lucene/misc/src/c/dotProduct.c:18; lucene/core/src/java25/org/apache/lucene/internal/vectorization/NativeVectorUtilSupport.java:52-57; lucene/core/src/java/org/apache/lucene/util/package-info.java:35; lucene/core/src/java/org/apache/lucene/util/ArrayUtil.java:33-34
History
  • lucene/CHANGES.txt:170; (at 645d4fe14a20) a Lucene clone: releases/lucene/10.6.0 has no lucene/misc/src/c; origin/main 2f424fd428d and origin/branch_11x 603af870375: dotProduct.c blob 1fbecbafc075

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

Repro files

About the diagrams

Made from the code at 645d4fe14a20 and the executed runs.

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.