CVE-2026-113207
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/chrome_content_browser_client.cc |
modified |
Files Changed
chrome/browser/policy/test/jit_policy_browsertest.cccomponents/site_isolation/features.hcomponents/site_isolation/features.cccomponents/site_isolation/site_isolation_policy_unittest.cccomponents/site_isolation/site_isolation_policy.hcontent/browser/site_info.ccchrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cccontent/browser/renderer_host/navigation_request.cccontent/browser/site_instance_impl_unittest.cccomponents/site_isolation/site_isolation_policy.ccchrome/browser/chrome_content_browser_client.ccchrome/browser/chrome_content_browser_client_unittest.cc
Patch
From 766951e328bc23e9bfdd0900852dd7a1405d811e Mon Sep 17 00:00:00 2001
From: Javier Castro <jacastro@chromium.org>
Date: Mon, 28 Sep 2026 04:01:49 -0700
Subject: [PATCH] [SiteIsolation] Fix JITLESS policy bypass via Origin-Agent-Cluster
Before this change, Origin-Agent-Cluster headers caused sites to bypass
the per-site JIT blocklist on partial site-isolation environments (e.g.,
Android). SiteInfo::Create() decides whether to consult the embedder's
per-site JIT policy or its default policy based on whether the site gets
a dedicated process, but it was being passed an inverted parameter
(!url_info.oac_header_request.has_value()), leading to the bypass.
Now, we replace the inverted condition with
agent_cluster_key.IsOriginKeyed() to accurately reflect when an
origin-keyed process is needed.
To support this:
* SiteInfo evaluates the embedder's JIT policy against the process lock
URL, falling back to the BrowsingInstance's default policy for
documents that share the default SiteInstanceGroup.
* ChromeContentBrowserClient proactively isolates origins that carry a
JITLESS exception policy, so those policies take effect regardless of
OAC headers or default SiteInstanceGroup configuration.
* SiteIsolationPolicy gates that isolation behind
kOriginIsolationForJitlessExceptions, enabled by default, mirroring
kOriginIsolationForJsOptExceptions.
Coauthored using Gemini.
Test: out/Default/content_unittests --gtest_filter='SiteInstanceTest.*:JitPolicyScopeTest.*'
Test: out/Default/components_unittests --gtest_filter='OriginIsolationForJitlessExceptions*'
Test: out/Default/unit_tests --gtest_filter='WillComputeSiteForNavigationTest.*'
Test: out/Default/browser_tests --gtest_filter='JavascriptOptimizerBrowserTest.ChangeJavascriptOptimizerStatePriorToSameOriginWindowOpen:JavascriptOptimizerBrowserTest.SubSiteRuleInheritanceBehavior'
Bug: 511806651
TAG=agy
CONV=d5f8a237-80e8-46b1-a851-f143107d6f85
Change-Id: I4455fd38e9a5f85bb3da02f568dcc3009969e26b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8358438
Commit-Queue: Artem Sumaneev <asumaneev@google.com>
Auto-Submit: Javier Castro <jacastro@chromium.org>
Reviewed-by: Artem Sumaneev <asumaneev@google.com>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1705975}
---
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index ca53122..c8eee68 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -2649,8 +2649,12 @@
void ChromeContentBrowserClient::WillComputeSiteForNavigation(
content::BrowserContext* browser_context,
const GURL& url) {
- if (!site_isolation::SiteIsolationPolicy::
- IsOriginIsolationForJsOptExceptionsEnabled(browser_context)) {
+ const bool isolate_js_opt = site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJsOptExceptionsEnabled(browser_context);
+ const bool isolate_jitless = site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJitlessExceptionsEnabled(browser_context);
+
+ if (!isolate_js_opt && !isolate_jitless) {
return;
}
@@ -2660,28 +2664,37 @@
return;
}
- // If the JS optimizer policy for this `url`'s origin differs from the default
- // JS optimizer policy, then the url needs to be put into its own process
- // (otherwise it will have the default JS setting applied). This lets JS
- // optimizer policy rules be applied to URLs on clients that have partial site
- // isolation (like Android). This also improves JS optimizer rules handling on
- // clients where subdomains of a site are not isolated. For example, if a.com
- // has site isolation, but sub.a.com needs a different rule (More information
- // at: crbug.com/377733397). Note that this will cause explicit opt-outs using
- // the Origin-Agent-Cluster header to be ignored. Note that it is safe to do
- // this multiple times for the same origin because AddFutureIsolatedOrigins
- // should drop requests to isolate an origin that is already isolated.
Profile* profile = Profile::FromBrowserContext(browser_context);
auto* map = HostContentSettingsMapFactory::GetForProfile(profile);
if (!map) {
return;
}
- if (map->GetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_OPTIMIZER,
- nullptr) !=
- map->GetContentSetting(url, url,
- ContentSettingsType::JAVASCRIPT_OPTIMIZER)) {
- url::Origin origin(url::Origin::Create(url));
+ auto has_setting_exception = [map, &url](ContentSettingsType type) {
+ return map->GetDefaultContentSetting(type, nullptr) !=
+ map->GetContentSetting(url, url, type);
+ };
+
+ // If the JS optimizer or JIT policy for this `url`'s origin differs from the
+ // default policy, then the url needs to be put into its own process
+ // (otherwise it will have the default setting applied). This lets policy
+ // rules be applied to URLs on clients that have partial site isolation (like
+ // Android). This also improves policy rules handling on clients where
+ // subdomains of a site are not isolated. For example, if a.com has site
+ // isolation, but sub.a.com needs a different rule (More information at:
+ // crbug.com/377733397). Note that this will cause explicit opt-outs using the
+ // Origin-Agent-Cluster header to be ignored. Note that it is safe to do this
+ // multiple times for the same origin because AddFutureIsolatedOrigins should
+ // drop requests to isolate an origin that is already isolated. Also note
+ // that the isolation only takes effect in BrowsingInstances created after
+ // this call, so it does not apply to the navigation that triggers it, nor
+ // to any other existing BrowsingInstances; those keep sharing a process with
+ // the origin's site until a new BrowsingInstance is created.
+ if ((isolate_js_opt &&
+ has_setting_exception(ContentSettingsType::JAVASCRIPT_OPTIMIZER)) ||
+ (isolate_jitless &&
+ has_setting_exception(ContentSettingsType::JAVASCRIPT_JIT))) {
+ url::Origin origin = url::Origin::Create(url);
content::ChildProcessSecurityPolicy* policy =
content::ChildProcessSecurityPolicy::GetInstance();
// The user added a content setting rule and then navigated, so specify the
diff --git a/components/site_isolation/features.cc b/components/site_isolation/features.cc
index 99e2775..e89b5c6 100644
--- a/components/site_isolation/features.cc
+++ b/components/site_isolation/features.cc
@@ -88,6 +88,16 @@
"IsolateOriginsShortlistPolicyUpdate",
base::FEATURE_DISABLED_BY_DEFAULT);
+// In order to have broader support for JITless policy exceptions, we'll apply
+// origin isolation on navigation for URLs that match rules in the
+// JAVASCRIPT_JIT content setting that don't match the default setting. This
+// mirrors kOriginIsolationForJsOptExceptions below, and ensures per-origin
+// JITless policies take effect even on platforms with partial site isolation.
+// Disabling this restores the previous behavior, where such origins could join
+// the default SiteInstanceGroup and fall back to the default JIT policy.
+BASE_FEATURE(kOriginIsolationForJitlessExceptions,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
// In order to have broader support for JavaScript optimizer exceptions, we'll
// apply origin isolation on navigation for URLs that match rules in the
// JAVASCRIPT_OPTIMIZER content setting that don't match the default setting.
diff --git a/components/site_isolation/features.h b/components/site_isolation/features.h
index 55aebca..8326394 100644
--- a/components/site_isolation/features.h
+++ b/components/site_isolation/features.h
@@ -22,6 +22,7 @@
#endif // BUIDLFLAG(IS_ANDROID)
BASE_DECLARE_FEATURE(kIsolateOriginsShortlist);
+BASE_DECLARE_FEATURE(kOriginIsolationForJitlessExceptions);
BASE_DECLARE_FEATURE(kOriginIsolationForJsOptExceptions);
BASE_DECLARE_FEATURE(kOriginIsolationMemoryThreshold);
extern const char kOriginIsolationMemoryThresholdParamName[];
diff --git a/components/site_isolation/site_isolation_policy.cc b/components/site_isolation/site_isolation_policy.cc
index 66048fd..5447686 100644
--- a/components/site_isolation/site_isolation_policy.cc
+++ b/components/site_isolation/site_isolation_policy.cc
@@ -193,6 +193,23 @@
}
// static
+bool SiteIsolationPolicy::IsOriginIsolationForJitlessExceptionsEnabled(
+ content::BrowserContext* browser_context) {
+ if (content::SiteIsolationPolicy::IsStrictOriginIsolationEnabled() ||
+ content::SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(
+ browser_context)) {
+ // Origin isolation for Jitless exceptions isn't needed if
+ // origin isolation is enabled for everything, much like JS optimizer
+ // exceptions.
+ return false;
+ }
+ // Dynamic origin isolation is required for this to work.
+ return content::SiteIsolationPolicy::AreDynamicIsolatedOriginsEnabled() &&
+ base::FeatureList::IsEnabled(
+ site_isolation::features::kOriginIsolationForJitlessExceptions);
+}
+
+// static
bool SiteIsolationPolicy::IsOriginIsolationForJsOptExceptionsEnabled(
content::BrowserContext* browser_context) {
if (content::SiteIsolationPolicy::IsStrictOriginIsolationEnabled() ||
diff --git a/components/site_isolation/site_isolation_policy.h b/components/site_isolation/site_isolation_policy.h
index f04baed..1db7f6c 100644
--- a/components/site_isolation/site_isolation_policy.h
+++ b/components/site_isolation/site_isolation_policy.h
@@ -42,6 +42,11 @@
static bool IsIsolationForOAuthSitesEnabled();
// Returns true if the isolation mode for isolating origins that match
+ // Jitless policy exceptions is enabled.
+ static bool IsOriginIsolationForJitlessExceptionsEnabled(
+ content::BrowserContext* browser_context);
+
+ // Returns true if the isolation mode for isolating origins that match
// JavaScript optimizer exceptions is enabled. This is different than
// IsOriginIsolationForJsOptExceptionsSupported() because
// IsOriginIsolationForJsOptExceptionsEnabled() returns false if the
diff --git a/content/browser/renderer_host/navigation_request.cc b/content/browser/renderer_host/navigation_request.cc
index ead75f2..67443de 100644
--- a/content/browser/renderer_host/navigation_request.cc
+++ b/content/browser/renderer_host/navigation_request.cc
@@ -5491,10 +5491,17 @@
policy->RecordDefaultOriginAgentClusterOriginIfNew(
isolation_context, origin, false /* is_global_walk_or_frame_removal */);
- url::Origin process_lock_origin =
- url::Origin::Create(instance->GetSiteInfo().GetProcessLockURL());
- // Cache the computed v8 optimization state so that all instances of an
- // origin in a BrowsingInstance are assigned to the same process.
+ // Cache whether V8 optimizations are disabled under the same origin that
+ // CheckShouldDisableV8Optimization() uses when it reads this cache (see
+ // site_info.cc); if the two disagree, the lookup can never hit. Documents
+ // that can share the default SiteInstanceGroup carry the
+ // BrowsingInstance's default policy rather than a site-specific one, so
+ // they are keyed by the default site URL. Everything else is keyed by its
+ // own process lock URL.
+ const url::Origin process_lock_origin =
+ url::Origin::Create(instance->CanPutSiteInstanceInDefaultGroup()
+ ? SiteInstanceImpl::GetDefaultSiteURL()
+ : instance->GetSiteInfo().GetProcessLockURL());
policy->AddV8OptimizationDisabledStateForOriginIfNotCached(
isolation_context.browsing_instance_id(), process_lock_origin,
instance->GetProcess()->AreV8OptimizationsDisabled());
diff --git a/content/browser/site_info.cc b/content/browser/site_info.cc
index 1fb65dff..fde1c9c 100644
--- a/content/browser/site_info.cc
+++ b/content/browser/site_info.cc
@@ -143,17 +143,31 @@
}
// Computes whether to disable v8-optimization for the
-// (browsing_instance_id, process_lock_origin) pair.
+// (browsing_instance_id, process_lock_origin) pair, where process_lock_origin
+// is the default site URL's origin for documents that can share the default
+// SiteInstanceGroup, and `process_lock_url`'s origin otherwise.
bool CheckShouldDisableV8Optimization(
BrowserContext* browser_context,
const BrowsingInstanceId& browsing_instance_id,
const std::optional<base::SafeRef<ProcessSelectionUserData>>&
process_selection_user_data,
const GURL& process_lock_url) {
+ // `process_lock_url` is empty for documents that will share the default
+ // SiteInstanceGroup: they all use the BrowsingInstance's default policy
+ // rather than a site-specific exception, so they share one cache entry,
+ // keyed by the default site URL. Note we can't key off the document's own
+ // origin here: a same-origin document that *does* get a dedicated process
+ // (e.g. via DocumentIsolationPolicy) computes its value from a different
+ // policy scope and must not share this entry. We also can't key off the
+ // empty URL, since that produces an opaque origin that never matches.
+ const url::Origin cache_origin = url::Origin::Create(
+ process_lock_url.is_empty() ? SiteInstanceImpl::GetDefaultSiteURL()
+ : process_lock_url);
+
std::optional<bool> are_v8_optimizations_disabled_result =
ChildProcessSecurityPolicyImpl::GetInstance()
- ->LookupAreV8OptimizationsDisabled(
- browsing_instance_id, url::Origin::Create(process_lock_url));
+ ->LookupAreV8OptimizationsDisabled(browsing_instance_id,
+ cache_origin);
if (are_v8_optimizations_disabled_result.has_value()) {
return are_v8_optimizations_disabled_result.value();
}
@@ -209,7 +223,7 @@
browser_context, GURL());
bool are_v8_optimizations_disabled = CheckShouldDisableV8Optimization(
browser_context, isolation_context.browsing_instance_id(), std::nullopt,
- GURL());
+ /*process_lock_url=*/GURL());
WebExposedIsolationLevel web_exposed_isolation_level =
SiteInfo::ComputeWebExposedIsolationLevelForEmptySite(
@@ -320,13 +334,19 @@
!RequiresDedicatedProcessInternal(
site_url, isolation_context, browser_context,
url_info.requests_coop_isolation(),
- !url_info.oac_header_request.has_value(),
- url_info.is_sandboxed, url_info.embedder_isolation_info,
+ agent_cluster_key.IsOriginKeyed(), url_info.is_sandboxed,
+ url_info.embedder_isolation_info,
url_info.cross_origin_isolation_key.has_value() &&
url_info.cross_origin_isolation_key
->cross_origin_isolated_through_dip)
? GURL()
: agent_cluster_key.GetURL();
+ // TODO(crbug.com/559676326): JIT policy does not currently have a
+ // per-BrowsingInstance state cache in ChildProcessSecurityPolicyImpl like V8
+ // optimization does. If dynamic user settings are ever added for JIT,
+ // same-origin window.open popups could mismatch the opener process if the
+ // policy changes mid-session. Consider unifying JIT and V8 optimization
+ // state caching.
is_jitless =
is_jitless || GetContentClient()->browser()->IsJitDisabledForSite(
browser_context, agent_cluster_url_or_default);
diff --git a/chrome/browser/chrome_content_browser_client_unittest.cc b/chrome/browser/chrome_content_browser_client_unittest.cc
index c935ba6..f41a15a3 100644
--- a/chrome/browser/chrome_content_browser_client_unittest.cc
+++ b/chrome/browser/chrome_content_browser_client_unittest.cc
@@ -82,6 +82,7 @@
#include "components/search/ntp_features.h"
#include "components/search_engines/search_engines_switches.h"
#include "components/site_isolation/features.h"
+#include "components/site_isolation/site_isolation_policy.h"
#include "components/variations/variations_associated_data.h"
#include "components/version_info/version_info.h"
#include "components/webui/chrome_urls/pref_names.h"
@@ -2263,6 +2264,12 @@
{site_isolation::features::kOriginIsolationForJsOptExceptions},
{features::kOriginKeyedProcessesByDefault});
+ if (!site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJsOptExceptionsEnabled(&profile_)) {
+ GTEST_SKIP()
+ << "Skipping test since JS Opt origin isolation is not enabled.";
+ }
+
const GURL url("http://allowed.test");
auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
@@ -2277,6 +2284,32 @@
}
TEST_F(WillComputeSiteForNavigationTest,
+ IsolatesSitesThatHaveAJitlessException) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitWithFeatures(
+ {}, {features::kOriginKeyedProcessesByDefault,
+ features::kStrictOriginIsolation});
+
+ if (!site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJitlessExceptionsEnabled(&profile_)) {
+ GTEST_SKIP()
+ << "Skipping test since Jitless origin isolation is not enabled.";
+ }
+
+ const GURL url("http://blocked.test");
+
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_BLOCK);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
+ EXPECT_TRUE(IsOriginIsolatedByUser(url));
+}
+
+TEST_F(WillComputeSiteForNavigationTest,
IgnoresSitesThatMatchTheJavaScriptOptimizerSetting) {
const GURL url("http://blocked.test");
@@ -2310,6 +2343,27 @@
EXPECT_FALSE(IsOriginIsolatedByUser(url));
}
+// A site with an explicit JIT rule that agrees with the default is not
+// isolated: WillComputeSiteForNavigation() compares the effective setting
+// against the default, so such a rule is indistinguishable from no rule.
+// TODO(crbug.com/413695645): once the rules can be enumerated, origins named
+// explicitly may start being isolated.
+TEST_F(WillComputeSiteForNavigationTest,
+ IgnoresSitesWhoseJitRuleMatchesTheDefault) {
+ const GURL url("http://allowed.test");
+
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
+
+ EXPECT_FALSE(IsOriginIsolatedByUser(url));
+}
+
TEST_F(WillComputeSiteForNavigationTest,
WhenStrictOriginIsolationIsEnabledDoesNotIsolateUrl) {
// WillComputeSiteForNavigation should not do any work if
@@ -2332,6 +2386,28 @@
EXPECT_FALSE(IsOriginIsolatedByUser(url));
}
+TEST_F(WillComputeSiteForNavigationTest,
+ WhenStrictOriginIsolationIsEnabledDoesNotIsolateUrlForJitless) {
+ // WillComputeSiteForNavigation should not do any work if
+ // StrictOriginIsolation is enabled.
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(features::kStrictOriginIsolation);
+
+ const GURL url("http://blocked-but-wont-be-isolated-by-feature.test");
+
+ // Create the exception.
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_BLOCK);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/chrome_content_browser_client_unittest.cc b/chrome/browser/chrome_content_browser_client_unittest.cc
index c935ba6..f41a15a3 100644
--- a/chrome/browser/chrome_content_browser_client_unittest.cc
+++ b/chrome/browser/chrome_content_browser_client_unittest.cc
@@ -82,6 +82,7 @@
#include "components/search/ntp_features.h"
#include "components/search_engines/search_engines_switches.h"
#include "components/site_isolation/features.h"
+#include "components/site_isolation/site_isolation_policy.h"
#include "components/variations/variations_associated_data.h"
#include "components/version_info/version_info.h"
#include "components/webui/chrome_urls/pref_names.h"
@@ -2263,6 +2264,12 @@
{site_isolation::features::kOriginIsolationForJsOptExceptions},
{features::kOriginKeyedProcessesByDefault});
+ if (!site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJsOptExceptionsEnabled(&profile_)) {
+ GTEST_SKIP()
+ << "Skipping test since JS Opt origin isolation is not enabled.";
+ }
+
const GURL url("http://allowed.test");
auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
@@ -2277,6 +2284,32 @@
}
TEST_F(WillComputeSiteForNavigationTest,
+ IsolatesSitesThatHaveAJitlessException) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitWithFeatures(
+ {}, {features::kOriginKeyedProcessesByDefault,
+ features::kStrictOriginIsolation});
+
+ if (!site_isolation::SiteIsolationPolicy::
+ IsOriginIsolationForJitlessExceptionsEnabled(&profile_)) {
+ GTEST_SKIP()
+ << "Skipping test since Jitless origin isolation is not enabled.";
+ }
+
+ const GURL url("http://blocked.test");
+
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_BLOCK);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
+ EXPECT_TRUE(IsOriginIsolatedByUser(url));
+}
+
+TEST_F(WillComputeSiteForNavigationTest,
IgnoresSitesThatMatchTheJavaScriptOptimizerSetting) {
const GURL url("http://blocked.test");
@@ -2310,6 +2343,27 @@
EXPECT_FALSE(IsOriginIsolatedByUser(url));
}
+// A site with an explicit JIT rule that agrees with the default is not
+// isolated: WillComputeSiteForNavigation() compares the effective setting
+// against the default, so such a rule is indistinguishable from no rule.
+// TODO(crbug.com/413695645): once the rules can be enumerated, origins named
+// explicitly may start being isolated.
+TEST_F(WillComputeSiteForNavigationTest,
+ IgnoresSitesWhoseJitRuleMatchesTheDefault) {
+ const GURL url("http://allowed.test");
+
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
+
+ EXPECT_FALSE(IsOriginIsolatedByUser(url));
+}
+
TEST_F(WillComputeSiteForNavigationTest,
WhenStrictOriginIsolationIsEnabledDoesNotIsolateUrl) {
// WillComputeSiteForNavigation should not do any work if
@@ -2332,6 +2386,28 @@
EXPECT_FALSE(IsOriginIsolatedByUser(url));
}
+TEST_F(WillComputeSiteForNavigationTest,
+ WhenStrictOriginIsolationIsEnabledDoesNotIsolateUrlForJitless) {
+ // WillComputeSiteForNavigation should not do any work if
+ // StrictOriginIsolation is enabled.
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(features::kStrictOriginIsolation);
+
+ const GURL url("http://blocked-but-wont-be-isolated-by-feature.test");
+
+ // Create the exception.
+ auto* map = HostContentSettingsMapFactory::GetForProfile(&profile_);
+ map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_ALLOW);
+ map->SetContentSettingDefaultScope(url, url,
+ ContentSettingsType::JAVASCRIPT_JIT,
+ ContentSetting::CONTENT_SETTING_BLOCK);
+
+ browser_client_.WillComputeSiteForNavigation(&profile_, url);
+ // Check that the URL is not isolated.
+ EXPECT_FALSE(IsOriginIsolatedByUser(url));
+}
+
class IsJitDisabledForSiteTest : public ChromeContentBrowserClientTest {
protected:
bool IsJitDisabledForSite(const GURL& site_url) {
diff --git a/chrome/browser/policy/test/jit_policy_browsertest.cc b/chrome/browser/policy/test/jit_policy_browsertest.cc
index a9deb341..abe35908 100644
--- a/chrome/browser/policy/test/jit_policy_browsertest.cc
+++ b/chrome/browser/policy/test/jit_policy_browsertest.cc
@@ -6,12 +6,14 @@
#include "base/values.h"
#include "chrome/browser/policy/policy_test_utils.h"
+#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "content/public/browser/render_process_host.h"
+#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
@@ -159,14 +161,26 @@
"foo.com", "bar.com",
/*expect_jit_disabled=*/expected_result_for_default);
- // Here there is an invalid policy as the JavaScript JIT policies only support
- // eTLD+1 as origin.
+ // Navigating to foo.com evaluates to the default policy because foo.com
+ // does not match the rule scoped to subdomain.foo.com.
ExpectThatPolicyDisablesJitOnUrl(
"subdomain.foo.com", "foo.com",
/*expect_jit_disabled=*/expected_result_for_default);
+ // On standard desktop, dynamic origin isolation isolates subdomain.foo.com,
+ // allowing the subdomain rule to match. However, when origin-keyed processes
+ // are enabled by default (e.g., on linux-oi-rel), dynamic origin isolation
+ // for JIT exceptions is disabled. Because embedded_test_server() runs over
+ // HTTP and OriginKeyedProcessesByDefault only isolates HTTPS origins,
+ // subdomain.foo.com is not origin-isolated and shares foo.com's site process,
+ // falling back to the default policy.
+ bool expected_result_for_subdomain =
+ content::SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(
+ browser()->GetProfile())
+ ? expected_result_for_default
+ : true;
ExpectThatPolicyDisablesJitOnUrl(
"subdomain.foo.com", "subdomain.foo.com",
- /*expect_jit_disabled=*/expected_result_for_default);
+ /*expect_jit_disabled=*/expected_result_for_subdomain);
}
INSTANTIATE_TEST_SUITE_P(DefaultDisabled,
diff --git a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
index bdd2ee1..cfd124e 100644
--- a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
+++ b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
@@ -420,12 +420,28 @@
EXPECT_FALSE(AreV8OptimizationsDisabledOnActiveWebContents());
}
-// Test that if there's a rule for a.com that differs from the default, then the
-// user can't specify a rule for sub.a.com that contradicts that rule.
-// TODO(crbug.com/413695645): Make it possible for users to specify overrides so
-// that sub.a.com's behavior can differ from a.com's behavior.
+// Tests how non-default rules for a site (e.g., a.com) inherit to sub-sites
+// (e.g., sub.a.com) when the rules contradict each other. Which rule applies
+// depends on the process sub.a.com lands in:
+//
+// - Under full site isolation, or under partial site isolation once a.com has
+// been dynamically isolated, sub.a.com shares a.com's site-keyed process and
+// inherits a.com's rule.
+// TODO(crbug.com/413695645): Make it possible for users to specify overrides
+// so that sub.a.com's behavior can differ from a.com's even under site
+// isolation.
+//
+// - Under origin-keyed processes by default, sub.a.com gets its own
+// origin-keyed process and follows its own rule.
+//
+// - Under partial site isolation before a.com is dynamically isolated,
+// sub.a.com would share a process with a.com, but that process hosts the
+// BrowsingInstance's default SiteInstanceGroup rather than being locked to
+// a.com. Since such a process may host any unisolated site, its policy scope
+// URL is empty and the global default rule applies instead of either site's
+// rule.
IN_PROC_BROWSER_TEST_F(JavascriptOptimizerBrowserTest,
- ExceptionForSiteAppliesToSubSiteButCannotBeOverridden) {
+ SubSiteRuleInheritanceBehavior) {
auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
auto* map = HostContentSettingsMapFactory::GetForProfile(profile());
map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_OPTIMIZER,
@@ -441,7 +457,9 @@
ContentSettingsType::JAVASCRIPT_OPTIMIZER,
ContentSetting::CONTENT_SETTING_BLOCK);
- // Since this exception matches the default, it will not be isolated.
+ // First navigation to sub.a.com. Its custom policy (ALLOW) matches the
+ // default, so it is not proactively isolated. Additionally, a.com has not
+ // been visited yet, so a.com is not dynamically isolated either.
ASSERT_TRUE(content::NavigateToURL(
web_contents(),
embedded_https_test_server().GetURL("sub.a.com", "/simple.html")));
@@ -451,48 +469,50 @@
content::ChildProcessSecurityPolicy::IsolatedOriginSource::
USER_TRIGGERED));
- if (content::AreStrictSiteInstancesEnabled() &&
+ if (content::SiteIsolationPolicy::UseDedicatedProcessesForAllSites() &&
!content::SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(
profile())) {
- // if a.com is isolated already (as is the case with full site isolation)
- // or if DefaultSiteInstanceGroups are enabled, and origin isolation is not
- // used, the navigation to sub.a.com will be made in a SiteInstance with a
- // "a.com" site URL, which will match a.com BLOCK rule.
+ // Full site isolation: sub.a.com is given a dedicated process for site
+ // https://a.com/. The policy scope URL is https://a.com/, which matches
+ // a.com's BLOCK rule.
EXPECT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
} else {
- // If nothing is isolated by default (like on Android), we'll navigate in a
- // default SiteInstance which won't match that rule and will instead
- // retrieve the default rule. TODO(crbug.com/413695645): make it possible
- // for users to specify overrides so that sub.a.com's behavior can differ
- // from a.com's behavior.
+ // Origin-keyed processes by default: sub.a.com gets an origin-keyed
+ // process, matching its own ALLOW rule.
+ //
+ // Partial site isolation: sub.a.com joins the default SiteInstanceGroup,
+ // which an unisolated a.com would share as well. Its scope URL evaluates
+ // to empty, matching the global ALLOW rule.
EXPECT_FALSE(AreV8OptimizationsDisabledOnActiveWebContents());
}
+ // Unless origin-keyed processes are enabled by default, navigating to a.com
+ // dynamically isolates the origin https://a.com/ for the remainder of the
+ // session due to its non-default BLOCK rule.
ASSERT_TRUE(content::NavigateToURL(
web_contents(),
embedded_https_test_server().GetURL("a.com", "/simple.html")));
EXPECT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
- // Navigate back to sub.a.com, and we would like for js-opt to be enabled, but
- // they are disabled instead, because url provided to
- // AreV8OptimizationsDisabledForSite is a.com, which matches the block rule.
- // Ideally we'd be able to specify rules here, but to do that we need to pass
- // in the origin instead of the site. Currently, the site is passed because
- // sub.a.com is not origin isolated. TODO(crbug.com/413695645): Make it
- // possible for users to specify overrides so that sub.a.com's behavior can
- // differ from a.com's behavior.
+ // Second navigation to sub.a.com.
ASSERT_TRUE(content::NavigateToURL(
web_contents(),
embedded_https_test_server().GetURL("sub.a.com", "/simple.html")));
+
if (content::SiteIsolationPolicy::AreOriginKeyedProcessesEnabledByDefault(
profile())) {
- // Under origin isolation, the rule won't match sub.a.com, so optimizers
- // remain enabled.
+ // Origin-keyed processes by default: sub.a.com remains in its own
+ // origin-keyed process, avoiding a.com's restriction and matching its
+ // ALLOW rule.
EXPECT_FALSE(AreV8OptimizationsDisabledOnActiveWebContents());
} else {
- // Under site isolation, sub.a.com will be evaluated as a.com so the rule
- // will match.
+ // Full site isolation and partial site isolation: Because https://a.com/
+ // is now dynamically isolated, sub.a.com is forced into a dedicated process
+ // for https://a.com/. The policy scope URL evaluates to https://a.com/,
+ // overriding its own ALLOW rule.
+ // TODO(crbug.com/413695645): Make it possible to specify origin-based
+ // overrides so sub.a.com's behavior can differ from a.com's.
EXPECT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
}
}
diff --git a/components/site_isolation/site_isolation_policy_unittest.cc b/components/site_isolation/site_isolation_policy_unittest.cc
index 39b59312..98b59b72 100644
--- a/components/site_isolation/site_isolation_policy_unittest.cc
+++ b/components/site_isolation/site_isolation_policy_unittest.cc
@@ -217,6 +217,79 @@
}
#endif
+class OriginIsolationForJitlessExceptionsTest : public BaseSiteIsolationTest {};
+
+TEST_F(OriginIsolationForJitlessExceptionsTest,
+ ReturnsFalseWhenFeatureIsDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
... (truncated)
Original Bug Report
JIT policy bypass via inverted Origin-Agent-Cluster logic in SiteInfo
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in SiteInfo::Create incorrectly inverts the check for Origin-Agent-Cluster headers. On platforms using partial site isolation (like Android), this allows an attacker to bypass site-specific JIT blocklist policies by serving an OAC header.
Affected files:
content/browser/site_info.cc
Estimated timestamp from git blame: 2026-01-31
Description
A logic error exists in content/browser/site_info.cc within SiteInfo::Create that evaluates the need for a dedicated process. At line 312, the code passes the inverted boolean !url_info.oac_header_request.has_value() as the requires_origin_keyed_process parameter to RequiresDedicatedProcessInternal.
Due to this inversion, if a site serves an Origin-Agent-Cluster (OAC) header, the parameter evaluates to false. On platforms where strict site isolation is not universally enforced (e.g., Android with partial site isolation), this causes RequiresDedicatedProcessInternal to incorrectly return false.
Consequently, the code sets agent_cluster_url_or_default to an empty GURL(). When ChromeContentBrowserClient::IsJitDisabledForSite is subsequently called to evaluate whether V8 JIT should be disabled for the site, the empty GURL() causes it to fall back to the global default policy (which is typically ALLOW).
This creates a potential security bypass: if a high-risk origin is explicitly hardened via a JIT blocklist (using ContentSettingsType::JAVASCRIPT_JIT), an attacker controlling that origin can trivially bypass the blocklist and re-enable JIT simply by serving an OAC header.
Potential Reproduction Steps
Note: These are suggested steps based on static analysis, as our tooling cannot execute code yet.
- Target an environment with partial site isolation enabled (e.g., Android) where
features::kDefaultSiteInstanceGroupsis enabled. - Ensure the victim has an enterprise or user policy that blocks JavaScript JIT specifically for
https://attacker.example(e.g.,JAVASCRIPT_JITset to BLOCK), but leaves the global default as ALLOW. - The attacker configures
https://attacker.exampleto respond with the HTTP headerOrigin-Agent-Cluster: ?1. - The victim navigates to
https://attacker.example. - Due to the inverted logic in
SiteInfo::Create, the browser evaluates the JIT policy against an empty URL instead of the actual site URL. - The browser falls back to the default ALLOW policy, and the renderer process is launched with V8 JIT compilation and optimizations enabled, bypassing the targeted security policy.
Suggested Fix
In content/browser/site_info.cc inside SiteInfo::Create, fix the logic error when calling RequiresDedicatedProcessInternal. Instead of !url_info.oac_header_request.has_value(), the code should likely pass whether the origin actually requests an origin-keyed process, such as by evaluating the requested state or checking url_info.RequestsOriginKeyedProcess(isolation_context).
GURL agent_cluster_url_or_default =
ShouldUseDefaultSiteInstanceGroup() &&
!RequiresDedicatedProcessInternal(
site_url, isolation_context, browser_context,
url_info.requests_coop_isolation(),
/* FIX: Remove the negation or use the proper getter */
url_info.oac_header_request.has_value(),
url_info.is_sandboxed, url_info.is_pdf,
url_info.cross_origin_isolation_key.has_value() &&
url_info.cross_origin_isolation_key
->cross_origin_isolated_through_dip)
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.