diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-ignored.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-ignored.ts.json new file mode 100644 index 000000000..7712a33bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-ignored.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/write-signals-ignored.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-listener.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-listener.mjs.json new file mode 100644 index 000000000..090e24684 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/write-signals-listener.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/write-signals-listener.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index c82cb9052..43b38e77a 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -3764,9 +3764,10 @@ static DWORD WINAPI scr_fs_rename_worker(LPVOID payload) { #else static void *scr_fs_rename_worker(void *payload) { (void)payload; - /* A pool thread keeps SIGPIPE and SIGXFSZ blocked: its file writes then - * report EPIPE and EFBIG without changing the signal mask per write - * (scr_fs_write_once). */ +#ifdef SCR_LIB + /* In a library, a pool thread keeps SIGPIPE and SIGXFSZ blocked: its file + * writes then report EPIPE and EFBIG without changing the signal mask per + * write (scr_fs_write_once). Executables ignore both signals (scr_init). */ sigset_t write_set; sigemptyset(&write_set); #ifdef SIGPIPE @@ -3776,6 +3777,7 @@ static void *scr_fs_rename_worker(void *payload) { sigaddset(&write_set, SIGXFSZ); #endif (void)pthread_sigmask(SIG_BLOCK, &write_set, NULL); +#endif scr_fs_rename_worker_loop(); return NULL; } diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c index 93169be80..324faf6a7 100644 --- a/packages/runtime/src/scr_child.c +++ b/packages/runtime/src/scr_child.c @@ -3208,10 +3208,9 @@ static void scr_exec_envp_free(char **envp); * next entry. The last such failure is EACCES if any candidate was denied, * else ENOENT. Relative entries are resolved by the child, which starts in * `cwd`. Returns posix_spawn's error number. */ -static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, - const posix_spawnattr_t *attr, char *const *argv, - char *const *envp, const char *cwd) { - (void)cwd; /* the file actions change into it before the exec */ +static int scr_spawn_resolved(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, char *const *argv, + char *const *envp) { if (envp == NULL) return posix_spawnp(pid, file, fa, attr, argv, environ); if (strchr(file, '/')) return posix_spawn(pid, file, fa, attr, argv, envp); const char *path = "/usr/bin:/bin"; @@ -3243,6 +3242,31 @@ static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_fil return denied ? EACCES : ENOENT; } +/* scr_spawn_resolved with the child's SIGPIPE and SIGXFSZ at their default + * actions, as libuv's spawns start them: executables ignore both + * (scr_init), and an exec keeps ignored dispositions. `attr` may be NULL. */ +static int scr_spawn_command(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, + posix_spawnattr_t *attr, char *const *argv, char *const *envp, + const char *cwd) { + (void)cwd; /* the file actions change into it before the exec */ + posix_spawnattr_t local; + if (attr == NULL) { + posix_spawnattr_init(&local); + attr = &local; + } + sigset_t defaults; + sigemptyset(&defaults); + sigaddset(&defaults, SIGPIPE); + sigaddset(&defaults, SIGXFSZ); + short flags = 0; + (void)posix_spawnattr_getflags(attr, &flags); + (void)posix_spawnattr_setsigdefault(attr, &defaults); + (void)posix_spawnattr_setflags(attr, (short)(flags | POSIX_SPAWN_SETSIGDEF)); + int err = scr_spawn_resolved(pid, file, fa, attr, argv, envp); + if (attr == &local) posix_spawnattr_destroy(&local); + return err; +} + /* env_pairs: NULL inherits the parent's environment; else [k,v,...] * replaces it, as Node's `env` option does, and the command is looked up * on the replacement's PATH (scr_spawn_command). */ diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index d28f058fa..0941db370 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -5,6 +5,9 @@ #include #include #include +#if !defined(_WIN32) && !defined(__wasi__) +#include +#endif #ifdef _WIN32 #include /* _O_BINARY */ #include /* _setmode, _fileno */ @@ -179,6 +182,15 @@ void scr_init(void) { * stream immediately. */ static char outbuf[1 << 16]; setvbuf(stdout, outbuf, _IOFBF, sizeof outbuf); +#if !defined(_WIN32) && !defined(__wasi__) + /* Node ignores SIGPIPE and SIGXFSZ: a write to a closed pipe or past + * RLIMIT_FSIZE fails with EPIPE or EFBIG, on any thread, instead of + * ending the process. A process.on listener replaces the disposition + * while it is registered (scr_events.c), and spawned children start with + * both at their default actions (scr_spawn_command). */ + signal(SIGPIPE, SIG_IGN); + signal(SIGXFSZ, SIG_IGN); +#endif #ifdef SCR_WORKERS } #endif diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index 3bb9a0725..73ff388a9 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -4136,13 +4136,14 @@ static bool scr_fs_write_fd_valid(double fd) { /* write(2) raises SIGPIPE before returning EPIPE when a pipe/socket peer has * closed; write(2)/pwrite(2) likewise raise SIGXFSZ before returning EFBIG at - * RLIMIT_FSIZE. Node turns both into catchable fs errors. Do the same per - * calling thread rather than changing process dispositions (which spawned - * children would inherit). Consume only a signal freshly generated by this - * call, preserving any signal that was already pending. */ + * RLIMIT_FSIZE. Node ignores both signals, so they arrive as catchable fs + * errors. Executables ignore them too (scr_init) and write directly. A + * library does not own its host's dispositions: it blocks the signals in + * the calling thread instead, and consumes only a signal freshly generated + * by this call, preserving any signal that was already pending. */ static ssize_t scr_fs_write_once(int fd, const void *data, size_t length, bool positioned, double position) { -#if defined(_WIN32) || defined(__wasi__) +#if defined(_WIN32) || defined(__wasi__) || !defined(SCR_LIB) return positioned ? scr_fs_pwrite(fd, data, length, position) : write(fd, data, length); diff --git a/tests/corpus/write-signals-ignored.ts b/tests/corpus/write-signals-ignored.ts new file mode 100644 index 000000000..565ffc061 --- /dev/null +++ b/tests/corpus/write-signals-ignored.ts @@ -0,0 +1,30 @@ +// Node ignores SIGPIPE and SIGXFSZ: the process keeps running when either +// arrives, and spawned children start with both at their default actions, +// through each spawn path. write-signals-listener.mjs covers a listener. +import { execFileSync, spawn, spawnSync } from "node:child_process"; + +process.kill(process.pid, "SIGPIPE"); +process.kill(process.pid, "SIGXFSZ"); +console.log("ignored"); + +for (const name of ["PIPE", "XFSZ"]) { + const child = spawnSync("/bin/sh", ["-c", `kill -${name} $$; echo survived`], { + encoding: "utf8", + }); + console.log("spawnSync", name, child.status, child.signal, JSON.stringify(child.stdout)); +} + +try { + execFileSync("/bin/sh", ["-c", "kill -PIPE $$; echo survived"], { encoding: "utf8" }); + console.log("execFileSync survived"); +} catch (error) { + console.log("execFileSync", (error as { signal?: string }).signal); +} + +const child = spawn("/bin/sh", ["-c", "kill -XFSZ $$; echo survived"]); +let output = ""; +child.stdout!.setEncoding("utf8"); +child.stdout!.on("data", (chunk: string) => (output += chunk)); +child.on("exit", (code, signal) => { + console.log("spawn", code, signal, JSON.stringify(output)); +}); diff --git a/tests/corpus/write-signals-listener.mjs b/tests/corpus/write-signals-listener.mjs new file mode 100644 index 000000000..da43dfa83 --- /dev/null +++ b/tests/corpus/write-signals-listener.mjs @@ -0,0 +1,12 @@ +// Node ignores SIGPIPE and SIGXFSZ until a listener is registered: the +// listener then receives each signal, and the process keeps running. +const keepAlive = setInterval(() => {}, 1000); +let received = 0; +for (const signal of ["SIGPIPE", "SIGXFSZ"]) { + process.on(signal, (name) => { + console.log("listener", name); + if (++received === 2) clearInterval(keepAlive); + }); +} +process.kill(process.pid, "SIGPIPE"); +setTimeout(() => process.kill(process.pid, "SIGXFSZ"), 20); diff --git a/tests/harness/corpus-costs/write-signals-ignored.ts.json b/tests/harness/corpus-costs/write-signals-ignored.ts.json new file mode 100644 index 000000000..4f7328ed7 --- /dev/null +++ b/tests/harness/corpus-costs/write-signals-ignored.ts.json @@ -0,0 +1 @@ +{ "plain": 4490, "san": 66079 } diff --git a/tests/harness/corpus-costs/write-signals-listener.mjs.json b/tests/harness/corpus-costs/write-signals-listener.mjs.json new file mode 100644 index 000000000..b73cebb1b --- /dev/null +++ b/tests/harness/corpus-costs/write-signals-listener.mjs.json @@ -0,0 +1 @@ +{ "plain": 3437, "san": 6636 } diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts index 76293fc5b..752b505b3 100644 --- a/tests/harness/windows-differential.test.ts +++ b/tests/harness/windows-differential.test.ts @@ -101,6 +101,9 @@ const WINDOWS_SKIPS: Record = { // spawns succeed, exposed here where every one fails. "process-named-signals.cjs": "POSIX signal delivery: Windows Node cannot send SIGWINCH or register SIGSTOP", + "write-signals-ignored.ts": + "POSIX signals: Windows has no SIGPIPE or SIGXFSZ, and the children are /bin/sh", + "write-signals-listener.mjs": "POSIX signals: Windows has no SIGPIPE or SIGXFSZ", "1360-spawn-sync.ts": 'posix-shaped: every spawn is ENOENT on Windows Node too, exposing the documented spawn-failure ""-vs-null stdout stance (1644 covers spawnSync here)', "1361-spawn-events.ts":