diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 4040c1adc4..03bb0e146c 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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> { + 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, diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs index 375cda4cb7..7ab6593144 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs @@ -66,11 +66,7 @@ pub(in crate::context_bootstrap) fn install_worker_exposed_interfaces<'s>( secure_context: bool, specs: Vec, ) -> 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, +) -> Result> { + 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( + scope: &mut v8::PinScope<'_, '_, C>, + realm_kind: RealmKind, + specs: Vec, +) -> Result> { + 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>, diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs index edc261971c..eb9e78fc3a 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs @@ -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::{ diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/template_registry.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/template_registry.rs index 6345c37981..411057fd3c 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/template_registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/template_registry.rs @@ -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> { - scope.get_slot::>().cloned() + pub(super) fn current(scope: &mut v8::PinScope<'_, '_, C>) -> Option> { + scope.as_mut().get_slot::>().cloned() + } + + pub(super) const fn profile(&self) -> TemplateBuildProfile { + self.profile } pub(super) fn metadata(&self, id: InterfaceId) -> Option { diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 1e180964b3..39b01845b8 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -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>, + 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, + service_worker: v8::Global, +} + +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 { + 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(()) } diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index d957cc66bb..b80d17f399 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -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 { diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index 2ecb989827..7572c3d03a 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -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::(); + 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();