From 59f9e3ccd788a55f6079f0fed6bb230d864ab50e Mon Sep 17 00:00:00 2001 From: dot Date: Fri, 9 Oct 2026 21:19:34 +0900 Subject: [PATCH] Translate WASI signal dispositions through libc sentinels Keep Python disposition values distinct from WASI function-table pointers. Recognize wasi-libc SIG_ERR before updating Python handlers and preserve ignored/default/custom dispositions. Add synchronous signal and rejected-registration regressions to the existing snippet. AI assistance covered source investigation, implementation, and automated reference/baseline tests. Isolated patched runtime validation is pending. No human validation is claimed. Assisted-by: Codex:model-version-unavailable --- crates/host_env/src/signal.rs | 64 +++++++++++++++++++-------- crates/vm/src/stdlib/_signal.rs | 31 ++++++++++--- extra_tests/snippets/stdlib_signal.py | 29 ++++++++++++ 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/crates/host_env/src/signal.rs b/crates/host_env/src/signal.rs index 67336eb039e..60b06d63e37 100644 --- a/crates/host_env/src/signal.rs +++ b/crates/host_env/src/signal.rs @@ -46,37 +46,63 @@ mod wasm { pub const SIGSEGV: i32 = 11; pub const SIGTERM: i32 = 15; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum Handler { + Default, + Ignore, + Custom(sighandler_t), + } + unsafe extern "C" { - fn signal(signum: i32, handler: sighandler_t) -> sighandler_t; + #[link_name = "signal"] + fn c_signal(signum: i32, handler: sighandler_t) -> sighandler_t; + #[link_name = "__SIG_IGN"] + fn c_sig_ign(signum: i32); + #[link_name = "__SIG_ERR"] + fn c_sig_err(signum: i32); fn raise(signum: i32) -> i32; } + impl Handler { + fn as_raw(self) -> sighandler_t { + match self { + Self::Default => 0, + Self::Ignore => c_sig_ign as *const () as sighandler_t, + Self::Custom(handler) => handler, + } + } + + fn from_raw(handler: sighandler_t) -> io::Result { + // wasi-libc uses function pointers for SIG_IGN and SIG_ERR because + // table index 1 is also a valid custom Wasm function pointer. + if handler == c_sig_err as *const () as sighandler_t { + Err(io::Error::last_os_error()) + } else if handler == 0 { + Ok(Self::Default) + } else if handler == c_sig_ign as *const () as sighandler_t { + Ok(Self::Ignore) + } else { + Ok(Self::Custom(handler)) + } + } + } + /// # Safety /// /// The caller must ensure `signalnum` is a valid platform signal number. - pub unsafe fn probe_handler(signalnum: i32) -> Option { - let handler = unsafe { signal(signalnum, SIG_IGN) }; - if handler == SIG_ERR { - None - } else { - unsafe { signal(signalnum, handler) }; - Some(handler) - } + pub unsafe fn probe_handler(signalnum: i32) -> Option { + let handler = unsafe { install_handler(signalnum, Handler::Ignore) }.ok()?; + unsafe { install_handler(signalnum, handler) }.ok()?; + Some(handler) } /// # Safety /// /// The caller must ensure `signalnum` is a valid platform signal number and - /// `handler` is accepted by the platform signal ABI. - pub unsafe fn install_handler( - signalnum: i32, - handler: sighandler_t, - ) -> io::Result { - let old = unsafe { signal(signalnum, handler) }; - if old == SIG_ERR { - return Err(io::Error::last_os_error()); - } - Ok(old) + /// a custom `handler` is accepted by the platform signal ABI. + pub unsafe fn install_handler(signalnum: i32, handler: Handler) -> io::Result { + let old = unsafe { c_signal(signalnum, handler.as_raw()) }; + Handler::from_raw(old) } pub fn raise_signal(signalnum: i32) -> io::Result<()> { diff --git a/crates/vm/src/stdlib/_signal.rs b/crates/vm/src/stdlib/_signal.rs index 67a765a6230..1c2bc792539 100644 --- a/crates/vm/src/stdlib/_signal.rs +++ b/crates/vm/src/stdlib/_signal.rs @@ -175,9 +175,16 @@ pub(crate) mod _signal { let Some(handler) = (unsafe { host_signal::probe_handler(signum) }) else { continue; }; - let py_handler = if handler == SIG_DFL { + let (is_default, is_ignored) = cfg_select! { + target_os = "wasi" => ( + handler == host_signal::Handler::Default, + handler == host_signal::Handler::Ignore, + ), + _ => (handler == SIG_DFL, handler == SIG_IGN), + }; + let py_handler = if is_default { Some(sig_dfl.clone()) - } else if handler == SIG_IGN { + } else if is_ignored { Some(sig_ign.clone()) } else { None @@ -223,15 +230,29 @@ pub(crate) mod _signal { } let sig_handler = if handler.is_callable() { - run_signal as *const () as sighandler_t + let callback = run_signal as *const () as sighandler_t; + cfg_select! { + target_os = "wasi" => host_signal::Handler::Custom(callback), + _ => callback, + } } else { const MSG: &str = "signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object"; - usize::try_from_borrowed_object(vm, &handler) + let disposition = usize::try_from_borrowed_object(vm, &handler) .ok() .filter(|&v| matches!(v, SIG_DFL | SIG_IGN)) - .ok_or_else(|| vm.new_type_error(MSG))? + .ok_or_else(|| vm.new_type_error(MSG))?; + cfg_select! { + target_os = "wasi" => { + if disposition == SIG_DFL { + host_signal::Handler::Default + } else { + host_signal::Handler::Ignore + } + }, + _ => disposition, + } }; signal::check_signals(vm)?; diff --git a/extra_tests/snippets/stdlib_signal.py b/extra_tests/snippets/stdlib_signal.py index 8e00b5d885b..39f103157fa 100644 --- a/extra_tests/snippets/stdlib_signal.py +++ b/extra_tests/snippets/stdlib_signal.py @@ -21,6 +21,35 @@ def handler(signum, frame): assert signal.getsignal(signal.SIGILL) is signal.SIG_DFL +# Ignoring a synchronously raised signal must not invoke a callback, including +# on WASI where libc uses a function pointer rather than integer 1 for SIG_IGN. +synchronous_signals = [] + + +def synchronous_handler(signum, frame): + synchronous_signals.append(signum) + + +previous = signal.signal(signal.SIGILL, synchronous_handler) +try: + signal.raise_signal(signal.SIGILL) + assert synchronous_signals == [signal.SIGILL] + assert signal.signal(signal.SIGILL, signal.SIG_IGN) is synchronous_handler + signal.raise_signal(signal.SIGILL) + assert synchronous_signals == [signal.SIGILL] + assert signal.getsignal(signal.SIGILL) is signal.SIG_IGN +finally: + signal.signal(signal.SIGILL, previous) + +if sys.platform == "wasi": + # wasi-libc rejects SIGKILL (9) and SIGSTOP (19), even though they are not + # exported by RustPython's minimal WASI signal module. + for uncatchable in (9, 19): + previous = signal.getsignal(uncatchable) + assert_raises(OSError, signal.signal, uncatchable, signal.SIG_IGN) + assert signal.getsignal(uncatchable) is previous + + # unix, and not WASI if "win" not in sys.platform and sys.platform != "wasi": signal.signal(signal.SIGALRM, handler)