Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebML
DescriptionHeap buffer overflow in WebML
ComponentWebML
Bug ClassOOB
Tracker493319454
Fix commitfc10b0d6304d (chromium/src) +13/-0
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-04-07

Changed Functions

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

Files Changed

  • services/webnn/tflite/graph_builder_tflite.cc
From fc10b0d6304d7d8f31fea83811aa673a7f13f720 Mon Sep 17 00:00:00 2001
From: Wei Wang <[email protected]>
Date: Wed, 18 Mar 2026 20:00:51 -0700
Subject: [PATCH] [WebNN] Reject fusing per-channel quantized gemm if the quantized dimension of filter is not 0

The FULLY_CONNECTED's underlying kernels expect the per-channel
quantization axis to be the output channel(axis 0). So reject
fusing per-channel quantized gemm and fall back to the unfused
operators path if the quantized dimension of filter is not 0.

Bug: 493319454
Change-Id: Ib7e1236a535dc6a34d3ff9b9f0124a101bd89dbf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7673406
Reviewed-by: Phillis Tang <[email protected]>
Commit-Queue: Wang, Wei4 <[email protected]>
Reviewed-by: Hu, Ningxin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1601718}
---

diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index 984596dc..d524a8d 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -2090,6 +2090,19 @@
     return std::nullopt;
   }
 
+  // The FULLY_CONNECTED's underlying kernels expect the per-channel
+  // quantization axis to be the output channel dimension (axis 0). This means
+  // the first dimension of the scale can not be equal to 1.
+  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/litert/src/tflite/kernels/internal/reference/integer_ops/fully_connected.h;l=68;drc=9213607704a73d1e877921d0454abb11f761bdcc
+  if (per_channel_quantization) {
+    const auto& scale_shape =
+        GetOperand(b_dequantize.scale_operand_id).descriptor.shape();
+
+    if (scale_shape[0] == 1) {
+      return std::nullopt;
+    }
+  }
+
   // The a_scale * b_scale should be about the same as c_scale for per-tensor
   // quantization.
   // https://source.chromium.org/chromium/chromium/src/+/main:third_party/tflite/src/tensorflow/lite/kernels/kernel_util.cc;l=303;drc=492dc9719f6e1845f4f5c0553cd5c7651115f671
Loading diff…

Original Bug Report

reported by [email protected]

Heap buffer overflow in TFLite FullyConnectedPerChannel via WebNN quantized GEMM with wrong per-axis quantization dimension

Title

Heap buffer overflow in TFLite FullyConnectedPerChannel via WebNN quantized GEMM with wrong per-axis quantization dimension

Summary

The WebNN-to-TFLite translation layer fuses a quantized GEMM into TFLite’s FULLY_CONNECTED operator without validating that the weight tensor’s per-channel quantization axis is dimension 0 (output channels). An attacker can supply a weight tensor with per-channel quantization along dimension 1 (input channels) instead, causing PrepareImpl to allocate per-channel multiplier and shift arrays sized to the input channel count rather than the output channel count. When the kernel evaluates, it iterates over output channels and reads past the end of those arrays, producing a heap-buffer-overflow in the GPU process. Affected platforms: all where the TFLite WebNN backend is active (Linux, ChromeOS, Android by default; Windows and macOS as a fallback). No special GPU hardware is required.

Bisect

TFLite upstream — FullyConnectedPerChannel introduced

This commit introduced per-channel quantization for FULLY_CONNECTED in a single change: PrepareImpl allocates per_channel_output_multiplier and per_channel_output_shift arrays sized to filter->dims->data[quantized_dimension], with only a self-referential consistency check (scale->size == filter->dims->data[quantized_dimension]) and no enforcement that quantized_dimension == 0. The optimized FullyConnectedPerChannel kernel then iterates over filter->dims->data[0] (output channels) elements, causing a heap OOB read when quantized_dimension != 0 and dim 0 > dim quantized_dimension.

Became web-reachable

This commit added quantized GEMM fusion in the WebNN TFLite graph builder without validating that the weight tensor’s per-channel quantization axis is dimension 0.

Root Cause

