chroot-user: rewrite in C, drop CHROOTENV_EXTRA_BINDS
Formatted via clang-format.
This commit is contained in:
parent
d03678c227
commit
edb59ee7bd
|
@ -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
|
182
pkgs/build-support/build-fhs-userenv/chrootenv.c
Normal file
182
pkgs/build-support/build-fhs-userenv/chrootenv.c
Normal 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;
|
||||
}
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue