CVE-2026-5867
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.cc
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