High watch CVSS 8.8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper resource exposure in CacheStorage
DescriptionImproper resource exposure in CacheStorage
ComponentCacheStorage
Sub-componentBlink>Storage>CacheStorage
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker552689418
Fix commit0b4f1821f123 (chromium/src) +108/-6
CWECWE-668 (Exposure to wrong sphere)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSalvatore Gulizia (Serotav)
Reported2026-08-26 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-03
Patch gap3 days — fix public in source before the advisory

Background

`SharedWorkerHost`
the browser-process object representing a shared worker; it derives the network keys (and, transitively, the V8 code cache key) the worker’s resource loads use.
`blink::StorageKey` / `ToPartialNetIsolationInfo()`
the storage partitioning identity of the worker and the helper that turns it into a net::IsolationInfo from which network keys are computed.
`net::NetworkIsolationPartition`
an extra dimension of the NetworkIsolationKey/NetworkAnonymizationKey that further separates otherwise-identical network state (and code cache) into distinct partitions.
`SharedWorkerSameSiteCookies`
the worker attribute (kAll vs kNone) recording whether the worker may use SameSite=Strict/Lax cookies; kNone is used for workers created via the Storage Access API that hold a first-party key but must not access those cookies.

Root Cause Analysis

SharedWorkerHost::GetNetworkIsolationKey() and GetNetworkAnonymizationKey() computed their keys purely from GetWorkerStorageKey().ToPartialNetIsolationInfo(), so two shared workers with the same StorageKey produced identical network keys — and therefore shared the same partitioned V8 code cache — regardless of their same_site_cookies value. The violated invariant is that a worker restricted to SameSite=None cookie access (e.g. a StorageAccessHandle-created worker) must not share cached state with a worker that has full SameSite cookie access, because that state can leak across the trust boundary the cookie restriction is meant to enforce.

The fix introduces a new NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone and a helper ComputeIsolationInfoWithPartition() that, when the partition is non-kGeneral and the IsolationInfo is non-empty, rebuilds the IsolationInfo carrying that partition; GetNetworkIsolationPartition() returns the new partition exactly when instance_.same_site_cookies() == kNone. Both key getters now route through ComputeIsolationInfoWithPartition(GetWorkerStorageKey(), GetNetworkIsolationPartition()), so a kNone worker yields a distinct key/cache-key string (the regression test asserts EXPECT_NE on the keys and their ToCacheKeyString()) and no longer shares the code cache with a kAll worker of the same StorageKey.

Key insight
The core mistake was that the shared worker’s network/code-cache key ignored its SameSite=None cookie restriction, letting a restricted and an unrestricted worker with the same StorageKey share cached state; the fix adds a dedicated kSharedWorkerSameSiteCookiesNone isolation partition so the two get distinct keys.

Attack Path

  1. Two workers, same StorageKey, different cookie access A site (via the Storage Access API) obtains a shared worker with SharedWorkerSameSiteCookies::kNone while another shared worker of the same StorageKey runs with kAll.
  2. Keys collide Because both keys derived only from the StorageKey, GetNetworkIsolationKey() returned identical values, so both workers map to the same partitioned code cache / network state.
  3. Shared cached state across the boundary The two workers share the V8 code cache (and network cache) even though one is not supposed to have the same cookie-scoped access as the other, exposing cached resource/code state across that boundary — the advisory classifies this as improper resource exposure in CacheStorage.
  4. Distinguishing content Any observable difference in cached content between the privileged and restricted contexts becomes cross-readable through the shared cache (inferred); the diff does not demonstrate a concrete read primitive beyond the shared keying.

Impact Assessment

