CVE-2026-7944
Overview
Background
- Generated code cache (V8 code cache)
- a persistent cache mapping resource URLs (keyed by an origin lock / secondary key) to compiled JavaScript/WASM bytecode, served to renderers via
CodeCacheHostImpl. - `GetOriginLock` / origin lock
- derives the per-process origin key used as the code cache’s secondary key, from the renderer’s
ProcessLock, to enforce site isolation in the cache. - `ProcessLock::is_pdf()` / `is_sandboxed()`
- predicates identifying a PDF renderer process and an origin-restricted (opaque-origin) sandboxed-iframe process, respectively.
- PDF / sandboxed-iframe processes
- less-trusted renderers that may share a hosting site’s URL but must be treated as distinct (sandboxed iframes have opaque origins; PDF renderers are lower trust).
Root Cause Analysis
GetOriginLock (in code_cache_host_impl.cc) built the code-cache secondary key purely from the process’s process_lock_url scheme (Case 4/5), without distinguishing PDF processes or origin-restricted sandboxed iframes from ordinary same-site renderers. Because a PDF viewer or sandboxed iframe can run with a process lock whose URL matches its hosting origin, it received the same code-cache origin key as the hosting site, letting it read — and, via DidGenerateCacheableMetadata, write/tamper with — compiled code cached for that origin.
The patch inserts a new case before the scheme check: if (process_lock.is_pdf() || process_lock.is_sandboxed()) return std::nullopt;, so these less-trusted processes get no origin lock and therefore no shared code-cache key, treating sandboxed iframes as the opaque origins they logically are and isolating PDF renderers. The companion comment edits in GetSecondaryKeyForCodeCache renumber the documented cases but show no logic change there. The added PdfObliviousToOpenWeb / SandboxedIframeObliviousToOpenWeb tests confirm the invariant: a PDF/sandboxed process must not fetch a resource the open-web origin cached.
std::nullopt for is_pdf()/is_sandboxed() process locks, denying them a shared key.Attack Path
- Obtain a less-trusted renderer
An attacker runs code in (or compromises) a PDF process or an origin-restricted sandboxed iframe whose
ProcessLockURL corresponds to a hosting origin (e.g.victim.example.com). - Shared cache key
Pre-patch
GetOriginLockreturned the hosting origin’s lock for that process, so itsCodeCacheHostImplused the same secondary key as the open-web site. - Read hosting origin's cached code
The process calls
FetchCachedCodefor a resource (e.g.script.js) and receives the compiled bytecode the open-web origin had cached, as the regression test shows should no longer happen. - Tamper with cached code (inferred)
Via
DidGenerateCacheableMetadata, the less-trusted process could also write entries under the hosting origin’s key, poisoning compiled code served to that origin; the per-commit message states this is what the fix prevents, and any concrete exploitation beyond cache read/tamper is (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcontent/browser/renderer_host/code_cache_host_impl_unittest.cc |
modified | |
BindLambdaForTestingcontent/browser/renderer_host/code_cache_host_impl_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/code_cache_host_impl.cccontent/browser/renderer_host/code_cache_host_impl_unittest.cc
Audit Directions
- Process-trust in cache keysAudit any persistent/shared cache keyed by origin or
process_lock_urlto confirm it also accounts foris_pdf(),is_sandboxed(), guest, and fenced states rather than URL/scheme alone. - Opaque-origin treatmentLook for places that derive a stable key from a sandboxed iframe’s hosting URL where an opaque origin should instead force isolation (
std::nullopt/unique key). - Write paths mirror read pathsWhere a fix blocks reads for less-trusted processes, verify the corresponding write/metadata-generation path (
DidGenerateCacheableMetadata) is isolated too, so tampering cannot occur under the shared key.
Patch
From 6fa33375364335468c0954cb3301e25f892f32cb Mon Sep 17 00:00:00 2001
From: Greg Thompson <grt@chromium.org>
Date: Fri, 27 Mar 2026 06:30:24 -0700
Subject: [PATCH] [CodeCache] Disable the code cache for PDF and sandboxed iframes
This prevents compromised PDF renderers or those for origin-restricted
sandboxed iframes from tampering with compiled code belonging to their
hosting origin.
Fixed: 495783187
Change-Id: I0015e8f27f7fb6f6315bf8345b1bbb5a358ed527
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7702794
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Greg Thompson <grt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1606170}
---
diff --git a/content/browser/renderer_host/code_cache_host_impl.cc b/content/browser/renderer_host/code_cache_host_impl.cc
index f8116be..2b75860 100644
--- a/content/browser/renderer_host/code_cache_host_impl.cc
+++ b/content/browser/renderer_host/code_cache_host_impl.cc
@@ -129,9 +129,11 @@
// initiated navigation to a data: URL). In these cases, the code should not be
// cached since the serialized value of opaque origins should not be used as a
// key.
-// Case 4. origin_lock if the scheme of origin_lock is
+// Case 4. a std::nullopt for PDF processes and origin-restricted sandboxed
+// iframes, to prevent them from accessing the cache of their hosting origins.
+// Case 5. origin_lock if the scheme of origin_lock is
// Http/Https/chrome/chrome-untrusted.
-// Case 5. std::nullopt otherwise.
+// Case 6. std::nullopt otherwise.
std::optional<GURL> GetOriginLock(ChildProcessId render_process_id) {
ProcessLock process_lock =
ChildProcessSecurityPolicyImpl::GetInstance()->GetProcessLock(
@@ -167,7 +169,15 @@
return std::nullopt;
}
- // Case 4: process_lock_url is used to enforce site-isolation in code caches.
+ // Case 4: PDF processes and origin-restricted sandboxed iframes should not
+ // have access to the code cache of their hosting origins. PDF processes are
+ // less trusted, and sandboxed iframes should be treated as having opaque
+ // origins.
+ if (process_lock.is_pdf() || process_lock.is_sandboxed()) {
+ return std::nullopt;
+ }
+
+ // Case 5: process_lock_url is used to enforce site-isolation in code caches.
// Http/https/chrome schemes are safe to be used as a secondary key. Other
// schemes could be enabled if they are known to be safe and if it is
// required to cache code from those origins.
@@ -428,9 +438,11 @@
// initiated navigation to a data: URL). In these cases, the code should not
// be cached since the serialized value of opaque origins should not be used
// as a key.
- // Case 3: origin_lock if the scheme of origin_lock is
+ // Case 3. a std::nullopt for PDF processes and origin-restricted sandboxed
+ // iframes, to prevent them from accessing the cache of their hosting origins.
+ // Case 4: origin_lock if the scheme of origin_lock is
// Http/Https/chrome/chrome-untrusted.
- // Case 4. std::nullopt otherwise.
+ // Case 5. std::nullopt otherwise.
static std::optional<GURL> GetSecondaryKeyForCodeCache(
const GURL& resource_url,
ChildProcessId render_process_id,
diff --git a/content/browser/renderer_host/code_cache_host_impl_unittest.cc b/content/browser/renderer_host/code_cache_host_impl_unittest.cc
index 857cb82..8761e95 100644
--- a/content/browser/renderer_host/code_cache_host_impl_unittest.cc
+++ b/content/browser/renderer_host/code_cache_host_impl_unittest.cc
@@ -21,6 +21,7 @@
#include "content/browser/code_cache/generated_code_cache_context.h"
#include "content/browser/process_lock.h"
#include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
@@ -77,13 +78,18 @@
bool IsSitePerProcessOrStricter() { return GetParam(); }
- void SetupRendererWithLock(ChildProcessId process_id, const GURL& url) {
+ void SetupRendererWithLock(ChildProcessId process_id,
+ const UrlInfo& url_info) {
ChildProcessSecurityPolicyImpl* p =
ChildProcessSecurityPolicyImpl::GetInstance();
p->AddForTesting(process_id, &browser_context_);
scoped_refptr<SiteInstanceImpl> site_instance =
- SiteInstanceImpl::CreateForTesting(&browser_context_, url);
+ SiteInstanceImpl::CreateForUrlInfo(
+ &browser_context_, url_info,
+ /*is_guest=*/false,
+ /*is_fenced=*/false,
+ /*is_fixed_storage_partition=*/false);
ChildProcessSecurityPolicyImpl::GetInstance()->LockProcess(
site_instance->GetIsolationContext(), process_id, false,
ProcessLock::FromSiteInfo(site_instance->GetSiteInfo()));
@@ -91,6 +97,10 @@
added_renderers_.push_back(process_id);
}
+ void SetupRendererWithLock(ChildProcessId process_id, const GURL& url) {
+ SetupRendererWithLock(process_id, UrlInfo::CreateForTesting(url));
+ }
+
protected:
BrowserTaskEnvironment task_environment_;
base::HistogramTester histogram_tester;
@@ -505,6 +515,120 @@
run_loop.Run();
}
+// Tests that a PDF page does not see a resource cached by an open web site
+// on the same origin. Validates that process separation is properly maintained
+// in the V8 cache.
+TEST_P(CodeCacheHostImplTest, PdfObliviousToOpenWeb) {
+ base::RunLoop run_loop;
+
+ // The URL of a resource loaded by both a site on the open web and a PDF
+ // page.
+ const GURL resource_url("https://victim.example.com/script.js");
+
+ // State for a site on the open web that loads the above resource.
+ const ChildProcessId kOpenWebProcessId(12);
+ const GURL open_web_site("https://victim.example.com/");
+ SetupRendererWithLock(kOpenWebProcessId, open_web_site);
+
+ // State for a PDF page that also loads the above resource on the same site.
+ // Note that this requires setting is_pdf to true in the starting UrlInfo.
+ const ChildProcessId kPdfProcessId(13);
+ UrlInfo url_info(UrlInfoInit(open_web_site).WithIsPdf(true));
+ SetupRendererWithLock(kPdfProcessId, url_info);
+
+ GeneratedCodeCacheContext::RunOrPostTask(
+ generated_code_cache_context_.get(), FROM_HERE,
+ base::BindLambdaForTesting([&]() {
+ // Create the open web's cache and put the resource into it.
+ auto open_web_host = CodeCacheHostImpl::Create(
+ kOpenWebProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ open_web_host->DidGenerateCacheableMetadata(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::Time::Now(),
+ mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
+
+ // Create a PDF page's cache and make sure the resource is absent.
+ // It should NOT receive the contents of the HTML's V8 cache!
+ auto pdf_host = CodeCacheHostImpl::Create(
+ kPdfProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ pdf_host->FetchCachedCode(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::BindLambdaForTesting([&](base::Time found_response_time,
+ mojo_base::BigBuffer found_data) {
+ EXPECT_EQ(found_response_time, base::Time());
+ EXPECT_EQ(found_data.size(), 0U);
+ run_loop.Quit();
+ }));
+ }));
+
+ run_loop.Run();
+}
+
+// Tests that an origin-restricted sandboxed iframe does not see a resource
+// cached by an open web site on the same origin.
+TEST_P(CodeCacheHostImplTest, SandboxedIframeObliviousToOpenWeb) {
+ base::RunLoop run_loop;
+
+ // The URL of a resource loaded by both a site on the open web and a
+ // sandboxed iframe.
+ const GURL resource_url("https://victim.example.com/script.js");
+
+ // State for a site on the open web that loads the above resource.
+ const ChildProcessId kOpenWebProcessId(12);
+ const GURL open_web_site("https://victim.example.com/");
+ SetupRendererWithLock(kOpenWebProcessId, open_web_site);
+
+ // State for a sandboxed iframe that also loads the above resource on the
+ // same site. Note that this requires setting is_sandboxed to true in the
+ // starting UrlInfo.
+ const ChildProcessId kSandboxedProcessId(13);
+ UrlInfo url_info(UrlInfoInit(open_web_site).WithSandbox(true));
+ SetupRendererWithLock(kSandboxedProcessId, url_info);
+
+ GeneratedCodeCacheContext::RunOrPostTask(
+ generated_code_cache_context_.get(), FROM_HERE,
+ base::BindLambdaForTesting([&]() {
+ // Create the open web's cache and put the resource into it.
+ auto open_web_host = CodeCacheHostImpl::Create(
+ kOpenWebProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
Regression Test / PoC
diff --git a/content/browser/renderer_host/code_cache_host_impl_unittest.cc b/content/browser/renderer_host/code_cache_host_impl_unittest.cc
index 857cb82..8761e95 100644
--- a/content/browser/renderer_host/code_cache_host_impl_unittest.cc
+++ b/content/browser/renderer_host/code_cache_host_impl_unittest.cc
@@ -21,6 +21,7 @@
#include "content/browser/code_cache/generated_code_cache_context.h"
#include "content/browser/process_lock.h"
#include "content/browser/site_instance_impl.h"
+#include "content/browser/url_info.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
@@ -77,13 +78,18 @@
bool IsSitePerProcessOrStricter() { return GetParam(); }
- void SetupRendererWithLock(ChildProcessId process_id, const GURL& url) {
+ void SetupRendererWithLock(ChildProcessId process_id,
+ const UrlInfo& url_info) {
ChildProcessSecurityPolicyImpl* p =
ChildProcessSecurityPolicyImpl::GetInstance();
p->AddForTesting(process_id, &browser_context_);
scoped_refptr<SiteInstanceImpl> site_instance =
- SiteInstanceImpl::CreateForTesting(&browser_context_, url);
+ SiteInstanceImpl::CreateForUrlInfo(
+ &browser_context_, url_info,
+ /*is_guest=*/false,
+ /*is_fenced=*/false,
+ /*is_fixed_storage_partition=*/false);
ChildProcessSecurityPolicyImpl::GetInstance()->LockProcess(
site_instance->GetIsolationContext(), process_id, false,
ProcessLock::FromSiteInfo(site_instance->GetSiteInfo()));
@@ -91,6 +97,10 @@
added_renderers_.push_back(process_id);
}
+ void SetupRendererWithLock(ChildProcessId process_id, const GURL& url) {
+ SetupRendererWithLock(process_id, UrlInfo::CreateForTesting(url));
+ }
+
protected:
BrowserTaskEnvironment task_environment_;
base::HistogramTester histogram_tester;
@@ -505,6 +515,120 @@
run_loop.Run();
}
+// Tests that a PDF page does not see a resource cached by an open web site
+// on the same origin. Validates that process separation is properly maintained
+// in the V8 cache.
+TEST_P(CodeCacheHostImplTest, PdfObliviousToOpenWeb) {
+ base::RunLoop run_loop;
+
+ // The URL of a resource loaded by both a site on the open web and a PDF
+ // page.
+ const GURL resource_url("https://victim.example.com/script.js");
+
+ // State for a site on the open web that loads the above resource.
+ const ChildProcessId kOpenWebProcessId(12);
+ const GURL open_web_site("https://victim.example.com/");
+ SetupRendererWithLock(kOpenWebProcessId, open_web_site);
+
+ // State for a PDF page that also loads the above resource on the same site.
+ // Note that this requires setting is_pdf to true in the starting UrlInfo.
+ const ChildProcessId kPdfProcessId(13);
+ UrlInfo url_info(UrlInfoInit(open_web_site).WithIsPdf(true));
+ SetupRendererWithLock(kPdfProcessId, url_info);
+
+ GeneratedCodeCacheContext::RunOrPostTask(
+ generated_code_cache_context_.get(), FROM_HERE,
+ base::BindLambdaForTesting([&]() {
+ // Create the open web's cache and put the resource into it.
+ auto open_web_host = CodeCacheHostImpl::Create(
+ kOpenWebProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ open_web_host->DidGenerateCacheableMetadata(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::Time::Now(),
+ mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
+
+ // Create a PDF page's cache and make sure the resource is absent.
+ // It should NOT receive the contents of the HTML's V8 cache!
+ auto pdf_host = CodeCacheHostImpl::Create(
+ kPdfProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ pdf_host->FetchCachedCode(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::BindLambdaForTesting([&](base::Time found_response_time,
+ mojo_base::BigBuffer found_data) {
+ EXPECT_EQ(found_response_time, base::Time());
+ EXPECT_EQ(found_data.size(), 0U);
+ run_loop.Quit();
+ }));
+ }));
+
+ run_loop.Run();
+}
+
+// Tests that an origin-restricted sandboxed iframe does not see a resource
+// cached by an open web site on the same origin.
+TEST_P(CodeCacheHostImplTest, SandboxedIframeObliviousToOpenWeb) {
+ base::RunLoop run_loop;
+
+ // The URL of a resource loaded by both a site on the open web and a
+ // sandboxed iframe.
+ const GURL resource_url("https://victim.example.com/script.js");
+
+ // State for a site on the open web that loads the above resource.
+ const ChildProcessId kOpenWebProcessId(12);
+ const GURL open_web_site("https://victim.example.com/");
+ SetupRendererWithLock(kOpenWebProcessId, open_web_site);
+
+ // State for a sandboxed iframe that also loads the above resource on the
+ // same site. Note that this requires setting is_sandboxed to true in the
+ // starting UrlInfo.
+ const ChildProcessId kSandboxedProcessId(13);
+ UrlInfo url_info(UrlInfoInit(open_web_site).WithSandbox(true));
+ SetupRendererWithLock(kSandboxedProcessId, url_info);
+
+ GeneratedCodeCacheContext::RunOrPostTask(
+ generated_code_cache_context_.get(), FROM_HERE,
+ base::BindLambdaForTesting([&]() {
+ // Create the open web's cache and put the resource into it.
+ auto open_web_host = CodeCacheHostImpl::Create(
+ kOpenWebProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ open_web_host->DidGenerateCacheableMetadata(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::Time::Now(),
+ mojo_base::BigBuffer(base::byte_span_from_cstring("hi")));
+
+ // Create a sandboxed iframe's cache and make sure the resource is
+ // absent. It should NOT receive the contents of the HTML's V8 cache!
+ auto sandboxed_host = CodeCacheHostImpl::Create(
+ kSandboxedProcessId, generated_code_cache_context_,
+ net::NetworkIsolationKey(net::SchemefulSite{open_web_site},
+ net::SchemefulSite{open_web_site}),
+ blink::StorageKey::CreateFirstParty(
+ url::Origin::Create(open_web_site)));
+ sandboxed_host->FetchCachedCode(
+ blink::mojom::CodeCacheType::kJavascript, resource_url,
+ base::BindLambdaForTesting([&](base::Time found_response_time,
+ mojo_base::BigBuffer found_data) {
+ EXPECT_EQ(found_response_time, base::Time());
+ EXPECT_EQ(found_data.size(), 0U);
+ run_loop.Quit();
+ }));
+ }));
+
+ run_loop.Run();
+}
+
INSTANTIATE_TEST_SUITE_P(All,
CodeCacheHostImplTest,
testing::Values(true, false));