High watch CVSS 4.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebML
DescriptionHeap buffer overflow in WebML
ComponentWebML
Sub-componentWebNN
Bug ClassOOB
Action tierwatch — corruption primitive but gated (flag) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker492668885
Fix commitd141d62357df (chromium/src) +81/-57
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedSyn4pse
Reported2026-03-14 (per vendor advisory)
Fix landed2026-03-19 (public git)
Disclosed2026-04-07
Patch gap19 days — fix public in source before the advisory

Background

WebNN / `convTranspose2d`
the Web Neural Network API’s transposed-convolution operator, which a page builds through MLGraphBuilder and which the service lowers onto a backend such as TFLite.
`Padding_SAME` vs `Padding_VALID`
TFLite padding modes; SAME lets the kernel auto-compute padding from sizes, while VALID requires an explicit PAD operator to be inserted by the builder.
TFLite `TransposeConv` SAME rule
TFLite computes a transpose conv’s SAME padding by feeding the output size into the ordinary forward-convolution padding formula, not the input size.
`output padding`
WebNN convTranspose2d allows a non-zero output-padding adjustment that TFLite’s SAME mode does not model.

Root Cause Analysis

CalculateExplicitPaddingForSamePaddingMode had a dedicated is_transposed_conv2d branch computing checked_total_padding = (input - 1) * stride + dilated_filter - input * stride, a formula derived from WebNN’s transpose-conv output-size definition rather than from how TFLite’s TransposeConv kernel actually interprets SAME padding. GetTfLitePaddingMode fed the input height/width into this calculation and ignored WebNN’s non-zero output padding, so the padding selected for the TFLite SAME path did not match the sizes the TFLite kernel would use, producing an output tensor whose real dimensions diverged from the WebNN-validated/allocated output — a size mismatch that underlies the heap buffer overflow (inferred for the exact write).

The fix removes the transpose-specific branch, makes CalculateExplicitPaddingForSamePaddingMode always use the forward-conv SAME formula, and has GetTfLitePaddingMode pass the output size (output.height/output.width) for transpose conv so the computation mirrors the TFLite kernel. It also rejects transpose conv outright when explicit padding would be required (return base::unexpected("Explicit padding is not supported for transpose conv2d.")), closing the companion bug (crbug.com/491869941) where the old code mis-padded the transpose-conv input, and it reworks the checked arithmetic to int32_t so intermediate subtractions cannot underflow an unsigned type.

Key insight
The transpose-conv SAME padding was computed from the input size with a WebNN-specific formula that disagreed with TFLite’s kernel (which uses the output size and rejects output padding), so the lowered graph’s tensor sizes were wrong; the fix aligns the calculation to the output size and refuses the unsupported explicit-padding/output-padding cases.

Attack Path

  1. Build a transpose conv graph From a renderer, script uses MLGraphBuilder.convTranspose2d with SAME-equivalent padding and (critically) a non-zero output padding, submitting the graph to the WebNN service.
  2. Mis-calculate padding The old is_transposed_conv2d branch, fed the input size and ignoring output padding, returns a padding that does not match TFLite’s TransposeConv SAME behavior.
  3. Size divergence The TFLite graph is built with a padding/output size inconsistent with the WebNN-validated output tensor, so the kernel’s working/output extent differs from the allocated buffer (the pre-fix WPT expectations show “Output tensor size mismatch” and wildly wrong sample values).
  4. Heap overflow Executing the graph reads/writes outside the output allocation during the transpose-conv kernel run, a heap buffer overflow in the WebNN backend (inferred); the fix now rejects these cases with “Explicit padding is not supported for transpose conv2d.”

Impact Assessment

An attacker able to submit a crafted WebNN graph (ordinary web content using the WebNN convTranspose2d API, where the feature is enabled) can drive a size mismatch leading to a heap buffer overflow in the WebNN service that lowers graphs to TFLite. The vendor advisory rates this high severity and labels it a heap buffer overflow in WebML; it does not report in-the-wild exploitation. The precise out-of-bounds location and any escalation to a controlled primitive or code execution are not shown by the diff (inferred).

Changed Functions

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

Files Changed

  • services/webnn/tflite/graph_builder_tflite.cc

