Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebML
DescriptionHeap buffer overflow in WebML
ComponentWebML
Bug ClassOOB
Tracker483971526
Fix commitce45888b8732 (chromium/src) +12/-21
CISA KEVNot listed
CreditedTobias Wienand
Disclosed2026-03-10

Background

WebNN (Web Neural Network API)
A browser API, implemented in the services/webnn process, that lets web pages describe and execute neural-network graphs against a native ML backend.
TFLite backend
WebNN’s LiteRT/TensorFlow-Lite execution path in graph_builder_tflite.cc that translates WebNN operators into TFLite operators for inference.
`roundingType`
A WebNN pooling option selecting floor or ceil rounding when computing an operator’s output spatial dimensions, which can differ from TFLite’s rounding.
VALID padding
A TFLite padding mode that applies no implicit padding and always floors the output size, in contrast to WebNN which may ceil.

Root Cause Analysis

The vulnerable code path is GetTfLitePaddingMode in graph_builder_tflite.cc, which converts WebNN pooling/convolution padding into a TFLite padding descriptor. The old code contained an early return that fired whenever the WebNN explicit_padding array {beginning->height, ending->height, beginning->width, ending->width} equalled no_padding ({0, 0, 0, 0}), immediately returning TfLitePadding{.mode = ::tflite::Padding_VALID} with no trailing paddings. That shortcut skipped the subsequent ceil-rounding reconciliation logic, so when the caller requested roundingType=ceil with zero explicit padding, WebNN’s expected output tensor was allocated at the larger ceil-based size while TFLite’s Padding_VALID op computed a smaller floor-based output (or vice versa), violating the invariant that the WebNN-computed output shape and the TFLite operator’s actual output shape must agree. The mismatch let TFLite write into a heap buffer sized against the wrong dimensions, producing the out-of-bounds write.

The fix removes the all-zeros early return entirely so the ceil/floor checks and the CalculatePaddingEndForCeilRoundingType logic always run, causing a compensating PAD operator (with correct explicit_padding) to be inserted even when the user supplied no padding.

Key insight
The single mistake was treating “user supplied no explicit padding” as equivalent to “no padding computation is needed,” which bypassed the reconciliation that accounts for TFLite and WebNN having different default rounding modes; the fix deletes that early return so padding is always recomputed and a PAD operator is emitted whenever ceil rounding demands it.

Attack Path

  1. Reach WebNN A malicious page uses the WebNN API (navigator.ml) to build a graph, targeting the TFLite backend in the WebNN service process.
  2. Craft a ceil pooling op The page adds an averagePool2d (or similar) operator with options.roundingType = "ceil" and no explicit padding, so explicit_padding equals {0, 0, 0, 0}.
  3. Trigger the early return During graph building, GetTfLitePaddingMode returns Padding_VALID with no paddings, skipping the ceil-rounding correction and leaving the TFLite output shape smaller than WebNN’s allocated shape.
  4. Execute the graph The page dispatches the graph; TFLite runs the VALID pooling op and writes results based on the mismatched geometry.
  5. Overflow the heap buffer The size disagreement between the expected and actual output tensor causes an out-of-bounds heap write in the WebNN service process.

Impact Assessment

An attacker who can run script in a renderer gains a heap buffer overflow (out-of-bounds write) in the WebNN service process, whose degree of control depends on the tensor-shape discrepancy produced by the rounding mismatch. The only preconditions are that WebNN with the TFLite backend is available/enabled and reachable, and that the attacker can submit a pooling or convolution operator with roundingType=ceil and zero explicit padding. This corrupts memory outside the renderer sandbox in a privileged GPU/utility-style service, making it a strong primitive for sandbox escape or further compromise.

Changed Functions

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

Files Changed

  • services/webnn/tflite/graph_builder_tflite.cc
  • third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
  • third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
  • third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
  • third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt

