Critical watch CVSS 8.3 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebView
DescriptionInappropriate implementation in WebView
ComponentWebView
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516947912
Fix commit635f4536b519 (chromium/src) +186/-4
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-06-08 (public git)
Disclosed2026-06-16
Patch gap8 days — fix public in source before the advisory

Background

`AwPrintManager`
the Android WebView browser-process object (android_webview/browser/aw_print_manager.cc) that receives print IPCs from the renderer and writes the rendered PDF into a file descriptor supplied by the embedding application.
`fd_`
the member file descriptor “into which the PDF of the document will be written,” previously initialized to -1 and now to base::kInvalidFd.
`DidPrintDocument`
the printing::mojom::PrintManagerHost handler, invoked in response to a renderer message, that validates the print request and posts SaveDataToFd to write the document bytes to fd_.
`std::exchange`
a C++ operation that atomically reads a variable’s old value and stores a new one, used here to take the fd and invalidate the member in a single step.

Root Cause Analysis

AwPrintManager::DidPrintDocument read the member fd_ directly and bound it into the SaveDataToFd task, while the only place that invalidated fd_ was AwPrintManager::PdfWritingDone, whose removed comment stated “Invalidate the file descriptor so it doesn’t get reused.” Because invalidation happened after writing completed rather than at the moment the fd was consumed, a second DidPrintDocument (a renderer-driven mojo message) could observe the same still-valid fd_ and use it again — the exact “fd reuse” the fix is titled to prevent. The invariant that a print fd is consumed exactly once was thus not enforced at the consumption site.

The fix exchanges fd_ for base::kInvalidFd at the top of DidPrintDocument (int print_fd = std::exchange(fd_, base::kInvalidFd);), returns early via PdfWritingDone(0) and callback.Run(false) when print_fd == base::kInvalidFd, and passes the captured local print_fd (not the member) to SaveDataToFd. PdfWritingDone is correspondingly hardened from assigning fd_ = base::kInvalidFd to asserting CHECK_EQ(fd_, base::kInvalidFd), so the member is guaranteed already cleared by the time writing is reported done.

Key insight
The single mistake was invalidating the print fd_ only after the write finished instead of at the moment it was consumed, leaving a window in which a repeated DidPrintDocument could reuse the same descriptor; the fix atomically takes and invalidates fd_ on entry with std::exchange and rejects any call that finds it already invalid.

Attack Path

  1. Initiate a WebView print An embedding-app print operation hands AwPrintManager a valid file descriptor stored in fd_, arming the print pipeline for a DidPrintDocument call.
  2. Replay the print IPC A compromised or malicious renderer sends the DidPrintDocument mojo message more than once for the same job; pre-patch, each call read the member fd_ directly and bound it into base::BindOnce(&SaveDataToFd, fd_, number_pages(), data) without first invalidating it.
  3. Reuse the descriptor Because fd_ was cleared only later in PdfWritingDone, a subsequent DidPrintDocument observes the same still-valid fd_ and consumes it again, so the descriptor is used more than once (the condition the added std::exchange/early-return now blocks).
  4. Operate on an unintended file Reusing a descriptor that may have been closed and recycled to a different resource lets print data be written to, or the fd otherwise acted upon, a descriptor no longer belonging to the original print target. (inferred — the diff shows only the reuse condition, not a concrete downstream write target or corruption sink.)

Impact Assessment

An attacker driving the renderer can cause the browser-process AwPrintManager to consume the same print file descriptor more than once, an inappropriate-implementation/logic error in WebView per the advisory. The precondition is an in-progress WebView print job whose DidPrintDocument handler can be reached repeatedly before the fd is invalidated. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. The advisory does not report exploited-in-the-wild status or a severity rating here.

Changed Functions

FunctionChangeNotes
if
android_webview/browser/aw_print_manager.cc
modified
PrintManagerPeer
android_webview/browser/aw_print_manager_unittest.cc
modified
AwPrintManagerTest
android_webview/browser/aw_print_manager_unittest.cc
modified
TEST_F
android_webview/browser/aw_print_manager_unittest.cc
modified
BindLambdaForTesting
android_webview/browser/aw_print_manager_unittest.cc
modified

Files Changed

  • android_webview/browser/aw_print_manager.cc
  • android_webview/browser/aw_print_manager.h
  • android_webview/browser/aw_print_manager_unittest.cc

Audit Directions

  • Consume-once resources cleared late
    Look for other IPC handlers (especially browser-process mojom host methods) that read a member handle/fd/pointer, post asynchronous work with it, and only null or invalidate the member in a completion callback; take-and-invalidate atomically at the consumption site with std::exchange instead.
  • Renderer-repeatable mojo messages
    Audit PrintManagerHost and sibling interfaces for handlers that assume a message arrives once per job but lack a guard rejecting duplicate or out-of-order calls from a compromised renderer.
  • Sentinel inconsistency
    Flag file-descriptor fields initialized or reset with raw -1 rather than base::kInvalidFd, since mismatched sentinels can defeat validity checks and mask reuse.
