diff options
author | Tavian Barnes <tavianator@tavianator.com> | 2024-05-07 15:42:46 -0400 |
---|---|---|
committer | Tavian Barnes <tavianator@tavianator.com> | 2024-05-07 15:42:46 -0400 |
commit | 452d6697e0f92326ab139eed4eadd9c2fd8b55ca (patch) | |
tree | 0feeb3722dcf6debb6c33c5175342bf1d70a1dba /src/xspawn.c | |
parent | a4299f9bc1d3e60a7e628561e8d650c2a241e1c2 (diff) | |
parent | c5cf2cf90834f2f56b2940d2a499a1a614ebfd21 (diff) | |
download | bfs-452d6697e0f92326ab139eed4eadd9c2fd8b55ca.tar.xz |
Merge branch 'main' into find2fdfind2fd
Diffstat (limited to 'src/xspawn.c')
-rw-r--r-- | src/xspawn.c | 652 |
1 files changed, 495 insertions, 157 deletions
diff --git a/src/xspawn.c b/src/xspawn.c index f76267b..0b0cea4 100644 --- a/src/xspawn.c +++ b/src/xspawn.c @@ -1,40 +1,32 @@ -/**************************************************************************** - * bfs * - * Copyright (C) 2018-2022 Tavian Barnes <tavianator@tavianator.com> * - * * - * Permission to use, copy, modify, and/or distribute this software for any * - * purpose with or without fee is hereby granted. * - * * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - ****************************************************************************/ +// Copyright © Tavian Barnes <tavianator@tavianator.com> +// SPDX-License-Identifier: 0BSD +#include "prelude.h" #include "xspawn.h" +#include "alloc.h" #include "bfstd.h" -#include "config.h" +#include "list.h" #include <errno.h> #include <fcntl.h> -#include <stdbool.h> #include <stdlib.h> #include <string.h> #include <sys/resource.h> #include <sys/types.h> -#include <sys/wait.h> #include <unistd.h> #if BFS_USE_PATHS_H -# include <paths.h> +# include <paths.h> +#endif + +#if _POSIX_SPAWN > 0 +# include <spawn.h> #endif /** * Types of spawn actions. */ enum bfs_spawn_op { + BFS_SPAWN_OPEN, BFS_SPAWN_CLOSE, BFS_SPAWN_DUP2, BFS_SPAWN_FCHDIR, @@ -45,120 +37,486 @@ enum bfs_spawn_op { * A spawn action. */ struct bfs_spawn_action { + /** The next action in the list. */ struct bfs_spawn_action *next; + /** This action's operation. */ enum bfs_spawn_op op; + /** The input fd (or -1). */ int in_fd; + /** The output fd (or -1). */ int out_fd; - int resource; - struct rlimit rlimit; + + /** Operation-specific args. */ + union { + /** BFS_SPAWN_OPEN args. */ + struct { + const char *path; + int flags; + mode_t mode; + }; + + /** BFS_SPAWN_SETRLIMIT args. */ + struct { + int resource; + struct rlimit rlimit; + }; + }; }; int bfs_spawn_init(struct bfs_spawn *ctx) { ctx->flags = 0; - ctx->actions = NULL; - ctx->tail = &ctx->actions; + SLIST_INIT(ctx); + +#if _POSIX_SPAWN > 0 + ctx->flags |= BFS_SPAWN_USE_POSIX; + + errno = posix_spawn_file_actions_init(&ctx->actions); + if (errno != 0) { + return -1; + } + + errno = posix_spawnattr_init(&ctx->attr); + if (errno != 0) { + posix_spawn_file_actions_destroy(&ctx->actions); + return -1; + } +#endif + return 0; } int bfs_spawn_destroy(struct bfs_spawn *ctx) { - struct bfs_spawn_action *action = ctx->actions; - while (action) { - struct bfs_spawn_action *next = action->next; +#if _POSIX_SPAWN > 0 + posix_spawnattr_destroy(&ctx->attr); + posix_spawn_file_actions_destroy(&ctx->actions); +#endif + + for_slist (struct bfs_spawn_action, action, ctx) { free(action); - action = next; } + return 0; } -int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) { - ctx->flags = flags; +#if _POSIX_SPAWN > 0 +/** Set some posix_spawnattr flags. */ +attr(maybe_unused) +static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) { + short prev; + errno = posix_spawnattr_getflags(&ctx->attr, &prev); + if (errno != 0) { + return -1; + } + + short next = prev | flags; + if (next != prev) { + errno = posix_spawnattr_setflags(&ctx->attr, next); + if (errno != 0) { + return -1; + } + } + return 0; } +#endif // _POSIX_SPAWN > 0 -/** Add a spawn action to the chain. */ -static struct bfs_spawn_action *bfs_spawn_add(struct bfs_spawn *ctx, enum bfs_spawn_op op) { - struct bfs_spawn_action *action = malloc(sizeof(*action)); - if (action) { - action->next = NULL; - action->op = op; - action->in_fd = -1; - action->out_fd = -1; - - *ctx->tail = action; - ctx->tail = &action->next; +/** Allocate a spawn action. */ +static struct bfs_spawn_action *bfs_spawn_action(enum bfs_spawn_op op) { + struct bfs_spawn_action *action = ALLOC(struct bfs_spawn_action); + if (!action) { + return NULL; } + + SLIST_ITEM_INIT(action); + action->op = op; + action->in_fd = -1; + action->out_fd = -1; return action; } -int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { - if (fd < 0) { - errno = EBADF; +int bfs_spawn_addopen(struct bfs_spawn *ctx, int fd, const char *path, int flags, mode_t mode) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_OPEN); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE); - if (action) { - action->out_fd = fd; - return 0; - } else { +#if _POSIX_SPAWN > 0 + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = posix_spawn_file_actions_addopen(&ctx->actions, fd, path, flags, mode); + if (errno != 0) { + free(action); + return -1; + } + } +#endif + + action->out_fd = fd; + action->path = path; + action->flags = flags; + action->mode = mode; + SLIST_APPEND(ctx, action); + return 0; +} + +int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_CLOSE); + if (!action) { return -1; } + +#if _POSIX_SPAWN > 0 + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = posix_spawn_file_actions_addclose(&ctx->actions, fd); + if (errno != 0) { + free(action); + return -1; + } + } +#endif + + action->out_fd = fd; + SLIST_APPEND(ctx, action); + return 0; } int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) { - if (oldfd < 0 || newfd < 0) { - errno = EBADF; + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_DUP2); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_DUP2); - if (action) { - action->in_fd = oldfd; - action->out_fd = newfd; - return 0; - } else { - return -1; +#if _POSIX_SPAWN > 0 + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = posix_spawn_file_actions_adddup2(&ctx->actions, oldfd, newfd); + if (errno != 0) { + free(action); + return -1; + } } +#endif + + action->in_fd = oldfd; + action->out_fd = newfd; + SLIST_APPEND(ctx, action); + return 0; } +/** + * https://www.austingroupbugs.net/view.php?id=1208#c4830 says: + * + * ... a search of the directories passed as the environment variable + * PATH ..., using the working directory of the child process after all + * file_actions have been performed. + * + * but macOS and NetBSD resolve the PATH *before* file_actions (because there + * posix_spawn() is its own syscall). + */ +#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__) + int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { - if (fd < 0) { - errno = EBADF; + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR); + if (!action) { return -1; } - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR); - if (action) { - action->in_fd = fd; - return 0; +#if BFS_HAS_POSIX_SPAWN_ADDFCHDIR +# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir +#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP +# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir_np +#endif + +#if _POSIX_SPAWN > 0 && defined(BFS_POSIX_SPAWN_FCHDIR) + if (ctx->flags & BFS_SPAWN_USE_POSIX) { + errno = BFS_POSIX_SPAWN_ADDFCHDIR(&ctx->actions, fd); + if (errno != 0) { + free(action); + return -1; + } + } +#else + ctx->flags &= ~BFS_SPAWN_USE_POSIX; +#endif + + action->in_fd = fd; + SLIST_APPEND(ctx, action); + return 0; +} + +int bfs_spawn_setrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) { + struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_SETRLIMIT); + if (!action) { + goto fail; + } + +#ifdef POSIX_SPAWN_SETRLIMIT + if (bfs_spawn_addflags(ctx, POSIX_SPAWN_SETRLIMIT) != 0) { + goto fail; + } + + errno = posix_spawnattr_setrlimit(&ctx->attr, resource, rl); + if (errno != 0) { + goto fail; + } +#else + ctx->flags &= ~BFS_SPAWN_USE_POSIX; +#endif + + action->resource = resource; + action->rlimit = *rl; + SLIST_APPEND(ctx, action); + return 0; + +fail: + free(action); + return -1; +} + +/** + * Context for resolving executables in the $PATH. + */ +struct bfs_resolver { + /** The executable to spawn. */ + const char *exe; + /** The $PATH to resolve in. */ + char *path; + /** A buffer to hold the resolved path. */ + char *buf; + /** The size of the buffer. */ + size_t len; + /** Whether the executable is already resolved. */ + bool done; + /** Whether to free(path). */ + bool free; +}; + +/** Free a $PATH resolution context. */ +static void bfs_resolve_free(struct bfs_resolver *res) { + if (res->free) { + free(res->path); + } + free(res->buf); +} + +/** Get the next component in the $PATH. */ +static bool bfs_resolve_next(const char **path, const char **next, size_t *len) { + *path = *next; + if (!*path) { + return false; + } + + *next = strchr(*path, ':'); + if (*next) { + *len = *next - *path; + ++*next; } else { - return -1; + *len = strlen(*path); + } + + if (*len == 0) { + // POSIX 8.3: "A zero-length prefix is a legacy feature that + // indicates the current working directory." + *path = "."; + *len = 1; } + + return true; } -int bfs_spawn_addsetrlimit(struct bfs_spawn *ctx, int resource, const struct rlimit *rl) { - struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_SETRLIMIT); - if (action) { - action->resource = resource; - action->rlimit = *rl; +/** Finish resolving an executable, potentially from the child process. */ +static int bfs_resolve_late(struct bfs_resolver *res) { + if (res->done) { return 0; + } + + char *buf = res->buf; + char *end = buf + res->len; + + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + char *cur = xstpencpy(buf, end, path, len); + cur = xstpecpy(cur, end, "/"); + cur = xstpecpy(cur, end, res->exe); + if (cur == end) { + bfs_bug("PATH resolution buffer too small"); + errno = ENOMEM; + return -1; + } + + if (xfaccessat(AT_FDCWD, buf, X_OK) == 0) { + res->exe = buf; + res->done = true; + return 0; + } + } + + errno = ENOENT; + return -1; +} + +/** Check if we can skip path resolution entirely. */ +static bool bfs_can_skip_resolve(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (ctx && !(ctx->flags & BFS_SPAWN_USE_PATH)) { + return true; + } + + if (strchr(res->exe, '/')) { + return true; + } + + return false; +} + +/** Check if any $PATH components are relative. */ +static bool bfs_resolve_relative(const struct bfs_resolver *res) { + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + if (path[0] != '/') { + return true; + } + } + + return false; +} + +/** Check if we can resolve the executable before file actions. */ +static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (!bfs_resolve_relative(res)) { + return true; + } + + if (ctx) { + for_slist (const struct bfs_spawn_action, action, ctx) { + if (action->op == BFS_SPAWN_FCHDIR) { + return false; + } + } + } + + return true; +} + +/** Get the required path resolution buffer size. */ +static size_t bfs_resolve_capacity(const struct bfs_resolver *res) { + size_t max = 0; + + const char *path; + const char *next = res->path; + size_t len; + while (bfs_resolve_next(&path, &next, &len)) { + if (len > max) { + max = len; + } + } + + // path + "/" + exe + '\0' + return max + 1 + strlen(res->exe) + 1; +} + +/** Begin resolving an executable, from the parent process. */ +static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const struct bfs_spawn *ctx) { + *res = (struct bfs_resolver) { + .exe = exe, + }; + + if (bfs_can_skip_resolve(res, ctx)) { + res->done = true; + return 0; + } + + res->path = getenv("PATH"); + if (!res->path) { +#if defined(_CS_PATH) + res->path = xconfstr(_CS_PATH); + res->free = true; +#elif defined(_PATH_DEFPATH) + res->path = _PATH_DEFPATH; +#else + errno = ENOENT; +#endif + } + if (!res->path) { + goto fail; + } + + bool can_finish = bfs_can_resolve_early(res, ctx); + +#if BFS_POSIX_SPAWNP_AFTER_FCHDIR + bool use_posix = ctx && (ctx->flags & BFS_SPAWN_USE_POSIX); + if (!can_finish && use_posix) { + // posix_spawnp() will do the resolution, so don't bother + // allocating a buffer + return 0; + } +#endif + + res->len = bfs_resolve_capacity(res); + res->buf = malloc(res->len); + if (!res->buf) { + goto fail; + } + + if (can_finish && bfs_resolve_late(res) != 0) { + goto fail; + } + + return 0; + +fail: + bfs_resolve_free(res); + return -1; +} + +#if _POSIX_SPAWN > 0 + +/** bfs_spawn() implementation using posix_spawn(). */ +static pid_t bfs_posix_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { + pid_t ret; + + if (res->done) { + errno = posix_spawn(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp); } else { + errno = posix_spawnp(&ret, res->exe, &ctx->actions, &ctx->attr, argv, envp); + } + + if (errno != 0) { return -1; } + + return ret; } -/** Actually exec() the new process. */ -static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { - int error; - const struct bfs_spawn_action *actions = ctx ? ctx->actions : NULL; +/** Check if we can use posix_spawn(). */ +static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs_spawn *ctx) { + if (!(ctx->flags & BFS_SPAWN_USE_POSIX)) { + return false; + } + +#if !BFS_POSIX_SPAWNP_AFTER_FCHDIR + if (!res->done) { + return false; + } +#endif + + return true; +} + +#endif // _POSIX_SPAWN > 0 +/** Actually exec() the new process. */ +static noreturn void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { xclose(pipefd[0]); - for (const struct bfs_spawn_action *action = actions; action; action = action->next) { + for_slist (const struct bfs_spawn_action, action, ctx) { + int fd; + // Move the error-reporting pipe out of the way if necessary... if (action->out_fd == pipefd[1]) { - int fd = dup_cloexec(pipefd[1]); + fd = dup_cloexec(pipefd[1]); if (fd < 0) { goto fail; } @@ -173,6 +531,17 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char ** } switch (action->op) { + case BFS_SPAWN_OPEN: + fd = open(action->path, action->flags, action->mode); + if (fd < 0) { + goto fail; + } + if (fd != action->out_fd) { + if (dup2(fd, action->out_fd) < 0) { + goto fail; + } + } + break; case BFS_SPAWN_CLOSE: if (close(action->out_fd) != 0) { goto fail; @@ -196,38 +565,28 @@ static void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char ** } } - execve(exe, argv, envp); + if (bfs_resolve_late(res) != 0) { + goto fail; + } -fail: - error = errno; + execve(res->exe, argv, envp); + +fail:; + int error = errno; // In case of a write error, the parent will still see that we exited // unsuccessfully, but won't know why - (void) xwrite(pipefd[1], &error, sizeof(error)); + (void)xwrite(pipefd[1], &error, sizeof(error)); xclose(pipefd[1]); _Exit(127); } -pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { - extern char **environ; - if (!envp) { - envp = environ; - } - - enum bfs_spawn_flags flags = ctx ? ctx->flags : 0; - char *resolved = NULL; - if (flags & BFS_SPAWN_USEPATH) { - exe = resolved = bfs_spawn_resolve(exe); - if (!resolved) { - return -1; - } - } - +/** bfs_spawn() implementation using fork()/exec(). */ +static pid_t bfs_fork_spawn(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { // Use a pipe to report errors from the child int pipefd[2]; if (pipe_cloexec(pipefd) != 0) { - free(resolved); return -1; } @@ -235,23 +594,21 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char if (pid < 0) { close_quietly(pipefd[1]); close_quietly(pipefd[0]); - free(resolved); return -1; } else if (pid == 0) { // Child - bfs_spawn_exec(exe, ctx, argv, envp, pipefd); + bfs_spawn_exec(res, ctx, argv, envp, pipefd); } // Parent xclose(pipefd[1]); - free(resolved); int error; ssize_t nbytes = xread(pipefd[0], &error, sizeof(error)); xclose(pipefd[0]); if (nbytes == sizeof(error)) { int wstatus; - waitpid(pid, &wstatus, 0); + xwaitpid(pid, &wstatus, 0); errno = error; return -1; } @@ -259,73 +616,54 @@ pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char return pid; } -char *bfs_spawn_resolve(const char *exe) { - if (strchr(exe, '/')) { - return strdup(exe); +/** Call the right bfs_spawn() implementation. */ +static pid_t bfs_spawn_impl(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp) { +#if _POSIX_SPAWN > 0 + if (bfs_use_posix_spawn(res, ctx)) { + return bfs_posix_spawn(res, ctx, argv, envp); } - - const char *path = getenv("PATH"); - - char *confpath = NULL; - if (!path) { -#if defined(_CS_PATH) - path = confpath = xconfstr(_CS_PATH); -#elif defined(_PATH_DEFPATH) - path = _PATH_DEFPATH; -#else - errno = ENOENT; #endif - } - if (!path) { - return NULL; - } - - size_t cap = 0; - char *ret = NULL; - while (true) { - const char *end = strchr(path, ':'); - size_t len = end ? (size_t)(end - path) : strlen(path); - // POSIX 8.3: "A zero-length prefix is a legacy feature that - // indicates the current working directory." - if (len == 0) { - path = "."; - len = 1; - } + return bfs_fork_spawn(res, ctx, argv, envp); +} - size_t total = len + 1 + strlen(exe) + 1; - if (cap < total) { - char *grown = realloc(ret, total); - if (!grown) { - goto fail; - } - ret = grown; - cap = total; - } +pid_t bfs_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { + // execvp()/posix_spawnp() are typically implemented with repeated + // execv() calls for each $PATH component until one succeeds. It's + // faster to resolve the full path ahead of time. + struct bfs_resolver res; + if (bfs_resolve_early(&res, exe, ctx) != 0) { + return -1; + } - memcpy(ret, path, len); - if (ret[len - 1] != '/') { - ret[len++] = '/'; - } - strcpy(ret + len, exe); + extern char **environ; + if (!envp) { + envp = environ; + } - if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) { - break; - } + pid_t ret = bfs_spawn_impl(&res, ctx, argv, envp); + bfs_resolve_free(&res); + return ret; +} - if (!end) { - errno = ENOENT; - goto fail; - } +char *bfs_spawn_resolve(const char *exe) { + struct bfs_resolver res; + if (bfs_resolve_early(&res, exe, NULL) != 0) { + return NULL; + } + if (bfs_resolve_late(&res) != 0) { + bfs_resolve_free(&res); + return NULL; + } - path = end + 1; + char *ret; + if (res.exe == res.buf) { + ret = res.buf; + res.buf = NULL; + } else { + ret = strdup(res.exe); } - free(confpath); + bfs_resolve_free(&res); return ret; - -fail: - free(confpath); - free(ret); - return NULL; } |