mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Ignore SIGPIPE and SIGXFSZ in executables, as Node does (#2578)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/write-signals-ignored.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/write-signals-listener.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#if !defined(_WIN32) && !defined(__wasi__)
|
||||
#include <signal.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include <fcntl.h> /* _O_BINARY */
|
||||
#include <io.h> /* _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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4490, "san": 66079 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3437, "san": 6636 }
|
||||
@@ -101,6 +101,9 @@ const WINDOWS_SKIPS: Record<string, string> = {
|
||||
// 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":
|
||||
|
||||
Reference in New Issue
Block a user