fix(scripts): skip execution after cross-document adoption

Check each prepared script's node Document before executing or reporting a
source failure, while settling its original ordering and load-delay ownership.
Apply the check to main-document classic/module and child-document script work.

Keep terminal events for scripts adopted during execution, and allow existing
top-level-await evaluations to continue. Cover removal, adoption, moving back,
source failures, child lifecycle completion, and adoption after evaluation starts.
This commit is contained in:
ldm0
2026-09-23 09:05:13 +08:00
parent 904efe05f0
commit ddee185e06
11 changed files with 443 additions and 19 deletions
@@ -425,6 +425,12 @@ impl JsContextHost {
if current_realm_id != Some(realm_id) || work.realm_id.is_some_and(|id| id != realm_id) {
return None;
}
let document = self
.frame_owner_current_child_snapshot(work.child_handle)?
.document_handle;
if self.dom_host().owner_document_handle(work.script_handle) != Some(document) {
return None;
}
let execution = match &work.source_result {
Ok(source) => FrameDocumentExternalClassicScriptExecution::script_job(
self.frame_owner_store
@@ -479,11 +485,8 @@ impl JsContextHost {
{
return false;
}
if self.dom_host().owner_document_handle(event.script_handle)
!= Some(snapshot.document_handle)
{
return false;
}
// Adoption is checked before execution. A script can move itself while
// running and must still receive its terminal event on the same element.
let _ = self.child_browsing_context_document_wrapper(scope, event.child_handle);
let host_ptr = self as *mut JsContextHost;
let Some(target) =
@@ -1910,7 +1913,8 @@ impl JsContextHost {
&mut self,
failed: FrameDocumentClassicSourceFailureWork,
) -> FrameDocumentClassicSourceFailureReportApplication {
let (target, _failure, script_element_event) = failed.into_parts();
let script_handle = failed.script_handle();
let (target, _failure, mut script_element_event) = failed.into_parts();
let child_handle = target.child_handle();
let owner = target.task_owner().document_owner();
let runner_owner_current = self.frame_parser_classic_scripts.has_runner(owner);
@@ -1949,6 +1953,15 @@ impl JsContextHost {
FrameDocumentClassicSourceFailureReportSkipReason::StaleRealm,
);
}
if self
.frame_owner_current_child_snapshot(child_handle)
.is_none_or(|snapshot| {
self.dom_host().owner_document_handle(script_handle)
!= Some(snapshot.document_handle)
})
{
script_element_event = None;
}
FrameDocumentClassicSourceFailureReportApplication::completed(
FrameDocumentClassicScriptCompletionAction::new(
FrameDocumentClassicScriptCompletionTarget::new(
@@ -21,6 +21,7 @@ use super::{
PageOwnedScriptFailureClassification, PageVm,
complete_page_owned_prepared_script_execution_failure_body,
complete_prepared_script_execution_failure_report_with_activity,
complete_prepared_script_execution_success_with_activity,
execute_prepared_script_on_script_execution_lane,
};
@@ -112,6 +113,18 @@ impl PageOwnedDocumentScriptHooks for MainPageOwnedDocumentScriptHooks<'_, '_> {
failure: PageOwnedDocumentScriptSourceFailure,
runtime_script_claim: Option<DynamicScriptPageTaskClaim>,
) -> PageOwnedDocumentScriptBodyExecution {
if self.page_vm.vm().prepared_script_changed_documents(&script) {
if let Some(claim) = runtime_script_claim {
self.page_vm
.vm_mut()
.cancel_claimed_runtime_owned_script_load_delay_body(claim, &script);
}
return complete_prepared_script_execution_success_with_activity(
script,
crate::script_vm::PreparedScriptBodyActivity::NotEntered,
)
.into_body_execution();
}
let (error, module_failure_policy, error_value) = failure.into_parts();
if let Some(claim) = runtime_script_claim {
let terminal_activity = self
@@ -372,11 +372,19 @@ impl ParserClassicDocumentScriptExecutionHooks
.document_runtime
.mark_script_already_started_by_node_id(failure.script.node_id);
let script_handle = DomHandle::new(failure.script.node_id.index());
let event = owner
let event = if owner
.page_vm
.vm()
.document_runtime
.plan_parser_owned_script_event_task(ScriptEventKind::Error, script_handle);
.prepared_script_changed_documents(&failure.script)
{
None
} else {
owner
.page_vm
.vm()
.document_runtime
.plan_parser_owned_script_event_task(ScriptEventKind::Error, script_handle)
};
tracing::debug!(
expected_owner = ?failure.owner,
script_node_id = ?failure.script.node_id,
@@ -18,7 +18,7 @@ use crate::script_vm::{
ParserModuleEvaluationSettlement, ParserModuleTerminalDisposition,
ParserOwnedModuleSuccessTerminal, PreparedModuleSuccessSettlement, PreparedScriptBodyActivity,
};
use crate::types::ScriptRun;
use crate::types::{ScriptRun, ScriptSkipReason};
use super::parser_owned_document_script::MainParserModuleExecution;
use super::parser_task_completion::{
@@ -62,6 +62,48 @@ impl ModuleScriptCompletionOutcome {
}
}
impl PageVm {
fn complete_moved_module_script(
&mut self,
mut continuation: ModuleScriptContinuation,
terminal_disposition: ParserModuleTerminalDisposition,
) -> ModuleScriptCompletionOutcome {
let load_delay = continuation.take_main_document_load_delay_binding();
let dynamic_owner = continuation.dynamic_script_owner_id();
let script = continuation.script;
if let Some(owner_id) = dynamic_owner {
self.vm_mut()
.cancel_runtime_owned_script_load_delay_body(&script, owner_id);
}
if let Some(binding) = load_delay {
let _ = self
.vm_mut()
.enqueue_main_document_script_load_delay_settlement_best_effort(&script, binding);
}
let run = ScriptRun::skipped(
script.node_id,
script.kind,
script.mode,
script.source_kind,
script.url,
ScriptSkipReason::NotInMainDocument,
);
if terminal_disposition == ParserModuleTerminalDisposition::ReturnToSelectedParserTask {
ModuleScriptCompletionOutcome::TerminalForSelectedTask {
run,
terminal: ParserOwnedModuleSuccessTerminal::new(
ParserModuleEvaluationSettlement::Completed,
None,
PreparedScriptBodyActivity::NotEntered,
),
}
} else {
ModuleScriptCompletionOutcome::Completed {
run,
task_effect: MainParserContinuationTaskEffect::NotApplied,
}
}
}
pub(super) fn complete_module_script_failure_for_runner(
&mut self,
script_continuation: ModuleScriptContinuation,
@@ -88,6 +130,13 @@ impl PageVm {
prepared_activity: PreparedScriptBodyActivity,
terminal_disposition: ParserModuleTerminalDisposition,
) -> ModuleScriptCompletionOutcome {
if matches!(prepared_activity, PreparedScriptBodyActivity::NotEntered)
&& self
.vm()
.prepared_script_changed_documents(&script_continuation.script)
{
return self.complete_moved_module_script(script_continuation, terminal_disposition);
}
let parser_owner = script_continuation
.parser_document_owner()
.map(MainParserDocumentOwner::task_owner);
@@ -151,6 +200,16 @@ impl PageVm {
mut script_continuation: ModuleScriptContinuation,
terminal_disposition: ParserModuleTerminalDisposition,
) -> Result<MainParserModuleExecution> {
if self
.vm()
.prepared_script_changed_documents(&script_continuation.script)
{
let (run, execution) = self
.complete_moved_module_script(script_continuation, terminal_disposition)
.into_parser_execution();
self.report.runs.push(run);
return Ok(execution);
}
let Some(graph) = script_continuation.completed_graph.take() else {
self.handle_module_script_continuation_graph_advance(
ModuleScriptContinuationGraphAdvance::Ready(Box::new(script_continuation)),
@@ -51,6 +51,69 @@ pub(super) async fn queue_real_child_module_terminal(
.context("real terminal producer should retain an exact child realm")
}
#[tokio::test(flavor = "current_thread")]
async fn child_parser_modules_check_preparation_document_before_execution() {
run_page_vm_async_test(async {
for fetch_fails in [false, true] {
for movement in ["before", "return", "during"] {
let source = if movement == "during" {
"parent.__adoptionEvents.push('execute'); parent.document.body.append(parent.__movedModule);"
} else {
"parent.__adoptionEvents.push('execute');"
};
let (base_url, server) = spawn_path_response_http_server(vec![(
"/adopted.mjs",
if fetch_fails { "HTTP/1.1 404 Not Found" } else { "HTTP/1.1 200 OK" },
source.to_owned(), Duration::ZERO,
)]).await;
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
let (mut page_vm, mut resource_source, mut owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(
&loader, Url::parse(&format!("{base_url}/page"))?,
);
queue_real_child_module_terminal(
&mut page_vm, &mut resource_source, &mut owner_wake_rx,
"adopted-child", &format!("{base_url}/adopted.mjs"),
).await?;
page_vm.vm_mut().eval(r#"
globalThis.__adoptionEvents = [];
globalThis.__childDocument = document.getElementById('adopted-child').contentDocument;
globalThis.__movedModule = __childDocument.querySelector('script');
__movedModule.onload = () => __adoptionEvents.push('load');
__movedModule.onerror = () => __adoptionEvents.push('error');
"#)?;
if movement != "during" {
page_vm.vm_mut().eval("document.body.append(__movedModule)")?;
if movement == "return" {
page_vm.vm_mut().eval("__childDocument.body.append(__movedModule)")?;
}
}
run_expected_child_module_script_terminal_turn(
&mut page_vm, "adopted child module source terminal",
).await;
assert!(page_vm.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::ChildDocumentScriptReady, &loader,
).await?);
let expected = if movement == "before" { "" } else if fetch_fails { "error" } else { "execute|load" };
assert_eq!(page_vm.vm_mut().eval("__adoptionEvents.join('|')")?, expected,
"fetch_fails={fetch_fails}, movement={movement}");
for source in [
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::DocumentLifecycle,
ChildFrameSemanticTurnKind::HostLoad,
] {
run_expected_child_frame_task_source_after_realm_prerequisite_for_wait(
&mut page_vm, source, "lifecycle after adopted module",
).await;
}
assert_eq!(page_vm.vm_mut().eval("__childDocument.readyState")?, "complete");
server.await?;
}
}
Ok::<_, anyhow::Error>(())
}).await.unwrap();
}
#[tokio::test(flavor = "current_thread")]
async fn child_parser_module_roots_compile_binary_wasm_responses() {
run_page_vm_async_test(async {
@@ -17,6 +17,182 @@ fn bound_parser_module(page_vm: &mut PageVm, position: u32, url: Url) -> Prepare
script
}
#[tokio::test(flavor = "current_thread")]
async fn prepared_main_classic_scripts_check_their_node_document() {
run_page_vm_async_test(async {
for (mutation, executes) in [
("__script.remove()", true),
("__other.body.append(__script)", false),
(
"__other.body.append(__script); document.body.append(__script)",
true,
),
] {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/adopted-classic.html").unwrap(),
);
let mut script = bound_parser_module(
&mut page_vm,
9301,
Url::parse("https://example.com/adopted.js").unwrap(),
);
script.kind = ScriptKind::Classic;
script.mode = ScriptMode::Normal;
script.source =
crate::planning::ScriptSource::Loaded("++globalThis.__runs;".to_owned());
page_vm
.vm_mut()
.eval(&format!(
r#"
globalThis.__runs = 0;
globalThis.__script = document.querySelector('script');
globalThis.__other = document.implementation.createHTMLDocument('');
{mutation};
"#
))
.unwrap();
let outcome = page_vm
.vm_mut()
.run_prepared_script(&loader, &script, None)
.await
.unwrap();
assert_eq!(
matches!(
outcome,
crate::script_vm::PreparedScriptExecutionOutcome::Completed(_)
),
executes,
"{mutation}",
);
assert_eq!(
page_vm.vm_mut().eval("__runs").unwrap(),
if executes { "1" } else { "0" }
);
}
})
.await;
}
#[tokio::test(flavor = "current_thread")]
async fn adopted_main_module_graphs_settle_without_execution_or_events() {
run_page_vm_async_test(async {
for fetch_fails in [false, true] {
for move_back in [false, true] {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/adopted-module.html").unwrap(),
);
let url = Url::parse("https://example.com/adopted.mjs").unwrap();
let script = bound_parser_module(&mut page_vm, 9302, url.clone());
page_vm
.vm_mut()
.eval(
r#"
globalThis.__counts = [0, 0, 0, 0];
globalThis.__script = document.querySelector('script');
__script.onload = () => ++__counts[1];
__script.onerror = () => ++__counts[2];
addEventListener('error', event => { ++__counts[3]; event.preventDefault(); });
"#,
)
.unwrap();
let work = install_parser_module_defer_work(&mut page_vm, script);
page_vm
.execute_post_parse_page_owned_task_on_named_owner_lane(&loader, work)
.await
.unwrap();
page_vm
.vm_mut()
.eval("document.implementation.createHTMLDocument('').body.append(__script)")
.unwrap();
if move_back {
page_vm
.vm_mut()
.eval("document.body.append(__script)")
.unwrap();
}
if fetch_fails {
enqueue_parser_owned_module_script_fetch_error_for_test(
&mut page_vm,
0,
&url,
"HTTP 404",
);
} else {
enqueue_parser_owned_module_script_fetch_completion_for_test(
&mut page_vm,
0,
&url,
"++globalThis.__counts[0];",
);
}
assert!(
run_next_main_module_fetch_terminal_for_test(&mut page_vm)
.unwrap()
.is_some()
);
run_and_finish_ready_parser_deferred_task_for_test(
&mut page_vm,
&loader,
"adopted module terminal",
)
.await;
run_parser_module_completion_turns_for_test(
&mut page_vm,
&loader,
0,
"adopted module terminal",
)
.await;
let expected = if !move_back {
"[0,0,0,0]"
} else if fetch_fails {
"[0,0,1,0]"
} else {
"[1,1,0,0]"
};
assert_eq!(
page_vm.vm_mut().eval("JSON.stringify(__counts)").unwrap(),
expected
);
}
}
})
.await;
}
#[tokio::test(flavor = "current_thread")]
async fn module_evaluation_continues_if_element_is_adopted_after_it_starts() {
run_page_vm_async_test(async {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader, Vec::new(), Url::parse("https://example.com/adopted-tla.html").unwrap(),
);
let url = Url::parse("https://example.com/adopted-tla.mjs").unwrap();
let script = bound_parser_module(&mut page_vm, 9303, url.clone());
page_vm.vm_mut().eval("globalThis.__runs = 0; globalThis.__script = document.querySelector('script');").unwrap();
let work = install_parser_module_defer_work(&mut page_vm, script);
page_vm.execute_post_parse_page_owned_task_on_named_owner_lane(&loader, work).await.unwrap();
enqueue_parser_owned_module_script_fetch_completion_for_test(
&mut page_vm, 0, &url,
"await new Promise(resolve => { globalThis.__resumeModule = resolve; }); ++globalThis.__runs;",
);
assert!(run_next_main_module_fetch_terminal_for_test(&mut page_vm).unwrap().is_some());
run_ready_parser_deferred_body_for_test(&mut page_vm, &loader, "module before adoption").await;
assert_eq!(page_vm.vm_mut().eval("__runs").unwrap(), "0");
page_vm.vm_mut().eval("document.implementation.createHTMLDocument('').body.append(__script); __resumeModule();").unwrap();
run_parser_module_completion_turns_for_test(&mut page_vm, &loader, 1, "module adopted after evaluation start").await;
assert_eq!(page_vm.vm_mut().eval("__runs").unwrap(), "1");
}).await;
}
#[tokio::test(flavor = "current_thread")]
async fn parser_module_error_reporting_preserves_inline_source_origin() {
run_page_vm_async_test(async {
+22 -2
View File
@@ -5311,6 +5311,19 @@ queueMicrotask(() => window.__mainParserClassicCheckpointEvents.push('script-mic
#[test]
fn main_parser_blocking_source_failure_uses_the_shared_completion_event_flow() {
for (movement, expected) in [
("", "error:true|error-microtask"),
("foreign.body.append(script);", ""),
(
"foreign.body.append(script); document.body.append(script);",
"error:true|error-microtask",
),
] {
assert_main_parser_blocking_source_failure_events(movement, expected);
}
}
fn assert_main_parser_blocking_source_failure_events(movement: &str, expected: &str) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
@@ -5350,6 +5363,13 @@ queueMicrotask(() => window.__mainParserClassicCheckpointEvents.push('script-mic
.vm_mut()
.eval("window.__mainParserClassicFailureEvents = []")
.expect("source failure event state should initialize");
page_vm.vm_mut().eval(&format!(r#"
(() => {{
const script = document.querySelector('script');
const foreign = document.implementation.createHTMLDocument('');
{movement}
}})()
"#)).expect("source failure script movement should complete");
let task_owner = page_vm
.vm()
@@ -5405,8 +5425,8 @@ queueMicrotask(() => window.__mainParserClassicCheckpointEvents.push('script-mic
.vm_mut()
.eval("__mainParserClassicFailureEvents.join('|')")
.expect("source failure events should evaluate"),
"error:true|error-microtask",
"source failure must dispatch error with currentScript cleared and settle its reactions before parser continuation"
expected,
"source failure must check the preparation Document before dispatching error and settling reactions"
);
}));
}
@@ -241,14 +241,17 @@ impl ParserClassicDocumentScriptExecutionHooks
let script_handle = failure.script_handle();
let script_url = failure.script_url().clone();
let error = failure.error().to_owned();
let event = owner
.page_vm
.vm()
.document_runtime
.plan_parser_owned_script_event_task(
let runtime = &owner.page_vm.vm().document_runtime;
let event = if runtime.dom_host().owner_document_handle(script_handle)
== Some(runtime.document_handle())
{
runtime.plan_parser_owned_script_event_task(
crate::host::ScriptEventKind::Error,
script_handle,
);
)
} else {
None
};
if let Some((script, _, source_network_result)) = failure.into_execution_failure_parts()
&& let Some(network_result) = source_network_result.as_deref()
{
@@ -1295,7 +1295,12 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
.set_script_already_started(handle, true);
let _ = self.scheduler.grant_parse_visible_reevaluation_credit();
} else if failure.is_external_source_failure() {
page_vm.vm_mut().queue_script_preparation_error(handle)?;
let runtime = &page_vm.vm().document_runtime;
if runtime.dom_host().owner_document_handle(handle)
== Some(runtime.document_handle())
{
page_vm.vm_mut().queue_script_preparation_error(handle)?;
}
} else {
page_vm
.vm_mut()
+17
View File
@@ -6542,7 +6542,24 @@ impl ScriptVm {
handle
}
// Main-document work retains its preparation owner until it is ready. Check
// adoption when entering execution, without cancelling its fetch or order slot.
pub(crate) fn prepared_script_changed_documents(&self, script: &PreparedScript) -> bool {
let node = script
.host_script_handle
.as_deref()
.and_then(|handle| self.document_runtime.resolve_host_script_handle(handle))
.unwrap_or(script.node_id);
self.document_runtime
.dom_host()
.owner_document_handle(node)
.is_some_and(|document| document != self.document_runtime.document_handle())
}
fn prepared_script_is_live_for_execution(&mut self, script: &PreparedScript) -> bool {
if self.prepared_script_changed_documents(script) {
return false;
}
let Some(handle) = script.host_script_handle.as_deref() else {
let allow_missing_handle = script.kind == ScriptKind::Classic
&& script.source_kind == ScriptSourceKind::Inline
@@ -44,6 +44,14 @@ impl FrameModuleScriptDocumentScriptHooks for ChildModuleScriptExecutionOwner<'_
work.script().mode,
work.pending_script_id().key(),
work.load_delay_token(),
)?;
self.check_script_preparation_document(
work.owner(),
work.realm_id(),
work.script_handle(),
work.script().mode,
work.pending_script_id().key(),
work.load_delay_token(),
)
}
@@ -57,6 +65,14 @@ impl FrameModuleScriptDocumentScriptHooks for ChildModuleScriptExecutionOwner<'_
work.script().mode,
work.pending_script_id().key(),
work.load_delay_token(),
)?;
self.check_script_preparation_document(
work.owner(),
work.realm_id(),
work.script_handle(),
work.script().mode,
work.pending_script_id().key(),
work.load_delay_token(),
)
}
@@ -262,6 +278,37 @@ impl FrameModuleScriptDocumentScriptHooks for ChildModuleScriptExecutionOwner<'_
}
impl ChildModuleScriptExecutionOwner<'_> {
fn check_script_preparation_document(
&mut self,
owner: FrameDocumentTaskOwner,
realm_id: FrameRealmId,
script_handle: DomHandle,
mode: ScriptMode,
pending_script_key: crate::document_script_scheduler::ParserPendingScriptKey,
load_delay_token: crate::frame_owner_model::DocumentLoadDelayTokenId,
) -> std::result::Result<(), DocumentScriptExecutionOutcome> {
let current = {
let host = self.vm._context_host.borrow();
host.frame_owner_current_child_snapshot_for_realm(realm_id)
.is_some_and(|snapshot| {
host.dom_host().owner_document_handle(script_handle)
== Some(snapshot.document_handle)
})
};
if current {
return Ok(());
}
let order_released =
self.complete_parser_deferred_module_script(owner, realm_id, mode, pending_script_key);
let released = self.release_module_script_load_delay(owner, mode, load_delay_token);
if order_released || released {
self.queue_lifecycle_followups_for_module_work(owner, realm_id);
Err(DocumentScriptExecutionOutcome::Progressed)
} else {
Err(DocumentScriptExecutionOutcome::NoProgress)
}
}
fn complete_parser_deferred_module_script(
&mut self,
owner: FrameDocumentTaskOwner,