File dpkg-CVE-2022-1664.patch of Package update-alternatives.24405

diff --git a/scripts/Dpkg/Source/Archive.pm b/scripts/Dpkg/Source/Archive.pm
index 33c181b20..2ddd04af8 100644
--- a/scripts/Dpkg/Source/Archive.pm
+++ b/scripts/Dpkg/Source/Archive.pm
@@ -21,9 +21,11 @@ use warnings;
 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 @@ sub extract {
     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 @@ sub extract {
     # 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 --git a/scripts/t/Dpkg_Source_Archive.t b/scripts/t/Dpkg_Source_Archive.t
index 7b70da68e..09496aeea 100644
--- a/scripts/t/Dpkg_Source_Archive.t
+++ b/scripts/t/Dpkg_Source_Archive.t
@@ -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;
openSUSE Build Service is sponsored by