Audit Directions

  • Backend padding/size formulas
    Audit every place a WebNN operator’s padding or output size is recomputed for a backend kernel and confirm it reproduces that kernel’s exact rule (TFLite transpose conv uses output size, not input size).
  • Unsigned checked arithmetic on differences
    Flag base::CheckedNumeric<uint32_t> subtractions (e.g. needed_input - input, ... - padding_begin) where intermediates can go negative; underflow yields huge sizes that bypass later bounds reasoning.
  • Operator attributes TFLite cannot express
    Review operators with WebNN-only attributes (output padding, dilations, groups) that are silently dropped when mapping to a backend; drop the attribute and you desynchronize allocated and computed tensor sizes.
From d141d62357df25a1ed50dc8494a73dca4fffa29c Mon Sep 17 00:00:00 2001
From: junwei <junwei.fu@intel.com>
Date: Thu, 19 Mar 2026 04:36:10 -0700
Subject: [PATCH] WebNN: Use output size for TransposeConv SAME padding in TFLite

This CL aligns the TFLite backend's padding calculation for
convTranspose2d with the TFLite kernel implementation.

Previous implementation ignores WebNN convTranspose2d's non-zero output
padding which is not supported by TFLite SAME padding mode. However,
TFLite's TransposeConv kernel calculates 'SAME' padding by treating the
output size as the input to a regular convolution formula.

This CL also fixes an issue of the previous implementation that
incorrectly pads the input of transpose conv for explicit paddings
(crbug.com/491869941) by rejecting it. It should crop the output after
zero-padding (VALID) transpose conv instead. It will be implemented in a
separate CL.

Bug: 492668885, 491869941
Change-Id: Ibfbcd2bf9b80b6ab2b2f0fccf9596975537f9cc8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7677538
Reviewed-by: Hu, Ningxin <ningxin.hu@intel.com>
Commit-Queue: Fu, Junwei <junwei.fu@intel.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1601883}
---

diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index d524a8d..4af70a3 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -216,40 +216,50 @@
 
 // Helper to calculate the explicit padding for tflite::Padding_SAME mode with
 // https://www.tensorflow.org/versions/r2.14/api_docs/python/tf/nn#notes_on_padding_2.
