← Chrome — 2026-W34

3dc832137518fd41f9a8eb29f5559bedb7d3d1ca  WebNN: Emit self-consistent ONNX pooling descriptors for ceil rounding

severity high class OOB CVSS 6.5 signal score 11.79 services/ AI review PLAUSIBLE
Structural signals: checked_int (+4.06), security_assert (+3.0), clamp_saturate (+2.92), area:services/ (+1.8)

The bug at a glance

The ORT WebNN backend emitted an inconsistent ONNX pooling descriptor (ceil_mode=1 without enlarged padding) whose ceil-rounded output requires the last window to sample input positions past the padded input bounds; the commit states this ‘implies an out-of-bounds read’ and that the default CPU execution provider silently produced a different result, so this is a genuine but EP-dependent out-of-bounds read reachable only through WebNN.

An attacker controlling a WebNN graph (model shape, pooling window, dilation, strides, roundingType=ceil) could emit a descriptor that drives a downstream ONNX execution provider to read input samples beyond the tensor bounds, potentially leaking adjacent memory into the output tensor or causing divergent/unstable results.

Root cause

For pool2d with roundingType ‘ceil’, the backend set ONNX ceil_mode=1 but left padding unchanged. The ceil-rounded output size means the last pooling window, especially with dilation, extends past the (padded) input extent, so the generated descriptor is self-inconsistent and the overhang maps to positions outside the input tensor. Execution providers handled this inconsistently: DML and OV GPU EPs rejected the graph while the default CPU EP silently read past the bounds. The fix adds CalculatePoolPaddingEndForCeilRounding(), which computes the exact extra ending padding ((output-1)*stride + effective_filter - input - padding_begin, clamped >=0) so the overhanging positions become explicit padding, then emits ceil_mode=0, making the descriptor self-consistent without changing numeric semantics.

Key code

Enlarge ending padding to absorb the ceil overhang so no window samples past input bounds

const auto padding_end = (base::CheckedNumeric(output_size) - 1) * stride +
                         effective_filter_size - input_size - padding_begin;
CHECK(padding_end.IsValid());
return std::max<int64_t>(padding_end.ValueOrDie(), 0);

Patch walkthrough

  • services/webnn/ort/graph_builder_ort.cc — Adds CalculatePoolPaddingEndForCeilRounding() and, when a spatial dimension used ceil rounding, grows pads[2]/pads[3] to cover the overhang and emits ceil_mode=0 instead of 1; this removes the descriptor inconsistency that implied an out-of-bounds input read in downstream execution providers while leaving padding-value semantics (and thus numeric output) unchanged.

Background

WebNN ORT backend (services/webnn/ort) — Translates WebNN graph operations into ONNX Runtime graph descriptors executed by backend execution providers (CPU/DML/OV).

ONNX ceil_mode — A global Pool attribute that rounds output spatial size up; by itself it does not add padding, so a ceil-rounded output can require windows that overhang the input.

Pooling dilation + ceil overhang — A dilated window combined with ceil rounding maximizes how far the last window reaches beyond the input, which is the specific case that triggered rejection or OOB reads.

Vulnerability window

  1. Reachability — Reachable from web content via the WebNN API (gated behind the WebNN feature/flag or origin trial — (inferred)) using the ORT backend, by building a pool2d (max/average/l2) graph with roundingType ‘ceil’.
  2. Trigger — Supply a pooling op whose dilation/stride/window with ceil rounding makes the last window overhang the input, producing the inconsistent ceil_mode=1 descriptor.
  3. Primitive — The default CPU execution provider silently reads input positions past the tensor bounds (out-of-bounds read) when executing the inconsistent descriptor (inferred — exact EP read behavior is downstream).
  4. Escalation — Out-of-bounds samples could surface adjacent memory in the pooling output (information disclosure) or cause divergent results; crash/rejection in stricter EPs (inferred).

Proof of concept

Conceptually, construct a WebNN pool2d (e.g. maxPool2d) with a dilated window, a stride, and roundingType ‘ceil’ chosen so the ceil-rounded output size forces the final window past the input extent, then run it on the ORT CPU execution provider; no exploit code provided.

Exploitation

  1. Graph construction — Build a WebNN pooling graph whose ceil rounding plus dilation produces an overhanging last window.
  2. Execution — Run inference on an execution provider (default CPU EP) that does not reject the inconsistent descriptor, causing reads beyond input bounds (inferred).
  3. Observation — Read the pooling output to recover whatever out-of-bounds data was sampled (inferred).

Detection & hunting

For defenders and detection engineers:

  • grep services/webnn for CreateAttribute(kAttrCeilMode, 1) or ceil_mode set without a corresponding padding adjustment — Flag any backend that forwards ceil_mode=1 to ONNX/other runtimes without converting the overhang to explicit padding.

Audit directions

  • Other WebNN ORT ops that depend on ceil/floor output-size rounding (conv2d output padding, resample, convTranspose) and the TFLite/DML backends — Verify each emits descriptors consistent with the rounded output size so no window or sampling index can reference positions outside the input tensor.

Before / after

Loading diff…