From d588464b41e1305529b66b589087593c61b4fa54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 13 Jun 2026 02:11:06 +0200 Subject: [PATCH 1/3] fix(events): errorMonitor in the linked EventEmitter twin + handle-band guards (#4633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings node-suite/events parity from 67/69 to 69/69. The issue's 12 listed failures had mostly been fixed by intervening work; two remained: 1. errors/error-monitor: the perry-stdlib events twin implements events.errorMonitor dispatch, but the implementation that actually links into compiled binaries is perry-ext-events — which had none. Port dispatch_error_monitor (monitor listeners observe every 'error' emit before regular listeners, never count as handling) into ext-events' emit and emit0. 2. on/async-iterator-abort: SIGSEGV. events.on(emitter, name, { signal }) target validation passed the EventEmitter HANDLE (0x38000..0x40000 band) into is_event_target and object_ptr_from_value, whose 0x10000 floors let it through to the GcHeader probe at handle-8 — unmapped memory. Raise both floors to the runtime-wide 0x100000 handle boundary. The post-fix test output matches Node; the remaining process-exit hang after signal abort is pre-existing across the events abort family and filed as #5056. --- crates/perry-ext-events/src/lib.rs | 39 +++++++++++++++++++ crates/perry-runtime/src/event_target.rs | 7 +++- .../src/node_stream_readwrite.rs | 7 +++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index 7c33fe8f14..3638f0412c 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -1013,6 +1013,43 @@ unsafe fn collect_emit_args(args_ptr: *const ArrayHeader) -> Vec { args } +/// String key under which a listener registered via the `events.errorMonitor` +/// symbol lands: `event_name_from_bits` stringifies symbol event names, and +/// `Symbol.for("events.errorMonitor")` renders as this. Mirrors the stdlib +/// twin's constant so the two implementations stay behaviorally identical. +const ERROR_MONITOR_EVENT_NAME: &str = "Symbol(events.errorMonitor)"; + +/// Node's `events.errorMonitor` semantics (#4633): listeners installed under +/// the monitor symbol observe every `'error'` emit BEFORE the regular +/// `'error'` listeners run, without counting as error handling - an +/// unhandled `'error'` still throws after the monitor fires. Mirrors +/// `dispatch_error_monitor` in perry-stdlib's events twin. +unsafe fn dispatch_error_monitor( + emitter: &mut EventEmitterHandle, + handle: Handle, + arg: Option, +) { + let snapshot: Vec = match emitter.events.get(ERROR_MONITOR_EVENT_NAME) { + Some(v) if !v.is_empty() => v.clone(), + _ => return, + }; + if snapshot.iter().any(|l| l.once) { + if let Some(v) = emitter.events.get_mut(ERROR_MONITOR_EVENT_NAME) { + v.retain(|l| !l.once); + } + emitter.prune_event_if_empty(ERROR_MONITOR_EVENT_NAME); + } + for l in snapshot { + if l.callback != 0 { + let args: &[f64] = match arg.as_ref() { + Some(a) => std::slice::from_ref(a), + None => &[], + }; + let _ = call_emitter_listener(handle, l.callback, args); + } + } +} + unsafe fn call_emitter_listener(handle: Handle, callback: i64, args: &[f64]) -> f64 { let receiver = nanbox_pointer_bits(handle); let callback_value = nanbox_pointer_bits(callback); @@ -1169,6 +1206,7 @@ pub unsafe extern "C" fn js_event_emitter_emit( let first_arg = first_arg_or_undefined(args_ptr); let emitted_args = collect_emit_args(args_ptr); if event_name == "error" { + dispatch_error_monitor(emitter, handle, Some(first_arg)); let has_error_once = emitter .pending_once_promises .get("error") @@ -1249,6 +1287,7 @@ pub unsafe extern "C" fn js_event_emitter_emit0(handle: Handle, event_bits: i64) let empty_args = js_array_alloc(0); if event_name == "error" { let error_value = undefined_value(); + dispatch_error_monitor(emitter, handle, None); let has_error_once = emitter .pending_once_promises .get("error") diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index fb3019cd3d..4120f0ecbf 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -388,7 +388,12 @@ unsafe fn is_event_target(target: *const ObjectHeader) -> bool { if target.is_null() { return false; } - if (target as usize) < crate::gc::GC_HEADER_SIZE + 0x10000 { + // Handle-based receivers (EventEmitter ids live at 0x38000..0x40000, + // widget/stream handles lower) are small integers, not heap pointers — + // the runtime-wide convention is "below 0x100000 = handle". Probing the + // GcHeader at handle-8 read unmapped memory and SIGSEGV'd when + // events.on(emitter, ...) validated its target (#4633). + if (target as usize) < crate::gc::GC_HEADER_SIZE + 0x100000 { return false; } let gc_header = diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index f0ba300899..f99556c17c 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -76,7 +76,12 @@ pub(super) fn string_value_eq(value: f64, expected: &[u8]) -> bool { pub(super) fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { let raw = raw_ptr_from_value(value); - if raw < 0x10000 || crate::buffer::is_registered_buffer(raw) { + // Below 0x100000 is the handle band (EventEmitter ids sit at + // 0x38000..0x40000, widget/stream handles lower) — never a heap object. + // The old 0x10000 floor let an EventEmitter handle through to the + // GcHeader probe at raw-8, which is unmapped memory (#4633 SIGSEGV in + // events.on(emitter, name, { signal }) target validation). + if raw < 0x100000 || crate::buffer::is_registered_buffer(raw) { return None; } unsafe { From a0cc25ad219631245f8f1cb0025bf59e687a8bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 13 Jun 2026 03:37:38 +0200 Subject: [PATCH 2/3] style(ext-events): split errorMonitor dispatch into a sibling module (2000-line cap) --- crates/perry-ext-events/src/error_monitor.rs | 41 ++++++++++++++++++++ crates/perry-ext-events/src/lib.rs | 39 +------------------ 2 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 crates/perry-ext-events/src/error_monitor.rs diff --git a/crates/perry-ext-events/src/error_monitor.rs b/crates/perry-ext-events/src/error_monitor.rs new file mode 100644 index 0000000000..4360b19a92 --- /dev/null +++ b/crates/perry-ext-events/src/error_monitor.rs @@ -0,0 +1,41 @@ +//! `events.errorMonitor` dispatch for the ext-events EventEmitter twin +//! (#4633). Split out of `lib.rs` to keep it under the 2000-line cap. + +use super::*; + +/// String key under which a listener registered via the `events.errorMonitor` +/// symbol lands: `event_name_from_bits` stringifies symbol event names, and +/// `Symbol.for("events.errorMonitor")` renders as this. Mirrors the stdlib +/// twin's constant so the two implementations stay behaviorally identical. +pub(super) const ERROR_MONITOR_EVENT_NAME: &str = "Symbol(events.errorMonitor)"; + +/// Node's `events.errorMonitor` semantics (#4633): listeners installed under +/// the monitor symbol observe every `'error'` emit BEFORE the regular +/// `'error'` listeners run, without counting as error handling - an +/// unhandled `'error'` still throws after the monitor fires. Mirrors +/// `dispatch_error_monitor` in perry-stdlib's events twin. +pub(super) unsafe fn dispatch_error_monitor( + emitter: &mut EventEmitterHandle, + handle: Handle, + arg: Option, +) { + let snapshot: Vec = match emitter.events.get(ERROR_MONITOR_EVENT_NAME) { + Some(v) if !v.is_empty() => v.clone(), + _ => return, + }; + if snapshot.iter().any(|l| l.once) { + if let Some(v) = emitter.events.get_mut(ERROR_MONITOR_EVENT_NAME) { + v.retain(|l| !l.once); + } + emitter.prune_event_if_empty(ERROR_MONITOR_EVENT_NAME); + } + for l in snapshot { + if l.callback != 0 { + let args: &[f64] = match arg.as_ref() { + Some(a) => std::slice::from_ref(a), + None => &[], + }; + let _ = call_emitter_listener(handle, l.callback, args); + } + } +} diff --git a/crates/perry-ext-events/src/lib.rs b/crates/perry-ext-events/src/lib.rs index 3638f0412c..b6b7d2f64e 100644 --- a/crates/perry-ext-events/src/lib.rs +++ b/crates/perry-ext-events/src/lib.rs @@ -28,6 +28,8 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use std::sync::{Mutex, MutexGuard, Once, OnceLock}; +mod error_monitor; +use error_monitor::dispatch_error_monitor; mod max_listeners; mod messages; mod target_helpers; @@ -1013,43 +1015,6 @@ unsafe fn collect_emit_args(args_ptr: *const ArrayHeader) -> Vec { args } -/// String key under which a listener registered via the `events.errorMonitor` -/// symbol lands: `event_name_from_bits` stringifies symbol event names, and -/// `Symbol.for("events.errorMonitor")` renders as this. Mirrors the stdlib -/// twin's constant so the two implementations stay behaviorally identical. -const ERROR_MONITOR_EVENT_NAME: &str = "Symbol(events.errorMonitor)"; - -/// Node's `events.errorMonitor` semantics (#4633): listeners installed under -/// the monitor symbol observe every `'error'` emit BEFORE the regular -/// `'error'` listeners run, without counting as error handling - an -/// unhandled `'error'` still throws after the monitor fires. Mirrors -/// `dispatch_error_monitor` in perry-stdlib's events twin. -unsafe fn dispatch_error_monitor( - emitter: &mut EventEmitterHandle, - handle: Handle, - arg: Option, -) { - let snapshot: Vec = match emitter.events.get(ERROR_MONITOR_EVENT_NAME) { - Some(v) if !v.is_empty() => v.clone(), - _ => return, - }; - if snapshot.iter().any(|l| l.once) { - if let Some(v) = emitter.events.get_mut(ERROR_MONITOR_EVENT_NAME) { - v.retain(|l| !l.once); - } - emitter.prune_event_if_empty(ERROR_MONITOR_EVENT_NAME); - } - for l in snapshot { - if l.callback != 0 { - let args: &[f64] = match arg.as_ref() { - Some(a) => std::slice::from_ref(a), - None => &[], - }; - let _ = call_emitter_listener(handle, l.callback, args); - } - } -} - unsafe fn call_emitter_listener(handle: Handle, callback: i64, args: &[f64]) -> f64 { let receiver = nanbox_pointer_bits(handle); let callback_value = nanbox_pointer_bits(callback); From 5205e1e6343d1f772c73bbfa77175c695e886e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 13 Jun 2026 03:54:36 +0200 Subject: [PATCH 3/3] style: use addr_class::is_handle_band for the handle-band guards (lint gate) --- crates/perry-runtime/src/event_target.rs | 9 ++++----- crates/perry-runtime/src/node_stream_readwrite.rs | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/perry-runtime/src/event_target.rs b/crates/perry-runtime/src/event_target.rs index 4120f0ecbf..aa8f4e3c62 100644 --- a/crates/perry-runtime/src/event_target.rs +++ b/crates/perry-runtime/src/event_target.rs @@ -389,11 +389,10 @@ unsafe fn is_event_target(target: *const ObjectHeader) -> bool { return false; } // Handle-based receivers (EventEmitter ids live at 0x38000..0x40000, - // widget/stream handles lower) are small integers, not heap pointers — - // the runtime-wide convention is "below 0x100000 = handle". Probing the - // GcHeader at handle-8 read unmapped memory and SIGSEGV'd when - // events.on(emitter, ...) validated its target (#4633). - if (target as usize) < crate::gc::GC_HEADER_SIZE + 0x100000 { + // widget/stream handles lower) are small integers, not heap pointers. + // Probing the GcHeader at handle-8 read unmapped memory and SIGSEGV'd + // when events.on(emitter, ...) validated its target (#4633). + if crate::value::addr_class::is_handle_band(target as usize) { return false; } let gc_header = diff --git a/crates/perry-runtime/src/node_stream_readwrite.rs b/crates/perry-runtime/src/node_stream_readwrite.rs index f99556c17c..4b9f77acdf 100644 --- a/crates/perry-runtime/src/node_stream_readwrite.rs +++ b/crates/perry-runtime/src/node_stream_readwrite.rs @@ -76,12 +76,12 @@ pub(super) fn string_value_eq(value: f64, expected: &[u8]) -> bool { pub(super) fn object_ptr_from_value(value: f64) -> Option<*mut ObjectHeader> { let raw = raw_ptr_from_value(value); - // Below 0x100000 is the handle band (EventEmitter ids sit at - // 0x38000..0x40000, widget/stream handles lower) — never a heap object. - // The old 0x10000 floor let an EventEmitter handle through to the - // GcHeader probe at raw-8, which is unmapped memory (#4633 SIGSEGV in + // The handle band (EventEmitter ids sit at 0x38000..0x40000, + // widget/stream handles lower) is never a heap object. The old 0x10000 + // floor let an EventEmitter handle through to the GcHeader probe at + // raw-8, which is unmapped memory (#4633 SIGSEGV in // events.on(emitter, name, { signal }) target validation). - if raw < 0x100000 || crate::buffer::is_registered_buffer(raw) { + if crate::value::addr_class::is_handle_band(raw) || crate::buffer::is_registered_buffer(raw) { return None; } unsafe {