diff options
-rw-r--r-- | pkgs/games/build-support/build-game.nix | 20 | ||||
-rw-r--r-- | pkgs/games/build-support/sandbox.c | 497 | ||||
-rw-r--r-- | pkgs/games/build-support/setup-hooks/make-sandbox.sh | 103 |
3 files changed, 617 insertions, 3 deletions
diff --git a/pkgs/games/build-support/build-game.nix b/pkgs/games/build-support/build-game.nix index b64f7457..e402787c 100644 --- a/pkgs/games/build-support/build-game.nix +++ b/pkgs/games/build-support/build-game.nix @@ -1,4 +1,4 @@ -{ stdenv, lib, file, unzip +{ stdenv, lib, file, unzip, gcc, makeSetupHook , withPulseAudio ? true, libpulseaudio ? null , alsaLib @@ -12,10 +12,19 @@ assert withPulseAudio -> libpulseaudio != null; , setSourceRoot ? "" , installCheckPhase ? "" , runtimeDependencies ? [] +, extraSandboxPaths ? [ "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" ] , ... }@attrs: -stdenv.mkDerivation ({ +let + sandboxHook = makeSetupHook { + substitutions = { + inherit gcc; + sandbox_main = ./sandbox.c; + }; + } ./setup-hooks/make-sandbox.sh; + +in stdenv.mkDerivation ({ buildInputs = [ stdenv.cc.cc ] ++ buildInputs; nativeBuildInputs = [ @@ -39,6 +48,11 @@ stdenv.mkDerivation ({ fi ''; + # Use ":!*!:" as delimiter as we can consider this highly unlikely to + # be part of a real path component and we're out of Nix territory, so + # the path components could contain almost anything. + extraSandboxPaths = lib.concatStringsSep ":!*!:" extraSandboxPaths; + runtimeDependencies = let deps = lib.singleton alsaLib ++ lib.optional withPulseAudio libpulseaudio @@ -71,5 +85,5 @@ stdenv.mkDerivation ({ dontPatchELF = true; } // removeAttrs attrs [ "buildInputs" "nativeBuildInputs" "preUnpack" "setSourceRoot" - "installCheckPhase" "runtimeDependencies" + "installCheckPhase" "runtimeDependencies" "extraSandboxPaths" ]) diff --git a/pkgs/games/build-support/sandbox.c b/pkgs/games/build-support/sandbox.c new file mode 100644 index 00000000..69553628 --- /dev/null +++ b/pkgs/games/build-support/sandbox.c @@ -0,0 +1,497 @@ +#define _GNU_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <errno.h> +#include <fcntl.h> +#include <libgen.h> +#include <limits.h> +#include <malloc.h> +#include <sched.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static bool write_proc(int proc_pid_fd, const char *fname, const char *buf, + size_t buflen, bool ignore_errors) +{ + int fd; + + if ((fd = openat(proc_pid_fd, fname, O_WRONLY)) == -1) { + fprintf(stderr, "open %s: %s\n", fname, strerror(errno)); + return false; + } + + if (write(fd, buf, buflen) == -1) { + if (!ignore_errors) + fprintf(stderr, "write %s: %s\n", fname, strerror(errno)); + close(fd); + return ignore_errors; + } + + close(fd); + return true; +} + +#define WRITE_IDMAP(file, value) \ + buflen = snprintf(buf, 100, "%1$lu %1$lu 1", (unsigned long)value); \ + if (buflen >= 100) { \ + fputs("Unable to write buffer for " file ".\n", stderr); \ + close(proc_pid_fd); \ + return false; \ + } else if (buflen < 0) { \ + perror("snprintf " file " buffer"); \ + close(proc_pid_fd); \ + return false; \ + } \ + if (!write_proc(proc_pid_fd, file, buf, buflen, false)) { \ + close(proc_pid_fd); \ + return false; \ + } + +static bool write_maps(pid_t parent_pid) +{ + int proc_pid_fd; + size_t buflen; + char buf[100]; + + buflen = snprintf(buf, 100, "/proc/%lu", (unsigned long)parent_pid); + if (buflen >= 100) { + fputs("Unable to write buffer for child pid proc path.\n", stderr); + return false; + } else if (buflen < 0) { + perror("snprintf child pid proc path"); + return false; + } + + if ((proc_pid_fd = open(buf, O_RDONLY | O_DIRECTORY)) == -1) { + fprintf(stderr, "open %s: %s\n", buf, strerror(errno)); + return false; + } + + WRITE_IDMAP("uid_map", geteuid()); + + // Kernels prior to Linux 3.19 which do not impose setgroups() + // restrictions won't have this file, so ignore failure. + write_proc(proc_pid_fd, "setgroups", "deny", 4, true); + + WRITE_IDMAP("gid_map", getegid()); + + return true; +} + +static bool makedirs(const char *path) +{ + char *tmp, *segment; + + if ((tmp = strdup(path)) == NULL) { + fprintf(stderr, "strdup of %s: %s\n", path, strerror(errno)); + return false; + } + + segment = dirname(tmp); + + if (!(segment[0] == '/' && segment[1] == '\0')) { + if (!makedirs(segment)) { + free(tmp); + return false; + } + } + + (void)mkdir(path, 0755); + free(tmp); + return true; +} + +static bool bind_mount(const char *path, bool restricted) +{ + int mflags = MS_BIND | MS_REC; + size_t srclen; + char src[PATH_MAX], target[PATH_MAX]; + + if (restricted) + mflags |= MS_NOSUID | MS_NODEV | MS_NOATIME; + + if (realpath(path, src) == NULL) { + fprintf(stderr, "realpath of %s: %s\n", path, strerror(errno)); + return false; + } + + if ((srclen = strlen(src)) > PATH_MAX - 4) { + fprintf(stderr, "`/tmp/%s' does not fit in PATH_MAX.\n", src); + return false; + } + + memcpy(target, "/tmp", 4); + memcpy(target + 4, src, srclen + 1); + + if (!makedirs(target)) + return false; + + if (mount(src, target, "", mflags, NULL) == -1) { + fprintf(stderr, "mount %s to %s: %s\n", src, target, strerror(errno)); + return false; + } + + return true; +} + +struct envar_offset { + int start; + int length; + int var_start; + int var_length; + struct envar_offset *next; +}; + +static struct envar_offset *alloc_offset(void) +{ + struct envar_offset *new_offset; + new_offset = malloc(sizeof(struct envar_offset)); + + if (new_offset == NULL) { + perror("malloc envar_offset"); + return NULL; + } + + new_offset->next = NULL; + return new_offset; +} + +static struct envar_offset *push_offset(struct envar_offset *current, + struct envar_offset **base) +{ + if (current == NULL) { + if ((current = alloc_offset()) != NULL) + *base = current; + return current; + } + + return current->next = alloc_offset(); +} + +static void free_offsets(struct envar_offset *base) +{ + struct envar_offset *next; + if (base == NULL) + return; + next = base->next; + free(base); + if (next != NULL) + free_offsets(next); +} + +static char *expand_xdg_fallback(const char *xdg_var) +{ + static char *home = NULL; + static size_t homelen; + char *result; + + if (home == NULL) { + if ((home = getenv("HOME")) == NULL) { + fputs("Unable find $HOME.\n", stderr); + return NULL; + } + homelen = strlen(home); + } + + if (strcmp(xdg_var, "XDG_DATA_HOME") == 0) { + result = malloc(homelen + 14); + if (result == NULL) { + perror("malloc XDG_DATA_HOME"); + return NULL; + } + memcpy(result, home, homelen); + memcpy(result + homelen, "/.local/share", 14); + return result; + } else if (strcmp(xdg_var, "XDG_CONFIG_HOME") == 0) { + result = malloc(homelen + 9); + if (result == NULL) { + perror("malloc XDG_CONFIG_HOME"); + return NULL; + } + memcpy(result, home, homelen); + memcpy(result + homelen, "/.config", 9); + return result; + } + + return NULL; +} + +static char *get_offset_var(struct envar_offset *offset, const char *haystack) +{ + char *tmp, *result; + + tmp = strndup(haystack + offset->var_start, offset->var_length); + + if (tmp == NULL) { + perror("strndup"); + return NULL; + } + + result = getenv(tmp); + if (result == NULL) { + if ((result = expand_xdg_fallback(tmp)) == NULL) { + fprintf(stderr, "Unable find variable %s in %s\n", tmp, haystack); + free(tmp); + return NULL; + } + free(tmp); + return result; + } + free(tmp); + return strdup(result); +} + +static char *replace_env_offset_free(const char *path, + struct envar_offset *offset) +{ + struct envar_offset *tmp_offset; + size_t buflen, pathlen, varlen, tmplen; + int inpos = 0, outpos = 0; + char *buf, *curvar; + + buflen = pathlen = strlen(path); + + if ((buf = malloc(buflen + 1)) == NULL) { + perror("malloc replace_env buffer"); + return NULL; + } + + while (offset != NULL) { + if ((curvar = get_offset_var(offset, path)) == NULL) { + free(buf); + free_offsets(offset); + return NULL; + } + + varlen = strlen(curvar); + tmplen = varlen + (buflen - offset->length); + + if (tmplen > buflen) { + if ((buf = realloc(buf, (buflen = tmplen) + 1)) == NULL) { + perror("realloc replace_env buffer"); + free(buf); + free(curvar); + free_offsets(offset); + return NULL; + } + } + + memcpy(buf + outpos, path + inpos, offset->start - inpos); + outpos += offset->start - inpos; + inpos = offset->start; + + memcpy(buf + outpos, curvar, varlen); + outpos += varlen; + inpos += offset->length; + + free(curvar); + + tmp_offset = offset; + offset = offset->next; + free(tmp_offset); + } + + memcpy(buf + outpos, path + inpos, pathlen - inpos); + *(buf + outpos + (pathlen - inpos)) = '\0'; + + return buf; +} + +static char *replace_env(const char *path) +{ + int i = 0, start = 0, var_start = 0; + size_t pathlen; + bool in_var = false, curly = false; + struct envar_offset *base = NULL, *offset = NULL; + + pathlen = strlen(path); + + while (i < pathlen) { + if (path[i] == '$' && !curly && !in_var) { + if (i + 1 >= pathlen) + break; + + start = i; + + if (path[i + 1] == '{') { + curly = true; + var_start = i + 2; + ++i; + } else { + in_var = true; + var_start = i + 1; + } + } else if (in_var) { + if (!(path[i] >= 'a' && path[i] <= 'z') && + !(path[i] >= 'A' && path[i] <= 'Z') && + !(path[i] >= '0' && path[i] <= '9') && + path[i] != '_' + ) { + in_var = false; + + if ((offset = push_offset(offset, &base)) == NULL) { + free_offsets(base); + return NULL; + } + + offset->start = start; + offset->length = i - start; + offset->var_start = var_start; + offset->var_length = i - var_start; + continue; + } + } else if (curly) { + if (path[i] == '}') { + curly = false; + + if ((offset = push_offset(offset, &base)) == NULL) { + free_offsets(base); + return NULL; + } + + offset->start = start; + offset->length = (i + 1) - offset->start; + offset->var_start = var_start; + offset->var_length = i - offset->var_start; + } + } + + ++i; + } + + if (in_var) { + if ((offset = push_offset(offset, &base)) == NULL) { + free_offsets(base); + return NULL; + } + + offset->start = start; + offset->length = i - start; + offset->var_start = var_start; + offset->var_length = i - var_start; + } + + return replace_env_offset_free(path, base); +} + +static bool extra_mount(const char *path) +{ + char *expanded; + if ((expanded = replace_env(path)) == NULL) + return false; + + if (!bind_mount(expanded, true)) { + free(expanded); + return false; + } + + free(expanded); + return true; +} + +#include PARAMS_FILE + +static bool setup_chroot(void) +{ + int mflags; + + mflags = MS_NOEXEC | MS_NOSUID | MS_NODEV | MS_NOATIME; + + if (mount("none", "/tmp", "tmpfs", mflags, NULL) == -1) { + perror("mount rootfs"); + return false; + } + + if (!bind_mount("/dev", false)) + return false; + + if (!bind_mount("/proc", false)) + return false; + + if (!bind_mount("/sys", false)) + return false; + + if (mkdir("/tmp/tmp", 0700) == -1) { + perror("mkdir private tmp"); + return false; + } + + if (!setup_app_paths()) + return false; + + if (chroot("/tmp") == -1) { + perror("chroot"); + return false; + } + + if (chdir("/") == -1) { + perror("chdir rootfs"); + return false; + } + + return true; +} + +int main(int argc, char **argv) +{ + int sync_pipe[2]; + char sync_status = '.'; + int child_status; + pid_t pid, parent_pid; + + if (pipe(sync_pipe) == -1) { + perror("pipe"); + return 1; + } + + parent_pid = getpid(); + + switch (pid = fork()) { + case -1: + perror("fork"); + return 1; + case 0: + close(sync_pipe[1]); + if (read(sync_pipe[0], &sync_status, 1) == -1) { + perror("read pipe from parent"); + _exit(1); + } else if (sync_status == 'X') + _exit(1); + close(sync_pipe[0]); + _exit(write_maps(parent_pid) ? 0 : 1); + default: + if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) { + perror("unshare"); + if (write(sync_pipe[1], "X", 1) == -1) + perror("signal child exit"); + waitpid(pid, NULL, 0); + return 1; + } + + close(sync_pipe[1]); + waitpid(pid, &child_status, 0); + if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 0) + break; + return 1; + } + + if (!setup_chroot()) + return 1; + + argv[0] = WRAPPED_PROGNAME; + if (execv(WRAPPED_PATH, argv) == -1) { + fprintf(stderr, "exec %s: %s\n", WRAPPED_PATH, strerror(errno)); + return 1; + } + + // Should never be reached. + return 1; +} diff --git a/pkgs/games/build-support/setup-hooks/make-sandbox.sh b/pkgs/games/build-support/setup-hooks/make-sandbox.sh new file mode 100644 index 00000000..7779234f --- /dev/null +++ b/pkgs/games/build-support/setup-hooks/make-sandbox.sh @@ -0,0 +1,103 @@ +sandbox_params_include="$(mktemp --suffix=.c)" +trap "rm -f '$sandbox_params_include'" EXIT +sandbox_references="" + +hasReference() { + local ref + for ref in $sandbox_references; do + if [ "$1" = "$ref" ]; then return 0; fi + done + + return 1 +} + +addReference() { + local toAdd="$1" + + sandbox_references="$sandbox_references $toAdd" + + echo 'if (!bind_mount("'"$toAdd"'", true)) return false;' \ + >> "$sandbox_params_include" +} + +gatherReferencesRecursive() { + local path="$1" + + if hasReference "$path"; then return; fi + addReference "$path" + + local valid_hash='[0-9a-df-np-sv-z]\{32\}' + local valid_name='[A-Za-z0-9+_?=-][A-Za-z0-9+._?=-]*' + local valid_path="$NIX_STORE/$valid_hash-$valid_name" + + local hashpaths="$( + find "$path" -type f -exec grep -hao "$valid_path" {} + + find "$path" -type l -exec readlink {} + + )" + + local hashpath + for hashpath in $hashpaths; do + local realsp + for realsp in "$NIX_STORE"/*; do + if echo "$hashpath" | grep -q -m 1 "^${realsp//./\\.}"; then + gatherReferencesRecursive "$realsp" + break + fi + done + done +} + +gatherReferences() { + [ -z "$sandbox_references" ] || return 0 + + echo 'static bool setup_app_paths(void) {' > "$sandbox_params_include" + + for output in $outputs; do + [ -e "${!output}" ] || continue + gatherReferencesRecursive "${!output}" + done + + if [ -n "$extraSandboxPaths" ]; then + local oldIfs="$IFS" + IFS=':!*!:' + local extra + for extra in $extraSandboxPaths; do + local extraC="$(echo "$extra" | sed -e 's/"\\/\\&/g')" + echo 'if (!extra_mount("'"$extraC"'")) return false;' \ + >> "$sandbox_params_include" + done + IFS="$oldIfs" + fi + + echo 'return true; }' >> "$sandbox_params_include" + cat "$sandbox_params_include" +} + +wrapSandbox() { + local progname="$1" + local wrapped="$2" + local output="$3" + + @gcc@/bin/gcc -g -std=gnu11 -Wall \ + -DWRAPPED_PATH=\""$wrapped"\" \ + -DWRAPPED_PROGNAME=\""$progname"\" \ + -DPARAMS_FILE=\""$sandbox_params_include"\" \ + -o "$output" @sandbox_main@ +} + +makeSandbox() { + gatherReferences + + for output in $outputs; do + [ -e "${!output}" ] || continue + local bin + for bin in "${!output}"/bin/*; do + local binbase="$(basename "$bin")" + local newdest="$(dirname "$bin")/.$binbase-wrapped" + mv "$bin" "$newdest" + wrapSandbox "$binbase" "$newdest" "$bin" + done + done +} + +postFixupHooks+=(makeSandbox) |