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
