CVE-2026-17858
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/webnn/ort/context_impl_ort.cc |
modified | |
ep_name_services/webnn/ort/device_allocator.cc |
modified | |
can_access_on_cpu_services/webnn/ort/tensor_impl_ort.cc |
modified | |
size_services/webnn/ort/tensor_impl_ort.cc |
modified | |
ifservices/webnn/ort/tensor_impl_ort.cc |
modified |
Files Changed
services/webnn/ort/context_impl_ort.ccservices/webnn/ort/device_allocator.ccservices/webnn/ort/device_allocator.hservices/webnn/ort/tensor_impl_ort.cc
Patch
From 6c250b17eb7c45c79f3f95d958b40e27ad3e32b3 Mon Sep 17 00:00:00 2001 From: Wei Wang <[email protected]> Date: Wed, 10 Jun 2026 13:06:59 -0700 Subject: [PATCH] [WebNN] Remove WebGPU EP device tensor support for ORT WebGPU EP device tensors can not be initialized, because GetTensorMutableData() returns an opaque device handle rather than a CPU-dereferenceable pointer. This means uninitialized memory contents could be leaked, posing an information disclosure risk. This CL removes support for WebGPU EP device tensor until they can be properly initialized. Bug: 520191468 Change-Id: I43c910f9129abbc19a8ba3120d183224511b12cd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7917174 Reviewed-by: Reilly Grant <[email protected]> Commit-Queue: Hu, Ningxin <[email protected]> Reviewed-by: Hu, Ningxin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1644836} --- diff --git a/services/webnn/ort/context_impl_ort.cc b/services/webnn/ort/context_impl_ort.cc index ada5d73..877ad879 100644 --- a/services/webnn/ort/context_impl_ort.cc +++ b/services/webnn/ort/context_impl_ort.cc @@ -507,12 +507,10 @@ const OrtApi* ort_api = PlatformFunctions::GetInstance()->ort_api(); OrtAllocator* allocator = nullptr; - bool can_access_on_cpu = true; - // Use the device allocator if it's present and should be used. Otherwise, use - // the default allocator which is CPU based and non-arena. - if (device_allocator_ && device_allocator_->ShouldUse(tensor_info)) { + // Use the device allocator if it's present. Otherwise, use the default + // allocator which is CPU based and non-arena. + if (device_allocator_) { allocator = device_allocator_->get(); - can_access_on_cpu = device_allocator_->CanAccessOnCPU(); } else { // `GetAllocatorWithDefaultOptions()` always returns the same pointer to the // same default allocator and its returned value should NOT be freed. @@ -543,7 +541,7 @@ return base::MakeRefCounted<TensorImplOrt>( std::move(receiver), *this, std::move(tensor_info), size, - std::move(tensor), can_access_on_cpu, device_allocator_); + std::move(tensor), device_allocator_); } base::expected<scoped_refptr<WebNNTensorImpl>, mojom::ErrorPtr> diff --git a/services/webnn/ort/device_allocator.cc b/services/webnn/ort/device_allocator.cc index 9820aa5d..34a21ab 100644 --- a/services/webnn/ort/device_allocator.cc +++ b/services/webnn/ort/device_allocator.cc @@ -10,7 +10,6 @@ #include "services/webnn/ort/ort_status.h" #include "services/webnn/ort/platform_functions_ort.h" #include "services/webnn/public/cpp/execution_providers_info.h" -#include "services/webnn/public/mojom/webnn_tensor.mojom.h" #include "third_party/windows_app_sdk_headers/src/inc/abi/winml/winml/onnxruntime_c_api.h" namespace webnn::ort { @@ -23,8 +22,7 @@ constexpr size_t kIntelNpuStandardPageSize = 4096; // Creates memory info for a specific EP. Currently, the device allocator only -// supports OpenVINO and WebGPU EPs. Returns an invalid memory info if not -// supported. +// supports OpenVINO. Returns an invalid memory info if not supported. ScopedOrtMemoryInfo CreateMemoryInfo(const OrtApi* ort_api, base::cstring_view ep_name) { ScopedOrtMemoryInfo memory_info; @@ -37,11 +35,6 @@ /*alignment*/ kIntelNpuStandardPageSize, OrtDeviceAllocator, ScopedOrtMemoryInfo::Receiver(memory_info).get())); CHECK(memory_info.get()); - } else if (ep_name == kWebGpuExecutionProvider) { - CHECK_STATUS(ort_api->CreateMemoryInfo( - "WebGPU_Buffer", OrtDeviceAllocator, /*id*/ 0, OrtMemTypeDefault, - ScopedOrtMemoryInfo::Receiver(memory_info).get())); - CHECK(memory_info.get()); } return memory_info; @@ -93,36 +86,19 @@ } CHECK(device_allocator.get()); - // SAFETY: ORT guarantees that `ep_name` is valid and null-terminated. return base::MakeRefCounted<DeviceAllocator>( base::PassKey<DeviceAllocator>(), std::move(env), - std::move(trivial_session), std::move(device_allocator), - UNSAFE_BUFFERS(base::cstring_view(ep_name))); + std::move(trivial_session), std::move(device_allocator)); } DeviceAllocator::DeviceAllocator(base::PassKey<DeviceAllocator>, scoped_refptr<Environment> env, ScopedOrtSession trivial_session, - ScopedOrtAllocator device_allocator, - base::cstring_view ep_name) + ScopedOrtAllocator device_allocator) : env_(std::move(env)), trivial_session_(std::move(trivial_session)), - device_allocator_(std::move(device_allocator)), - ep_name_(ep_name) {} + device_allocator_(std::move(device_allocator)) {} DeviceAllocator::~DeviceAllocator() = default; -bool DeviceAllocator::ShouldUse(const mojom::TensorInfoPtr& tensor_info) const { - // Since the WebGPU EP does not allow clients to access underlying tensors - // directly, only use it when WebNN developers do not need to access the - // underlying data. - if (ep_name_ == kWebGpuExecutionProvider && - (tensor_info->usage.Has(MLTensorUsageFlags::kRead) || - tensor_info->usage.Has(MLTensorUsageFlags::kWrite))) { - return false; - } - - return true; -} - } // namespace webnn::ort diff --git a/services/webnn/ort/device_allocator.h b/services/webnn/ort/device_allocator.h index bde208c0..b089b227 100644 --- a/services/webnn/ort/device_allocator.h +++ b/services/webnn/ort/device_allocator.h @@ -5,15 +5,10 @@ #ifndef SERVICES_WEBNN_ORT_DEVICE_ALLOCATOR_H_ #define SERVICES_WEBNN_ORT_DEVICE_ALLOCATOR_H_ -#include <string> - #include "base/memory/ref_counted.h" #include "base/memory/scoped_refptr.h" -#include "base/strings/cstring_view.h" #include "services/webnn/ort/ort_session_options.h" #include "services/webnn/ort/scoped_ort_types.h" -#include "services/webnn/public/cpp/execution_providers_info.h" -#include "services/webnn/public/mojom/webnn_tensor.mojom-forward.h" namespace webnn::ort { @@ -35,20 +30,13 @@ DeviceAllocator(base::PassKey<DeviceAllocator>, scoped_refptr<Environment> env, ScopedOrtSession trivial_session, - ScopedOrtAllocator device_allocator, - base::cstring_view ep_name); + ScopedOrtAllocator device_allocator); DeviceAllocator(const DeviceAllocator&) = delete; DeviceAllocator& operator=(const DeviceAllocator&) = delete; OrtAllocator* get() const { return device_allocator_.get(); } - // Whether to use this device allocator depends on the tensor's - // usage in `tensor_info`. - bool ShouldUse(const mojom::TensorInfoPtr& tensor_info) const; - // Whether the underlying tensor data can be accessed on CPU directly. - bool CanAccessOnCPU() const { return ep_name_ != kWebGpuExecutionProvider; } - private: friend class base::RefCounted<DeviceAllocator>; @@ -67,9 +55,6 @@ // does. ScopedOrtSession trivial_session_; ScopedOrtAllocator device_allocator_; - - // The name of the EP associated with this allocator. - std::string ep_name_; }; } // namespace webnn::ort diff --git a/services/webnn/ort/tensor_impl_ort.cc b/services/webnn/ort/tensor_impl_ort.cc index 64e482ec..d3e7327 100644 --- a/services/webnn/ort/tensor_impl_ort.cc +++ b/services/webnn/ort/tensor_impl_ort.cc @@ -21,20 +21,16 @@ mojom::TensorInfoPtr tensor_info, size_t size, ScopedOrtValue tensor, - bool can_access_on_cpu, scoped_refptr<DeviceAllocator> device_allocator) : WebNNTensorImpl(std::move(receiver), context, std::move(tensor_info)), device_allocator_((std::move(device_allocator))), tensor_(std::move(tensor)), - size_(size), - can_access_on_cpu_(can_access_on_cpu) { + size_(size) { // Initialize the tensor with zeros, otherwise, reading uninitialized memory // will get random values. // TODO(crbug.com/461303833): check whether fast HW clears can be used // instead. - if (can_access_on_cpu) { - std::ranges::fill(AsSpan(), 0); - }
Original Bug Report
Potential WebNN ORT WebGPU-EP device tensor zero-fill bypass leading to uninitialized memory leak
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential vulnerability in the WebNN ONNX Runtime (ORT) backend allows WebGPU Execution Provider device tensors to skip zero-initialization. This happens because the zero-filling implementation in TensorImplOrt is restricted to CPU-accessible tensors, with no hardware-clear fallback. An attacker could potentially leverage this to read uninitialized GPU-arena memory via a graph dispatch laundering technique.
Affected files:
services/webnn/ort/tensor_impl_ort.ccservices/webnn/ort/device_allocator.hservices/webnn/ort/context_impl_ort.ccservices/webnn/webnn_context_impl.ccservices/webnn/ort/graph_impl_ort.ccservices/webnn/public/mojom/tensor_usage_mojom_traits.h
Estimated timestamp from git blame: 2025-11-25
Description
A potential uninitialized GPU-process memory leak exists in Chromium’s WebNN ONNX Runtime (ORT) backend implementation. Specifically, in services/webnn/ort/tensor_impl_ort.cc, TensorImplOrt only zero-initializes backing memory when the tensor is directly CPU-accessible (can_access_on_cpu is true):
TensorImplOrt::TensorImplOrt(
mojo::PendingAssociatedReceiver<mojom::WebNNTensor> receiver,
WebNNContextImpl& context,
mojom::TensorInfoPtr tensor_info,
size_t size,
ScopedOrtValue tensor,
bool can_access_on_cpu,
scoped_refptr<DeviceAllocator> device_allocator)
: WebNNTensorImpl(std::move(receiver), context, std::move(tensor_info)),
device_allocator_((std::move(device_allocator))),
tensor_(std::move(tensor)),
size_(size),
can_access_on_cpu_(can_access_on_cpu) {
// Initialize the tensor with zeros, otherwise, reading uninitialized memory
// will get random values.
// TODO(crbug.com/461303833): check whether fast HW clears can be used
// instead.
if (can_access_on_cpu) {
std::ranges::fill(AsSpan(), 0);
}
}
When a tensor is created with default empty usage flags (i.e., readable and writable are omitted or set to false), the WebGPU Execution Provider (EP) allocator is selected as the device allocator (DeviceAllocator::ShouldUse returns true).
Because this allocator operates on GPU device buffers, CanAccessOnCPU() returns false. This causes can_access_on_cpu to be set to false, bypassing the CPU-based std::ranges::fill() zero-initialization logic. Since there is currently no GPU-side hardware-clear fallback implemented (as noted in the code’s TODO), the newly allocated GPU buffer contains recycled, uninitialized memory from the ORT WebGPU buffer arena.
An attacker can potentially bypass read restrictions on this uninitialized device tensor by passing it as an input to a graph dispatch operation that performs a bitwise identity transformation (such as reshape) into a CPU-readable output tensor, allowing the uninitialized GPU-process arena contents to be exfiltrated.
Suggested/Potential Steps to Reproduce
Note: These are suggested and potential steps based on source code analysis; our tooling does not currently have the capability to run code to confirm a live proof of concept.
- Enable the WebNN API (e.g., via
--enable-features=WebMachineLearningNeuralNetworkon Windows supporting the ORT WebGPU EP). - Create a WebNN GPU context:
const ctx = await navigator.ml.createContext({deviceType: 'gpu'}); - Allocate an input tensor without specified usage parameters, defaulting its usage to empty
{}:This tensor gets allocated from the WebGPU EP device allocator arena and skips zero-initialization.const A = await ctx.createTensor({dataType: 'float32', shape: [1048576]}); - Allocate a second, readable CPU-backed tensor to act as the destination:
const B = await ctx.createTensor({dataType: 'float32', shape: [1048576], readable: true}); - Construct a graph with a
reshapeoperator mapping an inputxtoyof the same shape:const builder = new MLGraphBuilder(ctx); const x = builder.input('x', {dataType: 'float32', shape: [1048576]}); const graph = await builder.build({'y': builder.reshape(x, [1048576])}); - Dispatch the graph, using the uninitialized tensor
Aas input and the readable tensorBas output:ctx.dispatch(graph, {'x': A}, {'y': B}); - Read back the contents of
Bto exfiltrate the uninitialized GPU-arena memory:const leaked = await ctx.readTensor(B);
Impact
Because the OrtEnv is a process-wide singleton shared across WebNN contexts, the recycled buffer arena may contain sensitive neural network model weights, intermediate layers, and inputs/outputs from WebNN sessions of other origins running in the same GPU process, leading to a potential cross-origin information leak.
Suggested Fix
Implement a GPU-side (hardware-clear) buffer clear fallback in the TensorImplOrt constructor (or during the WebGPU device tensor creation flow in ContextImplOrt) when can_access_on_cpu is false. The backing WebGPU buffer must be fully zero-initialized on the GPU device before it can be used or referenced by any graph execution operations.
Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.