CVE-2026-106197
Overview
Background
- `EvictBFCachedClientsIfLastActive`
- helper run while the browser is deciding whether a navigating main frame may enter BFCache. If the frame is the worker’s last active client, it evicts the worker’s other (BFCached) clients and returns
trueto block the frame from BFCache. - `RenderFrameHostImpl` lifecycle states
- an RFH can be
Active,InBackForwardCache, or inactive-but-not-cached (kPendingDeletion,kRunningUnloadHandlers).IsActive()is false for the latter two categories, andIsInBackForwardCache()distinguishes the cached case. - `EvictFromBackForwardCacheWithReason`
- intended to be called only on an RFH that is actually in BFCache. For an RFH that is not in BFCache (pending deletion), its flattened/tree-reasons path synchronously calls
NavigationController::Reload(), starting a new navigation in that tab. - `NavigationRequest`
- the browser’s per-navigation state object.
FrameTreeNode::TakeNavigationRequestfrees the node’s currentNavigationRequestwhen a new navigation replaces it.
Root Cause Analysis
The invariant is that EvictBFCachedClientsIfLastActive must only call EvictFromBackForwardCacheWithReason on sibling clients that are genuinely in BFCache. Before the patch, the loop in shared_worker_host.cc returned false only when other_rfh->IsActive(), and otherwise unconditionally executed bf_cached_clients.push_back(other_rfh). An RFH in kPendingDeletion / kRunningUnloadHandlers is neither active nor in BFCache, yet it fell through the IsActive() guard and was pushed into bf_cached_clients. The subsequent loop then called EvictFromBackForwardCacheWithReason on that not-cached RFH. For such an RFH the eviction path runs RenderFrameHostImpl::EvictFromBackForwardCacheWithFlattenedAndTreeReasons(), which calls NavigationController::Reload() synchronously, starting navigation N2 in the same tab. N2 runs FrameTreeNode::TakeNavigationRequest, which frees the in-progress navigation N1 — the very navigation whose SiteInstance selection was still executing many frames up the stack (RenderFrameHostManager::GetSiteInstanceForNavigationRequest → … → BackForwardCacheImpl::PopulateNonStickyReasonsForDocument → SharedWorkerHost::EvictBFCachedClientsIfLastActive). When control unwinds back into N1 (NavigationRequest::ResetStateForSiteInstanceChange), it writes through the freed NavigationRequest, giving a use-after-free write of a large UI-thread object in the browser process.
The patch changes the push to be conditional: if (other_rfh->IsInBackForwardCache()) { bf_cached_clients.push_back(other_rfh); }. This explicitly separates the three lifecycle categories — active clients return false early (handled above), BFCached clients are queued for eviction, and inactive-not-cached clients (pending deletion) are now simply skipped rather than evicted. The commit message notes that the naive alternative (if (!IsInBackForwardCache()) return false;) was rejected because it would wrongly treat a pending-deletion RFH as an active client and let the navigating frame incorrectly enter BFCache; the chosen fix keeps the last-active determination correct while never touching a not-cached RFH.
NavigationRequest whose own code was still executing dozens of frames up the stack.Attack Path
- Setup
Attacker serves a page (document 1) with COOP/COEP that creates a
SharedWorker, then navigates its own tab to document 2 and busy-loops so its renderer cannot acknowledge unload (the frame stays in pending-deletion / running-unload-handlers). - Shared client Document 2 (served with HTTP 201 so its BFCache verdict is “no”, selecting the reporting branch) connects to the same SharedWorker, so document 1’s pending-deletion RFH is still registered as a sibling client of the worker.
- Trigger navigation
Document 2 navigates the tab to
about:blank(no user interaction). The browser begins choosing a SiteInstance for this navigation N1 and, deep in the eligibility-reporting path, callsSharedWorkerHost::EvictBFCachedClientsIfLastActive. - Mis-eviction
The buggy loop pushes the pending-deletion sibling (document 1) into
bf_cached_clientsand callsEvictFromBackForwardCacheWithReasonon it even though it is not cached, which synchronously invokesNavigationController::Reload()→ navigation N2. - Free
N2’s
FrameTreeNode::TakeNavigationRequestfrees N1 while N1’sNavigationRequest*/thisis still live on the stack above. - UAF write
The stack unwinds into
NavigationRequest::ResetStateForSiteInstanceChange, which writes through the freedNavigationRequest, corrupting a freed PartitionAlloc slot (observed crash at the0xefefefef…freed pattern) — a controlled-timing use-after-free write in the browser process.
Impact Assessment
CrBrowserMain) use-after-free write of a content::NavigationRequest, reachable from an ordinary web page with no user interaction and without a compromised renderer. The freed object is a large, attacker-timed allocation and the stale access is a write, which the reporter characterizes as exploitable memory corruption rather than a mere crash, and which was used as the sandbox-escape final stage of a renderer-to-browser chain. Gating: the path runs when FreezeSharedWorker is disabled (Chrome 152 compiled default) with BFCacheWithSharedWorker enabled (default), so branded Chrome 152 is affected in default configuration; 153+ flipped FreezeSharedWorker on but the root cause is unchanged and reproduces with --disable-features=FreezeSharedWorker. The route=http/route=err variants reach the same free via SelectFrameHostForOnRequestFailedInternal.Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/worker_host/shared_worker_host.cc |
modified | |
ifcontent/browser/worker_host/shared_worker_service_impl_unittest.cc |
modified | |
TEST_Fcontent/browser/worker_host/shared_worker_service_impl_unittest.cc |
modified |
Files Changed
content/browser/worker_host/shared_worker_host.cccontent/browser/worker_host/shared_worker_service_impl_unittest.cc
Audit Directions
- "not active" used as "cached"Audit every other caller of
EvictFromBackForwardCacheWithReason/ BFCache eviction helpers for the same!IsActive()-as-IsInBackForwardCache()confusion; any site that treats pending-deletion/unload RFHs as cacheable is a candidate for the same synchronous-reload reentrancy. - Synchronous navigation during SiteInstance selectionReview the
GetSiteInstanceForNavigationRequest→ eligibility-reporting call chain for any other hook that can start or cancel a navigation while a rawNavigationRequest*/thisis held up-stack;FrameTreeNode::TakeNavigationRequestfreeing the in-flight request mid-stack is the general hazard. - SharedWorker/ServiceWorker client enumerationLook at other worker-host client-iteration loops (e.g. ServiceWorker, dedicated-worker lifecycles) that act on sibling documents during navigation for the same failure to filter by precise lifecycle state.
Patch
From 35e6b50ff0e339c4b830e568a85a18061a052cb0 Mon Sep 17 00:00:00 2001 From: Rakina Zata Amni <[email protected]> Date: Mon, 14 Sep 2026 03:17:45 -0700 Subject: [PATCH] Fix RFH state checks in EvictBFCachedClientsIfLastActive SharedWorkerHost::EvictBFCachedClientsIfLastActive checks if the main frame currently navigating away (render_frame_host) is the last active client of a SharedWorker. If so, it evicts other BFCached clients of the worker and returns true so that render_frame_host is blocked from entering BFCache. Previously, EvictBFCachedClientsIfLastActive checked `if (other_rfh->IsActive()) return false;`. For inactive RFHs that are not in BFCache (such as frames in kPendingDeletion or running unload handlers), IsActive() returned false, causing them to be added to `bf_cached_clients`. This resulted in calling EvictFromBackForwardCacheWithReason on RFHs that were not in BFCache, triggering a crash. Changing the check to `if (!other_rfh->IsInBackForwardCache()) return false;` was also insufficient because it treated inactive non-BFCached RFHs (e.g., kPendingDeletion) as active clients, returning false early and incorrectly allowing render_frame_host to enter BFCache when no active clients remained. This CL explicitly handles all three RFH lifecycle categories: 1. Active (other_rfh->IsActive()): Returns false because another active client exists. 2. In BFCache (other_rfh->IsInBackForwardCache()): Pushed to bf_cached_clients to be evicted if render_frame_host is the last active client. 3. Inactive & not in BFCache (kPendingDeletion, etc.): Skipped, as it is neither active nor stored in BFCache. BUG=560238696 TAG=agy CONV=29c67910-05a4-442d-8a19-f8396f02d251 Change-Id: Ia2a9233446cfa1619a9f887d5af130663f9755db Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8404085 Reviewed-by: Anna Sato <[email protected]> Commit-Queue: Rakina Zata Amni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697153} --- diff --git a/content/browser/worker_host/shared_worker_host.cc b/content/browser/worker_host/shared_worker_host.cc index ec04cbd..ec0f01d 100644 --- a/content/browser/worker_host/shared_worker_host.cc +++ b/content/browser/worker_host/shared_worker_host.cc @@ -1150,7 +1150,9 @@ // client. return false; } - bf_cached_clients.push_back(other_rfh); + if (other_rfh->IsInBackForwardCache()) { + bf_cached_clients.push_back(other_rfh); + } } for (RenderFrameHostImpl* rfh_to_evict : bf_cached_clients) { rfh_to_evict->EvictFromBackForwardCacheWithReason( diff --git a/content/browser/worker_host/shared_worker_service_impl_unittest.cc b/content/browser/worker_host/shared_worker_service_impl_unittest.cc index ef626d2..722640ee 100644 --- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc +++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc @@ -62,7 +62,8 @@ const std::string& name, blink::mojom::SharedWorkerCreationContextType creation_context_type, MockSharedWorkerClient* client, - MessagePortChannel* local_port) { + MessagePortChannel* local_port, + bool flush_connector = true) { auto options = blink::mojom::WorkerOptions::New(); options->name = name; blink::mojom::SharedWorkerInfoPtr info(blink::mojom::SharedWorkerInfo::New( @@ -88,6 +89,9 @@ connector->Connect(std::move(info), std::move(client_proxy), creation_context_type, pipe.TakePort1(), mojo::NullRemote()); + if (flush_connector) { + connector.FlushForTesting(); + } } void ConnectToSharedWorker( @@ -95,11 +99,12 @@ const GURL& url, const std::string& name, MockSharedWorkerClient* client, - MessagePortChannel* local_port) { + MessagePortChannel* local_port, + bool flush_connector = true) { ConnectToSharedWorkerWithContextType( std::move(connector), url, name, blink::mojom::SharedWorkerCreationContextType::kSecure, client, - local_port); + local_port, flush_connector); } } // namespace @@ -380,7 +385,7 @@ const GURL kUrl("http://example.com/w.js"); ConnectToSharedWorker( MakeSharedWorkerConnector(render_frame_host->GetGlobalId()), kUrl, "name", - &client, &local_port); + &client, &local_port, /*flush_connector=*/false); // Now asynchronously destroy |web_contents| so that the startup sequence at // least reaches SharedWorkerServiceImpl::StartWorker(). @@ -2029,4 +2034,69 @@ EXPECT_EQ(initial_bad_msg_count, renderer_host->bad_msg_count()); } +TEST_F(SharedWorkerServiceImplTest, EvictBFCachedClientsIfLastActive) { + std::unique_ptr<TestWebContents> web_contents1 = + CreateWebContents(GURL("http://example.com/")); + TestRenderFrameHost* rfh1 = web_contents1->GetPrimaryMainFrame(); + MockRenderProcessHost* renderer_host1 = rfh1->GetProcess(); + const int process_id1 = renderer_host1->GetDeprecatedID(); + renderer_host1->OverrideBinderForTesting( + blink::mojom::SharedWorkerFactory::Name_, + base::BindRepeating(&SharedWorkerServiceImplTest::BindSharedWorkerFactory, + base::Unretained(this), process_id1)); + + std::unique_ptr<TestWebContents> web_contents2 = + CreateWebContents(GURL("http://example.com/")); + TestRenderFrameHost* rfh2 = web_contents2->GetPrimaryMainFrame(); + + MockSharedWorkerClient client1; + MessagePortChannel local_port1; + const GURL kUrl("http://example.com/w.js"); + ConnectToSharedWorker(MakeSharedWorkerConnector(rfh1->GetGlobalId()), kUrl, + "name", &client1, &local_port1); + + MockSharedWorkerClient client2; + MessagePortChannel local_port2; + ConnectToSharedWorker(MakeSharedWorkerConnector(rfh2->GetGlobalId()), kUrl, + "name", &client2, &local_port2); + + RenderFrameHostImpl* rfh_impl1 = static_cast<RenderFrameHostImpl*>(rfh1); + RenderFrameHostImpl* rfh_impl2 = static_cast<RenderFrameHostImpl*>(rfh2); + + SharedWorkerServiceImpl* service = static_cast<SharedWorkerServiceImpl*>( + browser_context_->GetDefaultStoragePartition()->GetSharedWorkerService()); + + // 1. When rfh2 is Active: + // EvictBFCachedClientsIfLastActive returns false because rfh2 is active. + EXPECT_FALSE(service->EvictBFCachedClientsIfLastActive(rfh_impl1)); + EXPECT_FALSE(rfh_impl2->is_evicted_from_back_forward_cache()); + + // 2. When rfh2 is in BackForwardCache: + rfh_impl2->SetLifecycleState( + RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache); + // EvictBFCachedClientsIfLastActive returns true (rfh1 is last active client) + // and evicts rfh2 from BFCache. + EXPECT_TRUE(service->EvictBFCachedClientsIfLastActive(rfh_impl1)); + EXPECT_TRUE(rfh_impl2->is_evicted_from_back_forward_cache()); + + // 3. When a client (rfh3) is pending deletion (neither active nor in + // BFCache): + std::unique_ptr<TestWebContents> web_contents3 = + CreateWebContents(GURL("http://example.com/")); + TestRenderFrameHost* rfh3 = web_contents3->GetPrimaryMainFrame(); + MockSharedWorkerClient client3; + MessagePortChannel local_port3; + ConnectToSharedWorker(MakeSharedWorkerConnector(rfh3->GetGlobalId()), kUrl, + "name", &client3, &local_port3); + RenderFrameHostImpl* rfh_impl3 = static_cast<RenderFrameHostImpl*>(rfh3); + + rfh_impl3->SetLifecycleState( + RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers); + // EvictBFCachedClientsIfLastActive returns true because rfh1 is the last + // active client (rfh3 is pending deletion, so not active), but rfh3 is not + // evicted from BFCache because it is not in BFCache. + EXPECT_TRUE(service->EvictBFCachedClientsIfLastActive(rfh_impl1)); + EXPECT_FALSE(rfh_impl3->is_evicted_from_back_forward_cache()); +} + } // namespace content
Regression Test / PoC
diff --git a/content/browser/worker_host/shared_worker_service_impl_unittest.cc b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
index ef626d2..722640ee 100644
--- a/content/browser/worker_host/shared_worker_service_impl_unittest.cc
+++ b/content/browser/worker_host/shared_worker_service_impl_unittest.cc
@@ -62,7 +62,8 @@
const std::string& name,
blink::mojom::SharedWorkerCreationContextType creation_context_type,
MockSharedWorkerClient* client,
- MessagePortChannel* local_port) {
+ MessagePortChannel* local_port,
+ bool flush_connector = true) {
auto options = blink::mojom::WorkerOptions::New();
options->name = name;
blink::mojom::SharedWorkerInfoPtr info(blink::mojom::SharedWorkerInfo::New(
@@ -88,6 +89,9 @@
connector->Connect(std::move(info), std::move(client_proxy),
creation_context_type, pipe.TakePort1(),
mojo::NullRemote());
+ if (flush_connector) {
+ connector.FlushForTesting();
+ }
}
void ConnectToSharedWorker(
@@ -95,11 +99,12 @@
const GURL& url,
const std::string& name,
MockSharedWorkerClient* client,
- MessagePortChannel* local_port) {
+ MessagePortChannel* local_port,
+ bool flush_connector = true) {
ConnectToSharedWorkerWithContextType(
std::move(connector), url, name,
blink::mojom::SharedWorkerCreationContextType::kSecure, client,
- local_port);
+ local_port, flush_connector);
}
} // namespace
@@ -380,7 +385,7 @@
const GURL kUrl("http://example.com/w.js");
ConnectToSharedWorker(
MakeSharedWorkerConnector(render_frame_host->GetGlobalId()), kUrl, "name",
- &client, &local_port);
+ &client, &local_port, /*flush_connector=*/false);
// Now asynchronously destroy |web_contents| so that the startup sequence at
// least reaches SharedWorkerServiceImpl::StartWorker().
@@ -2029,4 +2034,69 @@
EXPECT_EQ(initial_bad_msg_count, renderer_host->bad_msg_count());
}
+TEST_F(SharedWorkerServiceImplTest, EvictBFCachedClientsIfLastActive) {
+ std::unique_ptr<TestWebContents> web_contents1 =
+ CreateWebContents(GURL("http://example.com/"));
+ TestRenderFrameHost* rfh1 = web_contents1->GetPrimaryMainFrame();
+ MockRenderProcessHost* renderer_host1 = rfh1->GetProcess();
+ const int process_id1 = renderer_host1->GetDeprecatedID();
+ renderer_host1->OverrideBinderForTesting(
+ blink::mojom::SharedWorkerFactory::Name_,
+ base::BindRepeating(&SharedWorkerServiceImplTest::BindSharedWorkerFactory,
+ base::Unretained(this), process_id1));
+
+ std::unique_ptr<TestWebContents> web_contents2 =
+ CreateWebContents(GURL("http://example.com/"));
+ TestRenderFrameHost* rfh2 = web_contents2->GetPrimaryMainFrame();
+
+ MockSharedWorkerClient client1;
+ MessagePortChannel local_port1;
+ const GURL kUrl("http://example.com/w.js");
+ ConnectToSharedWorker(MakeSharedWorkerConnector(rfh1->GetGlobalId()), kUrl,
+ "name", &client1, &local_port1);
+
+ MockSharedWorkerClient client2;
+ MessagePortChannel local_port2;
+ ConnectToSharedWorker(MakeSharedWorkerConnector(rfh2->GetGlobalId()), kUrl,
+ "name", &client2, &local_port2);
+
+ RenderFrameHostImpl* rfh_impl1 = static_cast<RenderFrameHostImpl*>(rfh1);
+ RenderFrameHostImpl* rfh_impl2 = static_cast<RenderFrameHostImpl*>(rfh2);
+
+ SharedWorkerServiceImpl* service = static_cast<SharedWorkerServiceImpl*>(
+ browser_context_->GetDefaultStoragePartition()->GetSharedWorkerService());
+
+ // 1. When rfh2 is Active:
+ // EvictBFCachedClientsIfLastActive returns false because rfh2 is active.
+ EXPECT_FALSE(service->EvictBFCachedClientsIfLastActive(rfh_impl1));
+ EXPECT_FALSE(rfh_impl2->is_evicted_from_back_forward_cache());
+
+ // 2. When rfh2 is in BackForwardCache:
+ rfh_impl2->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ // EvictBFCachedClientsIfLastActive returns true (rfh1 is last active client)
+ // and evicts rfh2 from BFCache.
+ EXPECT_TRUE(service->EvictBFCachedClientsIfLastActive(rfh_impl1));
+ EXPECT_TRUE(rfh_impl2->is_evicted_from_back_forward_cache());
+
+ // 3. When a client (rfh3) is pending deletion (neither active nor in
+ // BFCache):
+ std::unique_ptr<TestWebContents> web_contents3 =
+ CreateWebContents(GURL("http://example.com/"));
+ TestRenderFrameHost* rfh3 = web_contents3->GetPrimaryMainFrame();
+ MockSharedWorkerClient client3;
+ MessagePortChannel local_port3;
+ ConnectToSharedWorker(MakeSharedWorkerConnector(rfh3->GetGlobalId()), kUrl,
+ "name", &client3, &local_port3);
+ RenderFrameHostImpl* rfh_impl3 = static_cast<RenderFrameHostImpl*>(rfh3);
+
+ rfh_impl3->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers);
+ // EvictBFCachedClientsIfLastActive returns true because rfh1 is the last
+ // active client (rfh3 is pending deletion, so not active), but rfh3 is not
+ // evicted from BFCache because it is not in BFCache.
+ EXPECT_TRUE(service->EvictBFCachedClientsIfLastActive(rfh_impl1));
+ EXPECT_FALSE(rfh_impl3->is_evicted_from_back_forward_cache());
+}
+
} // namespace content
Original Bug Report
UAF of NavigationRequest via synchronous SharedWorker BFCache eviction during site-instance selection (browser process)
Security Bug
VULNERABILITY DETAILS
Browser-process (UI thread) use-after-free of content::NavigationRequest, reachable from an ordinary web page with no user interaction and no compromised renderer.
While the browser is choosing a SiteInstance for a navigation N1 (RenderFrameHostManager::GetFrameHostForNavigation → GetSiteInstanceForNavigationRequest → … → ShouldProactivelySwapBrowsingInstance → GetCompleteBackForwardCacheEligibilityForReporting → BackForwardCacheImpl::…PopulateNonStickyReasonsForDocument), SharedWorkerHost::EvictBFCachedClientsIfLastActive() “evicts” every non-active sibling client of a SharedWorker. One of those clients is the previous document of the same tab, still pending deletion (its renderer is busy and has not acknowledged unload). For a document that is not actually in the cache, RenderFrameHostImpl::EvictFromBackForwardCacheWithFlattenedAndTreeReasons() calls NavigationController::Reload() synchronously, which starts a new navigation N2 in that tab; FrameTreeNode::TakeNavigationRequest deletes N1 while N1’s own frames are 25–30 levels up the stack holding raw NavigationRequest* / this. Control returns into N1’s code (NavigationRequest::ResetStateForSiteInstanceChange in the default route) and writes through the freed object.
Page recipe (plain JS, one file, two loads): document 1 (served with COOP/COEP) creates a SharedWorker, navigates the tab to document 2 and busy-loops so it cannot ack unload; document 2 (served with HTTP 201 so its BFCache verdict is “no”, which selects the reporting branch above) connects to the same worker and navigates to about:blank. Variants route=http / route=err reach the same free from SelectFrameHostForOnRequestFailedInternal.
Gate: the path runs when FreezeSharedWorker is disabled (Chrome 152 compiled default; 153+ flipped it on) and BFCacheWithSharedWorker is enabled (default). So branded Chrome 152 is affected in default configuration; on 153+ the root cause is unchanged and reproduces with --disable-features=FreezeSharedWorker (used below as a labelled stand-in for the 152 default). The freed NavigationRequest is a large, attacker-timed UI-thread allocation and the stale access is a write, so this is exploitable memory corruption in the browser process, not just a crash.
VERSION
Chrome Version: 152.0.7977.x stable (default config, by source and by branded-defaults test); 153.0.8010.37 (Official Build) stable arm64 with --disable-features=FreezeSharedWorker (crash below); Chromium 153/154/155 ASan (Linux) same flag.
Operating System: macOS 26.6.2 (Apple M4 Max); Linux x86-64.
REPRODUCTION CASE
Attached (individual files):
poc.html— the PoC (both steps;?route=blank|http|err). No clicks;window.__next()always returns null.server.py— 127.0.0.1 server: serves step 1 with COOP+COEP, step 2 with status 201, the worker script, and the/hangendpoint forroute=http.run.sh—./run.sh macstarts the server and a separate Chrome instance with a throw-away profile and--disable-features=FreezeSharedWorker, waits for the crash, prints a RESULT line. Linux/ASan and CfT-152 modes documented inside (incl. why stock Chrome-for-Testing 152 needs the flag: CfT applies the field-trial testing config, which enables FreezeSharedWorker; branded 152 does not).autoclick.py— stdlib DevTools client used only to detect the browser going away (no clicks are sent for this PoC).macos-crash-153.0.8010.37.ips— crash report from the run below.
Steps (macOS, Chrome 153.0.8010.37):
cd poc && ./run.sh mac
Observed: browser process crashes ~1 s after the page loads: RESULT: BROWSER CRASHED (expected).
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: browser process (CrBrowserMain).
Crash State (Google Chrome 153.0.8010.37 arm64, attached .ips):
EXC_BAD_ACCESS / SIGSEGV, KERN_INVALID_ADDRESS at 0xefefefefefeff247 (= 0xefefefefefefefef + 0x258; PartitionAlloc freed-slot pattern)
pc Google Chrome Framework + 0x6381048 (NavigationRequest::ResetStateForSiteInstanceChange)
lr Google Chrome Framework + 0x212f31c (RenderFrameHostManager::GetSiteInstanceForNavigationRequest)
x8 = 0xefefefefefefefef x9 = 0xefefefefefeff237 x0 = x19 = 0x1340922b400 (stale NavigationRequest*)
#0 +0x6381048 #1 +0x212f31c #2 +0x212c460 #3 +0x19c2df8 #4 +0x19c153c #5 +0x19c0a90 #6 +0x1692e60 #7 +0x1affc5c
ASan (Chromium 155, Linux, same page): heap-use-after-free WRITE in content::NavigationRequest::ResetStateForSiteInstanceChange, freed by FrameTreeNode::TakeNavigationRequest ← Navigator::Navigate ← NavigationControllerImpl::Reload ← RenderFrameHostImpl::EvictFromBackForwardCacheWithFlattenedAndTreeReasons ← SharedWorkerHost::EvictBFCachedClientsIfLastActive on the same stack. [full ASan report to be attached as EVIDENCE.txt]
Client ID: n/a (local throw-away profile).
FULL EXPLOIT CHAIN
We also used this bug as the final, sandbox-escape stage of a full renderer-to-browser chain (renderer stages b/559726956 → b/559726782, the same front as the chain in b/559793874), ending in code execution in the browser process on Chrome 152-default configuration. This issue is filed on its own; the attached PoC needs no other bug and no user interaction.
CREDIT INFORMATION
Reporter credit: Xinyang Ge
Assisted-by: Claude