~ach/hermes

b5272e5b19e1c2f564821c920d24dc34dba5461a — Andrew Chambers 3 years ago a78a22f pluggable-sandbox
pluggable-sandbox
M csrc/hermes-linux-namespace-sandbox.c => csrc/hermes-linux-namespace-sandbox.c +230 -135
@@ 2,6 2,8 @@
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <sched.h>
#include <stdarg.h>
#include <stdio.h>


@@ 9,6 11,7 @@
#include <string.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>


@@ 21,26 24,14 @@
  and largely isolated from the host system.
*/

#define MAX_BINDS 256

typedef struct {
  char *host_dir;
  char *chroot_dir;
  int write;
} BindSpec;

/* various command line arguments */
static char *argv0 = NULL;
static char *chroot_dir = NULL;
static char *work_dir = "/";
static char *pkg_out = NULL;
static char *store_path = NULL;
static int isolate_network = 1;
static int verbose = 0;
static uid_t build_uid, build_gid;

/* --bind arguments */
static int bind_count;
static BindSpec binds[MAX_BINDS];

/* synchronization between child and parent. */
static int sync_pipe[2];
static volatile pid_t child = -1;


@@ 76,6 67,137 @@ static void die(const char *fmt, ...) {
  exit(2);
}

static char *xmallocprintf(const char *fmt, ...) {
  char *s;
  va_list ap;
  va_list ap2;

  va_start(ap, fmt);
  va_copy(ap2, ap);

  int n = vsnprintf(NULL, 0, fmt, ap) + 1;
  s = malloc(n);
  if (!s)
    die("malloc");
  vsprintf(s, fmt, ap2);
  va_end(ap);
  va_end(ap2);
  return s;
}

static void xmkdir(const char *pathname, mode_t mode) {
  if (mkdir(pathname, mode) != 0)
    die("mkdir '%s': %s", pathname, strerror(errno));
}

static void xmkdirorexists(const char *pathname, mode_t mode) {
  if (mkdir(pathname, mode) != 0) {
    if (errno != EEXIST) {
      die("mkdir '%s': %s", pathname, strerror(errno));
    }
  }
}

static void xmkdir_p(const char *path, mode_t mode) {
  const size_t len = strlen(path);
  char _path[PATH_MAX];
  char *p;

  if (len > sizeof(_path) - 1)
    die("mkdir_p path too long");
  strcpy(_path, path);

  for (p = _path + 1; *p; p++) {
    if (*p == '/') {
      *p = 0;
      xmkdirorexists(_path, mode);
      *p = '/';
    }
  }

  xmkdirorexists(_path, mode);
}

static void xsymlink(const char *target, const char *linkpath) {
  if (symlink(target, linkpath) != 0)
    die("symlink '%s' -> '%s': %s", linkpath, target, strerror(errno));
}

static void xsetuid(uid_t uid) {
  if (setuid(uid) != 0)
    die("setuid '%d': %s", uid, strerror(errno));
}

static void xsetgid(uid_t gid) {
  if (setgid(gid) != 0)
    die("setgid '%d': %s", gid, strerror(errno));
}

static pid_t xsetsid(void) {
  pid_t p;
  p = setsid();
  if (p < 0)
    die("setsid: %s", strerror(errno));
  return p;
}

static void xchroot(const char *path) {
  if (chroot(path) != 0)
    die("chroot '%s': %s", path, strerror(errno));
}

static void xclose(int fd) {
  if (close(fd) != 0)
    die("close '%d': %s", fd, strerror(errno));
}

static void xmount(const char *source, const char *target,
                   const char *filesystemtype, unsigned long mountflags,
                   const void *data) {
  if (mount(source, target, filesystemtype, mountflags, data) != 0)
    die("mount': %s", strerror(errno));
}

static pid_t xfork(void) {
  pid_t p;
  p = fork();
  if (p < 0)
    die("fork: %s", strerror(errno));
  return p;
}

static void xsigprocmask(int how, const sigset_t *set, sigset_t *oldset) {
  if (sigprocmask(how, set, oldset) != 0)
    die("sigprocmask: %s", strerror(errno));
}

static void xchown(const char *pathname, uid_t owner, gid_t group) {
  if (chown(pathname, owner, group) != 0)
    die("chown '%s' '%d' '%d': %s", pathname, owner, group, strerror(errno));
}

static const char *mustgetenv(const char *k) {
  const char *e = getenv(k);
  if (!e)
    die("%s is not set", k);
  return e;
}

