3dc832137518fd41f9a8eb29f5559bedb7d3d1ca WebNN: Emit self-consistent ONNX pooling descriptors for ceil rounding
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
- 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’.
- 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.
- 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).
- 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
- Graph construction — Build a WebNN pooling graph whose ceil rounding plus dilation produces an overhanging last window.
- Execution — Run inference on an execution provider (default CPU EP) that does not reject the inconsistent descriptor, causing reads beyond input bounds (inferred).
- 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.