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 <mtatarski@google.com>
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 <mtatarski@google.com>
Reviewed-by: Marc Treib <treib@chromium.org>
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