Skip to content

Commit

Permalink
darwin,libuv: use posix_spawn
Browse files Browse the repository at this point in the history
Spawning child processes in an Electron application with a hardened runtime has become slow in macOS Big Sur.

This patch is a squashed version of libuv/libuv#3064, with the addition of API availability annotations to fix a build warning (since Electron compiles with the `-Wunguarded-availability-new` flag). This patch should be removed when libuv PR 3064 is merged.

Fixes: libuv/libuv#3050
Fixes: electron#26143
PR-URL: libuv/libuv#3064

Authored-by: Juan Pablo Canepa <[email protected]>
Co-authored-by: Marcello Bastéa-Forte <[email protected]>
Electron patch prepared by: Pat DeSantis <[email protected]>
  • Loading branch information
pdesantis committed Dec 15, 2020
1 parent ef49fea commit 35bfc3f
Show file tree
Hide file tree
Showing 2 changed files with 315 additions and 0 deletions.
1 change: 1 addition & 0 deletions patches/node/.patches
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ src_allow_embedders_to_provide_a_custom_pageallocator_to.patch
allow_preventing_preparestacktracecallback.patch
fix_add_safeforterminationscopes_for_sigint_interruptions.patch
remove_makeexternal_case_for_uncached_internal_strings.patch
macos_libuv_use_posix_spawn.patch
314 changes: 314 additions & 0 deletions patches/node/macos_libuv_use_posix_spawn.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Pat DeSantis <[email protected]>
Date: Tue, 15 Dec 2020 13:28:07 -0500
Subject: macOS,libuv: use posix_spawn

Spawning child processes in an Electron application with a hardened runtime has become slow in macOS Big Sur. See the following issues for more details:
https://github.com/libuv/libuv/issues/3050
https://github.com/electron/electron/issues/26143

This patch is a squashed version of https://github.com/libuv/libuv/pull/3064, with the addition of API availability annotations to fix a build warning (since Electron compiles with the `-Wunguarded-availability-new` flag). This patch should be removed when libuv PR 3064 is merged.

diff --git a/deps/uv/src/unix/darwin-stub.h b/deps/uv/src/unix/darwin-stub.h
index 433e3efa73079e0fe68dcfdf67c3911723326b72..7b95436e3d60f2acda22ab5bacc2a745e8752faf 100644
--- a/deps/uv/src/unix/darwin-stub.h
+++ b/deps/uv/src/unix/darwin-stub.h
@@ -23,6 +23,7 @@
#define UV_DARWIN_STUB_H_

#include <stdint.h>
+#include <spawn.h>

struct CFArrayCallBacks;
struct CFRunLoopSourceContext;
@@ -110,4 +111,9 @@ static const int kFSEventStreamEventFlagRootChanged = 32;
static const int kFSEventStreamEventFlagUnmount = 128;
static const int kFSEventStreamEventFlagUserDropped = 2;

+/* Copied from https://opensource.apple.com/source/xnu/xnu-6153.101.6/libsyscall/wrappers/spawn/spawn_private.h.auto.html */
+int posix_spawnattr_set_uid_np(const posix_spawnattr_t*, uid_t) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0));
+int posix_spawnattr_set_gid_np(const posix_spawnattr_t*, gid_t) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0));
+int posix_spawnattr_set_groups_np(const posix_spawnattr_t*, int, gid_t*, uid_t) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0));
+
#endif /* UV_DARWIN_STUB_H_ */
diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c
index b021aaeba87d0b466341f40a016ef69e8beb7543..1525619866a779e4d4619b93ecbd357354f6b017 100644
--- a/deps/uv/src/unix/process.c
+++ b/deps/uv/src/unix/process.c
@@ -34,8 +34,11 @@
#include <poll.h>

#if defined(__APPLE__) && !TARGET_OS_IPHONE
+#include <spawn.h>
+#include <sys/kauth.h>
# include <crt_externs.h>
# define environ (*_NSGetEnviron())
+#include "darwin-stub.h"
#else
extern char **environ;
#endif
@@ -404,6 +407,239 @@ static void uv__process_child_init(const uv_process_options_t* options,
}
#endif