+// For transpose conv, caller should pass output size as input_size.
 std::optional<PaddingSizes> CalculateExplicitPaddingForSamePaddingMode(
     uint32_t input_size,
     uint32_t filter_size,
     uint32_t stride,
-    uint32_t dilation,
-    bool is_transposed_conv2d) {
-  base::CheckedNumeric<uint32_t> checked_dilated_filter_size =
-      (base::CheckedNumeric(filter_size) - 1) * dilation + 1;
-  base::CheckedNumeric<uint32_t> checked_input_size = input_size;
-  base::CheckedNumeric<uint32_t> checked_total_padding;
-  if (is_transposed_conv2d) {
-    // The checked_total_padding (beginningPadding + endingPadding) can be
-    // calculated from the expression `outputSize = (inputSize - 1) * stride +
-    // (filterSize - 1) * dilation + 1 - beginningPadding - endingPadding` that
-    // is documented in the section of computing convtranspose output size:
-    // https://www.w3.org/TR/webnn/#api-mlgraphbuilder-convtranspose2d
-    checked_total_padding = (checked_input_size - 1) * stride +
-                            checked_dilated_filter_size -
-                            checked_input_size * stride;
-  } else {
-    auto checked_output_size = (checked_input_size + stride - 1) / stride;
-    auto checked_needed_input_size =
-        (checked_output_size - 1) * stride + checked_dilated_filter_size;
-    if (!checked_needed_input_size.IsValid()) {
-      return std::nullopt;
-    }
-    checked_total_padding = checked_needed_input_size.ValueOrDie() > input_size
-                                ? checked_needed_input_size - input_size
-                                : base::CheckedNumeric<uint32_t>(0);
+    uint32_t dilation) {
+  // The SAME padding mode in TFLite follows the formula:
+  // output_size = ceil(input_size / stride)
+  // total_padding = (output_size - 1) * stride + dilated_filter_size -
+  // input_size See:
+  // https://www.tensorflow.org/versions/r2.14/api_docs/python/tf/nn#notes_on_padding_2
+  auto checked_dilated_filter_size = base::CheckedNumeric<int32_t>(filter_size);
+  checked_dilated_filter_size -= 1;
+  checked_dilated_filter_size *= base::CheckedNumeric<int32_t>(dilation);
+  checked_dilated_filter_size += 1;
+
+  auto checked_input_size = base::CheckedNumeric<int32_t>(input_size);
+  auto checked_stride = base::CheckedNumeric<int32_t>(stride);
+  base::CheckedNumeric<int32_t> checked_output_size = checked_input_size;
+  checked_output_size += checked_stride;
+  checked_output_size -= 1;
+  checked_output_size /= checked_stride;
+
+  base::CheckedNumeric<int32_t> checked_needed_input_size = checked_output_size;
+  checked_needed_input_size -= 1;
+  checked_needed_input_size *= checked_stride;
+  checked_needed_input_size += checked_dilated_filter_size;
+  if (!checked_needed_input_size.IsValid()) {
+    return std::nullopt;
   }
+  uint32_t needed_input_size;
+  if (!checked_needed_input_size.AssignIfValid(&needed_input_size)) {
+    return std::nullopt;
+  }
+  base::CheckedNumeric<uint32_t> checked_total_padding =
+      needed_input_size > input_size
+          ? base::CheckedNumeric<uint32_t>(needed_input_size) - input_size
+          : base::CheckedNumeric<uint32_t>(0);
 
   // Same upper padding.
-  auto checked_padding_begin = checked_total_padding / 2;
-  auto checked_padding_end = (checked_total_padding + 1) / 2;
+  base::CheckedNumeric<uint32_t> checked_padding_begin =
+      checked_total_padding / 2;
+  base::CheckedNumeric<uint32_t> checked_padding_end =
+      (checked_total_padding + 1) / 2;
   uint32_t padding_begin, padding_end;
   if (!checked_padding_begin.AssignIfValid(&padding_begin) ||
       !checked_padding_end.AssignIfValid(&padding_end)) {
@@ -274,19 +284,22 @@
     uint32_t output_size,
     uint32_t padding_begin) {
   // Calculate the dilated filter sizes that are validated in graph validation.
-  base::CheckedNumeric<uint32_t> checked_effective_filter_size = filter_size;
+  auto checked_effective_filter_size =
+      base::CheckedNumeric<int32_t>(filter_size);
   checked_effective_filter_size -= 1;
-  checked_effective_filter_size *= dilation;
+  checked_effective_filter_size *= base::CheckedNumeric<int32_t>(dilation);
   checked_effective_filter_size += 1;
   CHECK(checked_effective_filter_size.IsValid());
 
   // Adjust ending padding to match the specified output.
-  base::CheckedNumeric<uint32_t> checked_padding_end = output_size;
-  checked_padding_end -= 1;
-  checked_padding_end *= stride;
-  checked_padding_end += checked_effective_filter_size;
-  checked_padding_end -= input_size;
-  checked_padding_end -= padding_begin;
+  auto checked_padding_end_int32 = base::CheckedNumeric<int32_t>(output_size);
+  checked_padding_end_int32 -= 1;
+  checked_padding_end_int32 *= base::CheckedNumeric<int32_t>(stride);
+  checked_padding_end_int32 += checked_effective_filter_size;
+  checked_padding_end_int32 -= base::CheckedNumeric<int32_t>(input_size);
+  checked_padding_end_int32 -= base::CheckedNumeric<int32_t>(padding_begin);
+
+  auto checked_padding_end = checked_padding_end_int32.Cast<uint32_t>();
   // Check if the value is valid for rounding to uint32_t type.
   if (!checked_padding_end.IsValid()) {
     return base::unexpected("The padding end is too large.");
@@ -302,6 +315,7 @@
     const webnn::Size2d<uint32_t>& filter,
     const mojom::Size2d& stride,
     const mojom::Size2d& dilation,
+    const webnn::Size2d<uint32_t>& output,
     bool is_transposed_conv2d) {
   // WebNN explicit padding is in [beginning_height, ending_height,
   // beginning_width, ending_width] sequence.
@@ -315,13 +329,18 @@
 
   // Convert the explicit padding to tflite same padding mode, The TFLite PAD
   // operator need to be inserted if the calculated padding are not the same as
-  // explicit padding.
+  // explicit padding for direct conv.
+  //
+  // In TFLite, TransposeConv's SAME padding is calculated based on the
+  // output size. See:
+  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/litert/src/tflite/kernels/transpose_conv.cc;drc=7d88950ea445f5b671d18e64f9614aff397fde50;l=841
+  const uint32_t height_size =
+      is_transposed_conv2d ? output.height : input.height;
+  const uint32_t width_size = is_transposed_conv2d ? output.width : input.width;
   const auto padding_height = CalculateExplicitPaddingForSamePaddingMode(
-      input.height, filter.height, stride.height, dilation.height,
-      is_transposed_conv2d);
+      height_size, filter.height, stride.height, dilation.height);
   const auto padding_width = CalculateExplicitPaddingForSamePaddingMode(
-      input.width, filter.width, stride.width, dilation.width,
-      is_transposed_conv2d);
+      width_size, filter.width, stride.width, dilation.width);
   if (!padding_height || !padding_width) {
     return base::unexpected("Failed to calculate explicit padding.");
   }
@@ -332,6 +351,15 @@
     return TfLitePadding{.mode = ::tflite::Padding_SAME};
   }
 
+  // TFLite's TransposeConv SAME padding mode doesn't support output padding.
+  // Passing output size with non-zero output padding won't match and select
+  // TFLite SAME padding mode.
+  // TODO(crbug.com/493652470): Support explicit padding for transpose conv2d.
+  if (is_transposed_conv2d) {
+    return base::unexpected(
+        "Explicit padding is not supported for transpose conv2d.");
+  }
+
   // The explicit padding are used to insert a TfLite PAD operator.
   return TfLitePadding{.mode = ::tflite::Padding_VALID,
                        .paddings = explicit_padding};
@@ -380,7 +408,7 @@
     // Otherwise, a TFLite PAD operator will be inserted later using VALID
     // padding.
     return GetTfLitePaddingMode(padding2d, input, filter, stride, dilation,
-                                /*is_transposed_conv2d=*/false);
+                                output, /*is_transposed_conv2d=*/false);
   } else if (actual_output_height ==
                  base::ClampCeil<uint32_t>(calculated_output_sizes.height) &&
              actual_output_width ==
@@ -4091,10 +4119,12 @@
     }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_gpu-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_gpu-expected.txt
