fix(service-worker): make global prototype chain immutable

This commit is contained in:
ldm0
2026-09-14 15:06:19 +08:00
parent c9ff56bbbe
commit 41d173a365
7 changed files with 240 additions and 30 deletions
+10
View File
@@ -592,6 +592,16 @@ pub(crate) fn install_worker_lazy_exposed_interfaces<'s>(
.map_err(|error| anyhow!("failed to initialize worker caches accessor: {error}"))
}
pub(crate) fn prepare_service_worker_event_target_template<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
) -> Result<v8::Local<'s, v8::FunctionTemplate>> {
exposed_interfaces::prepare_worker_event_target_template(
scope,
exposed_interfaces::RealmKind::ServiceWorker,
constructor_specs(),
)
}
pub(in crate::context_bootstrap) fn build_profiled_exposed_interface_template<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
spec: self::specs::ConstructorSpec,
@@ -66,11 +66,7 @@ pub(in crate::context_bootstrap) fn install_worker_exposed_interfaces<'s>(
secure_context: bool,
specs: Vec<ConstructorSpec>,
) -> Result<()> {
let registry = ExposedInterfaceTemplateRegistry::install(
scope,
specs,
TemplateBuildProfile::for_realm(realm_kind),
)?;
let registry = worker_interface_template_registry(scope, realm_kind, specs)?;
IntrinsicInterfaceRegistry::initialize_for_current_context(scope, registry.len(), realm_kind)?;
for metadata in registry.metadata_entries() {
@@ -102,6 +98,36 @@ pub(in crate::context_bootstrap) fn install_worker_exposed_interfaces<'s>(
Ok(())
}
pub(in crate::context_bootstrap) fn prepare_worker_event_target_template<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
realm_kind: RealmKind,
specs: Vec<ConstructorSpec>,
) -> Result<v8::Local<'s, v8::FunctionTemplate>> {
let registry = worker_interface_template_registry(scope, realm_kind, specs)?;
let event_target_id = registry
.id_by_name("EventTarget")
.ok_or_else(|| anyhow!("worker interface registry is missing EventTarget"))?;
registry.get_or_build_template(scope, event_target_id)
}
fn worker_interface_template_registry<C>(
scope: &mut v8::PinScope<'_, '_, C>,
realm_kind: RealmKind,
specs: Vec<ConstructorSpec>,
) -> Result<Rc<ExposedInterfaceTemplateRegistry>> {
let expected_profile = TemplateBuildProfile::for_realm(realm_kind);
if let Some(registry) = ExposedInterfaceTemplateRegistry::current(scope) {
if registry.profile() != expected_profile {
return Err(anyhow!(
"worker interface registry profile mismatch: expected {expected_profile:?}, got {:?}",
registry.profile()
));
}
return Ok(registry);
}
ExposedInterfaceTemplateRegistry::install(scope, specs, expected_profile)
}
pub(crate) fn filter_window_exposed_interfaces(
scope: &mut v8::PinScope<'_, '_>,
global: v8::Local<'_, v8::Object>,
@@ -12,6 +12,7 @@ pub(super) use install::{
filter_window_exposed_interfaces, initialize_realm_interface_registry,
install_interface_template_metadata, install_window_exposed_interfaces,
install_worker_exposed_interfaces, is_lazy_exposed_interface,
prepare_worker_event_target_template,
};
#[cfg(test)]
pub(crate) use install::{
@@ -81,8 +81,12 @@ impl ExposedInterfaceTemplateRegistry {
.ok_or_else(|| anyhow!("exposed interface template registry was already installed"))
}
pub(super) fn current(scope: &mut v8::PinScope<'_, '_>) -> Option<Rc<Self>> {
scope.get_slot::<Rc<Self>>().cloned()
pub(super) fn current<C>(scope: &mut v8::PinScope<'_, '_, C>) -> Option<Rc<Self>> {
scope.as_mut().get_slot::<Rc<Self>>().cloned()
}
pub(super) const fn profile(&self) -> TemplateBuildProfile {
self.profile
}
pub(super) fn metadata(&self, id: InterfaceId) -> Option<ExposedInterfaceMetadata> {
+89 -11
View File
@@ -3003,6 +3003,7 @@ pub(super) fn install_worker_global_scope<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
state: Rc<RefCell<WorkerGlobalState>>,
service_worker_templates: Option<&PreparedServiceWorkerGlobalScopeTemplates>,
) -> Result<()> {
// Store state pointer as an external on the global so callbacks can find it.
let state_ptr = Rc::into_raw(state.clone()) as *mut c_void;
@@ -3029,7 +3030,12 @@ pub(super) fn install_worker_global_scope<'s>(
)
.initialize(scope, global)
.map_err(|error| anyhow!("failed to initialize worker global bootstrap properties: {error}"))?;
install_worker_global_scope_constructors(scope, global, &global_kind)?;
install_worker_global_scope_constructors(
scope,
global,
&global_kind,
service_worker_templates,
)?;
let realm_kind = match &global_kind {
super::thread::WorkerGlobalKind::Dedicated { .. } => {
crate::context_bootstrap::exposed_interfaces::RealmKind::DedicatedWorker
@@ -5511,10 +5517,57 @@ fn worker_create_image_bitmap_callback<'s>(
rv.set(promise.into());
}
pub(super) struct PreparedServiceWorkerGlobalScopeTemplates {
worker: v8::Global<v8::FunctionTemplate>,
service_worker: v8::Global<v8::FunctionTemplate>,
}
impl PreparedServiceWorkerGlobalScopeTemplates {
pub(super) fn global_template<'s>(
&self,
scope: &mut v8::PinScope<'s, '_, ()>,
) -> v8::Local<'s, v8::ObjectTemplate> {
v8::Local::new(scope, &self.service_worker).instance_template(scope)
}
}
pub(super) fn prepare_service_worker_global_scope_templates<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
) -> Result<PreparedServiceWorkerGlobalScopeTemplates> {
let event_target =
crate::context_bootstrap::prepare_service_worker_event_target_template(scope)?;
event_target.prototype_template(scope).set_immutable_proto();
let worker = worker_global_scope_template(scope, "WorkerGlobalScope");
worker.inherit(event_target);
let service_worker = worker_global_scope_template(scope, "ServiceWorkerGlobalScope");
service_worker.inherit(worker);
service_worker
.instance_template(scope)
.set_immutable_proto();
Ok(PreparedServiceWorkerGlobalScopeTemplates {
worker: v8::Global::new(scope, worker),
service_worker: v8::Global::new(scope, service_worker),
})
}
fn worker_global_scope_template<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
name: &'static str,
) -> v8::Local<'s, v8::FunctionTemplate> {
let template =
v8::FunctionTemplate::builder(worker_global_scope_constructor_callback).build(scope);
template.set_class_name(v8str(scope, name));
template.prototype_template(scope).set_immutable_proto();
template
}
fn install_worker_global_scope_constructors<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
global_kind: &super::thread::WorkerGlobalKind,
service_worker_templates: Option<&PreparedServiceWorkerGlobalScopeTemplates>,
) -> Result<()> {
let interface = match global_kind {
super::thread::WorkerGlobalKind::Dedicated { .. } => "DedicatedWorkerGlobalScope",
@@ -5522,6 +5575,16 @@ fn install_worker_global_scope_constructors<'s>(
super::thread::WorkerGlobalKind::Service { .. } => "ServiceWorkerGlobalScope",
};
web_api_interfaces::initialize(scope, global, interface)?;
if matches!(global_kind, super::thread::WorkerGlobalKind::Service { .. }) {
let templates = service_worker_templates.ok_or_else(|| {
anyhow!(
"service worker global scope templates were not prepared before context creation"
)
})?;
return install_prepared_service_worker_global_constructor(scope, global, templates);
}
let worker_ctor = worker_scope_constructor(scope, "WorkerGlobalScope")?;
let worker_proto = constructor_prototype(scope, worker_ctor, "WorkerGlobalScope")?;
set_worker_to_string_tag(scope, worker_proto, "WorkerGlobalScope");
@@ -5535,9 +5598,7 @@ fn install_worker_global_scope_constructors<'s>(
super::thread::WorkerGlobalKind::Shared { .. } => {
install_shared_worker_global_constructor(scope, global, worker_ctor, worker_proto)?
}
super::thread::WorkerGlobalKind::Service { .. } => {
install_service_worker_global_constructor(scope, global, worker_ctor, worker_proto)?
}
super::thread::WorkerGlobalKind::Service { .. } => unreachable!(),
}
Ok(())
}
@@ -5591,16 +5652,26 @@ fn install_shared_worker_global_constructor<'s>(
Ok(())
}
fn install_service_worker_global_constructor<'s>(
fn install_prepared_service_worker_global_constructor<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
worker_ctor: v8::Local<'s, v8::Function>,
worker_proto: v8::Local<'s, v8::Object>,
templates: &PreparedServiceWorkerGlobalScopeTemplates,
) -> Result<()> {
let service_ctor = worker_scope_constructor(scope, "ServiceWorkerGlobalScope")?;
let worker_template = v8::Local::new(scope, &templates.worker);
let worker_ctor = worker_template
.get_function(scope)
.ok_or_else(|| anyhow!("failed to instantiate WorkerGlobalScope constructor"))?;
let worker_proto = constructor_prototype(scope, worker_ctor, "WorkerGlobalScope")?;
set_worker_to_string_tag(scope, worker_proto, "WorkerGlobalScope");
WorkerGlobalScopeConstructorGlobalDeclaration::new(worker_ctor)
.initialize(scope, global)
.map_err(|error| anyhow!("failed to initialize WorkerGlobalScope global: {error}"))?;
let service_template = v8::Local::new(scope, &templates.service_worker);
let service_ctor = service_template
.get_function(scope)
.ok_or_else(|| anyhow!("failed to instantiate ServiceWorkerGlobalScope constructor"))?;
let service_proto = constructor_prototype(scope, service_ctor, "ServiceWorkerGlobalScope")?;
let _ = service_proto.set_prototype(scope, worker_proto.into());
let _ = service_ctor.set_prototype(scope, worker_ctor.into());
set_worker_to_string_tag(scope, service_proto, "ServiceWorkerGlobalScope");
ServiceWorkerGlobalScopeConstructorGlobalDeclaration::new(service_ctor)
.initialize(scope, global)
@@ -5608,7 +5679,14 @@ fn install_service_worker_global_constructor<'s>(
anyhow!("failed to initialize ServiceWorkerGlobalScope global: {error}")
})?;
ensure_worker_interface_constructor(scope, "NavigationPreloadManager")?;
let _ = global.set_prototype(scope, service_proto.into());
if !global
.get_prototype(scope)
.is_some_and(|prototype| prototype.strict_equals(service_proto.into()))
{
return Err(anyhow!(
"service worker global template did not install ServiceWorkerGlobalScope.prototype"
));
}
Ok(())
}
+47 -12
View File
@@ -90,7 +90,7 @@ use super::global_scope::{
fulfill_pending_worker_csp_report, fulfill_pending_worker_fetch,
fulfill_pending_worker_fetch_response, fulfill_pending_worker_xhr,
fulfill_pending_worker_xhr_response, install_worker_global_scope,
service_worker_fetch_handler_type,
prepare_service_worker_global_scope_templates, service_worker_fetch_handler_type,
};
use super::handle::{
WorkerBootstrapCompletion, WorkerBootstrapFailure, WorkerBootstrapSuccess,
@@ -1757,7 +1757,35 @@ async fn worker_main(
let scope = pin!(v8::HandleScope::new(isolate));
let scope = &mut scope.init();
*isolate_handle.lock() = Some(scope.thread_safe_handle());
let ctx = v8::Context::new(scope, Default::default());
let service_worker_templates =
if matches!(state.borrow().global_kind, WorkerGlobalKind::Service { .. }) {
match prepare_service_worker_global_scope_templates(scope) {
Ok(templates) => Some(templates),
Err(error) => {
tracing::error!(
url = %script_url,
error = %error,
"failed to prepare service worker global templates"
);
bootstrap_completion
.mark_install_global_failure(&script_url, error.to_string());
install_global_failed = true;
None
}
}
} else {
None
};
let global_template = service_worker_templates
.as_ref()
.map(|templates| templates.global_template(scope));
let ctx = v8::Context::new(
scope,
v8::ContextOptions {
global_template,
..Default::default()
},
);
crate::resource_owner::install_resource_owner_for_context(ctx, resource_owner_id);
crate::context_bootstrap::set_indexed_db_manager_for_context(
ctx,
@@ -1776,17 +1804,24 @@ async fn worker_main(
let scope = &mut v8::ContextScope::new(scope, ctx);
let global = ctx.global(scope);
if let Err(e) = install_worker_global_scope(scope, global, state.clone()) {
tracing::error!(url = %script_url, error = %e, "failed to install worker global scope");
bootstrap_completion.mark_install_global_failure(&script_url, e.to_string());
install_global_failed = true;
} else if script_kind == WorkerScriptKind::Classic {
let referrer_policy = { state.borrow().referrer_policy.clone() };
install_classic_worker_dynamic_module_runtime(
if !install_global_failed {
if let Err(e) = install_worker_global_scope(
scope,
referrer_policy,
module_evaluation_tx.clone(),
);
global,
state.clone(),
service_worker_templates.as_ref(),
) {
tracing::error!(url = %script_url, error = %e, "failed to install worker global scope");
bootstrap_completion.mark_install_global_failure(&script_url, e.to_string());
install_global_failed = true;
} else if script_kind == WorkerScriptKind::Classic {
let referrer_policy = { state.borrow().referrer_policy.clone() };
install_classic_worker_dynamic_module_runtime(
scope,
referrer_policy,
module_evaluation_tx.clone(),
);
}
}
}
if install_global_failed {
@@ -238,6 +238,62 @@ async fn service_worker_global_scope_does_not_expose_close() {
handle.terminate_and_join();
}
#[tokio::test]
async fn service_worker_global_prototype_chain_is_complete_and_immutable() {
ensure_v8();
let (bootstrap_tx, mut bootstrap_rx) =
tokio::sync::mpsc::unbounded_channel::<crate::worker::WorkerBootstrapCompletion>();
let handle = spawn_test_worker_with_options(
WorkerSpawnOptions::new(
r#"
const chain = [];
for (let value = self; value !== null; value = Object.getPrototypeOf(value)) {
chain.push(value);
}
const expected = [
self,
ServiceWorkerGlobalScope.prototype,
WorkerGlobalScope.prototype,
EventTarget.prototype,
Object.prototype,
];
if (chain.length !== expected.length ||
chain.some((value, index) => value !== expected[index])) {
throw new Error("service worker global prototype chain is incomplete");
}
for (const value of chain) {
const original = Object.getPrototypeOf(value);
if (Reflect.setPrototypeOf(value, {}) ||
Object.getPrototypeOf(value) !== original ||
!Reflect.setPrototypeOf(value, original)) {
throw new Error("service worker global prototype chain is mutable");
}
if (!Object.isExtensible(value)) {
throw new Error("immutable prototype object must remain extensible");
}
}
"#
.to_owned(),
"https://example.test/app/immutable-prototype-sw.js".to_owned(),
)
.with_global_kind(crate::worker::WorkerGlobalKind::Service {
registration_id: ServiceWorkerRegistrationId::from_u64_for_test(1),
version_id: ServiceWorkerVersionId::from_u64_for_test(1),
scope_url: url::Url::parse("https://example.test/app/").unwrap(),
})
.with_bootstrap_completion_sender(bootstrap_tx),
);
let bootstrap = timeout(TIMEOUT, bootstrap_rx.recv())
.await
.expect("timed out waiting for immutable service worker bootstrap")
.expect("service worker bootstrap channel closed");
bootstrap
.result
.expect("service worker global prototype chain should be complete and immutable");
handle.terminate_and_join();
}
#[tokio::test]
async fn worker_pause_evaluation_until_debugger_exposes_context_before_bootstrap() {
ensure_v8();