fix: libuv patches to address child_process.spawn slowness (#33337)

* fix: libuv patches to address child_process.spawn slowness

* chore: backport additional patches

Co-authored-by: deepak1556 <hop2deep@gmail.com>
This commit is contained in:
Jeremy Rose 2022-03-23 06:30:54 -07:00 committed by GitHub
parent a5ab10f3d2
commit f912130be6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1805 additions and 0 deletions

View file

@ -35,3 +35,14 @@ fix_failing_node_js_test_on_outdated.patch
be_compatible_with_cppgc.patch
feat_add_knostartdebugsignalhandler_to_environment_to_prevent.patch
worker_thread_add_asar_support.patch
process_monitor_for_exit_with_kqueue_on_bsds_3441.patch
unix_protect_fork_in_uv_spawn_from_signals.patch
process_bsd_handle_kevent_note_exit_failure_3451.patch
reland_macos_use_posix_spawn_instead_of_fork_3257.patch
process_reset_the_signal_mask_if_the_fork_fails_3537.patch
process_only_use_f_dupfd_cloexec_if_it_is_defined_3512.patch
unix_simplify_uv_cloexec_fcntl_3492.patch
unix_remove_uv_cloexec_ioctl_3515.patch
process_simplify_uv_write_int_calls_3519.patch
macos_don_t_use_thread-unsafe_strtok_3524.patch
process_fix_hang_after_note_exit_3521.patch

View file

@ -0,0 +1,54 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ben Noordhuis <info@bnoordhuis.nl>
Date: Wed, 9 Mar 2022 11:06:39 +0100
Subject: macos: don't use thread-unsafe strtok() (#3524)
Refs https://github.com/libuv/libuv/pull/3524
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index 8cde389b826b6b167437845eccd5fed440dcadc4..147164e7ea25abbf655452930d78ee0a714cce36 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -387,30 +387,22 @@ static void uv__spawn_init_posix_spawn_fncs(void) {
static void uv__spawn_init_can_use_setsid(void) {
- static const int MACOS_CATALINA_VERSION_MAJOR = 19;
- char version_str[256];
- char* version_major_str;
- size_t version_str_size = 256;
- int r;
- int version_major;
-
- /* Get a version string */
- r = sysctlbyname("kern.osrelease", version_str, &version_str_size, NULL, 0);
- if (r != 0)
+ int which[] = {CTL_KERN, KERN_OSRELEASE};
+ unsigned major;
+ unsigned minor;
+ unsigned patch;
+ char buf[256];
+ size_t len;
+
+ len = sizeof(buf);
+ if (sysctl(which, ARRAY_SIZE(which), buf, &len, NULL, 0))
return;
- /* Try to get the major version number. If not found
- * fall back to the fork/exec flow */
- version_major_str = strtok(version_str, ".");
- if (version_major_str == NULL)
+ /* NULL specifies to use LC_C_LOCALE */
+ if (3 != sscanf_l(buf, NULL, "%u.%u.%u", &major, &minor, &patch))
return;
- /* Parse the version major as a number. If it is greater than
- * the major version for macOS Catalina (aka macOS 10.15), then
- * the POSIX_SPAWN_SETSID flag is available */
- version_major = atoi_l(version_major_str, NULL); /* Use LC_C_LOCALE */
- if (version_major >= MACOS_CATALINA_VERSION_MAJOR)
- posix_spawn_can_use_setsid = 1;
+ posix_spawn_can_use_setsid = (major >= 19); /* macOS Catalina */
}

View file

@ -0,0 +1,70 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Tue, 1 Feb 2022 15:27:12 -0500
Subject: process,bsd: handle kevent NOTE_EXIT failure (#3451)
The kernel may return ESRCH if the child has already exited here.
This is rather annoying, and means we must indirectly handle
notification to our event loop of the process exit.
Refs: https://github.com/libuv/libuv/pull/3441
Refs: https://github.com/libuv/libuv/pull/3257
diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
index 16be13b99f5db77741aa276e90a437ef4eb5ba32..2dcc8b32f5165dd75061a1b55cc1abd2ab93ccc9 100644
--- a/deps/uv/src/unix/internal.h
+++ b/deps/uv/src/unix/internal.h
@@ -145,7 +145,8 @@ typedef struct uv__stream_queued_fds_s uv__stream_queued_fds_t;
/* loop flags */
enum {
- UV_LOOP_BLOCK_SIGPROF = 1
+ UV_LOOP_BLOCK_SIGPROF = 0x1,
+ UV_LOOP_REAP_CHILDREN = 0x2
};
/* flags of excluding ifaddr */
diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
index 35200f17495d80ed2d19ef9f6f76bbc92ee042f6..071fe0ce0938657d0fb840af62a432352e938a8a 100644
--- a/deps/uv/src/unix/kqueue.c
+++ b/deps/uv/src/unix/kqueue.c
@@ -285,7 +285,7 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
for (i = 0; i < nfds; i++) {
ev = events + i;
if (ev->filter == EVFILT_PROC) {
- uv__wait_children(loop);
+ loop->flags |= UV_LOOP_REAP_CHILDREN;
nevents++;
continue;
}
@@ -383,6 +383,11 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
nevents++;
}
+ if (loop->flags & UV_LOOP_REAP_CHILDREN) {
+ loop->flags &= ~UV_LOOP_REAP_CHILDREN;
+ uv__wait_children(loop);
+ }
+
if (reset_timeout != 0) {
timeout = user_timeout;
reset_timeout = 0;
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index c1f6bd4b0076f0835caf83c45a6a896e7ae5def9..2920b942962357827bae9bcad23af8333e8b007f 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -507,8 +507,12 @@ int uv_spawn(uv_loop_t* loop,
#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
struct kevent event;
EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
- if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL))
- abort();
+ if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL)) {
+ if (errno != ESRCH)
+ abort();
+ /* Process already exited. Call waitpid on the next loop iteration. */
+ loop->flags |= UV_LOOP_REAP_CHILDREN;
+ }
#endif
QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);

View file

