Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker553132214
Fix commit54db51dbd55d (chromium/src) +342/-64
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-26 (per vendor advisory)
Fix landed2026-09-15 (public git)
Disclosed2026-09-17
Patch gap2 days — fix public in source before the advisory

Background

`sessions::LiveTab` / `sessions::ContentLiveTab`
an abstraction over a restorable open tab; ContentLiveTab is the content-layer implementation backed by a WebContents, which owns the tab’s lifetime.
`TabRestoreService` / `TabRestoreServiceHelper::RestoreEntryById`
the service (and its shared helper) that re-opens recently closed tabs, groups, and windows, returning the LiveTab instances it created.
`chrome.sessions.restore()` / `SessionsRestoreFunction`
the extensions API (and its C++ backing function) that lets an extension re-open a closed tab and then inspects the returned tab to build its reply.
`base::WeakPtr<LiveTab>`
a pointer that automatically becomes null when the pointee is destroyed, letting callers detect a tab that was freed out from under them.

Root Cause Analysis

TabRestoreServiceHelper::RestoreEntryById collected raw LiveTab* pointers into a std::vector<LiveTab*> live_tabs as it restored tabs, and only afterward called context->ShowBrowserWindow() to show and activate the target browser window. Showing/activating a window can pump a nested event loop, and that reentrancy can synchronously close the just-restored tab (or its window), destroying the backing ContentLiveTab/WebContents while its raw pointer still sits in live_tabs. The stale vector was returned up to SessionsRestoreFunction, whose RestoreMostRecentEntry path only guarded the result with DCHECK(restored_tabs.size()) — a no-op in release builds — before casting restored_tabs[0] to ContentLiveTab* and dereferencing it, yielding a use-after-free of the freed tab.

The fix tracks restored tabs as std::vector<base::WeakPtr<LiveTab>> throughout the pipeline (via the new LiveTab::GetWeakPtr() / ContentLiveTab::GetWeakPtr()), drops any tab that is no longer valid when the restore completes, and changes RestoreEntryById to return std::optional so an invalid id (std::nullopt) is distinguishable from “restored but all tabs closed” (empty vector). SessionsRestoreFunction now replaces the DCHECK with a real restored_tabs.empty() check that returns kNoActiveTabError, so the freed/absent tab is never dereferenced.

Key insight
The code cached raw, ownership-agnostic LiveTab* pointers across a call (ShowBrowserWindow) that can reentrantly free those very tabs, then dereferenced them; the fix holds the tabs by WeakPtr, filters out any invalidated by that reentrancy, and turns the previously ignored empty result into an explicit error instead of a dereference.

Attack Path

  1. Invoke restore from an extension An extension with the sessions permission calls chrome.sessions.restore(), driving SessionsRestoreFunction into TabRestoreService::RestoreMostRecentEntry / RestoreEntryById.
  2. Capture raw pointer before showing the window TabRestoreServiceHelper::RestoreEntryById restores the tab and pushes its raw LiveTab* into live_tabs, then calls context->ShowBrowserWindow().
  3. Reentrantly close the restored tab Showing/activating the window pumps nested processing that closes the freshly restored tab (the regression test RestoreErrorWhenTabClosedDuringRestore does exactly this via a TabRestoreServiceObserver), destroying the ContentLiveTab/WebContents while its raw pointer remains in the returned vector.
  4. Dereference the freed tab Back in SessionsRestoreFunction, the release-mode DCHECK is skipped and static_cast<ContentLiveTab*>(restored_tabs[0]) is dereferenced, reading through the dangling pointer — a use-after-free in the browser process.
  5. Exploitation Reclaiming the freed ContentLiveTab/WebContents allocation with attacker-influenced data to gain a read/write primitive is (inferred); the patch demonstrates only the dangling-pointer dereference.

Impact Assessment

An attacker who can run an extension calling chrome.sessions.restore() can trigger a use-after-free in the privileged browser process by causing a restored tab to close during the restore operation; the precondition is the ability to close the restored tab synchronously during window activation (nested event processing). Any escalation to controlled read/write, memory corruption exploitation, or code execution is (inferred) and not shown by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/sessions/sessions_api.cc
modified
if
components/sessions/core/tab_restore_service_helper.cc
modified
switch
components/sessions/core/tab_restore_service_helper.cc
modified

