Closed Bug 2039568 (CVE-2026-12293) Opened 5 months ago Closed 5 months ago

GPURenderBundle externalTexture lifetime tracking bypass causes GPU-process use-after-free

Categories

(Core :: Graphics: WebGPU, defect, P1)

defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox151 --- wontfix
firefox152 + fixed
firefox153 + fixed

People

(Reporter: 5up3rh3i, Assigned: aleiserson)

References

Details

(4 keywords, Whiteboard: [client-bounty-form][adv-main152+])

Attachments

(2 files, 1 external file)

Title

[Graphics/WebGPU] Security vulnerability in Firefox: GPURenderBundle externalTexture lifetime tracking bypass causes GPU-process use-after-free

Product / Component

Firefox / Graphics: WebGPU (dom/webgpu, gfx/wgpu_bindings, third_party/rust/wgpu-core, Metal backend)

Affected Versions

Tested on Firefox Nightly 152.0a1.

  • Source revision: cf6415620d92de30e1e1a413ef98b58684eb160e
  • Build ID: 20260514203623
  • ASAN binary: obj-asan/dist/NightlyDebug.app/Contents/MacOS/firefox

Also tested on:

  • Earlier local ASAN Nightly build BuildID=20260513010200, where the same bug was reproducible.
  • A release build was used only as an additional behavioral check for GPU/device-loss behavior; the primary security evidence is from the ASAN build above.

Environment

  • OS: macOS 26.4.1, Darwin 25.4.0, arm64
  • Hardware/backend: Apple M4, WebGPU Metal backend
  • Build type: local Firefox Nightly ASAN debug/optimized build
  • ASAN/Debug/Release: ASAN + debug, --enable-address-sanitizer, --enable-debug, --enable-optimize=-O1, --disable-jemalloc
  • Relevant prefs used by the testcase:
    • dom.webgpu.enabled=true
    • dom.webgpu.external-texture.enabled=true
    • dom.webgpu.allow-present-without-readback=true
    • gfx.webgpu.ignore-blocklist=true
    • dom.webgpu.wgpu-backend="metal"
    • media.autoplay.default=0

Summary

Firefox WebGPU tracks GPUExternalTexture lifetime for direct render passes, but the same lifetime dependency is lost when the external texture is first captured inside a GPURenderBundle.

A web page can import an external texture from a video source, bind it in a render bundle, finish the bundle, then tear down the video/external texture source before executing the bundle in a later render pass. The direct render-pass path correctly rejects the stale external texture with External texture is expired. The render-bundle path bypasses this validation and reaches the Metal backend with a stale texture object, causing a GPU-process use-after-free in objc_retain while wgpu_hal::metal::command::Encoder::set_texture binds the texture.

This is a remotely reachable WebGPU memory-safety issue from web content when WebGPU/external texture are enabled.

Steps To Reproduce

  1. Build Firefox with ASAN enabled.
  2. Start Firefox with the WebGPU prefs listed above.
  3. Save the attached PoC file as webgpu_renderbundle_externaltexture_lifetime.html and serve it over HTTP from any local directory.
  4. Open the PoC with the query parameter mode=bundle, for example:
http://127.0.0.1:8000/webgpu_renderbundle_externaltexture_lifetime.html?mode=bundle&source=video&loop=1&batch=1&readbackEvery=0&deferSubmitMs=100&teardownVideo=1&close=0&beacon=1
  1. For the direct-path control, open the same PoC with mode=direct:
http://127.0.0.1:8000/webgpu_renderbundle_externaltexture_lifetime.html?mode=direct&source=video&loop=1&batch=1&readbackEvery=0&deferSubmitMs=100&teardownVideo=1&close=0&beacon=1

Expected Result

Firefox should apply the same GPUExternalTexture lifetime validation to render bundles and direct render passes. If an external texture source has expired before submit, both paths should reject the command with a WebGPU validation error and should not pass stale backend texture objects to Metal/wgpu.

Actual Result

The direct render-pass path reports the expected WebGPU validation error:

RESULT=EXPIRED_VALIDATION
JavaScript warning: Uncaptured WebGPU error: External texture is expired

The render-bundle path bypasses the expired-external-texture validation and crashes in the GPU process:

RESULT=ASAN_BUS
SUMMARY: AddressSanitizer: BUS (/usr/lib/libobjc.A.dylib:arm64+0x791c) in objc_retain+0x10

Crash / ASAN / Sanitizer Output

Short top stack from the latest ASAN build:

AddressSanitizer:DEADLYSIGNAL
==51263==ERROR: AddressSanitizer: BUS on unknown address
==51263==The signal is caused by a READ memory access.

#0  objc_retain
#1  wgpu_hal::metal::command::Encoder::set_texture
#2  wgpu_hal::metal::command::<impl CommandEncoder>::update_bind_group_state
#3  wgpu_hal::metal::command::<impl CommandEncoder>::set_bind_group
#4  wgpu_core::command::bundle::RenderBundle::execute
#5  wgpu_core::command::render::encode_render_pass
#6  wgpu_core::command::CommandEncoder::finish
#7  wgpu_core::global::Global::command_encoder_finish
#8  wgpu_bindings::server::process_message
#9  wgpu_server_messages
#10 mozilla::webgpu::WebGPUParent::RecvMessages
#11 mozilla::webgpu::PWebGPUParent::OnMessageReceived
#12 mozilla::gfx::PCanvasManagerParent::OnMessageReceived
#13 mozilla::ipc::MessageChannel::DispatchAsyncMessage

Earlier NSZombie-assisted runs also showed the same object-lifetime failure more directly:

-[AGXG16GFamilyTexture retain]: message sent to deallocated instance

Root Cause Analysis

Affected files/functions:

  • dom/webgpu/RenderBundleEncoder.cpp
    • RenderBundleEncoder::SetBindGroup
    • RenderBundleEncoder::Finish
  • dom/webgpu/RenderBundle.h
    • RenderBundle
  • dom/webgpu/RenderPassEncoder.cpp
    • RenderPassEncoder::ExecuteBundles
    • RenderPassEncoder::End
  • dom/webgpu/CommandEncoder.cpp
    • CommandEncoder::EndRenderPass
    • CommandEncoder::Finish
  • dom/webgpu/Queue.cpp
    • Queue::Submit
  • dom/webgpu/ExternalTexture.cpp
    • ExternalTexture::MaybeDestroy
    • external texture source destruction path
  • gfx/wgpu_bindings/src/server.rs
    • external texture creation / backend resource binding
  • third_party/rust/wgpu-core/src/command/bundle.rs
    • RenderBundle::execute
  • third_party/rust/wgpu-hal/src/metal/command.rs
    • Metal texture binding

Code path:

  1. A page imports a GPUExternalTexture from a video source.
  2. The page creates a bind group containing that external texture.
  3. The page records setBindGroup() into a GPURenderBundleEncoder.
  4. RenderBundleEncoder::Finish() creates a RenderBundle.
  5. The external texture source is torn down after bundle creation but before bundle execution/submission.
  6. The page executes the render bundle inside a later render pass.
  7. The command encoder is finished and sent to the WebGPU parent/GPU process.
  8. wgpu/Metal executes the bundle and attempts to bind the stale texture object.

Why the input reaches the vulnerable state:

Direct render passes retain enough external-texture metadata for CommandEncoder / CommandBuffer / Queue::Submit to detect that the external texture is expired. In that path, Queue::Submit rejects the command and reports External texture is expired.

The render-bundle path drops this dependency:

  • RenderBundleEncoder::SetBindGroup() records the bind group while encoding the bundle.
  • RenderBundleEncoder::Finish() transfers only canvas-context usage into the resulting RenderBundle.
  • RenderBundle stores mUsedCanvasContexts, but does not store the ExternalTexture objects or their source ids.
  • RenderPassEncoder::ExecuteBundles() merges only canvas-context dependencies from each bundle.
  • RenderPassEncoder::End() collects external textures only from direct render-pass bind groups, not from bind groups captured inside executed bundles.
  • CommandEncoder::EndRenderPass() and CommandEncoder::Finish() therefore create a command buffer without the bundle's external-texture lifetime dependency.
  • Queue::Submit() only validates the external textures present in the command buffer metadata, so the bundle's stale external texture is not rejected.

