Critical watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebML
DescriptionHeap buffer overflow in WebML
ComponentWebML
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker493310462
Fix commitb476481b77f6 (external/github.com/tensorflow/tensorflow) +12/-2
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Reported2026-03-17 · bounty $43000 (per vendor advisory)
Fix landed2026-03-24 (public git)
Disclosed2026-05-12
Patch gap49 days — fix public in source before the advisory

Background

`STRIDED_SLICE`
a TensorFlow Lite operator (used as the WebML/WebNN backend in Chrome) that extracts a strided sub-range of an input tensor, whose output shape is computed per dimension from begin, end, and stride.
Output shape computation
before the op runs, ResizeOutputTensor derives each output dimension (dim_shape) and pushes it into output_shape_vector, which drives how large the output tensor buffer is allocated.
Single-precision `float` exactness
an IEEE-754 32-bit float can represent integers exactly only up to 2^24; larger magnitudes are rounded to the nearest representable value.

Root Cause Analysis

In tensorflow/lite/kernels/strided_slice.cc, the per-dimension length was computed as dim_shape = std::ceil((dim_shape) / static_cast<float>(stride)), casting the integer span to float before dividing and taking the ceiling. The invariant violated is that dim_shape must equal the exact integer count of elements the slice yields; once the span exceeds 2^24, the float cast rounds the value, so std::ceil can produce a dimension that does not match the true number of elements produced by the strided copy. A miscomputed dim_shape makes output_shape_vector — and therefore the allocated output tensor — the wrong size relative to the data the operator actually writes, enabling a heap buffer overflow (inferred as the write sink).

The fix removes the float path entirely and performs exact integer ceiling division: it zeroes dim_shape when dim_shape and stride have opposite signs, otherwise normalizes both to positive (TFLITE_CHECK_LT(dim_shape, 0) then negates) and computes (dim_shape + stride - 1) / stride. This keeps the dimension exact for all magnitudes, so the output buffer is sized correctly.

Key insight
The core mistake was routing an integer dimension calculation through a 32-bit float, which silently loses precision above 2^24 and yields a wrong output size; the fix replaces it with exact integer round-up division (with explicit sign handling).

Attack Path

  1. Build a WebML/WebNN graph Web content uses the WebML/WebNN API to construct a model containing a STRIDED_SLICE operator backed by TFLite.
  2. Supply large slice parameters The operator is given begin/end/stride such that a dimension’s span exceeds 2^24, so the pre-patch static_cast<float> division rounds and std::ceil yields an incorrect dim_shape.
  3. Output tensor mis-sized ResizeOutputTensor pushes the wrong dim_shape into output_shape_vector, allocating an output tensor that does not match the amount of data the strided copy produces.
  4. Heap overflow on execution (inferred) When the operator executes and writes the slice into the mis-sized buffer, it writes out of bounds; the exact sink/offset is not in the diff and is (inferred).

Impact Assessment

An attacker delivering a crafted WebML/WebNN model with oversized STRIDED_SLICE dimensions can cause a heap buffer overflow in the process hosting the WebML/TFLite backend. The vendor advisory rates this critical and describes it as a heap buffer overflow in WebML; the precondition is reaching the TFLite STRIDED_SLICE path with a slice span larger than 2^24 elements, and any escalation beyond the overflow (controlled write, code execution) is (inferred).

Changed Functions

FunctionChangeNotes
if
tensorflow/lite/kernels/strided_slice.cc
modified

Files Changed

  • tensorflow/lite/kernels/strided_slice.cc

Audit Directions

  • Integer-to-float in size math
    Grep TFLite and other numeric backends for static_cast<float>/std::ceil/std::floor used while computing tensor dimensions, lengths, or allocation sizes, where values can exceed 2^24.
  • Shape computation vs. actual element count
    Audit other TFLite operators’ Resize*/Prepare paths to confirm the computed output shape exactly matches the number of elements the kernel later writes.
  • Attacker-controlled op attributes
    Review WebML/WebNN-exposed operators where begin/end/stride/axis-style parameters are web-controllable, ensuring large or signed values cannot desynchronize allocation size from write count.
From b476481b77f6e939e813ac93df22a4a6e7a3dd57 Mon Sep 17 00:00:00 2001
From: Reilly Grant <[email protected]>
Date: Tue, 24 Mar 2026 11:42:42 -0700
Subject: [PATCH] [M147] Avoid casting dimensions to float in STRIDED_SLICE ResizeOutputTensor

Single-precision floats can only precisely represent values up to 2^24. If the size of the output slice were too large the previous logic would calculate the incorrect output dimension.