Files Changed

  • components/sessions/core/tab_restore_types.cc
  • components/sessions/ios/ios_restore_live_tab.mm
  • components/sessions/content/content_live_tab.h
  • chrome/browser/android/recently_closed_tabs_bridge.cc
  • components/sessions/core/tab_restore_service_helper.cc
  • components/sessions/core/mock_tab_restore_service.h
  • chrome/browser/sessions/tab_restore_service_unittest.cc
  • components/sessions/core/tab_restore_service_impl.h
  • ios/chrome/browser/sessions/model/fake_tab_restore_service.mm
  • components/sessions/core/live_tab.h
  • components/sessions/ios/ios_webstate_live_tab.h
  • chrome/browser/extensions/api/sessions/sessions_apitest.cc
  • components/sessions/core/tab_restore_service.h
  • components/sessions/content/content_live_tab.cc
  • components/sessions/ios/ios_webstate_live_tab.mm
  • chrome/browser/sessions/session_restore_browsertest.cc
  • chrome/browser/sessions/tab_restore_browsertest.cc
  • components/sessions/core/tab_restore_service_impl.cc
  • chrome/browser/extensions/api/sessions/sessions_api.cc
  • components/sessions/core/tab_restore_service_helper.h
  • components/sessions/core/tab_restore_types.h
  • ios/chrome/browser/sessions/model/fake_tab_restore_service.h
  • components/sessions/ios/ios_restore_live_tab.h

Audit Directions

  • Raw pointers cached across reentrancy-capable calls
    Audit any code that stores raw LiveTab*, WebContents*, Browser*, or tab pointers and then calls a UI/activation routine (ShowBrowserWindow, window activation, focus changes) that can pump a nested event loop before the pointer is used; prefer WeakPtr or handle-based access across such calls.
  • `DCHECK`-only guards before a dereference
    Search for DCHECK(...size()), DCHECK(ptr), or similar assertions immediately preceding an indexed access or dereference; these vanish in release builds and must be replaced with real runtime checks on any path reachable from untrusted callers such as extension APIs.
  • Empty-vs-invalid conflation in return values
    Review APIs that fold “no such entry” and “operation completed but produced nothing” into one empty-collection return; callers frequently assume at least one element, so distinguishing the cases (as with the new std::optional) is needed wherever restore/lookup results are indexed without a length check.
From 54db51dbd55d1cab1a3dc053222f4f20a0f38477 Mon Sep 17 00:00:00 2001
From: Joe Mason <[email protected]>
Date: Mon, 14 Sep 2026 22:09:24 -0700
Subject: [PATCH] Reland "Return only live tabs from TabRestoreService restore operations"

This reverts commit a467c37711d44556022a49dbbbce6b1d4bda8964.

Reason for revert: Fixed MSAN errors

Original change's description:
> Revert "Return only live tabs from TabRestoreService restore operations"
>
> This reverts commit b10218380b91df1c63d4eb1a8d4ad50adfa22ea7.
>
> Reason for revert: Newly added tests are failing
>
> Failure Link:
> https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20ChromiumOS%20MSan%20Tests/61349/overview
>
> https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20MSan%20Tests/64914/overview
>
>
> Original change's description:
> > Return only live tabs from TabRestoreService restore operations
> >
> > TabRestoreService restore methods previously collected raw LiveTab
> > pointers before showing and activating the target browser window.
> > If showing the window triggers nested event processing that closes
> > restored tabs or the window before the operation completes, the
> > returned collection could include pointers to destroyed tabs.
> >
> > Track restored tabs using WeakPtr throughout the restore pipeline and
> > filter out any tabs that are no longer valid upon completion. In
> > addition, update SessionsRestoreFunction to handle empty restore results
> > gracefully by returning an error instead of assuming a tab is always
> > present.
> >
> > Fixed: 553132214
> > Change-Id: I007a5f97eecdd692080f23d866ee6cdf80c2a248
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8383396
> > Reviewed-by: Darryl James <[email protected]>
> > Commit-Queue: Joe Mason <[email protected]>
> > Auto-Submit: Joe Mason <[email protected]>
> > Owners-Override: Gabriel Charette <[email protected]>
> > Cr-Commit-Position: refs/heads/main@{#1696548}
>
> Bug: 553132214
> Change-Id: I9fc29f5113c263f1f3cb0c3c3ef5c951ffd6de63
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8405065
> Commit-Queue: Lingqi Chi <[email protected]>
> Bot-Commit: [email protected] <[email protected]>
> Commit-Queue: [email protected] <[email protected]>
> Auto-Submit: Lingqi Chi <[email protected]>
> Owners-Override: Lingqi Chi <[email protected]>
> Cr-Commit-Position: refs/heads/main@{#1697070}

Bug: 553132214
Fixed: 553132214
Change-Id: If281660e94ad3fbf5174416e83581adabebc7aea
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8405169
Reviewed-by: Darryl James <[email protected]>
Owners-Override: Gabriel Charette <[email protected]>
Commit-Queue: Joe Mason <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1697819}
---
diff --git a/chrome/browser/android/recently_closed_tabs_bridge.cc b/chrome/browser/android/recently_closed_tabs_bridge.cc
index 6878063b..87f18c5d 100644
--- a/chrome/browser/android/recently_closed_tabs_bridge.cc
+++ b/chrome/browser/android/recently_closed_tabs_bridge.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/android/recently_closed_tabs_bridge.h"
 
+#include <optional>
+#include <vector>
+
 #include "base/android/jni_array.h"
 #include "base/android/jni_string.h"
 #include "base/android/token_android.h"
@@ -333,11 +336,11 @@
   }
 
   AndroidLiveTabContextRestoreWrapper restore_context(model);
