mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
fix(service-worker): make global prototype chain immutable
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user