CVE-2026-10001
Overview
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 currentPageNodeacross discards, owned by the decorator via astd::unique_ptr(tab_page_decorator.h:70,tab_page_decorator.cc:31). - `DiscardPageNode`
- the
PageDiscardercall that synchronously performs a tab discard, which can run observers and replace or destroy the tab’sPageNode(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.”
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
- Trigger a discard
Cause the Performance Manager to run
DiscardMultiplePagesImpl, e.g. under memory pressure (page_discarding_helper.cc:135-136). - Capture the handle
The helper saves a raw
TabPageDecorator::TabHandle*for the first discard candidate (:239-241). - Close the tab during discard
An observer invoked synchronously inside
DiscardPageNodecloses the tab, destroying itsPageNodeand theTabHandleit owns (:245-246; owner relationship attab_page_decorator.cc:31). (inferred that a specific observer performs the close) - Use freed handle
After
DiscardPageNodereturns,tab_handle->page_node()andnode_after_discard->GetWebContents()dereference freed memory (:261-265). - Reclaim the slot An attacker who can reclaim the freed allocation could influence the read pointer and the following virtual call. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WeakHandleFromPageNodecomponents/performance_manager/decorators/tab_page_decorator.cc |
modified | |
ifcomponents/performance_manager/decorators/tab_page_decorator.cc |
modified | |
TEST_Fcomponents/performance_manager/decorators/tab_page_decorator_unittest.cc |
modified | |
TabPageDecoratorcomponents/performance_manager/public/decorators/tab_page_decorator.h |
modified |
Files Changed
chrome/browser/performance_manager/policies/page_discarding_helper.cccomponents/performance_manager/decorators/tab_page_decorator.cccomponents/performance_manager/decorators/tab_page_decorator_unittest.cccomponents/performance_manager/public/decorators/tab_page_decorator.h
Audit Directions
- Raw pointers across synchronous callbacksFlag 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 assumptionsWhen a comment guarantees an object lives until a specific callback, verify no re-entrant path destroys its owner earlier.
- Observer-driven re-entrancyAudit
TabStripModelObserver/PageNodeObservernotifications fired during memory operations for tab-close side effects that invalidate in-flight state.
Patch
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);
Regression Test / PoC
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
Original Bug Report
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.cccomponents/performance_manager/decorators/tab_page_decorator.ccchrome/browser/resource_coordinator/tab_lifecycle_unit.ccchrome/browser/ui/tabs/tab_strip_model.cccomponents/performance_manager/performance_manager_tab_helper.cccomponents/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
- From a compromised renderer, induce
MEMORY_PRESSURE_LEVEL_CRITICALto trigger theUrgentPageDiscardingPolicy. - Ensure a
TabStripModelObservergadget is active (e.g., a DevTools window or an observer that reacts to tab replacements) which will synchronously callCloseWebContentsAtfor the replacement tab created during discard. - The
PageDiscardingHelper::DiscardMultiplePagesImplwill capture theTabHandlepointer, call the synchronous discard, and then dereference the pointer after it has been freed by the re-entrant closure. - Use heap spraying primitives in the browser process to reclaim the
TabHandlememory 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.