-  std::vector<sessions::LiveTab*> restored_tabs =
+  std::optional<std::vector<sessions::LiveTab*>> restored_tabs =
       tab_restore_service_->RestoreEntryById(
           &restore_context, entry_id,
           static_cast<WindowOpenDisposition>(j_disposition));
-  return !restored_tabs.empty();
+  return !restored_tabs.value_or({}).empty();
 }
 
 bool RecentlyClosedTabsBridge::OpenRecentlyClosedEntry(
@@ -355,12 +358,12 @@
   }
 
   AndroidLiveTabContextRestoreWrapper restore_context(model);
-  std::vector<sessions::LiveTab*> restored_tabs =
+  std::optional<std::vector<sessions::LiveTab*>> restored_tabs =
       tab_restore_service_->RestoreEntryById(
           &restore_context, SessionID::FromSerializedValue(entry_session_id),
           WindowOpenDisposition::NEW_BACKGROUND_TAB);
   RestoreAndroidTabGroups(env, model, restore_context.GetTabGroups());
-  return !restored_tabs.empty();
+  return !restored_tabs.value_or({}).empty();
 }
 
 bool RecentlyClosedTabsBridge::OpenMostRecentlyClosedEntry(JNIEnv* env,
@@ -381,12 +384,12 @@
   // AndroidLiveTabContext. `restore_context` is required to rebuild groups
   // information. To avoid this just use the first entry in entries when
   // restoring.
-  std::vector<sessions::LiveTab*> restored_tabs =
+  std::optional<std::vector<sessions::LiveTab*>> restored_tabs =
       tab_restore_service_->RestoreEntryById(
           &restore_context, tab_restore_service_->entries().front()->id,
           WindowOpenDisposition::NEW_BACKGROUND_TAB);
   RestoreAndroidTabGroups(env, model, restore_context.GetTabGroups());
-  return !restored_tabs.empty();
+  return !restored_tabs.value_or({}).empty();
 }
 
 void RecentlyClosedTabsBridge::ClearRecentlyClosedEntries(JNIEnv* env) {
diff --git a/chrome/browser/extensions/api/sessions/sessions_api.cc b/chrome/browser/extensions/api/sessions/sessions_api.cc
index ef44b5a..c25f1353 100644
--- a/chrome/browser/extensions/api/sessions/sessions_api.cc
+++ b/chrome/browser/extensions/api/sessions/sessions_api.cc
@@ -749,7 +749,9 @@
   }
   std::vector<sessions::LiveTab*> restored_tabs =
       tab_restore_service->RestoreMostRecentEntry(context);
-  DCHECK(restored_tabs.size());
+  if (restored_tabs.empty()) {
+    return RespondNow(Error(kNoActiveTabError));
+  }
 
   sessions::ContentLiveTab* first_tab =
       static_cast<sessions::ContentLiveTab*>(restored_tabs[0]);
@@ -926,17 +928,19 @@
   if (!context) {
     return RespondNow(Error(kNoLiveTabContextError));
   }
-  std::vector<sessions::LiveTab*> restored_tabs =
+  std::optional<std::vector<sessions::LiveTab*>> restored_tabs =
       tab_restore_service->RestoreEntryById(
           context, SessionID::FromSerializedValue(session_id.id()),
           WindowOpenDisposition::UNKNOWN);
-  // If the ID is invalid, restored_tabs will be empty.
-  if (restored_tabs.empty()) {
+  if (!restored_tabs.has_value()) {
     return RespondNow(Error(kInvalidSessionIdError, session_id.ToString()));
   }
+  if (restored_tabs->empty()) {
+    return RespondNow(Error(kNoActiveTabError));
+  }
 
   sessions::ContentLiveTab* first_tab =
-      static_cast<sessions::ContentLiveTab*>(restored_tabs[0]);
+      static_cast<sessions::ContentLiveTab*>((*restored_tabs)[0]);
 
   // Retrieve the window through any of the tabs in restored_tabs.
   if (is_window) {
diff --git a/components/sessions/content/content_live_tab.cc b/components/sessions/content/content_live_tab.cc
index ab86ddab..2d4396f 100644
--- a/components/sessions/content/content_live_tab.cc
+++ b/components/sessions/content/content_live_tab.cc
@@ -69,6 +69,10 @@
   return serialized_ua_override;
 }
 
+base::WeakPtr<LiveTab> ContentLiveTab::GetWeakPtr() {
+  return weak_ptr_factory_.GetWeakPtr();
+}
+
 WEB_CONTENTS_USER_DATA_KEY_IMPL(ContentLiveTab);
 
 }  // namespace sessions
