Change:
doesn't work inside sandbox. It can sometimes be useful to build, or unpack something in an environment that has a lot of assumptions (say, self-extracting checksummed executable, or build system that expects FHS-compliant paths). Given containing: Given : System: 18.03pre118631. (Impala) Nix version: 1.11.15 Nixpkgs version: Sandboxing enabled: yes /cc
I think at this point you would need to use the to do this. See e.g. .
I have noticed the same. I came up with this simpler FHS environment to run Altera Quartus installer on NixOS with sandbox build:
Thanks! This looks nice, and no Ruby dependency. BTW, just commenting out line 45 in script (one that raises the exception) solves the issue. Probably makes sense to mount and only if available.
Yes, +1 for working buildFHSUserEnv in sandboxed build.

Discussion:
<del> #!/usr/bin/env ruby # Bind mounts hierarchy: from => to (relative) # If 'to' is nil, path will be the same mounts = { '/' => 'host', '/proc' => nil, '/sys' => nil, '/nix' => nil, '/tmp' => nil, '/var' => nil, '/run' => nil, '/dev' => nil, '/home' => nil, } # Propagate environment variables envvars = [ 'TERM', 'DISPLAY', 'XAUTHORITY', 'HOME', 'XDG_RUNTIME_DIR', 'LANG', 'SSL_CERT_FILE', 'DBUS_SESSION_BUS_ADDRESS', ] require 'tmpdir' require 'fileutils' require 'pathname' require 'set' require 'fiddle' def write_file(path, str) File.open(path, 'w') { |file| file.write str } end # Import C standard library and several needed calls $libc = Fiddle.dlopen nil def make_fcall(name, args, output) c = Fiddle::Function.new $libc[name], args, output lambda do |*args| ret = c.call *args raise SystemCallError.new Fiddle.last_error if ret < 0 return ret end end $fork = make_fcall 'fork', [], Fiddle::TYPE_INT CLONE_NEWNS   = 0x00020000 CLONE_NEWUSER = 0x10000000 $unshare = make_fcall 'unshare', [Fiddle::TYPE_INT], Fiddle::TYPE_INT MS_BIND = 0x1000 MS_REC  = 0x4000 MS_SLAVE  = 0x80000 $mount = make_fcall 'mount', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT # Read command line args abort "Usage: chrootenv program args..." unless ARGV.length >= 1 execp = ARGV # Populate extra mounts if not ENV["CHROOTENV_EXTRA_BINDS"].nil? $stderr.puts "CHROOTENV_EXTRA_BINDS is discussed for deprecation." $stderr.puts "If you have a usecase, please drop a note in issue #16030." $stderr.puts "Notice that we now bind-mount host FS to '/host' and symlink all directories from it to '/' by default." for extra in ENV["CHROOTENV_EXTRA_BINDS"].split(':') paths = extra.split('=') if not paths.empty? if paths.size <= 2 mounts[paths[0]] = paths[1] else $stderr.puts "Ignoring invalid entry in CHROOTENV_EXTRA_BINDS: #{extra}" end end end end # Set destination paths for mounts mounts = mounts.map { |k, v| [k, v.nil? ? k.sub(/^/*/, '') : v] }.to_h # Create temporary directory for root and chdir root = Dir.mktmpdir 'chrootenv' # Fork process; we need this to do a proper cleanup because # child process will chroot into temporary directory. # We use imported 'fork' instead of native to overcome # CRuby's meddling with threads; this should be safe because # we don't use threads at all. $cpid = $fork.call if $cpid == 0 # If we are root, no need to create new user namespace. if Process.uid == 0 $unshare.call CLONE_NEWNS # Mark all mounted filesystems as slave so changes # don't propagate to the parent mount namespace. $mount.call nil, '/', nil, MS_REC | MS_SLAVE, nil else # Save user UID and GID uid = Process.uid gid = Process.gid # Create new mount and user namespaces # CLONE_NEWUSER requires a program to be non-threaded, hence # native fork above. $unshare.call CLONE_NEWNS | CLONE_NEWUSER # Map users and groups to the parent namespace begin # setgroups is only available since Linux 3.19 write_file '/proc/self/setgroups', 'deny' rescue end write_file '/proc/self/uid_map', "#{uid} #{uid} 1" write_file '/proc/self/gid_map', "#{gid} #{gid} 1" end # Do rbind mounts. mounts.each do |from, rto| to = "#{root}/#{rto}" FileUtils.mkdir_p to $mount.call from, to, nil, MS_BIND | MS_REC, nil end # Don't make root private so privilege drops inside chroot are possible File.chmod(0755, root) # Chroot! Dir.chroot root Dir.chdir '/' # New environment new_env = Hash[ envvars.map { |x| [x, ENV[x]] } ] # Finally, exec! exec(new_env, *execp, close_others: true, unsetenv_others: true) end # Wait for a child. If we catch a signal, resend it to child and continue # waiting. def wait_child begin Process.wait # Return child's exit code if $?.exited? exit $?.exitstatus else exit 1 end rescue SignalException => e Process.kill e.signo, $cpid wait_child end end begin wait_child ensure # Cleanup FileUtils.rm_rf root, secure: true end </del>

