CVE-2026-85053
Overview
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::IsolationInfofrom which network keys are computed. - `net::NetworkIsolationPartition`
- an extra dimension of the
NetworkIsolationKey/NetworkAnonymizationKeythat further separates otherwise-identical network state (and code cache) into distinct partitions. - `SharedWorkerSameSiteCookies`
- the worker attribute (
kAllvskNone) recording whether the worker may use SameSite=Strict/Lax cookies;kNoneis 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.
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
- Two workers, same StorageKey, different cookie access
A site (via the Storage Access API) obtains a shared worker with
SharedWorkerSameSiteCookies::kNonewhile another shared worker of the sameStorageKeyruns withkAll. - 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. - 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. - 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
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
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/worker_host/shared_worker_host_unittest.cc |
modified | |
TESTnet/base/network_isolation_key_unittest.cc |
modified |
Files Changed
content/browser/worker_host/shared_worker_host.ccnet/base/network_isolation_partition.hnet/base/network_isolation_partition.ccnet/base/network_isolation_key_unittest.ccservices/network/public/cpp/network_isolation_partition_mojom_traits.cccontent/browser/worker_host/shared_worker_host_unittest.ccservices/network/public/mojom/network_isolation_partition.mojomcontent/browser/worker_host/shared_worker_host.h
Audit Directions
- Cache keys that omit an access dimensionAudit 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 surfacesReview 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 correctnessCheck helpers like
ComputeIsolationInfoWithPartitionthat only apply the partition when the info is non-empty; verify no path with an emptyIsolationInfostill needs partitioning to avoid a collision.
Patch
From 0b4f1821f123f397738a9ca6fd9f1666c55f9d45 Mon Sep 17 00:00:00 2001
From: Chris Fredrickson <cfredric@chromium.org>
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 <cfredric@chromium.org>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: mmenke <mmenke@chromium.org>
Reviewed-by: Asami Doi <asamidoi@chromium.org>
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);
Regression Test / PoC
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);
Original Bug Report
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
- A partitioned iframe registers its Service Worker and obtains storage access.
- The handle creates a blob SharedWorker with a first-party worker key while the worker inherits the iframe’s partitioned controller.
- Equal-time/equal-length A and B responses are installed in the two owners.
- The blob worker imports A through the partitioned controller; its first-party
CodeCacheHoststores A metadata beside B. - A fresh consumer imports B and accepts A’s wrong-source serialized graph.
- Exposed collections flush lazy
make(); V8 reparses source B through A’s SFI. - Retained child closures perform A-indexed baseline accesses against B’s short context and confuse the canonical JSArray maps.
- 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)