High watch ✔︎ verified CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PerformanceManager
DescriptionUse after free in PerformanceManager
ComponentPerformanceManager
Sub-componentInternals>PerformanceManager
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker513505927
Fix commit47758a7c428a (chromium/src) +67/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-05-20 (public git)
Disclosed2026-05-27
Patch gap7 days — fix public in source before the advisory

Background

`PageDiscardingHelper`
a Performance Manager policy that selects and discards background tabs to reclaim memory (page_discarding_helper.cc:135).
`TabPageDecorator::TabHandle`
a stable handle to a tab whose page_node() tracks the tab’s current PageNode across discards, owned by the decorator via a std::unique_ptr (tab_page_decorator.h:70, tab_page_decorator.cc:31).
`DiscardPageNode`
the PageDiscarder call that synchronously performs a tab discard, which can run observers and replace or destroy the tab’s PageNode (page_discarding_helper.cc:245-246).
`base::WeakPtr`
a pointer that automatically becomes null when the pointee is destroyed.

Root Cause Analysis

In PageDiscardingHelper::DiscardMultiplePagesImpl, the pre-fix code captures a bare TabPageDecorator::TabHandle* at page_discarding_helper.cc:239 and assigns it from TabPageDecorator::FromPageNode(node) at :241, before performing the discard. It then calls page_discarder_->DiscardPageNode(node, discard_reason) synchronously at :245-246, and on success dereferences the saved pointer at :261-265: tab_handle->page_node() followed by node_after_discard->GetWebContents().get(). The invariant the code relied on is stated in the header: a TabHandle lives until OnBeforeTabRemoved and survives discards (tab_page_decorator.h:63-68). That invariant does not hold if an observer run during the discard closes the tab, because the TabHandle is owned by the node’s TabPageDecorator::Data (tab_page_decorator.cc:31) and is destroyed when that data is destroyed, leaving the stack pointer dangling across DiscardPageNode.

The fix changes the local to base::WeakPtr<TabPageDecorator::TabHandle> and assigns it from the new TabPageDecorator::WeakHandleFromPageNode(node); it adds a ~TabHandle() and a base::WeakPtrFactory<TabHandle> weak_factory_ member (tab_page_decorator.h), so if the handle is destroyed during the discard the weak pointer evaluates to null and the subsequent dereference is skipped. The header comment added at tab_page_decorator.h makes the hazard explicit: observers triggered by the discard “might delete the PageNode which WILL delete the Tabhandle.”

Key insight
The code assumed a TabHandle always outlives a synchronous discard, but an observer can close the tab mid-discard and free it; switching the cached pointer to a base::WeakPtr makes the stale-handle case observable and safe.

Attack Path

  1. Trigger a discard Cause the Performance Manager to run DiscardMultiplePagesImpl, e.g. under memory pressure (page_discarding_helper.cc:135-136).
  2. Capture the handle The helper saves a raw TabPageDecorator::TabHandle* for the first discard candidate (:239-241).
  3. Close the tab during discard An observer invoked synchronously inside DiscardPageNode closes the tab, destroying its PageNode and the TabHandle it owns (:245-246; owner relationship at tab_page_decorator.cc:31). (inferred that a specific observer performs the close)
  4. Use freed handle After DiscardPageNode returns, tab_handle->page_node() and node_after_discard->GetWebContents() dereference freed memory (:261-265).
  5. Reclaim the slot An attacker who can reclaim the freed allocation could influence the read pointer and the following virtual call. (inferred)

Impact Assessment

A successful trigger yields a use-after-free read, and potentially a controlled pointer dereference and virtual call, in the unsandboxed browser process. The advisory rates this high severity with no in-the-wild exploitation. Escalation to code execution is (inferred); the shown code demonstrates only the dangling dereference.

Changed Functions

FunctionChangeNotes
WeakHandleFromPageNode
components/performance_manager/decorators/tab_page_decorator.cc
modified
if
components/performance_manager/decorators/tab_page_decorator.cc
modified
TEST_F
components/performance_manager/decorators/tab_page_decorator_unittest.cc
modified
TabPageDecorator
components/performance_manager/public/decorators/tab_page_decorator.h
modified