An attacker gains the ability to share (and thereby observe) partitioned network/V8-code-cache state between a SameSite=None-restricted shared worker and a fully-privileged shared worker of the same StorageKey, in the browser-process-computed keying that governs those caches; the precondition is being able to instantiate both worker variants for one StorageKey (e.g. via the Storage Access API). The advisory describes this as improper resource exposure and does not report exploitation in the wild; any escalation to reading specific cross-context secrets is (inferred).

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/worker_host/shared_worker_host_unittest.cc
modified
TEST
net/base/network_isolation_key_unittest.cc
modified

Files Changed

  • content/browser/worker_host/shared_worker_host.cc
  • net/base/network_isolation_partition.h
  • net/base/network_isolation_partition.cc
  • net/base/network_isolation_key_unittest.cc
  • services/network/public/cpp/network_isolation_partition_mojom_traits.cc
  • content/browser/worker_host/shared_worker_host_unittest.cc
  • services/network/public/mojom/network_isolation_partition.mojom
  • content/browser/worker_host/shared_worker_host.h

Audit Directions

  • Cache keys that omit an access dimension
    Audit every network/code/HTTP cache key derivation for contexts that share a StorageKey (or origin) but differ in an access right (cookie scope, Storage Access, credentials mode); the right must be part of the key.
  • Storage Access API surfaces
    Review other consumers that treat a Storage-Access-created context as first-party for keying purposes but restrict its cookie access, ensuring the restriction is reflected in isolation state.
  • Empty/`kGeneral` guard correctness
    Check helpers like ComputeIsolationInfoWithPartition that only apply the partition when the info is non-empty; verify no path with an empty IsolationInfo still needs partitioning to avoid a collision.
From 0b4f1821f123f397738a9ca6fd9f1666c55f9d45 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <[email protected]>
Date: Mon, 31 Aug 2026 14:44:07 -0700
Subject: [PATCH] Ensure SharedWorkers use distinct code caches based on SiteForCookies

Without partitioning the code cache based on this property of the
SharedWorker, two shared workers with the same StorageKey would share
the code cache, even if one is not supposed to have access to 3P cookies
and the other is.

Fixed: 552689418
Change-Id: Ice36567846bfab93b6242cfc526b0191f992a7ad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8308172
Commit-Queue: Chris Fredrickson <[email protected]>
Reviewed-by: Joe Mason <[email protected]>
Reviewed-by: mmenke <[email protected]>
Reviewed-by: Asami Doi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1689294}
---
diff --git a/content/browser/worker_host/shared_worker_host.cc b/content/browser/worker_host/shared_worker_host.cc
index 851958db..8917db9 100644
--- a/content/browser/worker_host/shared_worker_host.cc
+++ b/content/browser/worker_host/shared_worker_host.cc
@@ -94,6 +94,20 @@
                                 source);
 }
 
+net::IsolationInfo ComputeIsolationInfoWithPartition(
+    const blink::StorageKey& storage_key,
+    net::NetworkIsolationPartition partition) {
+  net::IsolationInfo isolation_info = storage_key.ToPartialNetIsolationInfo();
+  if (partition != net::NetworkIsolationPartition::kGeneral &&
+      !isolation_info.IsEmpty()) {
+    return net::IsolationInfo::Create(
+        isolation_info.request_type(), *isolation_info.top_frame_origin(),
+        *isolation_info.frame_origin(), isolation_info.site_for_cookies(),
+        isolation_info.nonce(), partition);
+  }
+  return isolation_info;
+}
+
 }  // namespace
 
 namespace content {
@@ -971,6 +985,14 @@
   return weak_factory_.GetWeakPtr();
 }
 