diff --git a/components/sessions/content/content_live_tab.h b/components/sessions/content/content_live_tab.h
index a5ec2743..b6b3f91 100644
--- a/components/sessions/content/content_live_tab.h
+++ b/components/sessions/content/content_live_tab.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_SESSIONS_CONTENT_CONTENT_LIVE_TAB_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/supports_user_data.h"
 #include "components/sessions/content/content_serialized_navigation_builder.h"
 #include "components/sessions/core/live_tab.h"
@@ -43,6 +44,7 @@
   std::unique_ptr<tab_restore::PlatformSpecificTabData>
   GetPlatformSpecificTabData() override;
   SerializedUserAgentOverride GetUserAgentOverride() override;
+  base::WeakPtr<LiveTab> GetWeakPtr() override;
 
  private:
   friend class content::WebContentsUserData<ContentLiveTab>;
@@ -55,6 +57,8 @@
   content::NavigationController& navigation_controller() {
     return GetWebContents().GetController();
   }
+
+  base::WeakPtrFactory<ContentLiveTab> weak_ptr_factory_{this};
 };
 
 }  // namespace sessions
diff --git a/components/sessions/core/live_tab.h b/components/sessions/core/live_tab.h
index d236139..eee207f 100644
--- a/components/sessions/core/live_tab.h
+++ b/components/sessions/core/live_tab.h
@@ -5,6 +5,7 @@
 #ifndef COMPONENTS_SESSIONS_CORE_LIVE_TAB_H_
 #define COMPONENTS_SESSIONS_CORE_LIVE_TAB_H_
 
+#include "base/memory/weak_ptr.h"
 #include "components/sessions/core/serialized_navigation_entry.h"
 #include "components/sessions/core/serialized_user_agent_override.h"
 #include "components/sessions/core/session_id.h"
@@ -40,6 +41,9 @@
 
   // Returns the user agent override, if any.
   virtual SerializedUserAgentOverride GetUserAgentOverride() = 0;
+
+  // Returns a WeakPtr that is invalidated when the tab is destroyed.
+  virtual base::WeakPtr<LiveTab> GetWeakPtr() = 0;
 };
 
 }  // namespace sessions
diff --git a/components/sessions/core/mock_tab_restore_service.h b/components/sessions/core/mock_tab_restore_service.h
index 24d6645..aa57903b 100644
--- a/components/sessions/core/mock_tab_restore_service.h
+++ b/components/sessions/core/mock_tab_restore_service.h
@@ -5,6 +5,7 @@
 #ifndef COMPONENTS_SESSIONS_CORE_MOCK_TAB_RESTORE_SERVICE_H_
 #define COMPONENTS_SESSIONS_CORE_MOCK_TAB_RESTORE_SERVICE_H_
 
+#include <optional>
 #include <vector>
 
 #include "components/sessions/core/tab_restore_service.h"
@@ -109,7 +110,7 @@
               (int num_to_remove),
               (override));
 