From 635f4536b519a7662d279afa47fe74cd8015d02e Mon Sep 17 00:00:00 2001
From: Peter Pakkenberg <pbirk@google.com>
Date: Mon, 08 Jun 2026 02:28:33 -0700
Subject: [PATCH] Prevent fd reuse in WebView printing

Fixed: 516947912
Change-Id: I737d9a5babddedc5add93a9fa6ccc78488df5c7b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902413
Reviewed-by: Nate Fischer <ntfschr@chromium.org>
Auto-Submit: Peter Pakkenberg <pbirk@chromium.org>
Commit-Queue: Peter Pakkenberg <pbirk@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1643054}
---

diff --git a/android_webview/browser/aw_print_manager.cc b/android_webview/browser/aw_print_manager.cc
index 873f779..5d46b93d 100644
--- a/android_webview/browser/aw_print_manager.cc
+++ b/android_webview/browser/aw_print_manager.cc
@@ -62,8 +62,8 @@
 }
 
 void AwPrintManager::PdfWritingDone(int page_count) {
-  // Invalidate the file descriptor so it doesn't get reused.
-  fd_ = base::kInvalidFd;
+  // The fd_ should have been reset when printing started.
+  CHECK_EQ(fd_, base::kInvalidFd);
   // 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.
@@ -145,6 +145,16 @@
 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);
