diff --git a/pkgs/build-support/build-fhs-userenv/chroot-user.rb b/pkgs/build-support/build-fhs-userenv/chroot-user.rb deleted file mode 100755 index 833aab16ceb..00000000000 --- a/pkgs/build-support/build-fhs-userenv/chroot-user.rb +++ /dev/null @@ -1,169 +0,0 @@ -#!/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 diff --git a/pkgs/build-support/build-fhs-userenv/chrootenv.c b/pkgs/build-support/build-fhs-userenv/chrootenv.c new file mode 100644 index 00000000000..8d6c98959cc --- /dev/null +++ b/pkgs/build-support/build-fhs-userenv/chrootenv.c @@ -0,0 +1,182 @@ +#define _GNU_SOURCE + +#include +#include + +#define errorf(status, fmt, ...) \ + error_at_line(status, errno, __FILE__, __LINE__, fmt, ##__VA_ARGS__) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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; +} diff --git a/pkgs/build-support/build-fhs-userenv/default.nix b/pkgs/build-support/build-fhs-userenv/default.nix index d91cdffcf39..5f3ec4dc8ea 100644 --- a/pkgs/build-support/build-fhs-userenv/default.nix +++ b/pkgs/build-support/build-fhs-userenv/default.nix @@ -2,16 +2,19 @@ let buildFHSEnv = callPackage ./env.nix { }; in -args@{ name, runScript ? "bash", extraBindMounts ? [], extraInstallCommands ? "", meta ? {}, passthru ? {}, ... }: +args@{ name, runScript ? "bash", extraInstallCommands ? "", meta ? {}, passthru ? {}, ... }: let - env = buildFHSEnv (removeAttrs args [ "runScript" "extraBindMounts" "extraInstallCommands" "meta" "passthru" ]); + env = buildFHSEnv (removeAttrs args [ "runScript" "extraInstallCommands" "meta" "passthru" ]); - # Sandboxing script - chroot-user = writeScript "chroot-user" '' - #! ${ruby}/bin/ruby - ${builtins.readFile ./chroot-user.rb} - ''; + chrootenv = stdenv.mkDerivation { + name = "chrootenv"; + + unpackPhase = "cp ${./chrootenv.c} chrootenv.c"; + installPhase = "cp chrootenv $out"; + + makeFlags = [ "chrootenv" ]; + }; init = run: writeScript "${name}-init" '' #! ${stdenv.shell} @@ -32,8 +35,7 @@ in runCommand name { passthru = passthru // { env = runCommand "${name}-shell-env" { shellHook = '' - ${lib.optionalString (extraBindMounts != []) ''export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:$CHROOTENV_EXTRA_BINDS"''} - exec ${chroot-user} ${init "bash"} "$(pwd)" + exec ${chrootenv} ${init "bash"} "$(pwd)" ''; } '' echo >&2 "" @@ -46,8 +48,7 @@ in runCommand name { mkdir -p $out/bin cat <$out/bin/${name} #! ${stdenv.shell} - ${lib.optionalString (extraBindMounts != []) ''export CHROOTENV_EXTRA_BINDS="${lib.concatStringsSep ":" extraBindMounts}:$CHROOTENV_EXTRA_BINDS"''} - exec ${chroot-user} ${init runScript} "\$(pwd)" "\$@" + exec ${chrootenv} ${init runScript} "\$(pwd)" "\$@" EOF chmod +x $out/bin/${name} ${extraInstallCommands}