Closed Bug 2005014 (CVE-2026-0880) Opened 11 months ago Closed 10 months ago

OOBW/R due to integer overflow in CopyToImageSurface (sandbox escape)

Categories

(Core :: Graphics, defect)

defect

Tracking

()

RESOLVED FIXED
148 Branch
Tracking Status
firefox-esr115 147+ fixed
firefox-esr140 147+ fixed
firefox146 --- wontfix
firefox147 + fixed
firefox148 + fixed

People

(Reporter: oskarlindberg348, Assigned: lsalzman)

References

Details

(4 keywords, Whiteboard: [client-bounty-form] [adv-main147+] [adv-esr140.7+] [adv-esr115.32+])

Attachments

(1 file, 1 external file)

Writeup follows

Flags: sec-bounty?

Background

Same as for bug 2004602

Vulnerability

In the same flow for 2004602,
The issue resides in CopyToImageSurface:

tatic cairo_surface_t* CopyToImageSurface(unsigned char* aData,
                                           const IntRect& aRect,
                                           int32_t aStride,
                                           SurfaceFormat aFormat) {
  MOZ_ASSERT(aData);

  auto aRectWidth = aRect.Width();
  auto aRectHeight = aRect.Height();

  cairo_surface_t* surf = cairo_image_surface_create(
      GfxFormatToCairoFormat(aFormat), aRectWidth, aRectHeight);
  // In certain scenarios, requesting larger than 8k image fails.  Bug 803568
  // covers the details of how to run into it, but the full detailed
  // investigation hasn't been done to determine the underlying cause.  We
  // will just handle the failure to allocate the surface to avoid a crash.
  if (cairo_surface_status(surf)) {
    gfxWarning() << "Invalid surface DTC " << cairo_surface_status(surf);
    return nullptr;
  }

  unsigned char* surfData = cairo_image_surface_get_data(surf);
  int surfStride = cairo_image_surface_get_stride(surf);
  int32_t pixelWidth = BytesPerPixel(aFormat);

  unsigned char* source = aData + aRect.Y() * aStride + aRect.X() * pixelWidth;

  MOZ_ASSERT(aStride >= aRectWidth * pixelWidth);
  for (int32_t y = 0; y < aRectHeight; ++y) {
    memcpy(surfData + y * surfStride, source + y * aStride,
           aRectWidth * pixelWidth); // [1]
  }
  cairo_surface_mark_dirty(surf);
  return surf;
}
  1. Since both y and surfStride are int32, their multiplication is an int32, which is sign extended before being added to surfData, making the __dst of memcpy, point backwards.

Looking at cairo internal limits for cairo_image_surface_create:

#define MAX_IMAGE_SIZE 32767

static cairo_bool_t
_cairo_image_surface_is_size_valid (int width, int height)
{
    return 0 <= width  &&  width <= MAX_IMAGE_SIZE &&
	   0 <= height && height <= MAX_IMAGE_SIZE;
}

32767*4 * 32767 = 0xFFFC0004 = -262140 (dword)

Can also be seen in IDA:
![[Pasted image 20251209200126.png]]
We can see that a to the second memcpy, we get r13 + rdi, r13 is the surface data.
rdi being the multiplciaiton result from the second block, sign extended (ebp being the width given as argument, and ecx the stride=4).

Asan report