+net::NetworkIsolationPartition SharedWorkerHost::GetNetworkIsolationPartition()
+    const {
+  return instance_.same_site_cookies() ==
+                 blink::mojom::SharedWorkerSameSiteCookies::kNone
+             ? net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone
+             : net::NetworkIsolationPartition::kGeneral;
+}
+
 net::NetworkIsolationKey SharedWorkerHost::GetNetworkIsolationKey() const {
   // Note: Since shared workers are partitioned by the storage key, we'll use
   // the storage key to create a NIK that matches the current partitioning
@@ -978,15 +1000,15 @@
   // different top-level sites will be able to share the same shared worker, so
   // it doesn't make sense to incorporate the top-level site into the NIK in
   // that case either.
-  return GetWorkerStorageKey()
-      .ToPartialNetIsolationInfo()
+  return ComputeIsolationInfoWithPartition(GetWorkerStorageKey(),
+                                           GetNetworkIsolationPartition())
       .network_isolation_key();
 }
 
 net::NetworkAnonymizationKey SharedWorkerHost::GetNetworkAnonymizationKey()
     const {
-  return GetWorkerStorageKey()
-      .ToPartialNetIsolationInfo()
+  return ComputeIsolationInfoWithPartition(GetWorkerStorageKey(),
+                                           GetNetworkIsolationPartition())
       .network_anonymization_key();
 }
 
diff --git a/content/browser/worker_host/shared_worker_host.h b/content/browser/worker_host/shared_worker_host.h
index 18d345a..3e8ef4a 100644
--- a/content/browser/worker_host/shared_worker_host.h
+++ b/content/browser/worker_host/shared_worker_host.h
@@ -34,6 +34,7 @@
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/unique_receiver_set.h"
 #include "net/base/network_isolation_key.h"
+#include "net/base/network_isolation_partition.h"
 #include "services/device/public/cpp/compute_pressure/buildflags.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "services/network/public/cpp/cross_origin_embedder_policy.h"
@@ -240,6 +241,8 @@
 
   base::WeakPtr<SharedWorkerHost> AsWeakPtr();
 
+  net::NetworkIsolationPartition GetNetworkIsolationPartition() const;
+
   net::NetworkIsolationKey GetNetworkIsolationKey() const;
 
   net::NetworkAnonymizationKey GetNetworkAnonymizationKey() const;
diff --git a/net/base/network_isolation_partition.cc b/net/base/network_isolation_partition.cc
index 062fb49d2..3cb58aa 100644
--- a/net/base/network_isolation_partition.cc
+++ b/net/base/network_isolation_partition.cc
@@ -19,6 +19,8 @@
       return "fedcm uncredentialed requests";
     case NetworkIsolationPartition::kDnsOverHttps:
       return "dns over https";
+    case NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone:
+      return "shared worker same site cookies none";
   }
 }
 
diff --git a/net/base/network_isolation_partition.h b/net/base/network_isolation_partition.h
index 599ce382..3b52dad 100644
--- a/net/base/network_isolation_partition.h
+++ b/net/base/network_isolation_partition.h
@@ -32,8 +32,15 @@
   kFedCmUncredentialedRequests = 2,
   // This use case isolates network state for DNS over HTTPS requests.
   kDnsOverHttps = 3,
+  // This use case isolates network state and V8 code cache for SharedWorkers
+  // with SameSite=None cookie access restrictions (e.g. StorageAccessHandle).
+  //
+  // SharedWorkers that were created via the Storage Access API use a
+  // first-party NetworkIsolationKey, but do not have access to
+  // SameSite=Strict/Lax cookies.
+  kSharedWorkerSameSiteCookiesNone = 4,
 
-  kMaxValue = kDnsOverHttps,
+  kMaxValue = kSharedWorkerSameSiteCookiesNone,
 };
 
 std::string NetworkIsolationPartitionToDebugString(
diff --git a/services/network/public/cpp/network_isolation_partition_mojom_traits.cc b/services/network/public/cpp/network_isolation_partition_mojom_traits.cc
index 0e275d333..d89bd4e 100644
--- a/services/network/public/cpp/network_isolation_partition_mojom_traits.cc
+++ b/services/network/public/cpp/network_isolation_partition_mojom_traits.cc
@@ -26,6 +26,8 @@
       return net::NetworkIsolationPartition::kFedCmUncredentialedRequests;
     case NetworkIsolationPartition::kDnsOverHttps:
       return net::NetworkIsolationPartition::kDnsOverHttps;
+    case NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone:
+      return net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone;
   }
   NOTREACHED();
 }
