CVE-2026-10001
Overview
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
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.