Overview

Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Browser
DescriptionUse after free in Browser
ComponentBrowser
Sub-componentUI>Browser
Bug ClassUAF
Tracker560238696
Fix commit35e6b50ff0e3 (chromium/src) +77/-5
CISA KEVNot listed
CreditedXinyang Ge
Disclosed2026-10-06

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 true to 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, and IsInBackForwardCache() 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::TakeNavigationRequest frees the node’s current NavigationRequest when 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.

Key insight
Eviction logic used “not active” as a proxy for “in BFCache”, so a sibling document that was merely pending deletion got force-evicted; that eviction synchronously reloaded the tab and freed the in-flight NavigationRequest whose own code was still executing dozens of frames up the stack.

Attack Path

  1. 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).
  2. 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.
  3. 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, calls SharedWorkerHost::EvictBFCachedClientsIfLastActive.
  4. Mis-eviction The buggy loop pushes the pending-deletion sibling (document 1) into bf_cached_clients and calls EvictFromBackForwardCacheWithReason on it even though it is not cached, which synchronously invokes NavigationController::Reload() → navigation N2.
  5. Free N2’s FrameTreeNode::TakeNavigationRequest frees N1 while N1’s NavigationRequest*/this is still live on the stack above.
  6. UAF write The stack unwinds into NavigationRequest::ResetStateForSiteInstanceChange, which writes through the freed NavigationRequest, corrupting a freed PartitionAlloc slot (observed crash at the 0xefefefef… freed pattern) — a controlled-timing use-after-free write in the browser process.

Impact Assessment

This is a browser-process (UI thread, 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

FunctionChangeNotes
for
content/browser/worker_host/shared_worker_host.cc
modified
if
content/browser/worker_host/shared_worker_service_impl_unittest.cc
modified
TEST_F
content/browser/worker_host/shared_worker_service_impl_unittest.cc
modified

Files Changed

  • content/browser/worker_host/shared_worker_host.cc
  • content/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 selection
    Review the GetSiteInstanceForNavigationRequest → eligibility-reporting call chain for any other hook that can start or cancel a navigation while a raw NavigationRequest*/this is held up-stack; FrameTreeNode::TakeNavigationRequest freeing the in-flight request mid-stack is the general hazard.
  • SharedWorker/ServiceWorker client enumeration
    Look 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.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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 /hang endpoint for route=http.
  • run.sh — ./run.sh mac starts 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

View on issue tracker