Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Printing
DescriptionUse after free in Printing
ComponentPrinting
Bug ClassUAF
Tracker502156940
Fix commit9c6640568d45 (chromium/src) +104/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
modified

Files Changed

  • chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java
  • printing/android/java/src/org/chromium/printing/PrintingContext.java
  • printing/android/java/src/org/chromium/printing/PrintingController.java
  • printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
From 9c6640568d45f977ffac2e300aacdeebd484f2ee Mon Sep 17 00:00:00 2001
From: Eric Lok <lokeric@google.com>
Date: Thu, 07 May 2026 14:22:03 -0700
Subject: [PATCH] android: Fix use-after-close in PrintingContextAndroid

Previously, the file descriptor (FD) was owned by the Java layer, and
the C++ layer only retained a raw integer reference. This created a
potential Use-After-Close (UAC) vulnerability if the print dialog was
dismissed and Java closed the FD before C++ finished writing to it. This
CL addresses the issue by duplicating the FD in the Java layer and
passing ownership of the duplicate to the C++ layer (using
base::ScopedFD). This ensures that C++ layer holds a valid handle to the
file until it completes its write operation, regardless of Java layer's
lifecycle.

As a bit of background, the FD is used for PDF printing.

Bug: 502156940
Test: PrintingControllerTest and manual testing.
Change-Id: If1470200a7351237f35cb3cd0cb7f6f28634bf18
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7778348
Reviewed-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Tommy Nyquist <nyquist@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Auto-Submit: Eric Lok <lokeric@google.com>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1627232}
---

diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java
index 3b879af..f653c4e0 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.java
@@ -390,6 +390,38 @@
     @Test
     @SmallTest
     @Feature({"Printing"})
+    public void testGetFileDescriptorAfterFinish() throws Exception {
+        WebPageStation page = mActivityTestRule.startOnUrl(URL);
+        final PrintingControllerImpl controller = createControllerOnUiThread();
+
+        startControllerOnUiThread(controller, page.getTab());
+
+        try (TemporaryFileHandler handler = new TemporaryFileHandler()) {
+            ThreadUtils.runOnUiThreadBlocking(
+                    () -> {
+                        Assert.assertNull(controller.getParcelFileDescriptor());
+                        controller.onStart();
+
+                        // Simulate onWrite to set the file descriptor
+                        controller.onWrite(
+                                new PageRange[] {PageRange.ALL_PAGES},
+                                handler.getFileDescriptor(),
+                                new CancellationSignal(),
+                                new WriteResultCallbackWrapperMock());
+
+                        // Check that it is now a valid FD (non-null ParcelFileDescriptor)
+                        Assert.assertNotNull(controller.getParcelFileDescriptor());
+
+                        controller.onFinish();
+                        // Verify it goes back to invalid after finish
+                        Assert.assertNull(controller.getParcelFileDescriptor());
+                    });
+        }
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"Printing"})
     public void testTabPrinterCanPrintHiddenTab() {
         WebPageStation page = mActivityTestRule.startOnUrl(URL);
         ChromeTabbedActivity cta = page.getActivity();
diff --git a/printing/android/java/src/org/chromium/printing/PrintingContext.java b/printing/android/java/src/org/chromium/printing/PrintingContext.java
index 288821b0..4c9a75e0 100644
--- a/printing/android/java/src/org/chromium/printing/PrintingContext.java
+++ b/printing/android/java/src/org/chromium/printing/PrintingContext.java
@@ -4,21 +4,27 @@
 
 package org.chromium.printing;
 
+import static org.chromium.printing.PrintingControllerImpl.INVALID_FD;
+
 import android.app.Activity;
+import android.os.ParcelFileDescriptor;
 
 import org.jni_zero.CalledByNative;
 import org.jni_zero.JNINamespace;
 import org.jni_zero.NativeMethods;
 
+import org.chromium.base.Log;
 import org.chromium.base.ThreadUtils;
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
 import org.chromium.ui.base.WindowAndroid;
 
+import java.io.IOException;
+
 /**
- * This class is responsible for communicating with its native counterpart through JNI to handle
- * the generation of PDF.  On the Java side, it works with a {@link PrintingController}
- * to talk to the framework.
+ * This class is responsible for communicating with its native counterpart through JNI to handle the
+ * generation of PDF. On the Java side, it works with a {@link PrintingController} to talk to the
+ * framework.
  */
 @JNINamespace("printing")
 @NullMarked
@@ -42,10 +48,28 @@
         return new PrintingContext(nativeObjectPointer, window);
     }
 
