diff --git a/coverage.sh b/coverage.sh index 9880f8e..aaadd6a 100755 --- a/coverage.sh +++ b/coverage.sh @@ -1936,6 +1936,44 @@ else runtests=$((runtests+1)) fi +print_header "mode=root,variant=apt: test special hooks using helpers" +cat << END > shared/test.sh +#!/bin/sh +set -eu +export LC_ALL=C.UTF-8 +mkfifo /tmp/myfifo +mkdir /tmp/root +ln -s /real /tmp/root/link +mkdir /tmp/root/real +run_testA() { + echo content > /tmp/foo + { { { $CMD --hook-helper /tmp/root root setup env 1 upload /tmp/foo \$1 < /tmp/myfifo 3>&-; echo \$? >&3; + } | $CMD --hook-listener 3>&- >/tmp/myfifo; echo \$?; } 3>&1; + } | { read xs1; [ "\$xs1" -eq 0 ]; read xs2; [ "\$xs2" -eq 0 ]; } + echo content | diff -u - /tmp/root/real/foo + rm /tmp/foo + rm /tmp/root/real/foo +} +run_testA link/foo +run_testA /link/foo +run_testA ///link///foo/// +run_testA /././link/././foo/././ +run_testA /link/../link/foo +run_testA /link/../../link/foo +run_testA /../../link/foo +rmdir /tmp/root/real +rm /tmp/root/link +rmdir /tmp/root +rm /tmp/myfifo +END +if [ "$HAVE_QEMU" = "yes" ]; then + ./run_qemu.sh + runtests=$((runtests+1)) +else + ./run_null.sh SUDO + runtests=$((runtests+1)) +fi + # test special hooks for mode in root unshare fakechroot proot; do print_header "mode=$mode,variant=apt: test special hooks with $mode mode" diff --git a/mmdebstrap b/mmdebstrap index 2c66a43..fda9887 100755 --- a/mmdebstrap +++ b/mmdebstrap @@ -2954,6 +2954,341 @@ sub hookhelper { return; } +sub hooklistener { + # we put everything in an eval block because that way we can easily handle + # errors without goto labels or much code duplication: the error handler + # has to send an "error" message to the other side + eval { + while (1) { + # get the next message + my $msg = "error"; + my $len = -1; + { + debug "reading next command"; + my $ret = read(STDIN, my $buf, 2 + 5) + // error "cannot read from socket: $!"; + debug "finished reading command"; + if ($ret == 0) { + error "received eof on socket"; + } + ($len, $msg) = unpack("nA5", $buf); + } + if ($msg eq "adios") { + # setup finished, so we break out of the loop + if ($len != 0) { + error "expected no payload but got $len bytes"; + } + last; + } elsif ($msg eq "openr") { + # handle the openr message + debug "received message: openr"; + my $infile; + { + my $ret = read(STDIN, $infile, $len) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # make sure that the requested path exists outside the chroot + if (!-e $infile) { + error "$infile does not exist"; + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + + open my $fh, '<', $infile + or error "failed to open $infile for reading: $!"; + + # read from the file and send as payload to the child process + while (1) { + # read from file + my $ret = read($fh, my $cont, 4096) + // error "cannot read from pipe: $!"; + if ($ret == 0) { last; } + debug "sending write"; + # send to child + print STDOUT pack("n", $ret) . "write" . $cont; + STDOUT->flush(); + debug "waiting for okthx"; + checkokthx \*STDIN; + if ($ret < 4096) { last; } + } + + # signal to the child process that we are done + debug "sending close"; + print STDOUT pack("n", 0) . "close"; + STDOUT->flush(); + debug "waiting for okthx"; + checkokthx \*STDIN; + + close $fh; + } elsif ($msg eq "openw") { + debug "received message: openw"; + # payload is the output directory + my $outfile; + { + my $ret = read(STDIN, $outfile, $len) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # make sure that the directory exists + my $outdir = dirname($outfile); + if (-e $outdir) { + if (!-d $outdir) { + error "$outdir already exists but is not a directory"; + } + } else { + my $num_created = make_path $outdir, { error => \my $err }; + if ($err && @$err) { + error( + join "; ", + ( + map { "cannot create " . (join ": ", %{$_}) } + @$err + )); + } elsif ($num_created == 0) { + error "cannot create $outdir"; + } + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + + # now we expect one or more "write" messages containing the + # tarball to write + open my $fh, '>', $outfile + or error "failed to open $outfile for writing: $!"; + + # handle "write" messages from the child process and feed + # their payload into the file handle until a "close" message + # is encountered + while (1) { + # receive the next message + my $ret = read(STDIN, my $buf, 2 + 5) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + my ($len, $msg) = unpack("nA5", $buf); + debug "received message: $msg"; + if ($msg eq "close") { + # finish the loop + if ($len != 0) { + error "expected no payload but got $len bytes"; + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + last; + } elsif ($msg ne "write") { + # we should not receive this message at this point + error "expected write but got: $msg"; + } + # read the payload + my $content; + { + my $ret = read(STDIN, $content, $len) + // error "error cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # write the payload to the file handle + print $fh $content + or error "cannot write to file handle: $!"; + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + } + close $fh; + } elsif (any { $_ eq $msg } ('mktar', 'mktac')) { + # handle the mktar message + debug "received message: $msg"; + my $indir; + { + my $ret = read(STDIN, $indir, $len) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # make sure that the requested path exists outside the chroot + if (!-e $indir) { + error "$indir does not exist"; + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + + # Open a tar process creating a tarfile of the instructed + # path. To emulate the behaviour of cp, change to the + # dirname of the requested path first. + open my $fh, '-|', 'tar', '--numeric-owner', '--xattrs', + '--format=pax', + '--pax-option=exthdr.name=%d/PaxHeaders/%f,' + . 'delete=atime,delete=ctime', + '--directory', + $msg eq 'mktar' ? dirname($indir) : $indir, + '--create', '--file', '-', + $msg eq 'mktar' ? basename($indir) : '.' + // error "failed to fork(): $!"; + + # read from the tar process and send as payload to the child + # process + while (1) { + # read from tar + my $ret = read($fh, my $cont, 4096) + // error "cannot read from pipe: $!"; + if ($ret == 0) { last; } + debug "sending write"; + # send to child + print STDOUT pack("n", $ret) . "write" . $cont; + STDOUT->flush(); + debug "waiting for okthx"; + checkokthx \*STDIN; + if ($ret < 4096) { last; } + } + + # signal to the child process that we are done + debug "sending close"; + print STDOUT pack("n", 0) . "close"; + STDOUT->flush(); + debug "waiting for okthx"; + checkokthx \*STDIN; + + close $fh; + if ($? != 0) { + error "tar failed"; + } + } elsif ($msg eq "untar") { + debug "received message: untar"; + # payload is the output directory + my $outdir; + { + my $ret = read(STDIN, $outdir, $len) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # make sure that the directory exists + if (-e $outdir) { + if (!-d $outdir) { + error "$outdir already exists but is not a directory"; + } + } else { + my $num_created = make_path $outdir, { error => \my $err }; + if ($err && @$err) { + error( + join "; ", + ( + map { "cannot create " . (join ": ", %{$_}) } + @$err + )); + } elsif ($num_created == 0) { + error "cannot create $outdir"; + } + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + + # now we expect one or more "write" messages containing the + # tarball to unpack + open my $fh, '|-', 'tar', '--numeric-owner', '--xattrs', + '--xattrs-include=*', '--directory', $outdir, + '--extract', '--file', + '-' // error "failed to fork(): $!"; + + # handle "write" messages from the child process and feed + # their payload into the tar process until a "close" message + # is encountered + while (1) { + # receive the next message + my $ret = read(STDIN, my $buf, 2 + 5) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + my ($len, $msg) = unpack("nA5", $buf); + debug "received message: $msg"; + if ($msg eq "close") { + # finish the loop + if ($len != 0) { + error "expected no payload but got $len bytes"; + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + last; + } elsif ($msg ne "write") { + # we should not receive this message at this point + error "expected write but got: $msg"; + } + # read the payload + my $content; + { + my $ret = read(STDIN, $content, $len) + // error "error cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + # write the payload to the tar process + print $fh $content + or error "cannot write to tar process: $!"; + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + } + close $fh; + if ($? != 0) { + error "tar failed"; + } + } elsif ($msg eq "nblks") { + # handle the nblks message + my $numblocks; + debug "received message: nblks"; + { + my $ret = read(STDIN, $numblocks, $len) + // error "cannot read from socket: $!"; + if ($ret == 0) { + error "received eof on socket"; + } + } + if ($numblocks !~ /^\d+$/) { + error "invalid number of blocks: $numblocks"; + } + debug "sending okthx"; + print STDOUT (pack("n", 0) . "okthx") + or error "cannot write to socket: $!"; + STDOUT->flush(); + } else { + error "unknown message: $msg"; + } + } + }; + if ($@) { + # inform the other side that something went wrong + print STDOUT (pack("n", 0) . "error") + or error "cannot write to socket: $!"; + STDOUT->flush(); + } + return; +} + sub guess_sources_format { my $content = shift; my $is_deb822 = 0; @@ -3019,6 +3354,14 @@ sub main() { hookhelper(); exit 0; } + + # this is the counterpart to --hook-helper and will receive and carry + # out its instructions + if (scalar @ARGV == 1 && $ARGV[0] eq "--hook-listener") { + hooklistener(); + exit 0; + } + # this is like: # lxc-usernsexec -- lxc-unshare -s 'MOUNT|PID|UTSNAME|IPC' ... # but without needing lxc @@ -4163,589 +4506,282 @@ sub main() { $fname, sprintf('%07o', $mode), sprintf('%07o', 0), # uid - sprintf('%07o', 0), # gid - sprintf('%011o', 0), # size - sprintf('%011o', $mtime), - '', # checksum - $type, - $linkname, - "ustar ", - '', # username - '', # groupname - defined($devmajor) ? sprintf('%07o', $devmajor) : '', - defined($devminor) ? sprintf('%07o', $devminor) : '', - '', # prefix - ); - # compute and insert checksum - substr($entry, 148, 7) - = sprintf("%06o\0", unpack("%16C*", $entry)); - $devtar .= $entry; - } - } elsif ($format eq 'directory') { - # nothing to do - } else { - error "unknown format: $format"; - } - - my $numblocks = 0; - my $exitstatus = 0; - my @taropts = ( - '--sort=name', - "--mtime=\@$mtime", - '--clamp-mtime', - '--numeric-owner', - '--one-file-system', - '--format=pax', - '--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime', - '-c', - '--exclude=./dev' - ); - # tar2sqfs and genext2fs do not support extended attributes - if ($format eq "squashfs") { - warning "tar2sqfs does not support extended attributes"; - } elsif ($format eq "ext2") { - warning "genext2fs does not support extended attributes"; - } else { - push @taropts, '--xattrs'; - } - - # disable signals so that we can fork and change behaviour of the signal - # handler in the parent and child without getting interrupted - my $sigset = POSIX::SigSet->new(SIGINT, SIGHUP, SIGPIPE, SIGTERM); - POSIX::sigprocmask(SIG_BLOCK, $sigset) or error "Can't block signals: $!"; - - my $pid; - - # a pipe to transfer the final tarball from the child to the parent - pipe my $rfh, my $wfh; - - # instead of two pipe calls, creating four file handles, we use socketpair - socketpair my $childsock, my $parentsock, AF_UNIX, SOCK_STREAM, PF_UNSPEC - or error "socketpair failed: $!"; - $options->{hooksock} = $childsock; - if ($options->{mode} eq 'unshare') { - $pid = get_unshare_cmd( - sub { - # child - local $SIG{'INT'} = 'DEFAULT'; - local $SIG{'HUP'} = 'DEFAULT'; - local $SIG{'PIPE'} = 'DEFAULT'; - local $SIG{'TERM'} = 'DEFAULT'; - - # unblock all delayed signals (and possibly handle them) - POSIX::sigprocmask(SIG_UNBLOCK, $sigset) - or error "Can't unblock signals: $!"; - - close $rfh; - close $parentsock; - open(STDOUT, '>&', STDERR) or error "cannot open STDOUT: $!"; - - setup($options); - - if (!$options->{dryrun} && $format eq 'ext2') { - my $numblocks = approx_disk_usage($options->{root}); - debug "sending nblks"; - print $childsock ( - pack("n", length "$numblocks") . "nblks$numblocks"); - $childsock->flush(); - debug "waiting for okthx"; - checkokthx $childsock; - } - - print $childsock (pack('n', 0) . 'adios'); - $childsock->flush(); - - close $childsock; - - if ($options->{dryrun}) { - info "simulate creating tarball..."; - } elsif (any { $_ eq $format } ('tar', 'squashfs', 'ext2')) { - info "creating tarball..."; - - # redirect tar output to the writing end of the pipe so - # that the parent process can capture the output - open(STDOUT, '>&', $wfh) or error "cannot open STDOUT: $!"; - - # Add ./dev as the first entries of the tar file. - # We cannot add them after calling tar, because there is no - # way to prevent tar from writing NULL entries at the end. - if (any { $_ eq 'output/dev' } @{ $options->{skip} }) { - info "skipping output/dev as requested"; - } else { - print $devtar; - } - - # pack everything except ./dev - 0 == system('tar', @taropts, '-C', $options->{root}, '.') - or error "tar failed: $?"; - - info "done"; - } elsif ($format eq 'directory') { - # nothing to do - } else { - error "unknown format: $format"; - } - - exit 0; - }, - \@idmap - ); - } elsif ( - any { $_ eq $options->{mode} } - ('root', 'fakechroot', 'proot', 'chrootless') - ) { - $pid = fork() // error "fork() failed: $!"; - if ($pid == 0) { - local $SIG{'INT'} = 'DEFAULT'; - local $SIG{'HUP'} = 'DEFAULT'; - local $SIG{'PIPE'} = 'DEFAULT'; - local $SIG{'TERM'} = 'DEFAULT'; - - # unblock all delayed signals (and possibly handle them) - POSIX::sigprocmask(SIG_UNBLOCK, $sigset) - or error "Can't unblock signals: $!"; - - close $rfh; - close $parentsock; - open(STDOUT, '>&', STDERR) or error "cannot open STDOUT: $!"; - - setup($options); - - if (!$options->{dryrun} && $format eq 'ext2') { - my $numblocks = approx_disk_usage($options->{root}); - print $childsock ( - pack("n", length "$numblocks") . "nblks$numblocks"); - $childsock->flush(); - debug "waiting for okthx"; - checkokthx $childsock; - } - - print $childsock (pack('n', 0) . 'adios'); - $childsock->flush(); - - close $childsock; - - if ($options->{dryrun}) { - info "simulate creating tarball..."; - } elsif (any { $_ eq $format } ('tar', 'squashfs', 'ext2')) { - info "creating tarball..."; - - # redirect tar output to the writing end of the pipe so that - # the parent process can capture the output - open(STDOUT, '>&', $wfh) or error "cannot open STDOUT: $!"; - - # Add ./dev as the first entries of the tar file. - # We cannot add them after calling tar, because there is no way - # to prevent tar from writing NULL entries at the end. - if (any { $_ eq 'output/dev' } @{ $options->{skip} }) { - info "skipping output/dev as requested"; - } else { - print $devtar; - } - - if ($options->{mode} eq 'fakechroot') { - # Fakechroot requires tar to run inside the chroot or - # otherwise absolute symlinks will include the path to the - # root directory - 0 == system('/usr/sbin/chroot', $options->{root}, 'tar', - @taropts, '-C', '/', '.') - or error "tar failed: $?"; - } elsif ($options->{mode} eq 'proot') { - # proot requires tar to run inside proot or otherwise - # permissions will be completely off - my @qemuopt = (); - if (defined $options->{qemu}) { - push @qemuopt, "--qemu=qemu-$options->{qemu}"; - push @taropts, "--exclude=./host-rootfs"; - } - 0 == system('proot', '--root-id', - "--rootfs=$options->{root}", '--cwd=/', @qemuopt, - 'tar', @taropts, '-C', '/', '.') - or error "tar failed: $?"; - } elsif ( - any { $_ eq $options->{mode} } - ('root', 'chrootless') - ) { - # If the chroot directory is not owned by the root user, - # then we assume that no measure was taken to fake root - # permissions. Since the final tarball should contain - # entries with root ownership, we instruct tar to do so. - my @owneropts = (); - if ((stat $options->{root})[4] != 0) { - push @owneropts, '--owner=0', '--group=0', - '--numeric-owner'; - } - 0 == system('tar', @taropts, @owneropts, '-C', - $options->{root}, '.') - or error "tar failed: $?"; - } else { - error "unknown mode: $options->{mode}"; - } - - info "done"; - } elsif ($format eq 'directory') { - # nothing to do - } else { - error "unknown format: $format"; - } - - exit 0; + sprintf('%07o', 0), # gid + sprintf('%011o', 0), # size + sprintf('%011o', $mtime), + '', # checksum + $type, + $linkname, + "ustar ", + '', # username + '', # groupname + defined($devmajor) ? sprintf('%07o', $devmajor) : '', + defined($devminor) ? sprintf('%07o', $devminor) : '', + '', # prefix + ); + # compute and insert checksum + substr($entry, 148, 7) + = sprintf("%06o\0", unpack("%16C*", $entry)); + $devtar .= $entry; } + } elsif ($format eq 'directory') { + # nothing to do } else { - error "unknown mode: $options->{mode}"; + error "unknown format: $format"; } - # parent - - my $got_signal = 0; - my $waiting_for = "setup"; - my $ignore = sub { - $got_signal = shift; - info "main() received signal $got_signal: waiting for $waiting_for..."; - }; - - local $SIG{'INT'} = $ignore; - local $SIG{'HUP'} = $ignore; - local $SIG{'PIPE'} = $ignore; - local $SIG{'TERM'} = $ignore; - - # unblock all delayed signals (and possibly handle them) - POSIX::sigprocmask(SIG_UNBLOCK, $sigset) - or error "Can't unblock signals: $!"; - - close $wfh; - close $childsock; - - debug "starting to listen for hooks"; - # handle special hook commands via parentsock - # we use eval() so that error() doesn't take this process down and - # thus leaves the setup() process without a parent - eval { - while (1) { - # get the next message - my $msg = "error"; - my $len = -1; - { - debug "reading from parentsock"; - my $ret = read($parentsock, my $buf, 2 + 5) - // error "cannot read from socket: $!"; - debug "finished reading from parentsock"; - if ($ret == 0) { - error "received eof on socket"; - } - ($len, $msg) = unpack("nA5", $buf); - } - if ($msg eq "adios") { - # setup finished, so we break out of the loop - if ($len != 0) { - error "expected no payload but got $len bytes"; - } - last; - } elsif ($msg eq "openr") { - # handle the openr message - debug "received message: openr"; - my $infile; - { - my $ret = read($parentsock, $infile, $len) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # make sure that the requested path exists outside the chroot - if (!-e $infile) { - error "$infile does not exist"; - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); + my $numblocks = 0; + my $exitstatus = 0; + my @taropts = ( + '--sort=name', + "--mtime=\@$mtime", + '--clamp-mtime', + '--numeric-owner', + '--one-file-system', + '--format=pax', + '--pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime', + '-c', + '--exclude=./dev' + ); + # tar2sqfs and genext2fs do not support extended attributes + if ($format eq "squashfs") { + warning "tar2sqfs does not support extended attributes"; + } elsif ($format eq "ext2") { + warning "genext2fs does not support extended attributes"; + } else { + push @taropts, '--xattrs'; + } - open my $fh, '<', $infile - or error "failed to open $infile for reading: $!"; + # disable signals so that we can fork and change behaviour of the signal + # handler in the parent and child without getting interrupted + my $sigset = POSIX::SigSet->new(SIGINT, SIGHUP, SIGPIPE, SIGTERM); + POSIX::sigprocmask(SIG_BLOCK, $sigset) or error "Can't block signals: $!"; - # read from the file and send as payload to the child process - while (1) { - # read from file - my $ret = read($fh, my $cont, 4096) - // error "cannot read from pipe: $!"; - if ($ret == 0) { last; } - debug "sending write"; - # send to child - print $parentsock pack("n", $ret) . "write" . $cont; - $parentsock->flush(); - debug "waiting for okthx"; - checkokthx $parentsock; - if ($ret < 4096) { last; } - } + my $pid; - # signal to the child process that we are done - debug "sending close"; - print $parentsock pack("n", 0) . "close"; - $parentsock->flush(); - debug "waiting for okthx"; - checkokthx $parentsock; + # a pipe to transfer the final tarball from the child to the parent + pipe my $rfh, my $wfh; - close $fh; - } elsif ($msg eq "openw") { - debug "received message: openw"; - # payload is the output directory - my $outfile; - { - my $ret = read($parentsock, $outfile, $len) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # make sure that the directory exists - my $outdir = dirname($outfile); - if (-e $outdir) { - if (!-d $outdir) { - error "$outdir already exists but is not a directory"; - } - } else { - my $num_created = make_path $outdir, { error => \my $err }; - if ($err && @$err) { - error( - join "; ", - ( - map { "cannot create " . (join ": ", %{$_}) } - @$err - )); - } elsif ($num_created == 0) { - error "cannot create $outdir"; - } - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); + # instead of two pipe calls, creating four file handles, we use socketpair + socketpair my $childsock, my $parentsock, AF_UNIX, SOCK_STREAM, PF_UNSPEC + or error "socketpair failed: $!"; + $options->{hooksock} = $childsock; + if ($options->{mode} eq 'unshare') { + $pid = get_unshare_cmd( + sub { + # child + local $SIG{'INT'} = 'DEFAULT'; + local $SIG{'HUP'} = 'DEFAULT'; + local $SIG{'PIPE'} = 'DEFAULT'; + local $SIG{'TERM'} = 'DEFAULT'; - # now we expect one or more "write" messages containing the - # tarball to write - open my $fh, '>', $outfile - or error "failed to open $outfile for writing: $!"; + # unblock all delayed signals (and possibly handle them) + POSIX::sigprocmask(SIG_UNBLOCK, $sigset) + or error "Can't unblock signals: $!"; - # handle "write" messages from the child process and feed - # their payload into the file handle until a "close" message - # is encountered - while (1) { - # receive the next message - my $ret = read($parentsock, my $buf, 2 + 5) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - my ($len, $msg) = unpack("nA5", $buf); - debug "received message: $msg"; - if ($msg eq "close") { - # finish the loop - if ($len != 0) { - error "expected no payload but got $len bytes"; - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); - last; - } elsif ($msg ne "write") { - # we should not receive this message at this point - error "expected write but got: $msg"; - } - # read the payload - my $content; - { - my $ret = read($parentsock, $content, $len) - // error "error cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # write the payload to the file handle - print $fh $content - or error "cannot write to file handle: $!"; - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); - } - close $fh; - } elsif (any { $_ eq $msg } ('mktar', 'mktac')) { - # handle the mktar message - debug "received message: $msg"; - my $indir; - { - my $ret = read($parentsock, $indir, $len) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # make sure that the requested path exists outside the chroot - if (!-e $indir) { - error "$indir does not exist"; - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); + close $rfh; + close $parentsock; + open(STDOUT, '>&', STDERR) or error "cannot open STDOUT: $!"; - # Open a tar process creating a tarfile of the instructed - # path. To emulate the behaviour of cp, change to the - # dirname of the requested path first. - open my $fh, '-|', 'tar', '--numeric-owner', '--xattrs', - '--format=pax', - '--pax-option=exthdr.name=%d/PaxHeaders/%f,' - . 'delete=atime,delete=ctime', - '--directory', - $msg eq 'mktar' ? dirname($indir) : $indir, - '--create', '--file', '-', - $msg eq 'mktar' ? basename($indir) : '.' - // error "failed to fork(): $!"; + setup($options); - # read from the tar process and send as payload to the child - # process - while (1) { - # read from tar - my $ret = read($fh, my $cont, 4096) - // error "cannot read from pipe: $!"; - if ($ret == 0) { last; } - debug "sending write"; - # send to child - print $parentsock pack("n", $ret) . "write" . $cont; - $parentsock->flush(); + if (!$options->{dryrun} && $format eq 'ext2') { + my $numblocks = approx_disk_usage($options->{root}); + debug "sending nblks"; + print $childsock ( + pack("n", length "$numblocks") . "nblks$numblocks"); + $childsock->flush(); debug "waiting for okthx"; - checkokthx $parentsock; - if ($ret < 4096) { last; } + checkokthx $childsock; } - # signal to the child process that we are done - debug "sending close"; - print $parentsock pack("n", 0) . "close"; - $parentsock->flush(); - debug "waiting for okthx"; - checkokthx $parentsock; + print $childsock (pack('n', 0) . 'adios'); + $childsock->flush(); - close $fh; - if ($? != 0) { - error "tar failed"; - } - } elsif ($msg eq "untar") { - debug "received message: untar"; - # payload is the output directory - my $outdir; - { - my $ret = read($parentsock, $outdir, $len) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # make sure that the directory exists - if (-e $outdir) { - if (!-d $outdir) { - error "$outdir already exists but is not a directory"; + close $childsock; + + if ($options->{dryrun}) { + info "simulate creating tarball..."; + } elsif (any { $_ eq $format } ('tar', 'squashfs', 'ext2')) { + info "creating tarball..."; + + # redirect tar output to the writing end of the pipe so + # that the parent process can capture the output + open(STDOUT, '>&', $wfh) or error "cannot open STDOUT: $!"; + + # Add ./dev as the first entries of the tar file. + # We cannot add them after calling tar, because there is no + # way to prevent tar from writing NULL entries at the end. + if (any { $_ eq 'output/dev' } @{ $options->{skip} }) { + info "skipping output/dev as requested"; + } else { + print $devtar; } + + # pack everything except ./dev + 0 == system('tar', @taropts, '-C', $options->{root}, '.') + or error "tar failed: $?"; + + info "done"; + } elsif ($format eq 'directory') { + # nothing to do } else { - my $num_created = make_path $outdir, { error => \my $err }; - if ($err && @$err) { - error( - join "; ", - ( - map { "cannot create " . (join ": ", %{$_}) } - @$err - )); - } elsif ($num_created == 0) { - error "cannot create $outdir"; - } + error "unknown format: $format"; } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); - # now we expect one or more "write" messages containing the - # tarball to unpack - open my $fh, '|-', 'tar', '--numeric-owner', '--xattrs', - '--xattrs-include=*', '--directory', $outdir, - '--extract', '--file', - '-' // error "failed to fork(): $!"; + exit 0; + }, + \@idmap + ); + } elsif ( + any { $_ eq $options->{mode} } + ('root', 'fakechroot', 'proot', 'chrootless') + ) { + $pid = fork() // error "fork() failed: $!"; + if ($pid == 0) { + local $SIG{'INT'} = 'DEFAULT'; + local $SIG{'HUP'} = 'DEFAULT'; + local $SIG{'PIPE'} = 'DEFAULT'; + local $SIG{'TERM'} = 'DEFAULT'; - # handle "write" messages from the child process and feed - # their payload into the tar process until a "close" message - # is encountered - while (1) { - # receive the next message - my $ret = read($parentsock, my $buf, 2 + 5) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - my ($len, $msg) = unpack("nA5", $buf); - debug "received message: $msg"; - if ($msg eq "close") { - # finish the loop - if ($len != 0) { - error "expected no payload but got $len bytes"; - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); - last; - } elsif ($msg ne "write") { - # we should not receive this message at this point - error "expected write but got: $msg"; - } - # read the payload - my $content; - { - my $ret = read($parentsock, $content, $len) - // error "error cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; - } - } - # write the payload to the tar process - print $fh $content - or error "cannot write to tar process: $!"; - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); - } - close $fh; - if ($? != 0) { - error "tar failed"; + # unblock all delayed signals (and possibly handle them) + POSIX::sigprocmask(SIG_UNBLOCK, $sigset) + or error "Can't unblock signals: $!"; + + close $rfh; + close $parentsock; + open(STDOUT, '>&', STDERR) or error "cannot open STDOUT: $!"; + + setup($options); + + if (!$options->{dryrun} && $format eq 'ext2') { + my $numblocks = approx_disk_usage($options->{root}); + print $childsock ( + pack("n", length "$numblocks") . "nblks$numblocks"); + $childsock->flush(); + debug "waiting for okthx"; + checkokthx $childsock; + } + + print $childsock (pack('n', 0) . 'adios'); + $childsock->flush(); + + close $childsock; + + if ($options->{dryrun}) { + info "simulate creating tarball..."; + } elsif (any { $_ eq $format } ('tar', 'squashfs', 'ext2')) { + info "creating tarball..."; + + # redirect tar output to the writing end of the pipe so that + # the parent process can capture the output + open(STDOUT, '>&', $wfh) or error "cannot open STDOUT: $!"; + + # Add ./dev as the first entries of the tar file. + # We cannot add them after calling tar, because there is no way + # to prevent tar from writing NULL entries at the end. + if (any { $_ eq 'output/dev' } @{ $options->{skip} }) { + info "skipping output/dev as requested"; + } else { + print $devtar; } - } elsif ($msg eq "nblks") { - # handle the nblks message - debug "received message: nblks"; - { - my $ret = read($parentsock, $numblocks, $len) - // error "cannot read from socket: $!"; - if ($ret == 0) { - error "received eof on socket"; + + if ($options->{mode} eq 'fakechroot') { + # Fakechroot requires tar to run inside the chroot or + # otherwise absolute symlinks will include the path to the + # root directory + 0 == system('/usr/sbin/chroot', $options->{root}, 'tar', + @taropts, '-C', '/', '.') + or error "tar failed: $?"; + } elsif ($options->{mode} eq 'proot') { + # proot requires tar to run inside proot or otherwise + # permissions will be completely off + my @qemuopt = (); + if (defined $options->{qemu}) { + push @qemuopt, "--qemu=qemu-$options->{qemu}"; + push @taropts, "--exclude=./host-rootfs"; } + 0 == system('proot', '--root-id', + "--rootfs=$options->{root}", '--cwd=/', @qemuopt, + 'tar', @taropts, '-C', '/', '.') + or error "tar failed: $?"; + } elsif ( + any { $_ eq $options->{mode} } + ('root', 'chrootless') + ) { + # If the chroot directory is not owned by the root user, + # then we assume that no measure was taken to fake root + # permissions. Since the final tarball should contain + # entries with root ownership, we instruct tar to do so. + my @owneropts = (); + if ((stat $options->{root})[4] != 0) { + push @owneropts, '--owner=0', '--group=0', + '--numeric-owner'; + } + 0 == system('tar', @taropts, @owneropts, '-C', + $options->{root}, '.') + or error "tar failed: $?"; + } else { + error "unknown mode: $options->{mode}"; } - if ($numblocks !~ /^\d+$/) { - error "invalid number of blocks: $numblocks"; - } - debug "sending okthx"; - print $parentsock (pack("n", 0) . "okthx") - or error "cannot write to socket: $!"; - $parentsock->flush(); + + info "done"; + } elsif ($format eq 'directory') { + # nothing to do } else { - error "unknown message: $msg"; + error "unknown format: $format"; } + + exit 0; } + } else { + error "unknown mode: $options->{mode}"; + } + + # parent + + my $got_signal = 0; + my $waiting_for = "setup"; + my $ignore = sub { + $got_signal = shift; + info "main() received signal $got_signal: waiting for $waiting_for..."; }; - if ($@) { - # inform the other side that something went wrong - print $parentsock (pack("n", 0) . "error") - or error "cannot write to socket: $!"; - $parentsock->flush(); + + local $SIG{'INT'} = $ignore; + local $SIG{'HUP'} = $ignore; + local $SIG{'PIPE'} = $ignore; + local $SIG{'TERM'} = $ignore; + + # unblock all delayed signals (and possibly handle them) + POSIX::sigprocmask(SIG_UNBLOCK, $sigset) + or error "Can't unblock signals: $!"; + + close $wfh; + close $childsock; + + debug "starting to listen for hooks"; + # handle special hook commands via parentsock + my $lpid = fork() // error "fork() failed: $!"; + if ($lpid == 0) { + # whatever the script writes on stdout is sent to the + # socket + # whatever is written to the socket, send to stdin + open(STDOUT, '>&', $parentsock) + or error "cannot open STDOUT: $!"; + open(STDIN, '<&', $parentsock) + or error "cannot open STDIN: $!"; + + # we execute ourselves under sh to avoid having to + # implement a clever parser of the quoting used in $script + # for the filenames + my @prefix = (); + if ($is_covering) { + @prefix = ($EXECUTABLE_NAME, "-MDevel::Cover=-silent,-nogcov"); + } + exec @prefix, $PROGRAM_NAME, "--hook-listener"; + } + waitpid($lpid, 0); + if ($? != 0) { # we cannot die here because that would leave the other thread # running without a parent warning "listening on child socket failed: $@";