static void write_file(const char *path, const char *fmt, ...) {
  va_list ap;
  FILE *f = fopen(path, "w");
  if (!f)
    die("fopen: %s", strerror(errno));

  va_start(ap, fmt);
  if (vfprintf(f, fmt, ap) < 0)
    die("fprintf: %s", strerror(errno));
  va_end(ap);

  if (fclose(f) != 0)
    die("fclose: %s", strerror(errno));
}

/*
  Helper function for configuring uid and gid maps in jail via
  the various control files.


@@ 86,86 208,96 @@ static void write_user_ctl_file(const char *ctl, pid_t child, const char *fmt,
                                ...) {
  int fd;
  int sz;
  char buf[4096];
  char buf[2048];
  va_list ap;

  snprintf(buf, sizeof(buf), "/proc/%d/%s", child, ctl);

  fd = open(buf, O_RDWR);
  if (fd < 0)
    die("%s\n", strerror(errno));
    die("%s", strerror(errno));

  va_start(ap, fmt);
  sz = vsnprintf(buf, sizeof(buf), fmt, ap);
  va_end(ap);

  if (write(fd, buf, sz) != sz)
    die("%s\n", strerror(errno));
    die("%s", strerror(errno));

  if (close(fd) < 0)
    die("%s\n", strerror(errno));
  xclose(fd);
}

static int post_clone(void *argv) {
  sigset_t set;
  char buf[4096];
  char buf[2048];

  if (child != -1)
    die("BUG");

  if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0)
    die("prctl: %s\n", strerror(errno));
    die("prctl: %s", strerror(errno));

  if (getpid() != 1)
    die("not init process in namespace");

  /* Wait for parent to setup uid mapping */
  if (close(sync_pipe[1]) < 0)
    die("close: %s", strerror(errno));
  xclose(sync_pipe[1]);
  if (read(sync_pipe[0], buf, 1) != 1)
    die("read failed: %s", strerror(errno));
  if (close(sync_pipe[0]) < 0)
    die("close: %s", strerror(errno));

  for (int i = 0; i < bind_count; i++) {
    int mount_flags;

    char *sep = "/";
    if (binds[i].chroot_dir[0] == '/')
      sep = "";
    snprintf(buf, sizeof(buf), "%s%s%s", chroot_dir, sep, binds[i].chroot_dir);

    if (verbose) {
      printf("%s binding %s -> %s\n", binds[i].write ? "rw" : "ro",
             binds[i].host_dir, buf);
    }

    mount_flags = MS_BIND | MS_REC;
    if (!binds[i].write)
      mount_flags |= MS_RDONLY;

    if (mount(binds[i].host_dir, buf, 0, mount_flags, 0) < 0)
      die("mount: %s", strerror(errno));
  }

  if (chdir(chroot_dir) < 0)
    die("chdir '%s': %s", chroot_dir, strerror(errno));

  if (chroot(".") < 0)
    die("chroot: %s", strerror(errno));

  if (chdir(work_dir) < 0)
    die("chdir '%s': %s", work_dir, strerror(errno));
  xclose(sync_pipe[0]);

  xmkdir("var", 0755);
  xmkdir("var/tmp", 0755);
  xmkdir("var/run", 0755);
  xmkdir("proc", 0755);
  xmkdir("sys", 0755);
  xmkdir("dev", 0755);
  xmkdir("tmp", 0755);
  xmkdir("etc", 0755);
  xmkdir("bin", 0755);
  xmkdir("usr", 0755);
  xmkdir("build", 0700);
  xsymlink("/bin", "usr/bin");
  xmkdir_p(store_path + 1, 0755);

  xchown("bin", build_uid, build_gid);
  xchown("build", build_uid, build_gid);

  write_file("etc/passwd",
             "root:x:0:0:root:/:/bin/sh\n"
             "builder:x:%d:%d:builder:/build:/bin/sh\n",
             build_uid, build_gid);

  write_file("etc/group",
             "bin:x:1:\n"
             "sys:x:2:\n"
             "kmem:x:3:\n"
             "tty:x:4:\n"
             "tape:x:5:\n"
             "daemon:x:6:\n"
             "floppy:x:7:\n"
             "disk:x:8:\n"
             "lp:x:9:\n"
             "dialout:x:10:\n"
             "audio:x:11:\n"
             "video:x:12:\n"
             "utmp:x:13:\n"
             "usb:x:14:\n"
             "builder:x:%d:\n",
             build_gid);

  xmount("/dev", "dev", 0, MS_BIND | MS_REC, 0);
  xmount("/proc", "proc", 0, MS_BIND | MS_REC, 0);
  xmount("/sys", "sys", 0, MS_BIND | MS_REC, 0);
  xmount(store_path, store_path + 1, 0, MS_BIND | MS_REC | MS_RDONLY, 0);
  xmount(pkg_out, pkg_out + 1, 0, MS_BIND | MS_REC, 0);

  xchroot(".");

  sigfillset(&set);
  xsigprocmask(SIG_BLOCK, &set, NULL);

  if (sigprocmask(SIG_BLOCK, &set, NULL) != 0)
    die("sigprocmask: %s", strerror(errno));

  pid_t build_pid = fork();

  if (build_pid < 0)
    die("fork: %s", strerror(errno));
  pid_t build_pid = xfork();

  if (build_pid) {
    /*


@@ 228,29 360,30 @@ static int post_clone(void *argv) {
  } else {

    /*
            We are the sandboxed child.
      We are the sandboxed child.
    */

    if (sigprocmask(SIG_UNBLOCK, &set, NULL) != 0)
      die("sigprocmask: %s", strerror(errno));

    if (setsid() < 0)
      die("setsid: %s", strerror(errno));
    xsigprocmask(SIG_UNBLOCK, &set, NULL);
    xsetsid();

    /*
      Finally become the uid/gid we were given
      on the command line.
    */
    if (setgid(build_gid) < 0)
      die("setgid: %s", strerror(errno));
    xsetgid(build_gid);
    xsetuid(build_uid);

    if (setuid(build_uid) < 0)
      die("setuid: %s", strerror(errno));
    if (chdir("/build") < 0)
      die("chdir: %s", strerror(errno));

    if (verbose)
      printf("execv: %s\n", (*(char **)argv));
    char *builder[] = {"/builder", NULL};
    char *env[] = {
        xmallocprintf("out=%s", pkg_out),
        "HERMES_FETCH_SOCKET=/fetch.sock",
        NULL,
    };

    execv((*(char **)argv), (char **)argv);
    execve(builder[0], builder, env);
    die("execve: %s", strerror(errno));
    return 1;
  }