Audit Directions

  • Early-return shortcuts on "default" inputs
    Flag conversions that skip normalization or size-reconciliation logic when an input looks like a no-op default (all-zero padding, unit stride, identity), since backends may still diverge on rounding or defaults.
  • Cross-framework output-shape agreement
    Audit every place where a WebNN-computed tensor shape is paired with a backend operator, confirming both sides compute the same dimensions under both floor and ceil roundingType.
  • `ceil`/`floor` rounding mismatches
    Review all pooling and convolution lowering paths that call CalculatePaddingEndForCeilRoundingType or emit Padding_VALID to ensure a PAD operator is inserted whenever rounding modes could differ.
From ce45888b8732e18758b0ee299bc307c2be7e062e Mon Sep 17 00:00:00 2001
From: Reilly Grant <reillyg@chromium.org>
Date: Thu, 19 Feb 2026 18:53:10 -0800
Subject: [PATCH] webnn: Compute TFLite padding even without explicit padding

TFLite and WebNN differ on their default rounding modes and so we always
need to do the checks below to figure out if padding is needed.

The test changes update an existing failing test which now produces a
different value.

Fixed: 483971526
Change-Id: I15bb79370e39b05f1d0bc49ac75c56127be5b5bc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7591423
Reviewed-by: Jiewei Qian <qjw@chromium.org>
Auto-Submit: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Jiewei Qian <qjw@chromium.org>
Commit-Queue: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1587587}
---

diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index 46fa3476..32cec66 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -345,16 +345,6 @@
     const mojom::Size2d& stride,
     const mojom::Size2d& dilation,
     const webnn::Size2d<uint32_t>& output) {
-  // WebNN explicit padding is in [beginning_height, ending_height,
-  // beginning_width, ending_width] sequence.
-  std::array<uint32_t, 4> explicit_padding = {
-      padding2d.beginning->height, padding2d.ending->height,
-      padding2d.beginning->width, padding2d.ending->width};
-  std::array<uint32_t, 4> no_padding = {0, 0, 0, 0};
-  if (explicit_padding == no_padding) {
-    return TfLitePadding{.mode = ::tflite::Padding_VALID};
-  }
-
   // TFLite always performs a floor operation in VALID mode. If WebNN's
   // RoundingType is ceil, the `actual_output_height` might be 1 greater than
   // what TFLite's VALID padding formula (floor based) would produce. In this
@@ -390,7 +380,7 @@
     // Otherwise, a TFLite PAD operator will be inserted later using VALID
     // padding.
     return GetTfLitePaddingMode(padding2d, input, filter, stride, dilation,
-                                /*is_transposed_conv2d*/ false);
+                                /*is_transposed_conv2d=*/false);
   } else if (actual_output_height ==
                  base::ClampCeil<uint32_t>(calculated_output_sizes.height) &&
              actual_output_width ==
@@ -405,8 +395,9 @@
         CalculatePaddingEndForCeilRoundingType(
             input.width, filter.width, stride.width, dilation.width,
             output.width, padding2d.beginning->width));
-    explicit_padding = {padding2d.beginning->height, padding_height_end,
-                        padding2d.beginning->width, padding_width_end};
+    std::array<uint32_t, 4> explicit_padding = {
+        padding2d.beginning->height, padding_height_end,
+        padding2d.beginning->width, padding_width_end};
     // The explicit padding are used to insert a TfLite PAD operator.
     return TfLitePadding{.mode = ::tflite::Padding_VALID,
                          .paddings = explicit_padding};
diff --git a/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
index 045daca9..ba89d4d7 100644
--- a/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
@@ -4,7 +4,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.03894805908203 should be close enough to expected 21.206613540649414 by ULP distance: expected a number less than or equal to 11n but got 10387645n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130187988281 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710541n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.03894805908203 should be close enough to expected 21.206613540649414 by ULP distance: expected a number less than or equal to 11n but got 10387645n
 [FAIL] [required] averagePool2d float32 4D tensor options.outputSizes ignores options.roundingType=floor
