CVE-2026-3914
Overview
Background
- WebNN
- The Web Neural Network API that lets web content build and run machine-learning computation graphs through the browser’s
services/webnnprocess. - `SCATTER_ND`
- A TFLite kernel that writes
updatesvalues into an output tensor at positions named by anindicestensor. - `ScatterND`
- The WebNN operator that scatters values into a copy of an input tensor at runtime-supplied indices, lowered onto the TFLite backend.
- Runtime indices
- Index values inside the
indicestensor that are computed while the graph executes rather than validated at build time, so they may fall outside the input’s bounds.
Root Cause Analysis
The vulnerable path is GraphBuilderTflite::SerializeScatterND in graph_builder_tflite.cc, which serialized the WebNN ScatterND operator by forwarding the caller-controlled indices tensor directly to the TFLite SCATTER_ND kernel via indices_tensor_info.index. The invariant that every scatter index must lie within [-N, N-1] of the target dimension was never enforced, and the TFLite SCATTER_ND kernel neither supports negative indices nor bounds-checks positive ones. Because the indices values are produced at runtime, an attacker could supply a value such as INT32_MAX that, when used to compute a destination offset, overflows the integer arithmetic and addresses memory outside the output tensor.
The fix routes the indices tensor through SerializeGatherIndices<int32_t>, the same clamp-and-normalize helper already used for GatherND, so out-of-range values are clamped into [-N, N-1] and negative indices are rewritten to their positive equivalents before reaching the kernel. This restores the bounds invariant at graph-build time, so the kernel only ever receives in-range indices and the overflowing offset computation can no longer occur.
indices and handing them to a SCATTER_ND kernel that performs no bounds or negative-index handling; the fix reuses the existing SerializeGatherIndices<int32_t> clamping so indices are constrained to [-N, N-1] and normalized before serialization.Attack Path
- Craft a graph
A malicious page uses the WebNN API to build a computation graph containing a
scatterNDoperator. - Supply hostile indices
The page provides an
indicestensor holding an out-of-bounds value such as2147483647(INT32_MAX) for a small input tensor. - Serialize unchecked
GraphBuilderTflite::SerializeScatterNDforwardsindices_tensor_info.indexto the TFLiteSCATTER_NDkernel with no clamping. - Trigger the overflow Executing the graph makes the kernel use the huge index to compute a destination offset, overflowing the integer arithmetic and pointing outside the output tensor’s allocation.
- Out-of-bounds write
The
updatesvalue is written to the miscomputed address, corrupting memory in the WebNN service.
Impact Assessment
services/webnn service process rather than the sandboxed renderer. The only precondition is the ability to build and execute a scatterND graph with attacker-controlled indices, which ordinary untrusted web content can do; the memory corruption can serve as a primitive toward further compromise of that process.Files Changed
services/webnn/tflite/graph_builder_tflite.ccthird_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
Audit Directions
- Runtime-computed indicesAudit every operator that forwards a caller-supplied index or offset tensor to a backend kernel and confirm it is clamped and range-normalized before serialization.
- Kernel bounds assumptionsWhenever a TFLite or other backend kernel omits its own bounds and negative-index handling, verify the WebNN serializer compensates rather than trusting the framework.
- Shared helper reuseCompare related operators such as
GatherNDandScatterNDto ensure both apply the sameSerializeGatherIndices-style sanitization, since divergence signals a missed check.
Patch
From 233323a72bc99df5c860c6da5711e1fb348686ac Mon Sep 17 00:00:00 2001
From: Reilly Grant <reillyg@chromium.org>
Date: Thu, 05 Feb 2026 18:12:58 -0800
Subject: [PATCH] webnn: Support negative indices and clamping for ScatterND in TFLite
The TFLite SCATTER_ND kernel does not support negative indices and may
exhibit undefined behavior if indices are out of bounds. so clamp the
values in `indices` to be in range of [-N, N-1] and transform negative
indices to positive the same as GatherND.
Bug: 481776048
Change-Id: I8dc11ca07400b30df82ad265efdeef88ebbfbbfe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7546434
Reviewed-by: Robbie McElrath <rmcelrath@chromium.org>
Commit-Queue: Hu, Ningxin <ningxin.hu@intel.com>
Reviewed-by: Hu, Ningxin <ningxin.hu@intel.com>
Commit-Queue: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1580553}
---
diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index cbba4a0..8050528 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -7889,11 +7889,19 @@
SerializeInputTensorInfo(scatter_nd.input_operand_id));
ASSIGN_OR_RETURN(const TensorInfo& indices_tensor_info,
SerializeInputTensorInfo(scatter_nd.indices_operand_id));
+
+ CHECK_EQ(indices_tensor_info.data_type, ::tflite::TensorType_INT32);
+ // The values in `indices` are computed at runtime, so they can exceed the
+ // boundary of the input. Clamp the values in `indices` to be in range of
+ // [-N, N-1] and transform negative indices to positive as TFLite doesn't
+ // support negative indexing, the logic is the same as GatherND.
+ ASSIGN_OR_RETURN(
+ const TensorIndex indices_tensor_index,
+ SerializeGatherIndices<int32_t>(indices_tensor_info, input_tensor_info));
const TensorIndex output_tensor_index =
SerializeOutputTensorInfo(scatter_nd.output_operand_id).index;
return SerializeWebNNScatterND(input_tensor_info, updates_tensor_info,
- indices_tensor_info.index,
- output_tensor_index);
+ indices_tensor_index, output_tensor_index);
}
auto GraphBuilderTflite::SerializeSlice(const mojom::Slice& slice)
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
index 1384e28..337e1c1 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
@@ -160,6 +160,38 @@
}
}
}
+ },
+ {
+ 'name': 'scatterND 2D int8 tensors with index out of bound',
+ 'graph': {
+ 'inputs': {
+ 'input': {
+ 'data': [0, 0],
+ 'descriptor': {shape: [2, 1], dataType: 'int8'}
+ },
+ 'indices': {
+ 'data': [2147483647 /* INT32_MAX */],
+ 'descriptor': {shape: [1, 1], dataType: 'int32'}
+ },
+ 'updates': {
+ 'data': [1],
+ 'descriptor': {shape: [1, 1], dataType: 'int8'}
+ }
+ },
+ 'operators': [{
+ 'name': 'scatterND',
+ 'arguments': [
+ {'input': 'input'}, {'indices': 'indices'}, {'updates': 'updates'}
+ ],
+ 'outputs': 'output'
+ }],
+ 'expectedOutputs': {
+ 'output': {
+ 'data': [0, 1],
+ 'descriptor': {shape: [2, 1], dataType: 'int8'}
+ }
+ }
+ }
}
];
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
index 1384e28..337e1c1 100644
--- a/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
+++ b/third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js
@@ -160,6 +160,38 @@
}
}
}
+ },
+ {
+ 'name': 'scatterND 2D int8 tensors with index out of bound',
+ 'graph': {
+ 'inputs': {
+ 'input': {
+ 'data': [0, 0],
+ 'descriptor': {shape: [2, 1], dataType: 'int8'}
+ },
+ 'indices': {
+ 'data': [2147483647 /* INT32_MAX */],
+ 'descriptor': {shape: [1, 1], dataType: 'int32'}
+ },
+ 'updates': {
+ 'data': [1],
+ 'descriptor': {shape: [1, 1], dataType: 'int8'}
+ }
+ },
+ 'operators': [{
+ 'name': 'scatterND',
+ 'arguments': [
+ {'input': 'input'}, {'indices': 'indices'}, {'updates': 'updates'}
+ ],
+ 'outputs': 'output'
+ }],
+ 'expectedOutputs': {
+ 'output': {
+ 'data': [0, 1],
+ 'descriptor': {shape: [2, 1], dataType: 'int8'}
+ }
+ }
+ }
}
];
Original Bug Report
WebNN ScatterND integer overflow in TFLite bounds check allows for 512MB controlled heap OOB write
Report description
WebNN ScatterND integer overflow in TFLite bounds check allows for 512MB controlled heap OOB write
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src/+/main/services/webnn/tflite/graph_builder_tflite.cc
The problem
Please describe the technical details of the vulnerability
Vulnerability Details
SerializeScatterND (graph_builder_tflite.cc:7879) passes runtime indices directly to TFLite with no bounds validation. The other scatter/gather ops either clamp (Gather, GatherND via SerializeGatherIndices) or reject runtime indices entirely (GatherElements, ScatterElements — constant-only).
TFLite’s bounds check in reference_ops::ScatterNd (third_party/tflite/src/tensorflow/lite/kernels/internal/reference/reference_ops.h) has an int32 overflow:
if (to_pos < 0 || to_pos + slice_size > output_flat_size) {
return kTfLiteError;
}
With shape=[3, 536870912] int8 and index 3:
to_pos= 1,610,612,736to_pos + slice_size= 2,147,483,648 → wraps to -2,147,483,648- -2,147,483,648 > 1,610,612,736 → false → bypassed
Result: 536,870,912 bytes of attacker-controlled data written past the buffer end. The attacker controls the write content via the updates tensor.
The write occurs in the GPU process.
Bisect: Introduced in b4d4b4cb019bd7240a52daa4ba61e3cc814f0384 (fix for CVE-2022-35939, TF 2.10.0).
Suggested fix:
if (to_pos < 0 || static_cast<int64_t>(to_pos) + slice_size > output_flat_size) {
Version
Chrome: 144.0.7559.110 (Stable), 146.0.7655.0 (Dev)
OS: macOS 15.7.2, Windows 11. Cross-platform.
Flags: --enable-features=WebMachineLearningNeuralNetwork --in-process-gpu
WebNN is behind a flag (W3C spec implementation in progress). --in-process-gpu is for crash visibility only, the bug triggers without it.
Reproduction
- Kill all Chrome instances, relaunch with flags above
- Open
scatternd_heap_oob.html, click “Trigger OOB Write” - Chrome crashes —
EXC_BREAKPOINT / SIGTRAP(PartitionAlloc heap corruption)
Standalone proof (no browser):
clang++ -O2 -fno-strict-overflow -fsanitize=address -g -o bug370_asan bug370_asan_proof.cc
./bug370_asan
Symbolized stack trace (thread 0, main thread):
#0 __pthread_kill + 10
#1 pthread_kill + 259
#2 abort + 126
#3 __sanitizer::Abort() + 88
#4 __sanitizer::Die() + 97
#5 __asan::ScopedInErrorReport::~ScopedInErrorReport() + 1300
#6 __asan::ReportGenericError() + 1782
#7 __asan_report_load1 + 54
#8 ScatterNd_vulnerable() at bug370_asan_proof.cc:98 ← OOB access detected here
#9 test_overflow_exploit() at bug370_asan_proof.cc:191 [inline]
#10 main() at bug370_asan_proof.cc:262
#11 start + 3056
Attachments:
scatternd_heap_oob.html— Browser PoC (59 lines)bug370_asan_proof.cc— Standalone proof, ASanheap-buffer-overflowcrash_chrome_stable.ips— Stable 144 crashcrash_report_chrome_dev.ips— Dev 146 crashcrash_asan_standalone.ips— ASan crash with symbolication
Crash Details
Type: GPU process
Stable 144.0.7559.110 (2/2):
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Termination Reason: Trace/BPT trap: 5
Faulting Thread: ThreadPoolSingleThreadSharedForegroundBlocking4
Dev 146.0.7655.0 (5/5):
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Termination Reason: Trace/BPT trap: 5
Faulting Thread: ThreadPoolForegroundWorker
Both are PA_IMMEDIATE_CRASH() from PartitionAlloc detecting heap metadata corruption.
lldb on ASan Chromium , the PoC fills the updates tensor with 0x42. Breakpoint on EvalScatterNd<int>, int8 output tensor at 0x29E183840, size 0x60000000. After return, 0x42 appears at every OOB position:
(lldb) memory read -c 64 0x2FE183840
0x2fe183840: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0x2fe183850: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0x2fe183860: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0x2fe183870: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
(lldb) memory read -c 16 0x30E183840 # +256MB
0x30e183840: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
(lldb) memory read -c 16 0x31E183840 # +512MB (first byte past the write)
0x31e183840: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
512MB of attacker-controlled 0x42 past the buffer boundary, stopping exactly at slice_size.
Note: ASan Chromium doesn’t flag the OOB because TFLite’s SimpleMemoryArena allocates a single contiguous buffer for all tensors (~6.4GB), and the OOB write stays within it. The standalone proof uses new[] with normal red zones, so ASan triggers immediately.
Impact analysis
Any website can trigger a 512MB attacker-controlled heap write in Chrome’s GPU process if the user has enabled the WebNN feature flag. The attacker controls the write content (via the updates tensor) and the write offset (via the indices tensor). No user interaction is required beyond navigating to the page. The GPU process runs with a weaker sandbox than the renderer and has IPC channels to the browser process, making this a potential stepping stone for sandbox escape.
The cause
What version of Chrome have you found the security issue in?
Stable, Dev
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a non-sandboxed process)
How would you like to be publicly acknowledged for your report?
cinzinga