CVE-2026-113225
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.java |
modified | |
PauseContentReplacementForTestingchrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc |
modified | |
ResumeContentReplacementForTestingchrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc |
modified | |
AutoOpenSidePanelTabModelObserverchrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelCoordinatorAndroidBridge.javachrome/browser/ui/side_panel/test/android/java/src/org/chromium/chrome/browser/ui/side_panel/test/SidePanelCoordinatorAndroidBrowserTestSupport.javachrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.javachrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.ccchrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.ccchrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelNativeBridgeSelector.javachrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.hchrome/browser/ui/side_panel/test/android/BUILD.gn
Patch
From 9520dad94e93c82d9051e0b214835f8349c6789a Mon Sep 17 00:00:00 2001 From: Linyu He <[email protected]> Date: Thu, 17 Sep 2026 21:54:16 -0700 Subject: [PATCH] SidePanel: Use precise timing to test "deferred content replacement" ------------ The issue ------------ Previously, testing deferred content replacement in `SidePanelCoordinatorAndroidBrowserTest` relied on: 1. Passing `use_thin_web_view=true` to create a real `ThinWebView` with `WebContents` in Java (`SidePanelCoordinatorAndroidBrowserTestSupport`). 2. Calling `ConfigDeferredViewReplacementForTesting(true)`, which delegated to `thinWebView.runOnNextFrame(...)`. We couldn't pause and inspect intermediate states (such as verifying `HasPendingReplacedEntryForTesting()` or the "will hide" events for a SidePanelEntry). ------------ Changes ------------ 1. Replace `configDeferredViewReplacementForTesting(boolean)` with two explicit control methods across Java, JNI, and C++: - `PauseContentReplacementForTesting()` - `ResumeContentReplacementForTesting()` 2. Remove `ThinWebView` creation and dependencies from `SidePanelCoordinatorAndroidBrowserTestSupport` and `side_panel_coordinator_android_browsertest.cc` since `ThinWebView` is no longer needed to test deferred content replacement. 3. Update all deferred content replacement browser tests to deterministically pause, assert intermediate pending states, and resume content replacement, grouping them at the end of `side_panel_coordinator_android_browsertest.cc`. With this CL, we can finally address this comment: https://chromium-review.git.corp.google.com/c/chromium/src/+/8163120/comment/49a049d0_2cff49fe/ This CL will also help verify the fix for the associated bug. Cq-Include-Trybots: luci.chromium.try:android-desktop-x64-rel Include-Ci-Only-Tests: true Bug: 562109703 Change-Id: I62a770fc85a5ed8811073cccd608d1c341f84576 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8426822 Commit-Queue: Linyu He <[email protected]> Reviewed-by: Shu Yang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700477} --- diff --git a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.java b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.java index 35d363e3..264b295 100644 --- a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.java +++ b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelContainerCoordinatorImpl.java @@ -87,7 +87,7 @@ */ private boolean mIsPreparingForAutoRestore; - private boolean mEnableDeferredViewReplacementForTesting; + private boolean mIsContentReplacementPausedForTesting; private boolean mSimulateAutoCloseConditionForTesting; SidePanelContainerCoordinatorImpl( @@ -248,20 +248,19 @@ } } }; - mPendingReplaceRunnable = removeOldViewRunnable; - ThinWebView thinWebView = findThinWebView(newContent.mView); - // If there is no ThinWebView, immediately complete the content View replacement since this - // won't cause UI flickers. - if (thinWebView == null) { - completePendingContentReplacementInternal(); + if (mIsContentReplacementPausedForTesting) { return; } - // If there is a ThinWebView, but we are in a test that doesn't explicitly enable the - // deferred View removal, also complete the View replacement immediately. - if (BuildConfig.IS_FOR_TEST && !mEnableDeferredViewReplacementForTesting) { + // If there is no ThinWebView, immediately complete the content View replacement since this + // won't cause UI flickers. + // + // If we are in a test, do the same since cross-platform tests using the cross-platform C++ + // side panel APIs can't wait for ThinWebView to render its first frame. + ThinWebView thinWebView = findThinWebView(newContent.mView); + if (thinWebView == null || BuildConfig.IS_FOR_TEST) { completePendingContentReplacementInternal(); return; } @@ -312,23 +311,34 @@ } /** - * Enables or disables deferred content View replacement for testing. + * Pauses content replacement in {@link #startReplacingPanelContent}, for testing. * - * <p>When (1) this is enabled and (2) the new active tab during a tab switch requires replacing - * the side panel content View with a {@code ThinWebView}, the old content View won't be removed - * until the {@code ThinWebView} has rendered the first frame. + * <p>Tests should use this method to simulate the deferred content replacement for {@link + * ThinWebView} in {@link #startReplacingPanelContent}, instead of setting up a {@link + * ThinWebView}. This is because this method and {@link #resumeContentReplacementForTesting} can + * give tests precise timing control so that we can verify intermediate states. * - * <p>(2) is <i>always</i> enabled in production to prevent UI flickers during tab switches. - * - * <p>In tests, (2) needs to be explicitly enabled since tests covering the deferred content - * View replacement need to wait for the replacement to complete. Not all tests have the "wait" - * logic, and it's hard to add it since there are many existing cross-platform side panel - * browser tests that assume synchronous replacement. - * - * @param enable Whether deferred View replacement is enabled. + * @see #startReplacingPanelContent + * @see #resumeContentReplacementForTesting */ - void configDeferredViewReplacementForTesting(boolean enable) { - mEnableDeferredViewReplacementForTesting = enable; + void pauseContentReplacementForTesting() { + mIsContentReplacementPausedForTesting = true; + } + + /** + * Resumes content replacement that's paused in {@link #startReplacingPanelContent}, for + * testing. + * + * <p>This immediately completes the pending content replacement, if it exists. + * + * @see #startReplacingPanelContent + * @see #pauseContentReplacementForTesting + */ + void resumeContentReplacementForTesting() { + assert mIsContentReplacementPausedForTesting + : "pauseContentReplacementForTesting() hasn't been called."; + mIsContentReplacementPausedForTesting = false; + completePendingContentReplacementInternal(); } /** diff --git a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelCoordinatorAndroidBridge.java b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelCoordinatorAndroidBridge.java index 41e503f0..fa0ec57 100644 --- a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelCoordinatorAndroidBridge.java +++ b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelCoordinatorAndroidBridge.java @@ -224,10 +224,15 @@ } @CalledByNativeForTesting - private void configDeferredViewReplacementForTesting( - @JniType("Profile*") Profile profile, boolean enable) { - log(TAG, "configDeferredViewReplacementForTesting", profile, enable); - mNativeBridgeSelector.configDeferredViewReplacementForTesting(profile, enable); // IN-TEST + private void pauseContentReplacementForTesting(@JniType("Profile*") Profile profile) { + log(TAG, "pauseContentReplacementForTesting", profile); + mNativeBridgeSelector.pauseContentReplacementForTesting(profile); // IN-TEST + } + + @CalledByNativeForTesting + private void resumeContentReplacementForTesting(@JniType("Profile*") Profile profile) { + log(TAG, "resumeContentReplacementForTesting", profile); + mNativeBridgeSelector.resumeContentReplacementForTesting(profile); // IN-TEST } @CalledByNativeForTesting diff --git a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelNativeBridgeSelector.java b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelNativeBridgeSelector.java index 1b56f338..46435ba 100644 --- a/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelNativeBridgeSelector.java +++ b/chrome/browser/ui/side_panel/internal/android/java/src/org/chromium/chrome/browser/ui/side_panel/SidePanelNativeBridgeSelector.java @@ -289,13 +289,23 @@ } /** - * See {@link SidePanelContainerCoordinatorImpl#configDeferredViewReplacementForTesting}. + * See {@link SidePanelContainerCoordinatorImpl#pauseContentReplacementForTesting}. * * <p>The given {@link Profile} must be the current {@link Profile}. */ - void configDeferredViewReplacementForTesting(Profile profile, boolean enable) { + void pauseContentReplacementForTesting(Profile profile) { assertCurrentProfile(profile); - mSidePanelContainerCoordinator.configDeferredViewReplacementForTesting(enable); // IN-TEST + mSidePanelContainerCoordinator.pauseContentReplacementForTesting(); // IN-TEST + } + + /** + * See {@link SidePanelContainerCoordinatorImpl#resumeContentReplacementForTesting}. + * + * <p>The given {@link Profile} must be the current {@link Profile}. + */ + void resumeContentReplacementForTesting(Profile profile) { + assertCurrentProfile(profile); + mSidePanelContainerCoordinator.resumeContentReplacementForTesting(); // IN-TEST } /** diff --git a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc index f53c1f3..553db170 100644 --- a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc +++ b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc @@ -549,11 +549,15 @@ } void SidePanelCoordinatorAndroid:: - ConfigDeferredViewReplacementForTesting( // IN-TEST - bool enable) { - Java_SidePanelCoordinatorAndroidBridge_configDeferredViewReplacementForTesting( // IN-TEST - AttachCurrentThread(), java_coordinator(), browser()->GetProfile(), - enable); + PauseContentReplacementForTesting() { // IN-TEST + Java_SidePanelCoordinatorAndroidBridge_pauseContentReplacementForTesting( // IN-TEST + AttachCurrentThread(), java_coordinator(), browser()->GetProfile()); +} + +void SidePanelCoordinatorAndroid:: + ResumeContentReplacementForTesting() { // IN-TEST + Java_SidePanelCoordinatorAndroidBridge_resumeContentReplacementForTesting( // IN-TEST + AttachCurrentThread(), java_coordinator(), browser()->GetProfile()); } void SidePanelCoordinatorAndroid:: diff --git a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.h b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.h index e4a9bf9..94a0f137 100644 --- a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.h +++ b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.h @@ -106,12 +106,15 @@ // Start of functions for testing // ///////////////////////////////////////////////////////////////// - // Enables/Disables deferred View replacement for testing. - // // See the Java - // `SidePanelContainerCoordinator#configDeferredViewReplacementForTesting` - // for detailed documentation. - void ConfigDeferredViewReplacementForTesting(bool enable); + // `SidePanelContainerCoordinatorImpl#pauseContentReplacementForTesting` + // for documentation. + void PauseContentReplacementForTesting(); + + // See the Java + // `SidePanelContainerCoordinatorImpl#resumeContentReplacementForTesting` + // for documentation. + void ResumeContentReplacementForTesting(); // See the Java // `SidePanelContainerCoordinator#simulateAutoCloseConditionForTesting` diff --git a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc index c7a2a44a..bc75061 100644 --- a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc +++ b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc @@ -142,12 +142,11 @@ std::unique_ptr<SidePanelEntry> CreateSidePanelEntry( SidePanelEntryKey key, BrowserWindowInterface* browser, - bool use_thin_web_view = false, base::RepeatingCallback<void(SidePanelNativeViewAndroid*)> on_view_created = {}) { SidePanelEntry::CreateContentCallback create_content_callback = base::BindRepeating( - [](BrowserWindowInterface* browser, bool use_thin_web_view, + [](BrowserWindowInterface* browser, base::RepeatingCallback<void(SidePanelNativeViewAndroid*)> on_view_created, SidePanelEntryScope& scope) { @@ -155,8 +154,7 @@ browser->GetWindow()->GetNativeWindow(); ScopedJavaLocalRef<jobject> java_view = Java_SidePanelCoordinatorAndroidBrowserTestSupport_createTestView( - AttachCurrentThread(), browser->GetProfile(), - window_android, use_thin_web_view); + AttachCurrentThread(), window_android); auto native_view = std::make_unique<SidePanelNativeViewAndroid>(java_view); @@ -166,8 +164,7 @@ return native_view; }, - base::Unretained(browser), use_thin_web_view, - std::move(on_view_created)); + base::Unretained(browser), std::move(on_view_created)); auto entry = std::make_unique<SidePanelEntry>( SidePanelType::kToolbar, key, create_content_callback, @@ -176,6 +173,8 @@ return entry; } +// Simulates GLiC's "automatically open side panel for a new tab" +// behavior. class AutoOpenSidePanelTabModelObserver : public TabModelObserver { public: AutoOpenSidePanelTabModelObserver(BrowserWindowInterface* browser, @@ -183,17 +182,23 @@ : browser_(browser), coordinator_(coordinator) {} ~AutoOpenSidePanelTabModelObserver() override = default; - void DidSelectTab(TabAndroid* tab, TabModel::TabSelectionType type) override { - auto key = SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry); + void DidAddTab(TabAndroid* tab, TabModel::TabLaunchType type) override { auto* registry = SidePanelRegistry::From(tab); - if (!registry->GetEntryForKey(key)) { - registry->Register( - CreateSidePanelEntry(key, browser_, /*use_thin_web_view=*/true)); - side_panel_entry_observers_.push_back( - std::make_unique<TestSidePanelEntryObserver>( - registry->GetEntryForKey(key))); - } - coordinator_->SidePanelUIBase::Show(key, + auto entry = CreateSidePanelEntry(entry_key_, browser_); + auto* entry_ptr = entry.get(); + + side_panel_entry_observers_.push_back( + std::make_unique<TestSidePanelEntryObserver>(entry_ptr)); + + registry->Register(std::move(entry)); + registry->SetActiveEntry(entry_ptr); + } + + void DidSelectTab(TabAndroid* tab, TabModel::TabSelectionType type) override { + // Simulates features like GLiC explicitly calling `Show()` on tab + // activation (see `GlicInstanceImpl::OnBoundTabActivated()`), in addition + // to `SidePanelCoordinatorAndroid` handling the active tab change. + coordinator_->SidePanelUIBase::Show(entry_key_, SidePanelOpenTrigger::kToolbarButton, /*suppress_animations=*/true); } @@ -204,6 +209,8 @@ } private: + const SidePanelEntryKey entry_key_{SidePanelEntryId::kTestTabScopedEntry}; + raw_ptr<BrowserWindowInterface> browser_; raw_ptr<SidePanelCoordinatorAndroid> coordinator_; std::vector<std::unique_ptr<TestSidePanelEntryObserver>> @@ -266,8 +273,7 @@ BrowserWindowInterface* window, std::optional<SidePanelEntryId> window_scoped_entry_id, std::vector<std::optional<SidePanelEntryId>> tab_scoped_entry_ids, - int active_tab_index, - bool use_thin_web_view = false) { + int active_tab_index) { CHECK(window); if (!tab_scoped_entry_ids.empty()) { CHECK_GE(active_tab_index, 0); @@ -288,7 +294,7 @@ if (window_scoped_entry_id.has_value()) { auto key = SidePanelEntryKey(window_scoped_entry_id.value()); SidePanelRegistry::From(window)->Register( - CreateSidePanelEntry(key, window, use_thin_web_view)); + CreateSidePanelEntry(key, window)); coordinator->SidePanelUIBase::Show(key, SidePanelOpenTrigger::kToolbarButton, /*suppress_animations=*/true); @@ -312,7 +318,7 @@ auto key = SidePanelEntryKey(tab_scoped_entry_ids[i].value()); SidePanelRegistry::From(tab)->Register( - CreateSidePanelEntry(key, window, use_thin_web_view)); + CreateSidePanelEntry(key, window)); coordinator->SidePanelUIBase::Show(key, SidePanelOpenTrigger::kToolbarButton, /*suppress_animations=*/true); @@ -438,7 +444,6 @@ ScopedJavaGlobalRef<jobject> entry_java_view; std::unique_ptr<SidePanelEntry> entry = CreateSidePanelEntry(entry_key, browser_, - /*use_thin_web_view=*/false, /*on_view_created=*/ base::BindRepeating( [](ScopedJavaGlobalRef<jobject>* java_view, @@ -517,7 +522,6 @@ SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry); auto first_entry = CreateSidePanelEntry(first_entry_key, browser_, - /*use_thin_web_view=*/false, /*on_view_created=*/ base::BindRepeating( [](ScopedJavaGlobalRef<jobject>* java_view, @@ -527,7 +531,6 @@ base::Unretained(&first_java_view))); auto second_entry = CreateSidePanelEntry(second_entry_key, browser_, - /*use_thin_web_view=*/false, /*on_view_created=*/ base::BindRepeating( [](ScopedJavaGlobalRef<jobject>* java_view, @@ -1098,89 +1101,6 @@ IN_PROC_BROWSER_TEST_F( SidePanelCoordinatorAndroidBrowserTest, - MaybeShowEntryOnTabStripModelChanged_SwitchTabs_BothTabsHaveActiveEntries_EnableDeferredViewReplacement_ReplacesSidePanelContent) { - // Arrange: Open 2 tabs. - tabs::TabInterface* first_tab = tab_list_->GetActiveTab(); - tabs::TabInterface* second_tab = ... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc
index c7a2a44a..bc75061 100644
--- a/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc
+++ b/chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android_browsertest.cc
@@ -142,12 +142,11 @@
std::unique_ptr<SidePanelEntry> CreateSidePanelEntry(
SidePanelEntryKey key,
BrowserWindowInterface* browser,
- bool use_thin_web_view = false,
base::RepeatingCallback<void(SidePanelNativeViewAndroid*)> on_view_created =
{}) {
SidePanelEntry::CreateContentCallback create_content_callback =
base::BindRepeating(
- [](BrowserWindowInterface* browser, bool use_thin_web_view,
+ [](BrowserWindowInterface* browser,
base::RepeatingCallback<void(SidePanelNativeViewAndroid*)>
on_view_created,
SidePanelEntryScope& scope) {
@@ -155,8 +154,7 @@
browser->GetWindow()->GetNativeWindow();
ScopedJavaLocalRef<jobject> java_view =
Java_SidePanelCoordinatorAndroidBrowserTestSupport_createTestView(
- AttachCurrentThread(), browser->GetProfile(),
- window_android, use_thin_web_view);
+ AttachCurrentThread(), window_android);
auto native_view =
std::make_unique<SidePanelNativeViewAndroid>(java_view);
@@ -166,8 +164,7 @@
return native_view;
},
- base::Unretained(browser), use_thin_web_view,
- std::move(on_view_created));
+ base::Unretained(browser), std::move(on_view_created));
auto entry = std::make_unique<SidePanelEntry>(
SidePanelType::kToolbar, key, create_content_callback,
@@ -176,6 +173,8 @@
return entry;
}
+// Simulates GLiC's "automatically open side panel for a new tab"
+// behavior.
class AutoOpenSidePanelTabModelObserver : public TabModelObserver {
public:
AutoOpenSidePanelTabModelObserver(BrowserWindowInterface* browser,
@@ -183,17 +182,23 @@
: browser_(browser), coordinator_(coordinator) {}
~AutoOpenSidePanelTabModelObserver() override = default;
- void DidSelectTab(TabAndroid* tab, TabModel::TabSelectionType type) override {
- auto key = SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry);
+ void DidAddTab(TabAndroid* tab, TabModel::TabLaunchType type) override {
auto* registry = SidePanelRegistry::From(tab);
- if (!registry->GetEntryForKey(key)) {
- registry->Register(
- CreateSidePanelEntry(key, browser_, /*use_thin_web_view=*/true));
- side_panel_entry_observers_.push_back(
- std::make_unique<TestSidePanelEntryObserver>(
- registry->GetEntryForKey(key)));
- }
- coordinator_->SidePanelUIBase::Show(key,
+ auto entry = CreateSidePanelEntry(entry_key_, browser_);
+ auto* entry_ptr = entry.get();
+
+ side_panel_entry_observers_.push_back(
+ std::make_unique<TestSidePanelEntryObserver>(entry_ptr));
+
+ registry->Register(std::move(entry));
+ registry->SetActiveEntry(entry_ptr);
+ }
+
+ void DidSelectTab(TabAndroid* tab, TabModel::TabSelectionType type) override {
+ // Simulates features like GLiC explicitly calling `Show()` on tab
+ // activation (see `GlicInstanceImpl::OnBoundTabActivated()`), in addition
+ // to `SidePanelCoordinatorAndroid` handling the active tab change.
+ coordinator_->SidePanelUIBase::Show(entry_key_,
SidePanelOpenTrigger::kToolbarButton,
/*suppress_animations=*/true);
}
@@ -204,6 +209,8 @@
}
private:
+ const SidePanelEntryKey entry_key_{SidePanelEntryId::kTestTabScopedEntry};
+
raw_ptr<BrowserWindowInterface> browser_;
raw_ptr<SidePanelCoordinatorAndroid> coordinator_;
std::vector<std::unique_ptr<TestSidePanelEntryObserver>>
@@ -266,8 +273,7 @@
BrowserWindowInterface* window,
std::optional<SidePanelEntryId> window_scoped_entry_id,
std::vector<std::optional<SidePanelEntryId>> tab_scoped_entry_ids,
- int active_tab_index,
- bool use_thin_web_view = false) {
+ int active_tab_index) {
CHECK(window);
if (!tab_scoped_entry_ids.empty()) {
CHECK_GE(active_tab_index, 0);
@@ -288,7 +294,7 @@
if (window_scoped_entry_id.has_value()) {
auto key = SidePanelEntryKey(window_scoped_entry_id.value());
SidePanelRegistry::From(window)->Register(
- CreateSidePanelEntry(key, window, use_thin_web_view));
+ CreateSidePanelEntry(key, window));
coordinator->SidePanelUIBase::Show(key,
SidePanelOpenTrigger::kToolbarButton,
/*suppress_animations=*/true);
@@ -312,7 +318,7 @@
auto key = SidePanelEntryKey(tab_scoped_entry_ids[i].value());
SidePanelRegistry::From(tab)->Register(
- CreateSidePanelEntry(key, window, use_thin_web_view));
+ CreateSidePanelEntry(key, window));
coordinator->SidePanelUIBase::Show(key,
SidePanelOpenTrigger::kToolbarButton,
/*suppress_animations=*/true);
@@ -438,7 +444,6 @@
ScopedJavaGlobalRef<jobject> entry_java_view;
std::unique_ptr<SidePanelEntry> entry =
CreateSidePanelEntry(entry_key, browser_,
- /*use_thin_web_view=*/false,
/*on_view_created=*/
base::BindRepeating(
[](ScopedJavaGlobalRef<jobject>* java_view,
@@ -517,7 +522,6 @@
SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry);
auto first_entry =
CreateSidePanelEntry(first_entry_key, browser_,
- /*use_thin_web_view=*/false,
/*on_view_created=*/
base::BindRepeating(
[](ScopedJavaGlobalRef<jobject>* java_view,
@@ -527,7 +531,6 @@
base::Unretained(&first_java_view)));
auto second_entry =
CreateSidePanelEntry(second_entry_key, browser_,
- /*use_thin_web_view=*/false,
/*on_view_created=*/
base::BindRepeating(
[](ScopedJavaGlobalRef<jobject>* java_view,
@@ -1098,89 +1101,6 @@
IN_PROC_BROWSER_TEST_F(
SidePanelCoordinatorAndroidBrowserTest,
- MaybeShowEntryOnTabStripModelChanged_SwitchTabs_BothTabsHaveActiveEntries_EnableDeferredViewReplacement_ReplacesSidePanelContent) {
- // Arrange: Open 2 tabs.
- tabs::TabInterface* first_tab = tab_list_->GetActiveTab();
- tabs::TabInterface* second_tab =
- tab_list_->OpenTab(GURL("about:blank"), /*index=*/1);
- ASSERT_FALSE(first_tab->IsActivated());
- ASSERT_TRUE(second_tab->IsActivated());
-
- // Arrange: Create and register SidePanelEntries for both tabs.
- //
- // Note that the entry for the 2nd tab uses a ThinWebView, which is required
- // to test deferred View replacement when we switch from the 1st tab to the
- // 2nd tab.
- auto* first_registry = SidePanelRegistry::From(first_tab);
- auto* second_registry = SidePanelRegistry::From(second_tab);
- auto first_entry_key = SidePanelEntryKey(SidePanelEntryId::kAboutThisSite);
- auto second_entry_key =
- SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry);
-
- auto first_entry = CreateSidePanelEntry(first_entry_key, browser_);
- TestSidePanelEntryObserver first_entry_observer(first_entry.get());
- first_registry->Register(std::move(first_entry));
-
- auto second_entry = CreateSidePanelEntry(second_entry_key, browser_,
- /*use_thin_web_view=*/true);
- TestSidePanelEntryObserver second_entry_observer(second_entry.get());
- second_registry->Register(std::move(second_entry));
-
- // Arrange: Show the SidePanelEntry for the 2nd tab.
- coordinator_->SidePanelUIBase::Show(second_entry_key,
- SidePanelOpenTrigger::kToolbarButton,
- /*suppress_animations=*/true);
- WaitUntilOpened(coordinator_);
- ASSERT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(second_entry_key));
-
- // Arrange: Switch to the first tab.
- tab_list_->ActivateTab(first_tab->GetHandle());
- WaitUntilClosed(coordinator_);
- ASSERT_FALSE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
-
- // Arrange: Show the SidePanelEntry for the first tab.
- coordinator_->SidePanelUIBase::Show(first_entry_key,
- SidePanelOpenTrigger::kToolbarButton,
- /*suppress_animations=*/true);
- WaitUntilOpened(coordinator_);
- ASSERT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
-
- // Act: Switch back to second tab. This should cause a content View
- // replacement.
- //
- // Note that since we enabled the deferred View replacement, we should wait
- // until there is no "pending replaced entry".
- coordinator_->ConfigDeferredViewReplacementForTesting(true);
- tab_list_->ActivateTab(second_tab->GetHandle());
- EXPECT_TRUE(base::test::RunUntil(
- [&]() { return !coordinator_->HasPendingReplacedEntryForTesting(); }));
- WaitUntilOpened(coordinator_);
-
- // Assert: Side panel should show second tab's entry (replaces first tab's
- // entry).
- EXPECT_FALSE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
- EXPECT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(second_entry_key));
-
- // Assert: The first entry should be notified of "hidden" events.
- EXPECT_EQ(SidePanelEntryHideReason::kBackgrounded,
- first_entry_observer.reason_for_last_entry_will_hide_.value());
- EXPECT_EQ(first_entry_key.id(),
- first_entry_observer.id_for_last_entry_hidden_.value());
- EXPECT_EQ(
- SidePanelEntryHideReason::kBackgrounded,
- first_entry_observer.reason_for_last_entry_hidden_with_reason_.value());
-
- // Clean up:
- coordinator_->ConfigDeferredViewReplacementForTesting(false);
-}
-
-IN_PROC_BROWSER_TEST_F(
- SidePanelCoordinatorAndroidBrowserTest,
MaybeShowEntryOnTabStripModelChanged_SwitchTabs_BothTabsHaveActiveEntries_BothTabsAlsoCallShowOnActiveTabChange_ReplacesSidePanelContent) {
// Arrange: Open 2 tabs.
tabs::TabInterface* first_tab = tab_list_->GetActiveTab();
@@ -1531,59 +1451,6 @@
IN_PROC_BROWSER_TEST_F(
SidePanelCoordinatorAndroidBrowserTest,
- MaybeShowEntryOnTabStripModelChanged_CloseTab_BothTabsHaveActiveEntries_EnableDeferredViewReplacement_ReplacesContent) {
- // Arrange: Open the 1st tab and show its entry backed by a ThinWebView.
- tabs::TabInterface* first_tab = tab_list_->GetActiveTab();
- auto* first_registry = SidePanelRegistry::From(first_tab);
- auto first_entry_key =
- SidePanelEntryKey(SidePanelEntryId::kTestTabScopedEntry);
- first_registry->Register(CreateSidePanelEntry(first_entry_key, browser_,
- /*use_thin_web_view=*/true));
- coordinator_->SidePanelUIBase::Show(first_entry_key,
- SidePanelOpenTrigger::kToolbarButton,
- /*suppress_animations=*/true);
- WaitUntilOpened(coordinator_);
- ASSERT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
-
- // Arrange: Open a 2nd tab and show its entry.
- tabs::TabInterface* second_tab =
- tab_list_->OpenTab(GURL("about:blank"), /*index=*/1);
- WaitUntilClosed(coordinator_);
- ASSERT_FALSE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
-
- auto* second_registry = SidePanelRegistry::From(second_tab);
- auto second_entry_key = SidePanelEntryKey(SidePanelEntryId::kAboutThisSite);
- second_registry->Register(CreateSidePanelEntry(second_entry_key, browser_));
- coordinator_->SidePanelUIBase::Show(second_entry_key,
- SidePanelOpenTrigger::kToolbarButton,
- /*suppress_animations=*/true);
- WaitUntilOpened(coordinator_);
- ASSERT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(second_entry_key));
-
- // Act: Close the 2nd tab.
- // This will cause an active tab change and a side panel content View
- // replacement.
- coordinator_->ConfigDeferredViewReplacementForTesting(true);
- tab_list_->CloseTab(second_tab->GetHandle());
-
- // Assert:
- // Note that since we enabled the deferred View replacement, we should wait
- // until there is no "pending replaced entry".
- WaitUntilOpened(coordinator_);
- EXPECT_TRUE(base::test::RunUntil(
- [&]() { return !coordinator_->HasPendingReplacedEntryForTesting(); }));
- EXPECT_TRUE(
- coordinator_->SidePanelUIBase::IsSidePanelEntryShowing(first_entry_key));
-
- // Clean up:
- coordinator_->ConfigDeferredViewReplacementForTesting(false);
-}
-
-IN_PROC_BROWSER_TEST_F(
- SidePanelCoordinatorAndroidBrowserTest,
MaybeShowEntryOnTabStripModelChanged_NullRegistry_DoesNotCrash) {
// Arrange
@@ -1698,9 +1565,8 @@
base::BindRepeating([](SidePanelNativeViewAndroid* view) {
SidePanelUtil::GetSidePanelContentProxy(view)->SetAvailable(false);
});
- registry->Register(CreateSidePanelEntry(second_entry_key, browser_,
- /*use_thin_web_view=*/false,
- on_view_created));
+ registry->Register(
... (truncated)
Original Bug Report
UAF in SidePanelCoordinatorAndroid due to incomplete replacement on tab close
Flapjack has identified 1 potential security vulnerability finding(s) for commit 9e697aa63f41 (Redirect: crrev.com/9e697aa63f41bc4cd66ed56120096347a591554c).
UAF in SidePanelCoordinatorAndroid due to incomplete replacement on tab close
Overview: A Use-After-Free (UAF) vulnerability exists in the Android browser process due to a logic flaw when completing pending side panel content replacements on tab closure. If a tab-scoped side panel entry replacement is initiated but the tab is closed before the asynchronous JNI replacement completes, the cleanup is bypassed because the tab’s active entry has already been cleared. This leaves a dangling pointer to the freed side panel entry, which is subsequently dereferenced when the JNI callback completes.
Affected files:
chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc
Vulnerability Details
A Use-After-Free (UAF) vulnerability exists in chrome/browser/ui/side_panel/internal/android/side_panel_coordinator_android.cc due to a logic flaw in CompletePendingContentReplacementForTab when handling tab-scoped to window-scoped side panel entry replacements.
When a tab-scoped side panel entry (E1 on Tab 1) is being replaced by a window-scoped entry (W1), StartReplacingPanelContent(W1) is called. This function immediately resets Tab 1’s active entry by calling:
if (auto* contextual_registry = GetActiveContextualRegistry()) {
contextual_registry->ResetActiveEntry();
}
This sets the active entry for Tab 1’s registry to std::nullopt. It then stores a raw pointer to E1 in pending_replaced_entry_ and triggers the asynchronous JNI content replacement via Java_SidePanelCoordinatorAndroidBridge_startReplacingPanelContent.
If Tab 1 is closed before the JNI replacement completes, OnTabClosed(Tab 1) is called and invokes CompletePendingContentReplacementForTab(Tab 1). Inside CompletePendingContentReplacementForTab:
void SidePanelCoordinatorAndroid::CompletePendingContentReplacementForTab(
TabAndroid* tab) {
if (auto* registry = SidePanelRegistry::From(tab)) {
if (pending_replaced_entry_ &&
registry->GetActiveEntry() == pending_replaced_entry_) {
Java_SidePanelCoordinatorAndroidBridge_completePendingContentReplacement(
AttachCurrentThread(), java_coordinator(), browser()->GetProfile());
}
}
}
However, because registry->GetActiveEntry() was already reset to std::nullopt during StartReplacingPanelContent, the comparison registry->GetActiveEntry() == pending_replaced_entry_ evaluates to false. As a result, the JNI method completePendingContentReplacement is never invoked, leaving pending_replaced_entry_ pointing to E1.
When Tab 1 is permanently destroyed, its SidePanelEntry (E1) is freed, leaving pending_replaced_entry_ as a dangling pointer. When the asynchronous JNI layout/render completes (or when another replacement starts), OnPanelContentReplaced() is called and dereferences the dangling pending_replaced_entry_ pointer to invoke OnEntryHidden():
void SidePanelCoordinatorAndroid::OnPanelContentReplaced() {
SPLOG("OnPanelContentReplaced");
CHECK(pending_replaced_entry_);
CHECK(pending_hide_reason_);
pending_replaced_entry_->OnEntryHidden();
pending_replaced_entry_->OnEntryHiddenWithReason(*pending_hide_reason_);
pending_replaced_entry_ = nullptr;
pending_hide_reason_ = std::nullopt;
}
Impact and Exploitability
This Use-After-Free occurs in the unsandboxed browser process (SidePanelCoordinatorAndroid). Because pending_replaced_entry_ is a raw_ptr, MiraclePtr (BackupRefPtr) mitigates exploitation on 64-bit Android builds. However, 32-bit Android architectures lack BackupRefPtr, enabling memory corruption/exploitation in the browser process.
Steps to Reproduce (Conceptual)
An attacker can trigger this via a malicious website on Android:
- Open a tab-scoped side panel entry (E1) on the attacker’s tab (Tab 1).
- Trigger a transition/replacement to a window-scoped side panel entry (W1).
- Register a
resizeevent listener on the website (which fires when the viewport layout changes due to the side panel opening) and immediately callwindow.close()to close itself. - Tab 1 is closed immediately, destroying Tab 1 and freeing E1.
- The asynchronous JNI replacement callback completes on the next frame, invoking
OnPanelContentReplaced()and dereferencing the freed E1 entry.
The descriptions of these vulnerabilities are LLM-generated and can contain mistakes. Your feedback is appreciated and will help us make improvements over time. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.