As a result, the JavaScript-visible GPUExternalTexture source can expire while the backend bundle still contains a stale Metal texture binding. The stale object is later used by wgpu_core::command::bundle::RenderBundle::execute, reaching wgpu_hal::metal::command::Encoder::set_texture and crashing in objc_retain.

Security Impact

Potential impact:

  • GPU-process use-after-free / use-after-destroy of a Metal texture object.
  • Memory corruption reachable from web content through WebGPU render bundle commands.
  • The crash occurs in the GPU/WebGPU parent-side execution path, not as a JavaScript exception in content.

Exploitability notes:

  • The attacker controls the render bundle, shader/bind group layout, external texture lifetime timing, submit order, and additional GPU allocation pressure around the stale object.
  • The direct path provides a strong control case: the same stale external texture is normally rejected, so the crash is caused by the render-bundle lifetime tracking gap.
  • The ASAN stack and NSZombie output identify a freed Objective-C Metal texture object being retained during backend command execution.

Attacker requirements:

  • Ability to run web content with WebGPU and external texture support enabled.
  • No browser extension or privileged script is required.
  • The observed testcase uses the Metal backend on macOS.

Attachments

  • webgpu_renderbundle_externaltexture_lifetime.html

Suggested Fix Direction

Render bundles should preserve the same external-texture lifetime dependencies as direct render passes.

Recommended fix direction:

  • Store external texture dependencies, or source ids, in RenderBundle.
  • Transfer external texture dependencies from RenderBundleEncoder::Finish() into the produced RenderBundle.
  • Merge external texture dependencies from executed bundles in RenderPassEncoder::ExecuteBundles() or RenderPassEncoder::End().
  • Ensure CommandEncoder::EndRenderPass() / CommandEncoder::Finish() include bundle-introduced external textures in the command buffer.
  • Keep Queue::Submit() as the final validation point for both direct and bundle paths.
  • Add a regression test that records an external texture into a render bundle, expires the source, executes the bundle, and verifies that Firefox returns External texture is expired instead of crashing.
Flags: sec-bounty?
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: WebGPU
Keywords: csectype-uaf
Product: Firefox → Core
Summary: Security vulnerability in Firefox: GPURenderBundle externalTexture lifetime tracking bypass causes GPU-process use-after-free → GPURenderBundle externalTexture lifetime tracking bypass causes GPU-process use-after-free

Jim can you take a look?

Flags: needinfo?(jimb)

Seems like the render bundle path is definitely broken, but I'm also dubious of this comment on code that is involved in handling this for regular render passes:

A compromised content process could skip this step, but equally it could skip setting the external texture's expired state even if this check were performed on the server side.

My conclusion from this is that we need a way for the server side to independently ensure the texture is still valid, not that it justifies putting additional checks on the client side.

Tentatively triaging as S2.

Severity: -- → S2
Assignee: nobody → aleiserson
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Priority: -- → P1

Not sure the best way to track this, so I'll note it here: the CTS appears to be missing coverage for the case where a submission fails because a render bundle references an expired external texture. (webgpu:api,validation,gpu_external_texture_expiration:* covers the render pass case, but not the render bundle case.)

The code path where the UAF occurs is actually not submission -- it's when the render bundle is being executed, i.e., replayed onto the primary command encoder. So adding a submit time "external texture is expired" check will not be sufficient to prevent that. This also means that there isn't really an equivalent case for render passes, so my concern in comment 2 may not be a problem. I did do some experiments modifying the content process to omit that check and submit the render pass anyways, I was not able to get a crash, but, I'd want to do some more investigation to be confident that path is okay.

