CVE-2026-17722
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifandroid_webview/browser/aw_print_manager.cc |
modified |
Files Changed
android_webview/browser/aw_pdf_exporter.ccandroid_webview/browser/aw_print_manager.ccandroid_webview/browser/aw_print_manager.handroid_webview/browser/aw_print_manager_unittest.cc
Patch
From 358b51690b9b26aad31363361472dba6e7b7c19c Mon Sep 17 00:00:00 2001 From: Sayed <[email protected]> Date: Thu, 18 Jun 2026 04:49:57 -0700 Subject: [PATCH] [AW] Manage WebView PDF export file descriptor with base::ScopedFD This CL duplicates the file descriptor passed from the Java layer during WebView PDF export using dup(), and wraps it in a base::ScopedFD. It updates AwPrintManager to accept and take ownership of this ScopedFD, moving it into the background thread pool task that writes the PDF data. This ensures that the file descriptor's lifetime is properly managed by C++ and is not prematurely closed by external events while the background task is still running. Fixed: 523592755 Change-Id: I9bbc4daea6fc69aa68393284ad469051e1ec7af5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7940556 Reviewed-by: Richard Coles <[email protected]> Reviewed-by: Peter Pakkenberg <[email protected]> Commit-Queue: Sayed Elabady <[email protected]> Cr-Commit-Position: refs/heads/main@{#1648947} --- diff --git a/android_webview/browser/aw_pdf_exporter.cc b/android_webview/browser/aw_pdf_exporter.cc index dd1d62ef..ad2aad86e 100644 --- a/android_webview/browser/aw_pdf_exporter.cc +++ b/android_webview/browser/aw_pdf_exporter.cc @@ -11,7 +11,9 @@ #include "android_webview/browser/aw_print_manager.h" #include "base/android/jni_android.h" #include "base/android/jni_array.h" +#include "base/files/scoped_file.h" #include "base/functional/bind.h" +#include "base/logging.h" #include "content/public/browser/browser_thread.h" #include "printing/print_settings.h" #include "printing/units.h" @@ -66,6 +68,11 @@ const JavaRef<jintArray>& pages, const JavaRef<jobject>& cancel_signal) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // Wrap the file descriptor in a ScopedFD to take ownership, since we are + // responsible for closing it when we are done (after Java detached it). + base::ScopedFD scoped_fd(fd); + printing::PageRanges page_ranges; JNI_AwPdfExporter_GetPageRanges(env, pages, &page_ranges); @@ -77,7 +84,8 @@ // Update the parameters of the current print manager. AwPrintManager* print_manager = AwPrintManager::FromWebContents(web_contents_); - print_manager->UpdateParam(CreatePdfSettings(env, obj, page_ranges), fd, + print_manager->UpdateParam(CreatePdfSettings(env, obj, page_ranges), + std::move(scoped_fd), base::BindRepeating(&AwPdfExporter::DidExportPdf, base::Unretained(this))); diff --git a/android_webview/browser/aw_print_manager.cc b/android_webview/browser/aw_print_manager.cc index 6415c94..b916d1a 100644 --- a/android_webview/browser/aw_print_manager.cc +++ b/android_webview/browser/aw_print_manager.cc @@ -10,6 +10,7 @@ #include "base/check_op.h" #include "base/file_descriptor_posix.h" #include "base/files/file_util.h" +#include "base/files/scoped_file.h" #include "base/functional/bind.h" #include "base/logging.h" #include "base/memory/ptr_util.h" @@ -30,14 +31,15 @@ namespace { -uint32_t SaveDataToFd(int fd, +uint32_t SaveDataToFd(base::ScopedFD fd, uint32_t page_count, scoped_refptr<base::RefCountedSharedMemoryMapping> data) { - bool result = fd > base::kInvalidFd && - base::IsValueInRangeForNumericType<int>(data->size()); - if (result) - result = base::WriteFileDescriptor(fd, *data); - return result ? page_count : 0; + bool did_write_successfully = + fd.is_valid() && base::IsValueInRangeForNumericType<int>(data->size()); + if (did_write_successfully) { + did_write_successfully = base::WriteFileDescriptor(fd.get(), *data); + } + return did_write_successfully ? page_count : 0; } } // namespace @@ -71,7 +73,7 @@ void AwPrintManager::PdfWritingDone(int page_count) { // The fd_ should have been reset when printing started. - CHECK_EQ(fd_, base::kInvalidFd); + CHECK(!fd_.is_valid()); // Trigger the callback to notify the embedding application that printing is // done. A non-positive `page_count` value (<=0) will be presented as an error // callback to the application. @@ -106,12 +108,12 @@ void AwPrintManager::UpdateParam( std::unique_ptr<printing::PrintSettings> settings, - int file_descriptor, + base::ScopedFD file_descriptor, PrintManager::PdfWritingDoneCallback callback) { DCHECK(settings); DCHECK(callback); settings_ = std::move(settings); - fd_ = file_descriptor; + fd_ = std::move(file_descriptor); set_pdf_writing_done_callback(std::move(callback)); set_cookie(printing::PrintSettings::NewCookie()); } @@ -153,11 +155,10 @@ void AwPrintManager::DidPrintDocument( printing::mojom::DidPrintDocumentParamsPtr params, DidPrintDocumentCallback callback) { - // Exchange the fd_ with kInvalidFd here to prevent it from being used more - // than once. - int print_fd = std::exchange(fd_, base::kInvalidFd); + // Extract the fd_ here to prevent it from being used more than once. + base::ScopedFD print_fd = std::move(fd_); - if (print_fd == base::kInvalidFd) { + if (!print_fd.is_valid()) { PdfWritingDone(0); std::move(callback).Run(false); return; @@ -195,7 +196,8 @@ base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}) ->PostTaskAndReplyWithResult( FROM_HERE, - base::BindOnce(&SaveDataToFd, print_fd, number_pages(), data), + base::BindOnce(&SaveDataToFd, std::move(print_fd), number_pages(), + data), base::BindOnce(&AwPrintManager::OnDidPrintDocumentWritingDone, weak_ptr_factory_.GetWeakPtr(), std::move(callback))); } diff --git a/android_webview/browser/aw_print_manager.h b/android_webview/browser/aw_print_manager.h index 58111a4..5a4cf11 100644 --- a/android_webview/browser/aw_print_manager.h +++ b/android_webview/browser/aw_print_manager.h @@ -8,6 +8,7 @@ #include <memory> #include "base/file_descriptor_posix.h" +#include "base/files/scoped_file.h" #include "base/memory/weak_ptr.h" #include "components/printing/browser/print_manager.h" #include "components/printing/common/print.mojom-forward.h" @@ -38,7 +39,7 @@ // Updates the parameters for printing. void UpdateParam(std::unique_ptr<printing::PrintSettings> settings, - int file_descriptor, + base::ScopedFD file_descriptor, PdfWritingDoneCallback callback); private: @@ -61,7 +62,7 @@ std::unique_ptr<printing::PrintSettings> settings_; // The file descriptor into which the PDF of the document will be written. - int fd_ = base::kInvalidFd; + base::ScopedFD fd_; WEB_CONTENTS_USER_DATA_KEY_DECL(); diff --git a/android_webview/browser/aw_print_manager_unittest.cc b/android_webview/browser/aw_print_manager_unittest.cc index 6b729c8..4e0930a 100644 --- a/android_webview/browser/aw_print_manager_unittest.cc +++ b/android_webview/browser/aw_print_manager_unittest.cc @@ -4,17 +4,20 @@ #include "android_webview/browser/aw_print_manager.h" +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> + #include <utility> -#include "base/files/file.h" #include "base/files/file_util.h" -#include "base/files/scoped_temp_dir.h" #include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/memory/read_only_shared_memory_region.h" #include "base/memory/scoped_refptr.h" #include "base/run_loop.h" #include "base/test/bind.h" +#include "base/test/run_until.h" #include "base/test/task_environment.h" #include "components/printing/common/print.mojom.h" #include "content/public/browser/web_contents.h" @@ -68,59 +71,20 @@
Regression Test / PoC
diff --git a/android_webview/browser/aw_print_manager_unittest.cc b/android_webview/browser/aw_print_manager_unittest.cc
index 6b729c8..4e0930a 100644
--- a/android_webview/browser/aw_print_manager_unittest.cc
+++ b/android_webview/browser/aw_print_manager_unittest.cc
@@ -4,17 +4,20 @@
#include "android_webview/browser/aw_print_manager.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
#include <utility>
-#include "base/files/file.h"
#include "base/files/file_util.h"
-#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
+#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "components/printing/common/print.mojom.h"
#include "content/public/browser/web_contents.h"
@@ -68,59 +71,20 @@
std::unique_ptr<content::WebContents> web_contents_;
};
-TEST_F(AwPrintManagerTest, FdIsResetOncePrintingStarts) {
- auto* print_manager = AwPrintManager::FromWebContents(web_contents());
- ASSERT_TRUE(print_manager);
-
- int test_fd = 123;
- auto settings = std::make_unique<printing::PrintSettings>();
-
- bool callback_called = false;
- print_manager->UpdateParam(
- std::move(settings), test_fd,
- base::BindLambdaForTesting(
- [&callback_called](int page_count) { callback_called = true; }));
-
- auto params = printing::mojom::DidPrintDocumentParams::New();
- params->document_cookie = 0; // Wrong cookie to fail early
- params->content = printing::mojom::DidPrintContentParams::New();
-
- auto* host = static_cast<printing::mojom::PrintManagerHost*>(print_manager);
-
- bool did_print_callback_called = false;
-
- // We pass a wrong cookie, which causes DidPrintDocument to fail early.
- // DidPrintDocument will call PdfWritingDone(0).
- // Inside PdfWritingDone(), it CHECKs that fd_ is set to base::kInvalidFd.
- // If fd_ was not reset when printing started (i.e. at the beginning of
- // DidPrintDocument), the test will crash.
- host->DidPrintDocument(
- std::move(params),
- base::BindLambdaForTesting([&did_print_callback_called](bool success) {
- did_print_callback_called = true;
- EXPECT_FALSE(success);
- }));
-
- EXPECT_TRUE(callback_called);
- EXPECT_TRUE(did_print_callback_called);
-}
-
TEST_F(AwPrintManagerTest, FdIsResetAfterSuccessfulPrint) {
auto* print_manager = AwPrintManager::FromWebContents(web_contents());
ASSERT_TRUE(print_manager);
- base::ScopedTempDir temp_dir;
- ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
- base::FilePath temp_file;
- ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &temp_file));
- base::File file(temp_file, base::File::FLAG_OPEN | base::File::FLAG_WRITE);
- int valid_fd = file.GetPlatformFile();
+ int fds[2];
+ ASSERT_EQ(0, pipe2(fds, O_CLOEXEC));
+ base::ScopedFD read_fd(fds[0]);
+ base::ScopedFD write_fd(fds[1]);
auto settings = std::make_unique<printing::PrintSettings>();
bool callback_called = false;
print_manager->UpdateParam(
- std::move(settings), valid_fd,
+ std::move(settings), std::move(write_fd),
base::BindLambdaForTesting([&callback_called](int page_count) {
callback_called = true;
EXPECT_EQ(1, page_count);
@@ -160,6 +124,123 @@
EXPECT_TRUE(callback_called);
EXPECT_TRUE(did_print_callback_called);
+
+ // Verify that the data was written to the pipe successfully.
+ std::string read_buf;
+ char buf[128];
+ ssize_t bytes_read;
+ while ((bytes_read = read(read_fd.get(), buf, sizeof(buf))) > 0) {
+ read_buf.append(buf, bytes_read);
+ }
+ EXPECT_EQ(read_buf, std::string(reinterpret_cast<const char*>(test_data),
+ sizeof(test_data)));
+}
+
+TEST_F(AwPrintManagerTest,
+ FdLifecycleManagedByBackgroundTaskEvenIfManagerDestroyed) {
+ auto* print_manager = AwPrintManager::FromWebContents(web_contents());
+ ASSERT_TRUE(print_manager);
+
+ int fds[2];
+ ASSERT_EQ(0, pipe2(fds, O_CLOEXEC));
+ base::ScopedFD read_fd(fds[0]);
+ base::ScopedFD write_fd(fds[1]);
+ ASSERT_TRUE(base::SetNonBlocking(read_fd.get()));
+
+ auto settings = std::make_unique<printing::PrintSettings>();
+
+ bool callback_called = false;
+ print_manager->UpdateParam(
+ std::move(settings), std::move(write_fd),
+ base::BindLambdaForTesting(
+ [&callback_called](int page_count) { callback_called = true; }));
+
+ auto params = printing::mojom::DidPrintDocumentParams::New();
+ params->document_cookie = PrintManagerPeer::GetCookie(print_manager);
+
+ auto content = printing::mojom::DidPrintContentParams::New();
+ const uint8_t test_data[] = "test data for lifecycle test";
+ base::MappedReadOnlyRegion mapped_region =
+ base::ReadOnlySharedMemoryRegion::Create(sizeof(test_data));
+ ASSERT_TRUE(mapped_region.IsValid());
+ mapped_region.mapping.GetMemoryAsSpan<uint8_t>().copy_from(test_data);
+
+ content->metafile_data_region = std::move(mapped_region.region);
+ params->content = std::move(content);
+
+ auto* host = static_cast<printing::mojom::PrintManagerHost*>(print_manager);
+
+ host->DidGetPrintedPagesCount(PrintManagerPeer::GetCookie(print_manager), 1);
+
+ host->DidPrintDocument(std::move(params),
+ base::BindLambdaForTesting([](bool success) {
+ ADD_FAILURE() << "Callback should not run";
+ }));
+
+ // Destroy the print manager before running the background task.
+ TearDown();
+
+ // Since the print manager is destroyed, the completion callback will not run.
+ // We use RunUntil to wait for the background file writing task to complete.
+ // Note: RunUntilIdle() is a banned pattern and cannot be used here.
+ std::string read_buf;
+ EXPECT_TRUE(base::test::RunUntil([&]() {
+ char buf[128];
+ ssize_t bytes_read = read(read_fd.get(), buf, sizeof(buf));
+ if (bytes_read > 0) {
+ read_buf.append(buf, bytes_read);
+ }
+ return read_buf == std::string(reinterpret_cast<const char*>(test_data),
+ sizeof(test_data));
+ }));
+}
+
+// Verifies that when DidPrintDocument fails early (e.g., due to a cookie
+// mismatch), the file descriptor is properly closed. This also implicitly
+// verifies that the print manager resets/extracts the fd_ when printing starts,
+// because PdfWritingDone(0) CHECKs that fd_ is invalid.
+TEST_F(AwPrintManagerTest, FdIsClosedOnCookieMismatch) {
+ auto* print_manager = AwPrintManager::FromWebContents(web_contents());
+ ASSERT_TRUE(print_manager);
+
+ int fds[2];
+ ASSERT_EQ(0, pipe2(fds, O_CLOEXEC));
+ base::ScopedFD read_fd(fds[0]);
+ base::ScopedFD write_fd(fds[1]);
+ ASSERT_TRUE(base::SetNonBlocking(read_fd.get()));
+
+ auto settings = std::make_unique<printing::PrintSettings>();
+
+ bool callback_called = false;
+ print_manager->UpdateParam(
+ std::move(settings), std::move(write_fd),
+ base::BindLambdaForTesting([&callback_called](int page_count) {
+ callback_called = true;
+ EXPECT_EQ(0, page_count);
+ }));
+
+ auto params = printing::mojom::DidPrintDocumentParams::New();
+ params->document_cookie =
+ PrintManagerPeer::GetCookie(print_manager) + 1; // Wrong cookie
+ params->content = printing::mojom::DidPrintContentParams::New();
+
+ auto* host = static_cast<printing::mojom::PrintManagerHost*>(print_manager);
+
+ bool did_print_callback_called = false;
+ host->DidPrintDocument(
+ std::move(params),
+ base::BindLambdaForTesting([&did_print_callback_called](bool success) {
+ did_print_callback_called = true;
+ EXPECT_FALSE(success);
+ }));
+
+ EXPECT_TRUE(callback_called);
+ EXPECT_TRUE(did_print_callback_called);
+
+ // The write_fd should be closed now due to cookie mismatch, meaning a read
+ // on read_fd should return 0 (EOF).
+ char buf;
+ EXPECT_EQ(0, read(read_fd.get(), &buf, 1));
}
} // namespace android_webview
Original Bug Report
Potential Use-After-Close FD in Android WebView AwPrintManager
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A raw file descriptor passed from Java to C++ during WebView PDF export is not duplicated, making its lifecycle dependent on the Android Print framework. A compromised renderer can force the framework to close this file descriptor while a background task holding the raw integer is pending. The background task may subsequently write arbitrary attacker-controlled data to a reallocated, sensitive file descriptor in the browser process.
Affected files:
android_webview/browser/aw_print_manager.ccandroid_webview/browser/aw_print_manager.handroid_webview/java/src/org/chromium/android_webview/AwPdfExporter.javacomponents/printing/browser/print_manager.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential Use-After-Close (UAC) vulnerability exists in the Android WebView PDF export functionality (AwPrintManager). When a host application requests a PDF export, a ParcelFileDescriptor is passed to the native C++ side. However, the native code extracts the raw integer file descriptor and stores it without duplicating it (dup()) and without managing it via a base::ScopedFD.
Because the native side does not take true ownership of the file descriptor, a compromised renderer can manipulate Mojo IPC messages to force a race condition. By triggering an error state that propagates back to the Java layer, the Android framework can be instructed to close the file descriptor while a heavily delayed BEST_EFFORT background task is still waiting to write to it. If the closed file descriptor integer is reallocated to a sensitive resource (like a Mojo IPC socket or Binder FD) before the background task executes, the attacker’s arbitrary PDF data will be written to that resource.
Potential Exploitation Steps
Note: These are suggested steps based on static analysis; our tooling agent does not yet have the ability to run code to verify a full exploit chain.
- Initiation: The Android host application initiates a PDF export (e.g., via the system print dialog). The Java layer (
AwPdfExporter.java) passes the raw integer FD to the nativeAwPdfExporter::ExportToPdf(). - Unsafe Storage: In
AwPrintManager::UpdateParam(), the raw integer is stored infd_. It is not duplicated, leaving its system lifecycle tied to the JavaParcelFileDescriptor. - First Malicious IPC: A compromised renderer intercepts the print request and sends a
PrintManagerHost.DidPrintDocumentMojo IPC to the browser process, providing the correct document cookie and a shared memory region containing a malicious payload. - Task Posting: On the UI thread,
AwPrintManager::DidPrintDocument()securely extractsfd_(setting it tokInvalidFd) and posts aSaveDataToFdtask to aBEST_EFFORTThreadPool. This task captures the raw integer FD by value. - Second Malicious IPC: Immediately after sending
DidPrintDocument, the compromised renderer sends aPrintManagerHost.PrintingFailedMojo IPC on the same channel. - Premature Closure:
PrintingFailedexecutes sequentially on the UI thread. It bypassesCHECK_EQ(fd_, base::kInvalidFd)(becausefd_was cleared in step 4) and callsPdfWritingDone(0). This triggers the Java callbackAwPdfExporter.didExportPdf(0), which setsmFd = nulland notifiesAwPrintDocumentAdapterto callonWriteFailed(null). The Android Print framework then closes the underlying system file descriptor. - Resource Reallocation: The attacker rapidly creates new connections (e.g., WebSockets, Mojo) in the browser process. The Linux kernel assigns the recently freed file descriptor integer to one of these sensitive resources.
- UAC Execution: The delayed
BEST_EFFORTbackground taskSaveDataToFdexecutes, callingbase::WriteFileDescriptorwith the old integer value. The malicious payload is written directly into the newly allocated, sensitive resource.
Impact
Writing arbitrary, attacker-controlled data to a privileged file descriptor (such as a Mojo pipe) in the browser process allows for a direct Sandbox Escape and potential Remote Code Execution (RCE) in the context of the browser process.
Suggested Fix
Modify AwPdfExporter and AwPrintManager to take ownership of the file descriptor using base::ScopedFD. When passing the file descriptor from Java to C++, use dup() to create an independent reference that is not tied to the Java ParcelFileDescriptor lifecycle. This is identical to a fix previously applied to PrintingContextAndroid.
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.