CVE-2026-5885
Overview
Background
- WebNN (`services/webnn`)
- the Web Neural Network service that builds and runs ML graphs on behalf of renderer-supplied
mojomoperation descriptions. - ORT backend / EP
- the ONNX Runtime backend; an Execution Provider (EP) is a hardware-specific implementation (e.g. CPU, DirectML) that consumes the graph nodes WebNN emits.
- `reverse` operation / `axes`
- a WebNN operator that reverses a tensor along the dimensions listed in
axes; an emptyaxesis spec-defined as a no-op. - Slice vs. Identity node
- the ORT graph builder previously lowered
reverseto aSlicenode; anIdentitynode simply copies its input to its output.
Root Cause Analysis
GraphBuilderOrt::AddReverseOperation unconditionally lowered a reverse operator into ORT Slice-node machinery even when reverse.axes was empty. Emitting a Slice node with zero axes is a degenerate construction that, per the commit message, certain EPs mishandle, leading to backend-specific crashes and potential out-of-bounds access during graph initialization. The invariant violated is that every emitted node must be well-formed for the backend, which a zero-axis Slice is not.
The fix adds an early branch: when reverse.axes.empty(), it builds a spec-compliant no-op Identity node (named via kInserted/kOpTypeIdentity/kToEmulate) and returns before any axis-based Slice code runs. This keeps the empty-axes case entirely out of the vulnerable path while preserving correct behavior.
axes array to Slice-node generation, producing a zero-axis node the ORT EPs mishandle; the fix detects the empty case up front and emits a harmless Identity node instead, returning before the Slice path.Attack Path
- Reach WebNN
Web content with access to the WebNN API (ORT backend) constructs a graph containing a
reverseoperation. - Send empty axes
The renderer supplies the
reverseop with an emptyaxesarray overmojomto the WebNN service. - Degenerate node emitted
The pre-patch
GraphBuilderOrt::AddReverseOperationgenerates aSlicenode with zero axes. - Backend mishandling During graph initialization an EP mishandles the empty-axis node, causing a crash and potential out-of-bounds access (inferred; the specific OOB sink is backend-dependent and not in the diff).
Impact Assessment
reverse op with empty axes to crash the graph build and potentially trigger out-of-bounds access during initialization in the service process. Reachability depends on WebNN being available on the platform/configuration (the ORT backend is not universally default-on), so this is not plainly reachable by all stock-build web content. The vendor advisory rates it medium (insufficient validation of untrusted input) and does not report in-the-wild exploitation; the OOB consequence beyond a crash is (inferred).Files Changed
services/webnn/ort/graph_builder_ort.cc
Audit Directions
- Empty collections into backend nodesaudit other WebNN/ORT operator builders for operators that accept empty
axes/dims/index arrays and pass them straight into node construction without a degenerate-case guard. - Spec no-ops lowered to real opswherever a spec-defined no-op is implemented by emitting a backend operator, verify the no-op is special-cased rather than producing a malformed node.
- Backend-specific validation gapsreview assumptions that the renderer-side validation suffices; EPs with differing requirements may need the graph builder to normalize or reject edge-case operands.
Patch
From bf5c0c7bfac1e6fde12d7c903c88c2632d8aa8fe Mon Sep 17 00:00:00 2001
From: Bryan Bernhart <bryan.bernhart@intel.com>
Date: Fri, 20 Feb 2026 13:26:41 -0800
Subject: [PATCH] WebNN: handle empty axes in ORT reverse operation
Previously, the ORT graph builder would attempt to generate a Slice node
with zero axes, which could lead to backend-specific crashes in certain
EPs. This CL ensures that empty axes are handled by emitting an Identity
node instead, maintaining spec compliance and preventing potential OOB
access during graph initialization.
Bug: 485203823
Change-Id: I80b310cb3609beb7907ab8ffbc823af1c6c74a90
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7595579
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Bernhart, Bryan <bryan.bernhart@intel.com>
Cr-Commit-Position: refs/heads/main@{#1588047}
---
diff --git a/services/webnn/ort/graph_builder_ort.cc b/services/webnn/ort/graph_builder_ort.cc
index 4126bbe..131a0046 100644
--- a/services/webnn/ort/graph_builder_ort.cc
+++ b/services/webnn/ort/graph_builder_ort.cc
@@ -2781,13 +2781,26 @@
}
void GraphBuilderOrt::AddReverseOperation(const mojom::Reverse& reverse) {
- const std::string node_name = GenerateNodeName(reverse.label);
const std::string input = GetOperandNameById(reverse.input_operand_id);
const std::string output = GetOperandNameById(reverse.output_operand_id);
CHECK(context_properties_.data_type_limits.reverse_input.Supports(
GetOperand(reverse.input_operand_id).descriptor));
+ // Workaround: explicitly empty axes for a reverse operation should result in
+ // a no-op per spec. But we map this to an Identity node to prevent ORT
+ // EPs from mishandling empty arrays.
+ if (reverse.axes.empty()) {
+ const std::string node_name = GenerateNodeName(base::JoinString(
+ {kInserted, kOpTypeIdentity, kToEmulate, reverse.label}, kUnderscore));
+ std::array<const char*, 1> inputs = {input.c_str()};
+ std::array<const char*, 1> outputs = {output.c_str()};
+ model_editor_.AddNode(kOpTypeIdentity, node_name, inputs, outputs);
+ return;
+ }
+
+ const std::string node_name = GenerateNodeName(reverse.label);
+
// Axes can be empty, which means no dimensions are reversed.
base::FixedArray<int64_t> axes(reverse.axes.begin(), reverse.axes.end());
size_t axes_size = axes.size();
Original Bug Report
[ORT] GPU process AV during reverse with empty axes
Latest stable release of WinML (OpenVINO EP 1.8 / OV 2025.4) contains a deterministic GPU process crash when executing a WebNN reverse operation with an empty axes array (options.axes = []).
Reproduction WPT:
https://wpt.live/webnn/conformance_tests/reverse.https.any.html?device=gpu&tc=reverse%20float32%204D%20input%20options.axes=%5b%5d
The crash results into a STATUS_ACCESS_VIOLATION due to an unchecked iterator dereference. OpenVINO attempts to find the max. axis index to calculate the required buffer length: length = *std::max_element(axis.begin(), axis.end()). When axis is empty, std::max_element returns the end() iterator. The subsequent dereference (*) attempts to read invalid memory, triggering an immediate hardware-level fault.
The issue is verified as fixed in OpenVINO nightly (due for OV 2026.1), but the fix has not been backported to the stable OV EP release or delivered to Chromium.