@@ 263,8 396,6 @@ int main(int argc, char **argv) {
  sigset_t set;

  argv0 = *argv;
  verbose = 0;
  bind_count = 0;

  for (;;) {
#define NEXT_ARG()                                                             \


@@ 278,66 409,34 @@ int main(int argc, char **argv) {
    if (argc == 0)
      break;

    if (strcmp(arg, "--chroot") == 0) {
      NEXT_ARG();
      if (!argc)
        die("--chroot expects an argument");

      chroot_dir = arg;
    } else if (strcmp(arg, "--workdir") == 0) {
      NEXT_ARG();
      if (!argc)
        die("--workdir expects an argument");

      work_dir = arg;
    } else if (strcmp(arg, "--build-uid") == 0) {
      NEXT_ARG();
      if (!argc)
        die("--build-uid expects an argument");

      build_uid = atoi(arg);
    } else if (strcmp(arg, "--build-gid") == 0) {
      NEXT_ARG();
      if (!argc)
        die("--build-gid expects an argument");

      build_gid = atoi(arg);
    } else if (strcmp(arg, "--ro-bind") == 0 || strcmp(arg, "--rw-bind") == 0) {
      int write = arg[3] == 'w';

      if (bind_count == MAX_BINDS)
        die("too many fs binds");

      if (!argc)
        die("bind flags expect 1 argument");

      NEXT_ARG();
      binds[bind_count].host_dir = arg;
      binds[bind_count].chroot_dir = arg;
      binds[bind_count].write = write;
      bind_count++;

    } else if (strcmp(arg, "--no-isolate-network") == 0) {
      isolate_network = 0;
    } else if (strcmp(arg, "--verbose") == 0) {
    if (strcmp(arg, "--verbose") == 0) {
      verbose = 1;
    } else if (strcmp(arg, "--") == 0) {
      NEXT_ARG();
      break;
    } else {
      die("unknown argument: %s", arg);
    }
#undef NEXT_ARG
  }

  if (argc == 0)
    die("missing command to exec");
  isolate_network =
      !!strcmp(mustgetenv("HERMES_SANDBOX_ISOLATE_NETWORK"), "no");
  store_path = strdup(mustgetenv("HERMES_SANDBOX_PKG_STORE_PATH"));
  pkg_out = strdup(mustgetenv("HERMES_SANDBOX_PKG_OUT_PATH"));
  build_uid = atoi(mustgetenv("HERMES_SANDBOX_BUILD_UID"));

  if (!chroot_dir)
    die("--chroot required");
  if ((strlen(store_path) == 0) || (strlen(pkg_out) == 0))
    die("HERMES_SANDBOX_PKG_STORE_PATH and HERMES_SANDBOX_PKG_OUT_PATH cannot "
        "be empty");

  if (build_uid == 0)
    die("hermes linux namespace sandbox does not support root as "
        "HERMES_SANDBOX_BUILD_UID");

  struct passwd *pw = getpwuid(build_uid);
  if (!pw)
    die("unable to lookup build user gid: uid=%d\n", build_uid);
  build_gid = pw->pw_gid;

  if (verbose) {
    printf("chroot_dir=%s\n", chroot_dir);
    printf("isolate_network=%d\n", isolate_network);
    printf("build_uid=%d\n", build_uid);
    printf("build_gid=%d\n", build_gid);


@@ 363,8 462,7 @@ int main(int argc, char **argv) {

  /* block all signals before we create the child so we can configure our signal
   * handler */
  if (sigprocmask(SIG_BLOCK, &set, NULL) != 0)
    die("sigprocmask: %s", strerror(errno));
  xsigprocmask(SIG_BLOCK, &set, NULL);

  if ((child = clone(post_clone, clone_stack + clone_stack_size, clone_flags,
                     argv)) < 0)


@@ 391,13 489,10 @@ int main(int argc, char **argv) {
  write_user_ctl_file("setgroups", child, "deny\n");
  write_user_ctl_file("gid_map", child, "0 0 4294967295\n");

  if (close(sync_pipe[0]) < 0)
    die("close: %s", strerror(errno));
  if (write(sync_pipe[1], "x", 1) != 1) {
  xclose(sync_pipe[0]);
  if (write(sync_pipe[1], "x", 1) != 1)
    die("write failed");
  }
  if (close(sync_pipe[1]) < 0)
    die("close: %s", strerror(errno));
  xclose(sync_pipe[1]);

  int status;
  if (waitpid(child, &status, 0) < 0)

M src/cmd/hermes/initstore.go => src/cmd/hermes/initstore.go +12 -8
@@ 4,14 4,14 @@ import (
	"os/user"

	"github.com/andrewchambers/hermes/store"

	"github.com/kballard/go-shellquote"
	flag "github.com/spf13/pflag"
)

func initStoreMain() {

	singleUser := flag.Bool("single-user", false, "Store is for a single users.")
	buildSandboxMethod := flag.String("build-sandbox-method", "", "Store is for a single users.")
	buildSandboxRunner := flag.String("build-sandbox-runner", "", "Path to sandbox runner binary.")
	buildUsers := flag.StringSlice("build-users", nil, "Comma separated list of users to perform package builds as.")
	authorizedUsers := flag.StringSlice("authorized-users", nil, "Comma separated list of users the store will authorized for store manipulation.")
	authorizedGroups := flag.StringSlice("authorized-groups", nil, "Comma separated list of groups the store will authorized for store manipulation")


@@ 28,10 28,6 @@ func initStoreMain() {

	if *singleUser {

		if *buildSandboxMethod == "" {
			*buildSandboxMethod = "none"
		}

		if *buildUsers == nil {
			*buildUsers = []string{curUser.Username}
		}


@@ 50,16 46,24 @@ func initStoreMain() {
			*buildUsers = []string{"hermes_build_user"}
		}

		if *buildSandboxRunner == "" {
			*buildSandboxRunner = "../hermes-linux-namespace-sandbox"
		}
	}

	sandboxRunnerParsed, err := shellquote.Split(*buildSandboxRunner)
	if err != nil {
		die("Unable to parse the sandbox runner args: %s\n", err)
	}

	err = store.InitStore(cfg.StorePath, store.StoreInitConfigTemplate{
		BuildSandboxMethod: *buildSandboxMethod,
		BuildSandboxRunner: sandboxRunnerParsed,
		BuildUsers:         *buildUsers,
		AuthorizedUsers:    *authorizedUsers,
		AuthorizedGroups:   *authorizedGroups,
		CacheServer:        *cacheServer,
	})
	if err != nil {
		die("Error initializing store: %s", err)
		die("Error initializing store: %s\n", err)
	}
}

M src/store/build.go => src/store/build.go +0 -6
@@ 1,11 1,7 @@
package store

import (
	"context"
	"io"
	"os/user"

	"github.com/andrewchambers/hermes/pkgs"
)

type BuildEnv struct {


@@ 14,5 10,3 @@ type BuildEnv struct {
	BuildOutputSink  io.Writer
	FetchurlSocket   string
}

type BuildFunc func(ctx context.Context, buildUser *user.User, buildEnv BuildEnv, pkg *pkgs.Package) error

M src/store/build_linux.go => src/store/build_linux.go +82 -217
@@ 9,260 9,125 @@ import (
	"os/exec"
	"os/user"
	"path/filepath"
	"strconv"
	"sync"

	"github.com/andrewchambers/hermes/fetch"
	"github.com/andrewchambers/hermes/pkgs"
	"github.com/andrewchambers/hermes/proctools"
	"github.com/pkg/errors"
	"golang.org/x/sys/unix"
)

const NativePlatformRequiresBuildUserLocking = false
const NativePlatformSandboxMethod = "hermes-linux-namespace-sandbox"
func sandboxedBuild(ctx context.Context, launcher []string, buildUser *user.User, buildEnv BuildEnv, pkg *pkgs.Package) error {
	tmpDir, err := ioutil.TempDir("", "")
	if err != nil {
		return err
	}

func makeSandboxedBuildFunc(cfg *Config) BuildFunc {
	return func(ctx context.Context, buildUser *user.User, buildEnv BuildEnv, pkg *pkgs.Package) error {
		tmpdir, err := ioutil.TempDir("", "")
	defer func() {
		err = rmTree(tmpDir)
		if err != nil {
			return err
			_, _ = fmt.Fprintf(buildEnv.BuildOutputSink, "unable to cleanup chroot: %s", err)
		}
	}()

		chroot := filepath.Join(tmpdir, "chroot")
		buildDir := filepath.Join(chroot, "build")
		buildTmp := filepath.Join(chroot, "tmp")
		varTmp := filepath.Join(chroot, "/var/tmp")
		binDir := filepath.Join(chroot, "bin")
		procDir := filepath.Join(chroot, "proc")
		sysDir := filepath.Join(chroot, "sys")
		devDir := filepath.Join(chroot, "dev")
		etc := filepath.Join(chroot, "etc")

		chrootDirs := []string{
			"",
			"root",
			"var",
			"var/tmp",
			"var/run",
			"proc",
			"sys",
			"dev",
			"tmp",
			"etc",
			"build",
			"bin",
			"usr",
		}

		defer func() {
			for _, d := range []string{procDir, sysDir, devDir} {
				// Be sure any system dirs are definitely unmounted
				// before doing a recursive cleanup. A defensive precaution
				// from rming files from the host filesystem.
				//
				// This remove call will fail if
				// it is not an empty dir.
				_, err := os.Stat(d)
				if err == nil {
					err = os.Remove(d)
					if err != nil {
						goto cleanupErr
					}
				} else {
					if !os.IsNotExist(err) {
						goto cleanupErr
					}
				}
			}

			// TODO XXX
			// this cleanup won't handle read only files
			// in the chroot, we can maybe use rmTree from store
			err = os.RemoveAll(tmpdir)
			if err != nil {
				goto cleanupErr
			}

		cleanupErr:
			if err != nil {
				fmt.Fprintf(buildEnv.BuildOutputSink, "unable to cleanup chroot: %s", err)
			}
		}()

		for _, d := range chrootDirs {
			err := os.Mkdir(filepath.Join(chroot, d), 0755)
			if err != nil {
				return err
			}
		}
	builder := filepath.Join(tmpDir, "builder")
	if err != nil {
		return err
	}

		err = os.MkdirAll(filepath.Join(chroot, buildEnv.StorePath), 0755)
		if err != nil {
			return err
		}
	proxySockPath := filepath.Join(tmpDir, "fetch.sock")
	l, err := net.Listen("unix", proxySockPath)
	if err != nil {
		return err
	}

		buildUserUidInt, err := strconv.ParseInt(buildUser.Uid, 10, 64)
		if err != nil {
			return err
		}
		buildUserGidInt, err := strconv.ParseInt(buildUser.Gid, 10, 64)
		if err != nil {
			return err
		}
	proxyErrors := make(chan error, 1)

		// TODO XXX the builder entry should depend on the build user.
		err = ioutil.WriteFile(filepath.Join(etc, "passwd"), []byte(fmt.Sprintf(`root:x:0:0:root:/:/bin/sh
builder:x:%s:%s:builder:/:/bin/sh
`, buildUser.Uid, buildUser.Gid)), 0444)
		if err != nil {
			return err
		}
	wg := &sync.WaitGroup{}
	wg.Add(1)
	go func() {
		defer wg.Done()

		// TODO XXX the builder entry should match on the build user.
		err = ioutil.WriteFile(filepath.Join(etc, "group"), []byte(fmt.Sprintf(`root:x:0:
bin:x:1:
sys:x:2:
kmem:x:3:
tty:x:4:
tape:x:5:
daemon:x:6:
floppy:x:7:
disk:x:8:
lp:x:9:
dialout:x:10:
audio:x:11:
video:x:12:
utmp:x:13:
usb:x:14:
builder:x:%s:
`, buildUser.Gid)), 0444)
		if err != nil {
			return err
		proxyConfig := &fetch.ProxyConfig{
			HashRequired: pkg.ContentHash() == "",
		}

		builder := filepath.Join(chroot, "builder")
		if err != nil {
			return err
		}
		dialServer := func() (net.Conn, error) { return net.Dial("unix", buildEnv.FetchurlSocket) }

		proxySockPath := filepath.Join(chroot, "fetch.sock")
		l, err := net.Listen("unix", proxySockPath)
		err := fetch.ProxyRequests(ctx, proxyConfig, l, dialServer)
		if err != nil {
			return err
		}

		proxyErrors := make(chan error, 1)

		wg := &sync.WaitGroup{}
		wg.Add(1)
		go func() {
			defer wg.Done()

			proxyConfig := &fetch.ProxyConfig{
				HashRequired: pkg.ContentHash() == "",
			}

			dialServer := func() (net.Conn, error) { return net.Dial("unix", buildEnv.FetchurlSocket) }

			err := fetch.ProxyRequests(ctx, proxyConfig, l, dialServer)
			if err != nil {
				select {
				case proxyErrors <- err:
				default:
				}
			}

		}()
		defer wg.Wait()
		defer l.Close()

		// We specifically let the build user write to binDir, this
		// is so they can configure some hardcoded paths if needed.
		// usually this is /bin/sh when activating a script.
		for _, d := range []string{binDir, buildDir, varTmp, buildTmp, proxySockPath} {
			err := os.Chown(d, int(buildUserUidInt), int(buildUserGidInt))
			if err != nil {
				return err
			select {
			case proxyErrors <- err:
			default:
			}
		}

		// This is mainly so build script shebangs often just work without
		// us needing to patch them.
		err = os.Symlink("/bin", filepath.Join(chroot, "usr", "bin"))
		if err != nil {
			return err
		}
	}()
	defer wg.Wait()
	defer l.Close()

		builderScript := pkg.BuilderScript()
	builderScript := pkg.BuilderScript()

		err = ioutil.WriteFile(builder, []byte(builderScript), 0555)
		if err != nil {
			return err
		}
	err = ioutil.WriteFile(builder, []byte(builderScript), 0555)
	if err != nil {
		return err
	}

		fullPkgPath := pkg.FullPkgPath(buildEnv.StorePath)
	fullPkgPath := pkg.FullPkgPath(buildEnv.StorePath)

		selfBin, err := os.Executable()
		if err != nil {
			return err
		}
		buildSandboxBinary := filepath.Join(filepath.Dir(selfBin), "hermes-linux-namespace-sandbox")

		sandboxCommand := []string{
			"--build-uid", buildUser.Uid,
			"--build-gid", buildUser.Gid,
			"--workdir", "/build",
			"--chroot", chroot,
			"--rw-bind", "/proc",
			"--rw-bind", "/sys",
			"--rw-bind", "/dev",
			"--ro-bind", buildEnv.StorePath,
			"--rw-bind", fullPkgPath,
		}
	buildSandboxBinary := launcher[0]

		if pkg.ContentHash() != "" {
			sandboxCommand = append(sandboxCommand, "--no-isolate-network")
		}
	isolateNetwork := "yes"
	if pkg.ContentHash() != "" {
		isolateNetwork = "no"
	}

		sandboxCommand = append(sandboxCommand, "--", "/builder")
	interactiveDebug := "no"
	if buildEnv.InteractiveDebug {
		interactiveDebug = "yes"
	}

		cmd := exec.CommandContext(ctx, buildSandboxBinary, sandboxCommand...)
		cmd.Env = []string{
			"HERMES_FETCH_SOCKET=/fetch.sock",
			"out=" + fullPkgPath,
		}
		cmd.Dir = buildDir
		if buildEnv.InteractiveDebug {
			cmd.Stdin = os.Stdin
			cmd.Stdout = os.Stdout
			cmd.Stderr = os.Stderr
		} else {
			cmd.Stdout = buildEnv.BuildOutputSink
			cmd.Stderr = buildEnv.BuildOutputSink
		}
	cmd := exec.Command(buildSandboxBinary, launcher...)
	cmd.Env = append([]string{
		"HERMES_SANDBOX_BUILD_UID=" + buildUser.Uid,
		"HERMES_SANDBOX_ISOLATE_NETWORK=" + isolateNetwork,
		"HERMES_SANDBOX_PKG_OUT_PATH=" + fullPkgPath,
		"HERMES_INTERACTIVE_DEBUG=" + interactiveDebug,
	}, os.Environ()...)

		err = cmd.Run()
		if err != nil {
			return err
		}
	cmd.Dir = tmpDir

		// Ugly, but simple way to force the proxy to exit.
		// we should maybe refactor to use context.Context.
		// or perhaps make a listener + conn that respects contexts.
		_ = l.Close()
		wg.Wait()
	if buildEnv.InteractiveDebug {
		cmd.Stdin = os.Stdin
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
	} else {
		cmd.Stdout = buildEnv.BuildOutputSink
		cmd.Stderr = buildEnv.BuildOutputSink
	}

		select {
		case err := <-proxyErrors:
			return errors.Wrap(err, "error while proxying fetch requests")
		default:
		}
	err = proctools.RunCmd(ctx, cmd, func() { cmd.Process.Signal(unix.SIGTERM) })
	if err != nil {
		return err
	}

		return nil
	// Ugly, but simple way to force the proxy to exit.
	// we should maybe refactor to use context.Context.
	// or perhaps make a listener + conn that respects contexts.
	_ = l.Close()
	wg.Wait()

	select {
	case err := <-proxyErrors:
		return errors.Wrap(err, "error while proxying fetch requests")
	default:
	}

	return nil
}

func unsandboxedBuild(ctx context.Context, buildUser *user.User, buildEnv BuildEnv, pkg *pkgs.Package) error {
func unsandboxedBuild(ctx context.Context, buildEnv BuildEnv, pkg *pkgs.Package) error {
	tmpdir, err := ioutil.TempDir("", "")
	if err != nil {
		return err

M src/store/init.go => src/store/init.go +5 -9
@@ 13,7 13,7 @@ import (
)

type StoreInitConfigTemplate struct {
	BuildSandboxMethod string
	BuildSandboxRunner []string
	BuildUsers         []string
	AuthorizedUsers    []string
	AuthorizedGroups   []string


@@ 81,6 81,10 @@ func InitStore(storePath string, configTemplate StoreInitConfigTemplate) error {
			"authorized_users",
			&configTemplate.AuthorizedUsers,
		},
		{
			"build_sandbox_runner",
			&configTemplate.BuildSandboxRunner,
		},
	} {

		if len(*c.vals) > 0 {


@@ 102,14 106,6 @@ func InitStore(storePath string, configTemplate StoreInitConfigTemplate) error {
		_, _ = fmt.Fprintf(&configBuffer, "cache_server = %q\n", configTemplate.CacheServer)
	}

	switch configTemplate.BuildSandboxMethod {
	case "":
	case "none":
		_, _ = fmt.Fprintf(&configBuffer, "build_sandbox_method = None\n")
	default:
		_, _ = fmt.Fprintf(&configBuffer, "build_sandbox_method = %q\n", configTemplate.BuildSandboxMethod)
	}

	_, err = cfgFile.Write(configBuffer.Bytes())
	if err != nil {
		return errors.Wrap(err, "error writing store config file")

M src/store/store.go => src/store/store.go +42 -28
@@ 132,6 132,11 @@ type Config struct {
	// The users we may run package builds as.
	BuildUsers []UserIdent

	// If the host sandbox does not provide
	// sufficient process isolation for secure builds,
	// this option enforces one build per build user concurrently.
	IsolateBuildUsers bool

	// If not empty, only user ids that are
	// a part of this set may use this package store.
	AuthorizedUserIDs []string


@@ 140,9 145,10 @@ type Config struct {
	// that are in this set may use this package store.
	AuthorizedGroupIDs []string

	// There are multiple types of build sandboxes
	// we want to support.
	BuildSandboxMethod string
	// Sandbox binary arguments, the first argument
	// must be an absolute path to the build sandbox binary,
	// a or relative path to it from the current program.
	BuildSandboxRunner []string
}

// Store represents a connection to the hermes


@@ 173,10 179,6 @@ type Store struct {
	// performing a build.
	builtinsLock *flock.Flock

	// Function used to build packages, based on
	// store config.
	buildFunc BuildFunc

	// Configuration of the package caches.
	cacheConfig *CacheConfig
}


@@ 268,14 270,37 @@ func loadConfig(configPath string) (*Config, error) {
					return nil, errors.Errorf("build_users must contain numbers or strings, got %T", v)
				}
			}
		case "build_sandbox_method":
		case "isolate_build_users":
			switch v := v.(type) {
			case hscript.Bool:
				cfg.IsolateBuildUsers = bool(v)
			default:
				return nil, errors.Errorf("isolate_build_users must be a bool, got %T", v)
			}
		case "build_sandbox_runner":
			switch v := v.(type) {
			case hscript.NoneType:
				cfg.BuildSandboxMethod = "none"
				cfg.BuildSandboxRunner = nil
			case hscript.String:
				cfg.BuildSandboxMethod = string(v)
				cfg.BuildSandboxRunner = []string{string(v)}
			// case XXX list of args.
			default:
				return nil, errors.Errorf("build_sandbox_method must be a string, got '%T'", v)
				return nil, errors.Errorf("build_sandbox_runner must be a string, got '%T'", v)
			}
			if len(cfg.BuildSandboxRunner) != 0 {
				if !filepath.IsAbs(cfg.BuildSandboxRunner[0]) {
					selfBin, err := os.Executable()
					if err != nil {
						return nil, err
					}

					runnerBin := filepath.Join(filepath.Dir(selfBin), cfg.BuildSandboxRunner[0])
					runnerBin, err = filepath.Abs(runnerBin)
					if err != nil {
						return nil, err
					}
					cfg.BuildSandboxRunner[0] = runnerBin
				}
			}
		default:
			return nil, errors.Errorf("unknown config key: '%s'", k)


@@ 410,20 435,6 @@ func Open(storePath string) (*Store, error) {
		return nil, errors.Errorf("current user is not one of the 'authorized_users' or in 'authorized_groups' in config %q", cfgPath)
	}

	var buildFunc BuildFunc

	switch cfg.BuildSandboxMethod {
	case "", NativePlatformSandboxMethod:
		buildFunc = makeSandboxedBuildFunc(cfg)
	case "none":
		if storeUserEuid == 0 {
			return nil, errors.New("refusing to allow unsandboxed builds as root")
		}
		buildFunc = unsandboxedBuild
	default:
		return nil, errors.Errorf("unknown sandbox method %q", cfg.BuildSandboxMethod)
	}

	db, err := extrasqlite.Open(filepath.Join(storePath, "hermes.db"), nil)
	if err != nil {
		return nil, errors.Wrap(err, "unable to open store database")


@@ 448,7 459,6 @@ func Open(storePath string) (*Store, error) {
		cacheConfig:  cacheConfig,
		gcLock:       flock.New(filepath.Join(lockDir, "gc.lock")),
		builtinsLock: flock.New(filepath.Join(lockDir, "build.lock")),
		buildFunc:    buildFunc,
	}

	return store, nil


@@ 1030,7 1040,7 @@ func (store *Store) BuildPackage(ctx context.Context, env BuildEnv, pkg *pkgs.Pa
		return errors.New("no build users in store config, unable to continue with build")
	}

	if NativePlatformRequiresBuildUserLocking {
	if store.cfg.IsolateBuildUsers {
		// We must aquire a user lock so
		// two builds aren't done as the same user at the same time.
		// This helps prevent two builds from interfering with eachother


@@ 1237,7 1247,11 @@ func (store *Store) buildPackage(ctx context.Context, buildUser *user.User, env 
		return errors.Wrapf(err, "unable to create %q in package database", fullPkgPath)
	}

	err = store.buildFunc(ctx, buildUser, env, pkg)
	if len(store.cfg.BuildSandboxRunner) != 0 {
		err = sandboxedBuild(ctx, store.cfg.BuildSandboxRunner, buildUser, env, pkg)
	} else {
		err = unsandboxedBuild(ctx, env, pkg)
	}
	if err != nil {
		return errors.Wrapf(err, "unable to build package %q", fullPkgPath)
	}