Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 45 additions & 19 deletions crates/host_env/src/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
// 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<sighandler_t> {
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<Handler> {
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<sighandler_t> {
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<Handler> {
let old = unsafe { c_signal(signalnum, handler.as_raw()) };
Handler::from_raw(old)
}

pub fn raise_signal(signalnum: i32) -> io::Result<()> {
Expand Down
31 changes: 26 additions & 5 deletions crates/vm/src/stdlib/_signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)?;
Expand Down
29 changes: 29 additions & 0 deletions extra_tests/snippets/stdlib_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +33 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the added signal test block

This starts a new signal exercise and the following assertions in an existing test file, but the repository's test-code policy permits only adding an expected-failure decorator or removing one and its TODO once the test passes, and explicitly forbids modifying test logic or assertions. Remove this added snippet coverage rather than editing this test file.

AGENTS.md reference: AGENTS.md:L275-L279

Useful? React with 👍 / 👎.

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)
Expand Down
Loading