Files Changed

  • chrome/browser/performance_manager/policies/page_discarding_helper.cc
  • components/performance_manager/decorators/tab_page_decorator.cc
  • components/performance_manager/decorators/tab_page_decorator_unittest.cc
  • components/performance_manager/public/decorators/tab_page_decorator.h

Audit Directions

  • Raw pointers across synchronous callbacks
    Flag any bare pointer or reference held across a call that can run observers (e.g. discard, close, notify) and re-validate or use base::WeakPtr.
  • Owned-handle lifetime assumptions
    When a comment guarantees an object lives until a specific callback, verify no re-entrant path destroys its owner earlier.
  • Observer-driven re-entrancy
    Audit TabStripModelObserver/PageNodeObserver notifications fired during memory operations for tab-close side effects that invalidate in-flight state.
From 47758a7c428a3b362a861512545406b1e0d7162f Mon Sep 17 00:00:00 2001
From: Joe Mason <[email protected]>
Date: Wed, 20 May 2026 11:12:37 -0700
Subject: [PATCH] Use a WeakPtr in TabHandle

Bug: 513505927
Change-Id: I8a0076507f6c0a0a9c120ffbdc5d0f4b26d716f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7862081
Commit-Queue: Patrick Monette <[email protected]>
Reviewed-by: Patrick Monette <[email protected]>
Auto-Submit: Joe Mason <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633710}
---

diff --git a/chrome/browser/performance_manager/policies/page_discarding_helper.cc b/chrome/browser/performance_manager/policies/page_discarding_helper.cc
index 1becb53..0c30392 100644
--- a/chrome/browser/performance_manager/policies/page_discarding_helper.cc
+++ b/chrome/browser/performance_manager/policies/page_discarding_helper.cc
@@ -13,6 +13,7 @@
 #include "base/containers/flat_set.h"
 #include "base/feature_list.h"
 #include "base/logging.h"
+#include "base/memory/weak_ptr.h"
 #include "base/metrics/histogram_macros.h"
 #include "build/build_config.h"
 #include "chrome/browser/performance_manager/policies/policy_features.h"
@@ -236,9 +237,9 @@
 
     // PageNode may be replaced after discard. TabHandle is not replaced after
     // discard.
-    TabPageDecorator::TabHandle* tab_handle;
+    base::WeakPtr<TabPageDecorator::TabHandle> tab_handle;
     if (!result.first_discard_time.has_value()) {
-      tab_handle = TabPageDecorator::FromPageNode(node);
+      tab_handle = TabPageDecorator::WeakHandleFromPageNode(node);
     }
 
     // Do the discard.
diff --git a/components/performance_manager/decorators/tab_page_decorator.cc b/components/performance_manager/decorators/tab_page_decorator.cc
index 1fcbfe52..3a979d2 100644
--- a/components/performance_manager/decorators/tab_page_decorator.cc
+++ b/components/performance_manager/decorators/tab_page_decorator.cc
@@ -31,6 +31,16 @@
   std::unique_ptr<TabHandle> tab_handle_;
 };
 
+TabPageDecorator::TabHandle::TabHandle(const PageNode* page_node)
+    : page_node_(page_node) {}
+
+TabPageDecorator::TabHandle::~TabHandle() = default;
+
+base::WeakPtr<TabPageDecorator::TabHandle>
+TabPageDecorator::TabHandle::GetWeakPtr() {
+  return weak_factory_.GetWeakPtr();
+}
+
 TabPageDecorator::TabPageDecorator() = default;
 TabPageDecorator::~TabPageDecorator() = default;
 
@@ -56,6 +66,16 @@
   return data->tab_handle();
 }
 
