CVE-2026-3915
Overview
Background
- WebNN (Web Neural Network API)
- A browser API, implemented in the
services/webnnprocess, 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.ccthat translates WebNN operators into TFLite operators for inference. - `roundingType`
- A WebNN pooling option selecting
floororceilrounding 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.
PAD operator is emitted whenever ceil rounding demands it.Attack Path
- Reach WebNN
A malicious page uses the WebNN API (
navigator.ml) to build a graph, targeting the TFLite backend in the WebNN service process. - Craft a ceil pooling op
The page adds an
averagePool2d(or similar) operator withoptions.roundingType = "ceil"and no explicit padding, soexplicit_paddingequals{0, 0, 0, 0}. - Trigger the early return
During graph building,
GetTfLitePaddingModereturnsPadding_VALIDwith no paddings, skipping the ceil-rounding correction and leaving the TFLite output shape smaller than WebNN’s allocated shape. - Execute the graph
The page dispatches the graph; TFLite runs the
VALIDpooling op and writes results based on the mismatched geometry. - 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
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
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.ccthird_party/blink/web_tests/platform/linux/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txtthird_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_gpu-expected.txtthird_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/averagePool2d.https.any_cpu-expected.txtthird_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" inputsFlag 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 agreementAudit every place where a WebNN-computed tensor shape is paired with a backend operator, confirming both sides compute the same dimensions under both
floorandceilroundingType. - `ceil`/`floor` rounding mismatchesReview all pooling and convolution lowering paths that call
CalculatePaddingEndForCeilRoundingTypeor emitPadding_VALIDto ensure aPADoperator is inserted whenever rounding modes could differ.
Patch
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
Regression Test / PoC
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
Original Bug Report
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?
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
Is the security issue related to a crash?
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
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://bughunters.google.com/about/rules/chrome-friends/chrome-vulnerability-reward-program-rules#chrome-fuzzer-program:~:text=Bugs in unlaunched,message at runtime
- https://bughunters.google.com/about/rules/chrome-friends/chrome-vulnerability-reward-program-rules#chrome-fuzzer-program:~:text=Bugs%20in%20unlaunched,message%20at%20runtime
- https://chromium-review.googlesource.com/c/chromium/src/+/5359192
- https://chromium-review.googlesource.com/c/chromium/src/+/7518276
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/xnnpack/src/src/xx-copy/xx-copy-scalar-memcpy.c;l=18;drc=1bbe3261c7cd6ca9d140b5cea7aed9df2982418a