==107697==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x76cfa2a9980f at pc 0x5dd4231ea70b bp 0x7ffd2abbf510 sp 0x7ffd2abbecd0
READ of size 131068 at 0x76cfa2a9980f thread T0
    #0 0x5dd4231ea70a in __asan_memcpy /builds/worker/fetches/llvm-project/compiler-rt/lib/asan/asan_interceptors_memintrinsics.cpp:63:3
    #1 0x76d02713be25 in mozilla::gfx::CopyToImageSurface(unsigned char*, mozilla::gfx::IntRectTyped<mozilla::gfx::UnknownUnits> const&, int, mozilla::gfx::SurfaceFormat) /home/user/Downloads/firefox/gfx/2d/DrawTargetCairo.cpp:226:5
    #2 0x76d02712c97e in mozilla::gfx::GetCairoSurfaceForSourceSurface(mozilla::gfx::SourceSurface*, bool, mozilla::gfx::IntRectTyped<mozilla::gfx::UnknownUnits> const&) /home/user/Downloads/firefox/gfx/2d/DrawTargetCairo.cpp:372:33
    #3 0x76d02712fd4c in mozilla::gfx::GfxPatternToCairoPattern(mozilla::gfx::Pattern const&, float, mozilla::gfx::BaseMatrix<float> const&) /home/user/Downloads/firefox/gfx/2d/DrawTargetCairo.cpp:463:31
    #4 0x76d02712f125 in mozilla::gfx::DrawTargetCairo::DrawPattern(mozilla::gfx::Pattern const&, mozilla::gfx::StrokeOptions const&, mozilla::gfx::DrawOptions const&, mozilla::gfx::DrawTargetCairo::DrawPatternType, bool) /home/user/Downloads/firefox/gfx/2d/DrawTargetCairo.cpp:1046:7
    #5 0x76d027134488 in mozilla::gfx::DrawTargetCairo::StrokeRect(mozilla::gfx::RectTyped<mozilla::gfx::UnknownUnits, float> const&, mozilla::gfx::Pattern const&, mozilla::gfx::StrokeOptions const&, mozilla::gfx::DrawOptions const&) /home/user/Downloads/firefox/gfx/2d/DrawTargetCairo.cpp:1294:3
    #6 0x76d02724b074 in nsDeviceContext::CreateRenderingContextCommon(bool) /home/user/Downloads/firefox/gfx/src/nsDeviceContext.cpp:146:9
    #7 0x76d02724abff in nsDeviceContext::CreateRenderingContext() /home/user/Downloads/firefox/gfx/src/nsDeviceContext.cpp:110:10
    #8 0x76d031a09e5f in mozilla::layout::PrintTranslator::CreateDrawTarget(mozilla::gfx::ReferencePtr, mozilla::gfx::IntSizeTyped<mozilla::gfx::UnknownUnits> const&, mozilla::gfx::SurfaceFormat) /home/user/Downloads/firefox/layout/printing/PrintTranslator.cpp:78:51

A read violation happens, a write will follow.

Implicaitons

Out of bounds read and write in the browser process on firefox latest

Reproduction

  1. Apply the following patch:
diff --git a/gfx/src/nsDeviceContext.cpp b/gfx/src/nsDeviceContext.cpp
index 055ebbabac41..43fdddb80921 100644
--- a/gfx/src/nsDeviceContext.cpp
+++ b/gfx/src/nsDeviceContext.cpp
@@ -10,11 +10,16 @@
 #include "gfxPoint.h"    // for gfxSize
 #include "gfxTextRun.h"  // for gfxFontGroup
 #include "mozilla/LookAndFeel.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DrawEventRecorder.h"
 #include "mozilla/gfx/PathHelpers.h"
+#include "mozilla/gfx/Point.h"
 #include "mozilla/gfx/PrintTarget.h"
 #include "mozilla/ProfilerMarkers.h"
 #include "mozilla/StaticPrefs_layout.h"
-#include "mozilla/Try.h"            // for MOZ_TRY
+#include "mozilla/Try.h"  // for MOZ_TRY
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/layout/printing/DrawEventRecorder.h"
 #include "mozilla/widget/Screen.h"  // for Screen
 #include "nsDebug.h"                // for NS_ASSERTION, etc
 #include "nsFontMetrics.h"          // for nsFontMetrics