@@ -14,7 +14,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.03125 should be close enough to expected 21.203125 by ULP distance: expected a number less than or equal to 11 but got 1268
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.03125 should be close enough to expected 21.203125 by ULP distance: expected a number less than or equal to 11 but got 1268
 [FAIL] [required] averagePool2d float16 4D tensor options.outputSizes ignores options.roundingType=floor
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
index 045daca9..ba89d4d7 100644
--- a/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
@@ -4,7 +4,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.03894805908203 should be close enough to expected 21.206613540649414 by ULP distance: expected a number less than or equal to 11n but got 10387645n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130187988281 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710541n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.03894805908203 should be close enough to expected 21.206613540649414 by ULP distance: expected a number less than or equal to 11n but got 10387645n
 [FAIL] [required] averagePool2d float32 4D tensor options.outputSizes ignores options.roundingType=floor
@@ -14,7 +14,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.03125 should be close enough to expected 21.203125 by ULP distance: expected a number less than or equal to 11 but got 1268
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.03125 should be close enough to expected 21.203125 by ULP distance: expected a number less than or equal to 11 but got 1268
 [FAIL] [required] averagePool2d float16 4D tensor options.outputSizes ignores options.roundingType=floor
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
index f849a8a..e6076a49 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_npu-expected.txt
@@ -8,7 +8,7 @@
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 50.63130569458008 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 2710542n
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 26.860942840576172 should be close enough to expected 40.29140853881836 by ULP distance: expected a number less than or equal to 11n but got 4867889n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float32 actual 36.13502502441406 should be close enough to expected 54.20252990722656 by ULP distance: expected a number less than or equal to 11n but got 4736288n
 [FAIL] [required] averagePool2d float32 4D tensor options.layout=nhwc and options.roundingType=ceil
@@ -28,7 +28,7 @@
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.roundingType=ceil and no padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 50.625 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 331
+  assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 26.859375 should be close enough to expected 40.28125 by ULP distance: expected a number less than or equal to 11 but got 594
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=floor
   assert_less_than_equal: assert_array_approx_equals_ulp: test averagePool2d float16 actual 36.125 should be close enough to expected 54.1875 by ULP distance: expected a number less than or equal to 11 but got 578
 [FAIL] [required] averagePool2d float16 4D tensor options.layout=nhwc and options.roundingType=ceil
Loading diff…

Original Bug Report

reported by to...@gmail.com

Heap Buffer Overflow (READ) in TFLite + XNNPack via WebNN


Report description

Heap Buffer Overflow (READ) in TFLite + XNNPack via WebNN


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/xx-copy/xx-copy-scalar-memcpy.c;l=18;drc=1bbe3261c7cd6ca9d140b5cea7aed9df2982418a


The problem

Please describe the technical details of the vulnerability

XNNPACK heap-buffer-overflow (OOB Read) via WebNN concat + avgPool2d ceil rounding

Environment

Ubuntu + AMD processor (for completeness, shouldn’t matter).

Base Chromium revision

a0062e558d37e03d9129522e5a3c6c29946d8195 (2026-02-10)

GN args

is_asan = true
is_debug = false
symbol_level = 1

Build

cd chromium/src
gn gen out/asan_shell
autoninja -C out/asan_shell content_shell

Running the PoC

WebNN is behind a feature flag. The GPU process is a separate process, so its ASAN errors don’t appear on the main process stderr. Setting ASAN_OPTIONS with log_path captures per-process ASAN output to disk.

ASAN_OPTIONS="log_path=/tmp/asan_log:detect_leaks=0" ./out/asan_shell/content_shell --enable-features=WebMachineLearningNeuralNetwork file:///path/to/poc.html

The GPU process crashes during graph dispatch (inference). ASAN output is at /tmp/asan_log.<gpu_pid>. The poc.html file is included in this report.

ASAN output