+    /**
+     * Takes a duplicated file descriptor stored in the controller. The caller (typically native
+     * code) takes ownership of the returned file descriptor and is responsible for closing it. This
+     * is done to prevent Use-After-Close issues by ensuring both Java and C++ have their own
+     * independent references to the file.
+     *
+     * @return The duplicated file descriptor, or {@link PrintingControllerImpl#INVALID_FD} if
+     *     failed.
+     */
     @CalledByNative
-    public int getFileDescriptor() {
+    public int takeDuplicatedFileDescriptor() {
         ThreadUtils.assertOnUiThread();
-        return mController.getFileDescriptor();
+        ParcelFileDescriptor pfd = mController.getParcelFileDescriptor();
+        if (pfd == null) return INVALID_FD;
+        try {
+            // Duplicate the file descriptor to pass ownership to C++.
+            // This prevents UAC as C++ holds its own reference.
+            return pfd.dup().detachFd();
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to duplicate file descriptor", e);
+            return INVALID_FD;
+        }
     }
 
     @CalledByNative
diff --git a/printing/android/java/src/org/chromium/printing/PrintingController.java b/printing/android/java/src/org/chromium/printing/PrintingController.java
index 8910f16..c8e61cd 100644
--- a/printing/android/java/src/org/chromium/printing/PrintingController.java
+++ b/printing/android/java/src/org/chromium/printing/PrintingController.java
@@ -4,7 +4,7 @@
 
 package org.chromium.printing;
 
-import android.print.PrintDocumentAdapter;
+import android.os.ParcelFileDescriptor;
 
 import org.chromium.build.annotations.NullMarked;
 import org.chromium.build.annotations.Nullable;
@@ -26,10 +26,9 @@
     int getDpi();
 
     /**
-     * @return The file descriptor number of the file into which Chromium will write the PDF.  This
-     *         is provided to us by {@link PrintDocumentAdapter#onWrite}.
+     * @return The ParcelFileDescriptor of the file into which Chromium will write the PDF.
      */
-    int getFileDescriptor();
+    @Nullable ParcelFileDescriptor getParcelFileDescriptor();
 
     /**
      * @return The media height in mils (thousands of an inch).
diff --git a/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java b/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
index e0f4dc03..47b7e59 100644
--- a/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
+++ b/printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
@@ -82,6 +82,9 @@
 
     private static final int BUFFER_SIZE = 8 * 1024; // 8 KB
 
+    /** Constant for invalid file descriptor- equivalent to base::kInvalidFd (-1) in C++. */
+    public static final int INVALID_FD = -1;
+
     private @Nullable String mErrorMessage;
 
     private int mRenderProcessId;
@@ -205,8 +208,8 @@
     }
 
     @Override
