From e2a759967f5f55f3112ff6357717acf946b54765 Mon Sep 17 00:00:00 2001 From: Johannes 'josch' Schauer Date: Sat, 15 Aug 2020 22:36:13 +0200 Subject: [PATCH] put hook listener into its own function and expose it to the CLI via --hook-listener --- coverage.sh | 38 +++ mmdebstrap | 688 +++++++++++++++++++++++++++------------------------- 2 files changed, 400 insertions(+), 326 deletions(-) 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 @@ -4418,334 +4761,27 @@ sub main() { 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 $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: $!"; - 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 $parentsock pack("n", $ret) . "write" . $cont; - $parentsock->flush(); - debug "waiting for okthx"; - checkokthx $parentsock; - if ($ret < 4096) { last; } - } - - # 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; - - 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(); - - # 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($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(); - - # 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 $parentsock pack("n", $ret) . "write" . $cont; - $parentsock->flush(); - debug "waiting for okthx"; - checkokthx $parentsock; - if ($ret < 4096) { last; } - } - - # 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; - - 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"; - } - } 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(); - - # 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($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"; - } - } 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 ($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(); - } else { - error "unknown message: $msg"; - } + # 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"); } - }; - if ($@) { - # inform the other side that something went wrong - print $parentsock (pack("n", 0) . "error") - or error "cannot write to socket: $!"; - $parentsock->flush(); + 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: $@";