(Cherry-picked from commit 8cedce08e98d8f11e3ee5ced7a25fb830ccfb935.)

PiperOrigin-RevId: 885789312
Bug: 493310462
Change-Id: I7a3a1fadc152b1716e8e45e0cdf1dc125f7d82fd
---

diff --git a/tensorflow/lite/kernels/strided_slice.cc b/tensorflow/lite/kernels/strided_slice.cc
index 5450c48..8444fb1 100644
--- a/tensorflow/lite/kernels/strided_slice.cc
+++ b/tensorflow/lite/kernels/strided_slice.cc
@@ -222,8 +222,18 @@
       // This is valid for both positive and negative strides
       dim_shape = end - begin;
     }
-    dim_shape = std::ceil((dim_shape) / static_cast<float>(stride));
-    dim_shape = dim_shape < 0 ? 0 : dim_shape;
+    // Ensure we can do an integer division (rounding up) even when dealing with
+    // negative numbers.
+    if (dim_shape < 0 != stride < 0) {
+      dim_shape = 0;
+    } else {
+      if (stride < 0) {
+        TFLITE_CHECK_LT(dim_shape, 0);
+        dim_shape = -dim_shape;
+        stride = -stride;
+      }
+      dim_shape = (dim_shape + stride - 1) / stride;
+    }
     output_shape_vector.push_back(dim_shape);
   }
 
Loading diff…

Original Bug Report

reported by [email protected]

Heap buffer overflow in TFLite StridedSlice via WebNN slice() due to float32 precision loss in output shape computation

Heap buffer overflow in TFLite StridedSlice via WebNN slice() due to float32 precision loss in output shape computation

Summary

The TFLite STRIDED_SLICE kernel computes output tensor dimensions using float32 arithmetic, which silently loses precision for dimension values exceeding 2^24. The WebNN validation layer computes the same dimensions using exact integer arithmetic, so the graph passes validation while TFLite allocates a smaller output buffer than the data actually written during evaluation. A web page can exploit this discrepancy to trigger a heap buffer overflow in the GPU process through the WebNN slice() API. Affected platforms: Linux, Windows, macOS, ChromeOS (any platform where WebNN uses the TFLite backend).

Bisect

Introducing Commit: 6dfe00ca4114

  • Date: 2018-01-23
  • Author: A. Unique TensorFlower
  • Review: PiperOrigin-RevId: 183020501

This commit added the STRIDED_SLICE kernel to TFLite with the float32 division in ResizeOutputTensor. The bug became web-reachable when Chrome integrated WebNN with the TFLite backend and translated MLGraphBuilder.slice() into the TFLite STRIDED_SLICE op.

Root Cause

When TFLite prepares a STRIDED_SLICE operation, ResizeOutputTensor computes the output dimension along each axis by dividing the slice extent by the stride using float32:

// third_party/tflite/src/tensorflow/lite/kernels/strided_slice.cc
int32_t begin = ::tflite::strided_slice::StridedSliceStartForAxis(
    op_params, effective_input_shape, idx);
int32_t end = ::tflite::strided_slice::StridedSliceEndForAxis(
    op_params, effective_input_shape, idx, begin);

dim_shape = std::ceil((end - begin) / static_cast<float>(stride));

IEEE 754 single-precision floats have a 24-bit significand, so integers above 2^24 (16,777,216) cannot all be represented exactly. When end - begin equals 16,777,217 and stride equals 1, the cast static_cast<float>(stride) produces 1.0f, but the implicit promotion of dim_shape (16,777,217) to float rounds it down to 16,777,216.0f. The std::ceil call has no effect since the value is already integral after rounding. The output tensor is therefore allocated with 16,777,216 elements along that axis instead of 16,777,217.

The reference implementation of StridedSlice does not consult the output shape at all, as the code itself documents:

// third_party/tflite/src/tensorflow/lite/kernels/internal/reference/strided_slice.h
// Note that the output_shape is not used herein.

Instead, it recomputes begin and end from the original integer parameters and iterates over the true range. When the innermost stride is 1, it issues a single memcpy of stop - start elements per outer iteration through SequentialTensorWriter::WriteN:

// third_party/tflite/src/tensorflow/lite/kernels/internal/portable_tensor.h
void WriteN(int position, int len) {
    memcpy(output_ptr_, &input_data_[position], sizeof(T) * len);
    output_ptr_ += len;
}

This writes the correct number of elements into a buffer that is too small.

