diff --git a/moli-file-api/src/blob_store.rs b/moli-file-api/src/blob_store.rs index e9163cc661..4979c1ee6e 100644 --- a/moli-file-api/src/blob_store.rs +++ b/moli-file-api/src/blob_store.rs @@ -246,6 +246,32 @@ where Some((String::from_utf8_lossy(&bytes).into_owned(), mime_type)) } + /// Revoke a departing context's URLs while preserving bytes retained by + /// wrappers, readers, or URLs created by another context. + pub fn retire_owner_resources(&self, owner_id: OwnerId) { + let retired = { + let mut urls = self.object_urls.lock(); + let mut retired = Vec::new(); + urls.retain(|_, state| { + if state.owner_id == Some(owner_id) { + retired.push(state.blob_id); + false + } else { + true + } + }); + retired + }; + for blob_id in retired { + self.release_blob_object_url_ref(blob_id); + } + for blob in self.blobs.lock().by_id.values_mut() { + if blob.owner_id == Some(owner_id) { + blob.owner_id = None; + } + } + } + /// Remove Blob/object URL entries owned by a context. pub fn cleanup_owner_resources(&self, owner_id: OwnerId) { let removed_blob_ids = { @@ -504,6 +530,30 @@ mod tests { assert!(store.blob_bytes_by_uuid_in_partition(&uuid, &10).is_none()); } + #[test] + fn retiring_owner_preserves_wrappers_readers_and_foreign_urls() { + let store = BlobStore::::default(); + let blob = store.create_blob(Some(1), Some(10), b"retained".to_vec(), "text/plain".into()); + let own_url = store + .create_object_url(Some(1), blob, "https://example.test") + .unwrap(); + let foreign_url = store + .create_object_url(Some(2), blob, "https://example.test") + .unwrap(); + store.retain_blob_reader_ref(blob); + store.retire_owner_resources(1); + assert!(store.object_url_bytes_and_type(&own_url).is_none()); + assert_eq!(store.blob_bytes(blob).unwrap(), b"retained"); + store.release_blob_wrapper_ref(blob); + store.release_blob_reader_ref(blob); + assert_eq!( + store.object_url_bytes_and_type(&foreign_url).unwrap().0, + b"retained" + ); + assert!(store.revoke_object_url(&foreign_url)); + assert!(store.blob_bytes(blob).is_none()); + } + #[test] fn cleanup_owner_removes_owned_blobs_and_urls() { let store = BlobStore::::default(); diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index a4c0c0d494..98c226c829 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -487,7 +487,7 @@ fn blob_mime_type(blob_id: BlobId) -> Option { } pub(crate) fn cleanup_owner_resources(owner_id: ResourceOwnerId) { - blob_store().cleanup_owner_resources(owner_id); + blob_store().retire_owner_resources(owner_id); } fn release_blob_wrapper_ref(blob_id: BlobId) { diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs index dda15594d3..fe481fec7a 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs @@ -530,6 +530,16 @@ pub(crate) fn new_dom_exception_value<'s>( scope, "DOMException", ) + .or_else(|| { + // A native exception must materialize its own realm's intrinsic. + // Reading the public Window property can run author code or enter + // an access check again after navigation detaches its global. + crate::context_bootstrap::exposed_interfaces::ensure_intrinsic_interface_prototype( + scope, + "DOMException", + ) + .ok() + }) { let exception = v8::Object::new(scope); initialize_dom_exception(scope, exception, message, name); 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 888e685109..fdeda3af90 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/mod.rs @@ -30,3 +30,5 @@ pub(super) use metadata::constructor_spec_is_eager; #[cfg(test)] pub(crate) use metadata::dedicated_worker_lazy_interface_names_for_test; pub(in crate::context_bootstrap) use template_registry::ExposedInterfaceTemplateRegistry; + +pub(crate) use realm_registry::retain_intrinsic_interfaces_in_realm; diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs index cdcba7a632..c735cf8d09 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/realm_registry.rs @@ -20,9 +20,9 @@ pub(super) struct IntrinsicInterfaceRegistry { } pub(super) struct RealmInterfaceObjects { - constructor: v8::Global, - prototype: v8::Global, - public_interface: v8::Global, + constructor: crate::util::RealmObjectHandle, + prototype: crate::util::RealmObjectHandle, + public_interface: crate::util::RealmObjectHandle, } impl IntrinsicInterfaceRegistry { @@ -156,9 +156,9 @@ impl IntrinsicInterfaceRegistry { RealmInterfaceEntry::Uninitialized | RealmInterfaceEntry::Materializing => {} } *slot = RealmInterfaceEntry::Ready(RealmInterfaceObjects { - constructor: v8::Global::new(scope, constructor), - prototype: v8::Global::new(scope, prototype), - public_interface: v8::Global::new(scope, public_interface), + constructor: crate::util::RealmObjectHandle::new(scope, constructor), + prototype: crate::util::RealmObjectHandle::new(scope, prototype), + public_interface: crate::util::RealmObjectHandle::new(scope, public_interface), }); Ok(()) } @@ -172,7 +172,7 @@ impl IntrinsicInterfaceRegistry { let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else { return None; }; - Some(v8::Local::new(scope, &objects.constructor)) + objects.constructor.to_local(scope) } pub(super) fn prototype<'s>( @@ -184,7 +184,7 @@ impl IntrinsicInterfaceRegistry { let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else { return None; }; - Some(v8::Local::new(scope, &objects.prototype)) + objects.prototype.to_local(scope) } pub(super) fn public_interface<'s>( @@ -196,10 +196,25 @@ impl IntrinsicInterfaceRegistry { let RealmInterfaceEntry::Ready(objects) = entries.get(id.index())? else { return None; }; - Some(v8::Local::new(scope, &objects.public_interface)) + objects.public_interface.to_local(scope) } } +pub(crate) fn retain_intrinsic_interfaces_in_realm(scope: &mut v8::PinScope<'_, '_>) { + let context = scope.get_current_context(); + let Some(registry) = context.get_slot::() else { + return; + }; + for entry in registry.entries.borrow_mut().iter_mut() { + if let RealmInterfaceEntry::Ready(objects) = entry { + objects.constructor.retain_in_realm(scope); + objects.prototype.retain_in_realm(scope); + objects.public_interface.retain_in_realm(scope); + } + } + crate::util::retain_context_v8_handle_state_for_safe_release(context, registry); +} + #[cfg(test)] mod tests { use std::pin::pin; diff --git a/moli-renderer-v8/src/document_runtime/lifecycle.rs b/moli-renderer-v8/src/document_runtime/lifecycle.rs index beaf3ba228..c8be855dae 100644 --- a/moli-renderer-v8/src/document_runtime/lifecycle.rs +++ b/moli-renderer-v8/src/document_runtime/lifecycle.rs @@ -37,6 +37,19 @@ fn image_is_small_for_priority_boost(element: &crate::dom::native::Element) -> b // the host document state accessors, current-script/parser visibility // bookkeeping, and pending resource-load delivery that feed DCL/load decisions. impl DocumentRuntime { + pub(crate) fn retire_v8_execution_state_for_context_teardown(&mut self) { + self.timeouts = HostTimeoutScheduler::default(); + self.events = HostEventTargetRegistry::default(); + self.script_lifecycle.clear_for_document_replacement(); + self.root_document_parser = None; + self.document_write_script_preload_scanner = None; + self.document_write_script_preloads.clear(); + self.pending_parser_blocking_work = None; + self.pending_parser_post_step_runtime_work = Default::default(); + self.reset_main_parser_continuation_for_document_replacement(); + self.main_document_script_preloads.clear(); + } + pub(crate) fn bind_main_document_script_preload_store( &mut self, store: crate::runtime::DocumentScriptPreloadStore, diff --git a/moli-renderer-v8/src/native_bridge/abort.rs b/moli-renderer-v8/src/native_bridge/abort.rs index b5993d1692..859d204023 100644 --- a/moli-renderer-v8/src/native_bridge/abort.rs +++ b/moli-renderer-v8/src/native_bridge/abort.rs @@ -23,6 +23,9 @@ pub(crate) use statics::{ }; const ABORT_SIGNAL_ID_SLOT: &str = "__lmAbortSignalId"; +const ABORT_SIGNAL_STATE_SLOT: &str = "__moliAbortSignalState"; +const ABORT_SIGNAL_ABORTED_SLOT: &str = "__moliAbortSignalAborted"; +const ABORT_SIGNAL_REASON_SLOT: &str = "__moliAbortSignalReason"; const ABORT_CONTROLLER_ID_SLOT: &str = "__lmAbortControllerId"; const ABORT_CONTROLLER_SIGNAL_SLOT: &str = "__lmAbortControllerSignal"; @@ -35,6 +38,9 @@ pub(crate) fn bind_signal_wrapper<'s>( return false; }; set_private_value(scope, wrapper, ABORT_SIGNAL_ID_SLOT, id); + if let Some(state) = get_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT) { + set_private_value(scope, wrapper, ABORT_SIGNAL_STATE_SLOT, state); + } true } @@ -44,13 +50,14 @@ pub(super) struct AbortStore { next_controller_id: u32, signals: HashMap, controllers: HashMap, + detached: bool, } #[derive(Default)] struct AbortSignalState { - signal: Option>, + signal: Option, aborted: bool, - reason: Option>, + detached: bool, abort_algorithms: Vec>, linked_target_listeners: Vec, // None for a source; Some (including empty) for a dependent signal's ordered roots. @@ -68,9 +75,82 @@ struct AbortLinkedTargetListener { struct AbortSignalDispatch { algorithms: Vec>, linked_target_listeners: Vec, + dispatch_event: bool, } impl AbortStore { + pub(super) fn clear_for_context_teardown(&mut self) { + self.detached = true; + for state in self.signals.values_mut() { + state.detached = true; + state.abort_algorithms.clear(); + state.linked_target_listeners.clear(); + } + } + + pub(crate) fn retire_context<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + context: v8::Local<'s, v8::Context>, + ) { + for state in self.signals.values_mut() { + let Some(handle) = state.signal.as_mut() else { + continue; + }; + let Some(signal) = handle.to_local(scope) else { + continue; + }; + if signal.get_creation_context(scope) != Some(context) { + continue; + } + handle.retain_in_realm(scope); + state.detached = true; + state.abort_algorithms.clear(); + state.linked_target_listeners.clear(); + } + } + + fn signal_passive_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, + ) -> Option> { + get_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT) + .and_then(|state| v8::Local::::try_from(state).ok()) + } + + fn signal_aborted_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, + ) -> bool { + Self::signal_passive_state(scope, signal) + .and_then(|state| get_private_value(scope, state, ABORT_SIGNAL_ABORTED_SLOT)) + .is_some_and(|value| value.is_true()) + } + + fn signal_reason_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, + ) -> Option> { + let state = Self::signal_passive_state(scope, signal)?; + get_private_value(scope, state, ABORT_SIGNAL_REASON_SLOT) + } + + fn publish_signal_abort<'s>( + scope: &mut v8::PinScope<'s, '_>, + signal: v8::Local<'s, v8::Object>, + reason: v8::Local<'s, v8::Value>, + ) { + if let Some(state) = Self::signal_passive_state(scope, signal) { + set_private_value( + scope, + state, + ABORT_SIGNAL_ABORTED_SLOT, + v8::Boolean::new(scope, true).into(), + ); + set_private_value(scope, state, ABORT_SIGNAL_REASON_SLOT, reason); + } + } + fn alloc_signal_id(&mut self) -> u32 { self.next_signal_id = self .next_signal_id @@ -102,9 +182,7 @@ impl AbortStore { scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) -> bool { - Self::signal_id_from_object(scope, object) - .and_then(|id| self.signal_state(id)) - .is_some() + Self::signal_id_from_object(scope, object).is_some() } fn controller_id_from_object<'s>( @@ -127,13 +205,25 @@ impl AbortStore { let signal_id = self.alloc_signal_id(); let mut state = AbortSignalState { aborted, + detached: self.detached, ..AbortSignalState::default() }; - state.signal = Some(v8::Global::new(scope, signal)); - if let Some(reason) = reason { - state.reason = Some(v8::Global::new(scope, reason)); - } + state.signal = Some(crate::util::RealmObjectHandle::new(scope, signal)); self.signals.insert(signal_id, state); + let passive_state = v8::Object::new(scope); + set_private_value( + scope, + passive_state, + ABORT_SIGNAL_ABORTED_SLOT, + v8::Boolean::new(scope, aborted).into(), + ); + set_private_value( + scope, + passive_state, + ABORT_SIGNAL_REASON_SLOT, + reason.unwrap_or_else(|| v8::undefined(scope).into()), + ); + set_private_value(scope, signal, ABORT_SIGNAL_STATE_SLOT, passive_state.into()); set_private_value( scope, signal, @@ -182,7 +272,7 @@ impl AbortStore { ) -> Option> { self.signal_state(id) .and_then(|state| state.signal.as_ref()) - .map(|signal| v8::Local::new(scope, signal)) + .and_then(|signal| signal.to_local(scope)) } pub(super) fn signal_aborted<'s>( @@ -190,9 +280,7 @@ impl AbortStore { scope: &mut v8::PinScope<'s, '_>, signal: v8::Local<'s, v8::Object>, ) -> bool { - Self::signal_id_from_object(scope, signal) - .and_then(|id| self.signal_state(id)) - .is_some_and(|state| state.aborted) + Self::signal_aborted_from_object(scope, signal) } pub(super) fn signal_reason<'s>( @@ -200,10 +288,7 @@ impl AbortStore { scope: &mut v8::PinScope<'s, '_>, signal: v8::Local<'s, v8::Object>, ) -> Option> { - Self::signal_id_from_object(scope, signal) - .and_then(|id| self.signal_state(id)) - .and_then(|state| state.reason.as_ref()) - .map(|reason| v8::Local::new(scope, reason)) + Self::signal_reason_from_object(scope, signal) } pub(crate) fn register_abort_algorithm<'s>( @@ -218,6 +303,9 @@ impl AbortStore { let Some(state) = self.signal_state_mut(signal_id) else { return false; }; + if state.detached { + return false; + } state .abort_algorithms .push(v8::Global::new(scope, algorithm)); @@ -283,12 +371,15 @@ impl AbortStore { reason: v8::Local<'s, v8::Value>, ) -> Option)>> { let signal_id = Self::signal_id_from_object(scope, signal)?; + if Self::signal_aborted_from_object(scope, signal) { + return None; + } + Self::publish_signal_abort(scope, signal, reason); let state = self.signal_state_mut(signal_id)?; if state.aborted { return None; } state.aborted = true; - state.reason = Some(v8::Global::new(scope, reason)); let dependent_signals = state.dependent_signals.clone(); // Publish every reason before any author callback can reenter abort. let mut signals = vec![(signal_id, signal)]; @@ -300,9 +391,13 @@ impl AbortStore { continue; } state.aborted = true; - state.reason = Some(v8::Global::new(scope, reason)); - if let Some(signal) = &state.signal { - signals.push((dependent_signal_id, v8::Local::new(scope, signal))); + if let Some(signal) = state + .signal + .as_ref() + .and_then(|signal| signal.to_local(scope)) + { + Self::publish_signal_abort(scope, signal, reason); + signals.push((dependent_signal_id, signal)); } } Some(signals) @@ -313,6 +408,7 @@ impl AbortStore { Some(AbortSignalDispatch { algorithms: std::mem::take(&mut state.abort_algorithms), linked_target_listeners: std::mem::take(&mut state.linked_target_listeners), + dispatch_event: !state.detached, }) } @@ -384,7 +480,9 @@ pub(crate) fn abort_signal<'s>( ); } // Snapshot listeners after earlier signals and abort algorithms. - abort_signal_events::dispatch_abort(scope, signal); + if dispatch.dispatch_event { + abort_signal_events::dispatch_abort(scope, signal); + } } } diff --git a/moli-renderer-v8/src/native_bridge/abort/controller.rs b/moli-renderer-v8/src/native_bridge/abort/controller.rs index ff0e78d888..2bd7c1e271 100644 --- a/moli-renderer-v8/src/native_bridge/abort/controller.rs +++ b/moli-renderer-v8/src/native_bridge/abort/controller.rs @@ -47,12 +47,12 @@ pub(crate) fn abort_controller_abort_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(_) = context_host_ptr_from_global_bridge(scope) else { rv.set_undefined(); return; }; let controller = args.this(); - let Some(controller_id) = AbortStore::controller_id_from_object(scope, controller) else { + let Some(_) = AbortStore::controller_id_from_object(scope, controller) else { rv.set_undefined(); return; }; @@ -67,26 +67,6 @@ pub(crate) fn abort_controller_abort_callback<'s>( } else { abort_error_value(scope) }; - let host = unsafe { &mut *host_ptr }; - let Some(signal_id) = host - .native_bridge_mut() - .abort - .controllers - .get(&controller_id) - .copied() - else { - rv.set_undefined(); - return; - }; - if host - .native_bridge_mut() - .abort - .signal_state(signal_id) - .is_some_and(|state| state.aborted) - { - rv.set_undefined(); - return; - } crate::native_bridge::abort::abort_signal(scope, signal, reason); rv.set_undefined(); } diff --git a/moli-renderer-v8/src/native_bridge/abort/signal.rs b/moli-renderer-v8/src/native_bridge/abort/signal.rs index ba43412ba6..dd7fdf6b07 100644 --- a/moli-renderer-v8/src/native_bridge/abort/signal.rs +++ b/moli-renderer-v8/src/native_bridge/abort/signal.rs @@ -1,31 +1,11 @@ use super::AbortStore; -use crate::util::context_host_ptr_from_global_bridge; pub(crate) fn abort_signal_aborted_getter_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_bool(false); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_bool(false); - return; - } - // SAFETY: as_ptr() — this getter may be called during event dispatch - // (re-entrant from another callback holding borrow_mut). See util.rs. - let aborted = AbortStore::signal_id_from_object(scope, signal) - .and_then(|id| { - unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state(id) - }) - .is_some_and(|state| state.aborted); - rv.set_bool(aborted); + rv.set_bool(AbortStore::signal_aborted_from_object(scope, args.this())); } pub(crate) fn abort_signal_reason_getter_callback<'s>( @@ -33,29 +13,10 @@ pub(crate) fn abort_signal_reason_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; - let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { - rv.set_undefined(); - return; + match AbortStore::signal_reason_from_object(scope, args.this()) { + Some(reason) => rv.set(reason), + None => rv.set_undefined(), } - let Some(reason) = AbortStore::signal_id_from_object(scope, signal) - .and_then(|id| { - unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state(id) - }) - .and_then(|state| state.reason.as_ref()) - .map(|reason| v8::Local::new(scope, reason)) - else { - rv.set_undefined(); - return; - }; - rv.set(reason); } pub(crate) fn abort_signal_throw_if_aborted_callback<'s>( @@ -63,30 +24,12 @@ pub(crate) fn abort_signal_throw_if_aborted_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { - rv.set_undefined(); - return; - }; let signal = args.this(); - if AbortStore::signal_id_from_object(scope, signal).is_none() { + if !AbortStore::signal_aborted_from_object(scope, signal) { rv.set_undefined(); return; } - let Some(reason) = AbortStore::signal_id_from_object(scope, signal) - .and_then(|id| { - unsafe { &mut *host_ptr } - .native_bridge_mut() - .abort - .signal_state(id) - }) - .filter(|state| state.aborted) - .and_then(|state| state.reason.as_ref()) - .map(|reason| v8::Local::new(scope, reason)) - else { - rv.set_undefined(); - return; - }; - // DOM requires throwing the stored abort reason itself. In particular, a - // string reason remains a string rather than being wrapped in `Error`. + let reason = AbortStore::signal_reason_from_object(scope, signal) + .unwrap_or_else(|| v8::undefined(scope).into()); scope.throw_exception(reason); } diff --git a/moli-renderer-v8/src/native_bridge/abort/statics.rs b/moli-renderer-v8/src/native_bridge/abort/statics.rs index 3b8512b1bb..5d196cbc73 100644 --- a/moli-renderer-v8/src/native_bridge/abort/statics.rs +++ b/moli-renderer-v8/src/native_bridge/abort/statics.rs @@ -91,20 +91,11 @@ pub(crate) fn abort_signal_any_callback<'s>( let signals = parsed.signals; for source_signal in &signals { - let Some(source_signal_id) = AbortStore::signal_id_from_object(scope, *source_signal) - else { + if !AbortStore::signal_aborted_from_object(scope, *source_signal) { continue; - }; - let Some(reason) = host - .native_bridge_mut() - .abort - .signal_state(source_signal_id) - .filter(|state| state.aborted) - .and_then(|state| state.reason.as_ref()) - .map(|reason| v8::Local::new(scope, reason)) - else { - continue; - }; + } + let reason = AbortStore::signal_reason_from_object(scope, *source_signal) + .unwrap_or_else(|| v8::undefined(scope).into()); crate::native_bridge::abort::abort_signal(scope, signal, reason); rv.set(signal.into()); return; diff --git a/moli-renderer-v8/src/native_bridge/context_host/bridge_install.rs b/moli-renderer-v8/src/native_bridge/context_host/bridge_install.rs index 2d8d83124d..d010aa887f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/bridge_install.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/bridge_install.rs @@ -81,7 +81,11 @@ impl JsContextHost { // The actual JsContextHost pointer for runtime_ptr_from_object compatibility. let bridge = unsafe { &mut (*host_ptr).bridge }; bridge.install_global(scope, global, host_ptr, bridge_ref.as_external_ptr())?; - crate::util::install_context_host_pointer_slot(scope.get_current_context(), host_ptr); + crate::util::install_context_host_pointer_slot( + scope.get_current_context(), + host_ptr, + unsafe { &*host_ptr }.context_host_lifecycle_handle(), + ); Ok(bridge_ref) } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/broadcast_channels.rs b/moli-renderer-v8/src/native_bridge/context_host/broadcast_channels.rs index 5bdf453a28..f4c83db268 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/broadcast_channels.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/broadcast_channels.rs @@ -59,6 +59,26 @@ impl JsContextHost { closed_count } + pub(crate) fn close_broadcast_channels_for_context_token( + &mut self, + context_token: RuntimeObservableContextToken, + ) -> usize { + let channel_ids = self + .broadcast_channel_wrappers + .iter() + .filter_map(|(channel_id, entry)| { + (entry.identity.realm_token() == context_token).then_some(*channel_id) + }) + .collect::>(); + let closed_count = channel_ids.len(); + for channel_id in channel_ids { + self.broadcast_channel_wrappers.remove(&channel_id); + self.broadcast_channel_registry + .close_broadcast_channel(channel_id); + } + closed_count + } + pub(crate) fn register_broadcast_channel_wrapper( &mut self, scope: &mut v8::PinScope<'_, '_>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index b7b965a35c..9028c98c53 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -962,7 +962,11 @@ impl JsContextHost { else { return false; }; - crate::util::install_context_host_pointer_slot(context, self as *mut Self); + crate::util::install_context_host_pointer_slot( + context, + self as *mut Self, + self.context_host_lifecycle_handle(), + ); let previous = context.set_slot(Rc::new(ChildWindowProxyFacadeContextHandle(handle))); debug_assert!(previous.is_none()); // Keep V8's unique default security token. A facade is not a real diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 4107af34a0..42923e4a19 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -212,6 +212,10 @@ impl JsContextHost { root_document_lifecycle: None, output_journal: None, page_context_resources_closed: false, + context_host_lifecycle: Rc::new(Cell::new(crate::util::ContextHostLifecycle::Active)), + deferred_context_host_release_queue: Default::default(), + retained_document_runtime: None, + published_document_host: false, page_default_context: None, v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry::default(), bridge: NativeDomBridge::new(bindings), diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index fb4105ec19..2f404a8d8f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -820,6 +820,8 @@ pub(crate) struct JsContextHost { root_document_lifecycle: Option, output_journal: Option, page_context_resources_closed: bool, + context_host_lifecycle: Rc>, + deferred_context_host_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue, page_default_context: Option>, pub(crate) v8_finalizers: crate::v8_finalizer::V8FinalizerRegistry, pub(super) bridge: NativeDomBridge, @@ -1099,6 +1101,8 @@ pub(crate) struct JsContextHost { stylo_computed_style_property_reads: Cell, #[cfg(test)] style_observation_environment_resolutions: Cell, + retained_document_runtime: Option>, + published_document_host: bool, } struct PendingTextControlChangeCommit { @@ -1253,16 +1257,66 @@ impl JsContextHost { self.app_manifest_link_change_epoch } + pub(crate) fn document_host_is_published(&self) -> bool { + self.published_document_host + } + + pub(crate) fn publish_document_host(&mut self) { + self.published_document_host = true; + } + + pub(crate) fn mark_page_context_detached(&self) { + if self.context_host_lifecycle.get() == crate::util::ContextHostLifecycle::Active { + self.context_host_lifecycle + .set(crate::util::ContextHostLifecycle::Detached); + } + } + + pub(crate) fn context_host_lifecycle_handle( + &self, + ) -> Rc> { + self.context_host_lifecycle.clone() + } + + pub(crate) fn deferred_context_host_release_queue( + &self, + ) -> crate::script_vm::RendererDeferredContextHostReleaseQueue { + self.deferred_context_host_release_queue.clone() + } + + pub(crate) fn adopt_retained_document_runtime( + &mut self, + document_runtime: Box, + ) { + assert!( + self.retained_document_runtime.is_none(), + "JsContextHost must adopt its DocumentRuntime at most once" + ); + assert!( + std::ptr::eq(self.runtime.cast_const(), document_runtime.as_ref()), + "retained DocumentRuntime transfer must preserve the host raw pointer" + ); + self.retained_document_runtime = Some(document_runtime); + } + pub(crate) fn bind_deferred_context_host_release_queue( + &mut self, + queue: crate::script_vm::RendererDeferredContextHostReleaseQueue, + ) { + self.deferred_context_host_release_queue = queue; + } + pub(crate) fn close_page_context_resources_for_teardown(&mut self) { + self.mark_page_context_detached(); if self.page_context_resources_closed { return; } self.page_context_resources_closed = true; self.bridge.web_mcp.clear(); + self.retire_all_window_execution_context_resources_for_teardown(); self.pending_history_traversal_admissions.clear(); self.retire_all_document_resource_loaders(); self.page_default_context = None; - self.v8_finalizers.clear_for_context_teardown(); + self.clear_pending_top_level_navigation(); self.unregister_all_service_worker_child_clients(); self.unregister_all_service_worker_popup_clients(); @@ -1272,11 +1326,20 @@ impl JsContextHost { self.close_owned_broadcast_channels(); self.close_owned_message_ports(); self.shutdown_workers(); + self.output_journal = None; + self.root_document_lifecycle = None; + self.child_default_context_bootstrap = None; + self.command_turn_output = None; + self.lightweight_popup_browsing_contexts.clear(); + self.internal_inspector_value_references.clear(); } } impl Drop for JsContextHost { fn drop(&mut self) { self.close_page_context_resources_for_teardown(); + self.context_host_lifecycle + .set(crate::util::ContextHostLifecycle::Destroyed); + self.v8_finalizers.clear_for_context_teardown(); } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs index 477989ba45..849412d49f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs @@ -100,6 +100,103 @@ impl PendingRuntimeObservableConsoleSourceEvent { } impl JsContextHost { + pub(crate) fn retire_all_window_execution_context_resources_for_teardown(&mut self) { + let owners = self + .window_execution_contexts + .keys() + .copied() + .collect::>(); + let mut realm_tokens = self + .window_execution_contexts + .values() + .map(WindowExecutionContextBinding::realm_token) + .chain( + self.window_execution_context_realms + .concrete_by_token + .keys() + .copied(), + ) + .collect::>(); + realm_tokens.sort_unstable(); + realm_tokens.dedup(); + + for realm_token in realm_tokens { + self.cancel_timers_for_context_token(realm_token); + self.retire_runtime_binding_context_token(realm_token); + self.retire_image_decode_requests_for_context_token(realm_token); + self.retire_webcrypto_context_token(realm_token); + self.retire_opfs_context_token(realm_token); + self.retire_workers_for_context_token(realm_token); + self.disconnect_shared_worker_clients_for_context_token(realm_token); + self.retire_window_xhrs_for_context_token(realm_token); + self.retire_window_fetches_for_context_token(realm_token); + self.retire_window_event_sources_for_context_token(realm_token); + self.retire_message_ports_for_context_token(realm_token); + self.retire_window_messages_for_context_token(realm_token); + self.close_broadcast_channels_for_context_token(realm_token); + self.retire_websockets_for_context_token(realm_token); + self.retire_window_execution_contexts_for_context_token(realm_token); + } + + for owner in owners { + self.cancel_window_execution_context_timers(owner); + self.retire_webcrypto_execution_context_owner(owner); + self.retire_opfs_execution_context_owner(owner); + self.retire_workers_for_execution_context_owner(owner); + self.disconnect_shared_worker_clients_for_execution_context_owner(owner); + self.retire_window_xhrs_for_execution_context_owner(owner); + self.retire_window_fetches_for_execution_context_owner(owner); + self.retire_window_event_sources_for_execution_context_owner(owner); + self.retire_window_messages_for_execution_context_owner(owner); + self.close_broadcast_channels_for_execution_context_owner(owner); + self.retire_websockets_for_execution_context_owner(owner); + self.retire_image_decode_requests_for_execution_context_owner(owner); + self.retire_message_ports_for_execution_context_owner(owner); + self.retire_window_execution_context(owner); + } + + self.retire_v8_execution_state_for_context_teardown(); + + // A detached Document realm may keep this host and its native DOM + // alive through the V8 Context slot. None of the host's active + // execution registries may in turn keep that Context alive with a + // strong Global handle: that would form an untraceable + // Context -> Rust host -> Global -> Context cycle. Chromium retires + // these LocalDOMWindow/ExecutionContext-owned services when the frame + // is detached while ordinary retained Document/Node values remain + // usable. + drop(std::mem::take(&mut self.custom_elements)); + self.child_custom_elements.clear(); + self.scoped_custom_elements.clear(); + drop(std::mem::take(&mut self.custom_element_reactions)); + drop(std::mem::take(&mut self.observers)); + drop(std::mem::take(&mut self.child_window_proxy_records)); + self.pending_service_worker_registers.clear(); + self.pending_service_worker_unregisters.clear(); + self.pending_service_worker_ready.clear(); + self.service_worker_registration_watchers.clear(); + self.pending_window_messages.clear(); + drop(std::mem::take(&mut self.directory_reader_callbacks)); + drop(std::mem::take(&mut self.misc_platform_api_tasks)); + drop(std::mem::take(&mut self.file_entry_file_callbacks)); + drop(std::mem::take(&mut self.user_interaction_tasks)); + self.pending_image_load_events.clear(); + self.pending_media_load_sequences.clear(); + self.pending_text_track_load_sequences.clear(); + self.pending_media_text_track_gates.clear(); + self.resource_timing_buffers = + super::resource_timing::SharedResourceTimingBufferRegistry::new(); + drop(std::mem::take(&mut self.history_queue)); + drop(std::mem::take(&mut self.rendering_updates)); + drop(std::mem::take(&mut self.view_transition_updates)); + drop(std::mem::take(&mut self.media_element_events)); + drop(std::mem::take(&mut self.element_toggle_events)); + drop(std::mem::take(&mut self.text_track_default_modes)); + self.child_window_event_listeners.clear(); + drop(std::mem::take(&mut self.event_callbacks)); + self.bridge.abort.clear_for_context_teardown(); + } + pub(crate) fn register_lightweight_popup_execution_context( &mut self, scope: &mut v8::PinScope<'_, '_>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/websockets/commands.rs b/moli-renderer-v8/src/native_bridge/context_host/websockets/commands.rs index d86a3135c7..b06d7e4124 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/websockets/commands.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/websockets/commands.rs @@ -127,6 +127,24 @@ impl JsContextHost { retired } + pub(crate) fn retire_websockets_for_context_token( + &mut self, + context_token: super::super::RuntimeObservableContextToken, + ) -> usize { + let socket_ids = self + .websockets + .iter() + .filter_map(|(socket_id, state)| { + (state.owner.realm_token() == context_token).then_some(*socket_id) + }) + .collect::>(); + let retired = socket_ids.len(); + for socket_id in socket_ids { + self.retire_websocket(socket_id); + } + retired + } + pub(crate) fn retire_websocket(&mut self, socket_id: u64) -> bool { let Some(state) = self.websockets.remove(&socket_id) else { return false; diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index f0d4e17ba5..8f0780e63c 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -448,14 +448,14 @@ pub(crate) fn contexts_share_wrapper_world( #[derive(Debug)] struct BridgeCachedWrapper { - wrapper: v8::Global, + wrapper: crate::util::RealmObjectHandle, creation_realm: Option, } impl BridgeCachedWrapper { fn new(scope: &mut v8::PinScope<'_, '_>, wrapper: v8::Local<'_, v8::Object>) -> Self { Self { - wrapper: v8::Global::new(scope, wrapper), + wrapper: crate::util::RealmObjectHandle::new(scope, wrapper), creation_realm: scope .get_current_context() .get_slot::() @@ -468,7 +468,7 @@ impl BridgeCachedWrapper { #[derive(Debug)] struct BridgeContextWindowWrapper { - wrapper: v8::Global, + wrapper: RefCell, } fn context_window_wrapper<'s>( @@ -477,7 +477,7 @@ fn context_window_wrapper<'s>( scope .get_current_context() .get_slot::() - .map(|entry| v8::Local::new(scope, &entry.wrapper)) + .and_then(|entry| entry.wrapper.borrow().to_local(scope)) } fn set_context_window_wrapper( @@ -487,7 +487,7 @@ fn set_context_window_wrapper( let _ = scope .get_current_context() .set_slot(Rc::new(BridgeContextWindowWrapper { - wrapper: v8::Global::new(scope, wrapper), + wrapper: RefCell::new(crate::util::RealmObjectHandle::new(scope, wrapper)), })); } @@ -514,7 +514,16 @@ impl BridgeContextWrapperCacheRetainForTest { self.cache.borrow().wrappers.len() } - pub(crate) fn wrapper_entry_count_for_realm( + pub(crate) fn strong_wrapper_entry_count(&self) -> usize { + self.cache + .borrow() + .wrappers + .values() + .filter(|entry| entry.wrapper.is_strong()) + .count() + } + + pub(crate) fn strong_wrapper_entry_count_for_realm( &self, realm_token: RuntimeObservableContextToken, ) -> usize { @@ -522,7 +531,10 @@ impl BridgeContextWrapperCacheRetainForTest { .borrow() .wrappers .values() - .filter(|entry| entry.creation_realm.map(NonZeroU64::get) == Some(realm_token.as_u64())) + .filter(|entry| { + entry.wrapper.is_strong() + && entry.creation_realm.map(NonZeroU64::get) == Some(realm_token.as_u64()) + }) .count() } } @@ -541,18 +553,41 @@ pub(crate) fn clear_context_wrapper_cache_for_teardown( include_shared_default_world: bool, ) { let context = scope.get_current_context(); - if !include_shared_default_world - && context + crate::util::detach_document_page_context(context); + if let Some(host) = crate::util::context_host_ptr_from_context_slot(context) { + unsafe { &mut *host } + .native_bridge_mut() + .abort + .retire_context(scope, context); + } + let all_entries = include_shared_default_world + || context .get_slot::() - .is_some() - { - return; - } + .is_none(); + let realm = context + .get_slot::() + .as_deref() + .map(|token| token.as_u64()); if let Some(cache) = context.get_slot::>() { - let mut cache = cache.borrow_mut(); - cache.wrappers.clear(); - cache.live_collection_wrappers.clear(); + let mut entries = cache.borrow_mut(); + for entry in entries.wrappers.values_mut() { + if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { + entry.wrapper.retain_in_realm(scope); + } + } + for entry in entries.live_collection_wrappers.values_mut() { + if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { + entry.wrapper.retain_in_realm(scope); + } + } + drop(entries); + crate::util::retain_context_v8_handle_state_for_safe_release(context, cache); } + if let Some(wrapper) = context.get_slot::() { + wrapper.wrapper.borrow_mut().retain_in_realm(scope); + crate::util::retain_context_v8_handle_state_for_safe_release(context, wrapper); + } + crate::context_bootstrap::exposed_interfaces::retain_intrinsic_interfaces_in_realm(scope); } #[derive(Debug, Default)] @@ -603,7 +638,7 @@ impl BridgeIdentityStore { .borrow() .wrappers .get(&reflector_id) - .map(|entry| v8::Local::new(scope, &entry.wrapper)) + .and_then(|entry| entry.wrapper.to_local(scope)) } pub(super) fn cache_wrapper( @@ -642,7 +677,7 @@ impl BridgeIdentityStore { .borrow() .live_collection_wrappers .get(descriptor) - .map(|entry| v8::Local::new(scope, &entry.wrapper)) + .and_then(|entry| entry.wrapper.to_local(scope)) } pub(super) fn cache_live_collection_wrapper( @@ -664,9 +699,11 @@ impl BridgeIdentityStore { let mut cache = self.default_world_wrapper_cache.borrow_mut(); cache.wrappers.retain(|_, entry| { entry.creation_realm.map(NonZeroU64::get) != Some(realm_token.as_u64()) + || !entry.wrapper.is_strong() }); cache.live_collection_wrappers.retain(|_, entry| { entry.creation_realm.map(NonZeroU64::get) != Some(realm_token.as_u64()) + || !entry.wrapper.is_strong() }); } @@ -749,9 +786,9 @@ mod tests { } #[test] - fn cached_wrapper_entry_uses_the_global_handle_niche() { + fn cached_wrapper_entry_bounds_the_handle_and_realm_metadata() { #[cfg(target_pointer_width = "64")] - assert_eq!(size_of::(), 24); + assert_eq!(size_of::(), 32); } #[test] diff --git a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs index a9aaf28a94..ad867a9b03 100644 --- a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs +++ b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs @@ -62,6 +62,7 @@ impl DenseReflectorMap { self.sparse.insert(id, value) } + #[cfg(test)] pub(super) fn clear(&mut self) { self.dense_base = None; self.dense.clear(); @@ -112,6 +113,13 @@ impl DenseReflectorMap { pub(super) fn values(&self) -> impl Iterator { self.dense.iter().flatten().chain(self.sparse.values()) } + + pub(super) fn values_mut(&mut self) -> impl Iterator { + self.dense + .iter_mut() + .flatten() + .chain(self.sparse.values_mut()) + } } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/traversal/filters.rs b/moli-renderer-v8/src/native_bridge/traversal/filters.rs index 1b419fe3b6..d2ad4fe4be 100644 --- a/moli-renderer-v8/src/native_bridge/traversal/filters.rs +++ b/moli-renderer-v8/src/native_bridge/traversal/filters.rs @@ -1,3 +1,4 @@ +use crate::v8_traced_webidl_callback::V8TracedWebIdlCallbackInterface; use crate::webidl::{PreparedWebIdlCallbackInterface, WebIdlCallbackInterface}; use crate::{ callback_invocation::{ @@ -28,37 +29,44 @@ pub(super) enum TraversalFilterResult { /// The callback-interface value owned by one TreeWalker or NodeIterator. /// -/// Traversal owns registration lifetime and reentrancy. The callback kernel -/// owns the rooted callback plus its relevant/incumbent contexts. The renderer +/// The wrapper owns a V8-traced callback carrier; the native registry only +/// observes it weakly. The callback kernel roots invocation snapshots. The renderer /// identity is deliberately kept beside those engine-neutral facts so a /// detached or replaced Window cannot re-enter through a still-reachable /// traversal wrapper. pub(in crate::native_bridge) struct TraversalFilter { - callback: WebIdlCallbackInterface, + callback: v8::Weak, execution_context: Option, } impl TraversalFilter { - pub(super) fn new( - scope: &mut v8::PinScope<'_, '_>, + pub(super) fn new<'s>( + scope: &mut v8::PinScope<'s, '_>, runtime_ptr: *mut JsContextHost, + wrapper: v8::Local<'s, v8::Object>, callback: WebIdlCallbackInterface, ) -> Self { let prepared = callback.prepare(scope); let relevant_context = prepared.relevant_context(scope); let execution_context = unsafe { &*runtime_ptr } .window_execution_context_identity_for_v8_context(scope, relevant_context); + let carrier = V8TracedWebIdlCallbackInterface::new(scope, callback).into_object(); + crate::util::set_private_value(scope, wrapper, "__lmTraversalFilter", carrier.into()); Self { - callback, + callback: v8::Weak::new(scope, carrier), execution_context, } } - pub(super) fn prepare(&self, scope: &mut v8::PinScope<'_, '_>) -> PreparedTraversalFilter { - PreparedTraversalFilter { - callback: self.callback.prepare(scope), + pub(super) fn prepare( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Option { + let carrier = self.callback.to_local(scope)?; + Some(PreparedTraversalFilter { + callback: V8TracedWebIdlCallbackInterface::from_object(carrier).prepare(scope), execution_context: self.execution_context, - } + }) } } diff --git a/moli-renderer-v8/src/native_bridge/traversal/state.rs b/moli-renderer-v8/src/native_bridge/traversal/state.rs index f19172fc17..a1955986d7 100644 --- a/moli-renderer-v8/src/native_bridge/traversal/state.rs +++ b/moli-renderer-v8/src/native_bridge/traversal/state.rs @@ -219,7 +219,7 @@ fn prepare_filter( filter: Option<&TraversalFilter>, ) -> Option> { let filter = filter?; - Some(Rc::new(filter.prepare(scope))) + Some(Rc::new(filter.prepare(scope)?)) } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/traversal/wrappers.rs b/moli-renderer-v8/src/native_bridge/traversal/wrappers.rs index 9ed6703daa..9b9b342239 100644 --- a/moli-renderer-v8/src/native_bridge/traversal/wrappers.rs +++ b/moli-renderer-v8/src/native_bridge/traversal/wrappers.rs @@ -115,12 +115,12 @@ pub(in crate::native_bridge) fn build_node_iterator_wrapper<'s>( what_to_show: u32, filter: Option, ) -> v8::Local<'s, v8::Object> { - let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, filter)); - let id = bridge.register_node_iterator(root, what_to_show, filter); let template = bridge.node_iterator_wrapper_template(); let wrapper = template .new_instance(scope) .expect("failed to instantiate NodeIterator wrapper"); + let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, wrapper, filter)); + let id = bridge.register_node_iterator(root, what_to_show, filter); let runtime_external = v8::External::new(scope, runtime_ptr as *mut c_void); assert!( wrapper.set_internal_field(0, runtime_external.into()), @@ -142,12 +142,12 @@ pub(in crate::native_bridge) fn build_tree_walker_wrapper<'s>( what_to_show: u32, filter: Option, ) -> v8::Local<'s, v8::Object> { - let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, filter)); - let id = bridge.register_tree_walker(root, what_to_show, filter); let template = bridge.tree_walker_wrapper_template(); let wrapper = template .new_instance(scope) .expect("failed to instantiate TreeWalker wrapper"); + let filter = filter.map(|filter| TraversalFilter::new(scope, runtime_ptr, wrapper, filter)); + let id = bridge.register_tree_walker(root, what_to_show, filter); let runtime_external = v8::External::new(scope, runtime_ptr as *mut c_void); assert!( wrapper.set_internal_field(0, runtime_external.into()), diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 42a27860b1..c377ea2952 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -891,9 +891,10 @@ pub(crate) use standalone_test_harness::StandaloneScriptVmHarness; use crate::document_runtime::{DeferredPageTaskLane, FollowupPageTaskDisposition}; use document_isolate::*; pub(crate) use document_isolate::{ - RendererDocumentIsolateBootstrap, RendererDocumentIsolateHandle, - RendererDocumentIsolateReservationAccounting, RendererPageScriptEnvironment, - ScriptVmDefaultWorldBootstrap, renderer_document_isolate_accounting_diagnostics, + RendererDeferredContextHostReleaseQueue, RendererDocumentIsolateBootstrap, + RendererDocumentIsolateHandle, RendererDocumentIsolateReservationAccounting, + RendererPageScriptEnvironment, ScriptVmDefaultWorldBootstrap, + renderer_document_isolate_accounting_diagnostics, }; pub(crate) use eval_exec::execute_source_text_on_current_stack; pub(crate) use input_helpers::*; @@ -907,6 +908,7 @@ pub(crate) use runtime_bindings::PromiseRejectDispatchSlot; pub(crate) use runtime_bindings::perform_microtask_checkpoint_and_report_pending_promise_rejections; use runtime_bindings::*; pub(crate) use runtime_work::*; +use std::ops::{Deref, DerefMut}; #[cfg(any(test, feature = "test-support"))] type ScriptGlobalsBaseline = Vec; @@ -950,15 +952,46 @@ fn register_main_window_execution_context_for_bootstrap( .context("failed to register main LocalWindow execution context") } +pub(super) struct ScriptVmDocumentRuntimeOwner { + document_runtime: Option>, +} + +impl ScriptVmDocumentRuntimeOwner { + fn new(document_runtime: Box) -> Self { + Self { + document_runtime: Some(document_runtime), + } + } + + fn take_for_retained_document_host(&mut self) -> Option> { + self.document_runtime.take() + } +} + +impl Deref for ScriptVmDocumentRuntimeOwner { + type Target = DocumentRuntime; + + fn deref(&self) -> &Self::Target { + self.document_runtime + .as_deref() + .expect("ScriptVm DocumentRuntime must remain owned until ScriptVm drop") + } +} + +impl DerefMut for ScriptVmDocumentRuntimeOwner { + fn deref_mut(&mut self) -> &mut Self::Target { + self.document_runtime + .as_deref_mut() + .expect("ScriptVm DocumentRuntime must remain owned until ScriptVm drop") + } +} + pub(super) struct ScriptVm { resource_owner_id: crate::resource_owner::ResourceOwnerId, /// Page/target-facing inspector state. This must drop before the renderer /// document isolate handle because the V8 inspector session touches the /// isolate-level backend while being destroyed. page_inspector: DocumentInspectorBinding, - /// Handle to renderer-owner document isolate-level V8 state. Multiple page - /// facades can share the holder while keeping page/context state here. - renderer_document_isolate: RendererDocumentIsolateHandle, renderer_document_isolate_teardown: RendererDocumentIsolateTeardown, renderer_page_script_environment: Option, page_default_context: v8::Global, @@ -973,7 +1006,7 @@ pub(super) struct ScriptVm { // `JsContextHost` stores a non-owning pointer into `document_runtime`, so it // must be dropped before the runtime field during normal Rust field teardown. _context_host: Rc>, - pub(super) document_runtime: Box, + pub(super) document_runtime: ScriptVmDocumentRuntimeOwner, post_domcontentloaded_page_task_tx: PageTaskSender, page_runtime_wake_tx: PageRuntimeWakeSender, queued_main_document_runtime_continuation_owner: @@ -1014,6 +1047,9 @@ pub(super) struct ScriptVm { #[cfg(test)] _page_task_residence_for_executor_test: Option, + // Native and V8 per-document state must drop before the final isolate + // handle; retained realms then release their host through its GC queue. + renderer_document_isolate: RendererDocumentIsolateHandle, } impl moli_layout::GeometryProvider for ScriptVm { @@ -2010,6 +2046,11 @@ impl ScriptVmPageRealmBootstrap { top_level_storage_key, reserved_service_worker_client_id, ))); + context_host + .borrow_mut() + .bind_deferred_context_host_release_queue( + renderer_document_isolate.deferred_context_host_release_queue(), + ); let main_document_owner = context_host .borrow() .current_main_document_task_owner() @@ -2383,7 +2424,7 @@ impl ScriptVmDefaultWorldBootstrap { page_default_runtime_observable_context_token: runtime_observable_context_token, root_frame_id, baseline_globals, - document_runtime, + document_runtime: ScriptVmDocumentRuntimeOwner::new(document_runtime), _context_host: context_host, page_context_cancel_tx, post_domcontentloaded_page_task_tx, @@ -2426,6 +2467,26 @@ impl ScriptVmDefaultWorldBootstrap { .borrow_mut() .bind_output_journal(environment.output_journal()); } + vm._context_host.borrow_mut().publish_document_host(); + vm.renderer_document_isolate + .with_renderer_document_isolate_mut(|isolate| { + let scope = pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + crate::util::retain_context_host_for_document_realm( + v8::Local::new(scope, &vm.page_default_context), + vm._context_host.clone(), + vm.renderer_document_isolate + .deferred_context_host_release_queue(), + ); + for child in vm.prebootstrapped_child_default_contexts.borrow().values() { + crate::util::retain_context_host_for_document_realm( + v8::Local::new(scope, &child.context), + vm._context_host.clone(), + vm.renderer_document_isolate + .deferred_context_host_release_queue(), + ); + } + }); Ok(vm) } } diff --git a/moli-renderer-v8/src/script_vm/document_isolate.rs b/moli-renderer-v8/src/script_vm/document_isolate.rs index a42c214980..0c8c3fc6d1 100644 --- a/moli-renderer-v8/src/script_vm/document_isolate.rs +++ b/moli-renderer-v8/src/script_vm/document_isolate.rs @@ -34,7 +34,7 @@ use crate::{ }; use anyhow::{Result, anyhow}; use std::{ - cell::{OnceCell, RefCell}, + cell::{Cell, OnceCell, RefCell}, rc::Rc, sync::atomic::{AtomicU64, Ordering}, }; @@ -44,6 +44,67 @@ static DOCUMENT_ISOLATE_DESTROYED_COUNT: AtomicU64 = AtomicU64::new(0); static DOCUMENT_ISOLATE_LIVE_COUNT: AtomicU64 = AtomicU64::new(0); static DOCUMENT_ISOLATE_RESERVED_COUNT: AtomicU64 = AtomicU64::new(0); +#[derive(Clone, Debug, Default)] +pub(crate) struct RendererDeferredContextHostReleaseQueue { + inner: Rc, +} + +struct RendererDeferredContextHostRelease { + _host: Rc>, + retained_v8_handle_state: Vec>, +} + +impl std::fmt::Debug for RendererDeferredContextHostRelease { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("RendererDeferredContextHostRelease") + .field( + "retained_v8_handle_state_count", + &self.retained_v8_handle_state.len(), + ) + .finish_non_exhaustive() + } +} + +#[derive(Debug, Default)] +struct RendererDeferredContextHostReleaseQueueInner { + pending: RefCell>, + isolate_shutting_down: Cell, +} + +impl RendererDeferredContextHostReleaseQueue { + pub(crate) fn defer( + &self, + host: Rc>, + retained_v8_handle_state: Vec>, + ) { + let release = RendererDeferredContextHostRelease { + _host: host, + retained_v8_handle_state, + }; + if self.inner.isolate_shutting_down.get() { + drop(release); + return; + } + self.inner.pending.borrow_mut().push(release); + } + + fn drain_on_entered_isolate(&self) { + loop { + let pending = std::mem::take(&mut *self.inner.pending.borrow_mut()); + if pending.is_empty() { + return; + } + drop(pending); + } + } + + fn begin_isolate_shutdown(&self) { + self.drain_on_entered_isolate(); + self.inner.isolate_shutting_down.set(true); + } +} + pub(crate) fn renderer_document_isolate_accounting_diagnostics() -> crate::runtime::RendererDocumentIsolateAccountingDiagnostics { crate::runtime::RendererDocumentIsolateAccountingDiagnostics { @@ -93,11 +154,8 @@ pub(super) struct ScriptVmPageRealmBootstrap { pub(super) resource_owner_id: ResourceOwnerId, pub(super) promise_reject_dispatch: PromiseRejectDispatchSlot, pub(super) page_inspector: DocumentInspectorBinding, - pub(super) renderer_document_isolate: RendererDocumentIsolateHandle, pub(super) renderer_document_isolate_teardown: RendererDocumentIsolateTeardown, - pub(super) document_runtime: Box, pub(super) root_frame_id: Option, - pub(super) context_host: Rc>, pub(super) prebootstrapped_child_default_contexts: SharedPrebootstrappedChildDefaultContexts, pub(super) page_context_cancel_tx: RendererPageContextCancelSender, pub(super) post_domcontentloaded_page_task_tx: PageTaskSender, @@ -105,6 +163,12 @@ pub(super) struct ScriptVmPageRealmBootstrap { pub(super) storage_bucket_store: crate::context_bootstrap::SharedStorageBucketStore, pub(super) renderer_page_script_environment: Option, pub(super) reuse_main_window_proxy: bool, + // Partial bootstrap can drop before publication. Release bridge owners + // before their native backing, and keep V8 alive until all of it + // has been released. + pub(super) context_host: Rc>, + pub(super) document_runtime: Box, + pub(super) renderer_document_isolate: RendererDocumentIsolateHandle, } pub(super) struct ScriptVmContextBootstrap { @@ -269,21 +333,24 @@ pub(crate) struct ScriptVmDefaultWorldBootstrap { pub(super) resource_owner_id: ResourceOwnerId, pub(super) promise_reject_dispatch: PromiseRejectDispatchSlot, pub(super) page_inspector: DocumentInspectorBinding, - pub(super) renderer_document_isolate: RendererDocumentIsolateHandle, pub(super) renderer_document_isolate_teardown: RendererDocumentIsolateTeardown, pub(super) renderer_page_script_environment: Option, pub(super) page_default_context: v8::Global, pub(super) bridge_ref: JsContextHostBridgeRef, pub(super) runtime_observable_context_token: RuntimeObservableContextToken, pub(super) baseline_globals: super::ScriptGlobalsBaseline, - pub(super) document_runtime: Box, pub(super) root_frame_id: Option, - pub(super) context_host: Rc>, pub(super) prebootstrapped_child_default_contexts: SharedPrebootstrappedChildDefaultContexts, pub(super) page_context_cancel_tx: RendererPageContextCancelSender, pub(super) post_domcontentloaded_page_task_tx: PageTaskSender, pub(super) page_runtime_wake_tx: PageRuntimeWakeSender, pub(super) storage_bucket_store: crate::context_bootstrap::SharedStorageBucketStore, + // Partial bootstrap can drop before publication. Release bridge owners + // before their native backing, and keep V8 alive until all of it + // has been released. + pub(super) context_host: Rc>, + pub(super) document_runtime: Box, + pub(super) renderer_document_isolate: RendererDocumentIsolateHandle, } #[derive(Clone, Copy, Debug)] @@ -342,6 +409,7 @@ impl RendererDocumentIsolateTeardown { #[derive(Clone)] pub(crate) struct RendererDocumentIsolateHandle { inner: Rc>, + deferred_context_host_releases: RendererDeferredContextHostReleaseQueue, } impl std::fmt::Debug for RendererDocumentIsolateHandle { @@ -352,6 +420,12 @@ impl std::fmt::Debug for RendererDocumentIsolateHandle { } impl RendererDocumentIsolateHandle { + pub(crate) fn deferred_context_host_release_queue( + &self, + ) -> RendererDeferredContextHostReleaseQueue { + self.deferred_context_host_releases.clone() + } + #[cfg(test)] pub(crate) fn new_standalone_without_owner_reservation_for_test( v8_foreground_task_sender: RendererPageV8ForegroundTaskSender, @@ -384,7 +458,11 @@ impl RendererDocumentIsolateHandle { foreground_wake, devtools_target_shutdown_registry, )?; + let deferred_context_host_releases = renderer_document_isolate + .deferred_context_host_releases + .clone(); let renderer_document_isolate = Self { + deferred_context_host_releases, inner: Rc::new(RefCell::new(renderer_document_isolate)), }; let isolate_backend = renderer_document_isolate.inspector_isolate_backend_handle(); @@ -445,7 +523,12 @@ impl RendererDocumentIsolateHandle { let inspector_backend = inspector_backend .as_mut() .expect("document isolate Inspector backend missing before ScriptVm drop"); - with_entered_owned_isolate(isolate, |isolate| op(isolate, inspector_backend)) + with_entered_owned_isolate(isolate, |isolate| { + let result = op(isolate, inspector_backend); + self.deferred_context_host_releases + .drain_on_entered_isolate(); + result + }) } pub(super) fn with_entered_renderer_document_isolate_and_inspector_mut( @@ -461,7 +544,12 @@ impl RendererDocumentIsolateHandle { let inspector_backend = inspector_backend .as_mut() .ok_or_else(|| anyhow!("document isolate Inspector backend unavailable"))?; - with_entered_owned_isolate(isolate, |isolate| op(isolate, inspector_backend)) + with_entered_owned_isolate(isolate, |isolate| { + let result = op(isolate, inspector_backend); + self.deferred_context_host_releases + .drain_on_entered_isolate(); + result + }) } pub(super) fn with_renderer_document_isolate_mut( @@ -469,7 +557,12 @@ impl RendererDocumentIsolateHandle { op: impl FnOnce(&mut v8::OwnedIsolate) -> T, ) -> T { let mut holder = self.inner.borrow_mut(); - with_entered_owned_isolate(&mut holder.isolate, op) + with_entered_owned_isolate(&mut holder.isolate, |isolate| { + let result = op(isolate); + self.deferred_context_host_releases + .drain_on_entered_isolate(); + result + }) } pub(super) fn with_entered_renderer_document_isolate( @@ -477,7 +570,12 @@ impl RendererDocumentIsolateHandle { op: impl FnOnce(&mut v8::OwnedIsolate) -> Result, ) -> Result { let mut holder = self.inner.borrow_mut(); - with_entered_owned_isolate(&mut holder.isolate, op) + with_entered_owned_isolate(&mut holder.isolate, |isolate| { + let result = op(isolate); + self.deferred_context_host_releases + .drain_on_entered_isolate(); + result + }) } pub(super) fn with_entered_renderer_document_isolate_and_bootstrap( @@ -488,7 +586,12 @@ impl RendererDocumentIsolateHandle { let RendererDocumentIsolateHolder { isolate, bootstrap, .. } = &mut *holder; - with_entered_owned_isolate(isolate, |isolate| op(isolate, &*bootstrap)) + with_entered_owned_isolate(isolate, |isolate| { + let result = op(isolate, &*bootstrap); + self.deferred_context_host_releases + .drain_on_entered_isolate(); + result + }) } pub(super) fn unregister_renderer_document_isolate_platform(&self) { @@ -506,6 +609,7 @@ impl RendererDocumentIsolateHandle { } pub(super) struct RendererDocumentIsolateHolder { + deferred_context_host_releases: RendererDeferredContextHostReleaseQueue, // Unregister before destroying the Inspector backend so owner shutdown // never observes a target whose isolate has already been disposed. _devtools_target_shutdown_registration: Option, @@ -675,6 +779,7 @@ impl RendererDocumentIsolateHolder { isolate: v8::OwnedIsolate, ) -> Self { Self { + deferred_context_host_releases: RendererDeferredContextHostReleaseQueue::default(), _devtools_target_shutdown_registration: devtools_target_shutdown_registration, inspector_backend: Some(inspector_backend), bootstrap, @@ -694,6 +799,7 @@ impl Drop for RendererDocumentIsolateHolder { unsafe { self.isolate.enter(); } + self.deferred_context_host_releases.begin_isolate_shutdown(); } } diff --git a/moli-renderer-v8/src/script_vm/drop_cleanup.rs b/moli-renderer-v8/src/script_vm/drop_cleanup.rs index 616dee19a7..f3557152a7 100644 --- a/moli-renderer-v8/src/script_vm/drop_cleanup.rs +++ b/moli-renderer-v8/src/script_vm/drop_cleanup.rs @@ -19,5 +19,10 @@ impl Drop for ScriptVm { .deactivate_page_vm_binding_for_teardown(); self.page_default_bridge_ref.take(); + if let Some(runtime) = self.document_runtime.take_for_retained_document_host() { + self._context_host + .borrow_mut() + .adopt_retained_document_runtime(runtime); + } } } diff --git a/moli-renderer-v8/src/script_vm/execution_contexts.rs b/moli-renderer-v8/src/script_vm/execution_contexts.rs index 0a26cfaa37..33f1450fc9 100644 --- a/moli-renderer-v8/src/script_vm/execution_contexts.rs +++ b/moli-renderer-v8/src/script_vm/execution_contexts.rs @@ -406,6 +406,8 @@ impl ScriptVm { Some(context) } Some(context) => { + let context_ptr = &context.context as *const v8::Global; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); self.cancel_history_traversals_for_retiring_window( crate::native_bridge::WindowExecutionContextOwner::Frame( context.local_window_id, @@ -555,6 +557,8 @@ impl ScriptVm { }; if !stale_prebootstrapped_contexts.is_empty() { for (_, context) in &stale_prebootstrapped_contexts { + let context_ptr = &context.context as *const v8::Global; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); self.cancel_history_traversals_for_retiring_window( crate::native_bridge::WindowExecutionContextOwner::Frame( context.local_window_id, @@ -623,6 +627,8 @@ impl ScriptVm { let Some(context) = self.child_frame_realm_store.remove(&execution_context_id) else { return; }; + let context_ptr: *const v8::Global = &context.context as *const _; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); self.cancel_history_traversals_for_retiring_window( crate::native_bridge::WindowExecutionContextOwner::Frame(context.local_window_id), ); @@ -696,8 +702,6 @@ impl ScriptVm { retired_timer_count, "retired child Runtime binding context" ); - let context_ptr: *const v8::Global = &context.context as *const _; - self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); assert!( self.page_inspector .destroy_context_registration(context.inspector_context_registration_id), @@ -806,6 +810,8 @@ impl ScriptVm { .destroy_context_registration(context.inspector_context_registration_id), "isolated context must retain its document-owned Inspector registration" ); + let context_ptr: *const v8::Global = &context.context as *const _; + self.clear_context_wrapper_cache_for_context_ptr(context_ptr, false); // V8 Inspector consumes the still-identifiable realm while processing // `context_destroyed` (including Runtime lifecycle projection for // named child worlds). Only after that notification may the strict diff --git a/moli-renderer-v8/src/script_vm/post_parse.rs b/moli-renderer-v8/src/script_vm/post_parse.rs index c09aab2f68..30d8eaddc8 100644 --- a/moli-renderer-v8/src/script_vm/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/post_parse.rs @@ -363,6 +363,13 @@ impl ScriptVmContextBootstrap { if let Some(registration) = realm_registration.as_mut() { registration.commit(); } + // Child Window exposure can reach bootstrap while the caller holds + // the host's RefCell borrow. Follow the bootstrap's raw host access + // discipline and end each native read before touching V8 again. + if unsafe { &*host_ptr }.document_host_is_published() { + let queue = unsafe { &*host_ptr }.deferred_context_host_release_queue(); + crate::util::retain_context_host_for_document_realm(local_context, context_host, queue); + } Ok(Self { context: v8::Global::new(scope, local_context), runtime_observable_context_token, diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs b/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs index a9cee40cd5..ae907ff2c8 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/child_documents.rs @@ -2986,7 +2986,8 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() { ) .expect("child wrapper cache setup should evaluate"); assert_eq!(child_wrappers, "child-wrappers"); - let child_wrapper_count = retained_child_cache.wrapper_entry_count_for_realm(child_realm_token); + let child_wrapper_count = + retained_child_cache.strong_wrapper_entry_count_for_realm(child_realm_token); assert!( child_wrapper_count >= 1, "child context wrappers should populate its default-world cache partition: {child_wrapper_count}" @@ -2999,7 +3000,8 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() { ) }) .expect("top default context token should be readable"); - let top_wrapper_count = retained_child_cache.wrapper_entry_count_for_realm(top_realm_token); + let top_wrapper_count = + retained_child_cache.strong_wrapper_entry_count_for_realm(top_realm_token); assert!( top_wrapper_count >= 1, "the shared default-world cache should contain live top-context wrappers" @@ -3031,12 +3033,13 @@ async fn child_default_bridge_ref_is_released_on_child_context_teardown() { "removed iframe should not keep a live child default context" ); assert_eq!( - retained_child_cache.wrapper_entry_count_for_realm(child_realm_token), + retained_child_cache.strong_wrapper_entry_count_for_realm(child_realm_token), 0, "destroying a child default context must retire its strong wrapper entries" ); assert!( - retained_child_cache.wrapper_entry_count_for_realm(top_realm_token) >= top_wrapper_count, + retained_child_cache.strong_wrapper_entry_count_for_realm(top_realm_token) + >= top_wrapper_count, "destroying a child context must preserve wrappers owned by the live top realm" ); assert_eq!( diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs index 54a5a75b29..8e11f4eceb 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs @@ -1,5 +1,77 @@ use super::*; +#[test] +fn retained_document_realm_keeps_native_values_until_the_last_v8_reference() { + let mut vm = new_parsed_test_vm( + "https://retained-document.test/old", + "

