CVE-2026-43794
Overview
Impact:
Description: Processing maliciously crafted web content may lead to memory corruption
Researchers: Dung Do (@_piers2) of Calif.io
Key insight. A no-copy WebGPU upload aliased caller storage the GPU read asynchronously without keeping it alive, and an array-length binding index was bumped by an unchecked uint32 add that could wrap onto a user binding — retaining the storage until GPU completion and using checkedSum close both.
Patch Details
diff --git a/Source/WebGPU/WebGPU/Queue.mm b/Source/WebGPU/WebGPU/Queue.mm
index 0bf08a83aa85..90851766fcfd 100644
--- a/Source/WebGPU/WebGPU/Queue.mm
+++ b/Source/WebGPU/WebGPU/Queue.mm
@@ -48,7 +48,7 @@
namespace WebGPU {
-constexpr static auto largeBufferSize = 32 * 1024 * 1024;
+constexpr static auto largeBufferSize = WGPU_LARGE_BUFFER_SIZE;
constexpr bool skipMemoryAttribution = true;
WTF_MAKE_TZONE_ALLOCATED_IMPL(Queue);
@@ -1225,8 +1225,16 @@ void Queue::writeTexture(const WGPUImageCopyTexture& destination, std::span<uint
return;
}
- if (noCopy)
+ if (noCopy) {
+ if (!newData.isEmpty()) {
+ // The MTLBuffer above was created with newBufferWithBytesNoCopy and aliases newData's storage; keep that storage alive until the GPU has consumed it.
+ __block Vector<uint8_t> retainedNewData = WTF::move(newData);
+ [m_commandBuffer addCompletedHandler:^(id<MTLCommandBuffer>) {
+ retainedNewData = { };
+ }];
+ }
finalizeBlitCommandEncoder();
+ }
}
void Queue::setLabel(String&& label)
diff --git a/Source/WebGPU/WebGPU/Queue.swift b/Source/WebGPU/WebGPU/Queue.swift
index ef2827dc6aff..d9838d068a28 100644
--- a/Source/WebGPU/WebGPU/Queue.swift
+++ b/Source/WebGPU/WebGPU/Queue.swift
@@ -25,8 +25,9 @@
public import Metal
internal import WebGPU_Internal.Queue
+import WebGPU_Private.WebGPUExt
-private let largeBufferSize = 32 * 1024 * 1024
+private let largeBufferSize = Int(WGPU_LARGE_BUFFER_SIZE)
// FIXME: Eventually all these "thunks" should be removed.
// swift-format-ignore: AlwaysUseLowerCamelCase
diff --git a/Source/WebGPU/WebGPU/RenderPipeline.mm b/Source/WebGPU/WebGPU/RenderPipeline.mm
index 8b446ce17590..9d40a2908f85 100644
--- a/Source/WebGPU/WebGPU/RenderPipeline.mm
+++ b/Source/WebGPU/WebGPU/RenderPipeline.mm
@@ -982,6 +982,13 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
for (auto& bindGroupLayout : pipelineLayout.bindGroupLayouts) {
auto& entries = pipelineEntries[bindGroupLayout.group];
HashMap<String, uint64_t> entryMap;
+ // Wrapping the bump would let the array-length entry alias a user binding and defeat the bounds check.
+ auto bumpForArrayLength = [&](uint32_t webBinding) -> std::optional<uint32_t> {
+ auto checked = checkedSum<uint32_t>(webBinding, limits().maxBindingsPerBindGroup);
+ if (checked.hasOverflowed())
+ return std::nullopt;
+ return checked.value();
+ };
for (auto& entry : bindGroupLayout.entries) {
auto visibility = convertVisibility(entry.visibility);
auto stage = visibility / 2;
@@ -991,7 +998,10 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
uint32_t webBinding = entry.webBinding;
if (auto& entryName = entry.name; entryName.length()) {
if (entryName.endsWith("_ArrayLength"_s)) {
- webBinding += limits().maxBindingsPerBindGroup;
+ auto bumped = bumpForArrayLength(webBinding);
+ if (!bumped)
+ return @"Binding index overflow in auto-generated layouts";
+ webBinding = *bumped;
isArrayLength = true;
}
}
@@ -1010,7 +1020,10 @@ NSString* Device::addPipelineLayouts(Vector<Vector<WGPUBindGroupLayoutEntry>>& p
WGPUBufferBindingType bufferTypeOverride = WGPUBufferBindingType_Undefined;
if (auto& entryName = entry.name; entryName.length()) {
if (isArrayLength) {
- webBinding += limits().maxBindingsPerBindGroup;
+ auto bumped = bumpForArrayLength(webBinding);
+ if (!bumped)
+ return @"Binding index overflow in auto-generated layouts";
+ webBinding = *bumped;
bufferTypeOverride = static_cast<WGPUBufferBindingType>(WGPUBufferBindingType_ArrayLength);
auto shortName = entryName.substring(2, entryName.length() - (sizeof("_ArrayLength") + 1));
if (auto it = entryMap.find(shortName); it != entryMap.end())
diff --git a/Source/WebGPU/WebGPU/WebGPUExt.h b/Source/WebGPU/WebGPU/WebGPUExt.h
index 5fec3ec7cceb..fcdd849578ed 100644
--- a/Source/WebGPU/WebGPU/WebGPUExt.h
+++ b/Source/WebGPU/WebGPU/WebGPUExt.h
@@ -41,6 +41,13 @@
#define WGPU_FUZZER_ASSERT_NOT_REACHED(...) WTFLogAlways(__VA_ARGS__)
#endif
+// Threshold above which the Metal backend uses newBufferWithBytesNoCopy in writeBuffer / writeTexture
+// and aliases the caller's storage rather than copying. Callers passing transfers >= this size MUST
+// keep the source bytes alive until the GPU has consumed them (e.g. via addCompletedHandler).
+// Value is 32 * 1024 * 1024; written as a single integer literal so Swift's clang macro importer
+// picks it up as `WGPU_LARGE_BUFFER_SIZE` rather than skipping it.
+#define WGPU_LARGE_BUFFER_SIZE 33554432
+
#include <optional>
#include <simd/simd.h>
#include <wtf/MachSendRight.h>
diff --git a/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp b/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
index 89b29f347c47..5d54a354824c 100644
--- a/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
+++ b/Source/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp
@@ -35,12 +35,24 @@
#include <WebCore/SharedMemory.h>
#include <WebCore/WebGPUBuffer.h>
#include <WebCore/WebGPUQueue.h>
+#include <WebGPU/WebGPU.h>
+#include <WebGPU/WebGPUExt.h>
#include <wtf/TZoneMallocInlines.h>
namespace WebKit {
WTF_MAKE_TZONE_ALLOCATED_IMPL(RemoteQueue);
+// For transfers at or above WGPU_LARGE_BUFFER_SIZE the backend uses newBufferWithBytesNoCopy and aliases `data`'s mapping; keep it alive until the GPU has consumed the bytes. Smaller transfers are copied into a Metal buffer synchronously, so `data` can be released as soon as we return.
+static void keepAliveUntilSubmittedWorkDone(WebCore::WebGPU::Queue& backing, RefPtr<WebCore::SharedMemory>&& data)
+{
+ if (!data || data->size() < WGPU_LARGE_BUFFER_SIZE)
+ return;
+ backing.onSubmittedWorkDone([data = WTF::move(data)]() mutable {
+ data = nullptr;
+ });
+}
+
RemoteQueue::RemoteQueue(WebCore::WebGPU::Queue& queue, WebGPU::ObjectHeap& objectHeap, Ref<IPC::StreamServerConnection>&& streamConnection, RemoteGPU& gpu, WebGPUIdentifier identifier)
: m_backing(queue)
, m_objectHeap(objectHeap)
@@ -98,7 +110,9 @@ void RemoteQueue::writeBuffer(
return;
}
- protectedBacking()->writeBufferNoCopy(*convertedBuffer, bufferOffset, data->mutableSpan(), 0, std::nullopt);
+ Ref backing = m_backing;
+ backing->writeBufferNoCopy(*convertedBuffer, bufferOffset, data->mutableSpan(), 0, std::nullopt);
+ keepAliveUntilSubmittedWorkDone(backing, WTF::move(data));
completionHandler(true);
}
@@ -128,15 +142,17 @@ void RemoteQueue::writeTexture(
auto convertedDestination = objectHeap->convertFromBacking(destination);
ASSERT(convertedDestination);
auto convertedDataLayout = objectHeap->convertFromBacking(dataLayout);
- ASSERT(convertedDestination);
+ ASSERT(convertedDataLayout);
auto convertedSize = objectHeap->convertFromBacking(size);
ASSERT(convertedSize);
- if (!convertedDestination || !convertedDestination || !convertedSize || !data || data->size() <= WebGPU::maxCrossProcessResourceCopySize) {
+ if (!convertedDestination || !convertedDataLayout || !convertedSize || !data || data->size() <= WebGPU::maxCrossProcessResourceCopySize) {
completionHandler(false);
return;
}
- protectedBacking()->writeTexture(*convertedDestination, data ? data->mutableSpan() : std::span<uint8_t> { }, *convertedDataLayout, *convertedSize);
+ Ref backing = m_backing;
+ backing->writeTexture(*convertedDestination, data->mutableSpan(), *convertedDataLayout, *convertedSize);
+ keepAliveUntilSubmittedWorkDone(backing, WTF::move(data));
completionHandler(true);
}
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43794 |
| Bugzilla | 317317 |
| Component | WebKit |
| Bug Class | UAF |
| Severity | medium |
| Commit | 4a92fe2ccba1be5f… |
| Advisory | Apple Advisory |
Terminology
- newBufferWithBytesNoCopy — A Metal API that wraps existing memory as an MTLBuffer without copying; the backing storage must outlive the GPU’s use of the buffer.
- _ArrayLength bindings — Auto-generated bindings placed above user bindings (offset by maxBindingsPerBindGroup) to hold array lengths; their index must not collide with user bindings.
- checkedSum / overflow — Overflow-checked addition; without it the binding-index bump can wrap and alias a user binding.
Root Cause Analysis
This fixes two memory-safety bugs in the WebGPU (Metal) backend: a use-after-free of buffer storage in Queue::writeTexture’s no-copy path, and an integer-overflow in auto-generated pipeline-layout binding indices. (1) writeTexture no-copy: for a large upload it creates an MTLBuffer with newBufferWithBytesNoCopy that ALIASES the caller’s newData storage rather than copying it; the GPU reads that storage asynchronously. Pre-patch, newData was allowed to be destroyed when writeTexture returned, so the aliased storage could be freed while the GPU was still consuming it — a use-after-free of GPU-visible memory. The fix moves newData into a __block Vector<uint8_t> and releases it only in the command buffer’s addCompletedHandler, keeping the storage alive until the GPU has finished. (2) addPipelineLayouts array-length bindings: auto-generated ‘_ArrayLength’ bindings are placed at webBinding + limits().maxBindingsPerBindGroup to sit in a reserved range above user bindings; pre-patch this uint32 addition was unchecked, so a large webBinding could overflow and wrap the array-length binding index back down to ALIAS a real user binding, defeating the bounds check that separates array-length metadata from user bindings. The fix computes the bumped index with checkedSum<uint32_t> and rejects the layout (‘Binding index overflow in auto-generated layouts’) on overflow. The restored invariants are that no-copy GPU buffers keep their backing storage alive until the GPU is done, and that array-length binding indices cannot wrap to collide with user bindings.
Attack Path
1. Trigger a no-copy texture upload
Use WebGPU writeTexture with data large enough to take the newBufferWithBytesNoCopy path that aliases the caller’s storage.
2. Free the aliased storage
Let the source Vector be destroyed while the GPU still reads the aliased buffer — a use-after-free of GPU-visible memory.
3. (Or) overflow a binding index
Alternatively, craft a pipeline layout whose ‘_ArrayLength’ binding index plus maxBindingsPerBindGroup overflows uint32 and wraps onto a user binding.
4. Corrupt GPU-process memory
Either the freed-buffer read or the aliased binding defeats a safety invariant, corrupting memory in the WebGPU/GPU path.
Impact Assessment
Two memory-corruption bugs in the WebGPU (GPU-process) path reachable from WebGPU content: a use-after-free of GPU-visible buffer storage, and an integer-overflow that aliases an array-length binding onto a user binding. The advisory rates it memory corruption; both are controllable primitives in the graphics path.
Audit Directions
noCopy buffer lifetimes
Audit WebGPU Queue/Buffer for newBufferWithBytesNoCopy (and other no-copy/aliasing) uses that don’t retain the backing storage until a command-buffer completion handler.
Binding-index arithmetic
Grep the WebGPU layout code for binding/index computations (offsets by maxBindingsPerBindGroup, strides) using unchecked integer arithmetic that could wrap and alias.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
Queue::writeTexture (noCopy path) |
Source/WebGPU/WebGPU/Queue.mm |
modified | Moves newData into a __block Vector retained until the command buffer’s completion handler, keeping the newBufferWithBytesNoCopy-aliased storage alive until the GPU consumes it (fixing a UAF). |
Device::addPipelineLayouts (_ArrayLength binding) |
Source/WebGPU/WebGPU/RenderPipeline.mm |
modified | Computes webBinding + maxBindingsPerBindGroup with checkedSum<uint32_t> and rejects on overflow, so an array-length binding index cannot wrap to alias a user binding. |
Files Changed
Source Files
Source/WebGPU/WebGPU/Queue.mmSource/WebGPU/WebGPU/Queue.swiftSource/WebGPU/WebGPU/RenderPipeline.mmSource/WebGPU/WebGPU/WebGPUExt.hSource/WebKit/GPUProcess/graphics/WebGPU/RemoteQueue.cpp