Chromium’s WebNN service translates MLGraphBuilder.slice() into TFLite’s STRIDED_SLICE. The WebNN validation layer in ValidateSliceAndInferOutput computes the output shape using exact integer ceiling division:

// services/webnn/public/cpp/graph_validation_utils.cc
uint32_t output_size = attributes.sizes[i] / attributes.strides[i] +
                       (attributes.sizes[i] % attributes.strides[i] != 0);

This produces the correct value of 16,777,217, so the operation passes validation. After TFLite builds the model, a post-build check in GraphImplTflite compares each output tensor’s byte count against the WebNN-expected size. However, this check only inspects the graph’s final output tensors, not intermediate tensors. By chaining two slice operations, where the first produces the oversized intermediate tensor and the second extracts a small sub-tensor as the graph output, the mismatch on the intermediate tensor goes undetected and the graph builds successfully. When the graph is dispatched, the first slice writes past the end of its allocated arena buffer, corrupting adjacent memory in the GPU process.

Reproduce

Tested at commit 7c89d33808e55 on Linux x86_64. No source modifications required.

Build configuration (out/asan-release/args.gn):

is_asan = true
is_debug = false
dcheck_always_on = false
target_cpu = "x64"
is_component_build = true

Build:

autoninja -C out/asan-release chrome

Serve the PoC and launch Chrome:

python3 -m http.server 8888 &
ASAN_OPTIONS=detect_odr_violation=0 out/asan-release/chrome \
  --no-sandbox \
  --enable-features=WebMachineLearningNeuralNetwork \
  --user-data-dir=/tmp/poc-$(date +%s) \
  http://localhost:8888/poc.html

The GPU process crashes within a few seconds of page load with a heap-buffer-overflow in __asan_memcpy, called from tflite::reference_ops::StridedSlice<unsigned char>.

=================================================================
==1307355==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b76f54a68c0 at pc 0x55d111bdcbae bp 0x7b7709aabcf0 sp 0x7b7709aab4b0
WRITE of size 65 at 0x7b76f54a68c0 thread T42 (ThreadPoolForeg)
    #0 0x55d111bdcbad in __asan_memcpy
    #1 0x7f775650a73e in void tflite::reference_ops::StridedSlice<unsigned char>(...) third_party/tflite/src/tensorflow/lite/kernels/internal/portable_tensor.h:130:5
    #2 0x7f7756504ed7 in tflite::ops::builtin::strided_slice::EvalImpl<...>(...) third_party/tflite/src/tensorflow/lite/kernels/internal/reference/strided_slice.h:140:3
    #3 0x7f77565056f9 in tflite::ops::builtin::strided_slice::Eval<...>(...) third_party/tflite/src/tensorflow/lite/kernels/strided_slice.cc:370:10
    #4 0x7f7755f62094 in tflite::Subgraph::InvokeImpl() third_party/tflite/src/tensorflow/lite/core/subgraph.cc
    #5 0x7f7755f6112e in tflite::Subgraph::Invoke() third_party/tflite/src/tensorflow/lite/core/subgraph.cc:1653:17
    #6 0x7f7755f3f6b4 in tflite::impl::Interpreter::Invoke() third_party/tflite/src/tensorflow/lite/core/interpreter.cc:247:48
    #7 0x7f77554ea7bd in webnn::tflite::GraphImplTflite::ComputeResources::DoDispatch(...) services/webnn/tflite/graph_impl_tflite.cc:328:41

0x7b76f54a68c0 is located 0 bytes after 2181038272-byte region [0x7b76734a6800,0x7b76f54a68c0)
allocated by thread T42 (ThreadPoolForeg) here:
    #0 0x55d111bdf772 in aligned_alloc
    #1 0x7f7755f9e814 in tflite::SimpleMemoryArena::Commit(bool*) third_party/tflite/src/tensorflow/lite/simple_memory_arena.cc:111:31
    #2 0x7f7755f14ec8 in tflite::ArenaPlanner::ExecuteAllocations(int, int) third_party/tflite/src/tensorflow/lite/arena_planner.cc:433:32
    #3 0x7f7755f5d87c in tflite::Subgraph::PrepareOpsAndTensors() third_party/tflite/src/tensorflow/lite/core/subgraph.cc:1604:42
    #4 0x7f7755f5beba in tflite::Subgraph::AllocateTensors(...) third_party/tflite/src/tensorflow/lite/core/subgraph.cc:1035:25
    #5 0x7f77554d8722 in webnn::tflite::GraphImplTflite::ComputeResources::Create(...) services/webnn/tflite/graph_impl_tflite.cc:221:34

SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy

Full ASAN log is attached as asan.log.

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker