native: only crash signals are said by default; GEX_LOG_CHILD_SIGNALS=1 says every one

A pipeline's SIGPIPE ends a child on every `yes | head`, and the session
ends its processes with SIGTERM: neither is said unless asked for. Said by
default: SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSYS and
SIGKILL (gex never ends a process with SIGKILL itself), and any gex
limitation delivered as a signal.

tests/child-signal.sh checks both modes, with a SIGSEGV child and a child
another process kills, on a64 and x86.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-25 15:02:31 -04:00
1 parent c8853f7ade
commit 4c36082b27
2 files changed
+56 -39

No files matched your search

+10 -6
View File
@@ -2722,14 +2722,18 @@ fn fork_process(store: &impl wasmtime::AsContext<Data = HostState>, req: ForkReq
Ok(())
}
/// A child ended by a signal says so, once, as a shell would: a process
/// gone with no word otherwise (one whose own crash handler does not catch
/// the signal) leaves only its absence. Not the signals a session ends its
/// processes with.
/// A child ended by a crash signal says so, once, as a shell would: one no
/// crash handler catches otherwise leaves only its absence. A gex limitation
/// delivered as a signal is said whatever the signal. The rest (a pipeline's
/// SIGPIPE, the SIGTERM a session ends its processes with) only under
/// `GEX_LOG_CHILD_SIGNALS=1`. gex itself never ends a process with SIGKILL,
/// so a SIGKILL is another process's doing and is said.
fn say_killed(pid: i32, exit: &Exit) {
let Exit::Signal { signal, pc, detail, internal } = exit else { return };
// SIGHUP, SIGINT, SIGTERM.
if !internal && matches!(signal, 1 | 2 | 15) {
static ALL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let all = *ALL.get_or_init(|| std::env::var("GEX_LOG_CHILD_SIGNALS").is_ok_and(|v| v == "1"));
// SIGILL, SIGTRAP, SIGABRT, SIGBUS, SIGFPE, SIGKILL, SIGSEGV, SIGSYS.
if !internal && !all && !matches!(signal, 4 | 5 | 6 | 7 | 8 | 9 | 11 | 31) {
return;
}
let detail = if detail.is_empty() || detail.starts_with("killed by signal") { String::new() } else { format!(": {detail}") };
+46 -33
View File
@@ -1,8 +1,9 @@
#!/bin/sh
# A child process killed by a signal says so on gex's stderr, as a shell
# would: one no crash handler catches (a SIGPIPE, a SIGKILL, an abort with
# none installed) otherwise left only its absence. A child ended
# by SIGTERM, as a session ends its processes, says nothing.
# A child process killed by a crash signal says so on gex's stderr, as a
# shell would: one no crash handler catches (an abort with none installed, a
# SIGSEGV, a SIGKILL from another process) otherwise left only its absence.
# A pipeline's SIGPIPE and a session's SIGTERM say nothing, unless
# GEX_LOG_CHILD_SIGNALS=1 asks for every signal.
#
# tests/child-signal.sh [a64|x86] (needs zig and the guest's gexc)
set -eu
@@ -20,43 +21,55 @@ mkdir -p "$WORK"
[ -n "${KEEP:-}" ] || trap 'rm -rf "$WORK"' EXIT
cat > "$WORK/cs.c" <<'EOF2'
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static int child(int how) {
pid_t p = fork();
if (p == 0) {
if (how == 0) { int fd[2]; pipe(fd); close(fd[0]); write(fd[1], "x", 1); }
if (how == 1) abort();
if (how == 3) kill(getpid(), SIGKILL);
if (how == 2) raise(SIGTERM);
_exit(0);
}
int st = 0;
waitpid(p, &st, 0);
printf("child %d: pid %d signal %d\n", how, p, WIFSIGNALED(st) ? WTERMSIG(st) : 0);
fflush(stdout);
return p;
}
int main(void) {
for (int how = 0; how < 4; how++) child(how);
for (int how = 0; how < 4; how++) {
pid_t p = fork();
if (p == 0) {
if (how == 0) { int fd[2]; pipe(fd); close(fd[0]); write(fd[1], "x", 1); }
if (how == 1) abort();
if (how == 2) raise(SIGTERM);
if (how == 3) raise(SIGSEGV);
_exit(0);
}
if (p > 0 && how == 3) {
/* and one another process kills */
pid_t q = fork();
if (q == 0) { for (;;) pause(); }
kill(q, SIGKILL);
int st = 0;
waitpid(q, &st, 0);
printf("child 4: pid %d signal %d\n", q, WIFSIGNALED(st) ? WTERMSIG(st) : 0);
}
int st = 0;
waitpid(p, &st, 0);
printf("child %d: pid %d signal %d\n", how, p, WIFSIGNALED(st) ? WTERMSIG(st) : 0);
fflush(stdout);
}
return 0;
}
EOF2
zig cc -target "$TARGET" -O1 -static -Wl,--image-base=0x1000000 -o "$WORK/cs" "$WORK/cs.c"
timeout "${TMO:-120}" "$GEXC" run "$WORK/cs" > "$WORK/out" 2> "$WORK/err" || true
fail=0
for want in "0 13 yes" "1 6 yes" "2 15 no" "3 9 yes"; do
set -- $want
line=$(grep "^child $1:" "$WORK/out" || true)
pid=$(echo "$line" | sed -n 's/.* pid \([0-9]*\) .*/\1/p')
sig=$(echo "$line" | sed -n 's/.* signal \([0-9]*\)$/\1/p')
said=no; [ -n "$pid" ] && grep -q "pid $pid killed by signal $2 " "$WORK/err" && said=yes
if [ "$sig" = "$2" ] && [ "$said" = "$3" ]; then :; else
echo "FAIL child-signal $ARCH: child $1 got signal '${sig:-?}' (want $2), said: $said (want $3)"
fail=1
fi
# Each case: child, its signal, whether it is said by default, and with every signal asked for.
for mode in default all; do
if [ $mode = all ]; then export GEX_LOG_CHILD_SIGNALS=1; else unset GEX_LOG_CHILD_SIGNALS; fi
timeout "${TMO:-120}" "$GEXC" run "$WORK/cs" > "$WORK/out.$mode" 2> "$WORK/err.$mode" || true
for want in "0 13 no yes" "1 6 yes yes" "2 15 no yes" "3 11 yes yes" "4 9 yes yes"; do
set -- $want
expect=$3; [ $mode = all ] && expect=$4
line=$(grep "^child $1:" "$WORK/out.$mode" || true)
pid=$(echo "$line" | sed -n 's/.* pid \([0-9]*\) .*/\1/p')
sig=$(echo "$line" | sed -n 's/.* signal \([0-9]*\)$/\1/p')
said=no; [ -n "$pid" ] && grep -q "pid $pid killed by signal $2 " "$WORK/err.$mode" && said=yes
if [ "$sig" != "$2" ] || [ "$said" != "$expect" ]; then
echo "FAIL child-signal $ARCH ($mode): child $1 got signal '${sig:-?}' (want $2), said: $said (want $expect)"
fail=1
fi
done
done
[ $fail = 0 ] && echo "ok child-signal $ARCH: SIGPIPE, SIGABRT and SIGKILL children named, SIGTERM quiet"
[ $fail = 0 ] && echo "ok child-signal $ARCH: SIGABRT, SIGSEGV and SIGKILL children named, SIGPIPE and SIGTERM only when asked"
exit $fail