Chrome · PictureInPicture
CVE-2026-17999
Logic Error in PictureInPicture
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.javachrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.javachrome/android/junit/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityUnitTest.javachrome/browser/android/BUILD.gnchrome/browser/android/media/document_picture_in_picture_bridge_android.cc
Patch
From 0aa2189d717b991f4bf223659049a83e57985190 Mon Sep 17 00:00:00 2001
From: Phil Yan <philyan@chromium.org>
Date: Mon, 15 Jun 2026 13:10:37 -0700
Subject: [PATCH] [Doc-PiP] Fix Android Document PiP async lifecycle race
An asynchronous startup gap on Android allowed the opener tab to
navigate or close without tearing down the Document PiP window. When a
close request occurred during the Intent startup phase (before the
Java WebContentsDelegate attached), WebContentsImpl::ClosePage()
silently no-oped, while the C++ controller detached from the opener.
Repo example: http://crbug.com/521615681#comment3
This CL fixes the race condition by introducing a two-way handshake:
1. Java registers its Activity with PictureInPictureWindowManager JNI,
passing its WebContents to verify session ownership. If the C++
session was closed while the intent was in flight, startup aborts.
2. If C++ closes the session before the delegate has attached,
it explicitly tells the Java Activity to finish() via JNI.
To keep the platform-specific logic clean, the JNI bridge registers a
PictureInPictureWindowManager::Observer to monitor session exits and
holds a JavaObjectWeakGlobalRef to the Activity, avoiding memory leaks
and keeping the C++ manager completely platform-agnostic.
Bug: 521615681
Change-Id: Ie58c6da77e78546dbec17d134d44ede0f8adad20
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7914193
Reviewed-by: Wenyu Fu <wenyufu@chromium.org>
Commit-Queue: Phil Yan <philyan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647023}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
index f52292c..f4f45e5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
@@ -25,6 +25,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatDelegate;
+import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;
import org.chromium.base.AconfigFlaggedApiDelegate;
@@ -142,6 +143,18 @@
}
mWebContents = webContents;
+
+ // Handshake with native WindowManager. Since Activity startup is async, C++ might have
+ // closed the session while this intent was in-flight. If so, abort to prevent an orphaned
+ // window.
+ boolean isSessionValid =
+ DocumentPictureInPictureActivityJni.get().registerJavaActivity(this, mWebContents);
+ if (!isSessionValid) {
+ Log.e(TAG, "Native PiP session already closed. Aborting startup.");
+ finish();
+ return;
+ }
+
WebContents parentWebContents =
sParentWebContentsForTesting != null
? sParentWebContentsForTesting
@@ -909,12 +922,22 @@
@NativeMethods
public interface Natives {
+ boolean registerJavaActivity(
+ DocumentPictureInPictureActivity activity, WebContents webContents);
+
void onActivityStartForTesting( // IN-TEST
WebContents parentWebContent, WebContents webContents);
void onBackToTab();
}
+ @CalledByNative
+ public void closeActivity() {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
static class DocumentPictureInPictureNightModeStateProvider implements NightModeStateProvider {
public void initialize(AppCompatDelegate delegate) {
delegate.setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
index e3ea963b..3d4b47c 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
@@ -279,6 +279,13 @@
ChromeTabUtils.loadUrlOnUiThread(mTab, dataUrl);
});
+ // Re-establish the native PiP session with the parent WebContents at its new URL.
+ ThreadUtils.runOnUiThreadBlocking(
+ () -> {
+ DocumentPictureInPictureActivity.onActivityStartForTesting(
+ mParentWebContents, mWebContents);
+ });
+
// Launch the PiP activity. Its verifyOpenerOrigin() will check the origin of
// mParentWebContents
// (which is now opaque, serializing to "null") against the intent's initial opener origin
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityUnitTest.java
index 379a2e08..8e071f1 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityUnitTest.java
@@ -11,6 +11,7 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -58,6 +59,7 @@
@Mock private AconfigFlaggedApiDelegate mAconfigFlaggedApiDelegate;
@Mock private AppTask mAppTask;
@Mock private DisplayAndroidManager mDisplayAndroidManager;
+ @Mock private DocumentPictureInPictureActivity.Natives mMockActivityNatives;
private DocumentPictureInPictureActivity mActivity;
@@ -68,6 +70,7 @@
DisplayAndroidManager.setInstanceForTesting(mDisplayAndroidManager);
DisplayAndroid.setNonMultiDisplayForTesting(mDisplayAndroid);
PictureInPictureBoundsCacheBridgeJni.setInstanceForTesting(mMockNatives);
+ DocumentPictureInPictureActivityJni.setInstanceForTesting(mMockActivityNatives);
// Common Display setup
when(mDisplayAndroid.getDipScale()).thenReturn(1.0f);
@@ -98,6 +101,7 @@
AndroidTaskUtils.setAppTaskForTesting(null);
DisplayAndroidManager.resetInstanceForTesting();
DisplayAndroid.setNonMultiDisplayForTesting(null);
+ DocumentPictureInPictureActivityJni.setInstanceForTesting(null);
}
@Test
@@ -309,4 +313,41 @@
// It should NOT cache these bounds.
verifyNoInteractions(mMockNatives);
}
+
+ @Test
+ @Config(sdk = Build.VERSION_CODES.S)
+ public void testPerformPreInflationStartup_AbortsIfSessionInvalid() {
+ // Mock registerJavaActivity to return false (indicating native session closed).
+ when(mMockActivityNatives.registerJavaActivity(any(), any())).thenReturn(false);
+
+ // Setup mock WebContents to prevent crashes during early setup.
+ WebContents webContents = mock(WebContents.class);
+ when(webContents.isDestroyed()).thenReturn(false);
+ DocumentPictureInPictureActivity.setWebContentsForTesting(webContents);
+
+ mActivity.performPreInflationStartup();
+
+ // Verify that performPreInflationStartup aborts early by calling finish().
+ verify(mActivity).finish();
+ }
+
+ @Test
+ @Config(sdk = Build.VERSION_CODES.S)
+ public void testCloseActivity() {
+ doReturn(false).when(mActivity).isFinishing();
+
+ mActivity.closeActivity();
+
+ verify(mActivity).finish();
+ }
+
+ @Test
+ @Config(sdk = Build.VERSION_CODES.S)
+ public void testCloseActivity_DoesNotFinishIfAlreadyFinishing() {
+ doReturn(true).when(mActivity).isFinishing();
+
+ mActivity.closeActivity();
+
+ verify(mActivity, never()).finish();
+ }
}
diff --git a/chrome/browser/android/BUILD.gn b/chrome/browser/android/BUILD.gn
index 0bf3459b7..0d14775 100644
--- a/chrome/browser/android/BUILD.gn
+++ b/chrome/browser/android/BUILD.gn
@@ -268,7 +268,6 @@
"initialize_feature_list_android.cc",
"initialize_feature_list_android.h",
"intent_handler.cc",
- "media/document_picture_in_picture_bridge_android.cc",
"media/media_capture_devices_dispatcher_android.cc",
"media_state_observer.cc",
"media_state_observer.h",
diff --git a/chrome/browser/android/media/document_picture_in_picture_bridge_android.cc b/chrome/browser/android/media/document_picture_in_picture_bridge_android.cc
deleted file mode 100644
index 38be33d..0000000
--- a/chrome/browser/android/media/document_picture_in_picture_bridge_android.cc
+++ /dev/null
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page