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

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WebML
DescriptionInteger overflow in WebML
ComponentWebML
Sub-componentWebNN
Bug ClassInteger Overflow
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.
Tracker494158331
Fix commit41c622eea273 (chromium/src) +62/-7
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedAnonymous
Reported2026-03-19 · bounty $43000 (per vendor advisory)
Fix landed2026-03-21 (public git)
Disclosed2026-04-07
Patch gap17 days — fix public in source before the advisory

Background

WebNN `Pool2d`
the pooling operator (max/average pool) in the WebNN graph, lowered to a TFLite/XNNPACK pooling kernel by graph_builder_tflite.cc.
Indirection buffer
an internal buffer XNNPACK’s pooling kernels allocate holding one pointer per pooling position; its byte size is roughly (pooling_size - 1 + output_height * step_height) * sizeof(void*).
`base::CheckedNumeric<int32_t>`
an overflow-checked integer wrapper whose .IsValid() reports whether any arithmetic in the chain overflowed the underlying type.
`Size2d`
a {height, width} pair; output_size2d, filter_size2d, and input_size2d describe the pooling output, window, and input dimensions.

Root Cause Analysis

The Pool2d builder computed the configuration for XNNPACK’s pooling kernels from web-controlled output dimensions, window size, strides, and padding without bounding the resulting internal indirection-buffer size. With large attacker-chosen dimensions, the product used to size that buffer (pooling_size, step_height, output_height, times sizeof(void*)) can exceed the range of the integer type used, so the computed size wraps and the kernel allocates an undersized indirection buffer that is later indexed per pooling position — a heap buffer overflow (inferred).

The fix reconstructs the same size computation ahead of time using base::CheckedNumeric<int32_t> for every multiply/add (checked_pooling_size, checked_step_height, checked_output_height, and checked_indirection_buffer_size *= sizeof(void*)) and, if !checked_indirection_buffer_size.IsValid(), returns base::unexpected(...) to reject the operation before any kernel allocation. For max-pool it uses output_size2d.height directly, and for average-pool it derives the height from padding and stride exactly as XNNPACK’s average-pooling-nhwc.c does, so the overflow check mirrors the real allocation formula.

The patch also hoists the output_operand/output_size2d/filter_size2d declarations earlier so the new check can use them (the later duplicate declarations are removed).

Key insight
The indirection-buffer size was computed in unchecked integer arithmetic over web-controlled dimensions and could overflow, under-sizing the allocation; the fix recomputes it with base::CheckedNumeric and rejects the op when the size is not representable.

Attack Path

  1. Construct an oversized Pool2d Web content builds a WebNN graph with a maxPool2d/averagePool2d whose output dimensions, window size, and/or strides are large enough that the indirection-buffer byte count exceeds int32_t/size_t range.
  2. Compile the graph The TFLite backend lowers the op; pre-patch, no bound is applied to the computed indirection-buffer size.
  3. Integer overflow The size arithmetic wraps, yielding a small value, and the XNNPACK pooling kernel allocates an undersized indirection buffer.
  4. Heap buffer overflow The kernel writes/reads one pointer per pooling position into the undersized buffer, overflowing the heap allocation (inferred downstream of the diff).

Impact Assessment

An attacker running a WebNN graph can cause an integer overflow in the Pool2d indirection-buffer sizing, leading to an undersized allocation and a heap buffer overflow in the WebNN/TFLite backend process. The vendor advisory rates this critical (“Integer overflow in WebML”) and does not report in-the-wild exploitation; the diff shows only the overflow computation and rejecting guard, so the resulting allocation under-size and overflow, and any write primitive or code execution, are (inferred).

Changed Functions

FunctionChangeNotes
if
services/webnn/tflite/graph_builder_tflite.cc
modified

Files Changed

  • services/webnn/tflite/graph_builder_tflite.cc

Audit Directions

  • Unchecked size arithmetic from graph dimensions
    Audit every WebNN op builder that computes buffer or allocation sizes from web-controlled shapes/strides/padding and ensure the arithmetic uses base::CheckedNumeric with an .IsValid() gate before allocation.
  • Mirror the backend's real formula
    When bounding an internal buffer, confirm the check reproduces the exact kernel allocation formula (here XNNPACK max- vs average-pooling), since an approximate check can still under/over-estimate.
  • Other XNNPACK indirection-buffer ops
    Review convolution, depthwise, and other ops that build XNNPACK indirection buffers for the same missing size bound.
From 41c622eea2736d8702b556f1e0ab8fa8a6bf4662 Mon Sep 17 00:00:00 2001
From: Wei Wang <wei4.wang@intel.com>
Date: Fri, 20 Mar 2026 19:57:55 -0700
Subject: [PATCH] [WebNN] Prevent Pool2d indirection buffer overflow in TFLite

Add a check to ensure the size of the internal indirection buffer used
by TFLite's Pool2d implementation does not exceed the maximum value
of a size_t integer.

