summary refs log tree commit diff
path: root/pkgs/build-support/build-fhs-userenv
diff options
context:
space:
mode:
authorYegor Timoshenko <yegortimoshenko@gmail.com>2017-11-03 12:07:45 +0000
committerYegor Timoshenko <yegortimoshenko@gmail.com>2017-11-09 19:58:55 +0000
commitedb59ee7bdd0e419d35ea9202feba251692f821e (patch)
tree4af8247996088bbe367a3fc9894cda3f9e0b18ae /pkgs/build-support/build-fhs-userenv
parentd03678c227bf40192a353939c61cb533c7abe5a0 (diff)
downloadnixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar.gz
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar.bz2
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar.lz
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar.xz
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.tar.zst
nixpkgs-edb59ee7bdd0e419d35ea9202feba251692f821e.zip
chroot-user: rewrite in C, drop CHROOTENV_EXTRA_BINDS
Formatted via clang-format.
Diffstat (limited to 'pkgs/build-support/build-fhs-userenv')
-rwxr-xr-xpkgs/build-support/build-fhs-userenv/chroot-user.rb169
-rw-r--r--pkgs/build-support/build-fhs-userenv/chrootenv.c182
-rw-r--r--pkgs/build-support/build-fhs-userenv/default.nix23
3 files changed, 194 insertions, 180 deletions
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..edead5d3ab6
--- /dev/null
+++ b/pkgs/build-support/build-fhs-userenv/chrootenv.c
@@ -0,0 +1,182 @@
+#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");
+      // 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 <<EOF >$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}