Closed Bug 2026601 Opened 7 months ago Closed 7 months ago

RecordedDrawTargetCreation stream desynchronization enables attacker-controlled event execution in parent process via printing IPC

Categories

(Core :: Graphics, defect)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox149 --- wontfix
firefox150 + fixed
firefox151 + fixed

People

(Reporter: mirzashihab2, Assigned: lsalzman)

Details

(Keywords: csectype-other, reporter-external, sec-want, Whiteboard: [client-bounty-form][adv-main150-])

Attachments

(2 files, 2 external files)

RecordedDrawTargetCreation stream desynchronization enables attacker-controlled event execution in parent process via printing IPC

Summary

RecordedDrawTargetCreation's deserializing constructor in gfx/2d/RecordedEventImpl.h (line 2257) fails to call aStream.SetIsBad() when Factory::CreateDataSourceSurface() returns null. The expected pixel data bytes are not consumed from the stream, desynchronizing it. The PrintTranslator::TranslateRecording() loop in the parent process then parses attacker-controlled bytes as subsequent drawing event types.

A compromised content process writes a crafted drawing recording to the shared file descriptor used by the printing IPC path. The parent process replays this recording, hitting the desync and executing attacker-injected events (including FONTDATA, which passes arbitrary bytes to OS font parsers running in the parent process).

The analogous code in RecordedSourceSurfaceCreation (line 3690) correctly calls aStream.SetIsBad() on the same failure condition.

Affected Component

  • gfx/2d/RecordedEventImpl.h, line 2257-2261 (missing SetIsBad())
  • layout/printing/PrintTranslator.cpp, line 27-68 (parent-side translation loop)
  • layout/printing/ipc/RemotePrintJobParent.cpp, line 205 (IPC entry point)

Severity

sec-high (S2) -- Parent-process memory corruption from compromised content process. Attacker controls which event types and data are parsed in the parent.

Root Cause

// gfx/2d/RecordedEventImpl.h:2244-2271
template <class S>
RecordedDrawTargetCreation::RecordedDrawTargetCreation(S& aStream)
    : RecordedEventDerived(DRAWTARGETCREATION), mExistingData(nullptr) {
  ReadElement(aStream, mRefPtr);
  ReadElementConstrained(aStream, mBackendType, ...);
  ReadElement(aStream, mRect);
  ReadElementConstrained(aStream, mFormat, ...);
  ReadElement(aStream, mHasExistingData);

  if (mHasExistingData) {
    RefPtr<DataSourceSurface> dataSurf =
        Factory::CreateDataSourceSurface(mRect.Size(), mFormat);
    if (!dataSurf) {
      gfxWarning()
          << "RecordedDrawTargetCreation had to reset mHasExistingData";
      mHasExistingData = false;
      return;  // BUG: no SetIsBad(), pixel data not consumed
    }
    // ... reads BytesPerPixel(mFormat) * mRect.width * mRect.height bytes ...
    mExistingData = dataSurf;
  }
}

When mHasExistingData is true in the serialized stream, the Record() method writes BytesPerPixel(mFormat) * mRect.width * mRect.height bytes of pixel data after the event header. The deserializing constructor is expected to consume those bytes.

When CreateDataSourceSurface fails (e.g., due to oversized dimensions), the constructor returns early at line 2261 without:

  1. Calling aStream.SetIsBad() to signal the error
  2. Consuming the expected pixel data bytes from the stream

The stream position is now wrong. The translation loop (PrintTranslator.cpp:51-68) checks aRecording.good() before each iteration -- since SetIsBad() was never called, the loop continues and reads the next byte from the unconsumed pixel data region as an event type.

Correct Comparison

RecordedSourceSurfaceCreation (line 3648-3692) handles the identical failure correctly:

// gfx/2d/RecordedEventImpl.h:3686-3691
if (!mDataSurface) {
    gfxCriticalNote
        << "RecordedSourceSurfaceCreation failed to allocate data of size "
        << size.width << " x " << size.height;
    aStream.SetIsBad();  // CORRECT: marks stream bad, loop stops
}

IPC Path (Content Process to Parent Process)

  1. Content process: DrawEventRecorderPRFileDesc writes drawing events to a shared file descriptor during window.print() rendering.
  2. Parent process: RemotePrintJobParent::ProcessPage() (line 205) calls mPrintTranslator->TranslateRecording(aRecording).
  3. PrintTranslator::TranslateRecording() (line 27-68) reads events from the shared file descriptor in a loop, calling DoWithEventFromStream then PlayEvent for each event.
  4. A compromised content process writes arbitrary bytes to the shared file descriptor instead of legitimate drawing events.

Exploitation

Trigger

A compromised content process crafts a recording containing:

  1. Valid header (magic, major/minor revision)
  2. A DRAWTARGETCREATION event with mHasExistingData=true and dimensions that fail allocation (e.g., 32768x32768 @ B8G8R8A8 -- stride * height overflows int32_t)
  3. Attacker-controlled bytes in the "pixel data" region that will be parsed as subsequent events after the desync

Injectable Events

