9b683ce132608085c67429f511ad07e48f2ebc9d WebNN: Keep ORT mapped D3D12 buffer alive
The bug at a glance
Dangling-pointer / use-after-free in the GPU process’s WebNN ORT backend. On the mapped-fallback interop path, TensorImplOrt held a raw Map()’d pointer whose validity was tied to the SharedImage representation; a disconnect could free the representation while a scheduled read/write still used the pointer, dereferencing freed GPU buffer memory. GPU-process memory corruption reachable via WebNN tensor operations is high severity, but it is gated to Windows D3D12 + the mapped-fallback code path (external importer unavailable or import failed) and requires a disconnect race, so it is high rather than critical.
In ContextImplOrt tensor creation, when the ORT external-resource importer is unavailable or import fails, the code falls back to Map()‘ing the D3D12 buffer and handing ORT a raw pointer. ORT holds no reference of its own to the buffer; the ComPtr lived only as a local, and the buffer’s lifetime was effectively bound to the SharedImage representation, which a disconnect can destroy while the tensor’s OrtValue still references the mapped memory.
Root cause
The raw mapped pointer handed to ORT points into the D3D12 resource’s mapping. Previously only the representation kept that resource alive; on disconnect the representation (and its buffer) could be freed while a scheduled read/write operation on the tensor still dereferenced the stale pointer. The fix threads the ComPtr into TensorImplOrt as member mapped_d3d12_buffer_, declared before tensor_ so it is destroyed after tensor_, giving the buffer a lifetime matching the tensor rather than the representation.
Key code
The mapped D3D12 buffer ComPtr is now owned by TensorImplOrt so its lifetime tracks the tensor, not the representation.
// fall back to using mapped D3D12 buffer. ORT holds no reference of its own,
// so d3d12_buffer is kept and passed to TensorImplOrt to keep the buffer
// alive independent of `representation`.
ComPtr<ID3D12Resource> d3d12_buffer;
if (tensor.get() == nullptr) {
d3d12_buffer = representation->GetD3D12Buffer();
CHECK(d3d12_buffer);
void* mapped_ptr = nullptr;
HRESULT hr = d3d12_buffer->Map(0, nullptr, &mapped_ptr);
...
}
return base::MakeRefCounted<TensorImplOrt>(..., std::move(d3d12_buffer), std::move(tensor));
Patch walkthrough
services/webnn/ort/context_impl_ort.cc— Removes the unconditional top-of-function GetD3D12Buffer()/CHECK. Declares a ComPtr d3d12_buffer that is populated only inside the mapped-fallback branch (tensor == nullptr), where the buffer is Map()’d. Passes std::move(d3d12_buffer) into the TensorImplOrt constructor so the buffer is retained; on the importer path it stays null (ORT owns its own reference there).services/webnn/ort/tensor_impl_ort.cc— Adds the mapped_d3d12_buffer parameter to the constructor and initializes the new member mapped_d3d12_buffer_ from it (in the correct member-init order relative to tensor_).services/webnn/ort/tensor_impl_ort.h— Adds the constructor parameter and the const ComPtr mapped_d3d12_buffer_ member, documented as existing only on the mapped-fallback path and deliberately declared before tensor_ so it is destroyed after tensor_, since tensor_ holds a raw pointer into this buffer’s mapping.
Background
WebNN ORT backend — The ONNX Runtime-based WebNN execution path in the GPU process (services/webnn/ort) that creates tensors backed by GPU buffers.
Mapped-fallback interop — When ORT’s external-resource importer is unavailable or fails, Chromium Map()s the D3D12 buffer and hands ORT a raw CPU-accessible pointer into it; ORT keeps no reference to the resource.
SharedImage representation — The object exposing GetD3D12Buffer(); it owned the only reference keeping the D3D12 resource alive, and can be freed on disconnect.
ComPtr lifetime / member order — Microsoft::WRL::ComPtr is a ref-counted COM smart pointer; declaring mapped_d3d12_buffer_ before tensor_ ensures destruction order keeps the mapping valid for tensor_’s whole lifetime.
Vulnerability window
- pre-fix — Raw mapped pointer’s buffer lifetime bound to representation; disconnect could free it while a scheduled read/write still used it, leaving the pointer dangling. (from commit message)
- fix — ComPtr moved into TensorImplOrt as a member on the mapped-fallback path so the buffer outlives the representation and matches the tensor.
- post-fix — Importer path unchanged (null buffer, ORT owns its reference); fallback path keeps the buffer alive for the tensor’s lifetime.
Proof of concept
No PoC is derivable from the diff and none should be fabricated. Conceptually, triggering it would require a Windows/D3D12 configuration where the ORT external importer is unavailable or import fails (forcing the mapped-fallback path), creating a WebNN tensor over a WebGPU-interop SharedImage, scheduling a read/write, and inducing a disconnect that frees the representation before the operation completes. These steps and the required environment are not shown in the patch.
Exploitation
- setup — WebNN context on Windows takes the mapped-fallback interop path (external importer unavailable or import fails). (inferred)
- schedule — A read/write operation is scheduled against the tensor, which dereferences the raw mapped pointer. (inferred)
- free — A disconnect frees the SharedImage representation and its D3D12 buffer before/while the scheduled operation runs. (from commit message)
- use — The scheduled operation reads/writes through the now-dangling mapped pointer into freed GPU buffer memory, the UAF primitive. (from commit message)
Detection & hunting
For defenders and detection engineers:
- GPU-process crash in WebNN ORT —
- Lifetime instrumentation —
Audit directions
- WebNN interop lifetimes —
- Mapped-buffer fallbacks —