CVE-2026-5867
Overview
Background
- WebNN / `convTranspose2d`
- the Web Neural Network API’s transposed-convolution operator, which a page builds through
MLGraphBuilderand 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
PADoperator 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
convTranspose2dallows 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.
Attack Path
- Build a transpose conv graph
From a renderer, script uses
MLGraphBuilder.convTranspose2dwith SAME-equivalent padding and (critically) a non-zerooutput padding, submitting the graph to the WebNN service. - Mis-calculate padding
The old
is_transposed_conv2dbranch, fed the input size and ignoring output padding, returns a padding that does not match TFLite’sTransposeConvSAME behavior. - 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).
- 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
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
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.cc
Audit Directions
- Backend padding/size formulasAudit 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 differencesFlag
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 expressReview 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.
Patch
From d141d62357df25a1ed50dc8494a73dca4fffa29c Mon Sep 17 00:00:00 2001 From: junwei <[email protected]> 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 <[email protected]> Commit-Queue: Fu, Junwei <[email protected]> Reviewed-by: Reilly Grant <[email protected]> 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 @@ }
Regression Test / PoC
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.
Original Bug Report
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
- https://chromium-review.googlesource.com/c/chromium/src/+/5635194
- https://source.chromium.org/chromium/chromium/src/+/main:services/webnn/tflite/graph_builder_tflite.cc;l=299
- https://source.chromium.org/chromium/chromium/src/+/main:services/webnn/tflite/graph_builder_tflite.cc;l=4024
- https://source.chromium.org/chromium/chromium/src/+/main:services/webnn/webnn_graph_builder_impl.cc;l=328
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/ml/webnn/ml_graph_builder.cc;l=650
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/tflite/src/tensorflow/lite/kernels/internal/optimized/optimized_ops.h;l=5015
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/tflite/src/tensorflow/lite/kernels/transpose_conv.cc;l=522
- https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1598914.zip