@ -0,0 +1,158 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Wed, 23 Mar 2022 15:39:38 +0900
Subject: process: fix hang after NOTE_EXIT (#3521)
Bug #3504 seems to affect more platforms than just OpenBSD. As this
seems to be a race condition in these kernels, we do not want to fail
because of it. Instead, we remove the WNOHANG flag from waitpid, and
track exactly which processes have exited. Should also be a slight speed
improvement for excessively large numbers of live children.
diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
index 071fe0ce0938657d0fb840af62a432352e938a8a..4c4d990ff5fa6c8ab937be2e4f79ccdaf90670c2 100644
--- a/deps/uv/src/unix/kqueue.c
+++ b/deps/uv/src/unix/kqueue.c
@@ -117,6 +117,7 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
unsigned int revents;
QUEUE* q;
uv__io_t* w;
+ uv_process_t* process;
sigset_t* pset;
sigset_t set;
uint64_t base;
@@ -284,12 +285,22 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
for (i = 0; i < nfds; i++) {
ev = events + i;
+ fd = ev->ident;
+
+ /* Handle kevent NOTE_EXIT results */
if (ev->filter == EVFILT_PROC) {
- loop->flags |= UV_LOOP_REAP_CHILDREN;
+ QUEUE_FOREACH(q, &loop->process_handles) {
+ process = QUEUE_DATA(q, uv_process_t, queue);
+ if (process->pid == fd) {
+ process->flags |= UV_HANDLE_REAP;
+ loop->flags |= UV_LOOP_REAP_CHILDREN;
+ break;
+ }
+ }
nevents++;
continue;
}
- fd = ev->ident;
+
/* Skip invalidated events, see uv__platform_invalidate_fd */
if (fd == -1)
continue;
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index 147164e7ea25abbf655452930d78ee0a714cce36..c8816b85b7e531648064e739fb89257565ad64bb 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -63,12 +63,18 @@ extern char **environ;
# include "zos-base.h"
#endif
-#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+#if defined(__APPLE__) || \
+ defined(__DragonFly__) || \
+ defined(__FreeBSD__) || \
+ defined(__NetBSD__) || \
+ defined(__OpenBSD__)
#include <sys/event.h>
+#else
+#define UV_USE_SIGCHLD
#endif
-#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
+#ifdef UV_USE_SIGCHLD
static void uv__chld(uv_signal_t* handle, int signum) {
assert(signum == SIGCHLD);
uv__wait_children(handle->loop);
@@ -80,6 +86,7 @@ void uv__wait_children(uv_loop_t* loop) {
int exit_status;
int term_signal;
int status;
+ int options;
pid_t pid;
QUEUE pending;
QUEUE* q;
@@ -93,19 +100,33 @@ void uv__wait_children(uv_loop_t* loop) {
process = QUEUE_DATA(q, uv_process_t, queue);
q = QUEUE_NEXT(q);
+#ifndef UV_USE_SIGCHLD
+ if ((process->flags & UV_HANDLE_REAP) == 0)
+ continue;
+ options = 0;
+ process->flags &= ~UV_HANDLE_REAP;
+#else
+ options = WNOHANG;
+#endif
+
do
- pid = waitpid(process->pid, &status, WNOHANG);
+ pid = waitpid(process->pid, &status, options);
while (pid == -1 && errno == EINTR);
- if (pid == 0)
+#ifdef UV_USE_SIGCHLD
+ if (pid == 0) /* Not yet exited */
continue;
+#endif
if (pid == -1) {
if (errno != ECHILD)
abort();
+ /* The child died, and we missed it. This probably means someone else
+ * stole the waitpid from us. Handle this by not handling it at all. */
continue;
}
+ assert(pid == process->pid);
process->status = status;
QUEUE_REMOVE(&process->queue);
QUEUE_INSERT_TAIL(&pending, &process->queue);
@@ -964,7 +985,7 @@ int uv_spawn(uv_loop_t* loop,
goto error;
}
-#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
+#ifdef UV_USE_SIGCHLD
uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
#endif
@@ -983,13 +1004,14 @@ int uv_spawn(uv_loop_t* loop,
* fail to open a stdio handle. This ensures we can eventually reap the child
* with waitpid. */
if (exec_errorno == 0) {
-#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+#ifndef UV_USE_SIGCHLD
struct kevent event;
EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL)) {
if (errno != ESRCH)
abort();
/* Process already exited. Call waitpid on the next loop iteration. */
+ process->flags |= UV_HANDLE_REAP;
loop->flags |= UV_LOOP_REAP_CHILDREN;
}
#endif
diff --git a/deps/uv/src/uv-common.h b/deps/uv/src/uv-common.h
index 8a190bf8fa8c5a282feaf251aec2a30c95776888..6001b0cf68d0b0268b578218b664a737f43c9521 100644
--- a/deps/uv/src/uv-common.h
+++ b/deps/uv/src/uv-common.h
@@ -130,7 +130,10 @@ enum {
UV_SIGNAL_ONE_SHOT = 0x02000000,
/* Only used by uv_poll_t handles. */
- UV_HANDLE_POLL_SLOW = 0x01000000
+ UV_HANDLE_POLL_SLOW = 0x01000000,
+
+ /* Only used by uv_process_t handles. */
+ UV_HANDLE_REAP = 0x10000000
};
int uv__loop_configure(uv_loop_t* loop, uv_loop_option option, va_list ap);

View file

@ -0,0 +1,170 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jeremy Rose <nornagon@nornagon.net>
Date: Mon, 31 Jan 2022 11:49:22 -0800
Subject: process: monitor for exit with kqueue on BSDs (#3441)
This adds a workaround for an xnu kernel bug that sometimes results in
SIGCHLD not being delivered. The workaround is to use kevent to listen
for EVFILT_PROC/NOTE_EXIT events instead of relying on SIGCHLD on *BSD.
Apple rdar: FB9529664
Refs: https://github.com/libuv/libuv/pull/3257
diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
index 12d4da93686e993830a7d09e74d08191fc808f4f..16be13b99f5db77741aa276e90a437ef4eb5ba32 100644
--- a/deps/uv/src/unix/internal.h
+++ b/deps/uv/src/unix/internal.h
@@ -282,6 +282,7 @@ uv_handle_type uv__handle_type(int fd);
FILE* uv__open_file(const char* path);
int uv__getpwuid_r(uv_passwd_t* pwd);
int uv__search_path(const char* prog, char* buf, size_t* buflen);
+void uv__wait_children(uv_loop_t* loop);
/* random */
int uv__random_devurandom(void* buf, size_t buflen);
diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c
index bf183d5fdc0ba89913469a294322eef84bc4cee8..35200f17495d80ed2d19ef9f6f76bbc92ee042f6 100644
--- a/deps/uv/src/unix/kqueue.c
+++ b/deps/uv/src/unix/kqueue.c
@@ -284,6 +284,11 @@ void uv__io_poll(uv_loop_t* loop, int timeout) {
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
for (i = 0; i < nfds; i++) {
ev = events + i;
+ if (ev->filter == EVFILT_PROC) {
+ uv__wait_children(loop);
+ nevents++;
+ continue;
+ }
fd = ev->ident;
/* Skip invalidated events, see uv__platform_invalidate_fd */
if (fd == -1)
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index f4aebb0490e198cd9adcadfeb6b006de479cc993..cfcba341e0e380ecd595e4b59e39c08a7b374a48 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -48,10 +48,20 @@ extern char **environ;
# include "zos-base.h"
#endif
+#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+#include <sys/event.h>
+#endif
+
+#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
static void uv__chld(uv_signal_t* handle, int signum) {
+ assert(signum == SIGCHLD);
+ uv__wait_children(handle->loop);
+}
+#endif
+
+void uv__wait_children(uv_loop_t* loop) {
uv_process_t* process;
- uv_loop_t* loop;
int exit_status;
int term_signal;
int status;
@@ -60,10 +70,7 @@ static void uv__chld(uv_signal_t* handle, int signum) {
QUEUE* q;
QUEUE* h;
- assert(signum == SIGCHLD);
-
QUEUE_INIT(&pending);
- loop = handle->loop;
h = &loop->process_handles;
q = QUEUE_HEAD(h);
@@ -419,7 +426,9 @@ int uv_spawn(uv_loop_t* loop,
if (err)
goto error;
+#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
+#endif
/* Acquire write lock to prevent opening new fds in worker threads */
uv_rwlock_wrlock(&loop->cloexec_lock);
@@ -478,6 +487,13 @@ int uv_spawn(uv_loop_t* loop,
/* Only activate this handle if exec() happened successfully */
if (exec_errorno == 0) {
+#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+ struct kevent event;
+ EV_SET(&event, pid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, 0);
+ if (kevent(loop->backend_fd, &event, 1, NULL, 0, NULL))
+ abort();
+#endif
+
QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);
uv__handle_start(process);
}
diff --git a/deps/uv/test/test-list.h b/deps/uv/test/test-list.h
index 59b95da9ebe3464bd1f9ce1c534122b1f9e06636..58489c4be7b3a7b36d5b01a1f07d411ef3d99ae3 100644
--- a/deps/uv/test/test-list.h
+++ b/deps/uv/test/test-list.h
@@ -318,6 +318,7 @@ TEST_DECLARE (spawn_reads_child_path)
TEST_DECLARE (spawn_inherit_streams)
TEST_DECLARE (spawn_quoted_path)
TEST_DECLARE (spawn_tcp_server)
+TEST_DECLARE (spawn_exercise_sigchld_issue)
TEST_DECLARE (fs_poll)
TEST_DECLARE (fs_poll_getpath)
TEST_DECLARE (fs_poll_close_request)
@@ -944,6 +945,7 @@ TASK_LIST_START
TEST_ENTRY (spawn_inherit_streams)
TEST_ENTRY (spawn_quoted_path)
TEST_ENTRY (spawn_tcp_server)
+ TEST_ENTRY (spawn_exercise_sigchld_issue)
TEST_ENTRY (fs_poll)
TEST_ENTRY (fs_poll_getpath)
TEST_ENTRY (fs_poll_close_request)
diff --git a/deps/uv/test/test-spawn.c b/deps/uv/test/test-spawn.c
index 9f2eb24b2d6daf339bec12027134409cfc3b4a82..dfd5458ef37c664af9a55a8383bdb3121885db3b 100644
--- a/deps/uv/test/test-spawn.c
+++ b/deps/uv/test/test-spawn.c
@@ -1891,6 +1891,44 @@ TEST_IMPL(spawn_quoted_path) {
#endif
}
+TEST_IMPL(spawn_exercise_sigchld_issue) {
+ int r;
+ int i;
+ uv_process_options_t dummy_options = {0};
+ uv_process_t dummy_processes[100];
+ char* args[2];
+
+ init_process_options("spawn_helper1", exit_cb);
+
+ r = uv_spawn(uv_default_loop(), &process, &options);
+ ASSERT_EQ(r, 0);
+
+ // This test exercises a bug in the darwin kernel that causes SIGCHLD not to
+ // be delivered sometimes. Calling posix_spawn many times increases the
+ // likelihood of encountering this issue, so spin a few times to make this
+ // test more reliable.
+ dummy_options.file = args[0] = "program-that-had-better-not-exist";
+ args[1] = NULL;
+ dummy_options.args = args;
+ dummy_options.exit_cb = fail_cb;
+ dummy_options.flags = 0;
+ for (i = 0; i < 100; i++) {
+ r = uv_spawn(uv_default_loop(), &dummy_processes[i], &dummy_options);
+ if (r != UV_ENOENT)
+ ASSERT_EQ(r, UV_EACCES);
+ uv_close((uv_handle_t*) &dummy_processes[i], close_cb);
+ }
+
+ r = uv_run(uv_default_loop(), UV_RUN_DEFAULT);
+ ASSERT_EQ(r, 0);
+
+ ASSERT_EQ(exit_cb_called, 1);
+ ASSERT_EQ(close_cb_called, 101);
+
+ MAKE_VALGRIND_HAPPY();
+ return 0;
+}
+
/* Helper for child process of spawn_inherit_streams */
#ifndef _WIN32
void spawn_stdin_stdout(void) {

View file

@ -0,0 +1,54 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Sat, 5 Mar 2022 12:52:04 -0500
Subject: process: only use F_DUPFD_CLOEXEC if it is defined (#3512)
We can save a syscall on most modern systems (required by POSIX 2008),
but not on all systems.
Also handle errors from CLOEXEC. Even though fcntl does not really
define there to be any, it could theoretically be EBADF if the user
happened to pass a bad file descriptor to the same number fd (such that
no other code happened to already fail on that).
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index d208f99be40df9f36447552daf2772c1cab1ce79..7705068730cb0536998bad7d304cb87df99b72e8 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -275,9 +275,20 @@ static void uv__process_child_init(const uv_process_options_t* options,
use_fd = pipes[fd][1];
if (use_fd < 0 || use_fd >= fd)
continue;
+#ifdef F_DUPFD_CLOEXEC /* POSIX 2008 */
pipes[fd][1] = fcntl(use_fd, F_DUPFD_CLOEXEC, stdio_count);
+#else
+ pipes[fd][1] = fcntl(use_fd, F_DUPFD, stdio_count);
+#endif
if (pipes[fd][1] == -1)
uv__write_errno(error_fd);
+#ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
+ n = uv__cloexec_fcntl(pipes[fd][1], 1);
+ if (n) {
+ uv__write_int(error_fd, n);
+ _exit(127);
+ }
+#endif
}
for (fd = 0; fd < stdio_count; fd++) {
@@ -300,8 +311,13 @@ static void uv__process_child_init(const uv_process_options_t* options,
}
if (fd == use_fd) {
- if (close_fd == -1)
- uv__cloexec_fcntl(use_fd, 0);
+ if (close_fd == -1) {
+ n = uv__cloexec_fcntl(use_fd, 0);
+ if (n) {
+ uv__write_int(error_fd, n);
+ _exit(127);
+ }
+ }
}
else {
fd = dup2(use_fd, fd);

View file

@ -0,0 +1,36 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Fri, 11 Mar 2022 12:05:24 -0500
Subject: process: reset the signal mask if the fork fails (#3537)
Fix a regression that sneaked into posix spawn changes.
Refs: https://github.com/libuv/libuv/pull/3257
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index b85aa3b94edd040952e0d350a47a38d9ba8a67d3..d208f99be40df9f36447552daf2772c1cab1ce79 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -790,11 +790,6 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
*pid = fork();
- if (*pid == -1) {
- /* Failed to fork */
- return UV__ERR(errno);
- }
-
if (*pid == 0) {
/* Fork succeeded, in the child process */
uv__process_child_init(options, stdio_count, pipes, error_fd);
@@ -804,6 +799,10 @@ static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
abort();
+ if (*pid == -1)
+ /* Failed to fork */
+ return UV__ERR(errno);
+
/* Fork succeeded, in the parent process */
return 0;
}

View file

@ -0,0 +1,63 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Mon, 7 Mar 2022 17:07:49 -0500
Subject: process: simplify uv__write_int calls (#3519)
Refs https://github.com/libuv/libuv/pull/3519
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index b6f9756c6a6710f5f10762b9299cc35047b98097..8cde389b826b6b167437845eccd5fed440dcadc4 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -216,16 +216,14 @@ static void uv__write_int(int fd, int val) {
n = write(fd, &val, sizeof(val));
while (n == -1 && errno == EINTR);
- if (n == -1 && errno == EPIPE)
- return; /* parent process has quit */
-
- assert(n == sizeof(val));
+ /* The write might have failed (e.g. if the parent process has died),
+ * but we have nothing left but to _exit ourself now too. */
+ _exit(127);
}
static void uv__write_errno(int error_fd) {
uv__write_int(error_fd, UV__ERR(errno));
- _exit(127);
}
@@ -284,10 +282,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
uv__write_errno(error_fd);
#ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
n = uv__cloexec(pipes[fd][1], 1);
- if (n) {
+ if (n)
uv__write_int(error_fd, n);
- _exit(127);
- }
#endif
}
@@ -313,10 +309,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
if (fd == use_fd) {
if (close_fd == -1) {
n = uv__cloexec(use_fd, 0);
- if (n) {
+ if (n)
uv__write_int(error_fd, n);
- _exit(127);
- }
}
}
else {
@@ -368,7 +362,6 @@ static void uv__process_child_init(const uv_process_options_t* options,
#endif
uv__write_errno(error_fd);
- abort();
}
#endif

View file

@ -0,0 +1,889 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Wed, 2 Mar 2022 15:15:39 -0500
Subject: Reland "macos: use posix_spawn instead of fork" (#3257)
Fixes: https://github.com/libuv/libuv/issues/3050
Refs: https://github.com/libuv/libuv/issues/3086
Refs: https://github.com/libuv/libuv/pull/3064
Refs: https://github.com/libuv/libuv/pull/3107
Refs: https://github.com/libuv/libuv/pull/3064
This reverts commit 217fdf4265589889d00c7c0622fde2710971a020, then fixes
several issues with it:
* remove error fast-cleanup code that triggers a nodejs bug
Refs: https://github.com/libuv/libuv/pull/3107#issuecomment-782482608
* protect posix_spawn from EINTR
This is not a documented valid error, but seems to have been observed.
* ignore setuid/setgid syscall
This kernel function is not permitted unless the process is setuid root,
so disable this syscall. Falling back to fork/exec should be okay for
the rare cases that the user decides they need to do setuid(getuid()) or
setuid(geteuid()) for the child.
Refs: https://github.com/libuv/libuv/pull/3107#issuecomment-782482608
* improve posix_spawn path search
Ports the improvements in musl back to this function
* fix some additional problems and formatting issues
We previously might fail to start a watcher, in rare failure cases,
resulting in a zombie that we would fail to kill. Also avoid creating
the signal-pipe unless required (addresses a review comment from Apple)
* fix fd->fd mapping reuse
There was a chance that when duplicating the fd's into stdio_count+fd we
might be closing a currently opened fd with that value.
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index 2920b942962357827bae9bcad23af8333e8b007f..b85aa3b94edd040952e0d350a47a38d9ba8a67d3 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -27,6 +27,7 @@
#include <assert.h>
#include <errno.h>
#include <signal.h>
+#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -35,8 +36,21 @@
#include <poll.h>
#if defined(__APPLE__) && !TARGET_OS_IPHONE
+# include <spawn.h>
+# include <paths.h>
+# include <sys/kauth.h>
+# include <sys/types.h>
+# include <sys/sysctl.h>
+# include <dlfcn.h>
# include <crt_externs.h>
+# include <xlocale.h>
# define environ (*_NSGetEnviron())
+
+/* macOS 10.14 back does not define this constant */
+# ifndef POSIX_SPAWN_SETSID
+# define POSIX_SPAWN_SETSID 1024
+# endif
+
#else
extern char **environ;
#endif
@@ -261,22 +275,22 @@ static void uv__process_child_init(const uv_process_options_t* options,
use_fd = pipes[fd][1];
if (use_fd < 0 || use_fd >= fd)
continue;
- pipes[fd][1] = fcntl(use_fd, F_DUPFD, stdio_count);
+ pipes[fd][1] = fcntl(use_fd, F_DUPFD_CLOEXEC, stdio_count);
if (pipes[fd][1] == -1)
uv__write_errno(error_fd);
}
for (fd = 0; fd < stdio_count; fd++) {
- close_fd = pipes[fd][0];
+ close_fd = -1;
use_fd = pipes[fd][1];
if (use_fd < 0) {
if (fd >= 3)
continue;
else {
- /* redirect stdin, stdout and stderr to /dev/null even if UV_IGNORE is
- * set
- */
+ /* Redirect stdin, stdout and stderr to /dev/null even if UV_IGNORE is
+ * set. */
+ uv__close_nocheckstdio(fd); /* Free up fd, if it happens to be open. */
use_fd = open("/dev/null", fd == 0 ? O_RDONLY : O_RDWR);
close_fd = use_fd;
@@ -285,28 +299,24 @@ static void uv__process_child_init(const uv_process_options_t* options,
}
}
- if (fd == use_fd)
- uv__cloexec_fcntl(use_fd, 0);
- else
+ if (fd == use_fd) {
+ if (close_fd == -1)
+ uv__cloexec_fcntl(use_fd, 0);
+ }
+ else {
fd = dup2(use_fd, fd);
+ }
if (fd == -1)
uv__write_errno(error_fd);
- if (fd <= 2)
+ if (fd <= 2 && close_fd == -1)
uv__nonblock_fcntl(fd, 0);
if (close_fd >= stdio_count)
uv__close(close_fd);
}
- for (fd = 0; fd < stdio_count; fd++) {
- use_fd = pipes[fd][1];
-
- if (use_fd >= stdio_count)
- uv__close(use_fd);
- }
-
if (options->cwd != NULL && chdir(options->cwd))
uv__write_errno(error_fd);
@@ -327,9 +337,8 @@ static void uv__process_child_init(const uv_process_options_t* options,
if ((options->flags & UV_PROCESS_SETUID) && setuid(options->uid))
uv__write_errno(error_fd);
- if (options->env != NULL) {
+ if (options->env != NULL)
environ = options->env;
- }
/* Reset signal mask just before exec. */
sigemptyset(&signewset);
@@ -348,6 +357,562 @@ static void uv__process_child_init(const uv_process_options_t* options,
#endif
+#if defined(__APPLE__)
+typedef struct uv__posix_spawn_fncs_tag {
+ struct {
+ int (*addchdir_np)(const posix_spawn_file_actions_t *, const char *);
+ } file_actions;
+} uv__posix_spawn_fncs_t;
+
+
+static uv_once_t posix_spawn_init_once = UV_ONCE_INIT;
+static uv__posix_spawn_fncs_t posix_spawn_fncs;
+static int posix_spawn_can_use_setsid;
+
+
+static void uv__spawn_init_posix_spawn_fncs(void) {
+ /* Try to locate all non-portable functions at runtime */
+ posix_spawn_fncs.file_actions.addchdir_np =
+ dlsym(RTLD_DEFAULT, "posix_spawn_file_actions_addchdir_np");
+}
+
+
+static void uv__spawn_init_can_use_setsid(void) {
+ static const int MACOS_CATALINA_VERSION_MAJOR = 19;
+ char version_str[256];
+ char* version_major_str;
+ size_t version_str_size = 256;
+ int r;
+ int version_major;
+
+ /* Get a version string */
+ r = sysctlbyname("kern.osrelease", version_str, &version_str_size, NULL, 0);
+ if (r != 0)
+ return;
+
+ /* Try to get the major version number. If not found
+ * fall back to the fork/exec flow */
+ version_major_str = strtok(version_str, ".");
+ if (version_major_str == NULL)
+ return;
+
+ /* Parse the version major as a number. If it is greater than
+ * the major version for macOS Catalina (aka macOS 10.15), then
+ * the POSIX_SPAWN_SETSID flag is available */
+ version_major = atoi_l(version_major_str, NULL); /* Use LC_C_LOCALE */
+ if (version_major >= MACOS_CATALINA_VERSION_MAJOR)
+ posix_spawn_can_use_setsid = 1;
+}
+
+
+static void uv__spawn_init_posix_spawn(void) {
+ /* Init handles to all potentially non-defined functions */
+ uv__spawn_init_posix_spawn_fncs();
+
+ /* Init feature detection for POSIX_SPAWN_SETSID flag */
+ uv__spawn_init_can_use_setsid();
+}
+
+
+static int uv__spawn_set_posix_spawn_attrs(
+ posix_spawnattr_t* attrs,
+ const uv__posix_spawn_fncs_t* posix_spawn_fncs,
+ const uv_process_options_t* options) {
+ int err;
+ unsigned int flags;
+ sigset_t signal_set;
+
+ err = posix_spawnattr_init(attrs);
+ if (err != 0) {
+ /* If initialization fails, no need to de-init, just return */
+ return err;
+ }
+
+ if (options->flags & (UV_PROCESS_SETUID | UV_PROCESS_SETGID)) {
+ /* kauth_cred_issuser currently requires exactly uid == 0 for these
+ * posixspawn_attrs (set_groups_np, setuid_np, setgid_np), which deviates
+ * from the normal specification of setuid (which also uses euid), and they
+ * are also undocumented syscalls, so we do not use them. */
+ err = ENOSYS;
+ goto error;
+ }
+
+ /* Set flags for spawn behavior
+ * 1) POSIX_SPAWN_CLOEXEC_DEFAULT: (Apple Extension) All descriptors in the
+ * parent will be treated as if they had been created with O_CLOEXEC. The
+ * only fds that will be passed on to the child are those manipulated by
+ * the file actions
+ * 2) POSIX_SPAWN_SETSIGDEF: Signals mentioned in spawn-sigdefault in the
+ * spawn attributes will be reset to behave as their default
+ * 3) POSIX_SPAWN_SETSIGMASK: Signal mask will be set to the value of
+ * spawn-sigmask in attributes
+ * 4) POSIX_SPAWN_SETSID: Make the process a new session leader if a detached
+ * session was requested. */
+ flags = POSIX_SPAWN_CLOEXEC_DEFAULT |
+ POSIX_SPAWN_SETSIGDEF |
+ POSIX_SPAWN_SETSIGMASK;
+ if (options->flags & UV_PROCESS_DETACHED) {
+ /* If running on a version of macOS where this flag is not supported,
+ * revert back to the fork/exec flow. Otherwise posix_spawn will
+ * silently ignore the flag. */
+ if (!posix_spawn_can_use_setsid) {
+ err = ENOSYS;
+ goto error;
+ }
+
+ flags |= POSIX_SPAWN_SETSID;
+ }
+ err = posix_spawnattr_setflags(attrs, flags);
+ if (err != 0)
+ goto error;
+
+ /* Reset all signal the child to their default behavior */
+ sigfillset(&signal_set);
+ err = posix_spawnattr_setsigdefault(attrs, &signal_set);
+ if (err != 0)
+ goto error;
+
+ /* Reset the signal mask for all signals */
+ sigemptyset(&signal_set);
+ err = posix_spawnattr_setsigmask(attrs, &signal_set);
+ if (err != 0)
+ goto error;
+
+ return err;
+
+error:
+ (void) posix_spawnattr_destroy(attrs);
+ return err;
+}
+
+
+static int uv__spawn_set_posix_spawn_file_actions(
+ posix_spawn_file_actions_t* actions,
+ const uv__posix_spawn_fncs_t* posix_spawn_fncs,
+ const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2]) {
+ int fd;
+ int fd2;
+ int use_fd;
+ int err;
+
+ err = posix_spawn_file_actions_init(actions);
+ if (err != 0) {
+ /* If initialization fails, no need to de-init, just return */
+ return err;
+ }
+
+ /* Set the current working directory if requested */
+ if (options->cwd != NULL) {
+ if (posix_spawn_fncs->file_actions.addchdir_np == NULL) {
+ err = ENOSYS;
+ goto error;
+ }
+
+ err = posix_spawn_fncs->file_actions.addchdir_np(actions, options->cwd);
+ if (err != 0)
+ goto error;
+ }
+
+ /* Do not return ENOSYS after this point, as we may mutate pipes. */
+
+ /* First duplicate low numbered fds, since it's not safe to duplicate them,
+ * they could get replaced. Example: swapping stdout and stderr; without
+ * this fd 2 (stderr) would be duplicated into fd 1, thus making both
+ * stdout and stderr go to the same fd, which was not the intention. */
+ for (fd = 0; fd < stdio_count; fd++) {
+ use_fd = pipes[fd][1];
+ if (use_fd < 0 || use_fd >= fd)
+ continue;
+ use_fd = stdio_count;
+ for (fd2 = 0; fd2 < stdio_count; fd2++) {
+ /* If we were not setting POSIX_SPAWN_CLOEXEC_DEFAULT, we would need to
+ * also consider whether fcntl(fd, F_GETFD) returned without the
+ * FD_CLOEXEC flag set. */
+ if (pipes[fd2][1] == use_fd) {
+ use_fd++;
+ fd2 = 0;
+ }
+ }
+ err = posix_spawn_file_actions_adddup2(
+ actions,
+ pipes[fd][1],
+ use_fd);
+ assert(err != ENOSYS);
+ if (err != 0)
+ goto error;
+ pipes[fd][1] = use_fd;
+ }
+
+ /* Second, move the descriptors into their respective places */
+ for (fd = 0; fd < stdio_count; fd++) {
+ use_fd = pipes[fd][1];
+ if (use_fd < 0) {
+ if (fd >= 3)
+ continue;
+ else {
+ /* If ignored, redirect to (or from) /dev/null, */
+ err = posix_spawn_file_actions_addopen(
+ actions,
+ fd,
+ "/dev/null",
+ fd == 0 ? O_RDONLY : O_RDWR,
+ 0);
+ assert(err != ENOSYS);
+ if (err != 0)
+ goto error;
+ continue;
+ }
+ }
+
+ if (fd == use_fd)
+ err = posix_spawn_file_actions_addinherit_np(actions, fd);
+ else
+ err = posix_spawn_file_actions_adddup2(actions, use_fd, fd);
+ assert(err != ENOSYS);
+ if (err != 0)
+ goto error;
+
+ /* Make sure the fd is marked as non-blocking (state shared between child
+ * and parent). */
+ uv__nonblock_fcntl(use_fd, 0);
+ }
+
+ /* Finally, close all the superfluous descriptors */
+ for (fd = 0; fd < stdio_count; fd++) {
+ use_fd = pipes[fd][1];
+ if (use_fd < stdio_count)
+ continue;
+
+ /* Check if we already closed this. */
+ for (fd2 = 0; fd2 < fd; fd2++) {
+ if (pipes[fd2][1] == use_fd)
+ break;
+ }
+ if (fd2 < fd)
+ continue;
+
+ err = posix_spawn_file_actions_addclose(actions, use_fd);
+ assert(err != ENOSYS);
+ if (err != 0)
+ goto error;
+ }
+
+ return 0;
+
+error:
+ (void) posix_spawn_file_actions_destroy(actions);
+ return err;
+}
+
+char* uv__spawn_find_path_in_env(char** env) {
+ char** env_iterator;
+ const char path_var[] = "PATH=";
+
+ /* Look for an environment variable called PATH in the
+ * provided env array, and return its value if found */
+ for (env_iterator = env; *env_iterator != NULL; env_iterator++) {
+ if (strncmp(*env_iterator, path_var, sizeof(path_var) - 1) == 0) {
+ /* Found "PATH=" at the beginning of the string */
+ return *env_iterator + sizeof(path_var) - 1;
+ }
+ }
+
+ return NULL;
+}
+
+
+static int uv__spawn_resolve_and_spawn(const uv_process_options_t* options,
+ posix_spawnattr_t* attrs,
+ posix_spawn_file_actions_t* actions,
+ pid_t* pid) {
+ const char *p;
+ const char *z;
+ const char *path;
+ size_t l;
+ size_t k;
+ int err;
+ int seen_eacces;
+
+ path = NULL;
+ err = -1;
+ seen_eacces = 0;
+
+ /* Short circuit for erroneous case */
+ if (options->file == NULL)
+ return ENOENT;
+
+ /* The environment for the child process is that of the parent unless overriden
+ * by options->env */
+ char** env = environ;
+ if (options->env != NULL)
+ env = options->env;
+
+ /* If options->file contains a slash, posix_spawn/posix_spawnp behave
+ * the same, and don't involve PATH resolution at all. Otherwise, if
+ * options->file does not include a slash, but no custom environment is
+ * to be used, the environment used for path resolution as well for the
+ * child process is that of the parent process, so posix_spawnp is the
+ * way to go. */
+ if (strchr(options->file, '/') != NULL || options->env == NULL) {
+ do
+ err = posix_spawnp(pid, options->file, actions, attrs, options->args, env);
+ while (err == EINTR);
+ return err;
+ }
+
+ /* Look for the definition of PATH in the provided env */
+ path = uv__spawn_find_path_in_env(options->env);
+
+ /* The following resolution logic (execvpe emulation) is copied from
+ * https://git.musl-libc.org/cgit/musl/tree/src/process/execvp.c
+ * and adapted to work for our specific usage */
+
+ /* If no path was provided in options->env, use the default value
+ * to look for the executable */
+ if (path == NULL)
+ path = _PATH_DEFPATH;
+
+ k = strnlen(options->file, NAME_MAX + 1);
+ if (k > NAME_MAX)
+ return ENAMETOOLONG;
+
+ l = strnlen(path, PATH_MAX - 1) + 1;
+
+ for (p = path;; p = z) {
+ /* Compose the new process file from the entry in the PATH
+ * environment variable and the actual file name */
+ char b[PATH_MAX + NAME_MAX];
+ z = strchr(p, ':');
+ if (!z)
+ z = p + strlen(p);
+ if ((size_t)(z - p) >= l) {
+ if (!*z++)
+ break;
+
+ continue;
+ }
+ memcpy(b, p, z - p);
+ b[z - p] = '/';
+ memcpy(b + (z - p) + (z > p), options->file, k + 1);
+
+ /* Try to spawn the new process file. If it fails with ENOENT, the
+ * new process file is not in this PATH entry, continue with the next
+ * PATH entry. */
+ do
+ err = posix_spawn(pid, b, actions, attrs, options->args, env);
+ while (err == EINTR);
+
+ switch (err) {
+ case EACCES:
+ seen_eacces = 1;
+ break; /* continue search */
+ case ENOENT:
+ case ENOTDIR:
+ break; /* continue search */
+ default:
+ return err;
+ }
+
+ if (!*z++)
+ break;
+ }
+
+ if (seen_eacces)
+ return EACCES;
+ return err;
+}
+
+
+static int uv__spawn_and_init_child_posix_spawn(
+ const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2],
+ pid_t* pid,
+ const uv__posix_spawn_fncs_t* posix_spawn_fncs) {
+ int err;
+ posix_spawnattr_t attrs;
+ posix_spawn_file_actions_t actions;
+
+ err = uv__spawn_set_posix_spawn_attrs(&attrs, posix_spawn_fncs, options);
+ if (err != 0)
+ goto error;
+
+ /* This may mutate pipes. */
+ err = uv__spawn_set_posix_spawn_file_actions(&actions,
+ posix_spawn_fncs,
+ options,
+ stdio_count,
+ pipes);
+ if (err != 0) {
+ (void) posix_spawnattr_destroy(&attrs);
+ goto error;
+ }
+
+ /* Try to spawn options->file resolving in the provided environment
+ * if any */
+ err = uv__spawn_resolve_and_spawn(options, &attrs, &actions, pid);
+ assert(err != ENOSYS);
+
+ /* Destroy the actions/attributes */
+ (void) posix_spawn_file_actions_destroy(&actions);
+ (void) posix_spawnattr_destroy(&attrs);
+
+error:
+ /* In an error situation, the attributes and file actions are
+ * already destroyed, only the happy path requires cleanup */
+ return UV__ERR(err);
+}
+#endif
+
+static int uv__spawn_and_init_child_fork(const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2],
+ int error_fd,
+ pid_t* pid) {
+ sigset_t signewset;
+ sigset_t sigoldset;
+
+ /* Start the child with most signals blocked, to avoid any issues before we
+ * can reset them, but allow program failures to exit (and not hang). */
+ sigfillset(&signewset);
+ sigdelset(&signewset, SIGKILL);
+ sigdelset(&signewset, SIGSTOP);
+ sigdelset(&signewset, SIGTRAP);
+ sigdelset(&signewset, SIGSEGV);
+ sigdelset(&signewset, SIGBUS);
+ sigdelset(&signewset, SIGILL);
+ sigdelset(&signewset, SIGSYS);
+ sigdelset(&signewset, SIGABRT);
+ if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
+ abort();
+
+ *pid = fork();
+
+ if (*pid == -1) {
+ /* Failed to fork */
+ return UV__ERR(errno);
+ }
+
+ if (*pid == 0) {
+ /* Fork succeeded, in the child process */
+ uv__process_child_init(options, stdio_count, pipes, error_fd);
+ abort();
+ }
+
+ if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
+ abort();
+
+ /* Fork succeeded, in the parent process */
+ return 0;
+}
+
+static int uv__spawn_and_init_child(
+ uv_loop_t* loop,
+ const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2],
+ pid_t* pid) {
+ int signal_pipe[2] = { -1, -1 };
+ int status;
+ int err;
+ int exec_errorno;
+ ssize_t r;
+
+#if defined(__APPLE__)
+ uv_once(&posix_spawn_init_once, uv__spawn_init_posix_spawn);
+
+ /* Special child process spawn case for macOS Big Sur (11.0) onwards
+ *
+ * Big Sur introduced a significant performance degradation on a call to
+ * fork/exec when the process has many pages mmaped in with MAP_JIT, like, say
+ * a javascript interpreter. Electron-based applications, for example,
+ * are impacted; though the magnitude of the impact depends on how much the
+ * app relies on subprocesses.
+ *
+ * On macOS, though, posix_spawn is implemented in a way that does not
+ * exhibit the problem. This block implements the forking and preparation
+ * logic with posix_spawn and its related primitives. It also takes advantage of
+ * the macOS extension POSIX_SPAWN_CLOEXEC_DEFAULT that makes impossible to
+ * leak descriptors to the child process. */
+ err = uv__spawn_and_init_child_posix_spawn(options,
+ stdio_count,
+ pipes,
+ pid,
+ &posix_spawn_fncs);
+
+ /* The posix_spawn flow will return UV_ENOSYS if any of the posix_spawn_x_np
+ * non-standard functions is both _needed_ and _undefined_. In those cases,
+ * default back to the fork/execve strategy. For all other errors, just fail. */
+ if (err != UV_ENOSYS)
+ return err;
+
+#endif
+
+ /* This pipe is used by the parent to wait until
+ * the child has called `execve()`. We need this
+ * to avoid the following race condition:
+ *
+ * if ((pid = fork()) > 0) {
+ * kill(pid, SIGTERM);
+ * }
+ * else if (pid == 0) {
+ * execve("/bin/cat", argp, envp);
+ * }
+ *
+ * The parent sends a signal immediately after forking.
+ * Since the child may not have called `execve()` yet,
+ * there is no telling what process receives the signal,
+ * our fork or /bin/cat.
+ *
+ * To avoid ambiguity, we create a pipe with both ends
+ * marked close-on-exec. Then, after the call to `fork()`,
+ * the parent polls the read end until it EOFs or errors with EPIPE.
+ */
+ err = uv__make_pipe(signal_pipe, 0);
+ if (err)
+ return err;
+
+ /* Acquire write lock to prevent opening new fds in worker threads */
+ uv_rwlock_wrlock(&loop->cloexec_lock);
+
+ err = uv__spawn_and_init_child_fork(options, stdio_count, pipes, signal_pipe[1], pid);
+
+ /* Release lock in parent process */
+ uv_rwlock_wrunlock(&loop->cloexec_lock);
+
+ uv__close(signal_pipe[1]);
+
+ if (err == 0) {
+ do
+ r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno));
+ while (r == -1 && errno == EINTR);
+
+ if (r == 0)
+ ; /* okay, EOF */
+ else if (r == sizeof(exec_errorno)) {
+ do
+ err = waitpid(*pid, &status, 0); /* okay, read errorno */
+ while (err == -1 && errno == EINTR);
+ assert(err == *pid);
+ err = exec_errorno;
+ } else if (r == -1 && errno == EPIPE) {
+ /* Something unknown happened to our child before spawn */
+ do
+ err = waitpid(*pid, &status, 0); /* okay, got EPIPE */
+ while (err == -1 && errno == EINTR);
+ assert(err == *pid);
+ err = UV_EPIPE;
+ } else
+ abort();
+ }
+
+ uv__close_nocheckstdio(signal_pipe[0]);
+
+ return err;
+}
+
int uv_spawn(uv_loop_t* loop,
uv_process_t* process,
const uv_process_options_t* options) {
@@ -355,18 +920,13 @@ int uv_spawn(uv_loop_t* loop,
/* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
return UV_ENOSYS;
#else
- sigset_t signewset;
- sigset_t sigoldset;
- int signal_pipe[2] = { -1, -1 };
int pipes_storage[8][2];
int (*pipes)[2];
int stdio_count;
- ssize_t r;
pid_t pid;
int err;
int exec_errorno;
int i;
- int status;
assert(options->file != NULL);
assert(!(options->flags & ~(UV_PROCESS_DETACHED |
@@ -379,6 +939,7 @@ int uv_spawn(uv_loop_t* loop,
uv__handle_init(loop, (uv_handle_t*)process, UV_PROCESS);
QUEUE_INIT(&process->queue);
+ process->status = 0;
stdio_count = options->stdio_count;
if (stdio_count < 3)
@@ -403,106 +964,24 @@ int uv_spawn(uv_loop_t* loop,
goto error;
}
- /* This pipe is used by the parent to wait until
- * the child has called `execve()`. We need this
- * to avoid the following race condition:
- *
- * if ((pid = fork()) > 0) {
- * kill(pid, SIGTERM);
- * }
- * else if (pid == 0) {
- * execve("/bin/cat", argp, envp);
- * }
- *
- * The parent sends a signal immediately after forking.
- * Since the child may not have called `execve()` yet,
- * there is no telling what process receives the signal,
- * our fork or /bin/cat.
- *
- * To avoid ambiguity, we create a pipe with both ends
- * marked close-on-exec. Then, after the call to `fork()`,
- * the parent polls the read end until it EOFs or errors with EPIPE.
- */
- err = uv__make_pipe(signal_pipe, 0);
- if (err)
- goto error;
-
#if !(defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__))
uv_signal_start(&loop->child_watcher, uv__chld, SIGCHLD);
#endif
- /* Acquire write lock to prevent opening new fds in worker threads */
- uv_rwlock_wrlock(&loop->cloexec_lock);
-
- /* Start the child with most signals blocked, to avoid any issues before we
- * can reset them, but allow program failures to exit (and not hang). */
- sigfillset(&signewset);
- sigdelset(&signewset, SIGKILL);
- sigdelset(&signewset, SIGSTOP);
- sigdelset(&signewset, SIGTRAP);
- sigdelset(&signewset, SIGSEGV);
- sigdelset(&signewset, SIGBUS);
- sigdelset(&signewset, SIGILL);
- sigdelset(&signewset, SIGSYS);
- sigdelset(&signewset, SIGABRT);
- if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
- abort();
-
- pid = fork();
- if (pid == -1)
- err = UV__ERR(errno);
-
- if (pid == 0)
- uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
+ /* Spawn the child */
+ exec_errorno = uv__spawn_and_init_child(loop, options, stdio_count, pipes, &pid);
- if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
- abort();
-
- /* Release lock in parent process */
- uv_rwlock_wrunlock(&loop->cloexec_lock);
-
- uv__close(signal_pipe[1]);
-
- if (pid == -1) {
- uv__close(signal_pipe[0]);
- goto error;
- }
-
- process->status = 0;
- exec_errorno = 0;
- do
- r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno));
- while (r == -1 && errno == EINTR);
-
- if (r == 0)
- ; /* okay, EOF */
- else if (r == sizeof(exec_errorno)) {
- do
- err = waitpid(pid, &status, 0); /* okay, read errorno */
- while (err == -1 && errno == EINTR);
- assert(err == pid);
- } else if (r == -1 && errno == EPIPE) {
- do
- err = waitpid(pid, &status, 0); /* okay, got EPIPE */
- while (err == -1 && errno == EINTR);
- assert(err == pid);
- } else
- abort();
-
- uv__close_nocheckstdio(signal_pipe[0]);
-
- for (i = 0; i < options->stdio_count; i++) {
- err = uv__process_open_stream(options->stdio + i, pipes[i]);
- if (err == 0)
- continue;
-
- while (i--)
- uv__process_close_stream(options->stdio + i);
-
- goto error;
- }
+#if 0
+ /* This runs into a nodejs issue (it expects initialized streams, even if the
+ * exec failed).
+ * See https://github.com/libuv/libuv/pull/3107#issuecomment-782482608 */
+ if (exec_errorno != 0)
+ goto error;
+#endif
- /* Only activate this handle if exec() happened successfully */
+ /* Activate this handle if exec() happened successfully, even if we later
+ * fail to open a stdio handle. This ensures we can eventually reap the child
+ * with waitpid. */
if (exec_errorno == 0) {
#if defined(__APPLE__) || defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
struct kevent event;
@@ -515,12 +994,22 @@ int uv_spawn(uv_loop_t* loop,
}
#endif
+ process->pid = pid;
+ process->exit_cb = options->exit_cb;
QUEUE_INSERT_TAIL(&loop->process_handles, &process->queue);
uv__handle_start(process);
}
- process->pid = pid;
- process->exit_cb = options->exit_cb;
+ for (i = 0; i < options->stdio_count; i++) {
+ err = uv__process_open_stream(options->stdio + i, pipes[i]);
+ if (err == 0)
+ continue;
+
+ while (i--)
+ uv__process_close_stream(options->stdio + i);
+
+ goto error;
+ }
if (pipes != pipes_storage)
uv__free(pipes);

View file

@ -0,0 +1,173 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Thu, 29 Jul 2021 12:09:51 -0400
Subject: unix: protect fork in uv_spawn from signals
Years ago, we found that various kernels (linux, macOS) were known to
fail if they try to deliver a signal during this syscall, so we prevent
that from happening. They may have fixed those issues, but it is
generally just a bad time for signals to arrive (glibc blocks them here,
for example, including some more internal ones that it won't let us
touch here).
We try to be a bit conservative, and leave many signals unblocked which
could happen during normal execution and should terminate the process if
they do. There is a small race window after the child starts before we
clear the old handlers, if the user was to send an fake signal from
elsewhere, but that should be quite unlikely.
PR-URL: https://github.com/libuv/libuv/pull/3251
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index cfcba341e0e380ecd595e4b59e39c08a7b374a48..c1f6bd4b0076f0835caf83c45a6a896e7ae5def9 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -26,6 +26,7 @@
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
+#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -223,13 +224,32 @@ static void uv__process_child_init(const uv_process_options_t* options,
int stdio_count,
int (*pipes)[2],
int error_fd) {
- sigset_t set;
+ sigset_t signewset;
int close_fd;
int use_fd;
- int err;
int fd;
int n;
+ /* Reset signal disposition first. Use a hard-coded limit because NSIG is not
+ * fixed on Linux: it's either 32, 34 or 64, depending on whether RT signals
+ * are enabled. We are not allowed to touch RT signal handlers, glibc uses
+ * them internally.
+ */
+ for (n = 1; n < 32; n += 1) {
+ if (n == SIGKILL || n == SIGSTOP)
+ continue; /* Can't be changed. */
+
+#if defined(__HAIKU__)
+ if (n == SIGKILLTHR)
+ continue; /* Can't be changed. */
+#endif
+
+ if (SIG_ERR != signal(n, SIG_DFL))
+ continue;
+
+ uv__write_errno(error_fd);
+ }
+
if (options->flags & UV_PROCESS_DETACHED)
setsid();
@@ -311,32 +331,10 @@ static void uv__process_child_init(const uv_process_options_t* options,
environ = options->env;
}
- /* Reset signal disposition. Use a hard-coded limit because NSIG
- * is not fixed on Linux: it's either 32, 34 or 64, depending on
- * whether RT signals are enabled. We are not allowed to touch
- * RT signal handlers, glibc uses them internally.
- */
- for (n = 1; n < 32; n += 1) {
- if (n == SIGKILL || n == SIGSTOP)
- continue; /* Can't be changed. */
-
-#if defined(__HAIKU__)
- if (n == SIGKILLTHR)
- continue; /* Can't be changed. */
-#endif
-
- if (SIG_ERR != signal(n, SIG_DFL))
- continue;
-
- uv__write_errno(error_fd);
- }
-
- /* Reset signal mask. */
- sigemptyset(&set);
- err = pthread_sigmask(SIG_SETMASK, &set, NULL);
-
- if (err != 0)
- uv__write_errno(error_fd);
+ /* Reset signal mask just before exec. */
+ sigemptyset(&signewset);
+ if (sigprocmask(SIG_SETMASK, &signewset, NULL) != 0)
+ abort();
#ifdef __MVS__
execvpe(options->file, options->args, environ);
@@ -345,6 +343,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
#endif
uv__write_errno(error_fd);
+ abort();
}
#endif
@@ -356,6 +355,8 @@ int uv_spawn(uv_loop_t* loop,
/* fork is marked __WATCHOS_PROHIBITED __TVOS_PROHIBITED. */
return UV_ENOSYS;
#else
+ sigset_t signewset;
+ sigset_t sigoldset;
int signal_pipe[2] = { -1, -1 };
int pipes_storage[8][2];
int (*pipes)[2];
@@ -432,25 +433,41 @@ int uv_spawn(uv_loop_t* loop,
/* Acquire write lock to prevent opening new fds in worker threads */
uv_rwlock_wrlock(&loop->cloexec_lock);
- pid = fork();
- if (pid == -1) {
+ /* Start the child with most signals blocked, to avoid any issues before we
+ * can reset them, but allow program failures to exit (and not hang). */
+ sigfillset(&signewset);
+ sigdelset(&signewset, SIGKILL);
+ sigdelset(&signewset, SIGSTOP);
+ sigdelset(&signewset, SIGTRAP);
+ sigdelset(&signewset, SIGSEGV);
+ sigdelset(&signewset, SIGBUS);
+ sigdelset(&signewset, SIGILL);
+ sigdelset(&signewset, SIGSYS);
+ sigdelset(&signewset, SIGABRT);
+ if (pthread_sigmask(SIG_BLOCK, &signewset, &sigoldset) != 0)
+ abort();
+
+ pid = fork();
+ if (pid == -1)
err = UV__ERR(errno);
- uv_rwlock_wrunlock(&loop->cloexec_lock);
- uv__close(signal_pipe[0]);
- uv__close(signal_pipe[1]);
- goto error;
- }
- if (pid == 0) {
+ if (pid == 0)
uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
+
+ if (pthread_sigmask(SIG_SETMASK, &sigoldset, NULL) != 0)
abort();
- }
/* Release lock in parent process */
uv_rwlock_wrunlock(&loop->cloexec_lock);
+
uv__close(signal_pipe[1]);
+ if (pid == -1) {
+ uv__close(signal_pipe[0]);
+ goto error;
+ }
+
process->status = 0;
exec_errorno = 0;
do

View file

@ -0,0 +1,91 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash@gmail.com>
Date: Sun, 6 Mar 2022 15:01:33 -0500
Subject: unix: remove uv__cloexec_ioctl() (#3515)
Now that uv__cloexec_fcntl() is simplified
(https://github.com/libuv/libuv/pull/3492), there is no benefit to
maintaining duplicate code paths for the same thing.
diff --git a/deps/uv/src/unix/core.c b/deps/uv/src/unix/core.c
index 6cd519ad5b3e7af8f5c71b18a59b88458a233f15..8c30802ad15316a8ec20ecedfb5123174d74276b 100644
--- a/deps/uv/src/unix/core.c
+++ b/deps/uv/src/unix/core.c
@@ -597,20 +597,6 @@ int uv__nonblock_ioctl(int fd, int set) {
return 0;
}
-
-
-int uv__cloexec_ioctl(int fd, int set) {
- int r;
-
- do
- r = ioctl(fd, set ? FIOCLEX : FIONCLEX);
- while (r == -1 && errno == EINTR);
-
- if (r)
- return UV__ERR(errno);
-
- return 0;
-}
#endif
@@ -645,7 +631,7 @@ int uv__nonblock_fcntl(int fd, int set) {
}
-int uv__cloexec_fcntl(int fd, int set) {
+int uv__cloexec(int fd, int set) {
int flags;
int r;
diff --git a/deps/uv/src/unix/internal.h b/deps/uv/src/unix/internal.h
index 2dcc8b32f5165dd75061a1b55cc1abd2ab93ccc9..543993989b8765247e140e9d950a25192e5e1ca5 100644
--- a/deps/uv/src/unix/internal.h
+++ b/deps/uv/src/unix/internal.h
@@ -175,11 +175,9 @@ struct uv__stream_queued_fds_s {
defined(__linux__) || \
defined(__OpenBSD__) || \
defined(__NetBSD__)
-#define uv__cloexec uv__cloexec_ioctl
#define uv__nonblock uv__nonblock_ioctl
#define UV__NONBLOCK_IS_IOCTL 1
#else
-#define uv__cloexec uv__cloexec_fcntl
#define uv__nonblock uv__nonblock_fcntl
#define UV__NONBLOCK_IS_IOCTL 0
#endif
@@ -197,8 +195,7 @@ struct uv__stream_queued_fds_s {
#endif
/* core */
-int uv__cloexec_ioctl(int fd, int set);
-int uv__cloexec_fcntl(int fd, int set);
+int uv__cloexec(int fd, int set);
int uv__nonblock_ioctl(int fd, int set);
int uv__nonblock_fcntl(int fd, int set);
int uv__close(int fd); /* preserves errno */
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index 7705068730cb0536998bad7d304cb87df99b72e8..b6f9756c6a6710f5f10762b9299cc35047b98097 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -283,7 +283,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
if (pipes[fd][1] == -1)
uv__write_errno(error_fd);
#ifndef F_DUPFD_CLOEXEC /* POSIX 2008 */
- n = uv__cloexec_fcntl(pipes[fd][1], 1);
+ n = uv__cloexec(pipes[fd][1], 1);
if (n) {
uv__write_int(error_fd, n);
_exit(127);
@@ -312,7 +312,7 @@ static void uv__process_child_init(const uv_process_options_t* options,
if (fd == use_fd) {
if (close_fd == -1) {
- n = uv__cloexec_fcntl(use_fd, 0);
+ n = uv__cloexec(use_fd, 0);
if (n) {
uv__write_int(error_fd, n);
_exit(127);

View file

@ -0,0 +1,36 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Ben Noordhuis <info@bnoordhuis.nl>
Date: Sat, 5 Mar 2022 18:55:49 +0100
Subject: unix: simplify uv__cloexec_fcntl() (#3492)
FD_CLOEXEC is the only defined flag for fcntl(F_SETFD) so don't bother
getting the status of that flag first with fcntl(F_GETFD), just set it.
diff --git a/deps/uv/src/unix/core.c b/deps/uv/src/unix/core.c
index a6425294086ff2f1435fdd6866380d3aaaf68200..6cd519ad5b3e7af8f5c71b18a59b88458a233f15 100644
--- a/deps/uv/src/unix/core.c
+++ b/deps/uv/src/unix/core.c
@@ -649,21 +649,9 @@ int uv__cloexec_fcntl(int fd, int set) {
int flags;
int r;
- do
- r = fcntl(fd, F_GETFD);
- while (r == -1 && errno == EINTR);
-
- if (r == -1)
- return UV__ERR(errno);
-
- /* Bail out now if already set/clear. */
- if (!!(r & FD_CLOEXEC) == !!set)
- return 0;
-
+ flags = 0;
if (set)
- flags = r | FD_CLOEXEC;
- else
- flags = r & ~FD_CLOEXEC;
+ flags = FD_CLOEXEC;
do
r = fcntl(fd, F_SETFD, flags);