App-EvalServerAdvanced
view release on metacpan or search on metacpan
lib/App/EvalServerAdvanced/Sandbox.pm view on Meta::CPAN
return "".$p
}
my $seccomp;
sub run_eval {
my $code = shift; # TODO this should be more than just code
my $language = shift;
my $files = shift;
my $work_path = Path::Tiny->tempdir("eval-XXXXXXXX");
$|++;
chmod(0555, $work_path); # have to fix permissions on the new / or nobody can do anything!
unless ($seccomp) {
App::EvalServerAdvanced::Sandbox::Internal->load_plugins();
$seccomp = App::EvalServerAdvanced::Seccomp->new();
$seccomp->load_yaml(config->sandbox->seccomp->yaml); # TODO allow multiple yamls
$seccomp->build_seccomp;
}
my @binds = config->sandbox->bind_mounts->@*;
# Setup SECCOMP for us
my $lang_config = config->language->$language;
die "Language $language not configured." unless $lang_config;
my $profile = $lang_config->seccomp_profile // "default";
# Get the nobody uid before we chroot, namespace and do other funky stuff.
my $nobody_uid = getpwnam("nobody");
die "Error, can't find a uid for 'nobody'. Replace with someone who exists" unless $nobody_uid;
my $exitcode = $namespace->run(code => sub {
delete $SIG{CHLD};
select(STDERR);
$|++;
select(STDOUT);
$|++;
binmode STDOUT, ":encoding(utf8)"; # Enable utf8 output.
binmode STDERR, ":encoding(utf8)"; # Enable utf8 output.
# This should end up actually reading from the IO::Async::Process stdin filehandle eventually
# but I'm not ready to setup the protocol for that yet.
# redirect STDIN to /dev/null, to avoid warnings in convoluted cases.
close(STDIN);
open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!";
my $tmpfs_size = config->sandbox->tmpfs_size // "16m"; # / # fix syntax in kate
my $jail_path = $work_path . "/jail";
my $jail_home = $jail_path . (config->sandbox->home_dir // "/home"); # " # ditto
my $jail_tmp = "$jail_path/tmp";
mount("tmpfs", $work_path, "tmpfs", 0, {size => $tmpfs_size});
mount("tmpfs", $work_path, "tmpfs", MS_PRIVATE, {size => $tmpfs_size});
path($jail_path)->mkpath();
# put this all in a tmpfs, so that we don't pollute anywhere if possible. TODO this should be overlayfs!
path("$work_path/tmp/.overlayfs")->mkpath();
# setup /tmp
path($jail_tmp)->mkpath;
umask(0);
for my $bind (@binds) {
my $src = _rel2abs($bind->{src});
my $target = $bind->{target};
if ($target eq config->sandbox->home_dir) {
# We need to use overlayfs to bring the homedir in, so it's writable inside
# without being writable to the outside
$target = $work_path . "/home";
} else {
$target = $jail_path . $target;
}
path($target)->mkpath;
eval {
mount($src, $target, undef, MS_BIND|MS_PRIVATE|MS_RDONLY, undef)
};
if ($@) {
die "Failed to mount ", $src, " to ", $target, ": $@\n";
}
}
my $overlay_opts = {upperdir => $jail_tmp, lowerdir => "$work_path/home", workdir => "$work_path/tmp/.overlayfs"};
path("$work_path/home")->mkpath; # Make sure it's made, even if it's not being mounted
path($jail_home)->mkpath;
mount("overlay", $jail_home, "overlay", 0, $overlay_opts);
# Setup /dev
path("$jail_path/dev")->mkpath;
for my $dev_name (keys config->sandbox->devices->%*) {
my ($type, $major, $minor) = config->sandbox->devices->$dev_name->@*;
_exit(213) unless $type eq 'c';
mknod("$jail_path/dev/$dev_name", S_IFCHR|0666, makedev($major, $minor));
}
path("$jail_path/tmp")->chmod(0777);
path($jail_home)->chmod(0777);
# Do these before the chroot. Just to avoid weird autoloading issues
set_resource_limits();
chdir($jail_path) or die "Jail was not made"; # ensure it exists before we chroot. unnecessary?
chroot($jail_path) or die $!;
chdir(config->sandbox->home_dir // "/home") or die "Couldn't chdir to the home"; #'
# TODO Also look at making calls about dropping capabilities(2). I don't think it's needed but it might be a good idea
# Here's where we actually drop our root privilege
$)="$nobody_uid $nobody_uid";
$(=$nobody_uid;
$<=$>=$nobody_uid;
POSIX::setgid($nobody_uid); #We just assume the uid is the same as the gid. Hot.
die "Failed to drop to nobody"
if $> != $nobody_uid
or $< != $nobody_uid;
%ENV = config->sandbox->environment->%*; # set the environment up
my $main_file;
# Create the other files.
for my $file (@$files) {
my $filename = $file->filename;
my $contents = $file->contents;
if ($filename eq '__code') {
$main_file = $file;
next; # don't write it here
}
my $path = path($filename);
$path->parent()->mkpath(); # try to create the directory needed. If it fails, the eval fails
open(my $fh, ">", $filename) or die "Can't write to $filename: $!";
print $fh $contents; # simple output, don't worry about encodings?
close($fh);
}
# Enable seccomp
$seccomp->apply_seccomp($profile); # TODO Make this optional, somehow for testing
# TODO make this accept a filename, that's already written instead of code
run_code($language, $code, $main_file);
});
rmdir($work_path) or warn "Couldn't remove tempdir";
( run in 1.097 second using v1.01-cache-2.11-cpan-df04353d9ac )