<ins> #define _GNU_SOURCE #include <errno.h> #include <error.h> #define errorf(status, fmt, ...)                                                error_at_line(status, errno, __FILE__, __LINE__, fmt, ##__VA_ARGS__) #include <dirent.h> #include <ftw.h> #include <sched.h> #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sysexits.h> #include <unistd.h> #include <sys/mount.h> #include <sys/stat.h> #include <sys/wait.h> char *env_whitelist[] = {"TERM", "DISPLAY", "XAUTHORITY", "HOME", "XDG_RUNTIME_DIR", "LANG", "SSL_CERT_FILE", "DBUS_SESSION_BUS_ADDRESS"}; char **env_build(char *names[], size_t len) { char *env, **ret = malloc((len + 1) * sizeof(char *)), **ptr = ret; for (size_t i = 0; i < len; i++) { if ((env = getenv(names[i]))) { if (asprintf(ptr++, "%s=%s", names[i], env) < 0) errorf(EX_OSERR, "asprintf"); } } *ptr = NULL; return ret; } struct bind { char *from; char *to; }; struct bind binds[] = {{"/", "host"},   {"/proc", "proc"}, {"/sys", "sys"}, {"/nix", "nix"}, {"/tmp", "tmp"},   {"/var", "var"}, {"/run", "run"}, {"/dev", "dev"},   {"/home", "home"}}; void bind(struct bind *bind) { DIR *src = opendir(bind->from); if (src) { if (closedir(src) < 0) errorf(EX_IOERR, "closedir"); if (mkdir(bind->to, 0755) < 0) errorf(EX_IOERR, "mkdir"); if (mount(bind->from, bind->to, "bind", MS_BIND | MS_REC, NULL) < 0) errorf(EX_OSERR, "mount"); } else { // https://github.com/NixOS/nixpkgs/issues/31104 if (errno != ENOENT) errorf(EX_OSERR, "opendir"); } } void spitf(char *path, char *fmt, ...) { va_list args; va_start(args, fmt); FILE *f = fopen(path, "w"); if (f == NULL) errorf(EX_IOERR, "spitf(%s): fopen", path); if (vfprintf(f, fmt, args) < 0) errorf(EX_IOERR, "spitf(%s): vfprintf", path); if (fclose(f) < 0) errorf(EX_IOERR, "spitf(%s): fclose", path); } int nftw_rm(const char *path, const struct stat *sb, int type, struct FTW *ftw) { if (remove(path) < 0) errorf(EX_IOERR, "nftw_rm"); return 0; } #define LEN(x) sizeof(x) / sizeof(*x) int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s command [arguments...]n" "Requires Linux kernel >= 3.19 with CONFIG_USER_NS.n", argv[0]); exit(EX_USAGE); } char tmpl[] = "/tmp/chrootenvXXXXXX"; char *root = mkdtemp(tmpl); if (root == NULL) errorf(EX_IOERR, "mkdtemp"); // Don't make root private so that privilege drops inside chroot are possible: if (chmod(root, 0755) < 0) errorf(EX_IOERR, "chmod"); pid_t cpid = fork(); if (cpid < 0) errorf(EX_OSERR, "fork"); if (cpid == 0) { uid_t uid = getuid(); gid_t gid = getgid(); // If we are root, no need to create new user namespace. if (uid == 0) { if (unshare(CLONE_NEWNS) < 0) errorf(EX_OSERR, "unshare() failed: You may have an old kernel or have CLONE_NEWUSER disabled by your distribution security settings."); // Mark all mounted filesystems as slave so changes // don't propagate to the parent mount namespace. if (mount(NULL, "/", NULL, MS_REC | MS_SLAVE, NULL) < 0) errorf(EX_OSERR, "mount"); } else { // Create new mount and user namespaces. CLONE_NEWUSER // requires a program to be non-threaded. if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) errorf(EX_OSERR, "unshare"); // Map users and groups to the parent namespace. // setgroups is only available since Linux 3.19: spitf("/proc/self/setgroups", "deny"); spitf("/proc/self/uid_map", "%d %d 1", uid, uid); spitf("/proc/self/gid_map", "%d %d 1", gid, gid); } if (chdir(root) < 0) errorf(EX_IOERR, "chdir"); for (size_t i = 0; i < LEN(binds); i++) bind(&binds[i]); if (chroot(root) < 0) errorf(EX_OSERR, "chroot"); if (chdir("/") < 0) errorf(EX_OSERR, "chdir"); argv++; if (execvpe(*argv, argv, env_build(env_whitelist, LEN(env_whitelist))) < 0) errorf(EX_OSERR, "execvpe"); } int status; if (waitpid(cpid, &status, 0) < 0) errorf(EX_OSERR, "waitpid"); if (nftw(root, nftw_rm, getdtablesize(), FTW_DEPTH | FTW_MOUNT | FTW_PHYS) < 0) errorf(EX_IOERR, "nftw"); if (WIFEXITED(status)) return WEXITSTATUS(status); else if (WIFSIGNALED(status)) kill(getpid(), WTERMSIG(status)); return EX_OSERR; } </ins>

