CVE-2026-5859
Overview
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, andinput_size2ddescribe 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).
base::CheckedNumeric and rejects the op when the size is not representable.Attack Path
- Construct an oversized Pool2d
Web content builds a WebNN graph with a
maxPool2d/averagePool2dwhose output dimensions, window size, and/or strides are large enough that the indirection-buffer byte count exceedsint32_t/size_trange. - Compile the graph The TFLite backend lowers the op; pre-patch, no bound is applied to the computed indirection-buffer size.
- Integer overflow The size arithmetic wraps, yielding a small value, and the XNNPACK pooling kernel allocates an undersized indirection buffer.
- 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
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
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.cc
Audit Directions
- Unchecked size arithmetic from graph dimensionsAudit every WebNN op builder that computes buffer or allocation sizes from web-controlled shapes/strides/padding and ensure the arithmetic uses
base::CheckedNumericwith an.IsValid()gate before allocation. - Mirror the backend's real formulaWhen 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 opsReview convolution, depthwise, and other ops that build XNNPACK indirection buffers for the same missing size bound.
Patch
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.
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