+#if defined(__APPLE__)
+int uv__spawn_set_posix_spawn_attrs(posix_spawnattr_t* attrs,
+ const uv_process_options_t* options) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0))
+{
+ 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) {
+ err = posix_spawnattr_set_uid_np(attrs, options->uid);
+ if (err != 0)
+ goto error;
+ }
+
+ if (options->flags & UV_PROCESS_SETGID) {
+ err = posix_spawnattr_set_gid_np(attrs, options->gid);
+ if (err != 0)
+ goto error;
+ }
+
+ if (options->flags & (UV_PROCESS_SETUID | UV_PROCESS_SETGID)) {
+ /* See the comment on the call to setgroups in uv__process_child_init above
+ * for why this is not a fatal error */
+ SAVE_ERRNO(posix_spawnattr_set_groups_np(attrs, 0, NULL, KAUTH_UID_NONE));
+ }
+
+ /* 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)
+ 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;
+}
+
+int uv__spawn_set_posix_spawn_file_actions(posix_spawn_file_actions_t* actions,
+ const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2]) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0))
+{
+ int 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) {
+ err = posix_spawn_file_actions_addchdir_np(actions, options->cwd);
+ if (err != 0)
+ goto error;
+ }
+
+ /* First, dupe any required fd into orbit, out of the range of
+ * the descriptors that should be mapped in. */
+ for(fd = 0 ; fd < stdio_count; ++fd) {
+ if (pipes[fd][1] < 0)
+ continue;
+
+ err = posix_spawn_file_actions_adddup2(actions, pipes[fd][1], stdio_count + fd);
+ if (err != 0)
+ goto error;
+ }
+
+ /* Second, move the descriptors into their respective places */
+ for(fd = 0 ; fd < stdio_count; ++fd) {
+ if (pipes[fd][1] < 0)
+ continue;
+
+ err = posix_spawn_file_actions_adddup2(actions, stdio_count + fd, fd);
+ if (err != 0)
+ goto error;
+ }
+
+ /* Finally, close all the superfluous descriptors */
+ for(fd = 0; fd < stdio_count; ++fd) {
+ if (pipes[fd][1] < 0)
+ continue;
+
+ err = posix_spawn_file_actions_addclose(actions, stdio_count + fd);
+ if (err != 0)
+ goto error;
+ }
+
+ /* Finally process the standard streams as per de documentation */
+ for(fd = 0 ; fd < 3 ; ++fd) {
+ /* If ignored, open as /dev/null */
+ const int oflags = fd == 0 ? O_RDONLY : O_RDWR;
+ const int mode = 0;
+
+ if (pipes[fd][1] != -1)
+ continue;
+
+ err = posix_spawn_file_actions_addopen(actions, fd, "/dev/null", oflags, mode);
+ if (err != 0)
+ goto error;
+ }
+
+ return err;
+
+error:
+ (void) posix_spawn_file_actions_destroy(actions);
+ return err;
+}
+
+int uv__spawn_and_init_child_posix_spawn(const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2],
+ pid_t* pid) __API_AVAILABLE(macos(10.15), ios(13.0), tvos(13.0), watchos(6.0)) {
+ int err;
+ posix_spawnattr_t attrs;
+ posix_spawn_file_actions_t actions;
+
+ err = uv__spawn_set_posix_spawn_attrs(&attrs, options);
+ if (err != 0)
+ goto error;
+
+ err = uv__spawn_set_posix_spawn_file_actions(&actions, options, stdio_count, pipes);
+ if (err != 0) {
+ (void) posix_spawnattr_destroy(&attrs);
+ goto error;
+ }
+
+ /* Preserve parent environment if not explicitly set */
+ char** env = options->env ? options->env : environ;
+
+ /* Spawn the child */
+ err = posix_spawnp(pid, options->file, &actions, &attrs, options->args, env);
+
+ /* 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
+
+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) {
+ *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();
+ }
+
+ /* Fork succeeded, in the parent process */
+ return 0;
+}
+
+int uv__spawn_and_init_child(const uv_process_options_t* options,
+ int stdio_count,
+ int (*pipes)[2],
+ int error_fd,
+ pid_t* pid) {
+
+#if defined(__APPLE__)
+ if (__builtin_available(macOS 10.15, *)) {
+ /* Especial 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 poxis_spawn and its related primitves. It also takes advantage of
+ * the macOS extension POSIX_SPAWN_CLOEXEC_DEFAULT that makes impossible to
+ * leak descriptors to the child process.
+ *
+ * see https://github.com/libuv/libuv/issues/3050
+ */
+ return uv__spawn_and_init_child_posix_spawn(options, stdio_count, pipes, pid);
+ } else {
+#endif
+ return uv__spawn_and_init_child_fork(options, stdio_count, pipes, error_fd, pid);
+#if defined(__APPLE__)
+ }
+#endif
+}

int uv_spawn(uv_loop_t* loop,
uv_process_t* process,
@@ -486,21 +722,16 @@ 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) {
- err = UV__ERR(errno);
+ /* Spawn the child */
+ err = uv__spawn_and_init_child(options, stdio_count, pipes, signal_pipe[1], &pid);
+ if (err != 0) {
uv_rwlock_wrunlock(&loop->cloexec_lock);
uv__close(signal_pipe[0]);
uv__close(signal_pipe[1]);
goto error;
}

- if (pid == 0) {
- uv__process_child_init(options, stdio_count, pipes, signal_pipe[1]);
- abort();
- }
-
/* Release lock in parent process */
uv_rwlock_wrunlock(&loop->cloexec_lock);
uv__close(signal_pipe[1]);

0 comments on commit 35bfc3f

Please sign in to comment.