@@ -45,6 +47,8 @@
       return NetworkIsolationPartition::kFedCmUncredentialedRequests;
     case net::NetworkIsolationPartition::kDnsOverHttps:
       return NetworkIsolationPartition::kDnsOverHttps;
+    case net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone:
+      return NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone;
   }
   NOTREACHED();
 }
diff --git a/services/network/public/mojom/network_isolation_partition.mojom b/services/network/public/mojom/network_isolation_partition.mojom
index 815fd33f..baf42c42 100644
--- a/services/network/public/mojom/network_isolation_partition.mojom
+++ b/services/network/public/mojom/network_isolation_partition.mojom
@@ -11,4 +11,5 @@
   kProtectedAudienceSellerWorklet,
   kFedCmUncredentialedRequests,
   kDnsOverHttps,
+  kSharedWorkerSameSiteCookiesNone,
 };
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 925de9e0..68eb091d 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -86,6 +86,18 @@
 
   base::WeakPtr<SharedWorkerHost> CreateHostWithExtendedLifetime(
       bool extended_lifetime) {
+    return CreateHostWithParams(blink::mojom::SharedWorkerSameSiteCookies::kAll,
+                                extended_lifetime);
+  }
+
+  base::WeakPtr<SharedWorkerHost> CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies same_site_cookies) {
+    return CreateHostWithParams(same_site_cookies, /*extended_lifetime=*/false);
+  }
+
+  base::WeakPtr<SharedWorkerHost> CreateHostWithParams(
+      blink::mojom::SharedWorkerSameSiteCookies same_site_cookies,
+      bool extended_lifetime) {
     blink::StorageKey creator_storage_key =
         blink::StorageKey::CreateFirstParty(url::Origin::Create(kWorkerUrl));
     bool is_opaque_origin_enabled = base::FeatureList::IsEnabled(
@@ -99,7 +111,7 @@
         network::mojom::CredentialsMode::kSameOrigin, "name",
         creator_storage_key, worker_storage_key, renderer_origin,
         blink::mojom::SharedWorkerCreationContextType::kSecure,
-        blink::mojom::SharedWorkerSameSiteCookies::kAll, extended_lifetime);
+        same_site_cookies, extended_lifetime);
     auto host = std::make_unique<SharedWorkerHost>(
         &service_, instance, site_instance_,
         std::vector<network::mojom::ContentSecurityPolicyPtr>(),
@@ -667,4 +679,37 @@
   }
 }
 
