add undocumented --chrooted-*-hook calling pivot_root in unshare mode

This commit is contained in:
Johannes Schauer Marin Rodrigues 2022-11-14 09:59:59 +01:00
parent 449fb248e2
commit ea146ad108
Signed by untrusted user: josch
GPG key ID: F2CBA5C78FBD83E1
3 changed files with 173 additions and 6 deletions

View file

@ -346,3 +346,7 @@ Test: error-if-stdout-is-tty
Test: variant-custom-timeout
Test: include-deb-file
Test: pivot_root
Modes: root unshare
Needs-QEMU: true

View file

@ -62,12 +62,17 @@ use version;
*_LINUX_CAPABILITY_VERSION_3 = \0x20080522;
*CAP_SYS_ADMIN = \21;
*PR_CAPBSET_READ = \23;
# from sys/mount.h
*MS_BIND = \0x1000;
*MS_REC = \0x4000;
*MNT_DETACH = \2;
our (
$CLONE_NEWNS, $CLONE_NEWUTS,
$CLONE_NEWIPC, $CLONE_NEWUSER,
$CLONE_NEWPID, $CLONE_NEWNET,
$_LINUX_CAPABILITY_VERSION_3, $CAP_SYS_ADMIN,
$PR_CAPBSET_READ
$PR_CAPBSET_READ, $MS_BIND,
$MS_REC, $MNT_DETACH
);
#<<<
@ -1608,6 +1613,55 @@ sub run_hooks {
{
foreach my $script (@{ $options->{"${name}_hook"} }) {
my $type = $script->[0];
$script = $script->[1];
if ($type eq "pivoted") {
info "running --chrooted-$name-hook in shell: sh -c "
. "'$script'";
my $pid = fork() // error "fork() failed: $!";
if ($pid == 0) {
# child
my @cmdprefix = ();
if ($options->{mode} eq 'fakechroot') {
# we are calling the chroot executable instead of
# chrooting the process so that fakechroot can handle
# it
@cmdprefix = ('chroot', $options->{root});
} elsif ($options->{mode} eq 'root') {
# unsharing the mount namespace is not enough for
# pivot_root to work as root (why?) unsharing the user
# namespace as well (but without remapping) makes
# pivot_root work (why??) but still makes later lazy
# umounts fail (why???). Since pivot_root is mainly
# useful for being able to run unshare mode inside
# unshare mode, we fall back to just calling chroot()
# until somebody has motivation and time to figure out
# what is going on.
chroot $options->{root}
or error "failed to chroot(): $!";
$options->{root} = "/";
chdir "/" or error "failed chdir() to /: $!";
} elsif ($options->{mode} eq 'unshare') {
0 == syscall &SYS_unshare, $CLONE_NEWNS
or error "unshare() failed: $!";
pivot_root($options->{root});
} else {
error "unknown mode: $options->{mode}";
}
0 == system(@cmdprefix, 'env', @env_opts, 'sh', '-c',
$script)
or error "command failed: $script";
exit 0;
}
waitpid($pid, 0);
$? == 0 or error "chrooted hook failed with exit code $?";
next;
}
# inode and device number of chroot before
my ($dev_before, $ino_before, undef) = stat($options->{root});
if (
$script =~ /^(
copy-in|copy-out
@ -1660,6 +1714,26 @@ sub run_hooks {
'sh', '-c', $script, 'exec', $options->{root})
or error "command failed: $script";
}
# If the chroot directory vanished, check if pivot_root was
# performed.
#
# Running pivot_root is only really useful in the customize-hooks
# because mmdebstrap uses apt from the outside to install packages
# and that will fail after pivot_root because the process doesn't
# have access to the system on the outside anymore.
if (!-e $options->{root}) {
my ($dev_root, $ino_root, undef) = stat("/");
if ($dev_before == $dev_root and $ino_before == $ino_root) {
info "detected pivot_root, changing chroot directory to /";
# the old chroot directory is now /
# the hook probably executed pivot_root
$options->{root} = "/";
chdir "/" or error "failed chdir() to /: $!";
} else {
error "chroot directory $options->{root} vanished";
}
}
}
};
@ -3146,6 +3220,23 @@ sub chrooted_realpath {
return $result;
}
sub pivot_root {
my $root = shift;
my $target = "/mnt";
my $put_old = "tmp";
0 == syscall &SYS_mount, $root, $target, 0, $MS_REC | $MS_BIND, 0
or error "mount failed: $!";
chdir "/mnt" or error "failed chdir() to /mnt: $!";
0 == syscall &SYS_pivot_root, my $new_root = ".", $put_old
or error "pivot_root failed: $!";
chroot "." or error "failed to chroot() to .: $!";
0 == syscall &SYS_umount2, $put_old, $MNT_DETACH
or error "umount2 failed: $!";
0 == syscall &SYS_umount2, my $sys = "sys", $MNT_DETACH
or error "umount2 failed: $!";
return;
}
sub hookhelper {
my ($root, $mode, $hook, $qemu, $verbosity, $command, @args) = @_;
$verbosity_level = $verbosity;
@ -4367,16 +4458,25 @@ sub main() {
'force-check-gpg' =>
sub { push @{ $options->{noop} }, 'force-check-gpg'; },
'setup-hook=s' => sub {
push @{ $options->{setup_hook} }, $_[1];
push @{ $options->{setup_hook} }, ["normal", $_[1]];
},
'extract-hook=s' => sub {
push @{ $options->{extract_hook} }, $_[1];
push @{ $options->{extract_hook} }, ["normal", $_[1]];
},
'chrooted-extract-hook=s' => sub {
push @{ $options->{extract_hook} }, ["pivoted", $_[1]];
},
'essential-hook=s' => sub {
push @{ $options->{essential_hook} }, $_[1];
push @{ $options->{essential_hook} }, ["normal", $_[1]];
},
'chrooted-essential-hook=s' => sub {
push @{ $options->{essential_hook} }, ["pivoted", $_[1]];
},
'customize-hook=s' => sub {
push @{ $options->{customize_hook} }, $_[1];
push @{ $options->{customize_hook} }, ["normal", $_[1]];
},
'chrooted-customize-hook=s' => sub {
push @{ $options->{customize_hook} }, ["pivoted", $_[1]];
},
'hook-directory=s' => sub {
my ($opt_name, $opt_value) = @_;
@ -4411,7 +4511,7 @@ sub main() {
# list of hooks
foreach my $hook (keys %scripts) {
push @{ $options->{"${hook}_hook"} },
(sort @{ $scripts{$hook} });
(map { ["normal", $_] } (sort @{ $scripts{$hook} }));
}
},
# Sometimes --simulate fails even though non-simulate succeeds because
@ -4452,6 +4552,14 @@ sub main() {
if (scalar @{ $options->{"${hook}_hook"} } > 0) {
warning "In dry-run mode, --$hook-hook options have no effect";
}
if ($options->{mode} eq 'chrootless') {
foreach my $script (@{ $options->{"${hook}_hook"} }) {
if ($script->[0] eq "pivoted") {
error "--chrooted-$hook-hook are illegal in "
. "chrootless mode";
}
}
}
}
}

55
tests/pivot_root Normal file
View file

@ -0,0 +1,55 @@
#!/bin/sh
set -eu
export LC_ALL=C.UTF-8
export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
trap "rm -f /tmp/chroot1.tar /tmp/chroot2.tar /tmp/chroot3.tar /tmp/mmdebstrap" EXIT INT TERM
if [ ! -e /mmdebstrap-testenv ]; then
echo "this test modifies the system and should only be run inside a container" >&2
exit 1
fi
if [ "$(id -u)" -eq 0 ] && ! id -u user > /dev/null 2>&1; then
adduser --gecos user --disabled-password user
fi
prefix=
[ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && prefix="runuser -u user --"
MMDEBSTRAP=
[ -e /usr/bin/mmdebstrap ] && MMDEBSTRAP=/usr/bin/mmdebstrap
[ -e ./mmdebstrap ] && MMDEBSTRAP=./mmdebstrap
$prefix {{ CMD }} --mode={{ MODE }} --variant=apt \
--include=mount \
{{ DIST }} /tmp/chroot1.tar {{ MIRROR }}
if [ {{ MODE }} = "unshare" ]; then
# calling pivot_root in root mode does not work for mysterious reasons:
# pivot_root: failed to change root from `.' to `mnt': Invalid argument
$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include=mount \
--customize-hook="upload $MMDEBSTRAP /$MMDEBSTRAP" \
--customize-hook='chmod +x "$1"/'"$MMDEBSTRAP" \
--customize-hook='mount -o rbind "$1" /mnt && cd /mnt && /sbin/pivot_root . mnt' \
--customize-hook='unshare -U echo nested unprivileged unshare' \
--customize-hook='{{ CMD }} --mode=unshare --variant=apt --include=mount {{ DIST }} /tmp/chroot3.tar {{ MIRROR }}' \
--customize-hook='copy-out /tmp/chroot3.tar /tmp' \
--customize-hook='rm "$1/'"$MMDEBSTRAP"'"' \
--customize-hook='umount -l mnt sys' \
{{ DIST }} /tmp/chroot2.tar {{ MIRROR }}
cmp /tmp/chroot1.tar /tmp/chroot2.tar
cmp /tmp/chroot1.tar /tmp/chroot3.tar
rm /tmp/chroot2.tar /tmp/chroot3.tar
fi
$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include=mount \
--customize-hook="upload $MMDEBSTRAP /$MMDEBSTRAP" \
--customize-hook='chmod +x "$1"/'"$MMDEBSTRAP" \
--chrooted-customize-hook='{{ CMD }} --mode=unshare --variant=apt --include=mount {{ DIST }} /tmp/chroot3.tar {{ MIRROR }}' \
--customize-hook='copy-out /tmp/chroot3.tar /tmp' \
--customize-hook='rm "$1/'"$MMDEBSTRAP"'"' \
{{ DIST }} /tmp/chroot2.tar {{ MIRROR }}
cmp /tmp/chroot1.tar /tmp/chroot2.tar
cmp /tmp/chroot1.tar /tmp/chroot3.tar