#! /usr/bin/perl

use strict;
use warnings;
use Errno ();
use Getopt::Long;
use Pod::Usage qw(pod2usage);
require 'syscall.ph';

our $VERSION = '0.01';

our @NEWDIRS = qw(etc etc/ssl run usr var var/log);
our @BINDDIRS = grep { -d "/$_" } qw(bin etc/ssl/certs lib lib64 sbin usr/bin usr/include usr/lib usr/lib64 usr/sbin usr/share usr/src);
our @TEMPDIRS = qw(tmp run/lock var/tmp);
our %KEEP_CAPS = map { $_ => 1 } (
    0,  # CAP_CHOWN
    3,  # CAP_FOWNER
    4,  # CAP_FSETID
    6,  # CAP_SETGID
    14, # CAP_IPC_LOCK
);

my ($root, $opt_umount);

GetOptions(
    'root=s' => \$root,
    umount   => \$opt_umount,
    help     => sub {
        pod2usage(0);
    },
) or exit(1);

# cleanup $root
die "--root not specified\n"
    unless $root;
die "--root must be an absolute path\n"
    unless $root =~ m{^/}s;
$root =~ s{/$}{}gs;

if ($opt_umount) {
    die "root does not exist, cowardly refusing to umount\n"
        unless -d $root;
    open my $fh, "-|", "LANG=C mount"
        or die "failed to exec mount:$!";
    while (my $line = <$fh>) {
        if ($line =~ m{ on $root/([^ ]+)}) {
            run_cmd("umount", "$root/$1");
        }
    }
    exit 0;
}

# create the directory if not exists
if (! -e $root) {
    mkdir $root
        or die "failed to create directory:$root:$!";
}

# create directories
for my $dir (@NEWDIRS, @BINDDIRS, @TEMPDIRS) {
    mkdir "$root/$dir"
       or $! == Errno::EEXIST
       or die "failed to create directory:$!";
}

# chmod the temporary directories
for my $dir (@TEMPDIRS) {
    chmod 01777, "$root/$dir"
        or die "failed to chmod $root/$dir:$!";
}

# create passwd and group
if (! -e "$root/etc/passwd") {
    system("egrep '^(root|nobody):' < /etc/passwd > $root/etc/passwd") == 0
        or die "failed to create $root/etc/passwd file:$!";
}
if (! -e "$root/etc/group") {
    system("egrep '^(root|nobody|nogroup):' < /etc/group > $root/etc/group") == 0
        or die "failed to create $root/etc/group file:$!";
}

for my $dir (@BINDDIRS) {
    if (is_empty("$root/$dir")) {
        run_cmd("mount", "--bind", "/$dir", "$root/$dir");
        run_cmd("mount", "-o", "remount,ro,bind", "$root/$dir");
    }
}

# create symlinks
symlink "../run/lock", "$root/var/lock";

# create devices
mkdir "$root/dev";
run_cmd(qw(mknod -m 666), "$root/dev/null", qw(c 1 3))
    unless -e "$root/dev/null";
run_cmd(qw(mknod -m 666), "$root/dev/zero", qw(c 1 5))
    unless -e "$root/dev/zero";
for my $f (qw(random urandom)) {
    run_cmd(qw(mknod -m 444), "$root/dev/$f", qw(c 1 9))
        unless -e "$root/dev/$f";
}

# just print the status if no args
if (! @ARGV) {
    print "jail is ready!\n";
    exit 0;
}

# chroot and exec
chroot($root)
    or die "failed to chroot to $root:$!";
chdir "/"
    or die "failed to chroot to /:$!";
drop_capabilities();
exec @ARGV;
die "failed to exec:$ARGV[0]:$!";

sub run_cmd {
    my @argv = @_;
    system(@argv) == 0
        or die "failed to exec $argv[0]:$!";
}

sub is_empty {
    my $path = shift;
    opendir my $fh, $path
        or die "failed to opendir:$path:$!";
    while (my $fn = readdir $fh) {
        return 0
            unless $fn eq '.' || $fn eq '..';
    }
    return 1;
}

sub drop_capabilities {
    for (my $i = 0; ; ++$i) {
       if (! $KEEP_CAPS{$i}) {
           # test if capability exists
           last if syscall(&SYS_prctl, 23) < 0;
           if (syscall(&SYS_prctl, 24, $i) < 0) {
               warn "failed to drop capability:$i";
           }
       }
    }
}

__END__

=head1 NAME

jailing - a minimalistic chroot jail builder/runner for Linux

=head1 SYNOPSIS

  # create and/or enter the jail, and optionally run the command
  jailing --root=/path/to/chroot/jail [cmd ...]

  # unmount the bind mounts of the jail
  jailing --root=/path/to/chroot/jail --umount

=head1 DESCRIPTION

The command creates a chroot jail if it does not exist, and runs the given commands within the jail.

The system directories are remounted read-only (via `mount --bind` and `mount -o remount,ro`) to minimalize the setup time and disk usage.  Other directories are created automatically.

=head1 OPTIONS

=head2 --root=path

mandatory argument specifying the root of the chroot jail.
The directory is automatically created if it does not exist.

=head2 --umount

unmounts the bound mount points for the jail

=head1 AUTHOR

Kazuho Oku

=head1 LICENSE

MIT