+TEST_F(SharedWorkerHostTest, NetworkIsolationPartitionForSameSiteCookies) {
+  base::WeakPtr<SharedWorkerHost> host_all = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kAll);
+  base::WeakPtr<SharedWorkerHost> host_none = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kNone);
+
+  ASSERT_TRUE(host_all);
+  ASSERT_TRUE(host_none);
+
+  EXPECT_EQ(host_all->GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(host_none->GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+
+  EXPECT_EQ(host_all->GetNetworkIsolationKey().GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(host_none->GetNetworkIsolationKey().GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+  EXPECT_NE(host_all->GetNetworkIsolationKey(),
+            host_none->GetNetworkIsolationKey());
+  EXPECT_NE(host_all->GetNetworkIsolationKey().ToCacheKeyString(),
+            host_none->GetNetworkIsolationKey().ToCacheKeyString());
+
+  EXPECT_EQ(
+      host_all->GetNetworkAnonymizationKey().network_isolation_partition(),
+      net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(
+      host_none->GetNetworkAnonymizationKey().network_isolation_partition(),
+      net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+  EXPECT_NE(host_all->GetNetworkAnonymizationKey(),
+            host_none->GetNetworkAnonymizationKey());
+}
+
 }  // namespace content
diff --git a/net/base/network_isolation_key_unittest.cc b/net/base/network_isolation_key_unittest.cc
index 89131568..d75938a 100644
--- a/net/base/network_isolation_key_unittest.cc
+++ b/net/base/network_isolation_key_unittest.cc
@@ -133,6 +133,24 @@
   EXPECT_TRUE(key4.IsTransient());
 }
 
+TEST(NetworkIsolationKeyTest, SharedWorkerSameSiteCookiesNonePartition) {
+  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
+  SchemefulSite site2 = SchemefulSite(GURL("http://b.test/"));
+
+  NetworkIsolationKey general_key(site1, site2, /*nonce=*/std::nullopt,
+                                  NetworkIsolationPartition::kGeneral);
+
+  NetworkIsolationKey shared_worker_none_key(
+      site1, site2, /*nonce=*/std::nullopt,
+      NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+
+  EXPECT_EQ(NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone,
+            shared_worker_none_key.GetNetworkIsolationPartition());
+  EXPECT_EQ(site1.Serialize() + " " + site2.Serialize() + " 4",
+            shared_worker_none_key.ToCacheKeyString());
+  EXPECT_NE(general_key, shared_worker_none_key);
+}
+
 TEST(NetworkIsolationKeyTest, CreateEmptyWithPartition) {
   NetworkIsolationKey key = NetworkIsolationKey::CreateEmptyWithPartition(
       NetworkIsolationPartition::kDnsOverHttps);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/worker_host/shared_worker_host_unittest.cc b/content/browser/worker_host/shared_worker_host_unittest.cc
index 925de9e0..68eb091d 100644
--- a/content/browser/worker_host/shared_worker_host_unittest.cc
+++ b/content/browser/worker_host/shared_worker_host_unittest.cc
@@ -86,6 +86,18 @@
 
   base::WeakPtr<SharedWorkerHost> CreateHostWithExtendedLifetime(
       bool extended_lifetime) {
+    return CreateHostWithParams(blink::mojom::SharedWorkerSameSiteCookies::kAll,
+                                extended_lifetime);
+  }
+
+  base::WeakPtr<SharedWorkerHost> CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies same_site_cookies) {
+    return CreateHostWithParams(same_site_cookies, /*extended_lifetime=*/false);
+  }
+
+  base::WeakPtr<SharedWorkerHost> CreateHostWithParams(
+      blink::mojom::SharedWorkerSameSiteCookies same_site_cookies,
+      bool extended_lifetime) {
     blink::StorageKey creator_storage_key =
         blink::StorageKey::CreateFirstParty(url::Origin::Create(kWorkerUrl));
     bool is_opaque_origin_enabled = base::FeatureList::IsEnabled(
@@ -99,7 +111,7 @@
         network::mojom::CredentialsMode::kSameOrigin, "name",
         creator_storage_key, worker_storage_key, renderer_origin,
         blink::mojom::SharedWorkerCreationContextType::kSecure,
-        blink::mojom::SharedWorkerSameSiteCookies::kAll, extended_lifetime);
+        same_site_cookies, extended_lifetime);
     auto host = std::make_unique<SharedWorkerHost>(
         &service_, instance, site_instance_,
         std::vector<network::mojom::ContentSecurityPolicyPtr>(),
@@ -667,4 +679,37 @@
   }
 }
 
+TEST_F(SharedWorkerHostTest, NetworkIsolationPartitionForSameSiteCookies) {
+  base::WeakPtr<SharedWorkerHost> host_all = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kAll);
+  base::WeakPtr<SharedWorkerHost> host_none = CreateHostWithSameSiteCookies(
+      blink::mojom::SharedWorkerSameSiteCookies::kNone);
+
+  ASSERT_TRUE(host_all);
+  ASSERT_TRUE(host_none);
+
+  EXPECT_EQ(host_all->GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(host_none->GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+
+  EXPECT_EQ(host_all->GetNetworkIsolationKey().GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(host_none->GetNetworkIsolationKey().GetNetworkIsolationPartition(),
+            net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+  EXPECT_NE(host_all->GetNetworkIsolationKey(),
+            host_none->GetNetworkIsolationKey());
+  EXPECT_NE(host_all->GetNetworkIsolationKey().ToCacheKeyString(),
+            host_none->GetNetworkIsolationKey().ToCacheKeyString());
+
+  EXPECT_EQ(
+      host_all->GetNetworkAnonymizationKey().network_isolation_partition(),
+      net::NetworkIsolationPartition::kGeneral);
+  EXPECT_EQ(
+      host_none->GetNetworkAnonymizationKey().network_isolation_partition(),
+      net::NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+  EXPECT_NE(host_all->GetNetworkAnonymizationKey(),
+            host_none->GetNetworkAnonymizationKey());
+}
+
 }  // namespace content
diff --git a/net/base/network_isolation_key_unittest.cc b/net/base/network_isolation_key_unittest.cc
index 89131568..d75938a 100644
--- a/net/base/network_isolation_key_unittest.cc
+++ b/net/base/network_isolation_key_unittest.cc
@@ -133,6 +133,24 @@
   EXPECT_TRUE(key4.IsTransient());
 }
 
+TEST(NetworkIsolationKeyTest, SharedWorkerSameSiteCookiesNonePartition) {
+  SchemefulSite site1 = SchemefulSite(GURL("http://a.test/"));
+  SchemefulSite site2 = SchemefulSite(GURL("http://b.test/"));
+
+  NetworkIsolationKey general_key(site1, site2, /*nonce=*/std::nullopt,
+                                  NetworkIsolationPartition::kGeneral);
+
+  NetworkIsolationKey shared_worker_none_key(
+      site1, site2, /*nonce=*/std::nullopt,
+      NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone);
+
+  EXPECT_EQ(NetworkIsolationPartition::kSharedWorkerSameSiteCookiesNone,
+            shared_worker_none_key.GetNetworkIsolationPartition());
+  EXPECT_EQ(site1.Serialize() + " " + site2.Serialize() + " 4",
+            shared_worker_none_key.ToCacheKeyString());
+  EXPECT_NE(general_key, shared_worker_none_key);
+}
+
 TEST(NetworkIsolationKeyTest, CreateEmptyWithPartition) {
   NetworkIsolationKey key = NetworkIsolationKey::CreateEmptyWithPartition(
       NetworkIsolationPartition::kDnsOverHttps);
Loading diff…

Original Bug Report

reported by [email protected]

StorageAccessHandle SharedWorker owner split causes V8 code-cache context confusion

TL;DR

A blob SharedWorker created through a cross-site iframe’s StorageAccessHandle has two inconsistent storage identities. Chromium creates the blob URL, worker and CodeCacheHost with a first-party StorageKey, but separately inherits the creator iframe’s partitioned Service Worker controller and key. A dynamic module body served by that controller can therefore generate V8 code cache that Chromium writes beside a distinct first-party CacheStorage response.

With equal-length, equal-timestamp module sources A and B, a fresh worker directly deserializes A’s graph and installs B as its live source. After A’s lazy make() bytecode is flushed, V8 reparses B while child closures retain A’s captured-block scope interpretation. Sparkplug performs the resulting context loads and stores without bounds checks, reaching adjacent canonical JSArray maps.

The corruption was developed into arbitrary read/write within the V8 heap.

REPRODUCTION CASE

Tested with Linux x64 ASAN Chromium 154.0.8012.0.

Serve the attachment directory with an ordinary static HTTP server:

python3 -m http.server 8000
ASAN_SYMBOLIZER_PATH="$PWD/llvm-symbolizer" \
ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:symbolize=1:fast_unwind_on_fatal=0:external_symbolizer_path=$PWD/llvm-symbolizer" \
./chrome --headless=new \
  --no-sandbox --disable-gpu \
  --user-data-dir=/tmp/fresh-profile \
  --no-first-run --disable-breakpad \
  --disable-in-process-stack-traces \
  --js-flags="--bytecode-old-time=0 --expose-gc" \
  http://127.0.0.1:8000/entrypoint.html

For ClusterFuzz’s allow at least 120 seconds.

VULNERABILITY DETAILS

Producer: StorageAccessHandle SharedWorker owner split

StorageAccessHandle binds its blob URL store and SharedWorker connector to a first-party key. The worker’s CacheStorage and CodeCacheHost use that worker key. Blob-worker Service Worker setup follows another path: InheritControllerFrom() copies the creator iframe’s partitioned key and controller. The worker therefore serves module A through a partitioned controller but reports generated metadata through a first-party CodeCacheHost beside B.

The PoC creates A and B at the same wall-clock tick in synchronized workers. Each response is fetched through its matching Service Worker before final cache.put, giving it the natural matching response URL while preserving body and microsecond response timestamp. The handle-created blob worker imports A through the inherited controller; delayed feedback writes A’s metadata into B’s first-party owner.

This is a worker-creation owner split, not a cache replacement, entry-key, cache-name, bucket or document-commit alias.

Sink: direct deserialization and lazy-reparse context confusion

V8’s serialized-code source identity contains source length and compilation mode, not a digest of source bytes. Equal-length A and B therefore pass. This is a fresh direct-deserialization path: V8 accepts A’s object graph and then replaces the deserialized script source with B.

A’s lazy make() has an extra block containing 150 captured locals. B removes that block while preserving function positions and literal IDs. After ordinary bytecode aging flushes make(), Compiler::Compile() reparses live source B through the retained A SFI. The new B function context is short, while child closures retain A’s captured-block scope interpretation.

Ignition checks these context accesses. Sparkplug’s baseline helpers directly load and store Context::OffsetOfElementAt(index) without checking the context length. The selected words are adjacent packed-object and packed-double JSArray maps. Reversible map exchange gives overlapping object/double views and was separately developed into arbitrary 64-bit reads and writes within the V8 cage.

The attachment derives one fake object and performs an ordinary property store; the forged map access faults in Builtins_StoreIC. There is no deliberate invalid pointer or null dereference.

The producer should keep the worker’s controller, CacheStorage and CodeCacheHost on one owner and bind feedback to a browser-issued response identity. V8 should bind serialized code to exact source bytes and reject lazy scope metadata incompatible with the reparsed scope tree.

Causal timeline

  1. A partitioned iframe registers its Service Worker and obtains storage access.
  2. The handle creates a blob SharedWorker with a first-party worker key while the worker inherits the iframe’s partitioned controller.
  3. Equal-time/equal-length A and B responses are installed in the two owners.
  4. The blob worker imports A through the partitioned controller; its first-party CodeCacheHost stores A metadata beside B.
  5. A fresh consumer imports B and accepts A’s wrong-source serialized graph.
  6. Exposed collections flush lazy make(); V8 reparses source B through A’s SFI.
  7. Retained child closures perform A-indexed baseline accesses against B’s short context and confuse the canonical JSArray maps.
  8. An ordinary property store through the malformed object faults in StoreIC.

ATTACHMENTS

  • entrypoint.html: Service Worker bootstrap and redirect.
  • index.html: first-party parent page.
  • child.html: storage-access, response synchronization and worker schedule.
  • sw.js: partitioned response routing.
  • seed-worker.js: synchronized CacheStorage response construction.
  • launder.js: blob worker import that generates metadata through the split owner.
  • consumer.js: direct deserialization, lazy aging and minimal causal crash.
  • asan.txt: one complete symbolized Chrome ASAN renderer crash.

CREDIT INFORMATION

Salvatore Gulizia (Serotav)

View on issue tracker