From 9c338eedf5bce89dcf708152e5dedbf02a140662 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 19:32:02 +0800 Subject: [PATCH] fix(events): suppress reentrant rejectionhandled events Extract an independently mergeable topic from wpt-misc-fix at a70a96f9d1e0573cc9721275fc03b78e4c35d3f1. Source commits: - 3fd12f80b72cd8e685d44db2af772c77c1e80b33 Validation: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast --- .../src/script_vm/runtime_bindings.rs | 8 ++++- .../tests/browser_api/promise_rejection.rs | 34 +++++++++++++++++++ .../src/worker/thread/dispatch.rs | 25 ++++++++------ .../src/worker/thread/tests/lifecycle.rs | 32 +++++++++++++++++ 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/moli-renderer-v8/src/script_vm/runtime_bindings.rs b/moli-renderer-v8/src/script_vm/runtime_bindings.rs index a447cf90a1..c28d77c91b 100644 --- a/moli-renderer-v8/src/script_vm/runtime_bindings.rs +++ b/moli-renderer-v8/src/script_vm/runtime_bindings.rs @@ -121,7 +121,6 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) .reason .as_ref() .map(|reason| v8::Local::new(scope, reason)); - remember_reported_promise_rejection(&state.reported, rejection.clone()); let outcome = dispatch_window_promise_rejection_event( scope, host_ptr, @@ -130,6 +129,13 @@ pub(super) fn flush_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) promise, reason, ); + // A handler added by the `unhandledrejection` listener must not + // synchronously turn this notification into `rejectionhandled`. + // Only promises that remain unhandled after dispatch belong in + // the outstanding reported-rejection set. + if !promise.has_handler() { + remember_reported_promise_rejection(&state.reported, rejection.clone()); + } if matches!(outcome, Ok(true)) { log_unhandled_promise_rejection(scope, reason); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs index 50705d4410..697fa54c67 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/promise_rejection.rs @@ -25,6 +25,40 @@ Promise.reject("main-owned"); ); } +#[test] +fn handler_added_during_unhandled_rejection_does_not_dispatch_rejectionhandled() { + let mut vm = new_storage_test_vm("https://handled-during-notification.test/"); + + vm.eval( + r#" +globalThis.__handledDuringNotificationEvents = []; +const reason = new Error("handled-during-notification"); +const rejected = Promise.reject(reason); +onunhandledrejection = event => { + __handledDuringNotificationEvents.push(event.type); + event.preventDefault(); + rejected.catch(value => { + __handledDuringNotificationEvents.push(value === reason ? "handler" : "wrong-reason"); + }); +}; +onrejectionhandled = event => { + __handledDuringNotificationEvents.push(event.type); +}; +"#, + ) + .expect("rejection handled during notification setup should evaluate"); + vm.eval("0") + .expect("unhandled rejection notification checkpoint should evaluate"); + vm.eval("0") + .expect("rejection handler reaction checkpoint should evaluate"); + + assert_eq!( + vm.eval("JSON.stringify(__handledDuringNotificationEvents)") + .expect("rejection handled during notification result should evaluate"), + r#"["unhandledrejection","handler"]"# + ); +} + #[test] fn universal_isolated_world_rejection_uses_its_registry_backed_realm() { let mut vm = new_storage_test_vm("https://isolated-promise-rejection.test/"); diff --git a/moli-renderer-v8/src/worker/thread/dispatch.rs b/moli-renderer-v8/src/worker/thread/dispatch.rs index 809ba72103..4969248827 100644 --- a/moli-renderer-v8/src/worker/thread/dispatch.rs +++ b/moli-renderer-v8/src/worker/thread/dispatch.rs @@ -500,10 +500,6 @@ fn flush_pending_worker_promise_rejections(scope: &mut v8::PinScope<'_, '_>) { .reason .as_ref() .map(|reason| v8::Local::new(scope, reason)); - remember_reported_worker_promise_rejection( - &reported_unhandled_rejections, - rejection.clone(), - ); let allows_default = dispatch_worker_promise_rejection_event( scope, "unhandledrejection", @@ -512,6 +508,14 @@ fn flush_pending_worker_promise_rejections(scope: &mut v8::PinScope<'_, '_>) { &parent_tx, &script_url, ); + // Do not expose a `rejectionhandled` transition when the handler was + // attached by the `unhandledrejection` listener itself. + if !promise.has_handler() { + remember_reported_worker_promise_rejection( + &reported_unhandled_rejections, + rejection.clone(), + ); + } if allows_default { log_unhandled_promise_rejection(scope, reason); } @@ -582,14 +586,13 @@ unsafe extern "C" fn worker_promise_reject_callback(message: v8::PromiseRejectMe }) .map(|index| reported.swap_remove(index)) }; - let reason = reported + let Some(rejection) = reported else { + return; + }; + let reason = rejection + .reason .as_ref() - .and_then(|rejection| { - rejection - .reason - .as_ref() - .map(|reason| v8::Local::new(scope, reason)) - }) + .map(|reason| v8::Local::new(scope, reason)) .or_else(|| message.get_value()); let _ = dispatch_worker_promise_rejection_event( scope, diff --git a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs index 4c5a0f02eb..8f6763d7da 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lifecycle.rs @@ -8661,6 +8661,38 @@ async fn worker_global_rejectionhandled_event_dispatches_for_late_handler() { ); } +#[tokio::test] +async fn worker_handler_added_during_unhandled_rejection_suppresses_rejectionhandled() { + ensure_v8(); + let mut handle = spawn_worker( + r#" + const events = []; + const rejected = Promise.reject("handled-during-worker-notification"); + onunhandledrejection = event => { + event.preventDefault(); + events.push(event.type); + rejected.catch(reason => { + events.push(reason === "handled-during-worker-notification" + ? "handler" + : "wrong-reason"); + }); + setTimeout(() => { + postMessage(events); + close(); + }, 0); + }; + onrejectionhandled = event => events.push(event.type); + "# + .into(), + "test://worker_handled_during_unhandledrejection".into(), + ); + + assert_eq!( + recv_post_json(&mut handle).await, + r#"["unhandledrejection","handler"]"# + ); +} + #[tokio::test] async fn worker_unhandledrejection_notification_allows_one_message_port_turn() { ensure_v8();