@@ -127,6 +132,18 @@ UniquePtr<gfxContext> nsDeviceContext::CreateRenderingContextCommon(
     RefPtr<DrawEventRecorder> recorder;
     mDeviceContextSpec->GetDrawEventRecorder(getter_AddRefs(recorder));
     dt = mPrintTarget->MakeDrawTarget(gfx::IntSize(mWidth, mHeight), recorder);
+
+    auto data_surf_size = mozilla::gfx::IntSize(0x100, 0x100);
+    auto data_surf_format = SurfaceFormat::R8G8B8A8;
+    auto data_surf =
+        Factory::CreateDataSourceSurface(data_surf_size, data_surf_format, true)
+            .take();
+
+    auto pattern =
+        SurfacePattern(data_surf, ExtendMode::REPEAT, Matrix(),
+                       SamplingFilter::GOOD, IntRect(0, 0, 0x7fff, 0x7fff));
+    const auto rect = Rect(0, 0, 0x100, 0x100);
+    dt->StrokeRect(rect, pattern);
   }
 
   if (!dt || !dt->IsValid()) {

  1. Host the triggering index.html (this just prints the page, save the result as pdf to trigger rendering, this is not a necessity for the bug. its for debugging reasons only)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Print Example</title>
    <style>
      /* This CSS rule hides the print button when the user is printing the page. */
      @media print {
        .print-button {
          display: none;
        }
      }
    </style>

    <script>
      window.print();
    </script>
  </head>
  <body>
    <header>
      <h1>This is the content you can print</h1>
      <p>This is a paragraph that will be included in the printout.</p>
    </header>

    <main>
      <p>You can also control what gets printed using CSS.</p>
      <p>
        For example, elements with the class `print-button` will be hidden in
        the print preview.
      </p>
    </main>

    <footer>
      <p>&copy; 2025 Print Example</p>
    </footer>

    <button class="print-button" onclick="window.print()">
      Print this Page
    </button>
  </body>
</html>

The vulnerability was found by manual auditing.

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

Thanks for the report. I noticed there's a mention of an IDA image in the text, but you'll have to attach that to the bug.

Flags: needinfo?(oskarlindberg348)
See Also: → CVE-2026-0879
Summary: OOBW/R due to integer overflow (sandbox escape) → OOBW/R due to integer overflow in CopyToImageSurface (sandbox escape)
Attached image ida_low_res —
Flags: needinfo?(oskarlindberg348)

Lee, would you be able to take a look at this one too?

Flags: needinfo?(lsalzman)
Attached file (secure) —
Assignee: nobody → lsalzman
Status: NEW → ASSIGNED

Pixman allows sizes that will fit in a size_t. But the stride math was using ints. Easy enough fix.

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

Comment on attachment 9532140 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: This requires compromising printing, so isn't entirely simple to achieve.
  • 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?: all
  • If not all supported branches, which bug introduced the flaw?: None
  • 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 #9532140 - Flags: sec-approval?

Comment on attachment 9532140 [details]
(secure)

Approved to land and request uplift

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

Comment on attachment 9532140 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Sec-high exploit in printing.
  • 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

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration:
  • User impact if declined:
  • Fix Landed on Version:
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky):
Attachment #9532140 - Flags: approval-mozilla-esr140?
Attachment #9532140 - Flags: approval-mozilla-esr115?
Attachment #9532140 - Flags: approval-mozilla-beta?
Severity: -- → S2
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 10 months ago
Resolution: --- → FIXED
Target Milestone: --- → 148 Branch

Comment on attachment 9532140 [details]
(secure)

Approved for 147.0b4.

Attachment #9532140 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c148/b147]

Comment on attachment 9532140 [details]
(secure)

Approved for 140.7esr and 115.32esr.

Attachment #9532140 - Flags: approval-mozilla-esr140?
Attachment #9532140 - Flags: approval-mozilla-esr140+
Attachment #9532140 - Flags: approval-mozilla-esr115?
Attachment #9532140 - Flags: approval-mozilla-esr115+

This requires compromising printing,

What do you mean by that? That the user has to explicitly hit the print button on the preview dialog and that window.print() alone can't trigger the bug? If the bug is triggered in print preview then a web page can exploit that through window.print(), but if it requires actual user interaction with printing then this is more of a high-end sec-moderate than a true sec-high.

The size_t casting in this patch worries me -- how many other places in cairo-involved code need the same fix?

Flags: needinfo?(lsalzman)

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

This requires compromising printing,

What do you mean by that? That the user has to explicitly hit the print button on the preview dialog and that window.print() alone can't trigger the bug? If the bug is triggered in print preview then a web page can exploit that through window.print(), but if it requires actual user interaction with printing then this is more of a high-end sec-moderate than a true sec-high.

The size_t casting in this patch worries me -- how many other places in cairo-involved code need the same fix?

I only saw two locations which I believe I addressed.

Flags: needinfo?(lsalzman)
Flags: sec-bounty? → sec-bounty+
Whiteboard: [client-bounty-form] → [client-bounty-form] [adv-main147+] [adv-esr140.7+] [adv-esr115.32+]
Alias: CVE-2026-0880
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: