chroot-user: rewrite in C, drop CHROOTENV_EXTRA_BINDS

Formatted via clang-format.
This commit is contained in:
Yegor Timoshenko 2017-11-03 12:07:45 +00:00
parent d03678c227
commit edb59ee7bd
3 changed files with 194 additions and 180 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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}