CVE-2026-7963
Overview
Background
- Blink runtime features
- process-wide feature flags (
blink::WebRuntimeFeatures) that enable or disable web-platform capabilities in a renderer, some of which (e.g.MojomJS) are highly privileged. - `EmbeddedWorkerStartParams.forced_enabled_runtime_features`
- a Mojo field carrying a list of runtime-feature names the browser asked the renderer to force-enable when starting a service worker.
- `UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker`
- a
ContentBrowserClienthook the embedder could implement to populate that list, invoked only when the service worker’s process was locked to a site. - `ProcessLock::IsLockedToSite`
- the check that a renderer process is dedicated to one site, previously used as the guard for applying the forced features.
Root Cause Analysis
When starting an embedded (service) worker, embedded_worker_instance.cc called ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker to populate params->forced_enabled_runtime_features, and the renderer-side embedded_worker_instance_client_impl.cc then looped over that list calling blink::WebRuntimeFeatures::EnableFeatureFromString(feature, true), force-enabling each named feature process-wide. The mechanism was gated only on rph->GetProcessLock().IsLockedToSite(), and the field was in fact no longer used, but it left a dormant, powerful capability: a list of feature-name strings that, if populated, would flip on highly privileged Blink features inside the worker’s renderer. As the commit states, a compromised service worker could — depending on the future call tree — cause sensitive features such as MojomJS to be enabled, weakening the renderer sandbox’s attack surface.
The fix removes the entire path: the UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker virtual and its default definition, the browser-side call and its CHECK, the renderer-side enable loop, and the forced_enabled_runtime_features field from the embedded_worker.mojom EmbeddedWorkerStartParams. With no field to carry feature names and no code to apply them, service worker startup can no longer force-enable arbitrary runtime features. This is a capability removal, not a check re-ordering.
Attack Path
- Compromise a service worker renderer An attacker gains code execution in a renderer running a service worker (the standard threat model for renderer compromise).
- Reach the feature-enable channel
The startup path fed
forced_enabled_runtime_featuresfrom the browser into the renderer, which calledEnableFeatureFromStringfor each name, so a value that reaches this list flips on the corresponding Blink runtime feature process-wide. - Enable a sensitive feature
Depending on the future call tree, a compromised worker could cause privileged features such as
MojomJSto be enabled, expanding the renderer’s capabilities beyond its intended sandbox surface (inferred; the commit describes this as a potential source of a security issue rather than a demonstrated exploit). - Removed by the patch
After the fix there is no
forced_enabled_runtime_featuresfield and no enable loop, so the escalation channel is closed.
Impact Assessment
MojomJS) in its renderer, broadening what a renderer-compromise attacker could reach. The precondition is a compromised/malicious service worker renderer; the field was already unused, so this is defense-in-depth hardening. The advisory classifies it as “Inappropriate implementation in ServiceWorker”; no exploited-in-the-wild status is asserted, and the escalation to enabling sensitive features is described by the commit as potential and is (inferred) rather than demonstrated.Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/renderer/service_worker/embedded_worker_instance_client_impl.cc |
modified |
Files Changed
content/browser/service_worker/embedded_worker_instance.cccontent/public/browser/content_browser_client.cccontent/public/browser/content_browser_client.hcontent/renderer/service_worker/embedded_worker_instance_client_impl.ccthird_party/blink/public/mojom/service_worker/embedded_worker.mojom
Audit Directions
- Dormant privileged fieldsAudit Mojo structs and
ContentBrowserClienthooks for fields/methods that force-enable privileged capabilities but appear unused, since they remain reachable if a future or compromised caller populates them. - String-named feature toggles crossing the browser/renderer boundaryReview any browser-to-renderer path that enables features by name (
EnableFeatureFromString-style), verifying the value cannot originate from or be influenced by a compromised renderer. - Process-lock as the sole gateExamine capabilities guarded only by
IsLockedToSite, confirming that site isolation alone is not treated as sufficient authorization to grant highly privileged features.
Patch
From b01d62a11f3b07e5cd754949abd04b1d879e56b4 Mon Sep 17 00:00:00 2001 From: Ari Chivukula <[email protected]> Date: Tue, 31 Mar 2026 07:38:24 -0700 Subject: [PATCH] Remove Blink runtime feature force-enable power from ServiceWorkers. This change removes the ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker method and the associated forced_enabled_runtime_features field from EmbeddedWorkerStartParams, which were used to enable specific Blink runtime features for service workers in process-isolated renderers. This field was no longer used, and is the potential source of a security issue as a compromised service worker could enable sensitive features like MojomJS depending on the future call tree. Fixed: 497250399 Change-Id: I5a8a00e91bdba42abc9e25196fba5f67fec66250 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7715723 Auto-Submit: Ari Chivukula <[email protected]> Commit-Queue: Ari Chivukula <[email protected]> Reviewed-by: Arthur Sonzogni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1607853} --- diff --git a/content/browser/service_worker/embedded_worker_instance.cc b/content/browser/service_worker/embedded_worker_instance.cc index 8269d6f..13dc706 100644 --- a/content/browser/service_worker/embedded_worker_instance.cc +++ b/content/browser/service_worker/embedded_worker_instance.cc @@ -394,22 +394,6 @@ params->devtools_worker_token.ToString()); } - // To enable runtime features, the render process must be locked to the site. - // These features are highly privileged, so the renderer process with such - // features enabled shouldn't be used for other sites. - // - // WebUI schemes are process isolated already. To isolate other sites, the - // embedder can override ContentBrowserClient::ShouldLockProcessToSite(). - if (rph->GetProcessLock().IsLockedToSite()) { - GetContentClient() - ->browser() - ->UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker( - context_->wrapper()->browser_context(), params->script_url, - params->forced_enabled_runtime_features); - } - CHECK(params->forced_enabled_runtime_features.empty() || - rph->GetProcessLock().IsLockedToSite()); - // TODO(crbug.com/40584626): Support changes to blink::RendererPreferences // while the worker is running. DCHECK(context_->wrapper()->browser_context() || diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc index 2e640bb..35e4074 100644 --- a/content/public/browser/content_browser_client.cc +++ b/content/public/browser/content_browser_client.cc @@ -499,11 +499,6 @@ return true; } -void ContentBrowserClient::UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker( - BrowserContext* context, - const GURL& script_url, - std::vector<std::string>& out_forced_enabled_runtime_features) {} - bool ContentBrowserClient::AllowSharedWorker( const GURL& worker_url, const net::SiteForCookies& site_for_cookies, diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h index ba90f08..27a2f10 100644 --- a/content/public/browser/content_browser_client.h +++ b/content/public/browser/content_browser_client.h @@ -934,20 +934,6 @@ const GURL& scope, BrowserContext* browser_context); - // Allows the embedder to enable process-wide blink features before starting a - // service worker. This is similar to - // `blink.mojom.CommitNavigationParams.force_enabled_origin_trials` but for - // RuntimeFeatures instead of Origin Trials. - // - // This method is only called when the process that will run the Service - // Worker is isolated. These features can be highly privileged, so the - // renderer process with such features enabled shouldn't be used for other - // sites. - virtual void UpdateEnabledBlinkRuntimeFeaturesInIsolatedWorker( - BrowserContext* context, - const GURL& script_url, - std::vector<std::string>& out_forced_enabled_runtime_features); - // Allow the embedder to control if a Shared Worker can be connected from a // given tab. // This is called on the UI thread. diff --git a/content/renderer/service_worker/embedded_worker_instance_client_impl.cc b/content/renderer/service_worker/embedded_worker_instance_client_impl.cc index b4a41b69..09b1643 100644 --- a/content/renderer/service_worker/embedded_worker_instance_client_impl.cc +++ b/content/renderer/service_worker/embedded_worker_instance_client_impl.cc @@ -98,10 +98,6 @@ start_data->is_cross_origin_isolated = params->cross_origin_isolated; - for (const auto& feature : params->forced_enabled_runtime_features) { - blink::WebRuntimeFeatures::EnableFeatureFromString(feature, true); - } - // `cache_storage` may be null if COEP is not enabled, we cannot bind // eagerly in that case. mojo::PendingRemote<blink::mojom::CacheStorage> cache_storage = diff --git a/third_party/blink/public/mojom/service_worker/embedded_worker.mojom b/third_party/blink/public/mojom/service_worker/embedded_worker.mojom index dfc719e3..68a0ff7 100644 --- a/third_party/blink/public/mojom/service_worker/embedded_worker.mojom +++ b/third_party/blink/public/mojom/service_worker/embedded_worker.mojom @@ -91,15 +91,6 @@ // Used to set up fetch requests. RendererPreferences renderer_preferences; - // List of blink runtime features (based on their names) to enable for this - // renderer process. To avoid exposing dangerous features to other sites, this - // is only populated if the renderer process is isolated. - // - // Ideally this would be part of content.mojom.Renderer, but - // EmbedderWorkerInstanceClient and content.mojom.Renderer use separate pipes - // so a ServiceWorker could be started before the features are enabled. - array<string> forced_enabled_runtime_features; - // Used to talk to the service worker from the browser process. pending_receiver<ServiceWorker> service_worker_receiver;
Original Bug Report
ProtectedMemory bypass for MojoJS via runtime-enabled feature string iteration gadget
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic error in the generated code for runtime-enabled features allows an attacker with an arbitrary memory write primitive to bypass ProtectedMemory mitigations for MojoJS. By corrupting the forced_enabled_runtime_features vector during Service Worker startup, an attacker can invoke a generated string-to-setter mapping that legitimately unprotects and enables the flag. This exposes window.Mojo to the web context, bypassing context checks and facilitating a renderer sandbox escape.
Affected files:
third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmplcontent/renderer/service_worker/embedded_worker_instance_client_impl.ccthird_party/blink/renderer/platform/runtime_enabled_features.json5
Estimated timestamp from git blame: 2024-05-16
Background
ProtectedMemory is designed to prevent data-only attacks from modifying security-critical variables (like is_mojo_js_enabled_) by keeping them in read-only memory pages. They are only briefly made writable during legitimate updates via base::AutoWritableMemory.
Vulnerability Details
A structural flaw exists in how RuntimeEnabledFeaturesBase::SetFeatureEnabledFromString is generated by third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmpl.
The generator creates a static kFeatures[] array mapping feature string names to their respective setter functions. Crucially, it includes features marked with is_protected_feature: true. The generated setter for a protected feature (e.g., SetMojoJSEnabled) legitimately uses base::AutoWritableMemory to unprotect, modify, and re-protect the memory.
Because this setter is reachable via a string-indexed lookup, it creates a powerful Data-Oriented Programming (DOP) gadget. If an attacker can control a string passed to WebRuntimeFeatures::EnableFeatureFromString, they can safely toggle ProtectedMemory variables without causing a segmentation fault.
Potential Attack Path
Note: These are suggested steps based on static analysis; our tooling does not currently have the ability to run code to produce a live Proof-of-Concept.
- Obtain Primitives: An attacker exploits a typical renderer vulnerability (e.g., a V8 bug) to gain arbitrary memory read/write capabilities.
- Trigger IPC: The attacker triggers a Service Worker startup sequence, causing the browser to send an
EmbeddedWorkerStartParamsPtrIPC to the renderer. - Corrupt Heap Data: During deserialization, the
params->forced_enabled_runtime_featuresstd::vector<std::string>is allocated on the renderer’s heap. While theStartWorkertask is queued, the attacker uses their write primitive to overwrite the vector’s backing store to point to a crafted string containing"MojoJS". - Execute Gadget:
EmbeddedWorkerInstanceClientImpl::StartWorkerexecutes and iterates over the corrupted vector, callingblink::WebRuntimeFeatures::EnableFeatureFromString("MojoJS", true). - Bypass ProtectedMemory: The gadget performs a binary search, finds
"MojoJS", and invokesSetMojoJSEnabled(true).base::AutoWritableMemorysafely unprotects the page, sets the globalis_mojo_js_enabled_flag to true, and re-protects it. - Evade Context Checks: The attacker creates a new execution context (e.g., a new
<iframe>). In Blink IDL,Mojouses[ContextEnabled=MojoJS, RuntimeEnabled=MojoJS]. The generated bindings (v8_window.cc) use anORcondition for these attributes. Because the globalRuntimeEnabledflag is now true, the check short-circuits, completely bypassing the strictContextFeatureSettings::CrashIfMojoJSNotAllowed()mitigation. - Sandbox Escape:
window.Mojois exposed to the attacker’s JavaScript, allowing them to bind privileged browser interfaces and escape the sandbox.
Suggested Fix
Modify third_party/blink/renderer/build/scripts/templates/runtime_enabled_features.cc.tmpl to explicitly exclude features marked with is_protected_feature: true from the kFeatures[] array used by SetFeatureEnabledFromString (and potentially IsFeatureEnabledFromString). Protected features should not be toggleable via dynamic string lookups.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.