Bug: 494158331
Change-Id: I984556f0f608badf8f73fcbb096da5f41170a958
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7687618
Reviewed-by: Hu, Ningxin <ningxin.hu@intel.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Wang, Wei4 <wei4.wang@intel.com>
Cr-Commit-Position: refs/heads/main@{#1602966}
---

diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index 68e4af4..18f18c0b 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -6884,6 +6884,68 @@
     return base::unexpected("Pool2d in tflite doesn't support dilations.");
   }
 
+  // Check the indirection buffer size to ensure it does not exceed the maximum
+  // value of a size_t integer.
+  const mojom::Operand& output_operand = GetOperand(pool2d.output_operand_id);
+  const auto& output_shape = output_operand.descriptor.shape();
+  const webnn::Size2d<uint32_t> output_size2d = {.height = output_shape[1],
+                                                 .width = output_shape[2]};
+  const webnn::Size2d<uint32_t> filter_size2d = {
+      .height = pool2d.window_dimensions->height,
+      .width = pool2d.window_dimensions->width};
+  base::CheckedNumeric<int32_t> checked_output_height = 0;
+
+  if (pool2d.kind == mojom::Pool2d::Kind::kMaxPool2d) {
+    // https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;l=488;drc=b269899e63e0110d1ccf964a741be2833a9ecd9b
+    checked_output_height = output_size2d.height;
+  } else {
+    // https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/average-pooling-nhwc.c;l=442;drc=b269899e63e0110d1ccf964a741be2833a9ecd9b
+    auto checked_top_height =
+        base::CheckedNumeric<int32_t>(pool2d.padding->beginning->height);
+    auto checked_stride_height =
+        base::CheckedNumeric<int32_t>(pool2d.strides->height);
+    checked_top_height += checked_stride_height;
+    checked_top_height -= 1;
+    checked_top_height /= checked_stride_height;
+
+    auto checked_bottom_height =
+        base::CheckedNumeric<int32_t>(pool2d.padding->ending->height);
+    checked_bottom_height += checked_stride_height;
+    checked_bottom_height -= 1;
+    checked_bottom_height /= checked_stride_height;
+
+    checked_output_height = checked_top_height;
+    checked_output_height += checked_bottom_height;
+    checked_output_height += 1;
+  }
+
+  auto checked_filter_height =
+      base::CheckedNumeric<int32_t>(filter_size2d.height);
+  auto checked_filter_width =
+      base::CheckedNumeric<int32_t>(filter_size2d.width);
+  auto checked_pooling_size = checked_filter_height;
+  checked_pooling_size *= checked_filter_width;
+
+  auto checked_output_width =
+      base::CheckedNumeric<int32_t>(output_size2d.width);
+  checked_output_width -= 1;
+  checked_output_width *= base::CheckedNumeric<int32_t>(
+      std::min(pool2d.strides->width, filter_size2d.width));
+  checked_output_width *= checked_filter_height;
+  auto checked_step_height = checked_pooling_size + checked_output_width;
+
+  auto checked_indirection_buffer_size = checked_pooling_size;
+  checked_indirection_buffer_size -= 1;
+  checked_output_height *= checked_step_height;
+  checked_indirection_buffer_size += checked_output_height;
+  checked_indirection_buffer_size *=
+      base::CheckedNumeric<int32_t>(sizeof(void*));
+  if (!checked_indirection_buffer_size.IsValid()) {
+    return base::unexpected(
+        "Pool2d doesn't support configurations requiring an internal "
+        "computation buffer that exceeds the maximum size.");
+  }
+
   ::tflite::BuiltinOperator operator_code;
   std::optional<TensorInfo> quantized_output;
   const mojom::Operand& input_operand = GetOperand(pool2d.input_operand_id);
@@ -6911,15 +6973,8 @@
 
   const auto& input_shape = input_operand.descriptor.shape();
   CHECK_EQ(input_shape.size(), 4u);
-  const mojom::Operand& output_operand = GetOperand(pool2d.output_operand_id);
-  const auto& output_shape = output_operand.descriptor.shape();
   const webnn::Size2d<uint32_t> input_size2d = {.height = input_shape[1],
                                                 .width = input_shape[2]};
-  const webnn::Size2d<uint32_t> output_size2d = {.height = output_shape[1],
-                                                 .width = output_shape[2]};
-  webnn::Size2d<uint32_t> filter_size2d = {
-      .height = pool2d.window_dimensions->height,
-      .width = pool2d.window_dimensions->width};
 
   // TODO(crbug.com/493988762): Explicitly restrict to int32_t in the WebNN spec
   // or opSupportLimits for synchronous frontend validation.
Loading diff…

Original Bug Report

reported by ki...@gmail.com

Heap OOB write in XNNPACK Pool2D via integer overflow

VULNERABILITY DETAILS