+// static
+base::WeakPtr<TabPageDecorator::TabHandle>
+TabPageDecorator::WeakHandleFromPageNode(const PageNode* page_node) {
+  auto* tab_handle = TabPageDecorator::FromPageNode(page_node);
+  if (tab_handle) {
+    return tab_handle->GetWeakPtr();
+  }
+  return nullptr;
+}
+
 void TabPageDecorator::MaybeTabCreated(const PageNode* page_node) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (page_node->GetType() != performance_manager::PageType::kTab) {
diff --git a/components/performance_manager/decorators/tab_page_decorator_unittest.cc b/components/performance_manager/decorators/tab_page_decorator_unittest.cc
index 7808b2a..097f650b 100644
--- a/components/performance_manager/decorators/tab_page_decorator_unittest.cc
+++ b/components/performance_manager/decorators/tab_page_decorator_unittest.cc
@@ -8,6 +8,7 @@
 #include <utility>
 
 #include "base/feature_list.h"
+#include "base/memory/weak_ptr.h"
 #include "components/performance_manager/graph/page_node_impl.h"
 #include "components/performance_manager/test_support/graph_test_harness.h"
 #include "components/performance_manager/test_support/mock_graphs.h"
@@ -64,6 +65,8 @@
       .Times(1);
 
   EXPECT_EQ(TabPageDecorator::FromPageNode(mock_graph.page.get()), nullptr);
+  EXPECT_EQ(TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get()),
+            nullptr);
 
   mock_graph.page->SetType(PageType::kTab);
 
@@ -72,8 +75,13 @@
   EXPECT_NE(handle, nullptr);
   EXPECT_EQ(handle->page_node(), mock_graph.page.get());
 
+  base::WeakPtr<TabPageDecorator::TabHandle> weak_handle =
+      TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get());
+  EXPECT_EQ(weak_handle.get(), handle);
+
   mock_graph.frame.reset();
   mock_graph.page.reset();
+  EXPECT_FALSE(weak_handle);
 }
 
 TEST_F(TabPageDecoratorTest, TestDiscarding) {
@@ -88,6 +96,8 @@
       .Times(1);
 
   EXPECT_EQ(TabPageDecorator::FromPageNode(mock_graph.page.get()), nullptr);
+  EXPECT_EQ(TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get()),
+            nullptr);
 
   mock_graph.page->SetType(PageType::kTab);
 
@@ -96,6 +106,10 @@
   EXPECT_NE(handle, nullptr);
   EXPECT_EQ(handle->page_node(), mock_graph.page.get());
 
+  base::WeakPtr<TabPageDecorator::TabHandle> weak_handle =
+      TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get());
+  EXPECT_EQ(weak_handle.get(), handle);
+
   auto new_page_node = TestNodeWrapper<PageNodeImpl>::Create(graph());
   // When kWebContentsDiscard is enabled, the page node is not replaced.
   auto& page_node_after_discard =
@@ -114,8 +128,19 @@
 
   mock_graph.page->OnAboutToBeDiscarded(page_node_after_discard->GetWeakPtr());
 
+  // WeakPtr should not be reset during discard.
+  EXPECT_TRUE(weak_handle);
+
   mock_graph.frame.reset();
   mock_graph.page.reset();
+  if (base::FeatureList::IsEnabled(::features::kWebContentsDiscard)) {
+    // PageNode doesn't change during discard so WeakPtr is reset now.
+    EXPECT_FALSE(weak_handle);
+  } else {
+    EXPECT_TRUE(weak_handle);
+    new_page_node.reset();
+    EXPECT_FALSE(weak_handle);
+  }
 }
 
 }  // namespace performance_manager
diff --git a/components/performance_manager/public/decorators/tab_page_decorator.h b/components/performance_manager/public/decorators/tab_page_decorator.h
index eef7554..8cd9a23e 100644
--- a/components/performance_manager/public/decorators/tab_page_decorator.h
+++ b/components/performance_manager/public/decorators/tab_page_decorator.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_PERFORMANCE_MANAGER_PUBLIC_DECORATORS_TAB_PAGE_DECORATOR_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "base/observer_list_types.h"
 #include "base/sequence_checker.h"
@@ -39,6 +40,16 @@
   // tab. Returns nullptr if `page_node` is not a tab.
   static TabPageDecorator::TabHandle* FromPageNode(const PageNode* page_node);
 
+  // Returns a WeakPtr to the `TabHandle` associated with `page_node`, or
+  // nullptr if `page_node` is not a tab. Although the TabHandle remains
+  // constant during tab discard, observers triggered by the discard might
+  // delete the PageNode which WILL delete the Tabhandle. So any caller that
+  // holds a pointer to a TabHandle during a discard must either observe
+  // OnBeforeTabRemoved() and invalidate the pointer when it's called, or hold a
+  // WeakPtr instead.
+  static base::WeakPtr<TabPageDecorator::TabHandle> WeakHandleFromPageNode(
+      const PageNode* page_node);
+
  private:
   void MaybeTabCreated(const PageNode* page_node);
 
