Medium chrome UAF 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Sharing
DescriptionUse after free in Sharing
ComponentSharing
Bug ClassUAF
Tracker547322272
Fix commit3c0d8469dea1 (chromium/src) +151/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
for
components/send_tab_to_self/fake_send_tab_to_self_model.cc
modified
if
ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm
modified
TEST_F
ios/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.cc
  • components/send_tab_to_self/fake_send_tab_to_self_model.h
  • ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h
  • ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm
  • ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
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.