old document

", + ); + vm.eval( + r#" + globalThis.savedNode = document.querySelector('p'); + globalThis.savedDecoder = new TextDecoder(); + globalThis.savedBlob = new Blob(['retained blob']); + globalThis.savedController = new AbortController(); + globalThis.savedSignal = savedController.signal; + globalThis.savedComposite = AbortSignal.any([savedSignal]); + globalThis.abortEventRan = false; + savedSignal.onabort = () => { abortEventRan = true; }; + globalThis.savedReason = {kind: 'old realm'}; + globalThis.savedFunction = () => [savedNode.textContent, document.URL]; + 'ready' + "#, + ) + .unwrap(); + let weak_host = vm.context_host_weak_for_test(); + let isolate = vm.renderer_document_isolate.clone(); + let context = vm.page_default_context.clone(); + drop(vm); + assert!( + weak_host.upgrade().is_some(), + "retained realm owns its native DOM" + ); + + let value = isolate.with_renderer_document_isolate_mut(|isolate| { + let scope = std::pin::pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + let context = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, context); + let source = crate::util::v8str(scope, r#" + savedNode.textContent = 'retained'; + savedController.abort(savedReason); + JSON.stringify([ + savedFunction(), + savedDecoder.decode(new Uint8Array([65])), + savedBlob.size, + savedSignal.aborted, + savedSignal.reason === savedReason, + savedComposite.aborted && savedComposite.reason === savedReason, + abortEventRan, + savedNode === document.querySelector('p'), + (() => { try { savedSignal.throwIfAborted(); } catch (e) { return e === savedReason; } })() + ]) + "#); + let script = v8::Script::compile(scope, source, None).expect("retained realm compiles"); + crate::script_execution::execute_compiled_script(scope, script) + .expect("retained native values remain usable") + .to_rust_string_lossy(scope) + }); + assert_eq!( + value, + r#"[["retained","https://retained-document.test/old"],"A",13,true,true,true,false,true,true]"# + ); + + isolate.with_renderer_document_isolate_mut(|isolate| { + drop(context); + isolate.low_memory_notification(); + }); + isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification()); + assert!( + weak_host.upgrade().is_none(), + "last V8 reference releases native DOM outside GC" + ); +} + #[test] fn runtime_binding_calls_freeze_the_invoking_realm_generation() { let mut vm = new_storage_test_vm("https://runtime-binding-source-realm.test/"); @@ -307,23 +379,33 @@ fn page_context_teardown_releases_opfs_handle_and_directory_iterator_registratio vm.close_page_context_resources_for_context_teardown(); + let handles = vm._context_host.borrow().opfs_handle_registry().unwrap(); + let iterators = vm + ._context_host + .borrow() + .opfs_directory_iterator_registry() + .unwrap(); assert_eq!( - vm._context_host - .borrow() - .opfs_handle_registry() - .expect("OPFS handle registry remains owned until host teardown") - .len(), - 0, - "page teardown must run handle finalizers before isolate teardown" + handles.len(), + 1, + "retained handle remains backed while its realm is alive" ); assert_eq!( - vm._context_host - .borrow() - .opfs_directory_iterator_registry() - .expect("OPFS iterator registry remains owned until host teardown") - .len(), + iterators.len(), + 16, + "retained iterators remain backed while their realm is alive" + ); + assert_eq!(vm.eval("__opfsRoot.kind").unwrap(), "directory"); + drop(vm); + assert_eq!( + handles.len(), 0, - "page teardown must run iterator finalizers before isolate teardown" + "final Context release retires native handle registrations" + ); + assert_eq!( + iterators.len(), + 0, + "final Context release retires native iterator registrations" ); } #[test] @@ -802,7 +884,7 @@ fn promise_reject_context_slot_does_not_retain_context_host_after_script_vm_drop ); } #[test] -fn context_wrapper_cache_is_cleared_on_script_vm_teardown() { +fn context_wrapper_cache_releases_native_roots_on_script_vm_teardown() { let mut vm = new_parsed_test_vm( "https://wrapper-cache-retention.test/", "
", @@ -846,7 +928,7 @@ fn context_wrapper_cache_is_cleared_on_script_vm_teardown() { drop(vm); assert_eq!( - retained_cache.wrapper_entry_count(), + retained_cache.strong_wrapper_entry_count(), 0, "page context teardown must clear strong wrapper cache entries before contexts are dropped" ); @@ -890,7 +972,7 @@ fn script_vm_page_context_teardown_is_idempotent() { ); } #[test] -fn page_context_teardown_releases_all_context_owned_v8_finalizers() { +fn page_context_teardown_preserves_finalizers_for_retained_native_objects() { let mut vm = new_parsed_test_vm( "https://v8-finalizer-teardown.test/", "", @@ -933,10 +1015,9 @@ fn page_context_teardown_releases_all_context_owned_v8_finalizers() { ); vm.close_page_context_resources_for_context_teardown(); - assert_eq!( - vm._context_host.borrow().v8_finalizers.len(), - 0, - "page context teardown must reset every weak handle before isolate teardown" + assert!( + vm._context_host.borrow().v8_finalizers.len() >= 128, + "retiring active execution must preserve native objects retained by author code" ); assert_eq!( vm._context_host @@ -947,7 +1028,13 @@ fn page_context_teardown_releases_all_context_owned_v8_finalizers() { ); vm.close_page_context_resources_for_context_teardown(); + assert_eq!(vm.eval("__finalizerObjects[2].size").unwrap(), "9"); + let weak_host = vm.context_host_weak_for_test(); drop(vm); + assert!( + weak_host.upgrade().is_none(), + "final isolate release must drop the native host" + ); } #[test] fn embedded_frame_owners_create_child_contexts_only_for_document_content() { @@ -1737,3 +1824,97 @@ fn frame_owner_content_accessors_live_on_exact_owner_prototypes() { r#"{"descriptors":[{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}},{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}},{"contentDocument":{"get":"function","set":"undefined","enumerable":true,"configurable":true},"contentWindow":{"get":"function","set":"undefined","enumerable":true,"configurable":true}}],"sameOriginValues":[true,true,true],"brandErrors":[["TypeError","TypeError"],["TypeError","TypeError"],["TypeError","TypeError"]],"absentFromBase":true}"# ); } + +#[test] +fn unreferenced_traversal_filters_release_retired_document() { + for method in ["createTreeWalker", "createNodeIterator"] { + let mut vm = new_parsed_test_vm("https://traversal.test/old", "old document"); + vm.eval(&format!( + "document.{method}(document, NodeFilter.SHOW_ALL, () => NodeFilter.FILTER_ACCEPT); 1" + )) + .expect("unreferenced traversal setup"); + let host = vm.context_host_weak_for_test(); + let isolate = vm.renderer_document_isolate.clone(); + drop(vm); + for _ in 0..3 { + isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification()); + } + assert!( + host.upgrade().is_none(), + "{method}: an unreferenced filter must not retain its retired Document host" + ); + } +} + +#[test] +fn retained_traversal_filters_release_host_after_last_reference() { + for method in ["createTreeWalker", "createNodeIterator"] { + let mut vm = new_parsed_test_vm( + "https://retained-traversal.test/old", + "old document", + ); + vm.eval(&format!( + r#" + window.retainedFilter = () => NodeFilter.FILTER_ACCEPT; + window.retainedTraversal = document.{method}( + document.body, NodeFilter.SHOW_ALL, retainedFilter + ); + 1 + "# + )) + .expect("retained traversal setup"); + let walker = vm + .with_default_context_scope_and_checkpoint_for_test(|scope, _| { + let context = scope.get_current_context(); + let value = context + .global(scope) + .get(scope, crate::util::v8str(scope, "retainedTraversal").into()) + .unwrap(); + let walker = v8::Local::::try_from(value).unwrap(); + Ok(v8::Global::new(scope, walker)) + }) + .unwrap(); + let host = vm.context_host_weak_for_test(); + let isolate = vm.renderer_document_isolate.clone(); + drop(vm); + for _ in 0..3 { + isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification()); + } + assert!( + host.upgrade().is_some(), + "a retained walker keeps native backing alive" + ); + let result = isolate.with_renderer_document_isolate_mut(|isolate| { + let scope = std::pin::pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + let caller = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, caller); + let walker = v8::Local::new(scope, &walker); + let context = walker.get_creation_context(scope).unwrap(); + let scope = &mut v8::ContextScope::new(scope, context); + let source = crate::util::v8str( + scope, + r#"JSON.stringify([ + retainedTraversal.filter === retainedFilter, + retainedTraversal.root.textContent + ])"#, + ); + let script = v8::Script::compile(scope, source, None).unwrap(); + crate::script_execution::execute_compiled_script(scope, script) + .unwrap() + .to_rust_string_lossy(scope) + }); + assert_eq!(result, r#"[true,"old document"]"#); + isolate.with_renderer_document_isolate_mut(|isolate| { + drop(walker); + isolate.low_memory_notification(); + }); + for _ in 0..3 { + isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification()); + } + assert!( + host.upgrade().is_none(), + "the last walker reference releases its retired host" + ); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/lazy_storage.rs b/moli-renderer-v8/src/script_vm/tests/lazy_storage.rs index 1a6cf10db2..f1ffb6f400 100644 --- a/moli-renderer-v8/src/script_vm/tests/lazy_storage.rs +++ b/moli-renderer-v8/src/script_vm/tests/lazy_storage.rs @@ -613,6 +613,95 @@ fn native_dom_exception_uses_registered_prototype_after_public_override() { .expect("native exception should use the realm registry"); } +#[test] +fn native_dom_exception_materializes_without_reading_the_public_constructor() { + let mut vm = new_storage_test_vm("https://native-dom-exception-lazy.test/"); + vm.eval( + r#" + globalThis.exceptionConstructorReads = 0; + delete globalThis.DOMException; + Object.defineProperty(globalThis, 'DOMException', { + configurable: true, + get() { + ++exceptionConstructorReads; + throw new Error('the public constructor must not be read'); + } + }); + "#, + ) + .expect("public constructor override should evaluate"); + assert_eq!( + constructor_materialization_count(&mut vm, "DOMException"), + 0 + ); + + vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| { + let exception = crate::context_bootstrap::new_dom_exception_value( + scope, + "native security failure", + "SecurityError", + ); + let exception = v8::Local::::try_from(exception).unwrap(); + let intrinsic = + crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "DOMException")?; + assert!( + exception + .get_prototype(scope) + .unwrap() + .strict_equals(intrinsic.into()) + ); + Ok(()) + }) + .expect("native exception should materialize through its realm registry"); + assert_eq!(vm.eval("exceptionConstructorReads").unwrap(), "0"); + assert_eq!( + constructor_materialization_count(&mut vm, "DOMException"), + 1 + ); +} + +#[test] +fn native_dom_exception_materializes_in_a_detached_realm() { + let mut vm = new_storage_test_vm("https://native-dom-exception-detached.test/"); + assert_eq!( + constructor_materialization_count(&mut vm, "DOMException"), + 0 + ); + let isolate = vm.renderer_document_isolate.clone(); + let context = vm.page_default_context.clone(); + drop(vm); + isolate.with_renderer_document_isolate_mut(|isolate| { + let scope = std::pin::pin!(v8::HandleScope::new(isolate)); + let scope = &mut scope.init(); + let context = v8::Local::new(scope, &context); + context.detach_global(); + let scope = &mut v8::ContextScope::new(scope, context); + let exception = crate::context_bootstrap::new_dom_exception_value( + scope, + "native security failure", + "SecurityError", + ); + let exception = v8::Local::::try_from(exception).unwrap(); + let intrinsic = + crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "DOMException") + .expect("detached realm must retain its intrinsic registry"); + assert!( + exception + .get_prototype(scope) + .unwrap() + .strict_equals(intrinsic.into()) + ); + assert_eq!(exception.get_creation_context(scope), Some(context)); + assert_eq!( + exception + .get(scope, crate::util::v8str(scope, "name").into()) + .unwrap() + .to_rust_string_lossy(scope), + "SecurityError" + ); + }); +} + #[test] fn illegal_storage_receivers_do_not_materialize_wrappers_or_opfs_state() { let mut vm = new_storage_test_vm("https://lazy-illegal-storage-receivers.test/"); diff --git a/moli-renderer-v8/src/util.rs b/moli-renderer-v8/src/util.rs index 8dd09648a6..23fc4d0698 100644 --- a/moli-renderer-v8/src/util.rs +++ b/moli-renderer-v8/src/util.rs @@ -18,7 +18,11 @@ pub use moli_v8_util::{ }; use moli_webapi_declare::WebApiValue; pub(crate) use moli_webapi_declare::define_array_data_property as define_v8_array_data_property; -use std::{ptr::NonNull, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + ptr::NonNull, + rc::Rc, +}; use url::Url; use widestring::U16String; @@ -26,10 +30,113 @@ const SCRIPT_BASE_URL_HOST_DEFINED_OPTIONS_MARKER: &str = "moli-script-base-url- // Access-check callbacks cannot inspect the global object to rediscover the host: // that property lookup would recursively invoke the same V8 access check. This -// non-owning slot is valid while the context can execute because the matching -// ScriptVm context state retains its `JsContextHostBridgeRef`. +// slot starts non-owning during fallible bootstrap, then a successfully +// published Document realm promotes it to a Context-owned host reference. That +// mirrors ScriptState/Oilpan's retained-realm edge closely enough for Moli's +// non-tracing Rust heap: a retained function or DOM wrapper keeps its native +// Document backing alive, while temporary WindowProxy facades remain +// non-owning and fail closed as soon as the host detaches. +struct ContextHostPointerSlot { + host_ptr: NonNull, + host_lifecycle: Rc>, + retained_realm_owner: RefCell>, +} + +struct RetainedDocumentRealmHostOwner { + host: Option>>, + deferred_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue, + retained_v8_handle_state: Vec>, +} + +impl Drop for RetainedDocumentRealmHostOwner { + fn drop(&mut self) { + if let Some(host) = self.host.take() { + self.deferred_release_queue + .defer(host, std::mem::take(&mut self.retained_v8_handle_state)); + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ContextHostLifecycle { + Active, + Detached, + Destroyed, +} + +/// Marks a V8 Context whose native Document host has been retired. +/// +/// Navigation can keep the stable WindowProxy while replacing its Context and +/// `JsContextHost`; final Page close also parks that proxy on a host-free +/// facade. In both cases retained functions and wrappers can outlive the Rust +/// host, so every raw-pointer entry point must fail closed before inspecting +/// embedder data. #[derive(Debug)] -struct ContextHostPointerSlot(NonNull); +struct DisconnectedPageContext; + +/// Marks a real Document realm whose browsing-context execution authority has +/// ended while its ordinary JS and DOM objects remain usable if author code +/// still retains them. +#[derive(Debug)] +struct DetachedDocumentPageContext; + +/// Active native registries root their objects until teardown. A detached +/// realm instead keeps those objects in its own V8-traced graph, so retaining +/// a Document preserves wrapper identity without a Rust -> V8 -> Rust cycle. +#[derive(Debug)] +pub(crate) enum RealmObjectHandle { + Strong(v8::Global), + Traced(v8::Weak), +} + +impl RealmObjectHandle { + pub(crate) fn new(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> Self { + if page_context_is_detached_document(scope.get_current_context()) { + Self::traced(scope, object) + } else { + Self::Strong(v8::Global::new(scope, object)) + } + } + + fn traced(scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>) -> Self { + const ROOTS: &str = "__moliRetainedRealmObjects"; + let context = object + .get_creation_context(scope) + .unwrap_or_else(|| scope.get_current_context()); + let anchor = context.get_extras_binding_object(scope); + let roots = get_private_value(scope, anchor, ROOTS) + .and_then(|value| v8::Local::::try_from(value).ok()) + .unwrap_or_else(|| { + let roots = v8::Array::new(scope, 0); + roots.set_prototype(scope, v8::null(scope).into()); + set_private_value(scope, anchor, ROOTS, roots.into()); + roots + }); + roots.set_index(scope, roots.length(), object.into()); + Self::Traced(v8::Weak::new(scope, object)) + } + + pub(crate) fn to_local<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + ) -> Option> { + match self { + Self::Strong(object) => Some(v8::Local::new(scope, object)), + Self::Traced(object) => object.to_local(scope), + } + } + + pub(crate) fn retain_in_realm(&mut self, scope: &mut v8::PinScope<'_, '_>) { + if let Self::Strong(object) = self { + let object = v8::Local::new(scope, &*object); + *self = Self::traced(scope, object); + } + } + + pub(crate) fn is_strong(&self) -> bool { + matches!(self, Self::Strong(_)) + } +} /// Materializes the unexposed prototype described by a WebIDL iterator /// FunctionTemplate. The temporary constructor is an implementation detail, so @@ -373,6 +480,9 @@ pub(super) fn context_host_ptr_from_global_bridge( scope: &mut v8::PinScope<'_, '_>, ) -> Option<*mut JsContextHost> { let context = scope.get_current_context(); + if page_context_is_disconnected(context) { + return None; + } if let Some(host_ptr) = context_host_ptr_from_context_slot(context) { return Some(host_ptr); } @@ -384,24 +494,123 @@ pub(super) fn context_host_ptr_from_global_bridge( pub(super) fn install_context_host_pointer_slot( context: v8::Local<'_, v8::Context>, host_ptr: *mut JsContextHost, + host_lifecycle: Rc>, ) { let host_ptr = NonNull::new(host_ptr).expect("V8 context JsContextHost pointer should not be null"); - let previous = context.set_slot(Rc::new(ContextHostPointerSlot(host_ptr))); assert!( - previous - .as_deref() - .is_none_or(|previous| previous.0 == host_ptr), - "V8 context JsContextHost pointer must not be rebound" + host_lifecycle.get() == ContextHostLifecycle::Active, + "a retired JsContextHost must not be installed into a V8 context" ); + if let Some(existing) = context.get_slot::() { + assert!( + existing.host_ptr == host_ptr && Rc::ptr_eq(&existing.host_lifecycle, &host_lifecycle), + "V8 context JsContextHost pointer must not be rebound" + ); + return; + } + let previous = context.set_slot(Rc::new(ContextHostPointerSlot { + host_ptr, + host_lifecycle: host_lifecycle.clone(), + retained_realm_owner: RefCell::new(None), + })); + debug_assert!(previous.is_none()); } pub(super) fn context_host_ptr_from_context_slot( context: v8::Local<'_, v8::Context>, ) -> Option<*mut JsContextHost> { + if page_context_is_disconnected(context) { + return None; + } context .get_slot::() - .map(|slot| slot.0.as_ptr()) + .map(|slot| slot.host_ptr.as_ptr()) +} + +pub(crate) fn retain_context_host_for_document_realm( + context: v8::Local<'_, v8::Context>, + host: Rc>, + deferred_release_queue: crate::script_vm::RendererDeferredContextHostReleaseQueue, +) { + let slot = context + .get_slot::() + .expect("published Document realm must have a JsContextHost pointer slot"); + let host_ptr = NonNull::new(host.as_ptr()) + .expect("published Document realm JsContextHost pointer should not be null"); + assert_eq!( + slot.host_ptr, host_ptr, + "published Document realm must retain its installed JsContextHost" + ); + assert!( + slot.host_lifecycle.get() != ContextHostLifecycle::Destroyed, + "destroyed JsContextHost must not gain a retained realm owner" + ); + let mut retained = slot.retained_realm_owner.borrow_mut(); + if let Some(existing) = retained.as_ref() { + assert!( + existing + .host + .as_ref() + .is_some_and(|existing| Rc::ptr_eq(existing, &host)), + "Document realm JsContextHost owner must not be rebound" + ); + return; + } + *retained = Some(RetainedDocumentRealmHostOwner { + host: Some(host), + deferred_release_queue, + retained_v8_handle_state: Vec::new(), + }); +} + +pub(crate) fn retain_context_v8_handle_state_for_safe_release( + context: v8::Local<'_, v8::Context>, + state: Rc, +) { + let slot = context + .get_slot::() + .expect("retained Context embedder state requires a Document host slot"); + let mut retained_owner = slot.retained_realm_owner.borrow_mut(); + let retained_owner = retained_owner + .as_mut() + .expect("retained Context embedder state requires a published real realm"); + retained_owner + .retained_v8_handle_state + .push(Box::new(state)); +} + +pub(crate) fn page_context_is_disconnected(context: v8::Local<'_, v8::Context>) -> bool { + context.get_slot::().is_some() + || context + .get_slot::() + .is_some_and(|slot| match slot.host_lifecycle.get() { + ContextHostLifecycle::Active => false, + ContextHostLifecycle::Detached => slot.retained_realm_owner.borrow().is_none(), + ContextHostLifecycle::Destroyed => true, + }) +} + +pub(crate) fn page_context_is_detached_document(context: v8::Local<'_, v8::Context>) -> bool { + context.get_slot::().is_some() + || context + .get_slot::() + .is_some_and(|slot| slot.host_lifecycle.get() == ContextHostLifecycle::Detached) +} + +pub(crate) fn detach_document_page_context(context: v8::Local<'_, v8::Context>) { + if page_context_is_disconnected(context) || page_context_is_detached_document(context) { + return; + } + let slot = context + .get_slot::() + .expect("detached Document realm must have a JsContextHost pointer slot"); + assert!( + slot.retained_realm_owner.borrow().is_some(), + "only a retained real Document realm may enter detached state" + ); + let previous = context.set_slot(Rc::new(DetachedDocumentPageContext)); + debug_assert!(previous.is_none()); } /// Convenience wrapper for callbacks that immediately need the host value. @@ -445,6 +654,13 @@ pub(super) fn context_host_ptr_from_window_object( scope: &mut v8::PinScope<'_, '_>, object: v8::Local<'_, v8::Object>, ) -> Option<*mut JsContextHost> { + if page_context_is_disconnected(scope.get_current_context()) + || object + .get_creation_context(scope) + .is_some_and(page_context_is_disconnected) + { + return None; + } if let Some(value) = object.get_internal_field(scope, 0) && let Ok(external) = v8::Local::::try_from(value) { diff --git a/moli-renderer-v8/src/v8_traced_webidl_callback.rs b/moli-renderer-v8/src/v8_traced_webidl_callback.rs index 7d93d2aa08..7c73670ca8 100644 --- a/moli-renderer-v8/src/v8_traced_webidl_callback.rs +++ b/moli-renderer-v8/src/v8_traced_webidl_callback.rs @@ -1,4 +1,4 @@ -//! V8-traced residence for a Web IDL callback function. +//! V8-traced residence for Web IDL callback functions and interfaces. //! //! Some Web API owners already live as JavaScript objects. Storing their //! callbacks in independent Rust `Global` roots would keep callback-to-owner @@ -11,13 +11,88 @@ //! Owners that need exact Window currentness must add that authorization before //! invoking the prepared callback. -use moli_webidl_callback::{PreparedWebIdlCallbackFunction, WebIdlCallbackFunction}; +use moli_webidl_callback::{ + PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackFunction, + WebIdlCallbackInterface, +}; use crate::util::{get_private_value, set_private_value}; const CALLBACK_VALUE_SLOT: &str = "__lmV8TracedWebIdlCallbackValue"; const CALLBACK_RELEVANT_GLOBAL_SLOT: &str = "__lmV8TracedWebIdlCallbackRelevantGlobal"; const CALLBACK_INCUMBENT_GLOBAL_SLOT: &str = "__lmV8TracedWebIdlCallbackIncumbentGlobal"; +const CALLBACK_CALLABLE_SLOT: &str = "__lmV8TracedWebIdlCallbackCallableAtConversion"; + +/// Callback-interface residence owned by a JavaScript wrapper. Context extras +/// retain the original realms even after their WindowProxies are detached. +pub(crate) struct V8TracedWebIdlCallbackInterface<'s> { + carrier: v8::Local<'s, v8::Object>, +} + +impl<'s> V8TracedWebIdlCallbackInterface<'s> { + pub(crate) fn new(scope: &mut v8::PinScope<'s, '_>, callback: WebIdlCallbackInterface) -> Self { + let carrier = v8::Object::new(scope); + let value = callback.value(scope); + let relevant = callback + .relevant_context(scope) + .get_extras_binding_object(scope); + let incumbent = callback + .incumbent_context(scope) + .get_extras_binding_object(scope); + let callable = v8::Boolean::new(scope, callback.callable_at_conversion()); + set_private_value(scope, carrier, CALLBACK_VALUE_SLOT, value); + set_private_value( + scope, + carrier, + CALLBACK_RELEVANT_GLOBAL_SLOT, + relevant.into(), + ); + set_private_value( + scope, + carrier, + CALLBACK_INCUMBENT_GLOBAL_SLOT, + incumbent.into(), + ); + set_private_value(scope, carrier, CALLBACK_CALLABLE_SLOT, callable.into()); + Self { carrier } + } + + pub(crate) const fn from_object(carrier: v8::Local<'s, v8::Object>) -> Self { + Self { carrier } + } + + pub(crate) const fn into_object(self) -> v8::Local<'s, v8::Object> { + self.carrier + } + + pub(crate) fn prepare( + &self, + scope: &mut v8::PinScope<'s, '_>, + ) -> PreparedWebIdlCallbackInterface { + let object = |scope: &mut v8::PinScope<'s, '_>, slot| { + get_private_value(scope, self.carrier, slot) + .and_then(|value| v8::Local::::try_from(value).ok()) + .expect("a traced callback interface must retain its registration facts") + }; + let callback = object(scope, CALLBACK_VALUE_SLOT); + let relevant_context = object(scope, CALLBACK_RELEVANT_GLOBAL_SLOT) + .get_creation_context(scope) + .expect("callback relevant realm anchor"); + let incumbent_context = object(scope, CALLBACK_INCUMBENT_GLOBAL_SLOT) + .get_creation_context(scope) + .expect("callback incumbent realm anchor"); + let callable = get_private_value(scope, self.carrier, CALLBACK_CALLABLE_SLOT) + .expect("callback-interface callability at conversion") + .is_true(); + PreparedWebIdlCallbackInterface::new( + scope, + callback, + relevant_context, + incumbent_context, + callable, + ) + } +} pub(crate) struct V8TracedWebIdlCallbackFunction<'s> { carrier: v8::Local<'s, v8::Object>, diff --git a/moli-webidl-callback/src/callback_interface.rs b/moli-webidl-callback/src/callback_interface.rs index 8316efc4e3..d4c4795541 100644 --- a/moli-webidl-callback/src/callback_interface.rs +++ b/moli-webidl-callback/src/callback_interface.rs @@ -32,15 +32,13 @@ impl WebIdlCallbackInterface { /// Event dispatch can therefore release all registration-store borrows /// before entering user code and its synchronous reentrancy. pub fn prepare(&self, scope: &mut v8::PinScope<'_, '_>) -> PreparedWebIdlCallbackInterface { - PreparedWebIdlCallbackInterface { - callback: v8::Global::new(scope, v8::Local::new(scope, &self.callback)), - relevant_context: v8::Global::new(scope, v8::Local::new(scope, &self.relevant_context)), - incumbent_context: v8::Global::new( - scope, - v8::Local::new(scope, &self.incumbent_context), - ), - callable_at_conversion: self.callable_at_conversion, - } + PreparedWebIdlCallbackInterface::new( + scope, + v8::Local::new(scope, &self.callback), + v8::Local::new(scope, &self.relevant_context), + v8::Local::new(scope, &self.incumbent_context), + self.callable_at_conversion, + ) } pub fn matches<'s>( @@ -83,6 +81,22 @@ pub struct PreparedWebIdlCallbackInterface { } impl PreparedWebIdlCallbackInterface { + /// Root a snapshot of callback-interface facts held by a traced owner. + pub fn new( + scope: &mut v8::PinScope<'_, '_>, + callback: v8::Local<'_, v8::Object>, + relevant_context: v8::Local<'_, v8::Context>, + incumbent_context: v8::Local<'_, v8::Context>, + callable_at_conversion: bool, + ) -> Self { + Self { + callback: v8::Global::new(scope, callback), + relevant_context: v8::Global::new(scope, relevant_context), + incumbent_context: v8::Global::new(scope, incumbent_context), + callable_at_conversion, + } + } + pub fn callback<'s>(&self, scope: &mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object> { v8::Local::new(scope, &self.callback) }