An integer overflow vulnerability exists in the WebNN TFLite XNNPACK backend when processing Pool2d operations, leading to heap out-of-bounds write.

The root cause resides in the WebNN frontend graph validation ValidatePool2dAndInferOutput. It reads windowDimensions into uint32_t window_height and window_width without restricting their maximum product [0].

  uint32_t window_height = input_height;
  uint32_t window_width = input_width;
  if (attributes.window_dimensions) {
    if (attributes.window_dimensions->height == 0 ||
        attributes.window_dimensions->width == 0) {
      return base::unexpected(ErrorWithLabel(
          label, "All window dimensions should be greater than 0."));
    }
    window_height = attributes.window_dimensions->height;
    window_width = attributes.window_dimensions->width; // [0]

These unchecked values flow down to the XNNPACK engine where they are used to compute pooling_size [1]. By supplying large windowDimensions (e.g. 1000092567 and 1152814792), the 64-bit unsigned product pooling_size can overflow the 2^60 boundary.

Later, XNNPACK calculates step_height [2] and subsequently indirection_buffer_size [3]. Crucially, indirection_buffer_size multiplies (pooling_size - 1) + ... by sizeof(void*) (8 bytes). This multiplication by 8 overflows the 64-bit size_t variable, causing the resulting allocation size to wrap around to a small number.

As a result, xnn_reallocate_memory allocates a smaller buffer than required [4].

  const size_t pooling_height = max_pooling_op->convolution_op->kernel_height;
  const size_t pooling_width = max_pooling_op->convolution_op->kernel_width;
  const size_t pooling_size = pooling_height * pooling_width; // [1]
  const size_t output_height = max_pooling_op->convolution_op->output_height;
  const size_t output_width = max_pooling_op->convolution_op->output_width;

  const size_t step_width =
    max_pooling_op->convolution_op->dilation_width > 1 ? pooling_width : min(max_pooling_op->convolution_op->stride_width, pooling_width);
  const size_t step_height = pooling_size + (output_width - 1) * step_width * pooling_height; // [2]

  if (input_height != max_pooling_op->convolution_op->last_input_height ||
      input_width != max_pooling_op->convolution_op->last_input_width ||
      channels != max_pooling_op->convolution_op->last_input_channels)
  {
    const size_t indirection_buffer_size = sizeof(void*) * ((pooling_size - 1) + output_height * step_height); // [3]
    const void** indirection_buffer =
      (const void**) xnn_reallocate_memory(max_pooling_op->convolution_op->indirection_buffer, indirection_buffer_size); // [4]

However, the memory initialization loop logic relies on the original kernel_height and kernel_width [5], causing the loop to iterate and write out-of-bounds pointers into the indirection_buffer [6].

        for (size_t output_x = 0; output_x < output_width; output_x++) {
          for (size_t pooling_x = 0; pooling_x < kernel_width; pooling_x++) { // [5]
            const size_t input_x = min(doz(output_x * stride_width + pooling_x * dilation_width, input_padding_left), input_x_max);
            const size_t index = output_y * step_height + output_x * step_width * kernel_height + pooling_x * kernel_height + pooling_y;
            indirection_buffer[index] = (const void*) ((uintptr_t) input + (input_y * input_width + input_x) * input_pixel_stride); // [6]
          }
        }

[0] https://source.chromium.org/chromium/chromium/src/+/main:services/webnn/public/cpp/graph_validation_utils.cc;drc=2d6b112c7b9888854636d22cee0aa4b9990c4425;l=2241

[1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=476

[2] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=482

[3] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=488

[4] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=490

[5] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/indirection.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=419

[6] https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/indirection.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=422

BISECTION

Introduced by XNNPACK upstream commit https://github.com/google/XNNPACK/commit/fae4eb257a2cf76c243d7a0cd123303f0e27ad2c (Rewrite maxpool kernels) which replaced the bounded tile constant with the attacker-controlled pooling size when calculating the indirection buffer size.

This regression was rolled into Chromium in commit https://chromium.googlesource.com/chromium/src/+/85fc641cf32e5687c9384f35053e1d3d3f4eea5a (Roll TFLite to Next Green Version).

VERSION

Chrome Version: HEAD

Operating System: Linux

REPRODUCTION CASE

  1. Build Chromium with Asan.
  2. Host the poc.html on an HTTP server.
  3. Run Chrome against the PoC.
$ python3 -m http.server
$ ./out/asan/chrome --enable-features=WebMachineLearningNeuralNetwork "http://localhost:8000/poc.html"

CRASH INFORMATION

Type of crash: GPU process

Crash log: see the attached asan.txt ASan trace.

SUGGESTED FIX

Add a base::CheckedNumeric<uint32_t> multiplication check to ValidatePool2dAndInferOutput.

See fix.patch for details.

CREDIT INFORMATION

Reporter credit: Anonymous

View on issue tracker