CVE-2026-5859
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.cc
Patch
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.
Original Bug Report
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]
}
}
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
- Build Chromium with Asan.
- Host the
poc.htmlon an HTTP server. - 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
- http://localhost:8000/poc.html
- https://chromium.googlesource.com/chromium/src/+/85fc641cf32e5687c9384f35053e1d3d3f4eea5a
- https://github.com/google/XNNPACK/commit/fae4eb257a2cf76c243d7a0cd123303f0e27ad2c
- https://source.chromium.org/chromium/chromium/src/+/main:services/webnn/public/cpp/graph_validation_utils.cc;drc=2d6b112c7b9888854636d22cee0aa4b9990c4425;l=2241
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/indirection.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=419
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/indirection.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=422
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=476
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=482
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=488
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/operators/max-pooling-nhwc.c;drc=e2430de679d9d16bbff0b3c6f923cc84b6fe3042;l=490