+
+  if (print_fd == base::kInvalidFd) {
+    PdfWritingDone(0);
+    std::move(callback).Run(false);
+    return;
+  }
+
   if (params->document_cookie != cookie()) {
     PdfWritingDone(0);
     std::move(callback).Run(false);
@@ -176,7 +186,8 @@
       {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
        base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})
       ->PostTaskAndReplyWithResult(
-          FROM_HERE, base::BindOnce(&SaveDataToFd, fd_, number_pages(), data),
+          FROM_HERE,
+          base::BindOnce(&SaveDataToFd, 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 bdcd853..e0f1ff8 100644
--- a/android_webview/browser/aw_print_manager.h
+++ b/android_webview/browser/aw_print_manager.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "base/file_descriptor_posix.h"
 #include "base/memory/weak_ptr.h"
 #include "components/printing/browser/print_manager.h"
 #include "components/printing/common/print.mojom-forward.h"
@@ -58,7 +59,7 @@
   std::unique_ptr<printing::PrintSettings> settings_;
 
   // The file descriptor into which the PDF of the document will be written.
-  int fd_ = -1;
+  int fd_ = base::kInvalidFd;
 
   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
new file mode 100644
index 0000000..6b729c8
--- /dev/null
+++ b/android_webview/browser/aw_print_manager_unittest.cc
@@ -0,0 +1,165 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "android_webview/browser/aw_print_manager.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/task_environment.h"
+#include "components/printing/common/print.mojom.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_content_client_initializer.h"
+#include "content/public/test/web_contents_tester.h"
+#include "printing/print_settings.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace android_webview {
+
+class PrintManagerPeer : public printing::PrintManager {
+ public:
+  static int GetCookie(printing::PrintManager* manager) {
+    return static_cast<PrintManagerPeer*>(manager)->cookie();
+  }
+};
+
+class AwPrintManagerTest : public testing::Test {
+ public:
+  AwPrintManagerTest() = default;
+  ~AwPrintManagerTest() override = default;
+
+  void SetUp() override {
+    test_content_client_initializer_ =
+        std::make_unique<content::TestContentClientInitializer>();
+    browser_context_ = std::make_unique<content::TestBrowserContext>();
+    web_contents_ = content::WebContentsTester::CreateTestWebContents(
+        browser_context_.get(), nullptr);
+    ASSERT_TRUE(web_contents_)
+        << "WebContentsTester::CreateTestWebContents returned null!";
+    AwPrintManager::CreateForWebContents(web_contents_.get());
+  }
+
+  void TearDown() override {
+    web_contents_.reset();
+    browser_context_.reset();
+    test_content_client_initializer_.reset();
+  }
+
+  content::WebContents* web_contents() { return web_contents_.get(); }
+
+ protected:
+  content::BrowserTaskEnvironment task_environment_;
+
+ private:
+  std::unique_ptr<content::TestContentClientInitializer>
+      test_content_client_initializer_;
+  std::unique_ptr<content::TestBrowserContext> browser_context_;
+  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();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/android_webview/browser/aw_print_manager_unittest.cc b/android_webview/browser/aw_print_manager_unittest.cc
new file mode 100644
index 0000000..6b729c8
--- /dev/null
+++ b/android_webview/browser/aw_print_manager_unittest.cc
@@ -0,0 +1,165 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "android_webview/browser/aw_print_manager.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/task_environment.h"
+#include "components/printing/common/print.mojom.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_task_environment.h"
+#include "content/public/test/test_browser_context.h"
+#include "content/public/test/test_content_client_initializer.h"
+#include "content/public/test/web_contents_tester.h"
+#include "printing/print_settings.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace android_webview {
+
+class PrintManagerPeer : public printing::PrintManager {
+ public:
+  static int GetCookie(printing::PrintManager* manager) {
+    return static_cast<PrintManagerPeer*>(manager)->cookie();
+  }
+};
+
+class AwPrintManagerTest : public testing::Test {
+ public:
+  AwPrintManagerTest() = default;
+  ~AwPrintManagerTest() override = default;
+
+  void SetUp() override {
+    test_content_client_initializer_ =
+        std::make_unique<content::TestContentClientInitializer>();
+    browser_context_ = std::make_unique<content::TestBrowserContext>();
+    web_contents_ = content::WebContentsTester::CreateTestWebContents(
+        browser_context_.get(), nullptr);
+    ASSERT_TRUE(web_contents_)
+        << "WebContentsTester::CreateTestWebContents returned null!";
+    AwPrintManager::CreateForWebContents(web_contents_.get());
+  }
+
+  void TearDown() override {
+    web_contents_.reset();
+    browser_context_.reset();
+    test_content_client_initializer_.reset();
+  }
+
+  content::WebContents* web_contents() { return web_contents_.get(); }
+
+ protected:
+  content::BrowserTaskEnvironment task_environment_;
+
+ private:
+  std::unique_ptr<content::TestContentClientInitializer>
+      test_content_client_initializer_;
+  std::unique_ptr<content::TestBrowserContext> browser_context_;
+  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();
+
+  auto settings = std::make_unique<printing::PrintSettings>();
+
+  bool callback_called = false;
+  print_manager->UpdateParam(
+      std::move(settings), valid_fd,
+      base::BindLambdaForTesting([&callback_called](int page_count) {
+        callback_called = true;
+        EXPECT_EQ(1, page_count);
+      }));
+
+  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 print data";
+  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);
+
+  base::RunLoop run_loop;
+  bool did_print_callback_called = false;
+
+  host->DidPrintDocument(
+      std::move(params),
+      base::BindLambdaForTesting(
+          [&did_print_callback_called,
+           quit_closure = run_loop.QuitClosure()](bool success) mutable {
+            did_print_callback_called = true;
+            EXPECT_TRUE(success);
+            std::move(quit_closure).Run();
+          }));
+
+  run_loop.Run();
+
+  EXPECT_TRUE(callback_called);
+  EXPECT_TRUE(did_print_callback_called);
+}
+
+}  // namespace android_webview
diff --git a/android_webview/test/BUILD.gn b/android_webview/test/BUILD.gn
index 8e9d4ad0..a5ccaec3fd 100644
--- a/android_webview/test/BUILD.gn
+++ b/android_webview/test/BUILD.gn
@@ -798,6 +798,8 @@
     "//components/policy/core/browser:test_support",
     "//components/prefs",
     "//components/prefs:test_support",
+    "//components/printing/common",
+    "//components/printing/common:mojo_interfaces",
     "//components/safe_browsing/content/browser/web_ui",
     "//components/safe_browsing/core/browser",
     "//components/safe_browsing/core/common",
@@ -812,6 +814,8 @@
     "//net",
     "//net:test_support",
     "//net/third_party/quiche:blind_sign_auth",
+    "//printing",
+    "//printing:settings",
     "//services/cert_verifier/public/mojom",
     "//services/network:test_support",
     "//services/tracing/public/cpp/background_tracing",
@@ -839,6 +843,7 @@
     "../browser/aw_origin_matched_header_unittest.cc",
     "../browser/aw_pac_processor_unittest.cc",
     "../browser/aw_permission_manager_unittest.cc",
+    "../browser/aw_print_manager_unittest.cc",
     "../browser/aw_user_agent_metadata_unittest.cc",
     "../browser/content_restriction/aw_content_restriction_url_loader_throttle_unittest.cc",
     "../browser/cookie_manager_unittest.cc",
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Write-After-Close via recycled File Descriptor in 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 compromised renderer can potentially trigger a write-after-close vulnerability in the unsandboxed WebView browser process by pipelining multiple Mojo DidPrintDocument requests. This occurs because the raw file descriptor is captured by value into background tasks without being immediately invalidated or guarded against concurrent execution. If the first task completes and the descriptor is closed and recycled, subsequent tasks will write attacker-controlled bytes to the reassigned descriptor.

Affected files:

  • android_webview/browser/aw_print_manager.cc
  • android_webview/browser/aw_print_manager.h

Estimated timestamp from git blame: 2020-12-03

Potential Security Vulnerability in android_webview

There is a potential Use-After-Close (Write-After-Free) vulnerability involving file descriptors inside android_webview/browser/aw_print_manager.cc and android_webview/browser/aw_print_manager.h that can be triggered by a compromised renderer.

Root Cause Analysis

In AwPrintManager, the file descriptor for writing the PDF document is tracked as a raw, borrowed integer (int fd_):

// android_webview/browser/aw_print_manager.h
int fd_ = -1;

When DidPrintDocument is invoked over Mojo, the file descriptor is captured by value into a base::ThreadPool task with BEST_EFFORT priority:

// android_webview/browser/aw_print_manager.cc
void AwPrintManager::DidPrintDocument(
    printing::mojom::DidPrintDocumentParamsPtr params,
    DidPrintDocumentCallback callback) {
  ...
  base::ThreadPool::CreateTaskRunner(
      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})
      ->PostTaskAndReplyWithResult(
          FROM_HERE, base::BindOnce(&SaveDataToFd, fd_, number_pages(), data),
          base::BindOnce(&AwPrintManager::OnDidPrintDocumentWritingDone,
                         weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

There are no concurrent-execution or in-flight guards in DidPrintDocument to prevent multiple requests. Additionally, the tracking variable fd_ is only reset to -1 within PdfWritingDone, which is invoked asynchronously via OnDidPrintDocumentWritingDone on the UI thread after a background worker thread has finished writing:

void AwPrintManager::OnDidPrintDocumentWritingDone(...) {
  PdfWritingDone(base::checked_cast<int>(page_count));
  ...
}

void AwPrintManager::PdfWritingDone(int page_count) {
  fd_ = base::kInvalidFd; // Reset happens after worker completes and triggers callback
  ...
}

Because the DidPrintDocument Mojo interface allows pipelining, a compromised renderer can issue multiple sequential DidPrintDocument messages with the same document cookie before the first task completes. Each duplicate request will pass the cookie check and capture the identical file descriptor fd_ (value N) by value into separate, queued ThreadPool worker tasks.

Potential Step-by-Step Exploitation Scenario

(Please note: These are theoretical steps; our tooling cannot run or execute proof-of-concept code.)

  1. Initiation: The user or host app starts a Print or “Save as PDF” action. AwPrintManager::UpdateParam sets fd_ to a valid system file descriptor N and creates a document cookie.
  2. Pipelining: A compromised renderer acquires the active document cookie and pipelines multiple DidPrintDocument Mojo calls back-to-back.
  3. Task Queueing: Two separate tasks are queued in the background ThreadPool, both capturing the raw file descriptor N by value:
    • Task 1: Bound to write Payload A to N.
    • Task 2: Bound to write Payload B (malicious payload) to N.
  4. Task 1 Completion & FD Close: Task 1 executes, writes Payload A, and returns. The UI thread runs PdfWritingDone, setting fd_ to base::kInvalidFd and triggering the Java completion callback. The Android print framework then closes the underlying system file descriptor N.
  5. FD Grooming: Since Task 2 has BEST_EFFORT priority, the attacker can stall it (e.g., by saturating worker threads) and simultaneously execute browser-side operations from the renderer (such as opening WebSQL/IndexedDB databases, establishing new Mojo data pipes, or opening network sockets) to allocate a new file descriptor. The OS kernel will reassign the newly freed descriptor slot N to one of these sensitive resources.
  6. Write-After-Close (Task 2 Execution): Task 2 finally executes, running SaveDataToFd(N, ..., Payload_B). It executes base::WriteFileDescriptor(N, ...) and writes attacker-controlled bytes directly into the reassigned browser-side resource, resulting in sandbox escape and arbitrary code execution within the unsandboxed WebView browser process.

Suggested Fix

To prevent duplicate or pipelined requests from accessing the same descriptor, the file descriptor should be invalidated immediately upon posting the first writing task, or we should track whether a task is in flight. For example:

void AwPrintManager::DidPrintDocument(
    printing::mojom::DidPrintDocumentParamsPtr params,
    DidPrintDocumentCallback callback) {
  if (params->document_cookie != cookie() || fd_ == base::kInvalidFd) {
    PdfWritingDone(0);
    std::move(callback).Run(false);
    return;
  }
  
  ...
  
  int fd_to_use = fd_;
  fd_ = base::kInvalidFd; // Invalidate immediately to prevent concurrent usage

  base::ThreadPool::CreateTaskRunner(...)
      ->PostTaskAndReplyWithResult(
          FROM_HERE, base::BindOnce(&SaveDataToFd, fd_to_use, number_pages(), data),
          ...);
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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.

View on issue tracker