CVE-2026-11652
Overview
Files Changed
chrome/browser/extensions/process_manager_browsertest.cc
Patch
From 4a7596d01ebc0d7624111a21ee0b5ccd198e8d28 Mon Sep 17 00:00:00 2001
From: Andrea Orru <andreaorru@chromium.org>
Date: Fri, 22 May 2026 11:45:06 -0700
Subject: [PATCH] [Extensions] Robust lazy background page keepalive tracking
Compromised renderers could send repetitive DecrementLazyKeepaliveCount
Mojo IPCs to force the browser's keepalive counter to zero prematurely.
We now enforce per-frame deduplication in ExtensionFrameHost and strict
activity matching in ProcessManager, so unbalanced decrements are
dropped rather than allowed to underflow the counter. Keepalives are
also correctly balanced on frame deletion.
Bug: 513156160
Change-Id: I48be5134d5970aea921b9d4f495737dfc6fbdd27
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7857632
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1635109}
---
diff --git a/chrome/browser/extensions/process_manager_browsertest.cc b/chrome/browser/extensions/process_manager_browsertest.cc
index 284e2acb..a00f730 100644
--- a/chrome/browser/extensions/process_manager_browsertest.cc
+++ b/chrome/browser/extensions/process_manager_browsertest.cc
@@ -56,8 +56,10 @@
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/browsertest_util.h"
+#include "extensions/browser/extension_frame_host.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
+#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/manifest_handlers/web_accessible_resources_info.h"
#include "extensions/common/permissions/permissions_data.h"
@@ -746,6 +748,220 @@
pm->GetLazyKeepaliveActivities(extension.get()).size());
}
+// Verifies that `ProcessManager` strictly enforces exact activity matching
+// before decrementing the lazy keepalive count. Untrusted or unbalanced
+// decrements with incorrect `extra_data` strings must be rejected to prevent
+// counter underflow. Regression test for https://crbug.com/513156160.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveDecrementRequiresMatchingActivity) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ constexpr char kTestActivity[] = "test activity";
+ pm->IncrementLazyKeepaliveCount(extension, Activity::PROCESS_MANAGER,
+ kTestActivity);
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Attempting to decrement with an unmatched activity (e.g., generic IPC
+ // activity) should fail and leave the keepalive count intact.
+ EXPECT_FALSE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::LIFECYCLE_MANAGEMENT, Activity::kIPC));
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Decrementing with the exact matching activity succeeds.
+ EXPECT_TRUE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::PROCESS_MANAGER, kTestActivity));
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+}
+
+// Verifies that `ExtensionFrameHost` "absorbs" repetitive keepalive increments
+// and decrements from a renderer frame, forwarding exactly one increment and
+// one decrement to `ProcessManager` scoped specifically to the frame's ID.
+// Regression test for https://crbug.com/513156160.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveIpcActivityIsFrameScoped) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ExtensionHost* host = pm->GetBackgroundHostForExtension(extension->id());
+ ASSERT_TRUE(host);
+
+ auto* observer =
+ ExtensionWebContentsObserver::GetForWebContents(host->host_contents());
+ ASSERT_TRUE(observer);
+ ExtensionFrameHost* extension_frame_host =
+ observer->extension_frame_host_for_testing();
+ ASSERT_TRUE(extension_frame_host);
+
+ content::RenderFrameHost* background_frame =
+ host->host_contents()->GetPrimaryMainFrame();
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ background_frame);
+
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // First increment forwards to `ProcessManager`.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+ // Second increment is "absorbed" locally by `ExtensionFrameHost`.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Decrementing directly on `ProcessManager` using generic Activity::kIPC
+ // fails because `ExtensionFrameHost` registered a unique frame-scoped
+ // activity string.
+ EXPECT_FALSE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::LIFECYCLE_MANAGEMENT, Activity::kIPC));
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // First decrement is "absorbed" locally by `ExtensionFrameHost`.
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+ // Second decrement drops local count to 0 and forwards to `ProcessManager`.
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ nullptr);
+}
+
+// Verifies that if a render frame is deleted or destroyed while holding active
+// IPC keepalive counts, `ExtensionFrameHost` correctly cleans up and balances
+// the keepalive count in `ProcessManager`.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveIpcActivityReleasedOnFrameDeletion) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ExtensionHost* host = pm->GetBackgroundHostForExtension(extension->id());
+ ASSERT_TRUE(host);
+
+ auto* observer =
+ ExtensionWebContentsObserver::GetForWebContents(host->host_contents());
+ ASSERT_TRUE(observer);
+ ExtensionFrameHost* extension_frame_host =
+ observer->extension_frame_host_for_testing();
+ ASSERT_TRUE(extension_frame_host);
+ content::RenderFrameHost* background_frame =
+ host->host_contents()->GetPrimaryMainFrame();
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ background_frame);
+
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // Increment keepalive count multiple times from the frame.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Simulating unexpected frame deletion causes `ExtensionFrameHost` to
+ // release the frame's keepalive in `ProcessManager`.
+ extension_frame_host->RenderFrameDeleted(background_frame);
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ nullptr);
+}
+
+// Verifies that `ExtensionFrameHost` silently drops mismatched keepalive
+// decrements from a frame (e.g., a decrement without a prior increment, or
Regression Test / PoC
diff --git a/chrome/browser/extensions/process_manager_browsertest.cc b/chrome/browser/extensions/process_manager_browsertest.cc
index 284e2acb..a00f730 100644
--- a/chrome/browser/extensions/process_manager_browsertest.cc
+++ b/chrome/browser/extensions/process_manager_browsertest.cc
@@ -56,8 +56,10 @@
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/browsertest_util.h"
+#include "extensions/browser/extension_frame_host.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_host_test_helper.h"
+#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/manifest_handlers/web_accessible_resources_info.h"
#include "extensions/common/permissions/permissions_data.h"
@@ -746,6 +748,220 @@
pm->GetLazyKeepaliveActivities(extension.get()).size());
}
+// Verifies that `ProcessManager` strictly enforces exact activity matching
+// before decrementing the lazy keepalive count. Untrusted or unbalanced
+// decrements with incorrect `extra_data` strings must be rejected to prevent
+// counter underflow. Regression test for https://crbug.com/513156160.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveDecrementRequiresMatchingActivity) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ constexpr char kTestActivity[] = "test activity";
+ pm->IncrementLazyKeepaliveCount(extension, Activity::PROCESS_MANAGER,
+ kTestActivity);
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Attempting to decrement with an unmatched activity (e.g., generic IPC
+ // activity) should fail and leave the keepalive count intact.
+ EXPECT_FALSE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::LIFECYCLE_MANAGEMENT, Activity::kIPC));
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Decrementing with the exact matching activity succeeds.
+ EXPECT_TRUE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::PROCESS_MANAGER, kTestActivity));
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+}
+
+// Verifies that `ExtensionFrameHost` "absorbs" repetitive keepalive increments
+// and decrements from a renderer frame, forwarding exactly one increment and
+// one decrement to `ProcessManager` scoped specifically to the frame's ID.
+// Regression test for https://crbug.com/513156160.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveIpcActivityIsFrameScoped) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ExtensionHost* host = pm->GetBackgroundHostForExtension(extension->id());
+ ASSERT_TRUE(host);
+
+ auto* observer =
+ ExtensionWebContentsObserver::GetForWebContents(host->host_contents());
+ ASSERT_TRUE(observer);
+ ExtensionFrameHost* extension_frame_host =
+ observer->extension_frame_host_for_testing();
+ ASSERT_TRUE(extension_frame_host);
+
+ content::RenderFrameHost* background_frame =
+ host->host_contents()->GetPrimaryMainFrame();
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ background_frame);
+
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // First increment forwards to `ProcessManager`.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+ // Second increment is "absorbed" locally by `ExtensionFrameHost`.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Decrementing directly on `ProcessManager` using generic Activity::kIPC
+ // fails because `ExtensionFrameHost` registered a unique frame-scoped
+ // activity string.
+ EXPECT_FALSE(pm->DecrementLazyKeepaliveCount(
+ extension, Activity::LIFECYCLE_MANAGEMENT, Activity::kIPC));
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // First decrement is "absorbed" locally by `ExtensionFrameHost`.
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+ // Second decrement drops local count to 0 and forwards to `ProcessManager`.
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ nullptr);
+}
+
+// Verifies that if a render frame is deleted or destroyed while holding active
+// IPC keepalive counts, `ExtensionFrameHost` correctly cleans up and balances
+// the keepalive count in `ProcessManager`.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveIpcActivityReleasedOnFrameDeletion) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ExtensionHost* host = pm->GetBackgroundHostForExtension(extension->id());
+ ASSERT_TRUE(host);
+
+ auto* observer =
+ ExtensionWebContentsObserver::GetForWebContents(host->host_contents());
+ ASSERT_TRUE(observer);
+ ExtensionFrameHost* extension_frame_host =
+ observer->extension_frame_host_for_testing();
+ ASSERT_TRUE(extension_frame_host);
+ content::RenderFrameHost* background_frame =
+ host->host_contents()->GetPrimaryMainFrame();
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ background_frame);
+
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // Increment keepalive count multiple times from the frame.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+
+ // Simulating unexpected frame deletion causes `ExtensionFrameHost` to
+ // release the frame's keepalive in `ProcessManager`.
+ extension_frame_host->RenderFrameDeleted(background_frame);
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ nullptr);
+}
+
+// Verifies that `ExtensionFrameHost` silently drops mismatched keepalive
+// decrements from a frame (e.g., a decrement without a prior increment, or
+// more decrements than increments) without underflowing the keepalive count
+// in `ProcessManager`. Regression test for https://crbug.com/513156160.
+IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest,
+ LazyKeepaliveMismatchedDecrementsAreIgnored) {
+ TestExtensionDir dir;
+ dir.WriteManifest(R"({
+ "name": "Lazy background",
+ "version": "1",
+ "manifest_version": 2,
+ "background": {
+ "page": "bg.html",
+ "persistent": false
+ }
+ })");
+ dir.WriteFile(FILE_PATH_LITERAL("bg.html"), "");
+
+ const Extension* extension = LoadExtension(dir.UnpackedPath());
+ ASSERT_TRUE(extension);
+ ASSERT_TRUE(BackgroundInfo::HasLazyBackgroundPage(extension));
+
+ ProcessManager* pm = ProcessManager::Get(profile());
+ ExtensionHost* host = pm->GetBackgroundHostForExtension(extension->id());
+ ASSERT_TRUE(host);
+
+ auto* observer =
+ ExtensionWebContentsObserver::GetForWebContents(host->host_contents());
+ ASSERT_TRUE(observer);
+ ExtensionFrameHost* extension_frame_host =
+ observer->extension_frame_host_for_testing();
+ ASSERT_TRUE(extension_frame_host);
+ content::RenderFrameHost* background_frame =
+ host->host_contents()->GetPrimaryMainFrame();
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ background_frame);
+
+ ASSERT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // Decrement without any prior increment is silently ignored.
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ // After a balanced increment/decrement, further decrements are also ignored.
+ extension_frame_host->IncrementLazyKeepaliveCount();
+ EXPECT_EQ(1, pm->GetLazyKeepaliveCount(extension));
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+ extension_frame_host->DecrementLazyKeepaliveCount();
+ EXPECT_EQ(0, pm->GetLazyKeepaliveCount(extension));
+
+ extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
+ nullptr);
+}
+
IN_PROC_BROWSER_TEST_F(ProcessManagerBrowserTest, ExtensionProcessReuse) {
const size_t kNumExtensions = 3;
content::RenderProcessHost::SetMaxRendererProcessCount(kNumExtensions - 1);
Original Bug Report
Use-After-Free in ProcessManager::CloseLazyBackgroundPageNow via keepalive manipulation
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A compromised extension renderer can trigger a premature extension suspension by manipulating the lazy keepalive count. If the extension has multiple frames in a tab with an active JavaScript dialog, synchronous destruction during the closing sequence leads to a Use-After-Free (UAF) in the browser process.
Affected files:
extensions/browser/process_manager.ccextensions/browser/extension_frame_host.ccextensions/common/mojom/frame.mojom
Estimated timestamp from git blame: 2023-10-16
Summary
A potential Use-After-Free (UAF) vulnerability exists in the browser process within extensions::ProcessManager::CloseLazyBackgroundPageNow. The issue arises because a compromised extension renderer can force the extension’s keepalive count to zero, triggering a suspension sequence while extension frames are still active. If multiple frames from the same WebContents are present and a JavaScript dialog is active, the synchronous destruction of the WebContents causes the browser to dereference dangling content::RenderFrameHost pointers.
Root Cause Analysis
1. Keepalive Count Underflow
In extensions/browser/process_manager.cc, the DecrementLazyKeepaliveCount method (called via the mojom::LocalFrameHost Mojo IPC) decrements the lazy_keepalive_count without sufficient validation in release builds:
void ProcessManager::DecrementLazyKeepaliveCount(...) {
BackgroundPageData& data = background_page_data_[extension_id];
DCHECK(data.lazy_keepalive_count > 0 || ...);
--data.lazy_keepalive_count;
// ...
if (data.lazy_keepalive_count == 0) {
// Initiates suspension...
}
}
A compromised renderer can send this IPC multiple times to force the counter to zero, even if it was never incremented by that renderer, or if other extension frames still rely on it.
2. Vulnerable Closing Loop
When the suspension sequence completes, ProcessManager::CloseLazyBackgroundPageNow is called. It identifies all extension-related frames and stores their raw pointers in a vector for closure:
std::vector<content::RenderFrameHost*> frames_to_close;
// ... collection of frames ...
for (content::RenderFrameHost* frame : frames_to_close) {
content::WebContents::FromRenderFrameHost(frame)->ClosePage();
UnregisterRenderFrameHost(frame);
}
3. Synchronous Destruction and UAF
content::WebContents::ClosePage() typically operates asynchronously. However, if a JavaScript dialog (like alert()) is currently visible, RenderFrameHostImpl::IsPageReadyToBeClosed() returns true, causing the destruction to proceed synchronously.
If the frames_to_close vector contains multiple frames from the same WebContents (e.g., two extension iframes in one tab), the first iteration’s call to ClosePage() will synchronously destroy the WebContents and all its associated RenderFrameHost objects. The subsequent iteration then attempts to dereference a dangling RenderFrameHost* via WebContents::FromRenderFrameHost(frame), resulting in a UAF in the browser process.
Potential Attack Steps
- From a compromised extension renderer, call the
DecrementLazyKeepaliveCountIPC repeatedly to force the extension’s keepalive count to zero. - Ensure the extension has multiple active iframes within a single tab (e.g., by embedding them in a parent page).
- Trigger a JavaScript dialog (e.g.,
window.alert()) in that tab. - Wait for the extension suspension timeout to trigger
CloseLazyBackgroundPageNow. - The browser process should crash or exhibit memory corruption when attempting to close the second iframe in the loop.
Suggested Fix
- Implement robust underflow protection in
ProcessManager::DecrementLazyKeepaliveCountfor release builds (e.g., usingCHECKor saturated arithmetic). - Use
content::GlobalRenderFrameHostIdorbase::WeakPtr<content::RenderFrameHost>instead of raw pointers in theframes_to_closevector to safely track frame lifetimes across synchronous calls. - Verify that the
RenderFrameHoststill exists before dereferencing it in the closing loop.
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.