Chrome · Sharing
CVE-2026-87609
UAF in Sharing
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/send_tab_to_self/fake_send_tab_to_self_model.cc |
modified | |
ifios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm |
modified | |
TEST_Fios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm |
modified |
Files Changed
components/send_tab_to_self/fake_send_tab_to_self_model.cccomponents/send_tab_to_self/fake_send_tab_to_self_model.hios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.hios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mmios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
Patch
From 3c0d8469dea16aaeb7bc9feb1d01cdaa5131945e Mon Sep 17 00:00:00 2001 From: Michael Tatarski <[email protected]> Date: Mon, 17 Aug 2026 06:31:33 -0700 Subject: [PATCH] [STTS][iOS] Fix UAF by tracking pending entry via GUID SendTabToSelfBrowserAgent previously held a raw pointer to a pending SendTabToSelfEntry when a remote entry arrived while the active tab was not visible. If entries were destroyed prior to notifying observers (e.g. during history clearing), dereferencing the pointer in DismissEntries caused a heap-use-after-free. This change stores the GUID string instead of a raw pointer and resolves the entry dynamically from the model when displaying the infobar. Fixed: 547322272 Change-Id: I841a2469f40aa18308b18a50d55ec832bd4540bc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8252063 Commit-Queue: Michael Tatarski <[email protected]> Reviewed-by: Marc Treib <[email protected]> Cr-Commit-Position: refs/heads/main@{#1680497} --- diff --git a/components/send_tab_to_self/fake_send_tab_to_self_model.cc b/components/send_tab_to_self/fake_send_tab_to_self_model.cc index 564b63a..177456e 100644 --- a/components/send_tab_to_self/fake_send_tab_to_self_model.cc +++ b/components/send_tab_to_self/fake_send_tab_to_self_model.cc @@ -259,13 +259,13 @@ return results; } -void FakeSendTabToSelfModel::RemoveEntryRemotely(const std::string& guid) { +void FakeSendTabToSelfModel::RemoveEntryRemotely(std::string guid) { auto it = entries_.find(guid); if (it != entries_.end()) { + entries_.erase(it); for (auto& observer : observers_) { observer.OnEntriesRemovedRemotely({guid}); } - entries_.erase(it); } } diff --git a/components/send_tab_to_self/fake_send_tab_to_self_model.h b/components/send_tab_to_self/fake_send_tab_to_self_model.h index cc74669a..6094e7d 100644 --- a/components/send_tab_to_self/fake_send_tab_to_self_model.h +++ b/components/send_tab_to_self/fake_send_tab_to_self_model.h @@ -94,8 +94,9 @@ std::vector<const SendTabToSelfEntry*> AddEntriesRemotely( std::vector<RemoteEntryParams> entries_params); - // Simulates an entry being removed from a remote device. - void RemoveEntryRemotely(const std::string& guid); + // Removes the entry corresponding to the `guid` from the local model and + // notify observers that the entry was removed remotely. + void RemoveEntryRemotely(std::string guid); const std::string& last_opened_guid() const { return last_opened_guid_; } const std::string& last_dismissed_guid() const { diff --git a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h index 328cf5cb..de9b50b 100644 --- a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h +++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h @@ -155,8 +155,9 @@ // Owned by the SendTabToSelfSyncService which should outlive this class raw_ptr<send_tab_to_self::SendTabToSelfModel> model_ = nullptr; - // The pending SendTabToSelf entry to display an InfoBar for. - raw_ptr<const send_tab_to_self::SendTabToSelfEntry> pending_entry_ = nullptr; + // The GUID of the pending SendTabToSelf entry to display an InfoBar for, + // if any. + std::optional<std::string> pending_entry_guid_; // The WebState that is being observed for activation, if any. raw_ptr<web::WebState> pending_web_state_ = nullptr; diff --git a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm index cbbdcc57..a35217ed 100644 --- a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm +++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm @@ -287,7 +287,10 @@ // Pick the most recently shared entry since only one infobar can be shown // at a time. - pending_entry_ = GetMostRecentlySharedEntry(new_entries); + const send_tab_to_self::SendTabToSelfEntry* entry = + GetMostRecentlySharedEntry(new_entries); + pending_entry_guid_ = + entry ? std::make_optional(entry->GetGUID()) : std::nullopt; return; } @@ -309,8 +312,8 @@ return; } - if (pending_entry_ && - std::ranges::contains(guids, pending_entry_->GetGUID())) { + if (pending_entry_guid_ && + std::ranges::contains(guids, *pending_entry_guid_)) { CleanUpObserversAndVariables(); } @@ -353,10 +356,16 @@ return; } - if (pending_entry_) { - DisplayInfoBar(new_active, pending_entry_, /*opened_tab_count=*/1); - CleanUpObserversAndVariables(); + if (!pending_entry_guid_) { + return; } + + const send_tab_to_self::SendTabToSelfEntry* entry = + model_->GetEntryByGUID(*pending_entry_guid_); + if (entry) { + DisplayInfoBar(new_active, entry, /*opened_tab_count=*/1); + } + CleanUpObserversAndVariables(); } #pragma mark - WebStateObserver @@ -368,10 +377,14 @@ return; } - DCHECK(pending_entry_); + DCHECK(pending_entry_guid_.has_value()); DCHECK(pending_web_state_); - DisplayInfoBar(pending_web_state_, pending_entry_, /*opened_tab_count=*/1); + const send_tab_to_self::SendTabToSelfEntry* entry = + model_->GetEntryByGUID(*pending_entry_guid_); + if (entry) { + DisplayInfoBar(pending_web_state_, entry, /*opened_tab_count=*/1); + } CleanUpObserversAndVariables(); } @@ -410,7 +423,7 @@ } void SendTabToSelfBrowserAgent::CleanUpObserversAndVariables() { - pending_entry_ = nullptr; + pending_entry_guid_.reset(); web_state_observation_.Reset(); pending_web_state_ = nullptr; diff --git a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm index 6400e4a..b70f0c2 100644 --- a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm +++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm @@ -397,6 +397,126 @@ EXPECT_EQ(0UL, infobar_manager->infobars().size()); } +// Tests that when an entry is added while the active WebState is not visible, +// and then the entry is removed remotely (deallocated), showing the WebState +// afterwards does not show an InfoBar or cause a use-after-free crash. +TEST_F(SendTabToSelfBrowserAgentTest, TestRemoteRemovePendingNotVisibleTab) { + // Add a web state, active but not visible. + web::WebState* web_state = + AppendNewWebState(GURL("http://www.blank.com"), + /*activate=*/true, /*is_visible=*/false); + InfoBarManagerImpl* infobar_manager = + InfoBarManagerImpl::FromWebState(web_state); + EXPECT_EQ(0UL, infobar_manager->infobars().size()); + + // Remote entry added while tab is not visible (so pending_entry_guid_ is + // set). + const SendTabToSelfEntry* entry = model_->AddEntryRemotely( + GURL("http://www.test.com/test-1"), "title", kDeviceID, + send_tab_to_self::PageContext(), send_tab_to_self::NavigationHistory()); + ASSERT_TRUE(entry); + std::string guid = entry->GetGUID(); + + // No visible web state, so expect no infobar yet. + EXPECT_EQ(0UL, infobar_manager->infobars().size()); + + // Remove the entry remotely (which erases the entry and calls + // DismissEntries). + model_->RemoveEntryRemotely(guid); + + // Show the web state. + web_state->WasShown(); + + // No infobar should be added since the pending entry was removed. + EXPECT_EQ(0UL, infobar_manager->infobars().size()); +} + +// Tests that removing an unrelated entry remotely removes its InfoBar but +// preserves the pending entry for a not-yet-visible WebState. +TEST_F(SendTabToSelfBrowserAgentTest, + TestRemoteRemoveUnrelatedEntryPreservesPending) { + // Add first web state, active and visible. + web::WebState* web_state1 = AppendNewWebState(GURL("http://www.blank.com")); + InfoBarManagerImpl* infobar_manager1 = + InfoBarManagerImpl::FromWebState(web_state1); + EXPECT_EQ(0UL, infobar_manager1->infobars().size()); + + // Add an entry for the visible web state.
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
index 6400e4a..b70f0c2 100644
--- a/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
+++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
@@ -397,6 +397,126 @@
EXPECT_EQ(0UL, infobar_manager->infobars().size());
}
+// Tests that when an entry is added while the active WebState is not visible,
+// and then the entry is removed remotely (deallocated), showing the WebState
+// afterwards does not show an InfoBar or cause a use-after-free crash.
+TEST_F(SendTabToSelfBrowserAgentTest, TestRemoteRemovePendingNotVisibleTab) {
+ // Add a web state, active but not visible.
+ web::WebState* web_state =
+ AppendNewWebState(GURL("http://www.blank.com"),
+ /*activate=*/true, /*is_visible=*/false);
+ InfoBarManagerImpl* infobar_manager =
+ InfoBarManagerImpl::FromWebState(web_state);
+ EXPECT_EQ(0UL, infobar_manager->infobars().size());
+
+ // Remote entry added while tab is not visible (so pending_entry_guid_ is
+ // set).
+ const SendTabToSelfEntry* entry = model_->AddEntryRemotely(
+ GURL("http://www.test.com/test-1"), "title", kDeviceID,
+ send_tab_to_self::PageContext(), send_tab_to_self::NavigationHistory());
+ ASSERT_TRUE(entry);
+ std::string guid = entry->GetGUID();
+
+ // No visible web state, so expect no infobar yet.
+ EXPECT_EQ(0UL, infobar_manager->infobars().size());
+
+ // Remove the entry remotely (which erases the entry and calls
+ // DismissEntries).
+ model_->RemoveEntryRemotely(guid);
+
+ // Show the web state.
+ web_state->WasShown();
+
+ // No infobar should be added since the pending entry was removed.
+ EXPECT_EQ(0UL, infobar_manager->infobars().size());
+}
+
+// Tests that removing an unrelated entry remotely removes its InfoBar but
+// preserves the pending entry for a not-yet-visible WebState.
+TEST_F(SendTabToSelfBrowserAgentTest,
+ TestRemoteRemoveUnrelatedEntryPreservesPending) {
+ // Add first web state, active and visible.
+ web::WebState* web_state1 = AppendNewWebState(GURL("http://www.blank.com"));
+ InfoBarManagerImpl* infobar_manager1 =
+ InfoBarManagerImpl::FromWebState(web_state1);
+ EXPECT_EQ(0UL, infobar_manager1->infobars().size());
+
+ // Add an entry for the visible web state.
+ const SendTabToSelfEntry* entry1 = model_->AddEntryRemotely(
+ GURL("http://www.test.com/first"), "title1", kDeviceID,
+ send_tab_to_self::PageContext(), send_tab_to_self::NavigationHistory());
+ ASSERT_TRUE(entry1);
+ std::string guid1 = entry1->GetGUID();
+ EXPECT_EQ(1UL, infobar_manager1->infobars().size());
+
+ // Add second web state, active but not visible.
+ web::WebState* web_state2 =
+ AppendNewWebState(GURL("http://www.blank.com"),
+ /*activate=*/true, /*is_visible=*/false);
+ InfoBarManagerImpl* infobar_manager2 =
+ InfoBarManagerImpl::FromWebState(web_state2);
+ EXPECT_EQ(0UL, infobar_manager2->infobars().size());
+
+ // Add the pending entry for the non-visible web state.
+ const SendTabToSelfEntry* pending_entry = model_->AddEntryRemotely(
+ GURL("http://www.test.com/pending"), "title2", kDeviceID,
+ send_tab_to_self::PageContext(), send_tab_to_self::NavigationHistory());
+ ASSERT_TRUE(pending_entry);
+ std::string pending_guid = pending_entry->GetGUID();
+ EXPECT_EQ(0UL, infobar_manager2->infobars().size());
+
+ // Remove the first entry remotely.
+ model_->RemoveEntryRemotely(guid1);
+ EXPECT_EQ(0UL, infobar_manager1->infobars().size());
+
+ // Show the second web state.
+ web_state2->WasShown();
+
+ // An infobar for the pending entry should now be added to the second web
+ // state.
+ ASSERT_EQ(1UL, infobar_manager2->infobars().size());
+ infobars::InfoBar* infobar = infobar_manager2->infobars()[0];
+ auto* delegate =
+ static_cast<send_tab_to_self::IOSSendTabToSelfInfoBarDelegate*>(
+ infobar->delegate());
+ EXPECT_EQ(pending_guid, delegate->GetGUID());
+}
+
+// Tests that when an entry is added while the active WebState is not visible,
+// and then the entry is removed remotely (deallocated), switching to another
+// active WebState does not show an InfoBar or cause a use-after-free crash.
+TEST_F(SendTabToSelfBrowserAgentTest,
+ TestRemoteRemovePendingNotVisibleTabSwitchesActiveTab) {
+ // Add a web state, active but not visible.
+ web::WebState* web_state1 =
+ AppendNewWebState(GURL("http://www.blank.com"),
+ /*activate=*/true, /*is_visible=*/false);
+ InfoBarManagerImpl* infobar_manager1 =
+ InfoBarManagerImpl::FromWebState(web_state1);
+ EXPECT_EQ(0UL, infobar_manager1->infobars().size());
+
+ // Remote entry added while tab 1 is not visible (so pending_entry_guid_ is
+ // set).
+ const SendTabToSelfEntry* entry = model_->AddEntryRemotely(
+ GURL("http://www.test.com/test-1"), "title", kDeviceID,
+ send_tab_to_self::PageContext(), send_tab_to_self::NavigationHistory());
+ ASSERT_TRUE(entry);
+ std::string guid = entry->GetGUID();
+
+ // Remove the entry remotely (which erases the entry and calls
+ // DismissEntries).
+ model_->RemoveEntryRemotely(guid);
+
+ // Add and activate a second web state.
+ web::WebState* web_state2 = AppendNewWebState(GURL("http://www.blank.com"));
+ InfoBarManagerImpl* infobar_manager2 =
+ InfoBarManagerImpl::FromWebState(web_state2);
+
+ // No infobar should be added to either web state since the entry was removed.
+ EXPECT_EQ(0UL, infobar_manager1->infobars().size());
+ EXPECT_EQ(0UL, infobar_manager2->infobars().size());
+}
+
// Tests that SendTabToSelfLoadNavigationUserData is correctly attached or
// detached when TabWillLoadUrl is triggered.
TEST_F(SendTabToSelfBrowserAgentTest, TestTabWillLoadUrl) {
@@ -759,8 +879,7 @@
public:
SendTabToSelfBrowserAgentToastEnabledTest()
: SendTabToSelfBrowserAgentTest(
- {send_tab_to_self::kSendTabToSelfPostSendToast},
- {}) {}
+ {send_tab_to_self::kSendTabToSelfPostSendToast}) {}
};
class SendTabToSelfBrowserAgentToastDisabledTest
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