CVE-2026-7896
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
CanFuseActivationAndGetOutputservices/webnn/tflite/graph_builder_tflite.cc |
modified | |
CanFuseQuantizeAndGetOutputservices/webnn/tflite/graph_builder_tflite.cc |
modified |
Files Changed
services/webnn/tflite/graph_builder_tflite.cc
Patch
From db6bda50f023057ffa82845f232852dea0f271e1 Mon Sep 17 00:00:00 2001 From: Lynne Jiang <[email protected]> Date: Wed, 25 Mar 2026 11:10:24 -0700 Subject: [PATCH] [webnn] Switch to use SerializeTemporaryTensorWithByteSizeCheck in tflite graph builder. Bug: 493747582 Binary-Size: Size increase is expected. Change-Id: I420f1a26507007af0e4c17a5a48b1ea30e1b4c9d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7693793 Reviewed-by: Reilly Grant <[email protected]> Reviewed-by: Phillis Tang <[email protected]> Commit-Queue: Lynne Jiang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1604964} --- diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc index 8b243bd..22a65aea 100644 --- a/services/webnn/tflite/graph_builder_tflite.cc +++ b/services/webnn/tflite/graph_builder_tflite.cc @@ -1147,11 +1147,12 @@ return input_tensor_info; } -GraphBuilderTflite::TensorInfo GraphBuilderTflite::SerializeOutputTensorInfo( +auto GraphBuilderTflite::SerializeOutputTensorInfo( OperandId operand_id, QuantizateParametersOffset quantize_params, bool operation_supports_float16, - std::optional<::tflite::TensorType> override_tensor_type) { + std::optional<::tflite::TensorType> override_tensor_type) + -> base::expected<TensorInfo, std::string> { auto it = operand_to_tensor_info_map_.find(operand_id); if (it != operand_to_tensor_info_map_.end()) { return it->second; @@ -1186,10 +1187,9 @@ tensor_type = *override_tensor_type; } } - const auto serialized_operand_result = - SerializeOperand(operand_id, quantize_params, tensor_type); - CHECK(serialized_operand_result.has_value()); - const TensorInfo output_tensor_info = serialized_operand_result.value(); + + ASSIGN_OR_RETURN(const TensorInfo output_tensor_info, + SerializeOperand(operand_id, quantize_params, tensor_type)); // Insert a TFLite cast operator to convert float32 to float16 if the operand // is graph output and the current operation doesn't support float16 // inference or override to float32 (for example the output tensor of @@ -1213,8 +1213,10 @@ if (output_tensor_info.data_type == ::tflite::TensorType_FLOAT16 && (!operation_supports_float16 || override_float32_type) && is_graph_output) { - const TensorIndex temporary_tensor_index = SerializeTemporaryTensor( - output_tensor_info.dimensions, ::tflite::TensorType_FLOAT32); + ASSIGN_OR_RETURN( + const TensorIndex temporary_tensor_index, + SerializeTemporaryTensorWithByteSizeCheck( + output_tensor_info.dimensions, ::tflite::TensorType_FLOAT32)); graph_output_cast_operators_.emplace_back(SerializeCastOperation( temporary_tensor_index, /*input_tensor_type=*/::tflite::TensorType_FLOAT32, @@ -1638,7 +1640,8 @@ OperandDataType::kFloat32); } -std::optional<GraphBuilderTflite::FusedActivationOutputInfo> +base::expected<std::optional<GraphBuilderTflite::FusedActivationOutputInfo>, + std::string> GraphBuilderTflite::CanFuseActivationAndGetOutput(OperandId output_operand_id) { std::optional<OperationId> next_op_id = GetSoleDependentOperationId(output_operand_id); @@ -1666,15 +1669,14 @@ return std::nullopt; } + ASSIGN_OR_RETURN(const TensorInfo output_tensor_info, + SerializeOutputTensorInfo(activation_output_operand_id)); fused_ops_to_skip_.insert(*next_op_id); - - return FusedActivationOutputInfo( - activation_output_operand_id, - SerializeOutputTensorInfo(activation_output_operand_id).index, - *activation_type); + return FusedActivationOutputInfo(activation_output_operand_id, + output_tensor_info.index, *activation_type); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Clamp& clamp, bool is_emulated) { if (!IsDequantizeOutput(clamp.input_operand_id)) { @@ -1715,7 +1717,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput( const mojom::Conv2d& conv2d, std::optional<OperandId> activation_output_operand_id) { @@ -1830,7 +1832,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Concat& concat) { std::optional<OperandDataType> first_input_quantized_type; if (!std::ranges::all_of( @@ -1897,7 +1899,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput( const mojom::ElementWiseBinary& binary) { if (!IsDequantizeOutput(binary.lhs_operand_id) || @@ -2004,7 +2006,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Elu& elu) { if (!IsDequantizeOutput(elu.input_operand_id)) { return std::nullopt; @@ -2041,7 +2043,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Gather& gather) { if (!IsDequantizeOutput(gather.input_operand_id)) { return std::nullopt; @@ -2073,7 +2075,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Gemm& gemm) { // TODO(crbug.com/372932099): Fuse quantized gemm when gemm.alpha or gemm.beta // isn't 1.0. @@ -2179,7 +2181,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Pad& pad) { // For edge padding mode, it is not supported in tflite schema. if (pad.mode->which() == mojom::PaddingMode::Tag::kEdge) { @@ -2223,7 +2225,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Pool2d& pool2d) { // L2Pool doesn't support quantized implementation. CHECK_NE(pool2d.kind, mojom::Pool2d::Kind::kL2Pool2d); @@ -2275,7 +2277,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Reduce& reduce) { // QDQ fusion only support reduce operation with kSum, kMean, kMax and kMin // kinds. @@ -2316,7 +2318,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput( const mojom::Resample2d& resample2d) { if (!IsDequantizeOutput(resample2d.input_operand_id)) { @@ -2356,7 +2358,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Reshape& reshape) { if (!IsDequantizeOutput(reshape.input_operand_id)) { return std::nullopt; @@ -2405,7 +2407,7 @@ return SerializeQuantizedOutput(*next_op); } -std::optional<GraphBuilderTflite::TensorInfo> +base::expected<std::optional<GraphBuilderTflite::TensorInfo>, std::string> GraphBuilderTflite::CanFuseQuantizeAndGetOutput(const mojom::Slice& slice) { if (!IsDequantizeOutput(slice.input_operand_id)) { return std::nullopt;
Original Bug Report
Integer overflow in XNNPack tensor size computation leads to heap OOB write in the GPU process via WebNN pool2d
Integer overflow in XNNPack tensor size computation leads to heap OOB write in the GPU process via WebNN pool2d
Summary
When the WebNN TFLite backend translates a maxPool2d (or averagePool2d) operation with explicit padding, it inserts an internal PAD node whose output tensor dimensions are not checked against the byte-size limit. XNNPack’s get_tensor_size function then computes the tensor’s byte size by multiplying the element count by the datatype width in a uint64_t, which silently wraps around for sufficiently large shapes. The memory planner allocates a workspace based on the wrapped value while the PAD kernel writes according to the true dimensions, producing an immediate heap buffer overflow in the GPU process. Affected platforms: all platforms where the TFLite WebNN backend is active (Linux, macOS, Windows, ChromeOS).
Bisect
XNNPack upstream — xnn_shape_multiply_all_dims never overflow-checked
- Commit:
0630d2941ca4b96b5f318ac3d03893922d55c3fa - Date: 2021-09-28
- Author: Marat Dukhan ([email protected])
- Subject: Refactor creation and setup of Operators from Nodes
- PiperOrigin-RevId: 399455001
This function is a simple batch_size *= shape->dim[i] loop with no overflow check, and has never had one. get_tensor_size multiplies the result by xnn_datatype_size_bits in uint64_t, which wraps for sufficiently large shapes, causing workspace underallocation.
Became web-reachable — InsertPadOperation introduced with unchecked SerializeTemporaryTensor
- Commit:
ded4396ef94db6b715dda888751563a079f5497e - Date: 2024-03-12
- Author: Junwei Fu ([email protected])
- Subject: webnn: Support Conv2d in TFLite converter
- Review: https://chromium-review.googlesource.com/c/chromium/src/+/5337479
This commit introduced InsertPadOperation using SerializeTemporaryTensor() (no byte-size check) to create internal PAD output tensors. The function validates individual dimensions fit in int32_t but never checks the total byte size against tensor_byte_length_limit. The pool2d path was added subsequently and reuses the same function.
Checked variant introduced but InsertPadOperation NOT updated
- Commit:
00516fef984120c951e854895bbbc80682f8b9d0 - Date: 2025-10-31
- Author: Lynne Jiang ([email protected])
- Subject: webnn: Enable temp tensor byte size check.
- Review: https://chromium-review.googlesource.com/c/chromium/src/+/7092679
This commit introduced SerializeTemporaryTensorWithByteSizeCheck() and updated one call site (float16-to-float32 cast dequantize path), but InsertPadOperation and other callers of SerializeTemporaryTensor were not updated.
Root Cause
The vulnerability spans two components: the Chromium WebNN translation layer and XNNPack’s tensor size arithmetic.
When maxPool2d or averagePool2d receives explicit padding that cannot be expressed as TFLite SAME or VALID, SerializePool2d calls InsertPadOperation to emit a separate PAD node ahead of the pool. This function computes the padded output dimensions with checked arithmetic, verifying each individual dimension fits in int32_t, then creates the tensor:
// services/webnn/tflite/graph_builder_tflite.cc:3563-3565
const TensorIndex output_tensor_index =
SerializeTemporaryTensor(output_shape, input_tensor_info.data_type,
input_tensor_info.quantize_params);
SerializeTemporaryTensor accepts any dimensions without validating total byte size:
// services/webnn/tflite/graph_builder_tflite.cc:3063-3074
TensorIndex GraphBuilderTflite::SerializeTemporaryTensor(
base::span<const int32_t> dimensions,
::tflite::TensorType tensor_type,
QuantizateParametersOffset quantize_params) {
const TensorIndex temporary_tensor_index =
base::checked_cast<TensorIndex>(tensors_.size());
tensors_.emplace_back(::tflite::CreateTensor(
builder_, builder_.CreateVector<int32_t>(dimensions), tensor_type,
/*buffer=*/0, /*name=*/0, quantize_params));
return temporary_tensor_index;
}
The codebase already provides a safe alternative, SerializeTemporaryTensorWithByteSizeCheck, which validates byte length against context_properties_.tensor_byte_length_limit before creation. InsertPadOperation does not use it.
XNNPack delegates the resulting PAD node and computes workspace requirements in get_tensor_size:
// third_party/xnnpack/src/src/tensor.c:779-781
uint64_t size_bits = xnn_datatype_size_bits(datatype);
size_bits *= xnn_shape_multiply_all_dims(shape);
return (size_bits + 7) >> 3;
xnn_shape_multiply_all_dims returns a size_t product of all dimensions:
// third_party/xnnpack/src/src/tensor.c:636-644
size_t xnn_shape_multiply_all_dims(const struct xnn_shape* shape) {
size_t batch_size = 1;
for (size_t i = 0; i < shape->num_dims; i++) {
batch_size *= shape->dim[i];
}
return batch_size;
}
Neither multiplication is overflow-checked. For a padded tensor shape [1, 32768, 32770, 536838146], the element count is 576,460,752,303,516,672. Multiplying by 32 bits yields 18,446,744,073,712,533,504, which exceeds UINT64_MAX by exactly 4,194,304. The wrapped size_bits value is 4,194,304, producing a workspace allocation of 524,288 bytes (512 KB).
The PAD operator’s SSE2 fill kernel then writes zero-padding into this workspace following the true 4D strides. After approximately 131,072 float writes the kernel crosses the 512 KB boundary and corrupts adjacent heap memory, which ASAN detects as a heap-buffer-overflow WRITE.
The PoC constructs this condition through maxPool2d with the following parameters: input [1, 1, 1, 536838146], window [32768, 32770], strides [32768, 32770], and padding [32767, 0, 32769, 0]. The pool’s output shape is [1, 1, 1, 536838146], which passes all WebNN validation, while the internal padded shape overflows XNNPack’s size computation. Using strides equal to the window dimensions keeps the max-pooling indirection buffer at approximately 24 GB, which is large but within the reach of servers and workstations.
Reproduce
Tested at commit 7c89d33808e55 on Linux x64.
ASAN build configuration (out/asan-release/args.gn):
is_asan = true
is_debug = false
dcheck_always_on = false
target_cpu = "x64"
is_component_build = true
Serve the PoC and launch:
python3 -m http.server 8888 --directory issue_tflite005_xnnpack_pad_oob --bind 127.0.0.1 &
ASAN_OPTIONS=detect_odr_violation=0 out/asan-release/chrome \
--no-sandbox \
--enable-features=WebMachineLearningNeuralNetwork \
--user-data-dir=/tmp/poc-$(date +%s) \
http://127.0.0.1:8888/poc.html
Graph construction takes approximately 60 to 90 seconds (the max-pooling indirection buffer is around 24 GB). After dispatch, the GPU process crashes immediately with an ASAN heap-buffer-overflow WRITE:
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b7439c02820
WRITE of size 16 at 0x7b7439c02820 thread T79 (ThreadPoolForeg)
#0 xnn_xx_fill_ukernel__sse2_u64 xx-fill-sse2-u64.c:32
...
#9 xnn_run_operator_with_index operator-run.c:2147
#10 xnn_invoke_runtime runtime.c:1143
#11 tflite::xnnpack::SubgraphInvoke(...) xnnpack_delegate.cc:1429
...
#15 webnn::tflite::GraphImplTflite::ComputeResources::DoDispatch(...) graph_impl_tflite.cc:328
0x7b7439c02820 is located 0 bytes after 524320-byte region [0x7b7439b82800,0x7b7439c02820)
allocated by thread T79 (ThreadPoolForeg) here:
#1 xnn_aligned_allocate allocator.c:46
#2 xnn_plan_memory allocator.h:60
#3 tflite::xnnpack::SubgraphPrepare(...) xnnpack_delegate.cc:1266
SUMMARY: AddressSanitizer: heap-buffer-overflow xx-fill-sse2-u64.c:32 in xnn_xx_fill_ukernel__sse2_u64
The complete ASAN log is in asan.log.
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.