// Copyright © Tavian Barnes // SPDX-License-Identifier: 0BSD #include "xspawn.h" #include "alloc.h" #include "bfstd.h" #include "config.h" #include "list.h" #include #include #include #include #include #include #include #include #if BFS_USE_PATHS_H # include #endif #if _POSIX_SPAWN > 0 # include #endif /** * 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; 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) { #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); } return 0; } #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 /** 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) { 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) { struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_DUP2); if (!action) { 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; } int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) { struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR); if (!action) { return -1; } #ifndef BFS_HAS_POSIX_SPAWN_FCHDIR # define BFS_HAS_POSIX_SPAWN_FCHDIR __NetBSD_Prereq__(10, 0, 0) #endif #ifndef BFS_HAS_POSIX_SPAWN_FCHDIR_NP # if __GLIBC__ # define BFS_HAS_POSIX_SPAWN_FCHDIR_NP __GLIBC_PREREQ(2, 29) # elif __ANDROID__ # define BFS_HAS_POSIX_SPAWN_FCHDIR_NP (__ANDROID_API__ >= 34) # else # define BFS_HAS_POSIX_SPAWN_FCHDIR_NP (__linux__ || __FreeBSD__ || __APPLE__) # endif #endif #if BFS_HAS_POSIX_SPAWN_FCHDIR # define BFS_POSIX_SPAWN_FCHDIR posix_spawn_file_actions_addfchdir #elif BFS_HAS_POSIX_SPAWN_FCHDIR_NP # define BFS_POSIX_SPAWN_FCHDIR 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_FCHDIR(&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; } #if _POSIX_SPAWN > 0 /** bfs_spawn() implementation using posix_spawn(). */ static pid_t bfs_posix_spawn(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { pid_t ret; errno = posix_spawn(&ret, exe, &ctx->actions, &ctx->attr, argv, envp); if (errno != 0) { return -1; } return ret; } #endif /** Actually exec() the new process. */ static noreturn void bfs_spawn_exec(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp, int pipefd[2]) { xclose(pipefd[0]); for_slist (const struct bfs_spawn_action, action, ctx) { // 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); int error; 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); } /** bfs_spawn() implementation using fork()/exec(). */ static pid_t bfs_fork_spawn(const char *exe, 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) { return -1; } pid_t pid = fork(); if (pid < 0) { close_quietly(pipefd[1]); close_quietly(pipefd[0]); return -1; } else if (pid == 0) { // Child bfs_spawn_exec(exe, ctx, argv, envp, pipefd); } // Parent xclose(pipefd[1]); int error; ssize_t nbytes = xread(pipefd[0], &error, sizeof(error)); xclose(pipefd[0]); if (nbytes == sizeof(error)) { int wstatus; xwaitpid(pid, &wstatus, 0); errno = error; return -1; } return pid; } /** Call the right bfs_spawn() implementation. */ static pid_t bfs_spawn_impl(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) { #if _POSIX_SPAWN > 0 if (ctx->flags & BFS_SPAWN_USE_POSIX) { return bfs_posix_spawn(exe, ctx, argv, envp); } #endif return bfs_fork_spawn(exe, ctx, argv, envp); } 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. char *resolved = NULL; if (ctx->flags & BFS_SPAWN_USE_PATH) { exe = resolved = bfs_spawn_resolve(exe); if (!resolved) { return -1; } } extern char **environ; if (!envp) { envp = environ; } pid_t ret = bfs_spawn_impl(exe, ctx, argv, envp); free(resolved); return ret; } char *bfs_spawn_resolve(const char *exe) { if (strchr(exe, '/')) { return strdup(exe); } 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; } 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; }