Currently there is logic in the Firefox bindings to manage destruction of external textures, I think some of this could be moved into wgpu to be more similar to the way it is handled for textures and buffers and avoid scheduling additional onSubmittedWorkDone callbacks to track when external textures can be expired. But in the interest of getting a minimal fix in place, I do not plan to tackle that now. I'm just planning to add detection to prevent executing a render bundle that uses a destroyed external texture, and also make sure that the submit time checks are correct for both passes and bundles.

Flags: needinfo?(jimb)
Keywords: sec-high

https://github.com/gfx-rs/wgpu/pull/9577 has a fix.

I have held back part of the test. The version of the test in the PR doesn't actually fail before the change. To unleash the chaos, the test has to clear the buffer/texture after creation and before creating the bind group (and flush the command buffer with the clear). But that version of the test fails with Vulkan validation errors and segfaults that could call attention to this being a security issue. I can of course share it privately if desired.

The other thing that's not called out explicitly in the PR, is the detail that external texture planes don't get added to texture_init_actions (f.k.a. used_texture_ranges). Because textures (regular, not external) can be discarded, texture_init_actions is completely populated even if the texture is already initialized when the bind group is created. The cases where we get in to trouble are buffers (which get an init action only if uninitialized when the bind group is created) and external textures (which never get an init action).

I think it's debatable whether it makes more sense to omit the init actions for external textures, or declare them anyways, and rely on the fact that they will be determined not necessary for the external texture planes created with create_texure_from_hal. But not doing anything with that now.

Initially I was going to add logic to BindGroup::try_raw that checked whether external textures in the bind group are destroyed. But that's redundant with the changes above.

Depends on: 2042510

I can no longer reproduce with the attached PoC in nightly 20260529091611.

(There are still two follow-ups: merging the piece of the test that I held back, and either adding tracking of external textures in render bundles forward to submission in dom/webgpu, or removing tracking of external textures in submissions from dom/webgpu entirely and doing it in wgpu instead. The latter is no longer a security issue after the fix that was made.)

:superhei, would appreciate your confirmation of the fix if you have an opportunity.

Also, something I didn't state explicitly before: there was an additional broken case unrelated to external textures that is also covered by the fix. If a buffer was initialized before creating a bind group using that buffer, the buffer was not included in used_buffer_ranges for the bind group, which resulted in a similar ability to perform some operations with the bind group after the buffer was destroyed. This resulted in Vulkan validation errors with my unit test.

Flags: needinfo?(5up3rh3i)

I tested Nightly 20260529091611 with the PoC on macOS/Metal.

I could not get the original source=video variant to progress past media metadata loading in my automated local session, but I
confirmed the same external-texture lifetime path with source=frame.

The direct path now reports:
External texture is expired

The render-bundle path no longer crashes and reports:
In a execute_bundle command, caused by: Buffer with '' label has been destroyed

No GPU-process crash or ASAN-style stale Metal texture behavior was observed. This confirms that the wgpu liveness fix blocks
the security issue. The remaining difference looks like the follow-up semantic tracking issue you mentioned.

Flags: needinfo?(5up3rh3i)
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
See Also: → 2043841
Group: core-security-release

Given the sec rating, I think a Beta-specific cherry-pick patch is needed as well.

Group: gfx-core-security
Flags: needinfo?(aleiserson)
Target Milestone: --- → 153 Branch
Attached file (secure) โ€”
Attachment #9592303 - Flags: approval-mozilla-beta?

Beta uplift requested.

Flags: needinfo?(aleiserson)
Attachment #9592303 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c153/b152]

Small adjustment to repro instructions:

  • Get the file dom/media/test/red-46x48.mp4 from the Firefox source and save it alongside the PoC HTML.
  • Use the URL http://127.0.0.1:8080/webgpu_renderbundle_externaltexture_lifetime.html?mode=bundle&source=video&loop=1&batch=1&readbackEvery=0&deferSubmitMs=100&teardownVideo=1&close=0&beacon=1&videoUrl=/red-46x48.mp4 (adds videoUrl=/red-46x48.mp4)
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main152+]
Alias: CVE-2026-12293
Attached file poc from zip file โ€”
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: