From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Pat DeSantis Date: Mon, 25 Jan 2021 15:53:44 -0500 Subject: darwin,libuv: use posix_spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spawning child processes in an Electron application with a hardened runtime has become slow in macOS Big Sur. This patch is a cherry-pick of https://github.com/libuv/libuv/pull/3064. This patch should be removed when Electron's libuv version is updated to a version containing this fix. Fixes: https://github.com/libuv/libuv/issues/3050 Fixes: https://github.com/electron/electron/issues/26143 PR-URL: https://github.com/libuv/libuv/pull/3064 Authored-by: Juan Pablo Canepa Co-authored-by: Marcello Bastéa-Forte Electron patch prepared by: Pat DeSantis diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index b021aaeba87d0b466341f40a016ef69e8beb7543..b4e44b049f5da2eccb051a6ec0403f5a401636d3 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -34,8 +35,20 @@ #include #if defined(__APPLE__) && !TARGET_OS_IPHONE +# include +# include +# include +# include +# include +# include # include # 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 @@ -44,7 +57,6 @@ extern char **environ; # include #endif - static void uv__chld(uv_signal_t* handle, int signum) { uv_process_t* process; uv_loop_t* loop; @@ -364,9 +376,8 @@ static void uv__process_child_init(const uv_process_options_t* options, _exit(127); } - if (options->env != NULL) { + if (options->env != NULL) 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 @@ -405,6 +416,476 @@ 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 (*set_uid_np)(const posix_spawnattr_t *, uid_t); + int (*set_gid_np)(const posix_spawnattr_t *, gid_t); + int (*set_groups_np)(const posix_spawnattr_t*, int, gid_t*, uid_t); + } spawnattr; + + 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; + + +void uv__spawn_init_posix_spawn_fncs(void) { + /* Try to locate all non-portable functions at runtime */ + posix_spawn_fncs.spawnattr.set_uid_np = + dlsym(RTLD_DEFAULT, "posix_spawnattr_set_uid_np"); + posix_spawn_fncs.spawnattr.set_gid_np = + dlsym(RTLD_DEFAULT, "posix_spawnattr_set_gid_np"); + posix_spawn_fncs.spawnattr.set_groups_np = + dlsym(RTLD_DEFAULT, "posix_spawnattr_set_groups_np"); + posix_spawn_fncs.file_actions.addchdir_np = + dlsym(RTLD_DEFAULT, "posix_spawn_file_actions_addchdir_np"); +} + + +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; + + /* By default, assume failure */ + posix_spawn_can_use_setsid = 0; + + /* 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(version_major_str); + if (version_major >= MACOS_CATALINA_VERSION_MAJOR) + posix_spawn_can_use_setsid = 1; +} + + +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(); +} + + +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) { + if (posix_spawn_fncs->spawnattr.set_uid_np == NULL) { + err = ENOSYS; + goto error; + } + + err = posix_spawn_fncs->spawnattr.set_uid_np(attrs, options->uid); + if (err != 0) + goto error; + } + + if (options->flags & UV_PROCESS_SETGID) { + if (posix_spawn_fncs->spawnattr.set_gid_np == NULL) { + err = ENOSYS; + goto error; + } + + err = posix_spawn_fncs->spawnattr.set_gid_np(attrs, options->gid); + if (err != 0) + goto error; + } + + if (options->flags & (UV_PROCESS_SETUID | UV_PROCESS_SETGID)) { + /* Using ngroups = 0 implied the group_array is empty, and so + * its contents are never traversed. Still the + * posix_spawn_set_groups_np function seems to require that the + * group_array pointer be non-null */ + const int ngroups = 0; + gid_t group_array = KAUTH_GID_NONE; + + if (posix_spawn_fncs->spawnattr.set_groups_np == NULL) { + err = ENOSYS; + goto error; + } + + /* 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_spawn_fncs->spawnattr.set_groups_np( + attrs, + ngroups, + &group_array, + 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) { + /* 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; +} + + +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 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; + } + + /* First, duplicate 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 documentation */ + for (fd = 0; fd < 3; fd++) { + int oflags; + const int mode = 0; + + oflags = fd == 0 ? O_RDONLY : O_RDWR; + + if (pipes[fd][1] != -1) { + /* If not ignored, make sure the fd is marked as non-blocking */ + uv__nonblock_fcntl(pipes[fd][1], 0); + } else { + /* If ignored, redirect to (or from) /dev/null, */ + err = posix_spawn_file_actions_addopen( + actions, + fd, + "/dev/null", + oflags, + mode); + 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; +} + + +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; + + path = NULL; + err = -1; + + /* 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) + return posix_spawnp(pid, options->file, actions, attrs, options->args, env); + + /* 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 taken from + * https://github.com/JuliaLang/libuv/commit/9af3af617138d6a6de7d72819ed362996ff255d9 + * and adapted to work around our own situations */ + + /* 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. */ + err = posix_spawn(pid, b, actions, attrs, options->args, env); + if (err != ENOENT) + return err; + + if (!*z++) + break; + } + + 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, + 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; + + 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); + + /* 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) { + int err; + +#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 + err = uv__spawn_and_init_child_fork(options, stdio_count, pipes, error_fd, pid); + + return err; +} + int uv_spawn(uv_loop_t* loop, uv_process_t* process, const uv_process_options_t* options) { @@ -486,21 +967,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]);