index 1082602..7d6b36e 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_gpu-expected.txt
+++ b/third_party/blink/web_tests/platform/mac/virtual/webnn-service-with-gpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_gpu-expected.txt
@@ -4,15 +4,13 @@
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.groups=2 options.strides=[2, 2]
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float32 actual 0 should be close enough to expected 0.2787136137485504 by ULP distance: expected a number less than or equal to 8n but got 1049539469n
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
-[FAIL] [required] convTranspose2d same output size different padding (padding=1, outputPadding=0))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
 [FAIL] [required] convTranspose2d same output size different padding (padding=2, outputPadding=2))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float16 actual 0 should be close enough to expected 0.27880859375 by ULP distance: expected a number less than or equal to 8 but got 13430
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_cpu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_cpu-expected.txt
index 1082602..7d6b36e 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_cpu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-cpu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_cpu-expected.txt
@@ -4,15 +4,13 @@
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.groups=2 options.strides=[2, 2]
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float32 actual 0 should be close enough to expected 0.2787136137485504 by ULP distance: expected a number less than or equal to 8n but got 1049539469n
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
-[FAIL] [required] convTranspose2d same output size different padding (padding=1, outputPadding=0))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
 [FAIL] [required] convTranspose2d same output size different padding (padding=2, outputPadding=2))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float16 actual 0 should be close enough to expected 0.27880859375 by ULP distance: expected a number less than or equal to 8 but got 13430
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_npu-expected.txt b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_npu-expected.txt
index 1082602..7d6b36e 100644
--- a/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_npu-expected.txt
+++ b/third_party/blink/web_tests/virtual/webnn-service-on-npu/external/wpt/webnn/conformance_tests/conv_transpose2d.https.any_npu-expected.txt
@@ -4,15 +4,13 @@
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.groups=2 options.strides=[2, 2]
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float32 actual 0 should be close enough to expected 0.2787136137485504 by ULP distance: expected a number less than or equal to 8n but got 1049539469n
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float32 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
-[FAIL] [required] convTranspose2d same output size different padding (padding=1, outputPadding=0))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
 [FAIL] [required] convTranspose2d same output size different padding (padding=2, outputPadding=2))
-  promise_test: Unhandled rejection with value: object "UnknownError: Failed to execute 'build' on 'MLGraphBuilder': Output tensor size mismatch: convTranspose2dOutput"
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.padding
-  assert_less_than_equal: assert_array_approx_equals_ulp: test convTranspose2d float16 actual 0 should be close enough to expected 0.27880859375 by ULP distance: expected a number less than or equal to 8 but got 13430
+  promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': Explicit padding is not supported for transpose conv2d."
 [FAIL] [required] convTranspose2d float16 4D input and filter tensors options.dilations
   promise_test: Unhandled rejection with value: object "NotSupportedError: Failed to execute 'build' on 'MLGraphBuilder': convTranspose2d doesn't support dilations and groups."
 Harness: the test ran to completion.
Loading diff…

Original Bug Report

reported by he...@gmail.com

