Wild heap write in parent process [@ _cairo_pdf_surface_update_object] via PRemotePrintJob NaN gradient alpha
Categories
(Core :: Printing: Output, defect)
Tracking
()
People
(Reporter: bugmon, Assigned: dholbert)
References
Details
(6 keywords, Whiteboard: [adv-main153+r][adv-esr140.13+r][adv-esr115.38+r])
Attachments
(5 files, 4 external files)
|
215 bytes,
text/html
|
Details | |
|
251 bytes,
text/plain
|
Details | |
|
527 bytes,
patch
|
Details | Diff | Splinter Review | |
|
5.31 KB,
patch
|
Details | Diff | Splinter Review | |
|
8.69 KB,
text/plain
|
Details |
A compromised content process can trigger a wild 8-byte heap write in the privileged parent process via the PRemotePrintJob IPC protocol. The content process writes a Moz2D recording into the parent-supplied page file descriptor containing a GRADIENTSTOPSCREATION event whose first stop has alpha=1.0 and second stop has alpha=NaN, followed by a FILLRECT using a LinearGradientPattern with those stops. When RemotePrintJobParent::RecvProcessPage replays this via PrintTranslator on a DrawTargetCairo PDF surface and calls EndPage() → cairo_surface_show_page(), cairo's PDF backend performs a wild heap write.
The root cause is a NaN-handling inconsistency between two cairo functions that gate transparency-group emission. NaN survives _cairo_restrict_value() because both NaN<0 and NaN>1 are false. _cairo_pattern_alpha_range() initializes alpha_min from stops[0].alpha=1.0 and the comparison alpha_min > NaN is false, so alpha_min stays 1.0; CAIRO_ALPHA_IS_OPAQUE(1.0) is true, so _cairo_pdf_surface_add_pdf_pattern_or_shading() leaves gstate_res.id = 0. But _cairo_pdf_surface_emit_pattern_stops() evaluates !CAIRO_ALPHA_IS_OPAQUE(NaN) = !(NaN>=0.996) = true, so emit_alpha=TRUE and alpha_function.id != 0. _cairo_pdf_surface_emit_gradient() then enters the alpha-function branch with gstate_res.id==0 (the assert at line 5069 is compiled out in release), and calls cairo_pdf_surface_emit_transparency_group() → _cairo_pdf_surface_update_object(surface, {.id=0}). That computes _cairo_array_index(&surface->objects, 0u-1u) = elements + (size_t)0xffffffff * sizeof(cairo_pdf_object_t) and writes object->u.offset = _cairo_output_stream_get_position(surface->output) — an 8-byte wild heap write at a fixed ~64GB offset past the array, with an attacker-influenced value (the PDF stream position, controllable by varying preceding recorded content).
This is reachable in shipping configuration: window.print() from content → user clicks Save to PDF (the default option in Firefox's print dialog) → parent constructs RemotePrintJobParent and hands an FD to the (compromised) content process, which writes the malicious recording and sends ProcessPage.
Build Info
- Branch: main
- Revision: db790003e5df7dcfced5bcf58823a594a1e8e2c2
- Timestamp: 2026-06-16T14:00:24+00:00
Affected Code
File: layout/printing/ipc/RemotePrintJobParent.cpp, line 142-213
mozilla::ipc::IPCResult RemotePrintJobParent::RecvProcessPage(
const int32_t& aWidthInPoints, const int32_t& aHeightInPoints,
nsTArray<uint64_t>&& aDeps) {
...
if (aDeps.IsEmpty()) {
FinishProcessingPage(pageSizeInPoints); // -> PrintPage()
return IPC_OK();
}
...
}
...
nsresult RemotePrintJobParent::PrintPage(...) {
nsresult rv = mPrintDeviceContext->BeginPage(aSizeInPoints);
...
if (!mPrintTranslator->TranslateRecording(aRecording)) { // replays attacker stream onto DrawTargetCairo(PDF)
...
}
...
rv = mPrintDeviceContext->EndPage(); // -> cairo_surface_show_page() emits the page; crash here
...
}
File: gfx/cairo/cairo/src/cairoint.h, line 754-762
static inline double
_cairo_restrict_value (double value, double min, double max)
{
if (value < min)
return min;
else if (value > max)
return max;
else
return value; /* NaN < min and NaN > max are both false -> NaN returned */
}
File: gfx/cairo/cairo/src/cairo-pattern.c, line 3022-3034
case CAIRO_PATTERN_TYPE_LINEAR:
case CAIRO_PATTERN_TYPE_RADIAL: {
const cairo_gradient_pattern_t *gradient = (cairo_gradient_pattern_t *) pattern;
unsigned int i;
assert (gradient->n_stops >= 1);
alpha_min = alpha_max = gradient->stops[0].color.alpha; /* 1.0 */
for (i = 1; i < gradient->n_stops; i++) {
if (alpha_min > gradient->stops[i].color.alpha) /* 1.0 > NaN is FALSE */
alpha_min = gradient->stops[i].color.alpha;
else if (alpha_max < gradient->stops[i].color.alpha)
alpha_max = gradient->stops[i].color.alpha;
}
/* alpha_min = 1.0 -> opaque */
File: gfx/cairo/cairo/src/cairo-pdf-surface.c, line 1917-1933
pdf_pattern.gstate_res.id = 0;
/* gradient patterns require an smask object to implement transparency */
if (pattern->type == CAIRO_PATTERN_TYPE_LINEAR ||
pattern->type == CAIRO_PATTERN_TYPE_RADIAL ||
pattern->type == CAIRO_PATTERN_TYPE_MESH)
{
double min_alpha;
_cairo_pattern_alpha_range (pattern, &min_alpha, NULL);
if (! CAIRO_ALPHA_IS_OPAQUE (min_alpha)) { /* min_alpha=1.0 -> opaque -> branch NOT taken */
pdf_pattern.gstate_res = _cairo_pdf_surface_new_object (surface);
...
}
} /* gstate_res.id stays 0 */
File: gfx/cairo/cairo/src/cairo-pdf-surface.c, line 4559-4565
for (i = 0; i < n_stops; i++) {
...
stops[i].color[3] = pattern->stops[i].color.alpha;
if (!CAIRO_ALPHA_IS_OPAQUE (stops[i].color[3])) /* !(NaN >= 0.996) = TRUE */
emit_alpha = TRUE; /* -> alpha_function.id != 0 */
stops[i].offset = pattern->stops[i].offset;
}
File: gfx/cairo/cairo/src/cairo-pdf-surface.c, line 5066-5084
if (alpha_function.id != 0) {
cairo_pdf_resource_t mask_resource;
assert (pdf_pattern->gstate_res.id != 0); /* no-op in release (NDEBUG) */
mask_resource = _cairo_pdf_surface_new_object (surface);
...
status = cairo_pdf_surface_emit_transparency_group (surface,
pdf_pattern,
pdf_pattern->gstate_res, /* {.id = 0} */
mask_resource);
File: gfx/cairo/cairo/src/cairo-pdf-surface.c, line 374-382
void
_cairo_pdf_surface_update_object (cairo_pdf_surface_t *surface,
cairo_pdf_resource_t resource)
{
cairo_pdf_object_t *object;
object = _cairo_array_index (&surface->objects, resource.id - 1); /* 0u - 1u = 0xffffffff */
object->u.offset = _cairo_output_stream_get_position (surface->output); /* wild 8-byte WRITE */
}
File: gfx/cairo/cairo/src/cairo-array.c, line 167-186
void *
_cairo_array_index (cairo_array_t *array, unsigned int index)
{
if (index == 0 && array->num_elements == 0)
return NULL;
assert (index < array->num_elements); /* no-op in release */
return array->elements + (size_t)index * array->element_size;
/* elements + (size_t)0xffffffff * sizeof(cairo_pdf_object_t) */
}
The parent's PrintTranslator replays attacker-controlled gradient stop colors verbatim onto a cairo PDF surface with no finiteness validation. cairo's input clamp _cairo_restrict_value passes NaN through, and the two opacity checks that gate gstate_res allocation vs alpha-function emission disagree on NaN, leading to an unchecked array index of 0-1 = 0xffffffff and a wild 8-byte write.
Exploit Chain
- Attacker achieves RCE in a sandboxed content process and awaits a PRemotePrintJob actor (created by the parent when the user prints / saves as PDF; window.print() prompts this, and the silent-PDF prefs in the testcase only automate clicking 'Save').
- Compromised content sends PRemotePrintJob::InitializePrint to the parent. Parent creates a cairo PDF PrintTarget, a PrintTranslator, opens an anonymous temp file, and returns the FD via PrintInitializationResult.
- Compromised content writes a Moz2D recording into the FD: header + DRAWTARGETCREATION + SETCURRENTDRAWTARGET + GRADIENTSTOPSCREATION (stops [{offset 0, alpha 1.0}, {offset 1, alpha NaN}]) + FILLRECT with a LinearGradientPattern referencing those stops + DRAWTARGETDESTRUCTION. The NaN alpha is just 4 raw float bytes in the recording; nothing in PrintTranslator/ReadPatternData/DrawTargetCairo validates finiteness.
- Compromised content sends PRemotePrintJob::ProcessPage(100, 100, []).
- Parent RemotePrintJobParent::RecvProcessPage → PrintPage: BeginPage; PrintTranslator::TranslateRecording replays events on DrawTargetCairo(PDF); GfxPatternToCairoPattern calls cairo_pattern_add_color_stop_rgba(..., NaN); _cairo_restrict_value passes NaN through; FillRect records the gradient on the cairo PDF/paginated surface.
- Parent EndPage → cairo_surface_show_page → _cairo_pdf_surface_write_page emits the recorded patterns. _cairo_pdf_surface_add_pdf_pattern_or_shading sees min_alpha=1.0 (NaN comparison is false) and leaves gstate_res.id=0. _cairo_pdf_surface_emit_pattern_stops sees !CAIRO_ALPHA_IS_OPAQUE(NaN) and sets alpha_function.id!=0.
- _cairo_pdf_surface_emit_gradient enters the alpha branch with gstate_res.id==0 (the assert is compiled out) and calls cairo_pdf_surface_emit_transparency_group → _cairo_pdf_surface_update_object(surface, {id=0}).
- _cairo_array_index(&surface->objects, 0u-1u=0xffffffff) returns elements + 0xffffffff*sizeof(cairo_pdf_object_t); object->u.offset = stream_position performs a wild 8-byte heap WRITE in the parent process. The written value (PDF output stream position) is attacker-influenced via the volume of preceding recorded content. With heap grooming to place a target at the fixed offset, this is a sandbox-escape memory-corruption primitive.
IPC Path
- PRemotePrintJob ProcessPage
- Protocol: PRemotePrintJob
- Parent process: Main
- Child process: Content
- Message flow: Content -> Main: InitializePrint(); Main -> Content: PrintInitializationResult(fd); Content writes recording to fd; Content -> Main: ProcessPage(w, h, [])
Steps to Reproduce
- Apply exploit.patch to mozilla-central (modifies only content-process code under XRE_IsContentProcess() guards).
- Build with the linux ASAN fuzzing mozconfig (--disable-debug so cairo's libc assert() is compiled out).
- Set prefs: print.always_print_silent=true, print.print_to_file=true, print_printer="Mozilla Save to PDF", dom.window_print.fuzzing.block_while_printing=true, print.prefer_system_dialog=false.
- Load test.html (which calls window.print()).
- Observe ASAN heap-buffer-overflow WRITE of size 8 in the parent process at _cairo_pdf_surface_update_object (cairo-pdf-surface.c:381) under PRemotePrintJobParent::OnMessageReceived → RemotePrintJobParent::RecvProcessPage.
Security Impact
- Severity: High
- Attacker capability: Sandbox escape primitive: a compromised content process triggers an 8-byte out-of-bounds heap WRITE in the privileged parent (chrome) process at a fixed large offset (objects.elements + 0xffffffff*sizeof(cairo_pdf_object_t) + 8) with an attacker-influenced 64-bit value (the cairo PDF output stream byte position, which the attacker controls by varying the amount of recorded drawing content emitted before the gradient). With heap grooming this provides a write-what-where–style primitive in the parent process.
- Preconditions: Renderer/content-process RCE is required. The PRemotePrintJob actor is created by the parent when the user prints; in the default shipping UI this means the user clicks 'Save'/'Print' in the print dialog after the page calls window.print() (the testcase prefs only automate that click). Affects platforms using the cairo PDF print target (Linux GTK by default; also Windows PDF output paths that use cairo).
ASAN Report
==97637==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x76eed9f75bb8 at pc 0x75ceaee7458c bp 0x7fff0c1f3310 sp 0x7fff0c1f3308
WRITE of size 8 at 0x76eed9f75bb8 thread T0
#0 0x75ceaee7458b in _cairo_pdf_surface_update_object /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:381:22
#1 0x75ceaee7458b in cairo_pdf_surface_emit_transparency_group /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:4850:5
#2 0x75ceaee6a98d in _cairo_pdf_surface_emit_gradient /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:5081:11
#3 0x75ceaee6a98d in _cairo_pdf_surface_emit_pattern /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:5261:11
#4 0x75ceaee6a98d in _cairo_pdf_surface_write_patterns_and_smask_groups /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:7754:15
#5 0x75ceaee61d6f in _cairo_pdf_surface_write_page /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:7862:14
#6 0x75ceaee61d6f in _cairo_pdf_surface_show_page /firefox/gfx/cairo/cairo/src/cairo-pdf-surface.c:5677:14
#7 0x75ceaefb8838 in _moz_cairo_surface_show_page /firefox/gfx/cairo/cairo/src/cairo-surface.c:2584:40
#8 0x75ceaef4c5ec in _cairo_paginated_surface_show_page /firefox/gfx/cairo/cairo/src/cairo-paginated-surface.c:606:5
#9 0x75ceaefb8838 in _moz_cairo_surface_show_page /firefox/gfx/cairo/cairo/src/cairo-surface.c:2584:40
#10 0x75ceb03df989 in mozilla::gfx::PrintTargetPDF::EndPage() /firefox/gfx/thebes/PrintTargetPDF.cpp:100:3
#11 0x75ceaf377ac6 in nsDeviceContext::EndPage() /firefox/gfx/src/nsDeviceContext.cpp:350:5
#12 0x75cebad8b3fa in mozilla::layout::RemotePrintJobParent::PrintPage(...) /firefox/layout/printing/ipc/RemotePrintJobParent.cpp:213:29
#13 0x75cebad8af61 in mozilla::layout::RemotePrintJobParent::FinishProcessingPage(...) /firefox/layout/printing/ipc/RemotePrintJobParent.cpp:186:17
#14 0x75cebad8ac3a in mozilla::layout::RemotePrintJobParent::RecvProcessPage(int const&, int const&, nsTArray<unsigned long>&&) /firefox/layout/printing/ipc/RemotePrintJobParent.cpp:156:5
#15 0x75ceba19ddbb in mozilla::layout::PRemotePrintJobParent::OnMessageReceived(IPC::Message const&) /firefox/obj-firefox-asan/ipc/ipdl/PRemotePrintJobParent.cpp:403:52
#16 0x75ceb8b66970 in mozilla::dom::PContentParent::OnMessageReceived(IPC::Message const&) /firefox/obj-firefox-asan/ipc/ipdl/PContentParent.cpp:6617:32
...
Address 0x76eed9f75bb8 is a wild pointer inside of access range of size 0x000000000008.
SUMMARY: AddressSanitizer: heap-buffer-overflow (/firefox/obj-firefox-asan/dist/bin/libxul.so+0x158ae58b)
| Reporter | ||
Comment 1•4 months ago
|
||
| Reporter | ||
Comment 2•4 months ago
|
||
| Reporter | ||
Comment 3•4 months ago
|
||
| Reporter | ||
Comment 4•4 months ago
|
||
| Reporter | ||
Comment 5•4 months ago
|
||
Updated•4 months ago
|
| Assignee | ||
Comment 7•4 months ago
|
||
I've now reported this to upstream cairo, here:
https://gitlab.freedesktop.org/cairo/cairo/-/work_items/968
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 8•4 months ago
|
||
Updated•4 months ago
|
| Assignee | ||
Comment 9•4 months ago
|
||
ni=jfkthame to be sure you see the phab review request. (Sorry for the spam if this is already on your radar.) I'm on holiday this week, but want to get the sec-approval/etc. wheels turning before I get back, if possible (particularly since this is a trivial-ish one-liner).
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 10•4 months ago
|
||
Comment on attachment 9599445 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Not easily.
- 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?: All branches. Yes, the flags are set correctly.
- 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?: Trivial. The patch should uplift cleanly.
- How likely is this patch to cause regressions; how much testing does it need?: Extremely unlikely. No testing needed.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
| Assignee | ||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Comment 11•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D307892
Updated•4 months ago
|
Comment 12•4 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: security bug, classified as sec-high & csectype-sandbox-escape. (Compromised content process can cause a wild 8-byte heap WRITE in the parent process)
- Code covered by automated testing?: no
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Just flipping the polarity of a bounds-check in a clamping function, to have the same logic except for how NaN is handled (now it'll be clamped instead of being allowed through).
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 13•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D307892
Updated•4 months ago
|
Comment 14•4 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined/Reason for urgency: security bug, classified as sec-high & csectype-sandbox-escape. (Compromised content process can cause a wild 8-byte heap WRITE in the parent process)
- Code covered by automated testing?: no
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Just flipping the polarity of a bounds-check in a clamping function, to have the same logic except for how NaN is handled (now it'll be clamped instead of being allowed through).
- String changes made/needed?: None
- Is Android affected?: yes
Comment 15•4 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: security bug, classified as sec-high & csectype-sandbox-escape. (Compromised content process can cause a wild 8-byte heap WRITE in the parent process)
- Code covered by automated testing?: no
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Just flipping the polarity of a bounds-check in a clamping function, to have the same logic except for how NaN is handled (now it'll be clamped instead of being allowed through).
- String changes made/needed?: None
- Is Android affected?: yes
| Assignee | ||
Comment 16•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D307892
Comment 17•4 months ago
|
||
Comment 18•4 months ago
|
||
Updated•4 months ago
|
Updated•4 months ago
|
Comment 19•4 months ago
|
||
| uplift | ||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 21•4 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 22•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•1 month ago
|
Description
•