-  MOCK_METHOD(std::vector<sessions::LiveTab*>,
+  MOCK_METHOD(std::optional<std::vector<sessions::LiveTab*>>,
               RestoreEntryById,
               (sessions::LiveTabContext * context,
                SessionID id,
diff --git a/components/sessions/core/tab_restore_service.h b/components/sessions/core/tab_restore_service.h
index 24ef1341..8e5e08d8 100644
--- a/components/sessions/core/tab_restore_service.h
+++ b/components/sessions/core/tab_restore_service.h
@@ -113,7 +113,8 @@
 
   // Restores the most recently closed entry. Does nothing if there are no
   // entries to restore. If the most recently restored entry is a tab, it is
-  // added to |context|. Returns the LiveTab instances of the restored tab(s).
+  // added to |context|. Returns the LiveTab instances of the restored tab(s)
+  // that are still open when the restore completes.
   virtual std::vector<LiveTab*> RestoreMostRecentEntry(
       LiveTabContext* context) = 0;
 
@@ -124,12 +125,15 @@
   virtual void RemoveLeastRecentlyUsedEntries(int num_to_remove) = 0;
 
   // Restores an entry by id. If there is no entry with an id matching |id|,
-  // this does nothing. If |context| is NULL, this creates a new window for the
-  // entry. |disposition| is respected, but the attributes (tabstrip index,
-  // browser window) of the tab when it was closed will be respected if
-  // disposition is UNKNOWN. Returns the LiveTab instances of the restored
-  // tab(s).
-  virtual std::vector<LiveTab*> RestoreEntryById(
+  // this does nothing and returns nullopt. Otherwise, returns the LiveTab
+  // instances of the restored tab(s) that are still open when the restore
+  // completes.
+  //
+  // If |context| is NULL, this creates a new window for the entry.
+  // |disposition| is respected, but the attributes (tabstrip index, browser
+  // window) of the tab when it was closed will be respected if disposition is
+  // UNKNOWN.
+  virtual std::optional<std::vector<LiveTab*>> RestoreEntryById(
       LiveTabContext* context,
       SessionID id,
       WindowOpenDisposition disposition) = 0;
diff --git a/components/sessions/core/tab_restore_service_helper.cc b/components/sessions/core/tab_restore_service_helper.cc
index 841b971..f291342 100644
--- a/components/sessions/core/tab_restore_service_helper.cc
+++ b/components/sessions/core/tab_restore_service_helper.cc
@@ -661,7 +661,8 @@
     return std::vector<LiveTab*>();
   }
   return RestoreEntryById(context, entries_.front()->id,
-                          WindowOpenDisposition::UNKNOWN);
+                          WindowOpenDisposition::UNKNOWN)
+      .value();
 }
 
 void TabRestoreServiceHelper::RemoveEntryById(SessionID id) {
@@ -692,7 +693,7 @@
     SessionID id,
     LiveTabContext* context,
     WindowOpenDisposition disposition,
-    std::vector<LiveTab*>* live_tabs) {
+    std::vector<base::WeakPtr<LiveTab>>* live_tabs) {
   // 1. Determine if `id` corresponds to a tab. If so, restore the tab.
   bool found_tab_to_delete = false;
   SessionID::id_type restored_tab_browser_id;
@@ -709,7 +710,9 @@
     context = RestoreTab(tab, context, disposition,
                          sessions::tab_restore::WINDOW, &restored_tab,
                          /*is_restoring_group_or_window=*/false);
-    live_tabs->push_back(restored_tab);
+    if (restored_tab) {
+      live_tabs->push_back(restored_tab->GetWeakPtr());
+    }
 
     // Cleanup.
     std::optional<tab_groups::TabGroupId> group_id = tab.group;
@@ -781,7 +784,9 @@
           DCHECK_EQ(new_context, context);
         }
         context = new_context;
-        live_tabs->push_back(restored_tab);
+        if (restored_tab) {
+          live_tabs->push_back(restored_tab->GetWeakPtr());
+        }
 
         window.tabs.erase(window.tabs.begin() + tab_i);
       }
@@ -833,7 +838,7 @@
             RestoreTab(tab, context, disposition, sessions::tab_restore::WINDOW,
                        &restored_tab, /*is_restoring_group_or_window=*/false);
         if (restored_tab) {
-          live_tabs->push_back(restored_tab);
+          live_tabs->push_back(restored_tab->GetWeakPtr());
           restored_tab_map[tab.id] = restored_tab;
         }
 
@@ -898,14 +903,13 @@
   return group_mapping;
 }
 