-    public int getFileDescriptor() {
-        return assumeNonNull(mFileDescriptor).getFd();
+    public @Nullable ParcelFileDescriptor getParcelFileDescriptor() {
+        return mFileDescriptor;
     }
 
     @Override
@@ -347,6 +350,11 @@
         // TODO(cimamoglu): Make use of CancellationSignal.
         if (ranges == null || ranges.length == 0) {
             callback.onWriteFailed(null);
+            try {
+                destination.close();
+            } catch (IOException e) {
+                /* ignore */
+            }
             return;
         }
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Sandbox Escape via FD Use-After-Close in PrintingContextAndroid

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 without the Chrome Security team.

Overview: PrintingContextAndroid caches a raw file descriptor from Java but is not notified when the descriptor is closed upon print dialog dismissal. A compromised renderer can exploit this by reclaiming the freed file descriptor integer for a sensitive browser resource. The attacker can then use the printing pipeline to write arbitrary data to the reclaimed descriptor, leading to a potential sandbox escape.

Affected files:

  • printing/printing_context_android.cc
  • printing/android/java/src/org/chromium/printing/PrintingControllerImpl.java
  • printing/printing_context_android.h
  • chrome/browser/printing/print_view_manager_base.cc
  • printing/metafile_skia.cc

Estimated timestamp from git blame: 2021-12-04

Summary

A potential Use-After-Close (UAC) vulnerability exists in the Android printing pipeline within the Chromium browser process. PrintingContextAndroid caches a raw POSIX file descriptor (FD) extracted from a Java-owned ParcelFileDescriptor. When the print dialog is dismissed or the activity is destroyed, Java safely closes the FD. However, the native code is not notified and retains the stale integer. A compromised renderer can cause the browser to reclaim this FD integer for a sensitive resource (e.g., a Mojo channel or database) and subsequently write attacker-controlled bytes to it, leading to a sandbox escape.

Root Cause Analysis

In printing/printing_context_android.cc, the method AskUserForSettingsReply caches a raw integer FD from the Java layer:

fd_ = Java_PrintingContext_getFileDescriptor(env, j_printing_context_);

This FD originates from PrintingControllerImpl.java, which holds it in a ParcelFileDescriptor (mFileDescriptor).

Several lifecycle events in PrintingControllerImpl.java—such as onFinish() (called when the print dialog is dismissed) and onActivityDestroyed()—invoke closeFileDescriptor(). This method calls mFileDescriptor.close(), releasing the FD in the Linux kernel.

Critically, none of these paths notify the native PrintingContextAndroid object to invalidate its cached fd_. The C++ object simply retains the dangling integer. Later, PrintingContextAndroid::PrintDocument writes directly to this unvalidated integer via MetafileSkia::SaveToFileDescriptor(fd_) and base::WriteFileDescriptor.

Potential Attack Scenario

The following steps describe how a compromised renderer could theoretically trigger this vulnerability. (Note: These are suggested steps based on static analysis; our tooling agent does not currently have the ability to execute code to verify this with a live Proof of Concept).

  1. Open Dialog & Populate FD: The compromised renderer sends a ScriptedPrint IPC with is_scripted=true. This opens the Android system print dialog. The Android framework calls onWrite in Java to generate a preview, which populates PrintingControllerImpl.mFileDescriptor. The browser replies with kCanceled to unblock the renderer, but the dialog remains open.
  2. Cache the FD in Native: The renderer sends a second ScriptedPrint IPC, this time with is_scripted=false. Because the dialog is still open, Java reports success. The native code fetches the FD from the active PrintingControllerImpl and caches the raw integer in a new PrintingContextAndroid object.
  3. Close the FD: The user dismisses the dialog (or the attacker programmatically triggers an Activity lifecycle change). PrintingControllerImpl.onFinish() closes the kernel FD. The C++ PrintingContextAndroid still holds the dangling integer.
  4. Reclaim the FD: The renderer rapidly sprays FD allocations in the browser process (e.g., by creating many Mojo IPC channels or Blobs). The Linux kernel reuses the lowest available FD integer, causing one of the new, sensitive browser resources to be assigned the same integer value as the cached fd_.
  5. Arbitrary File Write: The renderer sends DidPrintDocument with an arbitrary byte payload in a shared memory region. The browser bypasses PDF compositing (which happens by default on low-memory Android devices where OOPIF is disabled), reads the attacker’s raw payload via MetafileSkia::InitFromData, and writes it verbatim to the reclaimed fd_ using base::WriteFileDescriptor.

Suggested Fix

Do not cache the raw file descriptor integer in PrintingContextAndroid. Instead, refactor the code to query the ParcelFileDescriptor from Java exactly at the time the write is performed in PrintDocument(). Alternatively, implement a listener or callback mechanism that strictly nullifies or invalidates fd_ in the native layer when PrintingControllerImpl closes the ParcelFileDescriptor.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results 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.

View on issue tracker