CVE-2026-79109
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/printing/print_preview_dialog_controller.cc |
modified |
Files Changed
chrome/browser/printing/print_preview_dialog_controller.ccchrome/browser/printing/print_preview_dialog_controller.h
Patch
From 3d63d12873dcf84fa15de78a3b4c52c1aa113b27 Mon Sep 17 00:00:00 2001 From: Lei Zhang <[email protected]> Date: Wed, 22 Jul 2026 18:01:15 -0700 Subject: [PATCH] Print Preview: Store is_pdf bit outside of RequestPrintPreviewParams The mojom::RequestPrintPreviewParams struct has an `is_modifiable` bit that is potentially untrustworthy if it is set by a renderer process. It is also confusing because sometimes the browser process sets this bit. In the long run, there is no need for `is_modifiable` to exist, as the browser process can figure out the value on its own. So it does not need to be passed over IPC via RequestPrintPreviewParams. Start migrating away from it by storing a separate `is_pdf` bit in PrintPreviewDialogController. This bit comes from the browser process and never from a renderer process. Allow callers to read it via IsPrintingPdf() instead of reading `is_modifiable` via GetRequestParams(). Convert GetRequestParams() callers to use IsPrintingPdf() where possible. Bug: 498367544 Change-Id: I529977a18dd52c7c0ca3fc69fa75bec0364e744c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8129157 Commit-Queue: Lei Zhang <[email protected]> Reviewed-by: Andy Phan <[email protected]> Cr-Commit-Position: refs/heads/main@{#1666757} --- diff --git a/chrome/browser/printing/print_preview_dialog_controller.cc b/chrome/browser/printing/print_preview_dialog_controller.cc index e316d44..3bf649c 100644 --- a/chrome/browser/printing/print_preview_dialog_controller.cc +++ b/chrome/browser/printing/print_preview_dialog_controller.cc @@ -195,12 +195,13 @@ void PrintPreviewDialogController::PrintPreview( WebContents* initiator, - const mojom::RequestPrintPreviewParams& params) { + const mojom::RequestPrintPreviewParams& params, + bool is_pdf) { if (initiator->IsCrashed()) { return; } - if (!GetOrCreatePreviewDialog(initiator, params)) { + if (!GetOrCreatePreviewDialog(initiator, params, is_pdf)) { auto* print_view_manager = PrintViewManager::FromWebContents(initiator); if (print_view_manager) { print_view_manager->PrintPreviewDone(); @@ -217,14 +218,16 @@ WebContents* PrintPreviewDialogController::GetOrCreatePreviewDialogForTesting( WebContents* initiator) { + constexpr bool kIsPdf = false; mojom::RequestPrintPreviewParams params; - params.is_modifiable = true; - return GetOrCreatePreviewDialog(initiator, params); + params.is_modifiable = !kIsPdf; + return GetOrCreatePreviewDialog(initiator, params, kIsPdf); } WebContents* PrintPreviewDialogController::GetOrCreatePreviewDialog( WebContents* initiator, - const mojom::RequestPrintPreviewParams& params) { + const mojom::RequestPrintPreviewParams& params, + bool is_pdf) { DCHECK(initiator); // Get the print preview dialog for `initiator`. @@ -242,7 +245,7 @@ return nullptr; } - return CreatePrintPreviewDialog(tab, initiator, params); + return CreatePrintPreviewDialog(tab, initiator, params, is_pdf); } WebContents* PrintPreviewDialogController::GetPrintPreviewForContents( @@ -276,6 +279,15 @@ return it != preview_dialog_map_.end() ? &it->second.request_params : nullptr; } +std::optional<bool> PrintPreviewDialogController::IsPrintingPdf( + content::WebContents* preview_dialog) const { + auto it = preview_dialog_map_.find(preview_dialog); + if (it != preview_dialog_map_.end()) { + return it->second.is_pdf; + } + return std::nullopt; +} + void PrintPreviewDialogController::ForEachPreviewDialog( base::RepeatingCallback<void(content::WebContents*)> callback) { for (const auto& it : preview_dialog_map_) @@ -303,6 +315,8 @@ web_contents_collection_.StopObserving(it->second.initiator); it->second.initiator = nullptr; it->second.request_params = {}; + // Set to true to match the behavior for resetting `request_params`. + it->second.is_pdf = true; it->second.scoper.reset(); } @@ -318,9 +332,11 @@ PrintPreviewDialogController::InitiatorData::InitiatorData( content::WebContents* initiator, const mojom::RequestPrintPreviewParams& request_params, + bool is_pdf, std::unique_ptr<tabs::ScopedTabModalUI> scoper) : initiator(initiator), request_params(request_params), + is_pdf(is_pdf), scoper(std::move(scoper)) {} PrintPreviewDialogController::InitiatorData::~InitiatorData() = default; @@ -424,7 +440,8 @@ WebContents* PrintPreviewDialogController::CreatePrintPreviewDialog( tabs::TabInterface* tab, content::WebContents* initiator, - const mojom::RequestPrintPreviewParams& params) { + const mojom::RequestPrintPreviewParams& params, + bool is_pdf) { base::AutoReset<bool> auto_reset(&is_creating_print_preview_dialog_, true); // The dialog delegates are deleted when the dialog is closed. @@ -444,7 +461,8 @@ PrintViewManager::CreateForWebContents(preview_dialog); // Add an entry to the map. - InitiatorData data(initiator, params, tab ? tab->ShowModalUI() : nullptr); + InitiatorData data(initiator, params, is_pdf, + tab ? tab->ShowModalUI() : nullptr); preview_dialog_map_.emplace(preview_dialog, std::move(data)); // Make the print preview WebContents show up in the task manager. diff --git a/chrome/browser/printing/print_preview_dialog_controller.h b/chrome/browser/printing/print_preview_dialog_controller.h index 77fea0a8..23b5b35 100644 --- a/chrome/browser/printing/print_preview_dialog_controller.h +++ b/chrome/browser/printing/print_preview_dialog_controller.h @@ -7,6 +7,7 @@ #include <map> #include <memory> +#include <optional> #include "base/check.h" #include "base/functional/callback.h" @@ -57,7 +58,8 @@ // Initiates print preview for `initiator`. void PrintPreview(content::WebContents* initiator, - const mojom::RequestPrintPreviewParams& params); + const mojom::RequestPrintPreviewParams& params, + bool is_pdf); // Returns the preview dialog for `contents`. // Returns `contents` if `contents` is a preview dialog. @@ -74,6 +76,10 @@ const mojom::RequestPrintPreviewParams* GetRequestParams( content::WebContents* preview_dialog) const; + // Returns whether the initiator associated with `preview_dialog` is printing + // PDF content or not. Returns nullopt if no data exists for `preview_dialog`. + std::optional<bool> IsPrintingPdf(content::WebContents* preview_dialog) const; + // Runs `callback` on the dialog of each active print preview operation. void ForEachPreviewDialog( base::RepeatingCallback<void(content::WebContents*)> callback); @@ -95,9 +101,11 @@ content::WebContents* preview_dialog) { CHECK(initiator); CHECK(preview_dialog); + + constexpr bool kIsPdf = false; mojom::RequestPrintPreviewParams params; - params.is_modifiable = true; - InitiatorData data(initiator, params, /*scoper=*/nullptr); + params.is_modifiable = !kIsPdf; + InitiatorData data(initiator, params, kIsPdf, /*scoper=*/nullptr); preview_dialog_map_.emplace(preview_dialog, std::move(data)); } void DisassociateWebContentsesForTesting( @@ -117,6 +125,7 @@ struct InitiatorData { InitiatorData(content::WebContents* initiator, const mojom::RequestPrintPreviewParams& request_params, + bool is_pdf, std::unique_ptr<tabs::ScopedTabModalUI> scoper); InitiatorData(InitiatorData&&) noexcept; InitiatorData& operator=(InitiatorData&&) noexcept; @@ -124,6 +133,7 @@ raw_ptr<content::WebContents> initiator; mojom::RequestPrintPreviewParams request_params; + bool is_pdf; // Prevents other tab-modal UIs from showing. std::unique_ptr<tabs::ScopedTabModalUI> scoper; @@ -159,14 +169,16 @@ // Gets/Creates the print preview dialog for `initiator`. content::WebContents* GetOrCreatePreviewDialog( content::WebContents* initiator,
Original Bug Report
Potential Sandbox Escape via Forged is_modifiable Flag in Print IPC on macOS
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can forge the is_modifiable flag in print preview IPCs to bypass the sandboxed PrintCompositor. This allows the attacker to send arbitrary malicious PDF bytes to the unsandboxed PrintBackendService on macOS. Parsing these bytes with Apple CoreGraphics provides a potential path for a full sandbox escape.
Affected files:
chrome/browser/printing/print_view_manager.ccchrome/browser/ui/webui/print_preview/print_preview_ui.ccprinting/pdf_metafile_cg_mac.ccchrome/browser/printing/print_view_manager_base.ccchrome/browser/ui/webui/print_preview/print_preview_handler.ccchrome/browser/printing/print_preview_data_service.ccchrome/services/printing/print_backend_service_impl.cc
Estimated timestamp from git blame: 2026-02-10
Summary
There is a potential sandbox escape vulnerability in the Chrome printing architecture on macOS. A compromised HTML renderer can forge the is_modifiable flag during a print preview request. Because the browser fails to validate this flag against the actual renderer type, the attacker can bypass the sandboxed PrintCompositor and directly supply malicious PDF bytes. When printed, these bytes are parsed by Apple’s CoreGraphics library within an unsandboxed utility process (or the browser process), creating a direct path to a sandbox escape.
Technical Details
1. Unvalidated IPC Parameter
When a renderer initiates a print preview, it sends a RequestPrintPreview or ShowScriptedPrintPreview IPC to PrintViewManager (chrome/browser/printing/print_view_manager.cc). The renderer provides a mojom::RequestPrintPreviewParams struct containing an is_modifiable boolean. The browser passes this parameter directly to the PrintPreviewDialogController without verifying if the renderer actually hosts a PDF document (e.g., by checking render_frame_host->GetProcess()->IsPdf()).
2. PrintCompositor Bypass
When the WebUI triggers PrintPreviewUI::MetafileReadyForPrinting (chrome/browser/ui/webui/print_preview/print_preview_ui.cc), the browser calls ShouldUseCompositor(). This function simply returns the unvalidated is_modifiable flag. If the attacker sets this to false, the browser bypasses the sandboxed PrintCompositor and accepts the renderer’s raw shared memory region as a valid PDF document. The only validation performed in release builds is LooksLikePdf, which trivially checks that the payload begins with %PDF- and is at least 50 bytes long.
3. Unsandboxed Sink on macOS
When the user clicks “Print” in the preview dialog, the payload is sent to the PrintBackendService. On macOS, features::kEnableOopPrintDriversSandbox is false by default (printing/printing_features.cc), meaning the PrintBackendService is launched completely unsandboxed (Sandbox::kNoSandbox). The payload eventually reaches PdfMetafileCg::RenderPage (printing/pdf_metafile_cg_mac.cc), where the bytes are parsed using Apple CoreGraphics APIs (CGPDFDocumentCreateWithProvider and CGContextDrawPDFPage). Exploiting memory corruption in the system CoreGraphics PDF parser would yield full unsandboxed code execution.
Potential Attacker Steps
(Note: These are suggested/potential steps to trigger the vulnerability, as our tooling agent does not currently have the ability to run code to provide a working proof-of-concept.)
- Gain code execution in a standard HTML renderer process (e.g., via a V8 or Blink vulnerability).
- Send a
RequestPrintPreviewIPC to the browser process, maliciously setting theis_modifiableflag tofalsein the parameters. - Wait for the Print Preview WebUI to initialize and bind the
mojom::PrintPreviewUIinterface back to the compromised renderer. - Call
MetafileReadyForPrintingon thePrintPreviewUIinterface, passing a shared memory region containing a crafted PDF designed to exploit a known or zero-day vulnerability in Apple’s CoreGraphics PDF parser. - The attacker can use the compromised renderer to spoof the preview visual to look benign. When the user clicks “Print”, the malicious payload is processed by the unsandboxed
PrintBackendService, resulting in a sandbox escape.
Suggested Fix
The browser should not trust the is_modifiable flag provided by the renderer in PrintViewManager::RequestPrintPreview and PrintViewManager::ShowScriptedPrintPreview.
Instead, the browser should securely determine this state itself. For example, it can override or validate the renderer’s flag using the process state:
params->is_modifiable = !render_frame_host->GetProcess()->IsPdf();
This is similar to how PrintViewManagerBase::CompleteScriptedPrint already securely determines if the document is modifiable.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.