CVE-2026-11647
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifprinting/android/java/src/org/chromium/printing/PrintingControllerImpl.java |
modified |
Files Changed
chrome/android/javatests/src/org/chromium/chrome/browser/printing/PrintingControllerTest.javaprinting/android/java/src/org/chromium/printing/PrintingContext.javaprinting/android/java/src/org/chromium/printing/PrintingController.javaprinting/android/java/src/org/chromium/printing/PrintingControllerImpl.java
Patch
From 9c6640568d45f977ffac2e300aacdeebd484f2ee Mon Sep 17 00:00:00 2001 From: Eric Lok <[email protected]> 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 <[email protected]> Reviewed-by: Tommy Nyquist <[email protected]> Reviewed-by: Lei Zhang <[email protected]> Auto-Submit: Eric Lok <[email protected]> Commit-Queue: Lei Zhang <[email protected]> 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; }
Original Bug Report
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.ccprinting/android/java/src/org/chromium/printing/PrintingControllerImpl.javaprinting/printing_context_android.hchrome/browser/printing/print_view_manager_base.ccprinting/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).
- Open Dialog & Populate FD: The compromised renderer sends a
ScriptedPrintIPC withis_scripted=true. This opens the Android system print dialog. The Android framework callsonWritein Java to generate a preview, which populatesPrintingControllerImpl.mFileDescriptor. The browser replies withkCanceledto unblock the renderer, but the dialog remains open. - Cache the FD in Native: The renderer sends a second
ScriptedPrintIPC, this time withis_scripted=false. Because the dialog is still open, Java reports success. The native code fetches the FD from the activePrintingControllerImpland caches the raw integer in a newPrintingContextAndroidobject. - 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++PrintingContextAndroidstill holds the dangling integer. - 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_. - Arbitrary File Write: The renderer sends
DidPrintDocumentwith 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 viaMetafileSkia::InitFromData, and writes it verbatim to the reclaimedfd_usingbase::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.