High chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WebML
DescriptionInteger overflow in WebML
ComponentWebML
Bug ClassInteger Overflow
Tracker481776048
Fix commit233323a72bc9 (chromium/src) +42/-2
CISA KEVNot listed
Creditedcinzinga
Disclosed2026-03-10

Background

WebNN
The Web Neural Network API that lets web content build and run machine-learning computation graphs through the browser’s services/webnn process.
`SCATTER_ND`
A TFLite kernel that writes updates values into an output tensor at positions named by an indices tensor.
`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 indices tensor 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.

Key insight
The single core mistake was trusting runtime-computed 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

  1. Craft a graph A malicious page uses the WebNN API to build a computation graph containing a scatterND operator.
  2. Supply hostile indices The page provides an indices tensor holding an out-of-bounds value such as 2147483647 (INT32_MAX) for a small input tensor.
  3. Serialize unchecked GraphBuilderTflite::SerializeScatterND forwards indices_tensor_info.index to the TFLite SCATTER_ND kernel with no clamping.
  4. 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.
  5. Out-of-bounds write The updates value is written to the miscomputed address, corrupting memory in the WebNN service.

Impact Assessment

An attacker who can run WebNN graphs from web content gains an out-of-bounds write driven by an integer overflow in the WebNN/TFLite backend, executing in the 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.cc
  • third_party/blink/web_tests/external/wpt/webnn/conformance_tests/scatterND.https.any.js

Audit Directions

  • Runtime-computed indices
    Audit 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 assumptions
    Whenever 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 reuse
    Compare related operators such as GatherND and ScatterND to ensure both apply the same SerializeGatherIndices-style sanitization, since divergence signals a missed check.
From 233323a72bc99df5c860c6da5711e1fb348686ac Mon Sep 17 00:00:00 2001
From: Reilly Grant <[email protected]>
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 <[email protected]>
Commit-Queue: Hu, Ningxin <[email protected]>
Reviewed-by: Hu, Ningxin <[email protected]>
Commit-Queue: Reilly Grant <[email protected]>
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'}
+        }
+      }
+    }
   }
 ];
 
Loading diff…

Regression Test / PoC

shipped with the fix
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'}
+        }
+      }
+    }
   }
 ];
Loading diff…

Original Bug Report

reported by [email protected]

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,736
  • to_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

  1. Kill all Chrome instances, relaunch with flags above
  2. Open scatternd_heap_oob.html, click “Trigger OOB Write”
  3. 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, ASan heap-buffer-overflow
  • crash_chrome_stable.ips — Stable 144 crash
  • crash_report_chrome_dev.ips — Dev 146 crash
  • crash_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

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

View on issue tracker