about summary refs log tree commit diff
path: root/pkgs/games/build-support
diff options
context:
space:
mode:
authoraszlig <aszlig@redmoonstudios.org>2017-09-27 19:58:21 +0200
committeraszlig <aszlig@redmoonstudios.org>2017-10-03 23:41:20 +0200
commit2554e3ce9096c7036cbea55d78828794085734af (patch)
treeadc363f46193c1ab3368325d900525e23d6918b9 /pkgs/games/build-support
parent673cf2b1b38fb9a6fc4a5b01716feda7d3b861e4 (diff)
pkgs/build-game: Add preliminary sandbox hook
This is basically to make sure various games can't write to whatever
they want in the file system, so it's not a complete sandboxing
solution.

Currently there's a drawback in that we can't easily determine the
runtime dependencies while building a particular game, so we need to
recursively dig through all referenced store paths to look them up.

A better solution for this would be to gather the build time reference
graph prior to building so that we can limit searching for these
references within only the actual build outputs instead of churning
through all inputs.

In addition to that, we currently mount the namespaced root file system
on top of /tmp, which makes the real /tmp unavailable to us. While in
theory this shouldn't be a problem, it actually turns out it is indeed a
problem if the application wants to connect to the X server socket,
which is at something like /tmp/.X11-unix/X0 for display :0.

Apart from these drawbacks we have a working solution for simple
applications (not games, because they usually require X), which now get
its own chroot with only the paths accessible that are strictly
necessary.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
Diffstat (limited to 'pkgs/games/build-support')
-rw-r--r--pkgs/games/build-support/build-game.nix20
-rw-r--r--pkgs/games/build-support/sandbox.c497
-rw-r--r--pkgs/games/build-support/setup-hooks/make-sandbox.sh103
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)