OOB read in TFLite TransposeConvV2

Summary

WebNN convTranspose2d with outputPadding or outputSizes causes a heap-buffer-overflow in TFLite’s TransposeConvV2. The service-side ConvertToConvTranspose2dAttributes drops the adjustment fields and keeps only the final output shape, so GetTfLitePaddingMode misclassifies the operation as kTfLitePaddingSame. TFLite then recomputes padding assuming zero adjustment, producing a geometry mismatch that overreads the temporary col_data buffer during dispatch().

Details

The bug is a semantic truncation across the Blink -> service boundary that cascades into a OOB read.

First, the Blink layer correctly preserves the transpose-conv adjustment geometry. ConvertToConvTranspose2dAttributes captures both outputPadding and outputSizes:

const auto output_padding = options->getOutputPaddingOr({0, 0});
attributes.value().output_padding = webnn::Size2d<uint32_t>{
    .height = output_padding[0], .width = output_padding[1]};

if (options->hasOutputSizes()) {
  auto output_sizes = options->getOutputSizesOr({});
  attributes.value().output_sizes = webnn::Size2d<uint32_t>{
      .height = output_sizes[0], .width = output_sizes[1]};
}

However, the service-side reconstruction discards this information. The Mojo Conv2d type does not carry outputPadding or outputSizes. The service-side ConvertToConvTranspose2dAttributes reconstructs only the final output dimensions from the operand descriptor, losing the reason the output differs from canonical SAME geometry:

auto* output = GetMojoOperand(operands, conv2d.output_operand_id);
CHECK_EQ(output->descriptor.Rank(), 4u);
webnn::Size2d<uint32_t> output_sizes;
switch (context_properties.input_operand_layout) {
  case webnn::InputOperandLayout::kNhwc:
    output_sizes.height = output->descriptor.shape()[1];
    output_sizes.width = output->descriptor.shape()[2];
    component_attributes.filter_layout =
        ConvTranspose2dFilterOperandLayout::kOhwi;
    break;
  ...
}
component_attributes.output_sizes = std::move(output_sizes);

Consequently, the TFLite backend misclassifies the padding mode. Without the adjustment fields, it cannot distinguish canonical SAME from adjusted SAME. GetTfLitePaddingMode selects kTfLitePaddingSame based solely on the explicit padding tuple, and SerializeConv2d lowers the operation with that mode plus the (adjusted) output-shape tensor:

if (explicit_padding == upper_padding) {
  return TfLitePadding{.mode = ::tflite::Padding_SAME};
}
...
op_inputs = {output_shape_tensor_index, filter_tensor_info.index,
             explicit_pad_index.value_or(input_tensor_info.index), bias_index};
operator_kind = ::tflite::BuiltinOperator_TRANSPOSE_CONV;
builtin_options = ::tflite::CreateTransposeConvOptions(
                      builder_, padding_mode.mode, conv2d.strides->width,
                      conv2d.strides->height, activation_type)
                      .Union();

At runtime, this mismatch causes a heap-buffer-overflow. EvalFloat delegates to TransposeConvV2, which recomputes SAME padding from the output dimensions assuming zero adjustment. Its Col2im loop traverses col_data using the recomputed geometry:

for (int h = 0; h < height_col; ++h) {
  int w_pad = -pad_l;
  for (int w = 0; w < width_col; ++w) {
    T* im_patch_data = im_data + (h_pad * width + w_pad) * depth;
    for (int ih = h_pad; ih < h_pad + filter_h; ++ih) {
      for (int iw = w_pad; iw < w_pad + filter_w; ++iw) {
        if (ih >= 0 && ih < height && iw >= 0 && iw < width) {
          for (int i = 0; i < depth; ++i) {
            im_patch_data[i] += col_data[i];
          }
        }
        im_patch_data += depth;
        col_data += depth;
      }

For example, with padding [0,1,0,1], stride=[2,2], filter=[1,3,3,1], and an adjusted output like 7x7 / 7x6 / 6x7, Chromium tells TFLite to use SAME but passes a non-canonical output extent. The Col2im traversal runs past the 340-byte temporary buffer, producing a heap-buffer-overflow.

Bisection

This issue is introduced by the commit https://chromium-review.googlesource.com/c/chromium/src/+/5635194

Reproduction

Run chrome from https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1598914.zip with the following command:

./chrome --enable-features=ExperimentalWebMachineLearningNeuralNetwork,WebMachineLearningNeuralNetwork --no-sandbox poc.html

You would observe the OOB shown in asan.txt

View on issue tracker