mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(request): retain iframe settings after removal
Keep each child realm's associated native Document and captured origin after execution authority retires. Saved Request constructors continue reading live base changes on that Document and cannot resolve against a reinserted iframe. Keep removed Window globals attached, detach only for same-frame proxy reuse, and explicitly reject retired Window callback realms. Preserve initial empty Document bootstrap resolution and use intrinsic abort constructors internally. Cover about:blank, srcdoc, HTTP, realm cleanup, base mutation, reinsertion, referrers and dependent signals. Existing MessagePort retirement and nested blob iframe constructor tests remain unchanged and pass. Validation: cargo fmt --all; workspace Clippy with all targets/features and -D warnings; full nextest (19,144 passed, 16 configured skips, no retries).
This commit is contained in:
@@ -4,7 +4,7 @@ use crate::exception_reporting::{
|
||||
};
|
||||
use crate::{
|
||||
host::WINDOW_EVENT_SLOT,
|
||||
native_bridge::{JsContextHost, WindowExecutionContextIdentity},
|
||||
native_bridge::{JsContextHost, RuntimeObservableContextToken, WindowExecutionContextIdentity},
|
||||
util::v8str,
|
||||
};
|
||||
use moli_webidl_callback::{
|
||||
@@ -208,11 +208,22 @@ impl CallbackInvoker {
|
||||
let value: v8::Local<v8::Value> = v8::undefined(scope).into();
|
||||
return CallbackInvocationOutcome::Returned(v8::Global::new(scope, value));
|
||||
}
|
||||
if let (Some(host_ptr), Some(identity)) =
|
||||
(invocation.host_ptr, invocation.relevant_identity)
|
||||
&& !unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity)
|
||||
{
|
||||
return CallbackInvocationOutcome::Retired;
|
||||
if let Some(host_ptr) = invocation.host_ptr {
|
||||
let host = unsafe { &*host_ptr };
|
||||
let is_retired = match invocation.relevant_identity {
|
||||
Some(identity) => !host.window_execution_context_identity_is_current(identity),
|
||||
// A retained Window can keep its V8 global attached after its
|
||||
// registry entry is removed. Missing authority is not an
|
||||
// unrestricted callback realm. Worker contexts have no Window
|
||||
// token and continue using their own delivery authority.
|
||||
None => invocation
|
||||
.relevant_context
|
||||
.get_slot::<RuntimeObservableContextToken>()
|
||||
.is_some(),
|
||||
};
|
||||
if is_retired {
|
||||
return CallbackInvocationOutcome::Retired;
|
||||
}
|
||||
}
|
||||
|
||||
let result = with_webidl_callback_contexts(
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::document_script_scheduler::FrameDocumentClassicScriptSchedulerWork;
|
||||
use crate::dom::native::Node;
|
||||
use crate::dom_parser::DOM_PARSER_FOREIGN_NODE_SLOT;
|
||||
use crate::native_bridge::{
|
||||
OwnerDispatchScope,
|
||||
OwnerDispatchScope, WindowEnvironmentSettings,
|
||||
document::{detached_native_handle_for_runtime, is_html_document},
|
||||
node::remove_child_to_current_reaction_queue,
|
||||
throw_dom_exception,
|
||||
@@ -112,6 +112,7 @@ impl JsContextHost {
|
||||
self.clear_child_browsing_context_live_foreign_pairings(scope, document_handle);
|
||||
install_child_document_stream_methods(scope, document, handle);
|
||||
if let Some(window) = window {
|
||||
let _ = WindowEnvironmentSettings::bind_current_child_document(scope, self, handle);
|
||||
sync_child_document_window_slots(
|
||||
scope,
|
||||
document,
|
||||
|
||||
+8
-1
@@ -80,6 +80,10 @@ impl JsContextHost {
|
||||
let owner = self
|
||||
.current_child_document_task_owner(handle)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing child LocalWindow owner"))?;
|
||||
let frame_id = self
|
||||
.frame_owner_frame_id_for_child_handle(handle)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing child frame identity"))?
|
||||
.0;
|
||||
let execution_context_owner = WindowExecutionContextOwner::Frame(owner.local_window_id);
|
||||
let dispatch_scope = OwnerDispatchScope::Child(handle);
|
||||
if let Some((_, context)) =
|
||||
@@ -141,7 +145,9 @@ impl JsContextHost {
|
||||
stale.runtime_observable_context_token,
|
||||
);
|
||||
let stale_context = v8::Local::new(scope, &stale.context);
|
||||
stale_context.detach_global();
|
||||
if self.child_window_proxy_frame_is_current(handle, &stale.frame_id) {
|
||||
stale_context.detach_global();
|
||||
}
|
||||
}
|
||||
|
||||
let caller_global = scope.get_current_context().global(scope);
|
||||
@@ -179,6 +185,7 @@ impl JsContextHost {
|
||||
pending_contexts.borrow_mut().insert(
|
||||
handle,
|
||||
PrebootstrappedChildDefaultContext {
|
||||
frame_id,
|
||||
local_window_id: owner.local_window_id,
|
||||
context,
|
||||
bridge_ref,
|
||||
|
||||
+13
-2
@@ -12,7 +12,7 @@ use crate::{
|
||||
sync_window_location_history_navigation_runtime_surface,
|
||||
},
|
||||
native_bridge::{
|
||||
JsContextHost, OwnerDispatchScope, WindowExecutionContextOwner,
|
||||
JsContextHost, OwnerDispatchScope, WindowEnvironmentSettings, WindowExecutionContextOwner,
|
||||
child_window_surface::{
|
||||
bind_materialized_child_window_indexed_db_factory,
|
||||
initialize_child_window_realm_environment, rebind_child_window_document_environment,
|
||||
@@ -65,6 +65,7 @@ pub(in crate::native_bridge::context_host::child_frame_runtime) fn initialize_ch
|
||||
sync_child_document_window_slots(scope, document, global, true);
|
||||
set_object_slot(scope, global, "document", document.into());
|
||||
validate_child_window_realm_snapshot(host, &snapshot)?;
|
||||
bind_document_settings(host, scope, &snapshot)?;
|
||||
|
||||
Ok(ChildWindowRealmProjection {
|
||||
parent,
|
||||
@@ -125,7 +126,17 @@ pub(in crate::native_bridge::context_host::child_frame_runtime) fn rebind_child_
|
||||
.ok_or_else(|| anyhow::anyhow!("missing rebound child Document wrapper"))?;
|
||||
sync_child_document_window_slots(scope, document, global, true);
|
||||
set_object_slot(scope, global, "document", document.into());
|
||||
validate_child_window_realm_snapshot(host, &snapshot)
|
||||
validate_child_window_realm_snapshot(host, &snapshot)?;
|
||||
bind_document_settings(host, scope, &snapshot)
|
||||
}
|
||||
|
||||
fn bind_document_settings(
|
||||
host: &JsContextHost,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
snapshot: &super::model::ChildWindowRealmSnapshot,
|
||||
) -> Result<()> {
|
||||
WindowEnvironmentSettings::bind_current_child_document(scope, host, snapshot.handle)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing current child settings Document"))
|
||||
}
|
||||
|
||||
fn validate_registered_realm(
|
||||
|
||||
@@ -897,6 +897,18 @@ impl JsContextHost {
|
||||
self.child_window_proxy_records.clear_live_records(handle);
|
||||
}
|
||||
|
||||
/// Only navigation within the same browsing context reuses its proxy.
|
||||
/// A removed/reinserted iframe element can have the same DOM handle while
|
||||
/// exposing an entirely new browsing context and WindowProxy.
|
||||
pub(crate) fn child_window_proxy_frame_is_current(
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
frame_id: &str,
|
||||
) -> bool {
|
||||
self.frame_owner_frame_id_for_child_handle(handle)
|
||||
.is_some_and(|current| current.0 == frame_id)
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn retain_live_child_window_proxy_records(
|
||||
&mut self,
|
||||
live_handles: &HashSet<DomHandle>,
|
||||
|
||||
@@ -224,9 +224,10 @@ pub(crate) use selection_records::{
|
||||
};
|
||||
use websockets::WebSocketConnectionState;
|
||||
pub(crate) use window_execution_context::{
|
||||
DetachedWindowFetchContext, WindowExecutionContextAccessPolicy, WindowExecutionContextBinding,
|
||||
WindowExecutionContextIdentity, WindowExecutionContextOwner, WindowFetchContext,
|
||||
WindowOperationReceiver, WindowOperationReceiverCaptureError, WindowTaskTarget,
|
||||
DetachedWindowFetchContext, WindowEnvironmentSettings, WindowExecutionContextAccessPolicy,
|
||||
WindowExecutionContextBinding, WindowExecutionContextIdentity, WindowExecutionContextOwner,
|
||||
WindowFetchContext, WindowOperationReceiver, WindowOperationReceiverCaptureError,
|
||||
WindowTaskTarget,
|
||||
};
|
||||
use window_execution_context::{
|
||||
WindowExecutionContextRealmRecords, WindowExecutionContextRealmRegistration,
|
||||
@@ -236,6 +237,7 @@ use workers::WorkerConnectionState;
|
||||
pub(crate) use workers::WorkerOwnerScope;
|
||||
|
||||
pub(crate) struct PrebootstrappedChildDefaultContext {
|
||||
pub(crate) frame_id: String,
|
||||
pub(crate) local_window_id: crate::frame_owner_model::LocalWindowId,
|
||||
pub(crate) context: v8::Global<v8::Context>,
|
||||
pub(crate) bridge_ref: JsContextHostBridgeRef,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Registry-backed Window realms and Window-owned asynchronous work.
|
||||
//!
|
||||
//! The submodules deliberately keep three concepts separate:
|
||||
//! The submodules keep realm authority, operation targets, and settings separate:
|
||||
//!
|
||||
//! - `registry` is the authority for realm ownership and access policy;
|
||||
//! - `binding` is a stable ScriptState-like reference to one registered realm;
|
||||
@@ -8,6 +8,8 @@
|
||||
//! WebIDL conversion can run author code;
|
||||
//! - `fetch` couples that realm to the LocalWindow whose request lifetime it
|
||||
//! follows.
|
||||
//! - `settings` keeps a realm's associated Document available to pure APIs
|
||||
//! after its execution authority has retired.
|
||||
//!
|
||||
//! In particular, a binding is never rewritten to point at another Window.
|
||||
|
||||
@@ -15,6 +17,7 @@ mod binding;
|
||||
mod fetch;
|
||||
mod operation_receiver;
|
||||
mod registry;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use binding::WindowExecutionContextBinding;
|
||||
pub(crate) use fetch::{DetachedWindowFetchContext, WindowFetchContext, WindowTaskTarget};
|
||||
@@ -26,3 +29,4 @@ pub(super) use registry::{
|
||||
WindowExecutionContextRealmRecords, WindowExecutionContextRealmRegistration,
|
||||
WindowExecutionContextScopedRealmRegistration,
|
||||
};
|
||||
pub(crate) use settings::WindowEnvironmentSettings;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use super::super::JsContextHost;
|
||||
use super::super::OwnerDispatchScope;
|
||||
use super::WindowExecutionContextIdentity;
|
||||
use crate::document_runtime::DomHandle;
|
||||
use moli_url::WebOrigin;
|
||||
use std::rc::Rc;
|
||||
use url::Url;
|
||||
|
||||
/// Document-backed settings for pure APIs in a retained Window realm.
|
||||
///
|
||||
/// Unlike an execution-context binding, these survive owner retirement. They
|
||||
/// grant no authority to dispatch tasks or start network requests. The native
|
||||
/// Document remains in the host's DOM arena after iframe removal; retaining its
|
||||
/// identity lets subsequent calls observe live base changes without consulting
|
||||
/// a reused iframe handle. No V8 Global is held here, so this slot does not keep
|
||||
/// its own Context alive.
|
||||
pub(crate) struct WindowEnvironmentSettings {
|
||||
document: DomHandle,
|
||||
origin: WebOrigin,
|
||||
execution_identity: WindowExecutionContextIdentity,
|
||||
}
|
||||
|
||||
impl WindowEnvironmentSettings {
|
||||
pub(in crate::native_bridge::context_host) fn bind_current_child_document(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host: &JsContextHost,
|
||||
child_handle: DomHandle,
|
||||
) -> Option<()> {
|
||||
// A reused element can point to a new Window. Only a currently
|
||||
// registered realm may change its associated Document; a retained old
|
||||
// realm keeps its settings even when the same handle becomes live.
|
||||
let execution_identity = host
|
||||
.current_runtime_window_execution_context_identity_for_dispatch_scope(
|
||||
scope,
|
||||
OwnerDispatchScope::Child(child_handle),
|
||||
)?;
|
||||
let document = host.child_browsing_context_document_handle(child_handle)?;
|
||||
let owner = host.frame_owner_current_child_snapshot(child_handle)?;
|
||||
let origin = WebOrigin::from_serialized(&owner.settings.origin);
|
||||
let _previous = scope.get_current_context().set_slot(Rc::new(Self {
|
||||
document,
|
||||
origin,
|
||||
execution_identity,
|
||||
}));
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub(crate) fn for_current_realm(scope: &mut v8::PinScope<'_, '_>) -> Option<Rc<Self>> {
|
||||
scope.get_current_context().get_slot::<Self>()
|
||||
}
|
||||
|
||||
pub(crate) fn api_base_url(&self, host: &JsContextHost) -> Option<Url> {
|
||||
let document = host.dom_host().node(self.document)?.as_document()?;
|
||||
// Preserve the existing bootstrap view while an initial empty Document
|
||||
// still stands in for a locally available child snapshot. An explicit
|
||||
// base on the associated Document always wins. Consult the live route
|
||||
// only for this exact realm and Document, never a reused element.
|
||||
if document.base_element_url().is_none()
|
||||
&& host.window_execution_context_identity_is_current(self.execution_identity)
|
||||
&& let OwnerDispatchScope::Child(handle) = self.execution_identity.dispatch_scope()
|
||||
&& host.child_browsing_context_document_handle(handle) == Some(self.document)
|
||||
&& host
|
||||
.frame_owner_store
|
||||
.current_child_document_creation_kind(handle)
|
||||
.is_some_and(|kind| kind.is_initial_empty())
|
||||
{
|
||||
return host.child_browsing_context_base_url(handle);
|
||||
}
|
||||
Some(document.base_url().clone())
|
||||
}
|
||||
|
||||
pub(crate) fn origin(&self) -> &WebOrigin {
|
||||
&self.origin
|
||||
}
|
||||
}
|
||||
@@ -987,10 +987,9 @@ fn readable_stream_locked(
|
||||
pub(in crate::network_host) fn new_abort_signal_for_request<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let ctor = global
|
||||
.get(scope, v8str(scope, "AbortController").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())?;
|
||||
let ctor =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_constructor(scope, "AbortController")
|
||||
.ok()?;
|
||||
let controller = ctor.new_instance(scope, &[])?;
|
||||
controller.get(scope, v8str(scope, "signal").into())
|
||||
}
|
||||
@@ -1002,10 +1001,9 @@ pub(in crate::network_host) fn new_abort_signal_for_request_with_source<'s>(
|
||||
let Some(source) = source else {
|
||||
return new_abort_signal_for_request(scope);
|
||||
};
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let abort_signal_ctor = global
|
||||
.get(scope, v8str(scope, "AbortSignal").into())
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())?;
|
||||
let abort_signal_ctor =
|
||||
crate::context_bootstrap::ensure_intrinsic_interface_constructor(scope, "AbortSignal")
|
||||
.ok()?;
|
||||
let any = abort_signal_ctor
|
||||
.get(scope, v8str(scope, "any").into())
|
||||
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RequestUrlError {
|
||||
Resolve(ResolveContextUrlError),
|
||||
AssociatedDocumentUnavailable,
|
||||
OpaqueWorkerBase { input: String },
|
||||
}
|
||||
|
||||
@@ -18,6 +19,9 @@ impl fmt::Display for RequestUrlError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Resolve(error) => error.fmt(f),
|
||||
Self::AssociatedDocumentUnavailable => {
|
||||
f.write_str("Request settings Document is unavailable")
|
||||
}
|
||||
Self::OpaqueWorkerBase { input } => write!(f, "Failed to parse URL from {input}"),
|
||||
}
|
||||
}
|
||||
@@ -27,7 +31,7 @@ impl std::error::Error for RequestUrlError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::Resolve(error) => Some(error),
|
||||
Self::OpaqueWorkerBase { .. } => None,
|
||||
Self::AssociatedDocumentUnavailable | Self::OpaqueWorkerBase { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::super::headers::HeadersGuard;
|
||||
use super::*;
|
||||
use crate::native_bridge::WindowEnvironmentSettings;
|
||||
use crate::web_api_interfaces;
|
||||
use crate::webidl;
|
||||
use moli_url::WebOrigin;
|
||||
@@ -354,13 +355,20 @@ pub(crate) fn try_resolve_request_constructor_url_for_scope(
|
||||
}
|
||||
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
let api_base_url = child_handle
|
||||
.and_then(|handle| host.child_browsing_context_base_url(handle))
|
||||
.unwrap_or_else(|| {
|
||||
let owner = effective_subresource_request_owner(scope, host);
|
||||
subresource_api_base_url(scope, host, owner)
|
||||
.unwrap_or_else(|| host.document_url().clone())
|
||||
});
|
||||
let api_base_url =
|
||||
if let Some(settings) = WindowEnvironmentSettings::for_current_realm(scope) {
|
||||
settings
|
||||
.api_base_url(host)
|
||||
.ok_or(RequestUrlError::AssociatedDocumentUnavailable)?
|
||||
} else {
|
||||
child_handle
|
||||
.and_then(|handle| host.child_browsing_context_base_url(handle))
|
||||
.unwrap_or_else(|| {
|
||||
let owner = effective_subresource_request_owner(scope, host);
|
||||
subresource_api_base_url(scope, host, owner)
|
||||
.unwrap_or_else(|| host.document_url().clone())
|
||||
})
|
||||
};
|
||||
resolve_context_url(&api_base_url, input, None)
|
||||
.map(|url| url.to_string())
|
||||
.map_err(RequestUrlError::from)
|
||||
@@ -396,6 +404,9 @@ pub(super) fn normalize_request_referrer(scope: &mut v8::PinScope<'_, '_>, input
|
||||
}
|
||||
|
||||
fn current_request_context_origin(scope: &mut v8::PinScope<'_, '_>) -> Option<WebOrigin> {
|
||||
if let Some(settings) = WindowEnvironmentSettings::for_current_realm(scope) {
|
||||
return Some(settings.origin().clone());
|
||||
}
|
||||
if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
let owner = effective_subresource_request_owner(scope, host);
|
||||
|
||||
@@ -417,12 +417,18 @@ impl ScriptVm {
|
||||
context.runtime_observable_context_token,
|
||||
);
|
||||
let context_ptr = &context.context as *const v8::Global<v8::Context>;
|
||||
let reuses_window_proxy = self
|
||||
._context_host
|
||||
.borrow()
|
||||
.child_window_proxy_frame_is_current(child_handle, &context.frame_id);
|
||||
self.renderer_document_isolate
|
||||
.with_entered_renderer_document_isolate(|isolate| {
|
||||
let scope = pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = unsafe { v8::Local::new(scope, &*context_ptr) };
|
||||
context.detach_global();
|
||||
if reuses_window_proxy {
|
||||
context.detach_global();
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
// Cancellation can synchronously install a successor realm or
|
||||
@@ -544,11 +550,11 @@ impl ScriptVm {
|
||||
let mut contexts = self.prebootstrapped_child_default_contexts.borrow_mut();
|
||||
stale_prebootstrapped_handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| contexts.remove(&handle))
|
||||
.filter_map(|handle| contexts.remove(&handle).map(|context| (handle, context)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if !stale_prebootstrapped_contexts.is_empty() {
|
||||
for context in &stale_prebootstrapped_contexts {
|
||||
for (_, context) in &stale_prebootstrapped_contexts {
|
||||
self.cancel_history_traversals_for_retiring_window(
|
||||
crate::native_bridge::WindowExecutionContextOwner::Frame(
|
||||
context.local_window_id,
|
||||
@@ -557,19 +563,25 @@ impl ScriptVm {
|
||||
}
|
||||
{
|
||||
let mut host = self._context_host.borrow_mut();
|
||||
for context in &stale_prebootstrapped_contexts {
|
||||
for (_, context) in &stale_prebootstrapped_contexts {
|
||||
host.retire_window_execution_contexts_for_context_token(
|
||||
context.runtime_observable_context_token,
|
||||
);
|
||||
}
|
||||
}
|
||||
let context_host = self._context_host.clone();
|
||||
let _ = self
|
||||
.renderer_document_isolate
|
||||
.with_entered_renderer_document_isolate(|isolate| {
|
||||
let scope = pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
for context in &stale_prebootstrapped_contexts {
|
||||
v8::Local::new(scope, &context.context).detach_global();
|
||||
for (handle, context) in &stale_prebootstrapped_contexts {
|
||||
if context_host
|
||||
.borrow()
|
||||
.child_window_proxy_frame_is_current(*handle, &context.frame_id)
|
||||
{
|
||||
v8::Local::new(scope, &context.context).detach_global();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
@@ -692,37 +704,45 @@ impl ScriptVm {
|
||||
"child default context must retain its document-owned Inspector registration"
|
||||
);
|
||||
let context_host = self._context_host.clone();
|
||||
let detach_result = self
|
||||
let proxy_cleanup_result = self
|
||||
.renderer_document_isolate
|
||||
.with_entered_renderer_document_isolate(|isolate| {
|
||||
let scope = pin!(v8::HandleScope::new(isolate));
|
||||
let scope = &mut scope.init();
|
||||
let local_context = unsafe { v8::Local::new(scope, &*context_ptr) };
|
||||
local_context.detach_global();
|
||||
let host_ptr = (*context_host).as_ptr();
|
||||
let host = unsafe { &mut *host_ptr };
|
||||
if host.child_browsing_context_is_live(context.child_handle)
|
||||
&& !host.preserve_child_window_proxy_between_realms(scope, context.child_handle)
|
||||
{
|
||||
anyhow::bail!("failed to park the live child WindowProxy between realms");
|
||||
let reuses_window_proxy = host
|
||||
.child_window_proxy_frame_is_current(context.child_handle, &context.frame_id);
|
||||
// Navigation within the same frame reuses its WindowProxy.
|
||||
// Removal (including remove/reinsert of the same element)
|
||||
// creates a different browsing context. Keep the old proxy
|
||||
// attached to its retained Window so its constructors remain
|
||||
// callable after execution authority has been retired.
|
||||
if reuses_window_proxy {
|
||||
local_context.detach_global();
|
||||
if !host.preserve_child_window_proxy_between_realms(scope, context.child_handle)
|
||||
{
|
||||
anyhow::bail!("failed to park the live child WindowProxy between realms");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(reuses_window_proxy)
|
||||
});
|
||||
if let Err(error) = detach_result {
|
||||
tracing::warn!(
|
||||
match proxy_cleanup_result {
|
||||
Err(error) => tracing::warn!(
|
||||
%error,
|
||||
execution_context_id,
|
||||
child_handle = context.child_handle.index(),
|
||||
owner_realm_id = ?context.owner_realm_id,
|
||||
"failed to detach retired child WindowProxy global"
|
||||
);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"failed to finalize retired child WindowProxy"
|
||||
),
|
||||
Ok(reuses_window_proxy) => tracing::debug!(
|
||||
execution_context_id,
|
||||
child_handle = context.child_handle.index(),
|
||||
owner_realm_id = ?context.owner_realm_id,
|
||||
"detached retired child WindowProxy global for identity reuse"
|
||||
);
|
||||
reuses_window_proxy,
|
||||
"finalized retired child WindowProxy"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+193
@@ -600,6 +600,198 @@ fn request_relative_urls_follow_live_srcdoc_iframe_base_urls() {
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_retained_request_uses_child_document(vm: &mut ScriptVm, fallback_base: &str) {
|
||||
vm.exec(
|
||||
r#"
|
||||
globalThis.retainedChildDocument = requestBaseFrame.contentDocument;
|
||||
globalThis.RetainedChildRequest = requestBaseFrame.contentWindow.Request;
|
||||
retainedChildDocument.head.innerHTML = '<base href="https://fixture.test/child/">';
|
||||
"#,
|
||||
None,
|
||||
)
|
||||
.expect("child Document and constructors should be retained");
|
||||
|
||||
let fallback_base_url = Url::parse(fallback_base).unwrap();
|
||||
let allowed_referrer = fallback_base_url.join("/allowed-referrer").unwrap();
|
||||
let parent_item = fallback_base_url.join("/parent-base/item").unwrap();
|
||||
let assert_base = |vm: &mut ScriptVm, base: &str| {
|
||||
let result = vm
|
||||
.eval(&format!(
|
||||
r#"
|
||||
(() => {{
|
||||
const controller = new AbortController();
|
||||
controller.abort('retained-reason');
|
||||
const request = new RetainedChildRequest('item', {{signal: controller.signal}});
|
||||
return JSON.stringify({{
|
||||
urls: ['item', '../item?q#f', '?q', '#f'].map(input => new RetainedChildRequest(input).url),
|
||||
referrer: new RetainedChildRequest('item', {{referrer: 'referrer'}}).referrer,
|
||||
allowedReferrer: new RetainedChildRequest('item', {{referrer: {allowed_referrer}}}).referrer,
|
||||
signalAborted: request.signal.aborted,
|
||||
signalReason: request.signal.reason,
|
||||
parent: new Request('item').url
|
||||
}});
|
||||
}})()
|
||||
"#,
|
||||
allowed_referrer = serde_json::to_string(allowed_referrer.as_str()).unwrap(),
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("retained child Request at base {base}: {error}"));
|
||||
let base = Url::parse(base).unwrap();
|
||||
let relative_referrer = base.join("referrer").unwrap();
|
||||
let expected_referrer = if relative_referrer.origin() == allowed_referrer.origin() {
|
||||
relative_referrer.as_str()
|
||||
} else {
|
||||
"about:client"
|
||||
};
|
||||
let expected_urls =
|
||||
["item", "../item?q#f", "?q", "#f"].map(|input| base.join(input).unwrap().to_string());
|
||||
let actual: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||
assert_eq!(
|
||||
actual,
|
||||
serde_json::json!({
|
||||
"urls": expected_urls,
|
||||
"referrer": expected_referrer,
|
||||
"allowedReferrer": allowed_referrer.as_str(),
|
||||
"signalAborted": true,
|
||||
"signalReason": "retained-reason",
|
||||
"parent": parent_item.as_str(),
|
||||
}),
|
||||
"retained Document base {base}"
|
||||
);
|
||||
};
|
||||
|
||||
assert_base(vm, "https://fixture.test/child/");
|
||||
vm.exec("requestBaseFrame.remove();", None)
|
||||
.expect("iframe should detach");
|
||||
assert_base(vm, "https://fixture.test/child/");
|
||||
assert!(vm.live_child_default_runtime_realm_inventory().is_empty());
|
||||
assert_base(vm, "https://fixture.test/child/");
|
||||
|
||||
vm.exec(
|
||||
"retainedChildDocument.querySelector('base').href = 'https://fixture.test/changed/';",
|
||||
None,
|
||||
)
|
||||
.expect("retained Document base should remain mutable");
|
||||
assert_base(vm, "https://fixture.test/changed/");
|
||||
vm.exec(
|
||||
"retainedChildDocument.querySelector('base').remove();",
|
||||
None,
|
||||
)
|
||||
.expect("retained Document base should be removable");
|
||||
assert_base(vm, fallback_base);
|
||||
|
||||
vm.exec(
|
||||
"requestBaseFrame.removeAttribute('src'); document.body.appendChild(requestBaseFrame);",
|
||||
None,
|
||||
)
|
||||
.expect("the same iframe element should reattach");
|
||||
vm.exec(
|
||||
"requestBaseFrame.contentDocument.head.innerHTML = '<base href=\"https://replacement.test/new/\">';",
|
||||
None,
|
||||
)
|
||||
.expect("replacement child Document should get its own base");
|
||||
assert_eq!(
|
||||
vm.eval("new requestBaseFrame.contentWindow.Request('item').url")
|
||||
.unwrap(),
|
||||
"https://replacement.test/new/item"
|
||||
);
|
||||
assert_base(vm, fallback_base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_retains_initial_about_blank_document_after_iframe_removal() {
|
||||
let mut vm = new_storage_test_vm("https://request-base.test/dir/page.html");
|
||||
install_request_base_parent_document(&mut vm);
|
||||
vm.exec("document.body.appendChild(requestBaseFrame);", None)
|
||||
.unwrap();
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert_retained_request_uses_child_document(&mut vm, "https://request-base.test/parent-base/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_retains_srcdoc_document_after_iframe_removal() {
|
||||
let mut vm = new_storage_test_vm("https://request-base.test/dir/page.html");
|
||||
install_request_base_parent_document(&mut vm);
|
||||
vm.exec(
|
||||
"requestBaseFrame.srcdoc = '<!doctype html><html><head></head><body></body></html>'; \
|
||||
document.body.appendChild(requestBaseFrame);",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert_retained_request_uses_child_document(&mut vm, "https://request-base.test/parent-base/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_retains_child_settings_when_iframe_handle_is_reused_before_cleanup() {
|
||||
let mut vm = new_storage_test_vm("https://request-base.test/dir/page.html");
|
||||
install_request_base_parent_document(&mut vm);
|
||||
vm.exec(
|
||||
r#"
|
||||
document.body.appendChild(requestBaseFrame);
|
||||
requestBaseFrame.contentDocument.head.innerHTML = '<base href="https://fixture.test/child/">';
|
||||
globalThis.RetainedChildRequest = requestBaseFrame.contentWindow.Request;
|
||||
requestBaseFrame.remove();
|
||||
document.body.appendChild(requestBaseFrame);
|
||||
requestBaseFrame.contentDocument.head.innerHTML = '<base href="https://replacement.test/new/">';
|
||||
"#,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
vm.live_child_default_runtime_realm_inventory();
|
||||
assert_eq!(
|
||||
vm.eval("new RetainedChildRequest('item').url + '|' + new requestBaseFrame.contentWindow.Request('item').url")
|
||||
.unwrap(),
|
||||
"https://fixture.test/child/item|https://replacement.test/new/item"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_retains_child_origin_after_iframe_removal() {
|
||||
let mut vm = new_storage_test_vm("https://request-base.test/dir/page.html");
|
||||
install_request_base_parent_document(&mut vm);
|
||||
vm.exec(
|
||||
r#"
|
||||
document.body.appendChild(requestBaseFrame);
|
||||
requestBaseFrame.contentDocument.head.innerHTML = '<base href="https://fixture.test/child/">';
|
||||
globalThis.RetainedChildRequest = requestBaseFrame.contentWindow.Request;
|
||||
requestBaseFrame.remove();
|
||||
"#,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(vm.live_child_default_runtime_realm_inventory().is_empty());
|
||||
{
|
||||
// Give the top fixture different settings without changing its URL.
|
||||
// Only the constructor is retained; no child Document/Window reference
|
||||
// or live owner registry is available to supply its original origin.
|
||||
let mut host = vm._context_host.borrow_mut();
|
||||
let loader = host.current_main_document_resource_loader().unwrap();
|
||||
let context = loader.fetch_context();
|
||||
host.retire_document_resource_loader(context.owner())
|
||||
.unwrap();
|
||||
host.register_committed_document_resource_loader(
|
||||
crate::network::context::DocumentFetchContext::new(
|
||||
context.owner(),
|
||||
context.document_url().clone(),
|
||||
context.base_url().clone(),
|
||||
"https://other-origin.test",
|
||||
),
|
||||
crate::network::context::DocumentResourceAuthoritySource::Inherited(loader),
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"JSON.stringify([
|
||||
new RetainedChildRequest('item').url,
|
||||
new RetainedChildRequest('item', {referrer: 'https://request-base.test/allowed'}).referrer,
|
||||
new RetainedChildRequest('item', {referrer: 'https://other-origin.test/rejected'}).referrer
|
||||
])"#,
|
||||
)
|
||||
.unwrap(),
|
||||
r#"["https://fixture.test/child/item","https://request-base.test/allowed","about:client"]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn request_relative_urls_follow_live_http_iframe_base_urls() {
|
||||
let (server_url, server) = spawn_lightweight_popup_response_html_server(
|
||||
@@ -653,5 +845,6 @@ async fn request_relative_urls_follow_live_http_iframe_base_urls() {
|
||||
child_url.as_str(),
|
||||
child_url.as_str(),
|
||||
);
|
||||
assert_retained_request_uses_child_document(&mut vm, child_url.as_str());
|
||||
server.await.expect("iframe response server should finish");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user