Critical chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WebML
DescriptionInteger overflow in WebML
ComponentWebML
Bug ClassInteger Overflow
Tracker494158331
Fix commit41c622eea273 (chromium/src) +62/-7
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-04-07

Changed Functions

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

Files Changed

  • services/webnn/tflite/graph_builder_tflite.cc
From 41c622eea2736d8702b556f1e0ab8fa8a6bf4662 Mon Sep 17 00:00:00 2001
From: Wei Wang <[email protected]>
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 <[email protected]>
Reviewed-by: Reilly Grant <[email protected]>
Commit-Queue: Wang, Wei4 <[email protected]>
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 [email protected]

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