let buildFHSEnv = callPackage ./env.nix { }; in <del> args@{ name, runScript ? "bash", extraBindMounts ? [], extraInstallCommands ? "", meta ? {}, passthru ? {}, ... }: </del> <ins> args@{ name, runScript ? "bash", extraInstallCommands ? "", meta ? {}, passthru ? {}, ... }: </ins> let <del> env = buildFHSEnv (removeAttrs args [ "runScript" "extraBindMounts" "extraInstallCommands" "meta" "passthru" ]); </del> <ins> env = buildFHSEnv (removeAttrs args [ "runScript" "extraInstallCommands" "meta" "passthru" ]); </ins> <del> # Sandboxing script chroot-user = writeScript "chroot-user" '' #! ${ruby}/bin/ruby ${builtins.readFile ./chroot-user.rb} ''; </del> <ins> chrootenv = stdenv.mkDerivation { name = "chrootenv"; unpackPhase = "cp ${./chrootenv.c} chrootenv.c"; installPhase = "cp chrootenv $out"; makeFlags = [ "chrootenv" ]; }; </ins> init = run: writeScript "${name}-init" '' #! ${stdenv.shell}

passthru = passthru // { env = runCommand "${name}-shell-env" { shellHook = '' <del> ${lib.optionalString (extraBindMounts != []) ''export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:$CHROOTENV_EXTRA_BINDS"''} exec ${chroot-user} ${init "bash"} "$(pwd)" </del> <ins> exec ${chrootenv} ${init "bash"} "$(pwd)" </ins> ''; } '' echo >&2 ""

mkdir -p $out/bin cat <<EOF >$out/bin/${name} #! ${stdenv.shell} <del> ${lib.optionalString (extraBindMounts != []) ''export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:$CHROOTENV_EXTRA_BINDS"''} exec ${chroot-user} ${init runScript} "$(pwd)" "$@" </del> <ins> exec ${chrootenv} ${init runScript} "$(pwd)" "$@" </ins> EOF chmod +x $out/bin/${name} ${extraInstallCommands}