@@ -67,19 +78,26 @@
 // `TabPageObserver::OnBeforeTabRemoved` is called for said `TabHandle` object,
 // and they are guaranteed that `page_node()` will return the `PageNode`
 // associated with the tab.
+//
+// TODO(crbug.com/487681765): Migrate to TabInterface.
 class TabPageDecorator::TabHandle {
  public:
+  ~TabHandle();
+
   const PageNode* page_node() const { return page_node_; }
 
+  base::WeakPtr<TabHandle> GetWeakPtr();
+
  private:
   friend class TabPageDecorator::Data;
   friend class TabPageDecorator;
 
-  explicit TabHandle(const PageNode* page_node) : page_node_(page_node) {}
+  explicit TabHandle(const PageNode* page_node);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/performance_manager/decorators/tab_page_decorator_unittest.cc b/components/performance_manager/decorators/tab_page_decorator_unittest.cc
index 7808b2a..097f650b 100644
--- a/components/performance_manager/decorators/tab_page_decorator_unittest.cc
+++ b/components/performance_manager/decorators/tab_page_decorator_unittest.cc
@@ -8,6 +8,7 @@
 #include <utility>
 
 #include "base/feature_list.h"
+#include "base/memory/weak_ptr.h"
 #include "components/performance_manager/graph/page_node_impl.h"
 #include "components/performance_manager/test_support/graph_test_harness.h"
 #include "components/performance_manager/test_support/mock_graphs.h"
@@ -64,6 +65,8 @@
       .Times(1);
 
   EXPECT_EQ(TabPageDecorator::FromPageNode(mock_graph.page.get()), nullptr);
+  EXPECT_EQ(TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get()),
+            nullptr);
 
   mock_graph.page->SetType(PageType::kTab);
 
@@ -72,8 +75,13 @@
   EXPECT_NE(handle, nullptr);
   EXPECT_EQ(handle->page_node(), mock_graph.page.get());
 
+  base::WeakPtr<TabPageDecorator::TabHandle> weak_handle =
+      TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get());
+  EXPECT_EQ(weak_handle.get(), handle);
+
   mock_graph.frame.reset();
   mock_graph.page.reset();
+  EXPECT_FALSE(weak_handle);
 }
 
 TEST_F(TabPageDecoratorTest, TestDiscarding) {
@@ -88,6 +96,8 @@
       .Times(1);
 
   EXPECT_EQ(TabPageDecorator::FromPageNode(mock_graph.page.get()), nullptr);
+  EXPECT_EQ(TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get()),
+            nullptr);
 
   mock_graph.page->SetType(PageType::kTab);
 
@@ -96,6 +106,10 @@
   EXPECT_NE(handle, nullptr);
   EXPECT_EQ(handle->page_node(), mock_graph.page.get());
 
+  base::WeakPtr<TabPageDecorator::TabHandle> weak_handle =
+      TabPageDecorator::WeakHandleFromPageNode(mock_graph.page.get());
+  EXPECT_EQ(weak_handle.get(), handle);
+
   auto new_page_node = TestNodeWrapper<PageNodeImpl>::Create(graph());
   // When kWebContentsDiscard is enabled, the page node is not replaced.
   auto& page_node_after_discard =
@@ -114,8 +128,19 @@
 
   mock_graph.page->OnAboutToBeDiscarded(page_node_after_discard->GetWeakPtr());
 
+  // WeakPtr should not be reset during discard.
+  EXPECT_TRUE(weak_handle);
+
   mock_graph.frame.reset();
   mock_graph.page.reset();
+  if (base::FeatureList::IsEnabled(::features::kWebContentsDiscard)) {
+    // PageNode doesn't change during discard so WeakPtr is reset now.
+    EXPECT_FALSE(weak_handle);
+  } else {
+    EXPECT_TRUE(weak_handle);
+    new_page_node.reset();
+    EXPECT_FALSE(weak_handle);
+  }
 }
 
 }  // namespace performance_manager
Loading diff…

Original Bug Report

reported by [email protected]

