RecordedDrawTargetCreation stream desynchronization enables attacker-controlled event execution in parent process via printing IPC
Categories
(Core :: Graphics, defect)
Tracking
()
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 (missingSetIsBad())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:
- Calling
aStream.SetIsBad()to signal the error - 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)
- Content process:
DrawEventRecorderPRFileDescwrites drawing events to a shared file descriptor duringwindow.print()rendering. - Parent process:
RemotePrintJobParent::ProcessPage()(line 205) callsmPrintTranslator->TranslateRecording(aRecording). PrintTranslator::TranslateRecording()(line 27-68) reads events from the shared file descriptor in a loop, callingDoWithEventFromStreamthenPlayEventfor each event.- 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:
- Valid header (magic, major/minor revision)
- A
DRAWTARGETCREATIONevent withmHasExistingData=trueand dimensions that fail allocation (e.g., 32768x32768 @ B8G8R8A8 -- stride * height overflowsint32_t) - 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::PlayEventcallsInstallFontData()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.,
FILLRECTwith a pattern referencing an unregisteredSourceSurface): TriggersLookupSourceSurface()with a craftedReferencePtr, 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)
- Copy
poc_gtest_desync.cpptogfx/tests/gtest/TestRecordingDesync.cpp - Add
"TestRecordingDesync.cpp"toUNIFIED_SOURCESingfx/tests/gtest/moz.build - Build and run:
./mach build ./mach gtest TestRecordingDesync.DrawTargetCreationDesync - 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. - 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 |
Updated•7 months ago
|
Updated•7 months ago
|
| Reporter | ||
Comment 1•7 months ago
|
||
| Reporter | ||
Comment 2•7 months ago
|
||
| Assignee | ||
Comment 3•7 months ago
|
||
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.
| Assignee | ||
Comment 4•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Updated•7 months ago
|
Comment 6•7 months ago
|
||
Updated•7 months ago
|
Comment 7•7 months ago
|
||
The patch landed in nightly and beta is affected.
:lsalzman, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox150towontfix.
For more information, please visit BugBot documentation.
| Assignee | ||
Comment 8•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290285
Updated•7 months ago
|
Comment 9•7 months ago
|
||
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
Updated•7 months ago
|
Updated•7 months ago
|
Comment 10•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
| Assignee | ||
Updated•7 months ago
|
Comment 11•7 months ago
|
||
Unfortunately this bug is not eligible for our bug bounty program which is focused on higher-impact issues.
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•2 months ago
|
Description
•