Chrome · PictureInPicture › Clank
CVE-2026-106338
Spoofing in PictureInPicture (Clank)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Files Changed
chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.ccchrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/ToolbarPositionControllerTest.javachrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManager.javachrome/browser/about_flags.ccchrome/browser/picture_in_picture/picture_in_picture_window_manager_bridge.ccchrome/browser/flag_descriptions.hchrome/browser/flag-metadata.jsonchrome/android/chrome_java_sources.gnitools/metrics/histograms/enums.xmlchrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/ToolbarPositionController.javachrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManagerUnitTest.javachrome/android/BUILD.gnchrome/android/junit/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridgeUnitTest.javachrome/browser/flags/android/chrome_feature_list.hchrome/android/junit/BUILD.gnchrome/browser/picture_in_picture/BUILD.gnchrome/android/java/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridge.javachrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.javachrome/browser/picture_in_picture/picture_in_picture_window_manager.hchrome/browser/picture_in_picture/picture_in_picture_window_manager.ccchrome/browser/flags/android/chrome_feature_list.cc
Patch
From c793d3d8ae331aa6cd524b954cea7bf6e79e4091 Mon Sep 17 00:00:00 2001 From: Phil Yan <[email protected]> Date: Wed, 02 Sep 2026 15:16:27 -0700 Subject: [PATCH] [Clank] Move toolbar to top when PiP is active When the omnibox is positioned at the bottom on Android phones, an active Picture-in-Picture (PiP) window positioned at the bottom right can occlude the address bar and security indicators, creating a spoofing risk. Before: https://screencast.googleplex.com/cast/NjQzNzI0NDc2MzExMTQyNHxlNGI4YTM3Ny0yYQ After: https://screencast.googleplex.com/cast/NTUxOTcxNzc0NDc3MTA3Mnw3NzJjNWQ3Yy0wZA This change moves the toolbar to the top when Picture-in-Picture is active and restores it to the bottom when all PiP sessions finish: - Tracks active PiP sessions (both video and Document PiP) via C++ PictureInPictureWindowManager and exposes state to Java via PictureInPictureWindowManagerBridge. - Observes the PiP supplier in ToolbarPositionController to snap the toolbar to ControlsPosition.TOP when PiP starts and return to ControlsPosition.BOTTOM when PiP ends. - Guards this repositioning behind the feature flag kPictureInPictureMovesToolbarAndroid (disabled by default). - Adds unit tests in ToolbarPositionControllerTest, PictureInPictureWindowManagerBridgeUnitTest, and PictureInPictureWindowManagerTest. Bug: 448789663 Change-Id: I67c82905115b6c46407bf7af14d31a8ce617edab Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8332207 Reviewed-by: Patrick Noland <[email protected]> Commit-Queue: Phil Yan <[email protected]> Cr-Commit-Position: refs/heads/main@{#1691131} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridge.java new file mode 100644 index 0000000..1baea7ef --- /dev/null +++ b/chrome/android/java/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridge.java @@ -0,0 +1,55 @@ +// 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. + +package org.chromium.chrome.browser.media; + +import org.jni_zero.CalledByNative; +import org.jni_zero.JNINamespace; +import org.jni_zero.NativeMethods; + +import org.chromium.base.ThreadUtils; +import org.chromium.base.supplier.NonNullObservableSupplier; +import org.chromium.base.supplier.ObservableSuppliers; +import org.chromium.base.supplier.SettableNonNullObservableSupplier; +import org.chromium.build.annotations.NullMarked; + +/** + * Bridge between C++ PictureInPictureWindowManager and Java components. Exposes an + * ObservableSupplier that tracks whether a Picture-in-Picture window is active. + */ +@NullMarked +@JNINamespace("picture_in_picture") +public class PictureInPictureWindowManagerBridge { + private static final SettableNonNullObservableSupplier<Boolean> sIsPipShowingSupplier = + ObservableSuppliers.createNonNull(false); + + private PictureInPictureWindowManagerBridge() {} + + /** Returns an observable supplier indicating whether any PiP window is currently showing. */ + public static NonNullObservableSupplier<Boolean> getIsPictureInPictureShowingSupplier() { + return sIsPipShowingSupplier; + } + + /** Initializes the native observer and synchronizes initial PiP state. */ + public static void initializeWithNative() { + ThreadUtils.assertOnUiThread(); + sIsPipShowingSupplier.set( + PictureInPictureWindowManagerBridgeJni.get().isInPictureInPicture()); + } + + @CalledByNative + static void onPictureInPictureStateChanged(boolean isInPip) { + ThreadUtils.assertOnUiThread(); + sIsPipShowingSupplier.set(isInPip); + } + + static void resetForTesting() { + sIsPipShowingSupplier.set(false); + } + + @NativeMethods + public interface Natives { + boolean isInPictureInPicture(); + } +} diff --git a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManager.java b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManager.java index ab669bbd..5f23eb7 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManager.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManager.java @@ -113,6 +113,7 @@ import org.chromium.chrome.browser.layouts.LayoutStateProvider; import org.chromium.chrome.browser.layouts.LayoutType; import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; +import org.chromium.chrome.browser.media.PictureInPictureWindowManagerBridge; import org.chromium.chrome.browser.merchant_viewer.MerchantTrustSignalsCoordinator; import org.chromium.chrome.browser.merchant_viewer.PageInfoStoreInfoController.StoreInfoActionHandler; import org.chromium.chrome.browser.metrics.UmaActivityObserver; @@ -2063,6 +2064,7 @@ mOmniboxFocusStateSupplier, mFormFieldFocusedSupplier.getObservable(), mFindInPageShowingSupplier, + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier(), keyboardAccessoryStateSupplier.getInsetSupplier(), mWindowAndroid.getKeyboardDelegate(), mControlContainer, @@ -2867,6 +2869,8 @@ mControlContainer.setReadyForBitmapCapture(true); } + PictureInPictureWindowManagerBridge.initializeWithNative(); + TraceEvent.end("ToolbarManager.initializeWithNative"); } diff --git a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManagerUnitTest.java b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManagerUnitTest.java index 4b72f8a..0557ce1 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManagerUnitTest.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarManagerUnitTest.java @@ -88,6 +88,8 @@ import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer; import org.chromium.chrome.browser.layouts.scene_layer.SceneLayerJni; import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher; +import org.chromium.chrome.browser.media.PictureInPictureWindowManagerBridge; +import org.chromium.chrome.browser.media.PictureInPictureWindowManagerBridgeJni; import org.chromium.chrome.browser.merchant_viewer.MerchantTrustSignalsCoordinator; import org.chromium.chrome.browser.multiwindow.MultiInstanceOrchestrator; import org.chromium.chrome.browser.multiwindow.MultiInstanceOrchestratorFactory; @@ -221,6 +223,7 @@ @Mock private LargeIconBridge.Natives mLargeIconBridgeNatives; @Mock private ChromeAutocompleteSchemeClassifier.Natives mChromeAutocompleteSchemeClassifierJni; @Mock private PaintPreviewTabServiceFactory.Natives mPaintPreviewTabServiceFactoryNatives; + @Mock private PictureInPictureWindowManagerBridge.Natives mPictureInPictureBridgeNatives; @Mock private Runnable mOpenGridTabSwitcherHandler; @Mock private Tracker mTracker; @Mock private TopToolbarSceneLayer.Natives mTopToolbarSceneLayerNatives; @@ -313,6 +316,8 @@ when(mFaviconHelperNatives.init()).thenReturn(1L); LargeIconBridgeJni.setInstanceForTesting(mLargeIconBridgeNatives); when(mLargeIconBridgeNatives.init()).thenReturn(1L); + PictureInPictureWindowManagerBridgeJni.setInstanceForTesting( + mPictureInPictureBridgeNatives); PaintPreviewTabServiceFactoryJni.setInstanceForTesting( mPaintPreviewTabServiceFactoryNatives); when(mPaintPreviewTabServiceFactoryNatives.getServiceInstanceForCurrentProfile()) diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridgeUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridgeUnitTest.java new file mode 100644 index 0000000..aa3f32d --- /dev/null +++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/PictureInPictureWindowManagerBridgeUnitTest.java @@ -0,0 +1,93 @@ +// 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. + +package org.chromium.chrome.browser.media; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.annotation.Config; + +import org.chromium.base.Callback; +import org.chromium.base.test.BaseRobolectricTestRunner; + +/** Unit tests for {@link PictureInPictureWindowManagerBridge}. */ +@RunWith(BaseRobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PictureInPictureWindowManagerBridgeUnitTest { + @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Mock private PictureInPictureWindowManagerBridge.Natives mBridgeNatives; + @Mock private Callback<Boolean> mObserver; + + @Before + public void setUp() { + PictureInPictureWindowManagerBridgeJni.setInstanceForTesting(mBridgeNatives); + PictureInPictureWindowManagerBridge.resetForTesting(); + } + + @After + public void tearDown() { + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier() + .removeObserver(mObserver); + PictureInPictureWindowManagerBridge.resetForTesting(); + PictureInPictureWindowManagerBridgeJni.setInstanceForTesting(null); + } + + @Test + public void testInitialState_notShowing() { + assertFalse( + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier().get()); + } + + @Test + public void testInitializeWithNative_isInPipTrue() { + when(mBridgeNatives.isInPictureInPicture()).thenReturn(true); + + PictureInPictureWindowManagerBridge.initializeWithNative(); + + verify(mBridgeNatives).isInPictureInPicture(); + assertTrue( + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier().get()); + } + + @Test + public void testInitializeWithNative_isInPipFalse() { + when(mBridgeNatives.isInPictureInPicture()).thenReturn(false); + + PictureInPictureWindowManagerBridge.initializeWithNative(); + + verify(mBridgeNatives).isInPictureInPicture(); + assertFalse( + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier().get()); + } + + @Test + public void testStateChanged_updatesSupplierAndNotifiesObserver() { + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier() + .addSyncObserver(mObserver); + + // Native calls onPictureInPictureStateChanged(true) + PictureInPictureWindowManagerBridge.onPictureInPictureStateChanged(true); + assertTrue( + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier().get()); + verify(mObserver).onResult(true); + + // Native calls onPictureInPictureStateChanged(false) + PictureInPictureWindowManagerBridge.onPictureInPictureStateChanged(false); + assertFalse( + PictureInPictureWindowManagerBridge.getIsPictureInPictureShowingSupplier().get()); + verify(mObserver).onResult(false); + } +} diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc index 45a6b949..c71d30f 100644 --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc @@ -9382,6 +9382,12 @@ flag_descriptions::kAutoPictureInPictureAndroidName, flag_descriptions::kAutoPictureInPictureAndroidDescription, kOsAndroid, FEATURE_VALUE_TYPE(media::kAutoPictureInPictureAndroid)}, + + {"picture-in-picture-moves-toolbar-android", + flag_descriptions::kPictureInPictureMovesToolbarAndroidName, + flag_descriptions::kPictureInPictureMovesToolbarAndroidDescription, + kOsAndroid, + FEATURE_VALUE_TYPE(chrome::android::kPictureInPictureMovesToolbarAndroid)}, #endif {"document-patching", flag_descriptions::kDocumentPatchingName, diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json index 61d7b7d..f7353cd 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json @@ -8344,6 +8344,11 @@ "expiry_milestone": 160 }, { + "name": "picture-in-picture-moves-toolbar-android", + "owners": [ "[email protected]", "[email protected]" ], + "expiry_milestone": 175 + }, + { "name": "picture-in-picture-mute-control", "owners": [ "[email protected]", "[email protected]", "[email protected]" ], "expiry_milestone": 165 diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h index c172dbf..dfe394a 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -6410,6 +6410,12 @@ "When enabled, this sets the minimum " "height to 30% or 220dp, whichever is greater, for ephemeral pCCTs."; +inline constexpr char kPictureInPictureMovesToolbarAndroidName[] = + "Picture-in-Picture moves toolbar to top on Android"; +inline constexpr char kPictureInPictureMovesToolbarAndroidDescription[] = + "When enabled on Android, entering Picture-in-Picture moves the toolbar to " + "the top to prevent occlusion by the Picture-in-Picture window."; + inline constexpr char kReadAloudNativeName[] = "Read Aloud Native C++ Implementation"; inline constexpr char kReadAloudNativeDescription[] = diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc index f04514e..a67ef6a 100644 --- a/chrome/browser/flags/android/chrome_feature_list.cc +++ b/chrome/browser/flags/android/chrome_feature_list.cc @@ -448,6 +448,7 @@ &kPdfReuseFragment, &kPersistAcrossReboots, &kPersistAcrossRebootsDebugLogs, + &kPictureInPictureMovesToolbarAndroid, &kPowerSavingModeBroadcastReceiverInBackground, &kPreconnectOnTabCreation, &kPriceChangeModule, @@ -867,6 +868,7 @@ BASE_FEATURE(kPdfReuseFragment, base::FEATURE_DISABLED_BY_DEFAULT); BASE_FEATURE(kPersistAcrossReboots, base::FEATURE_ENABLED_BY_DEFAULT); BASE_FEATURE(kPersistAcrossRebootsDebugLogs, base::FEATURE_DISABLED_BY_DEFAULT); +BASE_FEATURE(kPictureInPictureMovesToolbarAndroid, base::FEATURE_DISABLED_BY_DEFAULT); BASE_FEATURE(kPowerSavingModeBroadcastReceiverInBackground, base::FEATURE_ENABLED_BY_DEFAULT); BASE_FEATURE(kPreconnectOnTabCreation, base::FEATURE_DISABLED_BY_DEFAULT); BASE_FEATURE(kPriceChangeModule, base::FEATURE_ENABLED_BY_DEFAULT); diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h index c34b41c5..fdba894 100644 --- a/chrome/browser/flags/android/chrome_feature_list.h +++ b/chrome/browser/flags/android/chrome_feature_list.h @@ -260,6 +260,7 @@ BASE_DECLARE_FEATURE(kPdfReuseFragment); BASE_DECLARE_FEATURE(kPersistAcrossReboots); BASE_DECLARE_FEATURE(kPersistAcrossRebootsDebugLogs); +BASE_DECLARE_FEATURE(kPictureInPictureMovesToolbarAndroid); BASE_DECLARE_FEATURE(kPowerSavingModeBroadcastReceiverInBackground); BASE_DECLARE_FEATURE(kPreconnectOnTabCreation); BASE_DECLARE_FEATURE(kPriceChangeModule); diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java index 0e7bd1a..1a719d4 100644 --- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java +++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java @@ -688,6 +688,8 @@ "PermissionDedicatedCpssSettingAndroid"; public static final String PERSIST_ACROSS_REBOOTS = "PersistAcrossReboots"; public static final String PERSIST_ACROSS_REBOOTS_DEBUG_LOGS = "PersistAcrossRebootsDebugLogs"; + public static final String PICTURE_IN_PICTURE_MOVES_TOOLBAR_ANDROID = + "PictureInPictureMovesToolbarAndroid"; public static final String POWER_SAVING_MODE_BROADCAST_RECEIVER_IN_BACKGROUND = "PowerSavingModeBroadcastReceiverInBackground"; public static final String PRECONNECT_ON_TAB_CREATION = "PreconnectOnTabCreation"; @@ -1306,6 +1308,8 @@ newCachedFlag(PERSIST_ACROSS_REBOOTS, true); public static final CachedFlag sPersistAcrossRebootsDebugLogs = newCachedFlag(PERSIST_ACROSS_REBOOTS_DEBUG_LOGS, false); + public static final CachedFlag sPictureInPictureMovesToolbarAndroid = + newCachedFlag(PICTURE_IN_PICTURE_MOVES_TOOLBAR_ANDROID, false); public static final CachedFlag sPowerSavingModeBroadcastReceiverInBackground = newCachedFlag(POWER_SAVING_MODE_BROADCAST_RECEIVER_IN_BACKGROUND, true); public static final CachedFlag sPriceChangeModule = newCachedFlag(PRICE_CHANGE_MODULE, true); @@ -1612,6 +1616,7 @@ sPdfReuseFragment, sPersistAcrossReboots, sPersistAcrossRebootsDebugLogs, + sPictureInPictureMovesToolbarAndroid, sPowerSavingModeBroadcastReceiverInBackground, sPriceChangeModule, sProtectRecentlyVisibleTab, diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc index 10d227e1..eede532 100644 --- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc +++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc @@ -280,6 +280,10 @@ pip_window_controller_->FocusInitiator(); } +bool PictureInPictureWindowManager::IsInPictureInPicture() const { + return pip_window_controller_ != nullptr; +} + content::WebContents* PictureInPictureWindowManager::GetWebContents() const { if (!pip_window_controller_) return nullptr; diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h index f4ebbef..157b6fa 100644 --- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h +++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h @@ -122,6 +122,10 @@ // Called to notify that the initiator web contents should be focused. void FocusInitiator(); + // Returns whether a picture-in-picture window (video or document pip) is + // currently open. + bool IsInPictureInPicture() const; + // Gets the web contents in the opener browser window. content::WebContents* GetWebContents() const; diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_bridge.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_bridge.cc new file mode 100644 index 0000000..c4fb08855 --- /dev/null +++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_bridge.cc ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
index 47cc0b610..d1c3c50 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
@@ -176,6 +176,21 @@
PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture());
}
+TEST_F(PictureInPictureWindowManagerTest, IsInPictureInPicture) {
+ PictureInPictureWindowManager* picture_in_picture_window_manager =
+ PictureInPictureWindowManager::GetInstance();
+ EXPECT_FALSE(picture_in_picture_window_manager->IsInPictureInPicture());
+
+ MockPictureInPictureWindowController controller;
+ picture_in_picture_window_manager->EnterPictureInPictureWithController(
+ &controller);
+ EXPECT_TRUE(picture_in_picture_window_manager->IsInPictureInPicture());
+
+ EXPECT_CALL(controller, Close(/*should_pause_video=*/false));
+ EXPECT_TRUE(picture_in_picture_window_manager->ExitPictureInPicture());
+ EXPECT_FALSE(picture_in_picture_window_manager->IsInPictureInPicture());
+}
+
TEST_F(PictureInPictureWindowManagerTest, OnEnterVideoPictureInPicture) {
PictureInPictureWindowManager* picture_in_picture_window_manager =
PictureInPictureWindowManager::GetInstance();
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