Potential Browser-process Use-After-Free in PageDiscardingHelper::DiscardMultiplePagesImpl

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A Use-After-Free (UAF) vulnerability potentially exists in the browser process due to a raw stack pointer being held across a synchronous discard operation. If a tab is synchronously closed during the discard process via an observer gadget, the pointer dangles and is subsequently dereferenced. This could lead to remote code execution in the unsandboxed browser process.

Affected files:

  • chrome/browser/performance_manager/policies/page_discarding_helper.cc
  • components/performance_manager/decorators/tab_page_decorator.cc
  • chrome/browser/resource_coordinator/tab_lifecycle_unit.cc
  • chrome/browser/ui/tabs/tab_strip_model.cc
  • components/performance_manager/performance_manager_tab_helper.cc
  • components/performance_manager/graph/page_node_impl.cc

Estimated timestamp from git blame: 2025-11-26

Analysis

A potential Use-After-Free (UAF) vulnerability has been identified in PageDiscardingHelper::DiscardMultiplePagesImpl within the browser process. This issue arises from the use of a bare stack pointer to a TabPageDecorator::TabHandle object that is held across a synchronous discard operation.

In the current Chromium codebase, the Performance Manager graph and its associated policies, including PageDiscardingHelper, reside on the UI thread. This allows for synchronous interaction with UI-bound components like WebContents and the TabStripModel.

Root Cause

The vulnerability is located in chrome/browser/performance_manager/policies/page_discarding_helper.cc:

// Capture a raw pointer to the TabHandle
TabPageDecorator::TabHandle* tab_handle;
if (!result.first_discard_time.has_value()) {
  tab_handle = TabPageDecorator::FromPageNode(node);
}

// Perform the discard synchronously on the UI thread
std::optional<base::ByteSize> estimated_memory_freed =
    page_discarder_->DiscardPageNode(node, discard_reason);

if (estimated_memory_freed.has_value()) {
  if (tab_handle) {
    // tab_handle may now be a dangling pointer
    const PageNode* node_after_discard = tab_handle->page_node();  // UAF Read
    if (node_after_discard) {
      result.first_content_after_discard =
          node_after_discard->GetWebContents().get();        // Potential Controlled Virtual Call
    }
  }
}

During the call to DiscardPageNode, the TabLifecycleUnit performs a synchronous replacement of the WebContents. During this process, TabPageDecorator moves the TabHandle from the old PageNode to the newly created replacement PageNode.

Critically, TabLifecycleUnit::FinishDiscard triggers notifications to TabStripModelObservers. If an observer (such as DevTools or a component managing Saved Tab Groups) synchronously closes the replacement tab, the new PageNode is destroyed. This destruction synchronously deletes the TabPageDecorator::Data attached to the node, which owns the TabHandle object via a std::unique_ptr. When DiscardPageNode returns, the tab_handle variable on the stack dangles.

Potential Impact

A subsequent dereference of tab_handle results in a UAF read. An attacker who can reclaim the freed memory (e.g., via heap spraying using Mojo allocations) could control the returned node_after_discard pointer. This leads to a controlled virtual function call (GetWebContents()), potentially allowing for arbitrary code execution in the unsandboxed browser process, effectively escaping the renderer sandbox.

Potential Reproduction Steps

  1. From a compromised renderer, induce MEMORY_PRESSURE_LEVEL_CRITICAL to trigger the UrgentPageDiscardingPolicy.
  2. Ensure a TabStripModelObserver gadget is active (e.g., a DevTools window or an observer that reacts to tab replacements) which will synchronously call CloseWebContentsAt for the replacement tab created during discard.
  3. The PageDiscardingHelper::DiscardMultiplePagesImpl will capture the TabHandle pointer, call the synchronous discard, and then dereference the pointer after it has been freed by the re-entrant closure.
  4. Use heap spraying primitives in the browser process to reclaim the TabHandle memory slot before the dereference occurs.

Suggested Fix

The TabHandle should not be stored as a raw pointer across the synchronous DiscardPageNode call. Instead, the code should either re-query the TabHandle from the graph using a WeakPtr<PageNode> or the TabHandle itself should be managed via a base::WeakPtr if supported by the decorator. Alternatively, verify the continued existence of the tab before accessing the pointer.

Note: These findings are based on manual code analysis. No working Proof-of-Concept has been executed.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker