diff -Nru dpkg-1.19.0.5ubuntu2/debian/changelog dpkg-1.19.0.5ubuntu2.4/debian/changelog --- dpkg-1.19.0.5ubuntu2/debian/changelog 2018-04-16 02:25:21.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/debian/changelog 2022-05-25 11:14:56.000000000 +0000 @@ -1,3 +1,45 @@ +dpkg (1.19.0.5ubuntu2.4) bionic-security; urgency=medium + + * SECURITY UPDATE: Directory traversal issue in dpkg-source + - scripts/Dpkg/Source/Archive.pm, scripts/t/Dpkg_Source_Archive.t: + Prevent directory traversal for in-place extracts. + - CVE-2022-1664 + + -- Marc Deslauriers Wed, 25 May 2022 07:14:56 -0400 + +dpkg (1.19.0.5ubuntu2.3) bionic; urgency=medium + + * d/rules: always run dh_autoreconf (LP: #1842947) + + -- Dan Streetman Thu, 05 Sep 2019 17:05:14 -0400 + +dpkg (1.19.0.5ubuntu2.2) bionic; urgency=medium + + * Cherry-pick upstream fixes for trigger loops (LP: #1828639) + - dpkg: Negate tortoise_not_in_hare() function name and return value + - dpkg: Initialize trigcyclenode's next member once + - dpkg: Factor trigproc_new_cyclenode() out from check_trigger_cycle() + - dpkg: Mark the package we are giving up on a trigger cycle as istobe normal + - dpkg: Switch dependtry from an int to an enum + - dpkg: Reset progress_bytrigproc once we have injected into the current iteration + - dpkg: Split trigger processing types into required, try-queued and try-deferred + - dpkg: Convert one trigger processing required type into the new try-queued + - dpkg: Move trigproc cycle reset inside try-deferred conditional + - dpkg: Introduce a new dependency try level for trigger processing + - dpkg: Introduce a new dependency try level for trigger cycle checks + + -- Julian Andres Klode Tue, 16 Jul 2019 14:57:11 +0200 + +dpkg (1.19.0.5ubuntu2.1) bionic; urgency=medium + + * Apply patch from upstream to add frontend locking (LP: #1796081): + - Add support for frontend locking. This makes it possible for frontends + using this new protocol, to safely lock the dpkg database w/o risk of + race conditions with other dpkg instances or frontends supporting the + same protocol. + + -- Julian Andres Klode Thu, 04 Oct 2018 14:21:49 +0200 + dpkg (1.19.0.5ubuntu2) bionic; urgency=medium * Add Zstandard compression and decompression support for binary packages diff -Nru dpkg-1.19.0.5ubuntu2/debian/rules dpkg-1.19.0.5ubuntu2.4/debian/rules --- dpkg-1.19.0.5ubuntu2/debian/rules 2018-04-16 02:23:34.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/debian/rules 2019-09-05 21:05:14.000000000 +0000 @@ -41,15 +41,10 @@ D := $(CURDIR)/debian/tmp -# Create configure script if necessary, automake handles rebuilding it. -configure: - dh_testdir - - autoreconf -v -i - # Configure the build tree -build-tree/config.status: configure +build-tree/config.status: dh_testdir + dh_autoreconf install -d build-tree cd build-tree && ../configure $(confflags) \ @@ -153,6 +148,7 @@ [ ! -f Makefile ] || $(MAKE) distclean rm -rf build-tree + dh_autoreconf_clean dh_clean diff -Nru dpkg-1.19.0.5ubuntu2/doc/frontend.txt dpkg-1.19.0.5ubuntu2.4/doc/frontend.txt --- dpkg-1.19.0.5ubuntu2/doc/frontend.txt 2017-01-09 02:19:43.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/doc/frontend.txt 2019-08-24 17:25:11.000000000 +0000 @@ -10,15 +10,15 @@ ---------------- Any frontend needing to make sure no write operation is currently happening, -should lock the dpkg database by locking the file «/lock» using -file record locks (i.e. fcntl(2) advisory locking). The whole file should -be locked, as that's the most portable way to perform this operation; this -can be achieved by using start=0, len=0 and whence=SEEK_SET. +and no other frontend is running should first acquire the frontend lock at +«/lock-frontend», and then acquire the dpkg database lock at +«/lock». When the frontend invokes dpkg, it should set the +environment variable DPKG_FRONTEND_LOCKED (to prevent dpkg from acquiring +the frontend lock), and then release the dpkg database lock, which will be +acquired by dpkg itself. This way no other frontend following this protocol +can race to perform operations while another one has one in progress. -Take into account there will be a race condition between the frontend -unlocking the database and the invoked dpkg locking it again, in which -another process could lock it. - -In the future this functionality will be available through a shared libdpkg -library, and all frontends will be expected to switch to that instead, -because this will fix the aforementioned race condition. +These locks must be file record locks (i.e. fcntl(2) advisory locking), and +the whole file should be locked, as that's the most portable way to perform +this operation; this can be achieved by using start=0, len=0 and +whence=SEEK_SET. diff -Nru dpkg-1.19.0.5ubuntu2/lib/dpkg/dbmodify.c dpkg-1.19.0.5ubuntu2.4/lib/dpkg/dbmodify.c --- dpkg-1.19.0.5ubuntu2/lib/dpkg/dbmodify.c 2018-01-03 01:23:14.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/lib/dpkg/dbmodify.c 2019-09-05 21:05:14.000000000 +0000 @@ -51,6 +51,7 @@ static enum modstatdb_rw cstatus=-1, cflags=0; static char *lockfile; +static char *frontendlockfile; static char *statusfile, *availablefile; static char *importanttmpfile=NULL; static FILE *importanttmp; @@ -140,6 +141,7 @@ char **store; } fnis[] = { { LOCKFILE, &lockfile }, + { FRONTENDLOCKFILE, &frontendlockfile }, { STATUSFILE, &statusfile }, { AVAILFILE, &availablefile }, { UPDATESDIR, &updatesdir }, @@ -185,6 +187,7 @@ } static int dblockfd = -1; +static int frontendlockfd = -1; bool modstatdb_is_locked(void) @@ -216,6 +219,18 @@ if (dblockfd >= 0) return true; + if (getenv("DPKG_FRONTEND_LOCKED") == NULL) { + frontendlockfd = open(frontendlockfile, O_RDWR | O_CREAT | O_TRUNC, 0660); + if (frontendlockfd == -1) { + if (errno == EACCES || errno == EPERM) + return false; + else + ohshite(_("unable to open/create frontend lockfile")); + } + } else { + frontendlockfd = -1; + } + dblockfd = open(lockfile, O_RDWR | O_CREAT | O_TRUNC, 0660); if (dblockfd == -1) { if (errno == EACCES || errno == EPERM) @@ -233,6 +248,9 @@ if (!modstatdb_can_lock()) ohshit(_("you do not have permission to lock the dpkg status database")); + if (frontendlockfd != -1) + file_lock(&frontendlockfd, FILE_LOCK_NOWAIT, frontendlockfile, + _("dpkg frontend")); file_lock(&dblockfd, FILE_LOCK_NOWAIT, lockfile, _("dpkg status database")); } @@ -241,8 +259,11 @@ { /* Unlock. */ pop_cleanup(ehflag_normaltidy); + if (frontendlockfd != -1) + pop_cleanup(ehflag_normaltidy); dblockfd = -1; + frontendlockfd = -1; } enum modstatdb_rw diff -Nru dpkg-1.19.0.5ubuntu2/lib/dpkg/dpkg.h dpkg-1.19.0.5ubuntu2.4/lib/dpkg/dpkg.h --- dpkg-1.19.0.5ubuntu2/lib/dpkg/dpkg.h 2018-01-13 15:03:05.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/lib/dpkg/dpkg.h 2019-09-05 21:05:14.000000000 +0000 @@ -78,6 +78,7 @@ #define STATUSFILE "status" #define AVAILFILE "available" #define LOCKFILE "lock" +#define FRONTENDLOCKFILE "lock-frontend" #define DIVERSIONSFILE "diversions" #define STATOVERRIDEFILE "statoverride" #define UPDATESDIR "updates/" diff -Nru dpkg-1.19.0.5ubuntu2/man/dpkg.man dpkg-1.19.0.5ubuntu2.4/man/dpkg.man --- dpkg-1.19.0.5ubuntu2/man/dpkg.man 2017-11-02 22:28:26.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/man/dpkg.man 2019-09-05 21:05:14.000000000 +0000 @@ -888,6 +888,10 @@ Sets the color mode (since dpkg 1.18.5). The currently accepted values are: \fBauto\fP (default), \fBalways\fP and \fBnever\fP. +.TP +.B DPKG_FRONTEND_LOCKED +Set by a package manager frontend to notify dpkg that it should not acquire +the frontend lock (since dpkg 1.19.1). .SS Internal environment .TP .B DPKG_ROOT diff -Nru dpkg-1.19.0.5ubuntu2/scripts/Dpkg/Source/Archive.pm dpkg-1.19.0.5ubuntu2.4/scripts/Dpkg/Source/Archive.pm --- dpkg-1.19.0.5ubuntu2/scripts/Dpkg/Source/Archive.pm 2017-11-02 22:28:26.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/scripts/Dpkg/Source/Archive.pm 2022-05-25 11:14:53.000000000 +0000 @@ -21,9 +21,11 @@ our $VERSION = '0.01'; use Carp; +use Errno qw(ENOENT); use File::Temp qw(tempdir); use File::Basename qw(basename); use File::Spec; +use File::Find; use Cwd; use Dpkg (); @@ -110,19 +112,13 @@ my %spawn_opts = (wait_child => 1); # Prepare destination - my $tmp; - if ($opts{in_place}) { - $spawn_opts{chdir} = $dest; - $tmp = $dest; # So that fixperms call works - } else { - my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; - unless (-e $dest) { - # Kludge so that realpath works - mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); - } - $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); - $spawn_opts{chdir} = $tmp; + my $template = basename($self->get_filename()) . '.tmp-extract.XXXXX'; + unless (-e $dest) { + # Kludge so that realpath works + mkdir($dest) or syserr(g_('cannot create directory %s'), $dest); } + my $tmp = tempdir($template, DIR => Cwd::realpath("$dest/.."), CLEANUP => 1); + $spawn_opts{chdir} = $tmp; # Prepare stuff that handles the input of tar $self->ensure_open('r', delete_sig => [ 'PIPE' ]); @@ -145,22 +141,94 @@ # have to be calculated using mount options and other madness. fixperms($tmp) unless $opts{no_fixperms}; - # Stop here if we extracted in-place as there's nothing to move around - return if $opts{in_place}; - - # Rename extracted directory - opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); - my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); - closedir($dir_dh); - my $done = 0; - erasedir($dest); - if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { - rename("$tmp/$entries[0]", $dest) - or syserr(g_('unable to rename %s to %s'), - "$tmp/$entries[0]", $dest); + # If we are extracting "in-place" do not remove the destination directory. + if ($opts{in_place}) { + my $canon_basedir = Cwd::realpath($dest); + # On Solaris /dev/null points to /devices/pseudo/mm@0:null. + my $canon_devnull = Cwd::realpath('/dev/null'); + my $check_symlink = sub { + my $pathname = shift; + my $canon_pathname = Cwd::realpath($pathname); + if (not defined $canon_pathname) { + return if $! == ENOENT; + + syserr(g_("pathname '%s' cannot be canonicalized"), $pathname); + } + return if $canon_pathname eq $canon_devnull; + return if $canon_pathname eq $canon_basedir; + return if $canon_pathname =~ m{^\Q$canon_basedir/\E}; + warning(g_("pathname '%s' points outside source root (to '%s')"), + $pathname, $canon_pathname); + }; + + my $move_in_place = sub { + my $relpath = File::Spec->abs2rel($File::Find::name, $tmp); + my $destpath = File::Spec->catfile($dest, $relpath); + + my ($mode, $atime, $mtime); + lstat $File::Find::name + or syserr(g_('cannot get source pathname %s metadata'), $File::Find::name); + ((undef) x 2, $mode, (undef) x 5, $atime, $mtime) = lstat _; + my $src_is_dir = -d _; + + my $dest_exists = 1; + if (not lstat $destpath) { + if ($! == ENOENT) { + $dest_exists = 0; + } else { + syserr(g_('cannot get target pathname %s metadata'), $destpath); + } + } + my $dest_is_dir = -d _; + if ($dest_exists) { + if ($dest_is_dir && $src_is_dir) { + # Refresh the destination directory attributes with the + # ones from the tarball. + chmod $mode, $destpath + or syserr(g_('cannot change directory %s mode'), $File::Find::name); + utime $atime, $mtime, $destpath + or syserr(g_('cannot change directory %s times'), $File::Find::name); + + # We should do nothing, and just walk further tree. + return; + } elsif ($dest_is_dir) { + rmdir $destpath + or syserr(g_('cannot remove destination directory %s'), $destpath); + } else { + $check_symlink->($destpath); + unlink $destpath + or syserr(g_('cannot remove destination file %s'), $destpath); + } + } + # If we are moving a directory, we do not need to walk it. + if ($src_is_dir) { + $File::Find::prune = 1; + } + rename $File::Find::name, $destpath + or syserr(g_('cannot move %s to %s'), $File::Find::name, $destpath); + }; + + find({ + wanted => $move_in_place, + no_chdir => 1, + dangling_symlinks => 0, + }, $tmp); } else { - rename($tmp, $dest) - or syserr(g_('unable to rename %s to %s'), $tmp, $dest); + # Rename extracted directory + opendir(my $dir_dh, $tmp) or syserr(g_('cannot opendir %s'), $tmp); + my @entries = grep { $_ ne '.' && $_ ne '..' } readdir($dir_dh); + closedir($dir_dh); + + erasedir($dest); + + if (scalar(@entries) == 1 && ! -l "$tmp/$entries[0]" && -d _) { + rename("$tmp/$entries[0]", $dest) + or syserr(g_('unable to rename %s to %s'), + "$tmp/$entries[0]", $dest); + } else { + rename($tmp, $dest) + or syserr(g_('unable to rename %s to %s'), $tmp, $dest); + } } erasedir($tmp); } diff -Nru dpkg-1.19.0.5ubuntu2/scripts/t/Dpkg_Source_Archive.t dpkg-1.19.0.5ubuntu2.4/scripts/t/Dpkg_Source_Archive.t --- dpkg-1.19.0.5ubuntu2/scripts/t/Dpkg_Source_Archive.t 2017-10-17 02:12:13.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/scripts/t/Dpkg_Source_Archive.t 2022-05-25 11:14:56.000000000 +0000 @@ -16,12 +16,120 @@ use strict; use warnings; -use Test::More tests => 1; +use Test::More tests => 4; +use Test::Dpkg qw(:paths); + +use File::Spec; +use File::Path qw(make_path rmtree); BEGIN { use_ok('Dpkg::Source::Archive'); } +use Dpkg; + +my $tmpdir = 't.tmp/Dpkg_Source_Archive'; + +rmtree($tmpdir); + +sub test_touch +{ + my ($name, $data) = @_; + + open my $fh, '>', $name + or die "cannot touch file $name\n"; + print { $fh } $data if $data; + close $fh; +} + +sub test_path_escape +{ + my $name = shift; + + my $treedir = File::Spec->rel2abs("$tmpdir/$name-tree"); + my $overdir = File::Spec->rel2abs("$tmpdir/$name-overlay"); + my $outdir = "$tmpdir/$name-out"; + my $expdir = "$tmpdir/$name-exp"; + + # This is the base directory, where we are going to be extracting stuff + # into, which include traps. + make_path("$treedir/subdir-a"); + test_touch("$treedir/subdir-a/file-a"); + test_touch("$treedir/subdir-a/file-pre-a"); + make_path("$treedir/subdir-b"); + test_touch("$treedir/subdir-b/file-b"); + test_touch("$treedir/subdir-b/file-pre-b"); + symlink File::Spec->abs2rel($outdir, $treedir), "$treedir/symlink-escape"; + symlink File::Spec->abs2rel("$outdir/nonexistent", $treedir), "$treedir/symlink-nonexistent"; + symlink "$treedir/file", "$treedir/symlink-within"; + test_touch("$treedir/supposed-dir"); + + # This is the overlay directory, which we'll pack and extract over the + # base directory. + make_path($overdir); + make_path("$overdir/subdir-a/aa"); + test_touch("$overdir/subdir-a/aa/file-aa", 'aa'); + test_touch("$overdir/subdir-a/file-a", 'a'); + make_path("$overdir/subdir-b/bb"); + test_touch("$overdir/subdir-b/bb/file-bb", 'bb'); + test_touch("$overdir/subdir-b/file-b", 'b'); + make_path("$overdir/symlink-escape"); + test_touch("$overdir/symlink-escape/escaped-file", 'escaped'); + test_touch("$overdir/symlink-nonexistent", 'nonexistent'); + make_path("$overdir/symlink-within"); + make_path("$overdir/supposed-dir"); + test_touch("$overdir/supposed-dir/supposed-file", 'something'); + + # Generate overlay tar. + system($Dpkg::PROGTAR, '-cf', "$overdir.tar", '-C', $overdir, qw( + subdir-a subdir-b + symlink-escape/escaped-file symlink-nonexistent symlink-within + supposed-dir + )) == 0 + or die "cannot create overlay tar archive\n"; + + # This is the expected directory, which we'll be comparing against. + make_path($expdir); + system('cp', '-a', $overdir, $expdir) == 0 + or die "cannot copy overlay hierarchy into expected directory\n"; + + # Store the expected and out reference directories into a tar to compare + # its structure against the result reference. + system($Dpkg::PROGTAR, '-cf', "$expdir.tar", '-C', $overdir, qw( + subdir-a subdir-b + symlink-escape/escaped-file symlink-nonexistent symlink-within + supposed-dir + ), '-C', $treedir, qw( + subdir-a/file-pre-a + subdir-b/file-pre-b + )) == 0 + or die "cannot create expected tar archive\n"; + + # This directory is supposed to remain empty, anything inside implies a + # directory traversal. + make_path($outdir); + + my $warnseen; + local $SIG{__WARN__} = sub { $warnseen = $_[0] }; + + # Perform the extraction. + my $tar = Dpkg::Source::Archive->new(filename => "$overdir.tar"); + $tar->extract($treedir, in_place => 1); + + # Store the result into a tar to compare its structure against a reference. + system($Dpkg::PROGTAR, '-cf', "$treedir.tar", '-C', $treedir, '.'); + + # Check results + ok(length $warnseen && $warnseen =~ m/points outside source root/, + 'expected warning seen'); + ok(system($Dpkg::PROGTAR, '--compare', '-f', "$expdir.tar", '-C', $treedir) == 0, + 'expected directory matches'); + ok(! -e "$outdir/escaped-file", + 'expected output directory is empty, directory traversal'); +} + +test_path_escape('in-place'); + # TODO: Add actual test cases. 1; diff -Nru dpkg-1.19.0.5ubuntu2/src/configure.c dpkg-1.19.0.5ubuntu2.4/src/configure.c --- dpkg-1.19.0.5ubuntu2/src/configure.c 2018-01-13 03:37:46.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/src/configure.c 2019-09-05 21:05:14.000000000 +0000 @@ -599,7 +599,7 @@ vdew_nonambig)); } - if (dependtry > 1) + if (dependtry >= DEPEND_TRY_CYCLES) if (findbreakcycle(pkg)) sincenothing = 0; diff -Nru dpkg-1.19.0.5ubuntu2/src/main.h dpkg-1.19.0.5ubuntu2.4/src/main.h --- dpkg-1.19.0.5ubuntu2/src/main.h 2017-11-02 22:28:26.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/src/main.h 2019-09-05 21:05:14.000000000 +0000 @@ -226,7 +226,53 @@ void deferred_remove(struct pkginfo *pkg); void deferred_configure(struct pkginfo *pkg); -extern int sincenothing, dependtry; +/* + * During the packages queue processing, the algorithm for deciding what to + * configure first is as follows: + * + * Loop through all packages doing a ‘try 1’ until we've been round and + * nothing has been done, then do ‘try 2’, and subsequent ones likewise. + * The incrementing of ‘dependtry’ is done by process_queue(). + * + * Try 1: + * Are all dependencies of this package done? If so, do it. + * Are any of the dependencies missing or the wrong version? + * If so, abort (unless --force-depends, in which case defer). + * Will we need to configure a package we weren't given as an + * argument? If so, abort ─ except if --force-configure-any, + * in which case we add the package to the argument list. + * If none of the above, defer the package. + * + * Try 2: + * Find a cycle and break it (see above). + * Do as for try 1. + * + * Try 3: + * Start processing triggers if necessary. + * Do as for try 2. + * + * Try 4: + * Same as for try 3, but check trigger cycles even when deferring + * processing due to unsatisfiable dependencies. + * + * Try 5 (only if --force-depends-version): + * Same as for try 2, but don't mind version number in dependencies. + * + * Try 6 (only if --force-depends): + * Do anyway. + */ +enum dependtry { + DEPEND_TRY_NORMAL = 1, + DEPEND_TRY_CYCLES = 2, + DEPEND_TRY_TRIGGERS = 3, + DEPEND_TRY_TRIGGERS_CYCLES = 4, + DEPEND_TRY_FORCE_DEPENDS_VERSION = 5, + DEPEND_TRY_FORCE_DEPENDS = 6, + DEPEND_TRY_LAST, +}; + +extern enum dependtry dependtry; +extern int sincenothing; /* from cleanup.c (most of these are declared in archives.h) */ @@ -290,8 +336,10 @@ /* from trigproc.c */ enum trigproc_type { - /** Opportunistic trigger processing. */ - TRIGPROC_TRY, + /** Opportunistic deferred trigger processing. */ + TRIGPROC_TRY_DEFERRED, + /** Opportunistic queued trigger processing. */ + TRIGPROC_TRY_QUEUED, /** Required trigger processing. */ TRIGPROC_REQUIRED, }; diff -Nru dpkg-1.19.0.5ubuntu2/src/packages.c dpkg-1.19.0.5ubuntu2.4/src/packages.c --- dpkg-1.19.0.5ubuntu2/src/packages.c 2017-11-02 22:28:26.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/src/packages.c 2019-09-05 21:05:14.000000000 +0000 @@ -50,7 +50,8 @@ static struct pkginfo *progress_bytrigproc; static struct pkg_queue queue = PKG_QUEUE_INIT; -int sincenothing = 0, dependtry = 1; +enum dependtry dependtry = DEPEND_TRY_NORMAL; +int sincenothing = 0; void enqueue_package(struct pkginfo *pkg) @@ -232,17 +233,22 @@ * trigger processing, w/o jumping into the next dependtry. */ dependtry++; sincenothing = 0; - assert(dependtry <= 4); + if (dependtry >= DEPEND_TRY_LAST) + internerr("exceeded dependtry %d (sincenothing=%d; queue.length=%d)", + dependtry, sincenothing, queue.length); } else if (sincenothing > queue.length * 2 + 2) { - /* XXX: This probably needs moving into a new dependtry instead. */ - if (progress_bytrigproc && progress_bytrigproc->trigpend_head) { + if (dependtry >= DEPEND_TRY_TRIGGERS && + progress_bytrigproc && progress_bytrigproc->trigpend_head) { enqueue_package(pkg); pkg = progress_bytrigproc; + progress_bytrigproc = NULL; action_todo = act_configure; } else { dependtry++; sincenothing = 0; - assert(dependtry <= 4); + if (dependtry >= DEPEND_TRY_LAST) + internerr("exceeded dependtry %d (sincenothing=%d, queue.length=%d)", + dependtry, sincenothing, queue.length); } } @@ -280,7 +286,7 @@ case act_configure: /* Do whatever is most needed. */ if (pkg->trigpend_head) - trigproc(pkg, TRIGPROC_REQUIRED); + trigproc(pkg, TRIGPROC_TRY_QUEUED); else deferred_configure(pkg); break; @@ -342,6 +348,15 @@ FOUND_OK = 3, }; +static enum found_status +found_forced_on(enum dependtry dependtry_forced) +{ + if (dependtry >= dependtry_forced) + return FOUND_FORCED; + else + return FOUND_DEFER; +} + /* * Return values: * 0: cannot be satisfied. @@ -399,7 +414,7 @@ pkg_name(possdependee, pnaw_always), versiondescribe(&provider->version, vdew_nonambig)); if (fc_dependsversion) - thisf = (dependtry >= 3) ? FOUND_FORCED : FOUND_DEFER; + thisf = found_forced_on(DEPEND_TRY_FORCE_DEPENDS_VERSION); debug(dbg_depcondetail, " bad version"); goto unsuitable; } @@ -412,7 +427,7 @@ versiondescribe(&possdependee->installed.version, vdew_nonambig)); if (fc_dependsversion) - thisf = (dependtry >= 3) ? FOUND_FORCED : FOUND_DEFER; + thisf = found_forced_on(DEPEND_TRY_FORCE_DEPENDS_VERSION); debug(dbg_depcondetail, " bad version"); goto unsuitable; } @@ -676,7 +691,7 @@ if (thisf > found) found= thisf; } if (fc_depends) { - thisf = (dependtry >= 4) ? FOUND_FORCED : FOUND_DEFER; + thisf = found_forced_on(DEPEND_TRY_FORCE_DEPENDS); if (thisf > found) { found = thisf; debug(dbg_depcondetail, " rescued by force-depends, found %d", found); diff -Nru dpkg-1.19.0.5ubuntu2/src/remove.c dpkg-1.19.0.5ubuntu2.4/src/remove.c --- dpkg-1.19.0.5ubuntu2/src/remove.c 2018-01-13 03:37:46.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/src/remove.c 2019-09-05 21:05:14.000000000 +0000 @@ -71,7 +71,10 @@ pkg_name(depender, pnaw_always)); continue; } - if (dependtry > 1) { if (findbreakcycle(pkgtoremove)) sincenothing= 0; } + if (dependtry >= DEPEND_TRY_CYCLES) { + if (findbreakcycle(pkgtoremove)) + sincenothing = 0; + } varbuf_snapshot(raemsgs, &raemsgs_state); ok= dependencies_ok(depender,pkgtoremove,raemsgs); if (ok == DEP_CHECK_HALT && diff -Nru dpkg-1.19.0.5ubuntu2/src/trigproc.c dpkg-1.19.0.5ubuntu2.4/src/trigproc.c --- dpkg-1.19.0.5ubuntu2/src/trigproc.c 2017-11-02 22:28:26.000000000 +0000 +++ dpkg-1.19.0.5ubuntu2.4/src/trigproc.c 2019-09-05 21:05:14.000000000 +0000 @@ -49,10 +49,11 @@ * we add it to that queue (unless --no-triggers). * * - * We want to prefer configuring packages where possible to doing - * trigger processing, but we want to prefer trigger processing to - * cycle-breaking and dependency forcing. This is achieved as - * follows: + * We want to prefer configuring packages where possible to doing trigger + * processing, although it would be better to prefer trigger processing + * to cycle-breaking we need to do the latter first or we might generate + * artificial trigger cycles, but we always want to prefer trigger + * processing to dependency forcing. This is achieved as follows: * * Each time during configure processing where a package D is blocked by * only (ie Depends unsatisfied but would be satisfied by) a t-awaiter W @@ -60,10 +61,11 @@ * (If --no-triggers and nonempty argument list and package isn't in * argument list then we don't do this.) * - * Each time in packages.c where we increment dependtry, we instead see - * if we have encountered such a t-pending T. If we do, we trigproc T - * instead of incrementing dependtry and this counts as having done - * something so we reset sincenothing. + * Each time in process_queue() where we increment dependtry, we instead + * see if we have encountered such a t-pending T. If we do and are in + * a trigger processing try, we trigproc T instead of incrementing + * dependtry and this counts as having done something so we reset + * sincenothing. * * * For --triggers-only and --configure, we go through each thing in the @@ -162,7 +164,7 @@ pkg_name(pkg, pnaw_nonambig)); pkg->clientdata->trigprocdeferred = NULL; - trigproc(pkg, TRIGPROC_TRY); + trigproc(pkg, TRIGPROC_TRY_DEFERRED); pop_error_context(ehflag_normaltidy); } @@ -207,8 +209,8 @@ } static bool -tortoise_not_in_hare(struct pkginfo *processing_now, - struct trigcycleperpkg *tortoise_pkg) +tortoise_in_hare(struct pkginfo *processing_now, + struct trigcycleperpkg *tortoise_pkg) { const char *processing_now_name, *tortoise_name; struct trigpend *hare_trig, *tortoise_trig; @@ -241,31 +243,24 @@ /* Not found in hare, yay! */ debug(dbg_triggersdetail, "%s pnow=%s tortoise=%s OK", __func__, processing_now_name, tortoise_name); - return true; + return false; } } - return false; + return true; } -/* - * Returns package we're to give up on. - */ -static struct pkginfo * -check_trigger_cycle(struct pkginfo *processing_now) +static struct trigcyclenode * +trigproc_new_cyclenode(struct pkginfo *processing_now) { struct trigcyclenode *tcn; - struct trigcycleperpkg *tcpp, *tortoise_pkg; - struct trigpend *tortoise_trig; + struct trigcycleperpkg *tcpp; + struct pkginfo *pkg; struct pkgiterator *iter; - struct pkginfo *pkg, *giveup; - const char *sep; - - debug(dbg_triggers, "check_triggers_cycle pnow=%s", - pkg_name(processing_now, pnaw_always)); tcn = nfmalloc(sizeof(*tcn)); tcn->pkgs = NULL; + tcn->next = NULL; tcn->then_processed = processing_now; iter = pkg_db_iter_new(); @@ -279,15 +274,34 @@ tcn->pkgs = tcpp; } pkg_db_iter_free(iter); + + return tcn; +} + +/* + * Returns package we are to give up on. + */ +static struct pkginfo * +check_trigger_cycle(struct pkginfo *processing_now) +{ + struct trigcyclenode *tcn; + struct trigcycleperpkg *tortoise_pkg; + struct trigpend *tortoise_trig; + struct pkginfo *giveup; + const char *sep; + + debug(dbg_triggers, "check_triggers_cycle pnow=%s", + pkg_name(processing_now, pnaw_always)); + + tcn = trigproc_new_cyclenode(processing_now); + if (!hare) { debug(dbg_triggersdetail, "check_triggers_cycle pnow=%s first", pkg_name(processing_now, pnaw_always)); - tcn->next = NULL; hare = tortoise = tcn; return NULL; } - tcn->next = NULL; hare->next = tcn; hare = tcn; if (tortoise_advance) @@ -301,7 +315,7 @@ for (tortoise_pkg = tortoise->pkgs; tortoise_pkg; tortoise_pkg = tortoise_pkg->next) { - if (tortoise_not_in_hare(processing_now, tortoise_pkg)) + if (!tortoise_in_hare(processing_now, tortoise_pkg)) return NULL; } /* Oh dear. hare is a superset of tortoise. We are making no @@ -335,8 +349,12 @@ debug(dbg_triggers, "check_triggers_cycle pnow=%s giveup=%s", pkg_name(processing_now, pnaw_always), pkg_name(giveup, pnaw_always)); - assert(giveup->status == PKG_STAT_TRIGGERSAWAITED || - giveup->status == PKG_STAT_TRIGGERSPENDING); + if (giveup->status != PKG_STAT_TRIGGERSAWAITED && + giveup->status != PKG_STAT_TRIGGERSPENDING) + internerr("package %s in non-trigger state %s", + pkg_name(giveup, pnaw_always), + pkg_status_name(giveup)); + giveup->clientdata->istobe = PKG_ISTOBE_NORMAL; pkg_set_status(giveup, PKG_STAT_HALFCONFIGURED); modstatdb_note(giveup); print_error_perpackage(_("triggers looping, abandoned"), @@ -370,33 +388,42 @@ assert(pkg->status == PKG_STAT_TRIGGERSPENDING || pkg->status == PKG_STAT_TRIGGERSAWAITED); - if (dependtry > 1) { - gaveup = check_trigger_cycle(pkg); - if (gaveup == pkg) - return; + if (dependtry < DEPEND_TRY_TRIGGERS && + type == TRIGPROC_TRY_QUEUED) { + /* We are not yet in a triggers run, so postpone this + * package completely. */ + enqueue_package(pkg); + return; + } + if (dependtry >= DEPEND_TRY_CYCLES) { if (findbreakcycle(pkg)) sincenothing = 0; } ok = dependencies_ok(pkg, NULL, &depwhynot); if (ok == DEP_CHECK_DEFER) { + if (dependtry >= DEPEND_TRY_TRIGGERS_CYCLES) { + gaveup = check_trigger_cycle(pkg); + if (gaveup == pkg) + return; + } + varbuf_destroy(&depwhynot); enqueue_package(pkg); return; } else if (ok == DEP_CHECK_HALT) { - /* We cannot process this package on this dpkg run, - * and we can get here repeatedly if this package is - * required to make progress for other packages. So - * reset the trigger cycles tracking to avoid bogus - * cycle detections. */ - trigproc_reset_cycle(); - - /* When doing opportunistic trigger processing, nothing - * requires us to be able to make progress; skip the - * package and silently ignore the error due to - * unsatisfiable dependencies. */ - if (type == TRIGPROC_TRY) { + /* When doing opportunistic deferred trigger processing, + * nothing requires us to be able to make progress; + * skip the package and silently ignore the error due + * to unsatisfiable dependencies. And because we can + * end up here repeatedly, if this package is required + * to make progress for other packages, we need to + * reset the trigger cycle tracking to avoid detecting + * bogus cycles*/ + if (type == TRIGPROC_TRY_DEFERRED) { + trigproc_reset_cycle(); + varbuf_destroy(&depwhynot); return; } @@ -416,11 +443,9 @@ varbuf_destroy(&depwhynot); } - if (dependtry <= 1) { - gaveup = check_trigger_cycle(pkg); - if (gaveup == pkg) - return; - } + gaveup = check_trigger_cycle(pkg); + if (gaveup == pkg) + return; printf(_("Processing triggers for %s (%s) ...\n"), pkg_name(pkg, pnaw_nonambig),