This is an automated email from the git hooks/post-receive script. guillem pushed a commit to branch master in repository dpkg.
View the commit online: https://git.dpkg.org/cgit/dpkg/dpkg.git/commit/?id=97881082b42cfebf999e4812f985a2d06ac583b0 commit 97881082b42cfebf999e4812f985a2d06ac583b0 (HEAD -> master) Author: Guillem Jover <[email protected]> AuthorDate: Thu Apr 16 14:45:00 2020 +0200 dpkg-fsys-usrunmess: New program This program undoes the merged-/usr-via-aliased-dirs mess done on filesystems that have been installed anew with recent installers with unfortunate defaults or systems migrated with the convert-usrmerge tool from the usrmerge package. Note: while dpkg.deb intends to contain no perl code, to be able to handle upgrades properly, and to reduce the perl usage on minimal systems, this one is made an exception as it is a script that does not depend on anything non-core including libdpkg-perl (except for libfile-fcntllock-perl checked at run-time), and is to be executed as a one-off thing. And it is more important to give people that have such broken systems recover from that, than keep the aforementioned properties very strictly for this particular case. --- debian/dpkg.install | 1 + debian/dpkg.manpages | 1 + man/Makefile.am | 2 + man/dpkg-fsys-usrunmess.pod | 147 +++++++++++++ man/po/po4a.cfg | 1 + scripts/.gitignore | 1 + scripts/Makefile.am | 5 + scripts/dpkg-fsys-usrunmess.pl | 473 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 631 insertions(+) diff --git a/debian/dpkg.install b/debian/dpkg.install index 89455c4f2..86164ebe5 100644 --- a/debian/dpkg.install +++ b/debian/dpkg.install @@ -2,6 +2,7 @@ etc/dpkg/dpkg.cfg.d etc/alternatives +sbin/dpkg-fsys-usrunmess usr/sbin/ sbin/start-stop-daemon usr/bin/dpkg usr/bin/dpkg-deb diff --git a/debian/dpkg.manpages b/debian/dpkg.manpages index 669a0104a..5d70349e2 100644 --- a/debian/dpkg.manpages +++ b/debian/dpkg.manpages @@ -1,5 +1,6 @@ usr/share/man/*/dpkg-deb.1 usr/share/man/*/dpkg-divert.1 +usr/share/man/*/dpkg-fsys-usrunmess.8 usr/share/man/*/dpkg-maintscript-helper.1 usr/share/man/*/dpkg-query.1 usr/share/man/*/dpkg-realpath.1 diff --git a/man/Makefile.am b/man/Makefile.am index 225522dee..669c33c6d 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -33,6 +33,7 @@ man_MANS = \ dpkg-deb.1 \ dpkg-distaddfile.1 \ dpkg-divert.1 \ + dpkg-fsys-usrunmess.8 \ dpkg-genbuildinfo.1 \ dpkg-genchanges.1 \ dpkg-gencontrol.1 \ @@ -107,6 +108,7 @@ EXTRA_DIST = \ dpkg-deb.pod \ dpkg-distaddfile.pod \ dpkg-divert.pod \ + dpkg-fsys-usrunmess.pod \ dpkg-genbuildinfo.pod \ dpkg-genchanges.pod \ dpkg-gencontrol.pod \ diff --git a/man/dpkg-fsys-usrunmess.pod b/man/dpkg-fsys-usrunmess.pod new file mode 100644 index 000000000..48c694bb5 --- /dev/null +++ b/man/dpkg-fsys-usrunmess.pod @@ -0,0 +1,147 @@ +# dpkg manual page - dpkg-fsys-usrunmess(8) +# +# Copyright © 2020-2021 Guillem Jover <[email protected]> +# +# This is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +=encoding utf8 + +=head1 NAME + +dpkg-fsys-usrunmess - undoes the merged-/usr-via-aliased-dirs mess + +=head1 SYNOPSIS + +B<dpkg-fsys-usrunmess> [B<option>...] + +=head1 DESCRIPTION + +B<dpkg-fsys-usrunmess> is a tool to fix up filesystems that have been +installed anew with recent installers with unfortunate defaults or +migrated to the broken merged /usr via aliased directories layout, +which is B<not> supported by dpkg. See the dpkg FAQ. + +The program will perform the following overall actions: + +=over + +=item * + +Check whether the system needs to be switched, otherwise do nothing, + +=item * + +Check for dpkg database consistency and otherwise abort, + +=item * + +Get the list of files and alternatives that need to be restored. + +=item * + +Create a shadow hierarchy under F</.usrunmess>, by creating the directories +symlinks or hardlinking or copying the files as needed. + +=item * + +Prompt for confirmation before proceeding, if requested on the command-line. + +=item * + +Lock the dpkg database. + +=item * + +Mark all packages as half-configured to force running maintainer scripts +that might need to recreate files. + +=item * + +Replace the aliased directories with the shadow ones, by creating a backup +of the old symlinked directories and renaming the shadow ones over. + +=item * + +Relabel MAC information for directories and symlinks if necessary. + +=item * + +Reconfigure all packages. + +=item * + +Remove backup symlinks. + +=item * + +Remove old moved objects, but defer directory removal. + +=item * + +Remove old deferred directories that are not referenced by dpkg-query. + +=item * + +Remove shadow root directory. + +=back + +B<Note>: When running the program from some shells such as L<bash(1)> or +L<zsh(1)>, after executing it, you might need to request the shell to +forget all remembered executable locations with for example C<hash -r>. + +B<Warning>: Note that this operation has the potential to render the system +unusable or broken in case of a sudden crash or reboot, unexpected state of +the system, or possible bugs in the script. Be prepared with recovery media +and consider doing backups beforehand. + +=head1 OPTIONS + +=over + +=item B<-p>, B<--prompt> + +Prompt at the time of no return, so that the debug output or the shadow +hierarchy can be evaluated before proceeding. + +=item B<-n>, B<--no-act> + +=item B<--dry-run> + +This option enables the dry-run mode, where no destructive action takes place, +only the preparatory part. + +=item B<-?>, B<--help> + +Show the usage message and exit. + +=item B<--version> + +Show the version and exit. + +=back + +=head1 ENVIRONMENT + +=over + +=item B<DPKG_USRUNMESS_NOACT> + +This setting defines whether to enable dry-run mode. + +=back + +=head1 SEE ALSO + +L<https://wiki.debian.org/Teams/Dpkg/FAQ#Q:_Does_dpkg_support_merged-.2Fusr-via-aliased-dirs.3F>. diff --git a/man/po/po4a.cfg b/man/po/po4a.cfg index b66da2f9c..53b8a0a02 100644 --- a/man/po/po4a.cfg +++ b/man/po/po4a.cfg @@ -36,6 +36,7 @@ [type:pod] dpkg-deb.pod $lang:$lang/dpkg-deb.pod [type:pod] dpkg-distaddfile.pod $lang:$lang/dpkg-distaddfile.pod [type:pod] dpkg-divert.pod $lang:$lang/dpkg-divert.pod +[type:pod] dpkg-fsys-usrunmess.pod $lang:$lang/dpkg-fsys-usrunmess.pod [type:pod] dpkg-genbuildinfo.pod $lang:$lang/dpkg-genbuildinfo.pod [type:pod] dpkg-genchanges.pod $lang:$lang/dpkg-genchanges.pod [type:pod] dpkg-gencontrol.pod $lang:$lang/dpkg-gencontrol.pod diff --git a/scripts/.gitignore b/scripts/.gitignore index 99f6d0900..2acfbb282 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -5,6 +5,7 @@ dpkg-buildflags dpkg-buildpackage dpkg-checkbuilddeps dpkg-distaddfile +dpkg-fsys-usrunmess dpkg-genbuildinfo dpkg-genchanges dpkg-gencontrol diff --git a/scripts/Makefile.am b/scripts/Makefile.am index 7275c8edb..f36f0c5c8 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -2,6 +2,9 @@ SUBDIRS = mk po +sbin_SCRIPTS = \ + dpkg-fsys-usrunmess + bin_SCRIPTS = \ dpkg-architecture \ dpkg-buildflags \ @@ -29,6 +32,7 @@ EXTRA_DIST = \ dpkg-buildpackage.pl \ dpkg-checkbuilddeps.pl \ dpkg-distaddfile.pl \ + dpkg-fsys-usrunmess.pl \ dpkg-genbuildinfo.pl \ dpkg-genchanges.pl \ dpkg-gencontrol.pl \ @@ -52,6 +56,7 @@ nobase_dist_pkgdata_DATA = \ CLEANFILES = \ $(test_data_objects) \ + $(sbin_SCRIPTS) \ $(bin_SCRIPTS) perllibdir = $(PERL_LIBDIR) diff --git a/scripts/dpkg-fsys-usrunmess.pl b/scripts/dpkg-fsys-usrunmess.pl new file mode 100755 index 000000000..9220df3c0 --- /dev/null +++ b/scripts/dpkg-fsys-usrunmess.pl @@ -0,0 +1,473 @@ +#!/usr/bin/perl +# +# dpkg-fsys-usrunmess - Undoes the merged-/usr-via-aliased-dirs mess +# +# Copyright © 2020-2021 Guillem Jover <[email protected]> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/> + +use strict; +use warnings; +use feature qw(state); + +our ($PROGNAME) = $0 =~ m{(?:.*/)?([^/]*)}; +our $PROGVERSION = '1.20.x'; +our $ADMINDIR = '/var/lib/dpkg/'; + +use POSIX; +use Getopt::Long qw(:config posix_default bundling_values no_ignorecase); + +eval q{ + pop @INC if $INC[-1] eq '.'; + use File::FcntlLock; +}; +if ($@) { + fatal('missing File::FcntlLock module; please install libfile-fcntllock-perl'); +} + +my $opt_noact = length $ENV{DPKG_USRUNMESS_NOACT} ? 1 : 0; +my $opt_prompt = 0; + +my @options_spec = ( + 'help|?' => sub { usage(); exit 0; }, + 'version' => sub { version(); exit 0; }, + 'dry-run|no-act|n' => \$opt_noact, + 'prompt|p' => \$opt_prompt, +); + +{ + local $SIG{__WARN__} = sub { usageerr($_[0]) }; + GetOptions(@options_spec); +} + +my @aliased_dirs; + +# +# Scan all dirs under / and check whether any are aliased to /usr. +# + +foreach my $path (glob '/*') { + debug("checking symlink? $path"); + next unless -l $path; + debug("checking merged-usr symlink? $path"); + my $symlink = readlink $path; + next unless $symlink eq "usr$path" or $symlink eq "/usr$path"; + debug("merged-usr breakage, queueing $path"); + push @aliased_dirs, $path; +} + +if (@aliased_dirs == 0) { + print "System is fine, no aliased directories found, congrats!\n"; + exit 0; +} + +# +# dpkg consistency checks +# + +debug('checking dpkg database consistency'); +system(qw(dpkg --audit)) == 0 + or fatal("cannot audit the dpkg database: $!"); + +debug('checking whether dpkg has been interrupted'); +if (glob "$ADMINDIR/updates/*") { + fatal('dpkg is in an inconsistent state, please fix that'); +} + +my $aliased_regex = '^(' . join('|', @aliased_dirs) . ')/'; + +# +# Get a list of all paths (including diversion) under the aliased dirs. +# + +my @search_args; +my %aliased_pathnames; +foreach my $dir (@aliased_dirs) { + push @search_args, "$dir/*"; +} +open my $fh_paths, '-|', 'dpkg-query', '--search', @search_args + or fatal("cannot execute dpkg-query --search: $!"); +while (<$fh_paths>) { + if (m/^diversion by [^ ]+ from: .*$/) { + # Ignore. + } elsif (m/^diversion by [^ ]+ to: (.*)$/) { + if (-e $1) { + add_pathname($1, 'diverted pathname'); + } + } elsif (m/^.*: (.*)$/) { + add_pathname($1, 'pathname'); + } +} +close $fh_paths; + +# +# Get a list of all update-alternatives under the aliased dirs. +# + +my @selections = qx(update-alternatives --get-selections); +foreach my $selection (@selections) { + my $name = (split(' ', $selection))[0]; + my $slaves = 0; + + open my $fh_alts, '-|', 'update-alternatives', '--query', $name + or fatal("cannot execute update-alternatives --query: $!"); + while (<$fh_alts>) { + if (m/^\s*$/) { + last; + } elsif (m/^Link: (.*)$/) { + add_pathname($1, 'alternative link'); + } elsif (m/^Slaves:\s*$/) { + $slaves = 1; + } elsif ($slaves and m/^\s\S+\s(\S+)$/) { + add_pathname($1, 'alternative slave'); + } else { + $slaves = 0; + } + } + close $fh_alts; +} + +my $sroot = '/.usrunmess'; +my @relabel; + +# +# Create a shadow hierarchy under / for the new unmessed dir: +# + +debug("creating shadow dir = $sroot"); +mkdir $sroot + or sysfatal("cannot create directory $sroot"); +foreach my $dir (@aliased_dirs) { + debug("creating shadow dir = $sroot$dir"); + mkdir "$sroot$dir" + or sysfatal("cannot create directory $sroot$dir"); + push @relabel, "$sroot$dir"; +} + +# +# Populate the split dirs with hardlinks or copies of the objects from +# their counter-parts in /usr. +# + +foreach my $pathname (sort keys %aliased_pathnames) { + my (@meta) = lstat $pathname + or sysfatal("cannot lstat object $pathname for shadow hierarchy"); + + if (-d _) { + my $mode = $meta[2]; + my ($uid, $gid) = @meta[4, 5]; + my ($atime, $mtime, $ctime) = @meta[8, 9, 10]; + + debug("creating shadow dir = $sroot$pathname"); + mkdir "$sroot$pathname" + or sysfatal("cannot mkdir $sroot$pathname"); + chmod $mode, "$sroot$pathname" + or sysfatal("cannot chmod $mode $sroot$pathname"); + chown $uid, $gid, "$sroot$pathname" + or sysfatal("cannot chown $uid $gid $sroot$pathname"); + utime $atime, $mtime, "$sroot$pathname" + or sysfatal("cannot utime $atime $mtime $sroot$pathname"); + push @relabel, "$sroot$pathname"; + } elsif (-f _) { + debug("creating shadow file = $sroot$pathname"); + copy("/usr$pathname", "$sroot$pathname"); + } elsif (-l _) { + my $target = readlink "/usr$pathname"; + + debug("creating shadow symlink = $sroot$pathname"); + symlink $target, "$sroot$pathname" + or sysfatal("cannot symlink $target to $sroot$pathname"); + push @relabel, "$sroot$pathname"; + } else { + fatal("unhandled object type for '$pathname'"); + } +} + +# +# Prompt at the point of no return, if the user requested it. +# + +if ($opt_prompt) { + print "Shadow hierarchy created at '$sroot', ready to proceed (y/N)? "; + my $reply = <STDIN>; + chomp $reply; + + if ($reply ne 'y' and $reply ne 'yes') { + print "Aborting migration, shadow hierarchy left in place.\n"; + exit 0; + } +} + +# +# Mark all packages as half-configured so that we can force a mass +# reconfiguration, to trigger any code in maintainer scripts that might +# create files. +# +# XXX: We do this manually by editing the status file. +# XXX: We do this for packages that might not have maintscripts, or might +# not involve affected directories. +# + +debug('marking all dpkg packages as half-configured'); +if (not $opt_noact) { + open my $fh_lock, '>', "$ADMINDIR/lock" + or sysfatal('cannot open dpkg database lock file'); + my $fs = File::FcntlLock->new(l_type => F_WRLCK); + $fs->lock($fh_lock, F_SETLKW) + or sysfatal('cannot get a write lock on dpkg database'); + + my $file_db = "$ADMINDIR/status"; + my $file_dbnew = $file_db . '.new'; + + open my $fh_dbnew, '>', $file_dbnew + or sysfatal('cannot open new dpkg database'); + open my $fh_db, '<', $file_db + or sysfatal('cannot open dpkg database'); + while (<$fh_db>) { + if (m/^Status: /) { + s/ installed$/ half-configured/; + } + print { $fh_dbnew } $_; + } + close $fh_db; + $fh_dbnew->flush() or sysfatal('cannot flush new dpkg database'); + $fh_dbnew->sync() or sysfatal('cannot fsync new dpkg database'); + close $fh_dbnew or sysfatal('cannot close new dpkg database'); + + rename $file_dbnew, $file_db + or sysfatal('cannot rename new dpkg database'); +} + +# +# Replace things as quickly as possible: +# + +foreach my $dir (@aliased_dirs) { + debug("making dir backup = $dir.aliased"); + if (not $opt_noact) { + rename $dir, "$dir.aliased" + or sysfatal("cannot make backup directory $dir.aliased"); + } + + debug("renaming $sroot$dir to $dir"); + if (not $opt_noact) { + rename "$sroot$dir", $dir + or sysfatal("cannot install fixed directory $dir"); + } +} + +mac_relabel(); + +# +# Re-configure all packages, so that postinst maintscripts are executed. +# + +debug('reconfigured all packages'); +if (not $opt_noact) { + local $ENV{DEBIAN_FRONTEND} = 'noninteractive'; + system(qw(dpkg --configure --pending)) == 0 + or fatal("cannot reconfigure packages: $!"); +} + +# +# Cleanup backup directories. +# + +foreach my $dir (@aliased_dirs) { + debug("removing backup = $dir.aliased"); + if (not $opt_noact) { + unlink "$dir.aliased" + or sysfatal("cannot cleanup backup directory $dir.aliased"); + } +} + +my %deferred_dirnames; + +# +# Cleanup moved objects. +# + +foreach my $pathname (sort keys %aliased_pathnames) { + my (@meta) = lstat $pathname + or sysfatal("cannot lstat object $pathname for cleanup"); + + if (-d _) { + # Skip directories as this might be shared by a proper path under the + # aliased hierearchy. And so that we can remove them in reverse order. + debug("deferring merged dir cleanup = /usr$pathname"); + $deferred_dirnames{"/usr$pathname"} = 1; + } else { + debug("cleaning up pathname = /usr$pathname"); + next if $opt_noact; + unlink "/usr$pathname" + or sysfatal("cannot unlink object /usr$pathname"); + } +} + +# +# Cleanup deferred directories. +# + +debug("cleaning up shadow deferred dir = $sroot"); +my $arg_max = POSIX::sysconf(POSIX::_SC_ARG_MAX) // POSIX::_POSIX_ARG_MAX; +my @batch_dirs; +my $batch_size = 0; + +foreach my $dir (keys %deferred_dirnames) { + my $dir_size = length($dir) + 1; + if ($batch_size + $dir_size < $arg_max) { + $batch_size += length($dir) + 1; + push @batch_dirs, $dir; + + } else { + next; + } + next if length $batch_size == 0; + + open my $fh_dirs, '-|', 'dpkg-query', '--search', @batch_dirs + or fatal("cannot execute dpkg-query --search: $!"); + while (<$fh_dirs>) { + if (m/^.*: (.*)$/) { + # If the directory is known by its aliased name, it should not be + # cleaned up. + if (exists $deferred_dirnames{$1}) { + delete $deferred_dirnames{$1}; + } + } + } + close $fh_dirs; + + @batch_dirs = (); + $batch_size = 0; +} + +if (not $opt_noact) { + foreach my $dirname (reverse sort keys %deferred_dirnames) { + rmdir $dirname + or sysfatal("cannot remove shadow directory $dirname"); + } +} + +if (not $opt_noact) { + debug("cleaning up shadow root dir = $sroot"); + rmdir $sroot + or sysfatal("cannot remove shadow directory $sroot"); +} + +print "Done, hierarchy unmessed, congrats!\n"; + +print "(Note: you might need to run 'hash -r' in your shell.)\n"; + +1; + +## +## Functions +## + +sub debug +{ + my $msg = shift; + + print { \*STDERR } "D: $msg\n"; +} + +sub fatal +{ + my $msg = shift; + + die "error: $msg\n"; +} + +sub sysfatal +{ + my $msg = shift; + + fatal("$msg: $!"); +} + +sub copy +{ + my ($src, $dst) = @_; + + # Try to hardlink first. + return if link $src, $dst; + + # If we are on different filesystems, try a copy. + if ($! == POSIX::EXDEV) { + # XXX: This will not preserve hardlinks, these would get restored + # after the next package upgrade. + system('cp', '-a', $src, $dst) == 0 + or fatal("cannot copy file $src to $dst: $?"); + } else { + sysfatal("cannot link file $src to $dst"); + } +} + +sub mac_relabel +{ + my $has_cmd = 0; + foreach my $path (split /:/, $ENV{PATH}) { + if (-x "$path/restorecon") { + $has_cmd = 1; + last; + } + } + return unless $has_cmd; + + foreach my $pathname (@relabel) { + system('restorecon', $pathname) == 0 + or fatal("cannot restore MAC context for $pathname: $?"); + } +} + +sub add_pathname +{ + my ($pathname, $origin) = @_; + + if ($pathname =~ m/$aliased_regex/) { + debug("adding $origin = $pathname"); + $aliased_pathnames{$pathname} = 1; + } +} + +sub version() +{ + printf "Debian %s version %s.\n", $PROGNAME, $PROGVERSION; +} + +sub usage +{ + printf +'Usage: %s [<option>...]' + . "\n\n" . +'Options: + -p, --prompt prompt before the point of no return. + -n, --no-act just check and create the new structure, no switch. + --dry-run ditto. + -?, --help show this help message. + --version show the version.' + . "\n", $PROGNAME; +} + +sub usageerr +{ + my $msg = shift; + + state $printforhelp = 'Use --help for program usage information.'; + + $msg = sprintf $msg, @_ if @_; + warn "$PROGNAME: error: $msg\n"; + warn "$printforhelp\n"; + exit 2; +} -- Dpkg.Org's dpkg