-std::vector<LiveTab*> TabRestoreServiceHelper::RestoreEntryById(
+std::optional<std::vector<LiveTab*>> TabRestoreServiceHelper::RestoreEntryById(
     LiveTabContext* context,
     SessionID id,
     WindowOpenDisposition disposition) {
   auto entry_iterator = GetEntryIteratorById(id);
   if (entry_iterator == entries_.end()) {
-    // Don't hoark here, we allow an invalid id.
-    return std::vector<LiveTab*>();
+    return std::nullopt;
   }
 
   if (observer_) {
@@ -923,7 +927,12 @@
   // |context| will be NULL in cases where one isn't already available (eg,
   // when invoked on Mac OS X with no windows open). In this case, create a
   // new browser into which we restore the tabs.
-  std::vector<LiveTab*> live_tabs;
+  //
+  // The restored tabs are tracked with WeakPtrs because restoring involves
+  // deletion-capable calls (e.g. showing and activating browser windows) that
+  // can synchronously close restored tabs, or even the window they were
+  // restored into, before this method returns.
+  std::vector<base::WeakPtr<LiveTab>> live_tabs;
   switch (entry.type) {
     case tab_restore::Type::TAB: {
       auto& tab = static_cast<const Tab&>(entry);
@@ -937,7 +946,9 @@
       LiveTab* restored_tab = nullptr;
       context = RestoreTab(tab, context, disposition, entry.type, &restored_tab,
                            /*is_restoring_group_or_window=*/false);
-      live_tabs.push_back(restored_tab);
+      if (restored_tab) {
+        live_tabs.push_back(restored_tab->GetWeakPtr());
+      }
       context->ShowBrowserWindow();
       break;
     }
@@ -1000,7 +1011,7 @@
             client_->OnTabRestored(
                 tab->navigations.at(tab->current_navigation_index)
                     .virtual_url());
-            live_tabs.push_back(restored_tab);
+            live_tabs.push_back(restored_tab->GetWeakPtr());
             restored_tab_map[tab->id] = restored_tab;
           }
 
@@ -1096,7 +1107,7 @@
               *tab.get(), context->GetTabCount(), group.tabs[0]->id == tab->id,
               /*restored_from_group_or_window_context=*/true, entry.type);
           if (restored_tab) {
-            live_tabs.push_back(restored_tab);
+            live_tabs.push_back(restored_tab->GetWeakPtr());
             restored_tab_map[tab->id] = restored_tab;
           }
           if (tab->split_id.has_value()) {
@@ -1135,7 +1146,9 @@
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/sessions/sessions_apitest.cc b/chrome/browser/extensions/api/sessions/sessions_apitest.cc
index 21687b64..9f83766 100644
--- a/chrome/browser/extensions/api/sessions/sessions_apitest.cc
+++ b/chrome/browser/extensions/api/sessions/sessions_apitest.cc
@@ -6,6 +6,7 @@
 
 #include <array>
 #include <memory>
+#include <string>
 #include <utility>
 
 #include "base/command_line.h"
@@ -14,7 +15,9 @@
 #include "base/functional/callback.h"
 #include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
+#include "base/memory/raw_ptr.h"
 #include "base/path_service.h"
+#include "base/scoped_observation.h"
 #include "base/strings/pattern.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
@@ -47,6 +50,8 @@
 #include "chrome/test/base/testing_browser_process.h"
 #include "components/sessions/content/content_live_tab.h"
 #include "components/sessions/core/session_id.h"
+#include "components/sessions/core/tab_restore_service.h"
+#include "components/sessions/core/tab_restore_service_observer.h"
 #include "components/sync/base/client_tag_hash.h"
 #include "components/sync/engine/data_type_activation_response.h"
 #include "components/sync/model/data_type_activation_request.h"
@@ -58,6 +63,7 @@
 #include "components/sync/test/mock_data_type_worker.h"
 #include "components/sync_sessions/session_store.h"
 #include "components/sync_sessions/session_sync_service.h"
+#include "components/tabs/public/tab_interface.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "extensions/browser/api_test_utils.h"
@@ -65,8 +71,10 @@
 #include "extensions/buildflags/buildflags.h"
 #include "extensions/common/extension_builder.h"
 #include "google_apis/gaia/gaia_id.h"
+#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
 #include "ui/base/base_window.h"
 #include "ui/base/page_transition_types.h"
+#include "url/gurl.h"
 
 #if BUILDFLAG(IS_CHROMEOS)
 #include "ash/constants/ash_switches.h"
@@ -595,6 +603,92 @@
       << error;
 }
 
+namespace {
+
+// Closes all tabs created by an in-progress restore as soon as the tab restore
+// service reports a change, i.e. before the restore operation returns.
+class ClosingTabRestoreServiceObserver
+    : public sessions::TabRestoreServiceObserver {
+ public:
+  ClosingTabRestoreServiceObserver(sessions::TabRestoreService* service,
+                                   BrowserWindowInterface* browser)
+      : browser_(browser) {
+    observation_.Observe(service);
+    for (tabs::TabInterface* tab :
+         TabListInterface::From(browser_)->GetAllTabs()) {
+      initial_tab_handles_.insert(tab->GetHandle());
+    }
+  }
+
+  ~ClosingTabRestoreServiceObserver() override = default;
+
+  size_t num_closed_tabs() const { return num_closed_tabs_; }
+
+  // sessions::TabRestoreServiceObserver:
+  void TabRestoreServiceChanged(sessions::TabRestoreService* service) override {
+    if (!service->IsRestoring() || num_closed_tabs_ > 0) {
+      return;
+    }
+    TabListInterface* tab_list = TabListInterface::From(browser_);
+    for (tabs::TabInterface* tab : tab_list->GetAllTabs()) {
+      if (!initial_tab_handles_.contains(tab->GetHandle())) {
+        ++num_closed_tabs_;
+        tab_list->CloseTab(tab->GetHandle());
+      }
+    }
+  }
+
+  void TabRestoreServiceDestroyed(
+      sessions::TabRestoreService* service) override {
+    observation_.Reset();
+  }
+
+ private:
+  raw_ptr<BrowserWindowInterface> browser_;
+  base::ScopedObservation<sessions::TabRestoreService,
+                          sessions::TabRestoreServiceObserver>
+      observation_{this};
+  absl::flat_hash_set<tabs::TabInterface::Handle> initial_tab_handles_;
+  size_t num_closed_tabs_ = 0;
+};
+
+}  // namespace
+
+// Tests that chrome.sessions.restore() returns an error rather than stale tab
+// information when the restored tab is closed again before the restore
+// operation finishes.
+IN_PROC_BROWSER_TEST_F(ExtensionSessionsTest,
+                       RestoreErrorWhenTabClosedDuringRestore) {
+  BrowserWindowInterface* browser = browser_window_interface();
+  TabListInterface* tab_list = TabListInterface::From(browser);
+  ASSERT_TRUE(tab_list);
+
+  // Open a tab, navigate it, and close it to create a recently closed tab
+  // entry. The navigation is needed for the closed tab to be persisted in the
+  // tab restore service.
+  tabs::TabInterface* tab = tab_list->OpenTab(GURL("about:blank"), /*index=*/1);
+  ASSERT_TRUE(tab);
+  ASSERT_TRUE(NavigateToURL(tab->GetContents(), GURL("chrome://version/")));
+  tab_list->CloseTab(tab->GetHandle());
+
+  sessions::TabRestoreService* tab_restore_service =
+      TabRestoreServiceFactory::GetForProfile(GetProfile());
+  ASSERT_TRUE(tab_restore_service);
+  ASSERT_EQ(1u, tab_restore_service->entries().size());
+  const int tab_count = tab_list->GetTabCount();
+
+  // Close the restored tab while the restore is still in progress.
+  ClosingTabRestoreServiceObserver closing_observer(tab_restore_service,
+                                                    browser);
+  std::string error = utils::RunFunctionAndReturnError(
+      CreateFunction<SessionsRestoreFunction>(true).get(), /*args=*/"[]",
+      GetProfile());
+
+  EXPECT_EQ(closing_observer.num_closed_tabs(), 1u);
+  EXPECT_EQ("No active tab.", error);
+  EXPECT_EQ(tab_count, tab_list->GetTabCount());
+}
+
 // Tests chrome.sessions.getRecentlyClosed() for windows. Opens a second browser
 // window with two tabs, closes it, then calls the extension API function and
 // verifies one window with two tabs was recently closed.
diff --git a/chrome/browser/sessions/session_restore_browsertest.cc b/chrome/browser/sessions/session_restore_browsertest.cc
index e3620151..60c3c3c 100644
--- a/chrome/browser/sessions/session_restore_browsertest.cc
+++ b/chrome/browser/sessions/session_restore_browsertest.cc
@@ -8,6 +8,7 @@
 
 #include <array>
 #include <memory>
+#include <optional>
 #include <ranges>
 #include <set>
 #include <string_view>
@@ -783,11 +784,13 @@
     if (tab.navigations[0].virtual_url() == url2) {
       timestamp = tab.navigations[0].timestamp();
       http_status_code = tab.navigations[0].http_status_code();
-      std::vector<sessions::LiveTab*> content = service->RestoreEntryById(
-          nullptr, tab.id, WindowOpenDisposition::UNKNOWN);
-      ASSERT_EQ(1U, content.size());
+      std::optional<std::vector<sessions::LiveTab*>> content =
+          service->RestoreEntryById(nullptr, tab.id,
+                                    WindowOpenDisposition::UNKNOWN);
+      ASSERT_TRUE(content.has_value());
+      ASSERT_EQ(1U, content->size());
       sessions::ContentLiveTab* live_tab =
-          static_cast<sessions::ContentLiveTab*>(content[0]);
+          static_cast<sessions::ContentLiveTab*>((*content)[0]);
       ASSERT_TRUE(live_tab);
       EXPECT_EQ(url2, live_tab->GetWebContents().GetURL());
       break;
diff --git a/chrome/browser/sessions/tab_restore_browsertest.cc b/chrome/browser/sessions/tab_restore_browsertest.cc
index cf14bf9e..d7f5e03 100644
--- a/chrome/browser/sessions/tab_restore_browsertest.cc
+++ b/chrome/browser/sessions/tab_restore_browsertest.cc
@@ -2368,7 +2368,7 @@
 
   // Restore the window.
   browser_created_observer.emplace();
-  std::vector<sessions::LiveTab*> restored_window_tabs =
+  std::optional<std::vector<sessions::LiveTab*>> restored_window_tabs =
       service->RestoreEntryById(BrowserLiveTabContext::From(second_browser),
                                 entries.front()->id,
                                 WindowOpenDisposition::NEW_FOREGROUND_TAB);
@@ -2379,7 +2379,8 @@
   // We will opt to open the saved group instead of individually restoring all
   // of the tabs in the group one at a time. Because of this, RestoreEntryById
   // will only return one tab as being restored.
-  ASSERT_EQ(1u, restored_window_tabs.size());
+  ASSERT_TRUE(restored_window_tabs.has_value());
+  ASSERT_EQ(1u, restored_window_tabs->size());
 
   ASSERT_NE(second_browser, third_browser);
   ASSERT_EQ(3, third_browser->GetTabStripModel()->count());
diff --git a/chrome/browser/sessions/tab_restore_service_unittest.cc b/chrome/browser/sessions/tab_restore_service_unittest.cc
index e4678a67..a4d0995 100644
--- a/chrome/browser/sessions/tab_restore_service_unittest.cc
+++ b/chrome/browser/sessions/tab_restore_service_unittest.cc
@@ -9,11 +9,13 @@
 #include <optional>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "base/compiler_specific.h"
 #include "base/functional/bind.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
@@ -39,12 +41,14 @@
 #include "components/sessions/content/content_live_tab.h"
 #include "components/sessions/content/content_test_helper.h"
 #include "components/sessions/content/session_tab_helper.h"
+#include "components/sessions/core/live_tab.h"
 #include "components/sessions/core/serialized_navigation_entry_test_helper.h"
 #include "components/sessions/core/session_id.h"
 #include "components/sessions/core/session_types.h"
 #include "components/sessions/core/tab_restore_service_client.h"
 #include "components/sessions/core/tab_restore_service_impl.h"
 #include "components/sessions/core/tab_restore_service_observer.h"
+#include "components/sessions/core/tab_restore_types.h"
 #include "components/split_tabs/split_tab_id.h"
 #include "components/split_tabs/split_tab_visual_data.h"
 #include "components/tab_groups/tab_group_id.h"
@@ -74,7 +78,10 @@
 using sessions::SerializedNavigationEntryTestHelper;
 
 using ::testing::_;
+using ::testing::IsEmpty;
+using ::testing::Optional;
 using ::testing::Return;
+using ::testing::UnorderedElementsAre;
 
 class MockLiveTab : public sessions::LiveTab {
  public:
@@ -92,6 +99,13 @@
       GetPlatformSpecificTabData,
       std::unique_ptr<sessions::tab_restore::PlatformSpecificTabData>());
   MOCK_METHOD0(GetUserAgentOverride, sessions::SerializedUserAgentOverride());
+
+  base::WeakPtr<sessions::LiveTab> GetWeakPtr() override {
+    return weak_ptr_factory_.GetWeakPtr();
+  }
+
+ private:
+  base::WeakPtrFactory<MockLiveTab> weak_ptr_factory_{this};
 };
 
 class MockLiveTabContext : public sessions::LiveTabContext {
@@ -1729,3 +1743,102 @@
   EXPECT_EQ(2, restored_tab_count);
   EXPECT_EQ(max_entries - 1, service_->entries().size());
 }
+
+// Restoring a window with a tab that's closed again before the restore
+// completes (e.g. while the browser window is being shown) excludes that tab
+// from the returned vector.
+TEST_F(TabRestoreServiceImplWithMockClientTest,
+       RestoreWindowEntryOmitsTabsClosedDuringRestore) {
+  // Create a window entry with three tabs.
+  auto window = std::make_unique<Window>();
+  for (int i = 0; i < 3; ++i) {
+    auto tab = std::make_unique<Tab>();
+    tab->navigations.push_back(ContentTestHelper::CreateNavigation(
+        base::StringPrintf("http://%d", i), base::NumberToString(i)));
+    tab->current_navigation_index = 0;
+    window->tabs.push_back(std::move(tab));
+  }
+  window->selected_tab_index = 0;
+  const SessionID entry_id = window->id;
+  mutable_entries()->push_back(std::move(window));
+
+  testing::NiceMock<MockLiveTabContext> restore_context;
+  ON_CALL(restore_context, GetSessionID())
+      .WillByDefault(Return(SessionID::NewUnique()));
+  ON_CALL(*mock_tab_restore_service_client_,
+          CreateLiveTabContext(_, _, _, _, _, _, _, _))
+      .WillByDefault(Return(&restore_context));
+
+  auto restored_tab1 = std::make_unique<testing::NiceMock<MockLiveTab>>();
+  auto restored_tab2 = std::make_unique<testing::NiceMock<MockLiveTab>>();
+  EXPECT_CALL(restore_context, AddRestoredTab(_, _, _, _, _))
+      .WillOnce(Return(restored_tab1.get()))
+      .RetiresOnSaturation();
+  EXPECT_CALL(restore_context, AddRestoredTab(_, _, _, _, _))
+      .WillOnce(Return(restored_tab2.get()))
+      .RetiresOnSaturation();
+  // A tab that can't be created for a tab entry should be excluded from the
+  // list, rather than included as a nullptr.
+  EXPECT_CALL(restore_context, AddRestoredTab(_, _, _, _, _))
+      .WillOnce(Return(nullptr))
+      .RetiresOnSaturation();
+
+  // Close the first restored tab while the window is being shown, which happens
+  // before RestoreEntryById() returns.
... (truncated)
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.