CVE-2026-8001
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
PrintBackendServiceImplbase/threading/thread_restrictions.h |
modified | |
MetafilePlayerbase/threading/thread_restrictions.h |
modified | |
PrintBackendServiceManagerbase/threading/thread_restrictions.h |
modified | |
PrintPreviewUIUntrustedbase/threading/thread_restrictions.h |
modified | |
PrinterQuerybase/threading/thread_restrictions.h |
modified | |
ifchrome/browser/printing/print_job_worker.cc |
modified |
Files Changed
base/threading/thread_restrictions.hchrome/browser/printing/print_job_worker.ccchrome/browser/printing/print_job_worker_oop.ccprinting/printed_document.ccprinting/printed_document.h
Patch
From 3ac886fd67262e99f07b0107cd93d16f379a78b2 Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Mon, 30 Mar 2026 18:13:10 -0700
Subject: [PATCH] Printing: Fix locking in PrintedDocument
The PrintedDocument class has a `lock_` to guard the `mutable_` field,
but it is not used consistently. Add GUARDED_BY() to annotate `mutable_`
and get the compiler to check for cases where it is not guarded. Based
on its findings, the unsafe issues are:
1. Trying to post a task to run DebugDumpTask() on another thread later
without a lock. To fix this, just run the task on the current thread
while holding the lock. This has the potential to do I/O on the UI
thread, but only when the browser has a debugging command line
switch, which is very rare. So make an exception for this and let the
debug-only function use base::ScopedAllowBlocking.
Tidy the code slightly and rename it to DumpMetafileIfDebugEnabled().
2. Exposing GetMetafile() as a public method, which requires locking,
but the callers do not have a way to acquire the lock. To fix this,
get rid of this getter with an impossible locking requirement, and
replace it with HasDocument() and GetDocumentData(), which are
tailored to the two external callers. These new methods acquire the
lock properly. For the internal GetMetafile() caller, just access the
underlying `mutable_` field directly.
Bug: 494764371
Change-Id: I3a6ae714e01955bde474ae146416bc8ff92b4e41
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7709456
Auto-Submit: Lei Zhang <thestig@chromium.org>
Commit-Queue: Rebekah Potter <rbpotter@chromium.org>
Reviewed-by: Rebekah Potter <rbpotter@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1607530}
---
diff --git a/base/threading/thread_restrictions.h b/base/threading/thread_restrictions.h
index b53745d..a1bb1a4 100644
--- a/base/threading/thread_restrictions.h
+++ b/base/threading/thread_restrictions.h
@@ -391,9 +391,11 @@
#if BUILDFLAG(IS_MAC)
class PrintBackendServiceImpl;
#endif
+class MetafilePlayer;
class PrintBackendServiceManager;
class PrintPreviewUIUntrusted;
class PrinterQuery;
+void DumpMetafileIfDebugEnabled(const std::u16string&, const MetafilePlayer*);
base::FilePath GetAbsoluteSystemDestinationLocation(const base::FilePath&);
} // namespace printing
namespace proxy_resolver {
@@ -691,6 +693,9 @@
base::FilePath* file_path); // http://crbug.com/110709
friend bool disk_cache::CleanupDirectorySync(const base::FilePath&);
friend bool gl::init::InitializeStaticGLBindings(gl::GLImplementationParts);
+ friend void printing::DumpMetafileIfDebugEnabled(
+ const std::u16string&,
+ const printing::MetafilePlayer*);
friend base::FilePath printing::GetAbsoluteSystemDestinationLocation(
const base::FilePath&);
diff --git a/chrome/browser/printing/print_job_worker.cc b/chrome/browser/printing/print_job_worker.cc
index 403e4060..271d8e44 100644
--- a/chrome/browser/printing/print_job_worker.cc
+++ b/chrome/browser/printing/print_job_worker.cc
@@ -180,7 +180,7 @@
#endif // BUILDFLAG(IS_WIN)
if (do_spool_document) {
- if (!document_->GetMetafile()) {
+ if (!document_->HasDocument()) {
PostWaitForPage();
return;
}
diff --git a/chrome/browser/printing/print_job_worker_oop.cc b/chrome/browser/printing/print_job_worker_oop.cc
index b4be18a4..99a8c91 100644
--- a/chrome/browser/printing/print_job_worker_oop.cc
+++ b/chrome/browser/printing/print_job_worker_oop.cc
@@ -285,11 +285,8 @@
bool PrintJobWorkerOop::SpoolDocument() {
DCHECK(task_runner()->RunsTasksInCurrentSequence());
- const MetafilePlayer* metafile = document()->GetMetafile();
- DCHECK(metafile);
- base::MappedReadOnlyRegion region_mapping =
- metafile->GetDataAsSharedMemoryRegion();
- if (simulate_spooling_memory_errors_ || !region_mapping.IsValid()) {
+ PrintedDocument::DocumentData document_data = document()->GetDocumentData();
+ if (simulate_spooling_memory_errors_ || !document_data.data.IsValid()) {
PRINTER_LOG(ERROR)
<< "Spooling document via service failed due to shared memory error.";
content::GetUIThreadTaskRunner({})->PostTask(
@@ -303,8 +300,8 @@
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&PrintJobWorkerOop::SendRenderPrintedDocument,
- ui_weak_factory_.GetWeakPtr(), metafile->GetDataType(),
- std::move(region_mapping.region)));
+ ui_weak_factory_.GetWeakPtr(), document_data.data_type,
+ std::move(document_data.data)));
return true;
}
diff --git a/printing/printed_document.cc b/printing/printed_document.cc
index 9467b19..546c55c 100644
--- a/printing/printed_document.cc
+++ b/printing/printed_document.cc
@@ -23,6 +23,7 @@
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
+#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
@@ -62,15 +63,21 @@
}
#endif // BUILDFLAG(IS_WIN)
-void DebugDumpTask(const std::u16string& doc_name,
- const MetafilePlayer* metafile) {
- DCHECK(PrintedDocument::HasDebugDumpPath());
+} // namespace
+
+// Not in an anonymous namespace so it can be friends with
+// base::ScopedAllowBlocking.
+void DumpMetafileIfDebugEnabled(const std::u16string& doc_name,
+ const MetafilePlayer* metafile) {
+ if (!PrintedDocument::HasDebugDumpPath()) {
+ return;
+ }
static constexpr base::FilePath::CharType kExtension[] =
FILE_PATH_LITERAL(".pdf");
-
- std::u16string name = doc_name;
- base::FilePath path = PrintedDocument::CreateDebugDumpPath(name, kExtension);
+ base::ScopedAllowBlocking allow_blocking;
+ base::FilePath path =
+ PrintedDocument::CreateDebugDumpPath(doc_name, kExtension);
base::File file(path,
base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
#if BUILDFLAG(IS_ANDROID)
@@ -80,6 +87,8 @@
#endif // BUILDFLAG(IS_ANDROID)
}
+namespace {
+
void DebugDumpDataTask(const std::u16string& doc_name,
const base::FilePath::StringType& extension,
const base::RefCountedMemory* data) {
@@ -170,27 +179,30 @@
#endif // BUILDFLAG(IS_WIN)
void PrintedDocument::SetDocument(std::unique_ptr<MetafilePlayer> metafile) {
- {
- base::AutoLock lock(lock_);
- mutable_.metafile_ = std::move(metafile);
- }
+ DumpMetafileIfDebugEnabled(name(), metafile.get());
- if (HasDebugDumpPath()) {
- base::ThreadPool::PostTask(
- FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
- base::BindOnce(&DebugDumpTask, name(), mutable_.metafile_.get()));
- }
+ base::AutoLock lock(lock_);
+ mutable_.metafile_ = std::move(metafile);
}
-const MetafilePlayer* PrintedDocument::GetMetafile() {
- return mutable_.metafile_.get();
+bool PrintedDocument::HasDocument() const {
+ base::AutoLock lock(lock_);
+ return !!mutable_.metafile_;
+}
+
+PrintedDocument::DocumentData PrintedDocument::GetDocumentData() const {
+ base::AutoLock lock(lock_);
+ base::MappedReadOnlyRegion region_mapping =
+ mutable_.metafile_->GetDataAsSharedMemoryRegion();
+ return {std::move(region_mapping.region), mutable_.metafile_->GetDataType()};
}
mojom::ResultCode PrintedDocument::RenderPrintedDocument(
PrintingContext* context) {
base::AutoLock lock(lock_);
- mojom::ResultCode result = context->PrintDocument(
- *GetMetafile(), *immutable_.settings_, mutable_.expected_page_count_);
+ mojom::ResultCode result =
+ context->PrintDocument(*mutable_.metafile_, *immutable_.settings_,
+ mutable_.expected_page_count_);
if (result != mojom::ResultCode::kSuccess)
return result;
diff --git a/printing/printed_document.h b/printing/printed_document.h
index 5c9b6b70..4b4498bf 100644
--- a/printing/printed_document.h
Original Bug Report
Unlocked MetafilePlayer access in OOP SpoolDocument leads to heap-use-after-free in the browser process
Unlocked MetafilePlayer access in OOP SpoolDocument leads to heap-use-after-free in the browser process
Summary
On Linux and macOS, where out-of-process (OOP) printing is enabled by default, the browser-process worker thread in PrintJobWorkerOop::SpoolDocument reads a MetafilePlayer pointer from PrintedDocument without holding the required lock. A concurrent call to SetDocument on the UI thread can replace and destroy that MetafilePlayer while the worker thread is still using it, resulting in a heap-use-after-free. A compromised renderer can trigger this race by sending a DidPrintDocument Mojo message during the window between GetMetafile and the subsequent dereference. The crash occurs in the browser process.
Bisect
Introducing Commit: 311052a7201fcffe5b820f950b5446fe1dee1235
- Date: 2022-04-08
- Author: Alan Screen
- Review: https://chromium-review.googlesource.com/q/311052a7201fcffe5b820f950b5446fe1dee1235
Root Cause
PrintedDocument guards its mutable state with a lock and documents this requirement in the header:
// printing/printed_document.h:73-75
// Retrieves the metafile with the data to print. Lock must be held when
// calling this function
const MetafilePlayer* GetMetafile();
The implementation of GetMetafile returns the raw pointer without any locking of its own:
// printing/printed_document.cc:185-187
const MetafilePlayer* PrintedDocument::GetMetafile() {
return mutable_.metafile_.get();
}
SetDocument replaces the metafile under the lock, destroying the previous one:
// printing/printed_document.cc:172-176
void PrintedDocument::SetDocument(std::unique_ptr<MetafilePlayer> metafile) {
{
base::AutoLock lock(lock_);
mutable_.metafile_ = std::move(metafile);
}
...
}
The OOP printing worker violates the locking contract. PrintJobWorkerOop::SpoolDocument runs on a dedicated Printing_Worker thread and calls GetMetafile without holding the lock, then proceeds to call virtual methods on the returned pointer:
// chrome/browser/printing/print_job_worker_oop.cc:285-296
bool PrintJobWorkerOop::SpoolDocument() {
DCHECK(task_runner()->RunsTasksInCurrentSequence());
const MetafilePlayer* metafile = document()->GetMetafile();
DCHECK(metafile);
base::MappedReadOnlyRegion region_mapping =
metafile->GetDataAsSharedMemoryRegion();
...
}
Between the call to GetMetafile and the dereference of the returned pointer, the UI thread is free to process an incoming DidPrintDocument Mojo message from the renderer. That handler reaches PrintDocument, which calls SetDocument with a new metafile. SetDocument acquires the lock and moves a new unique_ptr into mutable_.metafile_, destroying the old MetafilePlayer that the worker thread still references.
// chrome/browser/printing/print_view_manager_base.cc:295-300
std::unique_ptr<MetafileSkia> metafile = std::make_unique<MetafileSkia>();
CHECK(metafile->InitFromData(*print_data));
PrintedDocument* document = print_job_->document();
document->SetDocument(std::move(metafile));
The DidPrintDocument handler does not enforce single-use semantics. It only checks that the document cookie matches the active print job:
// chrome/browser/printing/print_view_manager_base.cc:590-596
void PrintViewManagerBase::DidPrintDocument(
mojom::DidPrintDocumentParamsPtr params,
DidPrintDocumentCallback callback) {
if (!PrintJobHasDocument(params->document_cookie)) {
OnDidPrintDocument(std::move(callback), /*succeeded=*/false);
return;
}
...
A compromised renderer can therefore call DidPrintDocument repeatedly with a matching cookie, each call replacing the metafile. OOP printing is enabled by default on Linux and macOS via the kEnableOopPrintDrivers feature flag.
Reproduce
Note: this is a race condition that requires source patches and a configured CUPS printer to reproduce. Please do not use ClusterFuzz for validation; follow the manual steps below instead. Testing is recommended on Linux, where OOP printing and CUPS integration are most mature. On macOS the code path appears structurally identical, but we were unable to trigger the race despite the same patches and printer configuration.
Tested at commit 7c89d33808e551aed6122c1f324864784011c158 on Linux x86_64 with an ASAN build (is_asan = true, is_debug = false). A CUPS printer must be configured; here we use a cups-pdf virtual printer for verification (sudo apt install cups-pdf).
Apply the attached patch.diff. The patch inserts a Sleep(1s) on the browser side between GetMetafile and the dereference to stabilize the race timing. The renderer side repeatedly sends DidPrintDocument every 500ms starting from when the print-ready document is finalized.
cd ~/chromium/src
git apply issue_metafile_toctou/patch.diff
autoninja -C out/asan-release chrome
Launch:
ASAN_OPTIONS=detect_odr_violation=0 out/asan-release/chrome \
--kiosk-printing \
--user-data-dir=/tmp/poc-$(date +%s) \
file://$(pwd)/issue_metafile_toctou/poc.html
The page calls window.print() after two seconds. The --kiosk-printing flag makes the print preview auto-accept without user interaction; it is not required for the vulnerability itself and can be omitted, in which case the user must manually select a system printer and click Print in the preview dialog. The browser process crashes with a heap-use-after-free within roughly ten seconds.
==4164388==ERROR: AddressSanitizer: heap-use-after-free on address 0x7cb1217e6c40 at pc 0x55fdefce502b bp 0x7b70f2bd4210 sp 0x7b70f2bd4208
READ of size 8 at 0x7cb1217e6c40 thread T35 (Printing_Worker)
#0 0x55fdefce502a in printing::PrintJobWorkerOop::SpoolDocument() chrome/browser/printing/print_job_worker_oop.cc:293:17
#1 0x55fdefc9591a in printing::PrintJobWorker::OnNewPage() chrome/browser/printing/print_job_worker.cc:180:10
#2 0x55fdefce6694 in base::internal::Invoker<...>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
#3 0x7f71a2560622 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
...
0x7cb1217e6c40 is located 0 bytes inside of 408-byte region [0x7cb1217e6c40,0x7cb1217e6dd8)
freed by thread T0 (chrome) here:
#0 0x55fde7c9c062 in operator delete(void*, unsigned long)
#1 0x7f71765ec634 in printing::PrintedDocument::SetDocument(std::__Cr::unique_ptr<printing::MetafilePlayer, std::__Cr::default_delete<printing::MetafilePlayer>>) gen/third_party/libc++/src/include/__memory/unique_ptr.h:74:5
#2 0x55fdefcf8962 in printing::PrintViewManagerBase::PrintDocument(scoped_refptr<base::RefCountedMemory>, gfx::Size const&, gfx::Rect const&, gfx::Point const&) chrome/browser/printing/print_view_manager_base.cc:300:13
#3 0x55fdefcfc219 in printing::PrintViewManagerBase::OnComposePdfDoneImpl(...) chrome/browser/printing/print_view_manager_base.cc:562:3
#4 0x55fdefcfc4fa in printing::PrintViewManagerBase::OnComposeDocumentDone(...) chrome/browser/printing/print_view_manager_base.cc:577:7
...
previously allocated by thread T0 (chrome) here:
#0 0x55fde7c9b45d in operator new(unsigned long)
#1 0x55fdefcf885d in printing::PrintViewManagerBase::PrintDocument(...) gen/third_party/libc++/src/include/__memory/unique_ptr.h:756:26
...
SUMMARY: AddressSanitizer: heap-use-after-free chrome/browser/printing/print_job_worker_oop.cc:293:17 in printing::PrintJobWorkerOop::SpoolDocument()
The complete ASAN log is in issue_metafile_toctou/asan.log.
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.