==3511981==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7cf3f8f74708 at pc 0x60c72c4ed02b bp 0x7be37d4f98a0 sp 0x7be37d4f9060
READ of size 12 at 0x7cf3f8f74708 thread T71 (ThreadPoolForeg)
    #0 __asan_memcpy
    #1 xnn_xx_copy_ukernel__scalar_memcpy  third_party/xnnpack/src/src/xx-copy/xx-copy-scalar-memcpy.c:18:3
    #2 xnn_compute_univector_strided       third_party/xnnpack/src/src/operator-run.c:1733:5
    #3 xnn_run_operator_with_index         third_party/xnnpack/src/src/operator-run.c:2482:9
    #4 xnn_invoke_runtime                  third_party/xnnpack/src/src/runtime.c:1207:38
    #5 SubgraphInvoke                      third_party/tflite/src/tensorflow/lite/delegates/xnnpack/xnnpack_delegate.cc:1421:25
    #6 tflite::Subgraph::Invoke()          third_party/tflite/src/tensorflow/lite/core/subgraph.cc:1653:17
    #7 DoDispatch                          services/webnn/tflite/graph_impl_tflite.cc:306:41

0x7cf3f8f74708 is located 0 bytes after 200-byte region [0x7cf3f8f74640,0x7cf3f8f74708)
allocated by thread T71 here:
    #0 operator new(unsigned long)
    #1 SubgraphInit  third_party/tflite/src/tensorflow/lite/delegates/xnnpack/xnnpack_delegate.cc:1230:12

SUMMARY: AddressSanitizer: heap-buffer-overflow third_party/xnnpack/src/src/xx-copy/xx-copy-scalar-memcpy.c:18:3

Root cause

The bug is in GetPool2dTfLitePaddingMode() in graph_builder_tflite.cc (line 354):

base::expected<TfLitePadding, std::string> GetPool2dTfLitePaddingMode(
    const mojom::Padding2d& padding2d, ..., const webnn::Size2d<uint32_t>& output) {
  std::array<uint32_t, 4> explicit_padding = { ... };
  std::array<uint32_t, 4> no_padding = {0, 0, 0, 0};
  if (explicit_padding == no_padding) {
    return TfLitePadding{.mode = ::tflite::Padding_VALID};  // BUG
  }
  // ... ceil rounding handling code (lines 394-412) never reached ...
}

When explicit padding is [0,0,0,0], the function immediately returns Padding_VALID without checking whether the output dimensions require ceil rounding. This bypasses the ceil rounding handling code at lines 394-412, which would add extra ending padding to make TFLite’s floor-based formula produce the correct output size.

The consequence is a shape mismatch between WebNN’s validated graph and XNNPACK’s internal computation: WebNN computes pool output shapes using ceil rounding, but XNNPACK recomputes them using floor division. The concat operator then reads past the smaller actual allocation.

Removing the early return and rebuilding content_shell eliminates the crash.

Bisection

Introducing commit: 6c9e4fea79849343c09bae0214cd76904f43be22 (2024-03-13) Title: “webnn: Support Pool2d in in //services/webnn/tflite” CL: https://chromium-review.googlesource.com/c/chromium/src/+/5359192

The parent commit returned "pool2d is not implemented" for pool2d. This commit added SerializePool2d(), which reused GetTfLitePaddingMode() (written for Conv2d) without ceil rounding awareness. The Blink IDL already had roundingType: 'ceil' at this point, so the bug was immediately reachable from JavaScript.

Verified by simulating both states on the current tree: disabling pool2d serialization produces no crash; re-enabling it reproduces the heap-buffer-overflow.

Impact analysis

Affected platforms

Tested on Linux (x86_64) with the TFLite+XNNPACK backend. The bug is in graph_builder_tflite.cc which is used on all platforms with the TFLite backend: Linux, Android (ARM64, x86/x64), and ChromeOS.

Impact

  • Attack vector: Any website, no user interaction required beyond navigation. WebNN is behind a feature flag (WebMachineLearningNeuralNetwork), which is not yet enabled by default, but eligible for VRP according to the rules and it is also in origin trial since Jan 31: https://chromium-review.googlesource.com/c/chromium/src/+/7518276.
  • Process: GPU process. On Android, this is unsandboxed.
  • Primitive: OOB heap read.
  • Consequence: Heap leak in a process shared between different origins. Maybe useful if heap leak can propagate back to JS

The cause

What version of Chrome have you found the security issue in?

147.0.7681.0 dev

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a non-sandboxed process)

How would you like to be publicly acknowledged for your report?

Tobias Wienand

View on issue tracker