2018-09-18 09:20:24 +00:00
|
|
|
#!/usr/bin/perl
|
|
|
|
#
|
|
|
|
# Copyright: 2018 Johannes Schauer <josch@mister-muffin.de>
|
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
# of this software and associated documentation files (the "Software"), to
|
|
|
|
# deal in the Software without restriction, including without limitation the
|
|
|
|
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
|
|
# sell copies of the Software, and to permit persons to whom the Software is
|
|
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
use English;
|
|
|
|
use Getopt::Long;
|
|
|
|
use Pod::Usage;
|
|
|
|
use File::Copy;
|
|
|
|
use File::Path qw(make_path remove_tree);
|
|
|
|
use File::Temp qw(tempfile tempdir);
|
|
|
|
use Cwd qw(abs_path);
|
|
|
|
require "syscall.ph";
|
2018-09-23 17:47:14 +00:00
|
|
|
use Fcntl qw(S_IFCHR S_IFBLK FD_CLOEXEC F_GETFD F_SETFD);
|
2018-09-23 17:36:07 +00:00
|
|
|
use List::Util qw(any none);
|
2019-01-13 09:17:46 +00:00
|
|
|
use POSIX qw(SIGINT SIGHUP SIGPIPE SIGTERM SIG_BLOCK SIG_UNBLOCK);
|
2018-09-18 09:20:24 +00:00
|
|
|
|
|
|
|
# from sched.h
|
|
|
|
use constant {
|
|
|
|
CLONE_NEWNS => 0x20000,
|
|
|
|
CLONE_NEWUTS => 0x4000000,
|
|
|
|
CLONE_NEWIPC => 0x8000000,
|
|
|
|
CLONE_NEWUSER => 0x10000000,
|
|
|
|
CLONE_NEWPID => 0x20000000,
|
|
|
|
CLONE_NEWNET => 0x40000000,
|
|
|
|
};
|
|
|
|
|
|
|
|
# type codes:
|
|
|
|
# 0 -> normal file
|
|
|
|
# 1 -> hardlink
|
|
|
|
# 2 -> symlink
|
|
|
|
# 3 -> character special
|
|
|
|
# 4 -> block special
|
|
|
|
# 5 -> directory
|
|
|
|
my @devfiles = (
|
|
|
|
# filename mode type link target major minor
|
|
|
|
[ "./dev/", 0755, 5, '', undef, undef ],
|
|
|
|
[ "./dev/console", 0666, 3, '', 5, 1 ],
|
|
|
|
[ "./dev/fd", 0777, 2, '/proc/self/fd', undef, undef ],
|
|
|
|
[ "./dev/full", 0666, 3, '', 1, 7 ],
|
|
|
|
[ "./dev/null", 0666, 3, '', 1, 3 ],
|
|
|
|
[ "./dev/ptmx", 0666, 3, '', 5, 2 ],
|
|
|
|
[ "./dev/pts/", 0755, 5, '', undef, undef ],
|
|
|
|
[ "./dev/random", 0666, 3, '', 1, 8 ],
|
|
|
|
[ "./dev/shm/", 0755, 5, '', undef, undef ],
|
|
|
|
[ "./dev/stderr", 0777, 2, '/proc/self/fd/2', undef, undef ],
|
|
|
|
[ "./dev/stdin", 0777, 2, '/proc/self/fd/0', undef, undef ],
|
|
|
|
[ "./dev/stdout", 0777, 2, '/proc/self/fd/1', undef, undef ],
|
|
|
|
[ "./dev/tty", 0666, 3, '', 5, 0 ],
|
|
|
|
[ "./dev/urandom", 0666, 3, '', 1, 9 ],
|
|
|
|
[ "./dev/zero", 0666, 3, '', 1, 5 ],
|
|
|
|
);
|
|
|
|
|
|
|
|
# tar cannot figure out the decompression program when receiving data on
|
|
|
|
# standard input, thus we do it ourselves. This is copied from tar's
|
|
|
|
# src/suffix.c
|
|
|
|
sub get_tar_compress_options($) {
|
|
|
|
my $filename = shift;
|
|
|
|
if ($filename =~ /\.(gz|tgz|taz)$/) {
|
|
|
|
return ('--gzip');
|
|
|
|
} elsif ($filename =~ /\.(Z|taZ)$/) {
|
|
|
|
return ('--compress');
|
|
|
|
} elsif ($filename =~ /\.(bz2|tbz|tbz2|tz2)$/) {
|
|
|
|
return ('--bzip2');
|
|
|
|
} elsif ($filename =~ /\.lz$/) {
|
|
|
|
return ('--lzip');
|
|
|
|
} elsif ($filename =~ /\.(lzma|tlz)$/) {
|
|
|
|
return ('--lzma');
|
|
|
|
} elsif ($filename =~ /\.lzo$/) {
|
|
|
|
return ('--lzop');
|
|
|
|
} elsif ($filename =~ /\.lz4$/) {
|
|
|
|
return ('--use-compress-program', 'lz4');
|
|
|
|
} elsif ($filename =~ /\.(xz|txz)$/) {
|
|
|
|
return ('--xz');
|
|
|
|
}
|
|
|
|
return ();
|
|
|
|
}
|
|
|
|
|
2018-12-05 07:06:26 +00:00
|
|
|
sub test_unshare($) {
|
|
|
|
my $verbose = shift;
|
2018-09-24 18:07:46 +00:00
|
|
|
if ($EFFECTIVE_USER_ID == 0) {
|
2018-12-05 07:06:26 +00:00
|
|
|
if ($verbose) {
|
|
|
|
print STDERR "E: cannot use unshare mode when executing as root\n";
|
|
|
|
}
|
2018-09-24 18:07:46 +00:00
|
|
|
return 0;
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
# arguments to syscalls have to be stored in their own variable or
|
|
|
|
# otherwise we will get "Modification of a read-only value attempted"
|
|
|
|
my $unshare_flags = CLONE_NEWUSER;
|
|
|
|
# we spawn a new per process because if unshare succeeds, we would
|
2018-10-01 15:14:59 +00:00
|
|
|
# otherwise have unshared the mmdebstrap process itself which we don't want
|
2018-09-18 09:20:24 +00:00
|
|
|
my $pid = fork() // die "fork() failed: $!";
|
|
|
|
if ($pid == 0) {
|
|
|
|
my $ret = syscall &SYS_unshare, $unshare_flags;
|
2018-12-05 07:06:26 +00:00
|
|
|
if ($ret == 0) {
|
2018-09-18 09:20:24 +00:00
|
|
|
exit 0;
|
|
|
|
} else {
|
2018-12-05 07:06:26 +00:00
|
|
|
if ($verbose) {
|
|
|
|
print STDERR "E: unshare syscall failed: $!\n";
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
waitpid($pid, 0);
|
|
|
|
if (($? >> 8) != 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
2018-09-24 18:09:08 +00:00
|
|
|
# if newuidmap and newgidmap exist, the exit status will be 1 when
|
|
|
|
# executed without parameters
|
|
|
|
system "newuidmap 2>/dev/null";
|
|
|
|
if (($? >> 8) != 1) {
|
2018-12-05 07:06:26 +00:00
|
|
|
if ($verbose) {
|
|
|
|
if (($? >> 8) == 127) {
|
|
|
|
print STDERR "E: cannot find newuidmap\n";
|
|
|
|
} else {
|
|
|
|
print STDERR "E: newuidmap returned unknown exit status\n";
|
|
|
|
}
|
|
|
|
}
|
2018-09-24 18:09:08 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
system "newgidmap 2>/dev/null";
|
|
|
|
if (($? >> 8) != 1) {
|
2018-12-05 07:06:26 +00:00
|
|
|
if ($verbose) {
|
|
|
|
if (($? >> 8) == 127) {
|
|
|
|
print STDERR "E: cannot find newgidmap\n";
|
|
|
|
} else {
|
|
|
|
print STDERR "E: newgidmap returned unknown exit status\n";
|
|
|
|
}
|
|
|
|
}
|
2018-09-24 18:09:08 +00:00
|
|
|
return 0;
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub read_subuid_subgid() {
|
|
|
|
my $username = getpwuid $<;
|
|
|
|
my ($subid, $num_subid, $fh, $n);
|
|
|
|
my @result = ();
|
|
|
|
|
|
|
|
if (! -e "/etc/subuid") {
|
|
|
|
printf STDERR "/etc/subuid doesn't exist\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (! -r "/etc/subuid") {
|
|
|
|
printf STDERR "/etc/subuid is not readable\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
open $fh, "<", "/etc/subuid" or die "cannot open /etc/subuid for reading: $!";
|
|
|
|
while (my $line = <$fh>) {
|
|
|
|
($n, $subid, $num_subid) = split(/:/, $line, 3);
|
|
|
|
last if ($n eq $username);
|
|
|
|
}
|
|
|
|
close $fh;
|
|
|
|
push @result, ["u", 0, $subid, $num_subid];
|
|
|
|
|
|
|
|
if (scalar(@result) < 1) {
|
|
|
|
printf STDERR "/etc/subuid does not contain an entry for $username\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (scalar(@result) > 1) {
|
|
|
|
printf STDERR "/etc/subuid contains multiple entries for $username\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
open $fh, "<", "/etc/subgid" or die "cannot open /etc/subgid for reading: $!";
|
|
|
|
while (my $line = <$fh>) {
|
|
|
|
($n, $subid, $num_subid) = split(/:/, $line, 3);
|
|
|
|
last if ($n eq $username);
|
|
|
|
}
|
|
|
|
close $fh;
|
|
|
|
push @result, ["g", 0, $subid, $num_subid];
|
|
|
|
|
|
|
|
if (scalar(@result) < 2) {
|
|
|
|
printf STDERR "/etc/subgid does not contain an entry for $username\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (scalar(@result) > 2) {
|
|
|
|
printf STDERR "/etc/subgid contains multiple entries for $username\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return @result;
|
|
|
|
}
|
|
|
|
|
|
|
|
# This function spawns two child processes forming the following process tree
|
|
|
|
#
|
|
|
|
# A
|
|
|
|
# |
|
|
|
|
# fork()
|
|
|
|
# | \
|
|
|
|
# B C
|
|
|
|
# | |
|
|
|
|
# | fork()
|
|
|
|
# | | \
|
|
|
|
# | D E
|
|
|
|
# | | |
|
|
|
|
# |unshare()
|
|
|
|
# | close()
|
|
|
|
# | | |
|
|
|
|
# | | read()
|
|
|
|
# | | newuidmap(D)
|
|
|
|
# | | newgidmap(D)
|
|
|
|
# | | /
|
|
|
|
# | waitpid()
|
|
|
|
# | |
|
|
|
|
# | fork()
|
|
|
|
# | | \
|
|
|
|
# | F G
|
|
|
|
# | | |
|
|
|
|
# | | exec()
|
|
|
|
# | | /
|
|
|
|
# | waitpid()
|
|
|
|
# | /
|
|
|
|
# waitpid()
|
|
|
|
#
|
|
|
|
# To better refer to each individual part, we give each process a new
|
|
|
|
# identifier after calling fork(). Process A is the main process. After
|
|
|
|
# executing fork() we call the parent and child B and C, respectively. This
|
|
|
|
# first fork() is done because we do not want to modify A. B then remains
|
|
|
|
# waiting for its child C to finish. C calls fork() again, splitting into
|
|
|
|
# the parent D and its child E. In the parent D we call unshare() and close a
|
|
|
|
# pipe shared by D and E to signal to E that D is done with calling unshare().
|
|
|
|
# E notices this by using read() and follows up with executing the tools
|
|
|
|
# new[ug]idmap on D. E finishes and D continues with doing another fork().
|
|
|
|
# This is because when unsharing the PID namespace, we need a PID 1 to be kept
|
|
|
|
# alive or otherwise any child processes cannot fork() anymore themselves. So
|
|
|
|
# we keep F as PID 1 and finally call exec() in G.
|
|
|
|
sub get_unshare_cmd(&$) {
|
|
|
|
my $cmd = shift;
|
|
|
|
my $idmap = shift;
|
|
|
|
|
|
|
|
my $unshare_flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUTS | CLONE_NEWIPC;
|
|
|
|
|
|
|
|
if (0) {
|
|
|
|
$unshare_flags |= CLONE_NEWNET;
|
|
|
|
}
|
|
|
|
|
|
|
|
# fork a new process and let the child get unshare()ed
|
|
|
|
# we don't want to unshare the parent process
|
|
|
|
my $gcpid = fork() // die "fork() failed: $!";
|
|
|
|
if ($gcpid == 0) {
|
|
|
|
# Create a pipe for the parent process to signal the child process that it is
|
|
|
|
# done with calling unshare() so that the child can go ahead setting up
|
|
|
|
# uid_map and gid_map.
|
|
|
|
pipe my $rfh, my $wfh;
|
|
|
|
|
|
|
|
# We have to do this dance with forking a process and then modifying the
|
|
|
|
# parent from the child because:
|
|
|
|
# - new[ug]idmap can only be called on a process id after that process has
|
|
|
|
# unshared the user namespace
|
|
|
|
# - a process looses its capabilities if it performs an execve() with nonzero
|
|
|
|
# user ids see the capabilities(7) man page for details.
|
|
|
|
# - a process that unshared the user namespace by default does not have the
|
|
|
|
# privileges to call new[ug]idmap on itself
|
|
|
|
#
|
|
|
|
# this also works the other way around (the child setting up a user namespace
|
|
|
|
# and being modified from the parent) but that way, the parent would have to
|
|
|
|
# stay around until the child exited (so a pid would be wasted). Additionally,
|
|
|
|
# that variant would require an additional pipe to let the parent signal the
|
|
|
|
# child that it is done with calling new[ug]idmap. The way it is done here,
|
|
|
|
# this signaling can instead be done by wait()-ing for the exit of the child.
|
|
|
|
my $ppid = $$;
|
|
|
|
my $cpid = fork() // die "fork() failed: $!";
|
|
|
|
if ($cpid == 0) {
|
|
|
|
# child
|
|
|
|
|
|
|
|
# Close the writing descriptor at our end of the pipe so that we
|
|
|
|
# see EOF when parent closes its descriptor.
|
|
|
|
close $wfh;
|
|
|
|
|
|
|
|
# Wait for the parent process to finish its unshare() call by
|
|
|
|
# waiting for an EOF.
|
|
|
|
0 == sysread $rfh, my $c, 1 or die "read() did not receive EOF";
|
|
|
|
|
|
|
|
# The program's new[ug]idmap have to be used because they are
|
|
|
|
# setuid root. These privileges are needed to map the ids from
|
|
|
|
# /etc/sub[ug]id to the user namespace set up by the parent.
|
|
|
|
# Without these privileges, only the id of the user itself can be
|
|
|
|
# mapped into the new namespace.
|
|
|
|
#
|
|
|
|
# Since new[ug]idmap is setuid root we also don't need to write
|
|
|
|
# "deny" to /proc/$$/setgroups beforehand (this is otherwise
|
|
|
|
# required for unprivileged processes trying to write to
|
|
|
|
# /proc/$$/gid_map since kernel version 3.19 for security reasons)
|
|
|
|
# and therefore the parent process keeps its ability to change its
|
|
|
|
# own group here.
|
|
|
|
#
|
|
|
|
# Since /proc/$ppid/[ug]id_map can only be written to once,
|
|
|
|
# respectively, instead of making multiple calls to new[ug]idmap,
|
|
|
|
# we assemble a command line that makes one call each.
|
|
|
|
my $uidmapcmd = "";
|
|
|
|
my $gidmapcmd = "";
|
|
|
|
foreach (@{$idmap}) {
|
|
|
|
my ($t, $hostid, $nsid, $range) = @{$_};
|
|
|
|
if ($t ne "u" and $t ne "g" and $t ne "b") {
|
|
|
|
die "invalid idmap type: $t";
|
|
|
|
}
|
|
|
|
if ($t eq "u" or $t eq "b") {
|
|
|
|
$uidmapcmd .= " $hostid $nsid $range";
|
|
|
|
}
|
|
|
|
if ($t eq "g" or $t eq "b") {
|
|
|
|
$gidmapcmd .= " $hostid $nsid $range";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
my $idmapcmd = '';
|
|
|
|
if ($uidmapcmd ne "") {
|
2018-09-24 18:09:28 +00:00
|
|
|
0 == system "newuidmap $ppid $uidmapcmd" or die "newuidmap $ppid $uidmapcmd failed: $!";
|
2018-09-18 09:20:24 +00:00
|
|
|
}
|
|
|
|
if ($gidmapcmd ne "") {
|
2018-09-24 18:09:28 +00:00
|
|
|
0 == system "newgidmap $ppid $gidmapcmd" or die "newgidmap $ppid $gidmapcmd failed: $!";
|
2018-09-18 09:20:24 +00:00
|
|
|
}
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# parent
|
|
|
|
|
|
|
|
# After fork()-ing, the parent immediately calls unshare...
|
|
|
|
0 == syscall &SYS_unshare, $unshare_flags or die "unshare() failed: $!";
|
|
|
|
|
|
|
|
# .. and then signals the child process that we are done with the
|
|
|
|
# unshare() call by sending an EOF.
|
|
|
|
close $wfh;
|
|
|
|
|
|
|
|
# Wait for the child process to finish its setup by waiting for its
|
|
|
|
# exit.
|
|
|
|
$cpid == waitpid $cpid, 0 or die "waitpid() failed: $!";
|
|
|
|
my $exit = $? >> 8;
|
|
|
|
if ($exit != 0) {
|
|
|
|
die "child had a non-zero exit status: $exit";
|
|
|
|
}
|
|
|
|
|
|
|
|
# Currently we are nobody (uid and gid are 65534). So we become root
|
|
|
|
# user and group instead.
|
|
|
|
#
|
|
|
|
# We are using direct syscalls instead of setting $(, $), $< and $>
|
|
|
|
# because then perl would do additional stuff which we don't need or
|
|
|
|
# want here, like checking /proc/sys/kernel/ngroups_max (which might
|
|
|
|
# not exist). It would also also call setgroups() in a way that makes
|
|
|
|
# the root user be part of the group unknown.
|
|
|
|
0 == syscall &SYS_setgid, 0 or die "setgid failed: $!";
|
|
|
|
0 == syscall &SYS_setuid, 0 or die "setuid failed: $!";
|
|
|
|
0 == syscall &SYS_setgroups, 0, 0 or die "setgroups failed: $!";
|
|
|
|
|
|
|
|
if (1) {
|
|
|
|
# When the pid namespace is also unshared, then processes expect a
|
|
|
|
# master pid to always be alive within the namespace. To achieve
|
|
|
|
# this, we fork() here instead of exec() to always have one dummy
|
|
|
|
# process running as pid 1 inside the namespace. This is also what
|
|
|
|
# the unshare tool does when used with the --fork option.
|
|
|
|
#
|
|
|
|
# Otherwise, without a pid 1, new processes cannot be forked
|
|
|
|
# anymore after pid 1 finished.
|
|
|
|
my $cpid = fork() // die "fork() failed: $!";
|
|
|
|
if ($cpid != 0) {
|
|
|
|
# The parent process will stay alive as pid 1 in this
|
|
|
|
# namespace until the child finishes executing. This is
|
|
|
|
# important because pid 1 must never die or otherwise nothing
|
|
|
|
# new can be forked.
|
|
|
|
$cpid == waitpid $cpid, 0 or die "waitpid() failed: $!";
|
|
|
|
exit ($? >> 8);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&{$cmd}();
|
|
|
|
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# parent
|
|
|
|
return $gcpid;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub havemknod($) {
|
|
|
|
my $root = shift;
|
|
|
|
my $havemknod = 0;
|
|
|
|
if (-e "$root/test-dev-null") {
|
|
|
|
die "/test-dev-null already exists";
|
|
|
|
}
|
2018-10-03 05:29:50 +00:00
|
|
|
TEST: {
|
2018-09-23 13:26:47 +00:00
|
|
|
# we fork so that we can read STDERR
|
|
|
|
my $pid = open my $fh, '-|' // die "failed to fork(): $!";
|
|
|
|
if ($pid == 0) {
|
|
|
|
open(STDERR, '>&', STDOUT);
|
|
|
|
# we use mknod(1) instead of the system call because creating the
|
|
|
|
# right dev_t argument requires makedev(3)
|
|
|
|
exec 'mknod', "$root/test-dev-null", 'c', '1', '3';
|
|
|
|
}
|
|
|
|
chomp (my $content = do { local $/; <$fh> });
|
|
|
|
close $fh;
|
|
|
|
{
|
2018-10-03 05:29:50 +00:00
|
|
|
last TEST unless $? == 0 and $content eq '';
|
|
|
|
last TEST unless -c "$root/test-dev-null";
|
|
|
|
last TEST unless open my $fh, '>', "$root/test-dev-null";
|
|
|
|
last TEST unless print $fh 'test';
|
2018-09-23 13:26:47 +00:00
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
$havemknod = 1;
|
|
|
|
}
|
|
|
|
if (-e "$root/test-dev-null") {
|
|
|
|
unlink "$root/test-dev-null" or die "cannot unlink /test-dev-null";
|
|
|
|
}
|
|
|
|
return $havemknod;
|
|
|
|
}
|
|
|
|
|
2018-09-23 17:47:14 +00:00
|
|
|
sub print_progress {
|
|
|
|
my $perc = shift;
|
|
|
|
if (!-t STDERR) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ($perc eq "done") {
|
|
|
|
# \e[2K clears everything on the current line (i.e. the progress bar)
|
|
|
|
print STDERR "\e[2Kdone\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ($perc >= 100) {
|
|
|
|
$perc = 100;
|
|
|
|
}
|
|
|
|
my $width = 50;
|
|
|
|
my $num_x = int($perc*$width/100);
|
|
|
|
my $bar = '=' x $num_x;
|
|
|
|
if ($num_x != $width) {
|
|
|
|
$bar .= '>';
|
|
|
|
$bar .= ' ' x ($width - $num_x - 1);
|
|
|
|
}
|
|
|
|
printf STDERR "%6.2f [%s]\r", $perc, $bar;
|
|
|
|
}
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
sub run_progress {
|
|
|
|
my ($get_exec, $line_handler, $line_has_error, $verbose) = @_;
|
2018-09-23 17:47:14 +00:00
|
|
|
pipe my $rfh, my $wfh;
|
2019-01-13 09:17:46 +00:00
|
|
|
my $got_signal = 0;
|
|
|
|
my $ignore = sub {
|
2019-01-13 21:04:25 +00:00
|
|
|
print STDERR "I: run_progress() received signal $_[0]: waiting for child...\n";
|
2019-01-13 09:17:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
# delay signals so that we can fork and change behaviour of the signal
|
|
|
|
# handler in parent and child without getting interrupted
|
|
|
|
my $sigset = POSIX::SigSet->new(SIGINT, SIGHUP, SIGPIPE, SIGTERM);
|
|
|
|
POSIX::sigprocmask(SIG_BLOCK, $sigset) or die "Can't block signals: $!\n";
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
my $pid1 = open(my $pipe, '-|') // die "failed to fork(): $!";
|
2019-01-13 09:17:46 +00:00
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
if ($pid1 == 0) {
|
2019-01-13 09:17:46 +00:00
|
|
|
# child: default signal handlers
|
|
|
|
$SIG{'INT'} = 'DEFAULT';
|
|
|
|
$SIG{'HUP'} = 'DEFAULT';
|
|
|
|
$SIG{'PIPE'} = 'DEFAULT';
|
|
|
|
$SIG{'TERM'} = 'DEFAULT';
|
|
|
|
|
|
|
|
# unblock all delayed signals (and possibly handle them)
|
|
|
|
POSIX::sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock signals: $!\n";
|
|
|
|
|
2018-09-23 17:47:14 +00:00
|
|
|
close $rfh;
|
|
|
|
# Unset the close-on-exec flag, so that the file descriptor does not
|
2019-01-13 21:04:25 +00:00
|
|
|
# get closed when we exec
|
2018-09-23 17:47:14 +00:00
|
|
|
my $flags = fcntl( $wfh, F_GETFD, 0 ) or die "fcntl F_GETFD: $!";
|
|
|
|
fcntl($wfh, F_SETFD, $flags & ~FD_CLOEXEC ) or die "fcntl F_SETFD: $!";
|
|
|
|
my $fd = fileno $wfh;
|
2019-01-13 21:04:25 +00:00
|
|
|
# redirect stderr to stdout so that we can capture it
|
2018-09-23 17:47:14 +00:00
|
|
|
open(STDERR, '>&', STDOUT);
|
2019-01-13 21:04:25 +00:00
|
|
|
my @execargs = $get_exec->($fd);
|
|
|
|
exec { $execargs[0] } @execargs;
|
2019-01-11 23:48:40 +00:00
|
|
|
die 'cannot exec() ' . (join ' ', @execargs);
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
|
|
|
close $wfh;
|
|
|
|
|
|
|
|
# spawn two processes:
|
2019-01-13 21:04:25 +00:00
|
|
|
# parent will parse stdout to look for errors
|
2018-09-23 17:47:14 +00:00
|
|
|
# child will parse $rfh for the progress meter
|
2019-01-13 21:04:25 +00:00
|
|
|
my $pid2 = fork() // die "failed to fork(): $!";
|
|
|
|
if ($pid2 == 0) {
|
2019-01-13 09:17:46 +00:00
|
|
|
# child: default signal handlers
|
|
|
|
$SIG{'INT'} = 'IGNORE';
|
|
|
|
$SIG{'HUP'} = 'IGNORE';
|
|
|
|
$SIG{'PIPE'} = 'IGNORE';
|
|
|
|
$SIG{'TERM'} = 'IGNORE';
|
|
|
|
|
|
|
|
# unblock all delayed signals (and possibly handle them)
|
|
|
|
POSIX::sigprocmask(SIG_UNBLOCK, $sigset) or die "Can't unblock signals: $!\n";
|
|
|
|
|
2018-12-27 20:08:53 +00:00
|
|
|
print_progress 0.0 if not $verbose;
|
2018-09-23 17:47:14 +00:00
|
|
|
while (my $line = <$rfh>) {
|
2019-01-13 21:04:25 +00:00
|
|
|
next if $verbose;
|
|
|
|
my $output = $line_handler->($line);
|
|
|
|
next unless $output;
|
|
|
|
print_progress $output;
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
2018-12-27 20:08:53 +00:00
|
|
|
print_progress "done" if not $verbose;
|
2018-09-23 17:47:14 +00:00
|
|
|
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
2019-01-13 09:17:46 +00:00
|
|
|
# parent: ignore signals
|
|
|
|
# by using "local", the original is automatically restored once the
|
|
|
|
# function returns
|
|
|
|
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 die "Can't unblock signals: $!\n";
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
my $output = '';
|
|
|
|
my $has_error = 0;
|
|
|
|
while (my $line = <$pipe>) {
|
|
|
|
$has_error = $line_has_error->($line);
|
2018-12-27 20:08:53 +00:00
|
|
|
if ($verbose) {
|
|
|
|
print STDERR $line;
|
|
|
|
} else {
|
|
|
|
# forward captured apt output
|
2019-01-13 21:04:25 +00:00
|
|
|
$output .= $line;
|
2018-12-27 20:08:53 +00:00
|
|
|
}
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
2019-01-13 21:04:25 +00:00
|
|
|
|
|
|
|
close($pipe);
|
2018-09-23 17:47:14 +00:00
|
|
|
my $fail = 0;
|
2019-01-13 21:04:25 +00:00
|
|
|
if ($? != 0 or $has_error) {
|
2018-09-23 17:47:14 +00:00
|
|
|
$fail = 1;
|
|
|
|
}
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
waitpid $pid2, 0;
|
2018-09-23 17:47:14 +00:00
|
|
|
$? == 0 or die "progress parsing failed";
|
|
|
|
|
2019-01-13 09:17:46 +00:00
|
|
|
if ($got_signal) {
|
2019-01-13 21:04:25 +00:00
|
|
|
die "run_progress() received signal: $got_signal";
|
2019-01-13 09:17:46 +00:00
|
|
|
}
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
# only print failure after progress output finished or otherwise it
|
|
|
|
# might interfere with the remaining output
|
2018-09-23 17:47:14 +00:00
|
|
|
if ($fail) {
|
2019-01-13 21:04:25 +00:00
|
|
|
print STDERR $output;
|
|
|
|
die ((join ' ', $get_exec->('<$fd>')) . ' failed');
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
sub run_dpkg_progress {
|
2018-12-27 20:08:53 +00:00
|
|
|
my $options = shift;
|
|
|
|
my @debs = @{$options->{PKGS} // []};
|
|
|
|
my $verbose = $options->{VERBOSE} // 0;
|
2019-01-13 21:04:25 +00:00
|
|
|
my $get_exec = sub { return @{$options->{ARGV}}, "--status-fd=$_[0]", @debs; };
|
|
|
|
my $line_has_error = sub { return 0; };
|
|
|
|
my $num = 0;
|
|
|
|
# each package has one install and one configure step, thus the total
|
|
|
|
# number is twice the number of packages
|
|
|
|
my $total = (scalar @debs) * 2;
|
|
|
|
my $line_handler = sub {
|
|
|
|
if ($_[0] =~ /^processing: (install|configure): /) {
|
|
|
|
$num += 1;
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
2019-01-13 21:04:25 +00:00
|
|
|
return $num/$total*100;
|
|
|
|
};
|
|
|
|
run_progress $get_exec, $line_handler, $line_has_error, $verbose;
|
|
|
|
}
|
2019-01-13 09:17:46 +00:00
|
|
|
|
2019-01-13 21:04:25 +00:00
|
|
|
sub run_apt_progress {
|
|
|
|
my $options = shift;
|
|
|
|
my @debs = @{$options->{PKGS} // []};
|
|
|
|
my $verbose = $options->{VERBOSE} // 0;
|
|
|
|
my $get_exec = sub {
|
2019-01-14 21:23:02 +00:00
|
|
|
return (
|
|
|
|
@{$options->{ARGV}},
|
|
|
|
"-oAPT::Status-Fd=$_[0]",
|
|
|
|
# prevent apt from messing up the terminal and allow dpkg to
|
|
|
|
# receive SIGINT and quit immediately without waiting for
|
|
|
|
# maintainer script to finish
|
|
|
|
'-oDpkg::Use-Pty=false',
|
|
|
|
@debs
|
|
|
|
)};
|
2019-01-13 21:04:25 +00:00
|
|
|
my $line_has_error = sub {
|
|
|
|
# apt-get doesn't report a non-zero exit if the update failed. Thus, we
|
|
|
|
# have to parse its output. See #778357, #776152, #696335 and #745735
|
|
|
|
if ($_[0] =~ /^(W: |Err:)/) {
|
|
|
|
return 1;
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
2019-01-13 21:04:25 +00:00
|
|
|
return 0;
|
|
|
|
};
|
|
|
|
my $line_handler = sub {
|
|
|
|
if ($_[0] =~ /(pmstatus|dlstatus):[^:]+:(\d+\.\d+):.*/) {
|
|
|
|
return $2;
|
2018-12-27 20:08:53 +00:00
|
|
|
}
|
2019-01-13 21:04:25 +00:00
|
|
|
};
|
|
|
|
run_progress $get_exec, $line_handler, $line_has_error, $verbose;
|
2018-09-23 17:47:14 +00:00
|
|
|
}
|
|
|
|
|
2019-01-13 09:17:46 +00:00
|
|
|
sub run_chroot(&$) {
|
|
|
|
my $cmd = shift;
|
|
|
|
my $options = shift;
|
|
|
|
|
|
|
|
my @cleanup_tasks = ();
|
|
|
|
|
|
|
|
my $cleanup = sub {
|
|
|
|
my $signal = $_[0];
|
|
|
|
while (my $task = pop @cleanup_tasks) {
|
|
|
|
$task->();
|
|
|
|
}
|
|
|
|
if ($signal) {
|
|
|
|
print STDERR "W: pid $PID cought signal: $signal\n";
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
local $SIG{INT} = $cleanup;
|
|
|
|
local $SIG{HUP} = $cleanup;
|
|
|
|
local $SIG{PIPE} = $cleanup;
|
|
|
|
local $SIG{TERM} = $cleanup;
|
|
|
|
|
|
|
|
eval {
|
|
|
|
if (any { $_ eq $options->{mode} } ('root', 'unshare')) {
|
|
|
|
# if more than essential should be installed, make the system look
|
|
|
|
# more like a real one by creating or bind-mounting the device nodes
|
|
|
|
foreach my $file (@devfiles) {
|
|
|
|
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
|
|
|
|
next if $fname eq './dev/';
|
|
|
|
if ($type == 0) { # normal file
|
|
|
|
die "type 0 not implemented";
|
|
|
|
} elsif ($type == 1) { # hardlink
|
|
|
|
die "type 1 not implemented";
|
|
|
|
} elsif ($type == 2) { # symlink
|
|
|
|
if (!$options->{havemknod}) {
|
|
|
|
if ($options->{mode} eq 'fakechroot' and $linkname =~ /^\/proc/) {
|
|
|
|
# there is no /proc in fakechroot mode
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
if (any { $_ eq $options->{mode} } ('root', 'unshare')) {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
unlink "$options->{root}/$fname" or warn "cannot unlink $fname: $!";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
|
|
|
|
}
|
|
|
|
} elsif ($type == 3 or $type == 4) { # character/block special
|
|
|
|
if (!$options->{havemknod}) {
|
|
|
|
open my $fh, '>', "$options->{root}/$fname" or die "cannot open $options->{root}/$fname: $!";
|
|
|
|
close $fh;
|
|
|
|
if ($options->{mode} eq 'unshare') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or warn "umount $fname failed: $?";
|
|
|
|
unlink "$options->{root}/$fname" or warn "cannot unlink $fname: $!";
|
|
|
|
};
|
|
|
|
} elsif ($options->{mode} eq 'root') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', "$options->{root}/$fname") or warn "umount failed: $?";
|
|
|
|
unlink "$options->{root}/$fname" or warn "cannot unlink $fname: $!";
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
die "unknown mode: $options->{mode}";
|
|
|
|
}
|
|
|
|
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount $fname failed: $?";
|
|
|
|
}
|
|
|
|
} elsif ($type == 5) { # directory
|
|
|
|
if (!$options->{havemknod}) {
|
|
|
|
if (any { $_ eq $options->{mode} } ('root', 'unshare')) {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
rmdir "$options->{root}/$fname" or warn "cannot rmdir $fname: $!";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
make_path "$options->{root}/$fname" or die "cannot make_path $fname";
|
|
|
|
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
|
|
|
|
}
|
|
|
|
if ($options->{mode} eq 'unshare') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', '--no-mtab', "$options->{root}/$fname") or warn "umount $fname failed: $?";
|
|
|
|
};
|
|
|
|
} elsif ($options->{mode} eq 'root') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', "$options->{root}/$fname") or warn "umount $fname failed: $?";
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
die "unknown mode: $options->{mode}";
|
|
|
|
}
|
|
|
|
0 == system('mount', '-o', 'bind', "/$fname", "$options->{root}/$fname") or die "mount $fname failed: $?";
|
|
|
|
} else {
|
|
|
|
die "unsupported type: $type";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elsif (any { $_ eq $options->{mode} } ('proot', 'fakechroot')) {
|
|
|
|
# we cannot mount in fakechroot and proot mode
|
|
|
|
# in proot mode we have /dev bind-mounted already through --bind=/dev
|
|
|
|
} else {
|
|
|
|
die "unknown mode: $options->{mode}";
|
|
|
|
}
|
|
|
|
# We can only mount /proc and /sys after extracting the essential
|
|
|
|
# set because if we mount it before, then base-files will not be able
|
|
|
|
# to extract those
|
|
|
|
if ($options->{mode} eq 'root') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', "$options->{root}/sys") or warn "umount /sys failed: $?";
|
|
|
|
};
|
|
|
|
0 == system('mount', '-t', 'sysfs', '-o', 'nosuid,nodev,noexec', 'sys', "$options->{root}/sys") or die "mount /sys failed: $?";
|
|
|
|
} elsif ($options->{mode} eq 'unshare') {
|
|
|
|
# naturally we have to clean up after ourselves in sudo mode where we
|
|
|
|
# do a real mount. But we also need to unmount in unshare mode because
|
|
|
|
# otherwise, even with the --one-file-system tar option, the
|
|
|
|
# permissions of the mount source will be stored and not the mount
|
|
|
|
# target (the directory)
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
# since we cannot write to /etc/mtab we need --no-mtab
|
|
|
|
# unmounting /sys only seems to be successful with --lazy
|
|
|
|
0 == system('umount', '--no-mtab', '--lazy', "$options->{root}/sys") or warn "umount /sys failed: $?";
|
|
|
|
};
|
|
|
|
# without the network namespace unshared, we cannot mount a new
|
|
|
|
# sysfs. Since we need network, we just bind-mount.
|
|
|
|
#
|
|
|
|
# we have to rbind because just using bind results in "wrong fs
|
|
|
|
# type, bad option, bad superblock" error
|
|
|
|
0 == system('mount', '-o', 'rbind', '/sys', "$options->{root}/sys") or die "mount /sys failed: $?";
|
|
|
|
} elsif (any { $_ eq $options->{mode} } ('proot', 'fakechroot')) {
|
|
|
|
# we cannot mount in fakechroot and proot mode
|
|
|
|
# in proot mode we have /proc bind-mounted already through --bind=/proc
|
|
|
|
} else {
|
|
|
|
die "unknown mode: $options->{mode}";
|
|
|
|
}
|
|
|
|
if ($options->{mode} eq 'root') {
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
0 == system('umount', "$options->{root}/proc") or die "umount /proc failed: $?";
|
|
|
|
};
|
|
|
|
0 == system('mount', '-t', 'proc', 'proc', "$options->{root}/proc") or die "mount /proc failed: $?";
|
|
|
|
} elsif ($options->{mode} eq 'unshare') {
|
|
|
|
# naturally we have to clean up after ourselves in sudo mode where we
|
|
|
|
# do a real mount. But we also need to unmount in unshare mode because
|
|
|
|
# otherwise, even with the --one-file-system tar option, the
|
|
|
|
# permissions of the mount source will be stored and not the mount
|
|
|
|
# target (the directory)
|
|
|
|
push @cleanup_tasks, sub {
|
|
|
|
# since we cannot write to /etc/mtab we need --no-mtab
|
|
|
|
0 == system('umount', '--no-mtab', "$options->{root}/proc") or die "umount /proc failed: $?";
|
|
|
|
};
|
|
|
|
0 == system('mount', '-t', 'proc', 'proc', "$options->{root}/proc") or die "mount /proc failed: $?";
|
|
|
|
} elsif (any { $_ eq $options->{mode} } ('proot', 'fakechroot')) {
|
|
|
|
# we cannot mount in fakechroot and proot mode
|
|
|
|
# in proot mode we have /sys bind-mounted already through --bind=/sys
|
|
|
|
} else {
|
|
|
|
die "unknown mode: $options->{mode}";
|
|
|
|
}
|
|
|
|
|
|
|
|
# prevent daemons from starting
|
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/usr/sbin/policy-rc.d" or die "cannot open policy-rc.d: $!";
|
|
|
|
print $fh "#!/bin/sh\n";
|
|
|
|
print $fh "exit 101\n";
|
|
|
|
close $fh;
|
|
|
|
chmod 0755, "$options->{root}/usr/sbin/policy-rc.d" or die "cannot chmod policy-rc.d: $!";
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
move("$options->{root}/sbin/start-stop-daemon", "$options->{root}/sbin/start-stop-daemon.REAL") or die "cannot move start-stop-daemon";
|
|
|
|
open my $fh, '>', "$options->{root}/sbin/start-stop-daemon" or die "cannot open policy-rc.d: $!";
|
|
|
|
print $fh "#!/bin/sh\n";
|
|
|
|
print $fh "echo \"Warning: Fake start-stop-daemon called, doing nothing\">&2\n";
|
|
|
|
close $fh;
|
|
|
|
chmod 0755, "$options->{root}/sbin/start-stop-daemon" or die "cannot chmod start-stop-daemon: $!";
|
|
|
|
}
|
|
|
|
|
|
|
|
&{$cmd}();
|
|
|
|
|
|
|
|
# cleanup
|
|
|
|
move("$options->{root}/sbin/start-stop-daemon.REAL", "$options->{root}/sbin/start-stop-daemon") or die "cannot move start-stop-daemon";
|
|
|
|
unlink "$options->{root}/usr/sbin/policy-rc.d" or die "cannot unlink policy-rc.d";
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
my $error = $@;
|
|
|
|
|
|
|
|
# we use the cleanup function to do the unmounting
|
|
|
|
$cleanup->(0);
|
|
|
|
|
|
|
|
if ($error) {
|
|
|
|
die "run_chroot failed: $error";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-18 09:20:24 +00:00
|
|
|
sub setup {
|
|
|
|
my $options = shift;
|
|
|
|
|
2018-09-24 18:09:43 +00:00
|
|
|
if (0) {
|
|
|
|
foreach my $key (sort keys %{$options}) {
|
|
|
|
my $value = $options->{$key};
|
|
|
|
if (!defined $value) {
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
if (ref $value eq '') {
|
|
|
|
print STDERR "I: $key: $options->{$key}\n";
|
|
|
|
} elsif (ref $value eq 'ARRAY') {
|
|
|
|
print STDERR "I: $key: [" . (join ', ', @{$value}) . "]\n";
|
|
|
|
} else {
|
|
|
|
die "unknown type";
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
my ($conf, $tmpfile) = tempfile(UNLINK => 1) or die "cannot open apt.conf: $!";
|
|
|
|
print $conf "Apt::Architecture \"$options->{nativearch}\";\n";
|
|
|
|
# the host system might have configured additional architectures
|
|
|
|
# force only the native architecture
|
|
|
|
if (scalar @{$options->{foreignarchs}} > 0) {
|
2018-11-04 19:37:36 +00:00
|
|
|
print $conf "Apt::Architectures { \"$options->{nativearch}\"; ";
|
|
|
|
foreach my $arch (@{$options->{foreignarchs}}) {
|
|
|
|
print $conf "\"$arch\"; ";
|
|
|
|
}
|
|
|
|
print $conf "};\n";
|
2018-09-18 09:20:24 +00:00
|
|
|
} else {
|
|
|
|
print $conf "Apt::Architectures \"$options->{nativearch}\";\n";
|
|
|
|
}
|
|
|
|
print $conf "Dir::Etc \"$options->{root}/etc/apt\";\n";
|
|
|
|
print $conf "Dir::State \"$options->{root}/var/lib/apt\";\n";
|
|
|
|
print $conf "Dir::Cache \"$options->{root}/var/cache/apt\";\n";
|
2018-10-22 09:12:11 +00:00
|
|
|
print $conf "Dir::Log \"$options->{root}/var/log/apt\";\n";
|
2018-09-18 09:20:24 +00:00
|
|
|
# for authentication, use the keyrings from the host
|
|
|
|
print $conf "Dir::Etc::Trusted \"/etc/apt/trusted.gpg\";\n";
|
|
|
|
print $conf "Dir::Etc::TrustedParts \"/etc/apt/trusted.gpg.d\";\n";
|
|
|
|
close $conf;
|
|
|
|
|
2018-10-22 15:05:56 +00:00
|
|
|
{
|
|
|
|
my @directories = ('/etc/apt/apt.conf.d', '/etc/apt/sources.list.d',
|
|
|
|
'/etc/apt/preferences.d', '/var/cache/apt',
|
|
|
|
'/var/lib/apt/lists/partial', '/var/lib/dpkg',
|
|
|
|
'/etc/dpkg/dpkg.cfg.d/');
|
|
|
|
# if dpkg and apt operate from the outside we need some more
|
|
|
|
# directories because dpkg and apt might not even be installed inside
|
|
|
|
# the chroot
|
|
|
|
if ($options->{mode} eq 'chrootless') {
|
|
|
|
push @directories, ('/var/log/apt', '/var/lib/dpkg/triggers',
|
|
|
|
'/var/lib/dpkg/info', '/var/lib/dpkg/alternatives',
|
|
|
|
'/var/lib/dpkg/updates');
|
|
|
|
}
|
|
|
|
foreach my $dir (@directories) {
|
|
|
|
make_path("$options->{root}/$dir") or die "failed to create $dir: $!";
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
}
|
|
|
|
|
2018-09-23 17:43:14 +00:00
|
|
|
# We put certain configuration items in their own configuration file
|
|
|
|
# because they have to be valid for apt invocation from outside as well as
|
|
|
|
# from inside the chroot.
|
|
|
|
# The config filename is chosen such that any settings in it will be
|
|
|
|
# overridden by what the user specified with --aptopt.
|
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/etc/apt/apt.conf.d/00mmdebstrap" or die "cannot open /etc/apt/apt.conf.d/00mmdebstrap: $!";
|
|
|
|
print $fh "Apt::Install-Recommends false;\n";
|
|
|
|
print $fh "Acquire::Languages \"none\";\n";
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
2018-09-18 09:20:24 +00:00
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/var/lib/dpkg/status" or die "failed to open(): $!";
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
2019-01-07 12:16:51 +00:00
|
|
|
# /var/lib/dpkg/available is required to exist or otherwise package
|
|
|
|
# removals will fail
|
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/var/lib/dpkg/available" or die "failed to open(): $!";
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
2018-09-18 09:20:24 +00:00
|
|
|
if (scalar @{$options->{foreignarchs}} > 0) {
|
|
|
|
open my $fh, '>', "$options->{root}/var/lib/dpkg/arch" or die "cannot open /var/lib/dpkg/arch: $!";
|
|
|
|
print $fh "$options->{nativearch}\n";
|
2018-11-02 16:24:28 +00:00
|
|
|
foreach my $arch (@{$options->{foreignarchs}}) {
|
2018-09-18 09:20:24 +00:00
|
|
|
print $fh "$arch\n";
|
|
|
|
}
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scalar @{$options->{aptopts}} > 0) {
|
2018-09-23 17:43:14 +00:00
|
|
|
open my $fh, '>', "$options->{root}/etc/apt/apt.conf.d/99mmdebstrap" or die "cannot open /etc/apt/apt.conf.d/99mmdebstrap: $!";
|
2018-09-18 09:20:24 +00:00
|
|
|
foreach my $opt (@{$options->{aptopts}}) {
|
|
|
|
if (-r $opt) {
|
|
|
|
copy $opt, $fh or die "cannot copy $opt: $!";
|
|
|
|
} else {
|
|
|
|
print $fh $opt;
|
|
|
|
if ($opt !~ /;$/) {
|
|
|
|
print $fh ';';
|
|
|
|
}
|
|
|
|
if ($opt !~ /\n$/) {
|
|
|
|
print $fh "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scalar @{$options->{dpkgopts}} > 0) {
|
2018-10-22 15:05:56 +00:00
|
|
|
# FIXME: in chrootless mode, dpkg will only read the configuration
|
|
|
|
# from the host
|
2018-09-23 17:43:14 +00:00
|
|
|
open my $fh, '>', "$options->{root}/etc/dpkg/dpkg.cfg.d/99mmdebstrap" or die "cannot open /etc/dpkg/dpkg.cfg.d/99mmdebstrap: $!";
|
2018-09-18 09:20:24 +00:00
|
|
|
foreach my $opt (@{$options->{dpkgopts}}) {
|
|
|
|
if (-r $opt) {
|
|
|
|
copy $opt, $fh or die "cannot copy $opt: $!";
|
|
|
|
} else {
|
|
|
|
print $fh $opt;
|
|
|
|
if ($opt !~ /\n$/) {
|
|
|
|
print $fh "\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
2018-12-06 23:17:10 +00:00
|
|
|
## setup merged usr
|
|
|
|
#my @amd64_dirs = ('lib32', 'lib64', 'libx32'); # only amd64 for now
|
|
|
|
#foreach my $dir ("bin", "sbin", "lib", @amd64_dirs) {
|
|
|
|
# symlink "usr/$dir", "$options->{root}/$dir" or die "cannot create symlink: $!";
|
|
|
|
# make_path("$options->{root}/usr/$dir") or die "cannot create /usr/$dir: $!";
|
|
|
|
#}
|
2018-09-18 09:20:24 +00:00
|
|
|
|
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/etc/fstab" or die "cannot open fstab: $!";
|
|
|
|
print $fh "# UNCONFIGURED FSTAB FOR BASE SYSTEM\n";
|
|
|
|
close $fh;
|
|
|
|
chmod 0644, "$options->{root}/etc/fstab" or die "cannot chmod fstab: $!";
|
|
|
|
}
|
|
|
|
|
2018-10-02 08:09:22 +00:00
|
|
|
# write /etc/apt/sources.list
|
2018-09-18 09:20:24 +00:00
|
|
|
{
|
|
|
|
open my $fh, '>', "$options->{root}/etc/apt/sources.list" or die "cannot open /etc/apt/sources.list: $!";
|
2018-10-02 08:09:22 +00:00
|
|
|
print $fh $options->{sourceslist};
|
2018-09-18 09:20:24 +00:00
|
|
|
close $fh;
|
|
|
|
}
|
|
|
|
|
2018-10-22 15:05:56 +00:00
|
|
|
# allow network access from within
|
|
|
|
copy("/etc/resolv.conf", "$options->{root}/etc/resolv.conf") or die "cannot copy /etc/resolv.conf: $!";
|
|
|
|
copy("/etc/hostname", "$options->{root}/etc/hostname") or die "cannot copy /etc/hostname: $!";
|
|
|
|
|
|
|
|
if ($options->{havemknod}) {
|
|
|
|
foreach my $file (@devfiles) {
|
|
|
|
my ($fname, $mode, $type, $linkname, $devmajor, $devminor) = @{$file};
|
|
|
|
if ($type == 0) { # normal file
|
|
|
|
die "type 0 not implemented";
|
|
|
|
} elsif ($type == 1) { # hardlink
|
|
|
|
die "type 1 not implemented";
|
|
|
|
} elsif ($type == 2) { # symlink
|
2018-10-23 14:03:32 +00:00
|
|
|
if ($options->{mode} eq 'fakechroot' and $linkname =~ /^\/proc/) {
|
|
|
|
# there is no /proc in fakechroot mode
|
|
|
|
next;
|
|
|
|
}
|
2018-10-22 15:05:56 +00:00
|
|
|
symlink $linkname, "$options->{root}/$fname" or die "cannot create symlink $fname";
|
|
|
|
next; # chmod cannot work on symlinks
|
|
|
|
} elsif ($type == 3) { # character special
|
|
|
|
0 == system('mknod', "$options->{root}/$fname", 'c', $devmajor, $devminor) or die "mknod failed: $?";
|
|
|
|
} elsif ($type == 4) { # block special
|
|
|
|
0 == system('mknod', "$options->{root}/$fname", 'b', $devmajor, $devminor) or die "mknod failed: $?";
|
|
|
|
} elsif ($type == 5) { # directory
|
|
|
|
make_path "$options->{root}/$fname", { error => \my $err };
|
|
|
|
if (@$err) {
|
|
|
|
die "cannot create $fname";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
die "unsupported type: $type";
|
|
|
|
}
|
|
|
|
chmod $mode, "$options->{root}/$fname" or die "cannot chmod $fname: $!";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-18 09:20:24 +00:00
|
|
|
# we tell apt about the configuration via a config file passed via the
|
|
|
|
# APT_CONFIG environment variable instead of using the --option command
|
|
|
|
# line arguments because configuration settings like Dir::Etc have already
|
|
|
|
# been evaluated at the time that apt takes its command line arguments
|
|
|
|
# into account.
|
|
|
|
$ENV{"APT_CONFIG"} = "$tmpfile";
|
2018-09-21 06:04:40 +00:00
|
|
|
|
2018-09-23 20:27:49 +00:00
|
|
|
print STDERR "I: running apt-get update...\n";
|
2018-12-27 20:08:53 +00:00
|
|
|
run_apt_progress({
|
|
|
|
ARGV => ['apt-get', 'update'],
|
|
|
|
VERBOSE => $options->{verbose}
|
|
|
|
});
|
2018-09-21 06:04:40 +00:00
|
|
|
|
|
|
|
# check if anything was downloaded at all
|
|
|
|
{
|
|
|
|
open my $fh, '-|', 'apt-get', 'indextargets' // die "failed to fork(): $!";
|
|
|
|
chomp (my $indextargets = do { local $/; <$fh> });
|
|
|
|
close $fh;
|
|
|
|
if ($indextargets eq '') {
|
2018-10-02 02:11:12 +00:00
|
|
|
print STDERR "content of /etc/apt/sources.list:\n";
|
|
|
|
copy("$options->{root}/etc/apt/sources.list", *STDERR);
|
2018-09-21 06:04:40 +00:00
|
|
|
die "apt-get update didn't download anything";
|
|
|
|
}
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
|
|
|
|
# setting PATH for chroot, ldconfig, start-stop-daemon...
|
2018-11-23 16:30:32 +00:00
|
|
|
if (defined $ENV{PATH} && $ENV{PATH} ne "") {
|
|
|
|
$ENV{PATH} = "$ENV{PATH}:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
|
|
} else {
|
|
|
|
$ENV{PATH} = "/usr/sbin:/usr/bin:/sbin:/bin";
|
|
|
|
}
|
2018-09-18 09:20:24 +00:00
|
|
|
|
|
|
|
my %pkgs_to_install;
|
|
|
|
if (defined $options->{include}) {
|
|
|
|
for my $pkg (split /,/, $options->{include}) {
|
|
|
|
$pkgs_to_install{$pkg} = ();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($options->{variant} eq 'buildd') {
|
|
|
|
$pkgs_to_install{'build-essential'} = ();
|
|
|
|
}
|
|
|
|
# To figure out the right package set for the apt variant we can use:
|
|
|
|
# $ apt-get dist-upgrade -o dir::state::status=/dev/null
|
|
|
|
# This is because that variants only contain essential packages and
|
|
|
|
# apt and libapt treats apt as essential. If we want to install less
|
|
|
|
# (essential variant) then we have to compute the package set ourselves.
|
|
|
|
# Same if we want to install priority based variants.
|
2018-10-22 15:05:56 +00:00
|
|
|
if (any { $_ eq $options->{variant} } ('extract', 'custom')) {
|
|
|
|
print STDERR "I: downloading packages with apt...\n";
|
2018-12-27 20:08:53 +00:00
|
|
|
run_apt_progress({
|
|
|
|
ARGV => ['apt-get', '--yes',
|
|
|
|
'-oApt::Get::Download-Only=true',
|
|
|
|
'install'],
|
|
|
|
PKGS => [keys %pkgs_to_install],
|
|
|
|
VERBOSE => $options->{verbose}
|
|
|
|
});
|
2018-10-22 15:05:56 +00:00
|
|
|
} elsif ($options->{variant} eq 'apt') {
|
|
|
|
# if we just want to install Essential:yes packages, apt and their
|
|
|
|
# dependencies then we can make use of libapt treating apt as
|
|
|
|
# implicitly essential. An upgrade with the (currently) empty status
|
|
|
|
# file will trigger an installation of the essential packages plus apt.
|
|
|
|
#
|
|
|
|
# 2018-09-02, #debian-dpkg on OFTC, times in UTC+2
|
|
|
|
# 23:39 < josch> I'll just put it in my script and if it starts
|
|
|
|
# breaking some time I just say it's apt's fault. :P
|
|
|
|
# 23:42 < DonKult> that is how it usually works, so yes, do that :P (<-
|
|
|
|
# and please add that line next to it so you can
|
|
|
|
# remind me in 5+ years that I said that after I wrote
|
|
|
|
# in the bugreport: "Are you crazy?!? Nobody in his
|
|
|
|
# right mind would even suggest depending on it!")
|
|
|
|
print STDERR "I: downloading packages with apt...\n";
|
2018-12-27 20:08:53 +00:00
|
|
|
run_apt_progress({
|
|
|
|
ARGV => ['apt-get', '--yes',
|
|
|
|
'-oApt::Get::Download-Only=true',
|
|
|
|
'dist-upgrade'],
|
|
|
|
VERBOSE => $options->{verbose}
|
|
|
|
});
|
2018-10-22 15:05:56 +00:00
|
|
|
} elsif (any { $_ eq $options->{variant} } ('essential', 'standard', 'important', 'required', 'buildd', 'minbase')) {
|
2018-09-18 09:20:24 +00:00
|
|
|
my %ess_pkgs;
|
2018-10-22 09:29:56 +00:00
|
|
|
open(my $pipe_apt, '-|', 'apt-get', 'indextargets', '--format', '$(FILENAME)', 'Created-By: Packages') or die "cannot start apt-get indextargets: $!";
|
2018-09-18 09:20:24 +00:00
|
|
|
while (my $fname = <$pipe_apt>) {
|
|
|
|
chomp $fname;
|
|
|
|
open (my $pipe_cat, '-|', '/usr/lib/apt/apt-helper', 'cat-file', $fname) or die "cannot start apt-helper cat-file: $!";
|
|
|
|
|
2018-09-23 19:15:12 +00:00
|
|
|
my $pkgname;
|
|
|
|
my $ess = '';
|
|
|
|
my $prio = 'optional';
|
2018-10-22 09:29:56 +00:00
|
|
|
my $arch = '';
|
2018-09-23 19:15:12 +00:00
|
|
|
while (my $line = <$pipe_cat>) {
|
|
|
|
chomp $line;
|
|
|
|
# Dpkg::Index takes 10 seconds to parse a typical Packages
|
|
|
|
# file. Thus we instead use a simple parser that just retrieve
|
|
|
|
# the information we need.
|
|
|
|
if ($line ne "") {
|
|
|
|
if ($line =~ /^Package: (.*)/) {
|
|
|
|
$pkgname = $1;
|
|
|
|
} elsif ($line =~ /^Essential: yes$/) {
|
|
|
|
$ess = 'yes'
|
|
|
|
} elsif ($line =~ /^Priority: (.*)/) {
|
|
|
|
$prio = $1;
|
2018-10-22 09:29:56 +00:00
|
|
|
} elsif ($line =~ /^Architecture: (.*)/) {
|
|
|
|
$arch = $1;
|
2018-09-23 19:15:12 +00:00
|
|
|
}
|
|
|
|
next;
|
|
|
|
}
|
2018-10-22 09:29:56 +00:00
|
|
|
# we are only interested of packages of native architecture or
|
|
|
|
# Architecture:all
|
|
|
|
if ($arch eq $options->{nativearch} or $arch eq 'all') {
|
|
|
|
# the line is empty, thus a package stanza just finished
|
|
|
|
# processing and we can handle it now
|
|