summaryrefslogtreecommitdiffstats
path: root/src/xspawn.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/xspawn.c')
-rw-r--r--src/xspawn.c318
1 files changed, 318 insertions, 0 deletions
diff --git a/src/xspawn.c b/src/xspawn.c
new file mode 100644
index 0000000..93c270a
--- /dev/null
+++ b/src/xspawn.c
@@ -0,0 +1,318 @@
+/****************************************************************************
+ * 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. *
+ ****************************************************************************/
+
+#include "xspawn.h"
+#include "util.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/**
+ * Types of spawn actions.
+ */
+enum bfs_spawn_op {
+ BFS_SPAWN_CLOSE,
+ BFS_SPAWN_DUP2,
+ BFS_SPAWN_FCHDIR,
+ BFS_SPAWN_SETRLIMIT,
+};
+
+/**
+ * A spawn action.
+ */
+struct bfs_spawn_action {
+ struct bfs_spawn_action *next;
+
+ enum bfs_spawn_op op;
+ int in_fd;
+ int out_fd;
+ int resource;
+ struct rlimit rlimit;
+};
+
+int bfs_spawn_init(struct bfs_spawn *ctx) {
+ ctx->flags = 0;
+ ctx->actions = NULL;
+ ctx->tail = &ctx->actions;
+ 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;
+ free(action);
+ action = next;
+ }
+ return 0;
+}
+
+int bfs_spawn_setflags(struct bfs_spawn *ctx, enum bfs_spawn_flags flags) {
+ ctx->flags = flags;
+ return 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;
+ }
+ return action;
+}
+
+int bfs_spawn_addclose(struct bfs_spawn *ctx, int fd) {
+ if (fd < 0) {
+ errno = EBADF;
+ return -1;
+ }
+
+ struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_CLOSE);
+ if (action) {
+ action->out_fd = fd;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
+ if (oldfd < 0 || newfd < 0) {
+ errno = EBADF;
+ 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;
+ }
+}
+
+int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
+ if (fd < 0) {
+ errno = EBADF;
+ return -1;
+ }
+
+ struct bfs_spawn_action *action = bfs_spawn_add(ctx, BFS_SPAWN_FCHDIR);
+ if (action) {
+ action->in_fd = fd;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+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;
+ return 0;
+ } else {
+ return -1;
+ }
+}
+
+/** 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;
+
+ xclose(pipefd[0]);
+
+ for (const struct bfs_spawn_action *action = actions; action; action = action->next) {
+ // Move the error-reporting pipe out of the way if necessary...
+ if (action->out_fd == pipefd[1]) {
+ int fd = dup_cloexec(pipefd[1]);
+ if (fd < 0) {
+ goto fail;
+ }
+ xclose(pipefd[1]);
+ pipefd[1] = fd;
+ }
+
+ // ... and pretend the pipe doesn't exist
+ if (action->in_fd == pipefd[1]) {
+ errno = EBADF;
+ goto fail;
+ }
+
+ switch (action->op) {
+ case BFS_SPAWN_CLOSE:
+ if (close(action->out_fd) != 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_DUP2:
+ if (dup2(action->in_fd, action->out_fd) < 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_FCHDIR:
+ if (fchdir(action->in_fd) != 0) {
+ goto fail;
+ }
+ break;
+ case BFS_SPAWN_SETRLIMIT:
+ if (setrlimit(action->resource, &action->rlimit) != 0) {
+ goto fail;
+ }
+ break;
+ }
+ }
+
+ execve(exe, argv, envp);
+
+fail:
+ 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));
+
+ 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;
+ }
+ }
+
+ // Use a pipe to report errors from the child
+ int pipefd[2];
+ if (pipe_cloexec(pipefd) != 0) {
+ free(resolved);
+ return -1;
+ }
+
+ pid_t pid = fork();
+ 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);
+ }
+
+ // 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);
+ errno = error;
+ return -1;
+ }
+
+ return pid;
+}
+
+char *bfs_spawn_resolve(const char *exe) {
+ if (strchr(exe, '/')) {
+ return strdup(exe);
+ }
+
+ const char *path = getenv("PATH");
+
+ char *confpath = NULL;
+ if (!path) {
+ path = confpath = xconfstr(_CS_PATH);
+ 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;
+ }
+
+ size_t total = len + 1 + strlen(exe) + 1;
+ if (cap < total) {
+ char *grown = realloc(ret, total);
+ if (!grown) {
+ goto fail;
+ }
+ ret = grown;
+ cap = total;
+ }
+
+ memcpy(ret, path, len);
+ if (ret[len - 1] != '/') {
+ ret[len++] = '/';
+ }
+ strcpy(ret + len, exe);
+
+ if (xfaccessat(AT_FDCWD, ret, X_OK) == 0) {
+ break;
+ }
+
+ if (!end) {
+ errno = ENOENT;
+ goto fail;
+ }
+
+ path = end + 1;
+ }
+
+ free(confpath);
+ return ret;
+
+fail:
+ free(confpath);
+ free(ret);
+ return NULL;
+}