WebNN’s dequantizeLinear allows per-channel quantization where the scale tensor has a non-unit dimension indicating the quantization axis. When fusing a quantized GEMM into TFLite’s FULLY_CONNECTED, CanFuseQuantizeAndGetOutput(const mojom::Gemm&) checks only whether B’s scale has more than one element and whether bTranspose is true:

// services/webnn/tflite/graph_builder_tflite.cc:2072-2083
size_t number_of_b_scale =
    GetOperand(b_dequantize.scale_operand_id).descriptor.NumberOfElements();
const bool per_channel_quantization = number_of_b_scale != 1;
if (per_channel_quantization && !gemm.b_transpose) {
  return std::nullopt;
}

There is no validation that B’s quantization axis will map to TFLite FULLY_CONNECTED’s filter dimension 0 (output channels). If B has shape [N, K] and its scale has shape [1, K], SerializeQuantizeParams computes the quantization axis by scanning the scale shape for the non-unit dimension, arriving at axis 1:

// services/webnn/tflite/graph_builder_tflite.cc:7001-7009
for (size_t i = 0; i < scale_shape.size(); ++i) {
  if (scale_shape[i] != 1) {
    axis = (input_rank - scale_shape.size()) + i;   // yields axis = 1
  }
}

TFLite’s fully_connected::PrepareImpl validates only that the scale array length matches the filter size along the declared quantization dimension, which is a self-consistent but semantically wrong check:

// third_party/tflite/src/tensorflow/lite/kernels/fully_connected.cc:453-463
TF_LITE_ENSURE_EQ(context, affine_quantization->scale->size,
                  filter->dims->data[affine_quantization->quantized_dimension]);
// ...
data->per_channel_output_multiplier.resize(per_channel_quantization_size);
data->per_channel_output_shift.resize(per_channel_quantization_size);

For a filter of shape [1024, 2] with quantized_dimension = 1, the arrays are allocated with size 2. The optimized FullyConnectedPerChannel kernel then passes these 2-element arrays as per-row multipliers to the ruy GEMM backend, which expects 1024 entries (one per output row). The ruy kernel reads well past the allocated buffer.

Reproduce

Tested at commit 3ad31ba232d9a804b4de78d788e391f82b40a906. No source modifications required.

Build:

autoninja -C out/asan-release chrome

Windows

ASAN_OPTIONS=detect_odr_violation=0 out/asan-release/chrome.exe ^
  --no-sandbox ^
  --enable-features=WebMachineLearningNeuralNetwork ^
  --disable-features=WebNNOnnxRuntime ^
  --user-data-dir=%TEMP%\poc-tflite005 ^
  --enable-logging=stderr ^
  poc.html

Linux

out/asan-release/chrome \
  --no-sandbox \
  --enable-features=WebMachineLearningNeuralNetwork \
  --enable-logging=stderr \
  poc.html

The GPU process crashes within seconds. ASAN reports a heap-buffer-overflow READ of size 32 in ruy::Kernel8bitAvx2Impl, called from FullyConnectedPerChannel. The complete ASAN log is attached in issue_tflite005/asan.log.

==23132==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x1254fc5da8af
READ of size 32 at 0x1254fc5da8af thread T23
    #0 ruy::Kernel8bitAvx2Impl<32>           kernel_avx2_fma.cc:359
    #5 GemmImplUsingRuy::Run                 cpu_backend_gemm_ruy.h:141
    #6 optimized_integer_ops::FullyConnectedPerChannel  fully_connected.h:95
    #7 fully_connected::EvalQuantized<1>     fully_connected.cc:1510
    #8 fully_connected::Eval<1>              fully_connected.cc:1795
    #12 webnn::tflite::GraphImplTflite::ComputeResources::DoDispatch  graph_impl_tflite.cc:328

0x1254fc5da8af is located 1 bytes before 8-byte region
allocated by thread T23 here:
    #1 std::vector<int>::resize              vector.h:1362
    #2 fully_connected::PrepareImpl          fully_connected.cc:463

SUMMARY: AddressSanitizer: heap-buffer-overflow ruy::Kernel8bitAvx2Impl

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker