Skip to content

Commit 59f9e3c

Browse files
committed
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
1 parent bd8fdb7 commit 59f9e3c

3 files changed

Lines changed: 100 additions & 24 deletions

File tree

‎crates/host_env/src/signal.rs‎

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,37 +46,63 @@ mod wasm {
4646
pub const SIGSEGV: i32 = 11;
4747
pub const SIGTERM: i32 = 15;
4848

49+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50+
pub enum Handler {
51+
Default,
52+
Ignore,
53+
Custom(sighandler_t),
54+
}
55+
4956
unsafe extern "C" {
50-
fn signal(signum: i32, handler: sighandler_t) -> sighandler_t;
57+
#[link_name = "signal"]
58+
fn c_signal(signum: i32, handler: sighandler_t) -> sighandler_t;
59+
#[link_name = "__SIG_IGN"]
60+
fn c_sig_ign(signum: i32);
61+
#[link_name = "__SIG_ERR"]
62+
fn c_sig_err(signum: i32);
5163
fn raise(signum: i32) -> i32;
5264
}
5365

66+
impl Handler {
67+
fn as_raw(self) -> sighandler_t {
68+
match self {
69+
Self::Default => 0,
70+
Self::Ignore => c_sig_ign as *const () as sighandler_t,
71+
Self::Custom(handler) => handler,
72+
}
73+
}
74+
75+
fn from_raw(handler: sighandler_t) -> io::Result<Self> {
76+
// wasi-libc uses function pointers for SIG_IGN and SIG_ERR because
77+
// table index 1 is also a valid custom Wasm function pointer.
78+
if handler == c_sig_err as *const () as sighandler_t {
79+
Err(io::Error::last_os_error())
80+
} else if handler == 0 {
81+
Ok(Self::Default)
82+
} else if handler == c_sig_ign as *const () as sighandler_t {
83+
Ok(Self::Ignore)
84+
} else {
85+
Ok(Self::Custom(handler))
86+
}
87+
}
88+
}
89+
5490
/// # Safety
5591
///
5692
/// The caller must ensure `signalnum` is a valid platform signal number.
57-
pub unsafe fn probe_handler(signalnum: i32) -> Option<sighandler_t> {
58-
let handler = unsafe { signal(signalnum, SIG_IGN) };
59-
if handler == SIG_ERR {
60-
None
61-
} else {
62-
unsafe { signal(signalnum, handler) };
63-
Some(handler)
64-
}
93+
pub unsafe fn probe_handler(signalnum: i32) -> Option<Handler> {
94+
let handler = unsafe { install_handler(signalnum, Handler::Ignore) }.ok()?;
95+
unsafe { install_handler(signalnum, handler) }.ok()?;
96+
Some(handler)
6597
}
6698

6799
/// # Safety
68100
///
69101
/// The caller must ensure `signalnum` is a valid platform signal number and
70-
/// `handler` is accepted by the platform signal ABI.
71-
pub unsafe fn install_handler(
72-
signalnum: i32,
73-
handler: sighandler_t,
74-
) -> io::Result<sighandler_t> {
75-
let old = unsafe { signal(signalnum, handler) };
76-
if old == SIG_ERR {
77-
return Err(io::Error::last_os_error());
78-
}
79-
Ok(old)
102+
/// a custom `handler` is accepted by the platform signal ABI.
103+
pub unsafe fn install_handler(signalnum: i32, handler: Handler) -> io::Result<Handler> {
104+
let old = unsafe { c_signal(signalnum, handler.as_raw()) };
105+
Handler::from_raw(old)
80106
}
81107

82108
pub fn raise_signal(signalnum: i32) -> io::Result<()> {

‎crates/vm/src/stdlib/_signal.rs‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,16 @@ pub(crate) mod _signal {
175175
let Some(handler) = (unsafe { host_signal::probe_handler(signum) }) else {
176176
continue;
177177
};
178-
let py_handler = if handler == SIG_DFL {
178+
let (is_default, is_ignored) = cfg_select! {
179+
target_os = "wasi" => (
180+
handler == host_signal::Handler::Default,
181+
handler == host_signal::Handler::Ignore,
182+
),
183+
_ => (handler == SIG_DFL, handler == SIG_IGN),
184+
};
185+
let py_handler = if is_default {
179186
Some(sig_dfl.clone())
180-
} else if handler == SIG_IGN {
187+
} else if is_ignored {
181188
Some(sig_ign.clone())
182189
} else {
183190
None
@@ -223,15 +230,29 @@ pub(crate) mod _signal {
223230
}
224231

225232
let sig_handler = if handler.is_callable() {
226-
run_signal as *const () as sighandler_t
233+
let callback = run_signal as *const () as sighandler_t;
234+
cfg_select! {
235+
target_os = "wasi" => host_signal::Handler::Custom(callback),
236+
_ => callback,
237+
}
227238
} else {
228239
const MSG: &str =
229240
"signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object";
230241

231-
usize::try_from_borrowed_object(vm, &handler)
242+
let disposition = usize::try_from_borrowed_object(vm, &handler)
232243
.ok()
233244
.filter(|&v| matches!(v, SIG_DFL | SIG_IGN))
234-
.ok_or_else(|| vm.new_type_error(MSG))?
245+
.ok_or_else(|| vm.new_type_error(MSG))?;
246+
cfg_select! {
247+
target_os = "wasi" => {
248+
if disposition == SIG_DFL {
249+
host_signal::Handler::Default
250+
} else {
251+
host_signal::Handler::Ignore
252+
}
253+
},
254+
_ => disposition,
255+
}
235256
};
236257

237258
signal::check_signals(vm)?;

‎extra_tests/snippets/stdlib_signal.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@ def handler(signum, frame):
2121
assert signal.getsignal(signal.SIGILL) is signal.SIG_DFL
2222

2323

24+
# Ignoring a synchronously raised signal must not invoke a callback, including
25+
# on WASI where libc uses a function pointer rather than integer 1 for SIG_IGN.
26+
synchronous_signals = []
27+
28+
29+
def synchronous_handler(signum, frame):
30+
synchronous_signals.append(signum)
31+
32+
33+
previous = signal.signal(signal.SIGILL, synchronous_handler)
34+
try:
35+
signal.raise_signal(signal.SIGILL)
36+
assert synchronous_signals == [signal.SIGILL]
37+
assert signal.signal(signal.SIGILL, signal.SIG_IGN) is synchronous_handler
38+
signal.raise_signal(signal.SIGILL)
39+
assert synchronous_signals == [signal.SIGILL]
40+
assert signal.getsignal(signal.SIGILL) is signal.SIG_IGN
41+
finally:
42+
signal.signal(signal.SIGILL, previous)
43+
44+
if sys.platform == "wasi":
45+
# wasi-libc rejects SIGKILL (9) and SIGSTOP (19), even though they are not
46+
# exported by RustPython's minimal WASI signal module.
47+
for uncatchable in (9, 19):
48+
previous = signal.getsignal(uncatchable)
49+
assert_raises(OSError, signal.signal, uncatchable, signal.SIG_IGN)
50+
assert signal.getsignal(uncatchable) is previous
51+
52+
2453
# unix, and not WASI
2554
if "win" not in sys.platform and sys.platform != "wasi":
2655
signal.signal(signal.SIGALRM, handler)

0 commit comments

Comments
 (0)