37219708642eeca0f8c182cf833aaca8943bace1 GPU OOB read: RenderBundle-blind ExternalTexture guard permits argument-buffer rewrite to non-resident textures
Triage note: Under RC_REMOVE_SEC_ACCEL_3 adds m_renderBundleConsumerCount (inc on RenderBundle ctor, dec on dtor) and makes BindGroup::updateExternalTextures return false while any live RenderBundle holds the bind group, plus CommandEncoder::addResidentResource keeping resources resident during encoding. Comments state the bundle's ICB references these argument buffers and its residency snapshot is frozen; the fix prevents external-texture rebinding from leaving a GPU-process dangling/stale resource reference. RC_REMOVE_SEC_ACCEL_3 bracketing is a strong silent-security-fix signal and the surface (WebGPU) is renderer-reachable.
The bug at a glance
In the WebGPU GPU-process backend, BindGroup::updateExternalTextures() could rebind/mutate a bind group’s Metal argument buffers while a live RenderBundle’s indirect command buffer (ICB) still referenced those buffers under a frozen residency snapshot. Replaying such a RenderBundle would then dereference stale/freed GPU resources. The patch adds a per-BindGroup live-RenderBundle consumer count that blocks external-texture rebinding while any bundle holds the group, plus CommandEncoder::addResidentResource to keep the referenced MTLResources resident during encoding. This is a GPU-process use-after-free / stale-resource defect reachable from renderer WebGPU calls; high/UAF is supported, though full RCE escalation on the GPU timeline is (inferred).
A residency-lifetime bug across the RenderBundle boundary: a RenderBundle freezes which resources its ICB will use, but external-texture rebinding on a shared BindGroup could swap the argument buffer out from under that frozen snapshot.
Root cause
The invariant is that the Metal argument buffers and resources captured in a RenderBundle’s frozen residency snapshot (its ICB references them) must remain valid and resident until the bundle is destroyed. Pre-patch (RC_REMOVE_SEC_ACCEL_3) BindGroup::updateExternalTextures only guarded on !m_bindGroupLayout || externalTexture.openCommandEncoderCount(), with no notion of RenderBundles holding the group. The dangerous sequence: 1. a RenderBundle is created referencing one or more BindGroups whose argument buffers its ICB bakes in; 2. the bundle’s residency snapshot is frozen at construction; 3. script calls updateExternalTextures() on a shared BindGroup (e.g. via importExternalTexture rebinding) which mutates/replaces the argument-buffer contents the ICB still points at; 4. the bundle is later executed, and the GPU dereferences a stale or freed resource through the frozen snapshot.
The patch adds a mutable size_t m_renderBundleConsumerCount to BindGroup with addRenderBundleConsumer()/removeRenderBundleConsumer(). RenderBundle’s initWithUsage increments the count for every bind group in m_bindGroups (comment: ‘This bundle’s indirect command buffer references these bind groups’ argument buffers, and its residency snapshot is frozen here’), and the newly non-default RenderBundle::~RenderBundle() decrements it. BindGroup::updateExternalTextures now returns false (refusing the rebind) when m_renderBundleConsumerCount is nonzero, so external-texture rebinding cannot mutate an argument buffer any live bundle still references. Separately, CommandEncoder::addResidentResource(id) routes a resource to addTexture/addBuffer (only if retention containers exist), and ComputePassEncoder/RenderPassEncoder call m_parentEncoder->addResidentResource(resource.mtlResources[i]) for each used resource so the encoder keeps them resident through encoding.
The triage bugclass (UAF) is consistent with the diff: the added machinery is pure lifetime/residency accounting. What the diff strictly proves is a stale-reference / residency-lifetime defect on GPU-process Metal resources; whether the stale buffer is genuinely freed (true UAF) versus reused/overwritten depends on Metal allocator behavior not shown here, so the UAF label is well-motivated but the precise freed-vs-reused distinction is (inferred).
Key code
Block external-texture rebinding while any live RenderBundle references the bind group
// BindGroup.mm updateExternalTextures (non-RC path)
if (m_renderBundleConsumerCount)
return false;
// RenderBundle.mm
for (RefPtr bindGroup : m_bindGroups)
bindGroup->addRenderBundleConsumer(); // ctor: snapshot frozen here
...
RenderBundle::~RenderBundle()
{
for (RefPtr bindGroup : m_bindGroups)
bindGroup->removeRenderBundleConsumer();
}
Fix approach
The fix gives each BindGroup a live-RenderBundle consumer count: constructing a RenderBundle increments it for every referenced bind group (when its ICB and residency snapshot are frozen), destroying the bundle decrements it. updateExternalTextures() refuses (returns false) while the count is nonzero, so the argument buffers an ICB references can no longer be mutated while a bundle still exists. In parallel, CommandEncoder::addResidentResource is called for every resource used by compute and render pass encoders, ensuring those MTLResources are retained/kept resident for the encoder’s lifetime rather than relying on transient references. Together they close the window where a frozen bundle snapshot could reference a resource that was rebound or allowed to go non-resident. The consumer count is the new guard-of-record; the external-texture rebind hot path now short-circuits to false when bundles are outstanding.
Patch walkthrough
Source/WebGPU/WebGPU/BindGroup.h— Adds addRenderBundleConsumer()/removeRenderBundleConsumer() and the mutable m_renderBundleConsumerCount member used to track live RenderBundles referencing this bind group.Source/WebGPU/WebGPU/BindGroup.mm— updateExternalTextures returns false early when m_renderBundleConsumerCount is nonzero, refusing to rebind external textures while a bundle holds the group.Source/WebGPU/WebGPU/CommandEncoder.h— Declares addResidentResource(id).Source/WebGPU/WebGPU/CommandEncoder.mm— Implements addResidentResource, dispatching to addTexture/addBuffer based on the MTLResource protocol, but only when retention containers exist.Source/WebGPU/WebGPU/ComputePassEncoder.mm— Calls m_parentEncoder->addResidentResource(resource.mtlResources[i]) for each used resource so compute-pass resources stay resident.Source/WebGPU/WebGPU/RenderBundle.mm— ctor increments every bind group’s consumer count as the residency snapshot freezes; the no-longer-defaulted dtor decrements them.Source/WebGPU/WebGPU/RenderPassEncoder.mm— Adds addResidentResource calls at the three resource-binding sites so render-pass resources are kept resident during encoding.
Background
WebGPU BindGroup / argument buffer — A BindGroup binds resources (textures, buffers, samplers) for a shader; on Metal it is backed by MTLBuffer argument buffers that shaders index, so mutating the argument buffer changes what shaders dereference.
RenderBundle / indirect command buffer (ICB) — A RenderBundle is a pre-recorded, replayable set of render commands; on Metal it is an MTLIndirectCommandBuffer that bakes in references to the bind groups’ argument buffers at record time.
Residency / residency snapshot — Metal requires resources used by the GPU to be made resident; a RenderBundle freezes the set of resources (its residency snapshot) it will use, so anything it references must stay valid and resident until replay.
External texture — A WebGPU external texture (e.g. from video) can be rebound via updateExternalTextures(), which rewrites the bind group’s argument buffer entries for those texture slots.
GPU-process attack surface — WebGPU commands originate in the renderer and execute in the GPU process; a lifetime bug there corrupts GPU-process state, a distinct and valuable sandbox boundary.
Vulnerability window
- Setup — Renderer creates BindGroups (including external textures) and records a RenderBundle whose ICB references their argument buffers.
- Freeze — RenderBundle construction freezes the residency snapshot of the referenced resources.
- Trigger — Script calls updateExternalTextures() on a shared BindGroup, mutating/replacing argument-buffer contents the live bundle still references.
- Use-after-free — The bundle is executed and the GPU dereferences a stale/freed resource through the frozen snapshot.
- Post-patch — updateExternalTextures refuses while the consumer count is nonzero, and encoders mark resources resident, so the frozen snapshot stays valid.
Proof of concept
No regression test ships in the diff. Conceptual trigger (inferred, WebGPU/JS path): create a GPUBindGroup containing an external texture, record it into a GPURenderBundle (freezing its residency snapshot), then call the external-texture rebind path (importExternalTexture / updateExternalTextures) while the bundle is still alive, and execute the bundle via executeBundles. Pre-patch the rebind mutates the argument buffer the ICB references. Exact JS-to-updateExternalTextures mapping is (inferred).
Exploitation
- Reach — Renderer WebGPU: create bind groups with external textures, record a render bundle, and invoke external-texture rebinding.
- Trigger — Call updateExternalTextures on a bind group a live RenderBundle still references, then replay the bundle.
- Primitive — GPU-process dereference of a stale/freed MTLResource via the frozen ICB residency snapshot.
- Escalation (inferred) — Open questions: whether the stale argument-buffer slot can be made to point at attacker-reclaimed memory and whether that steers a GPU read/write to a useful address; not demonstrated by the diff, so impact above GPU-process corruption is (inferred).
Detection & hunting
For defenders and detection engineers:
- updateExternalTextures called while RenderBundles reference the bind group — Log/alert when m_renderBundleConsumerCount would have been nonzero at a rebind attempt.
- GPU-process faults during executeBundles — Metal validation / GPU fault reports referencing argument buffers of destroyed or rebound resources.
- Non-resident resource access in encoders — Enable Metal resident-set validation to catch resources used without being made resident, the gap addResidentResource closes.
Remaining risk in the patch
- addResidentResource early-return on missing containers — addResidentResource returns immediately if
!m_retainedTextures && !m_retainedBuffers; if an encoding path reaches resource binding before those containers are allocated, residency would silently not be enforced. - Count is per-BindGroup, not per-argument-buffer-slot — updateExternalTextures is refused wholesale while any bundle references the group; the guard is coarse and relies on balanced add/remove across every RenderBundle destruction path to avoid a stuck-nonzero or prematurely-zero count.
Audit directions
- Other BindGroup mutators besides updateExternalTextures — Check any API that rewrites argument-buffer contents for the same missing live-consumer guard.
- Residency-snapshot freezing in other replayable objects — Audit whether other cached/replayable GPU objects freeze resource references without reference-counting their consumers.
- addResidentResource coverage — Confirm every encoder resource-binding loop now marks resources resident; addResidentResource no-ops if retention containers are absent, so verify those are always set.
- RenderBundle m_bindGroups lifetime — Verify RefPtr ownership of bind groups in RenderBundle matches the consumer-count increments/decrements under all destruction paths.