After the desync, the attacker controls which event types are parsed. High-impact options:

  • FONTDATA (type 46): RecordedFontData::PlayEvent calls InstallFontData() which passes attacker-controlled bytes to the platform font parser (FreeType on Linux, DirectWrite on Windows, CoreText on macOS) in the parent process. Font parsers are historically rich in memory corruption bugs.

  • SOURCESURFACECREATION (type 29): Triggers allocation with attacker-controlled size/format, followed by attacker-controlled data reads.

  • Events referencing non-existent objects (e.g., FILLRECT with a pattern referencing an unregistered SourceSurface): Triggers LookupSourceSurface() with a crafted ReferencePtr, causing null dereference or use of uninitialized translator state.

Impact

  • Parent-process memory corruption: Attacker controls event types and data parsed in the parent process
  • Font parser attack surface: FONTDATA sends arbitrary bytes to OS font parsers (FreeType/DirectWrite/CoreText) in the parent, which have a long history of exploitable vulnerabilities
  • Controlled crash: Events referencing non-existent objects crash the parent process (DoS)
  • All platforms: The printing recording path is used on all platforms

Steps to Reproduce

Method 1: GTest (in-tree, recommended)

  1. Copy poc_gtest_desync.cpp to gfx/tests/gtest/TestRecordingDesync.cpp
  2. Add "TestRecordingDesync.cpp" to UNIFIED_SOURCES in gfx/tests/gtest/moz.build
  3. Build and run:
    ./mach build
    ./mach gtest TestRecordingDesync.DrawTargetCreationDesync
    
  4. Expected output (bug present):
    === BUG CONFIRMED: RecordedDrawTargetCreation stream desync ===
    Stream position after event: <N> (expected: <N>)
    Stream still good: YES (BUG)
    Next byte read as event type: 11 (SETTRANSFORM=11)
    Attacker controls event type parsed after desync.
    
  5. A second test (SourceSurfaceCreationCorrect) verifies the correct behavior for contrast.

Method 2: Python binary generator

Run poc_stream_desync.py to generate malicious recording binaries:

  • poc_desync_recording.bin -- SETTRANSFORM injection (basic desync proof)
  • poc_desync_fontdata.bin -- FONTDATA injection (font parser attack)

These can be fed to PrintTranslator::TranslateRecording() by replacing the shared file descriptor in a modified build.

Suggested Fix

One-line fix matching the existing correct pattern:

--- a/gfx/2d/RecordedEventImpl.h
+++ b/gfx/2d/RecordedEventImpl.h
@@ -2257,6 +2257,7 @@ RecordedDrawTargetCreation::RecordedDrawTargetCreation(S& aStream)
     if (!dataSurf) {
       gfxWarning()
           << "RecordedDrawTargetCreation had to reset mHasExistingData";
       mHasExistingData = false;
+      aStream.SetIsBad();
       return;
     }

This matches the existing correct behavior in RecordedSourceSurfaceCreation (line 3690).

Variant Analysis

Checked all recording event types with allocation-then-read patterns for the same missing SetIsBad():

Event Type Has allocation failure path? Calls SetIsBad? Status
RecordedDrawTargetCreation Yes (line 2257) No VULNERABLE
RecordedSourceSurfaceCreation Yes (line 3686) Yes (line 3690) Safe
RecordedFontData Yes Yes Safe
RecordedFilterNodeSetAttribute Yes Yes Safe
RecordedGradientStopsCreation Potential (32-bit overflow) Partial Low risk

Affected Versions

  • Firefox Nightly, Beta, Release (all current)
  • Firefox ESR (all current)
  • Thunderbird (all versions using Gecko)
  • All platforms (Windows, macOS, Linux, Android)

Files

File Role
gfx/2d/RecordedEventImpl.h:2244-2271 Buggy deserialization (missing SetIsBad)
gfx/2d/RecordedEventImpl.h:3648-3692 Correct comparison (RecordedSourceSurfaceCreation)
layout/printing/PrintTranslator.cpp:27-68 Parent-side translation loop
layout/printing/ipc/RemotePrintJobParent.cpp:205 IPC entry point (parent reads content's recording)
gfx/thebes/PrintTargetRecording.cpp Content-side recording target
Flags: sec-bounty?
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics
Product: Firefox → Core
Severity: -- → S2
Attached file poc_gtest_desync.cpp —
Attached file poc_stream_desync.py —

I am going to label this as a sec-vector, because it doesn't actually introduce a new vulnerability that can't be achieved by just sending an arbitary recording stream from the content process already. This just gives a slightly different way of sending that data, but the fundamental idea of content process sending an arbitary stream is already achievable even without this.

Keywords: sec-vector
Attached file (secure) —
Assignee: nobody → lsalzman
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Keywords: sec-vector → sec-low
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

The patch landed in nightly and beta is affected.
:lsalzman, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(lsalzman)
Attached file (secure) —
Attachment #9560732 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: Possible corruption in recording stream.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Adds a bit more validation.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9560732 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]
Flags: needinfo?(lsalzman)

Unfortunately this bug is not eligible for our bug bounty program which is focused on higher-impact issues.

Flags: sec-bounty? → sec-bounty-
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main150+]
Keywords: sec-low → sec-want
Whiteboard: [client-bounty-form][adv-main150+] → [client-bounty-form][adv-main150-]
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: