Date: Mon, 1 Dec 2008 02:03:50 +0300 (MSK) From: Eygene Ryabinkin <rea-fbsd@codelabs.ru> To: FreeBSD-gnats-submit@freebsd.org Cc: freebsd-vuxml@freebsd.org, secteam@freebsd.org Subject: [vuxml] [patch] lang/perl5.8: document and fix CVE-2005-0448 Message-ID: <20081130230350.96F5DB8019@phoenix.codelabs.ru>
next in thread | raw e-mail | index | archive | help
>Submitter-Id: current-users >Originator: Eygene Ryabinkin >Organization: Code Labs >Confidential: no >Synopsis: [vuxml] [patch] lang/perl5.8: document and fix CVE-2005-0448 >Severity: critical >Priority: high >Category: ports >Class: sw-bug >Release: FreeBSD 7.1-PRERELEASE amd64 >Environment: System: FreeBSD 7.1-PRERELEASE amd64 >Description: As was recently discovered, perl 5.8.8 has missed the fix for CVE-2005-0448: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905#85 http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286922#36 >How-To-Repeat: Look at the above URLs. Use the following scripts (packed in shar archive) to check the issue. Prerequisites for testing: directory '/pr' owned by root and two files inside it, 'passwd' and 'sh', both owned by root too. The invocation sequence is simple. 1. spawn 'prepare-XXX.sh' as ordinary user: this will prepare the directory structure; 2. from the same directory run as root 'perl rmtree.pl'; 3. wait for a couple of milliseconds and spawn 'spawn-XXX.sh' as ordinary user. --- vuln-test.shar begins here --- # This is a shell archive. Save it in a file, remove anything before # this line, and then unpack it by entering "sh file". Note, it may # create directories; files and directories will be owned by you and # have default permissions. # # This archive contains: # # vuln-test/prepare-setuid.sh # vuln-test/prepare-unlink.sh # vuln-test/rmtree.pl # vuln-test/spawn-setuid.sh # vuln-test/spawn-unlink.sh # echo x - vuln-test/prepare-setuid.sh sed 's/^X//' >vuln-test/prepare-setuid.sh << '9c54eac5eb535fdf6f93fce30544605e' X#!/bin/sh X# Prepares directory for testing of CVE-2005-0448. X Xmkdir -p hier/sh Xperl -e 'open F, ">hier/sh/$_" foreach (1..100000);' Xchmod 4755 hier/sh 9c54eac5eb535fdf6f93fce30544605e echo x - vuln-test/prepare-unlink.sh sed 's/^X//' >vuln-test/prepare-unlink.sh << '42f366cf566b8c56fa9dedf27d4c4eae' X#!/bin/sh X# Prepares directory for testing of CVE-2005-0448. X Xmkdir -p hier/d Xperl -e 'open F, ">hier/d/$_" foreach (1..100000);' Xtouch hier/passwd 42f366cf566b8c56fa9dedf27d4c4eae echo x - vuln-test/rmtree.pl sed 's/^X//' >vuln-test/rmtree.pl << '278957334ec6138c45b1bda9645cdb05' X#!/usr/bin/perl -w X Xuse strict; X Xuse File::Path; X Xrmtree("hier", 1, 0); 278957334ec6138c45b1bda9645cdb05 echo x - vuln-test/spawn-setuid.sh sed 's/^X//' >vuln-test/spawn-setuid.sh << 'cccb5451c4bca28aff1735483a5e1028' X#!/bin/sh X Xmv hier/sh hier/sh.old && ln -s /pr/sh hier/sh cccb5451c4bca28aff1735483a5e1028 echo x - vuln-test/spawn-unlink.sh sed 's/^X//' >vuln-test/spawn-unlink.sh << '92cf7ebfb6f9cb94b8ce3012299a6342' X#!/bin/sh X Xmv hier h && ln -s /pr hier 92cf7ebfb6f9cb94b8ce3012299a6342 exit --- vuln-test.shar ends here --- >Fix: The following patch fixes the issue by updating the File::Path to the most recent version (see ports/129301 for an overview about p5-File-Path): --- perl5.8-fix-CVE-2005-0448-and-related-ones.diff begins here --- >From e70588395eea5f069f7c386d728cae88b413cb51 Mon Sep 17 00:00:00 2001 From: Eygene Ryabinkin <rea-fbsd@codelabs.ru> Date: Mon, 1 Dec 2008 01:22:24 +0300 It was discovered that CVE-2005-0448 was resurrected in perl 5.8.8: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905 http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286922 The fixes in bug 286905 are either for 5.10.0 or made for Debian, so it is not very clear if they will properly work on FreeBSD. Meanwhile, File::Path 2.07 is here and it fixes almost everything (or at least seem to fix ;)). So my recipe is the following: - take Path.pm from FreeBSD's devel/p5-File-Path 2.07_1 (ports/129301); - take Path.t from 5.10.0 (it really does matter only for 'make test' or simular, 5.10.0 should be fine for this); - implant these two into our Perl 5.8.8. Signed-off-by: Eygene Ryabinkin <rea-fbsd@codelabs.ru> --- lang/perl5.8/Makefile | 2 +- lang/perl5.8/files/patch-lib::File::Path | 1558 ++++++++++++++++++++++++++++++ 2 files changed, 1559 insertions(+), 1 deletions(-) create mode 100644 lang/perl5.8/files/patch-lib::File::Path diff --git a/lang/perl5.8/Makefile b/lang/perl5.8/Makefile index 7252ed7..e92bca5 100644 --- a/lang/perl5.8/Makefile +++ b/lang/perl5.8/Makefile @@ -7,7 +7,7 @@ PORTNAME= perl PORTVERSION= ${PERL_VER} -PORTREVISION= 1 +PORTREVISION= 2 CATEGORIES= lang devel perl5 MASTER_SITES= CPAN \ ${MASTER_SITE_LOCAL:S/$/:local/} \ diff --git a/lang/perl5.8/files/patch-lib::File::Path b/lang/perl5.8/files/patch-lib::File::Path new file mode 100644 index 0000000..a37ed11 --- /dev/null +++ b/lang/perl5.8/files/patch-lib::File::Path @@ -0,0 +1,1558 @@ +This one fixes CVE-2005-0448 and related, see + http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905 + http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286922 + +The method is simple: I had implanted File::Path.pm from the FreeBSD's +devel/p5-File-Path, version 2.07_1 (additionally patched version 2.07, +see ports/129301). File::Path.t was taken from perl-5.10.0. + +diff --git a/lib/File/Path.pm b/lib/File/Path.pm +index 2e41ff3..f38d242 100644 +--- lib/File/Path.pm ++++ lib/File/Path.pm +@@ -1,285 +1,869 @@ + package File::Path; + ++use 5.005_04; ++use strict; ++ ++use Cwd 'getcwd'; ++use File::Basename (); ++use File::Spec (); ++ ++BEGIN { ++ if ($] < 5.006) { ++ # can't say 'opendir my $dh, $dirname' ++ # need to initialise $dh ++ eval "use Symbol"; ++ } ++} ++ ++use Exporter (); ++use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); ++$VERSION = '2.07'; ++@ISA = qw(Exporter); ++@EXPORT = qw(mkpath rmtree); ++@EXPORT_OK = qw(make_path remove_tree); ++ ++my $Is_VMS = $^O eq 'VMS'; ++my $Is_MacOS = $^O eq 'MacOS'; ++ ++# These OSes complain if you want to remove a file that you have no ++# write permission to: ++my $Force_Writeable = grep {$^O eq $_} qw(amigaos dos epoc MSWin32 MacOS os2); ++ ++sub _carp { ++ require Carp; ++ goto &Carp::carp; ++} ++ ++sub _croak { ++ require Carp; ++ goto &Carp::croak; ++} ++ ++sub _error { ++ my $arg = shift; ++ my $message = shift; ++ my $object = shift; ++ ++ if ($arg->{error}) { ++ $object = '' unless defined $object; ++ $message .= ": $!" if $!; ++ push @{${$arg->{error}}}, {$object => $message}; ++ } ++ else { ++ _carp(defined($object) ? "$message for $object: $!" : "$message: $!"); ++ } ++} ++ ++sub make_path { ++ push @_, {} unless @_ and UNIVERSAL::isa($_[-1],'HASH'); ++ goto &mkpath; ++} ++ ++sub mkpath { ++ my $old_style = !(@_ and UNIVERSAL::isa($_[-1],'HASH')); ++ ++ my $arg; ++ my $paths; ++ ++ if ($old_style) { ++ my ($verbose, $mode); ++ ($paths, $verbose, $mode) = @_; ++ $paths = [$paths] unless UNIVERSAL::isa($paths,'ARRAY'); ++ $arg->{verbose} = $verbose; ++ $arg->{mode} = defined $mode ? $mode : 0777; ++ } ++ else { ++ $arg = pop @_; ++ $arg->{mode} = delete $arg->{mask} if exists $arg->{mask}; ++ $arg->{mode} = 0777 unless exists $arg->{mode}; ++ ${$arg->{error}} = [] if exists $arg->{error}; ++ $paths = [@_]; ++ } ++ return _mkpath($arg, $paths); ++} ++ ++sub _mkpath { ++ my $arg = shift; ++ my $paths = shift; ++ ++ my(@created,$path); ++ foreach $path (@$paths) { ++ next unless defined($path) and length($path); ++ $path .= '/' if $^O eq 'os2' and $path =~ /^\w:\z/s; # feature of CRT ++ # Logic wants Unix paths, so go with the flow. ++ if ($Is_VMS) { ++ next if $path eq '/'; ++ $path = VMS::Filespec::unixify($path); ++ } ++ next if -d $path; ++ my $parent = File::Basename::dirname($path); ++ unless (-d $parent or $path eq $parent) { ++ push(@created,_mkpath($arg, [$parent])); ++ } ++ print "mkdir $path\n" if $arg->{verbose}; ++ if (mkdir($path,$arg->{mode})) { ++ push(@created, $path); ++ } ++ else { ++ my $save_bang = $!; ++ my ($e, $e1) = ($save_bang, $^E); ++ $e .= "; $e1" if $e ne $e1; ++ # allow for another process to have created it meanwhile ++ if (!-d $path) { ++ $! = $save_bang; ++ if ($arg->{error}) { ++ push @{${$arg->{error}}}, {$path => $e}; ++ } ++ else { ++ _croak("mkdir $path: $e"); ++ } ++ } ++ } ++ } ++ return @created; ++} ++ ++sub remove_tree { ++ push @_, {} unless @_ and UNIVERSAL::isa($_[-1],'HASH'); ++ goto &rmtree; ++} ++ ++sub rmtree { ++ my $old_style = !(@_ and UNIVERSAL::isa($_[-1],'HASH')); ++ ++ my $arg; ++ my $paths; ++ ++ if ($old_style) { ++ my ($verbose, $safe); ++ ($paths, $verbose, $safe) = @_; ++ $arg->{verbose} = $verbose; ++ $arg->{safe} = defined $safe ? $safe : 0; ++ ++ if (defined($paths) and length($paths)) { ++ $paths = [$paths] unless UNIVERSAL::isa($paths,'ARRAY'); ++ } ++ else { ++ _carp ("No root path(s) specified\n"); ++ return 0; ++ } ++ } ++ else { ++ $arg = pop @_; ++ ${$arg->{error}} = [] if exists $arg->{error}; ++ ${$arg->{result}} = [] if exists $arg->{result}; ++ $paths = [@_]; ++ } ++ ++ $arg->{prefix} = ''; ++ $arg->{depth} = 0; ++ ++ my @clean_path; ++ $arg->{cwd} = getcwd() or do { ++ _error($arg, "cannot fetch initial working directory"); ++ return 0; ++ }; ++ for ($arg->{cwd}) { /\A(.*)\Z/; $_ = $1 } # untaint ++ ++ for my $p (@$paths) { ++ # need to fixup case and map \ to / on Windows ++ my $ortho_root = $^O eq 'MSWin32' ? _slash_lc($p) : $p; ++ my $ortho_cwd = $^O eq 'MSWin32' ? _slash_lc($arg->{cwd}) : $arg->{cwd}; ++ my $ortho_root_length = length($ortho_root); ++ $ortho_root_length-- if $^O eq 'VMS'; # don't compare '.' with ']' ++ if ($ortho_root_length ++ && (substr($ortho_root, 0, $ortho_root_length) ++ eq substr($ortho_cwd, 0, $ortho_root_length))) { ++ local $! = 0; ++ _error($arg, "cannot remove path when cwd is $arg->{cwd}", $p); ++ next; ++ } ++ ++ if ($Is_MacOS) { ++ $p = ":$p" unless $p =~ /:/; ++ $p .= ":" unless $p =~ /:\z/; ++ } ++ elsif ($^O eq 'MSWin32') { ++ $p =~ s{[/\\]\z}{}; ++ } ++ else { ++ $p =~ s{/\z}{}; ++ } ++ push @clean_path, $p; ++ } ++ ++ @{$arg}{qw(device inode perm)} = (lstat $arg->{cwd})[0,1] or do { ++ _error($arg, "cannot stat initial working directory", $arg->{cwd}); ++ return 0; ++ }; ++ ++ return _rmtree($arg, \@clean_path); ++} ++ ++sub _rmtree { ++ my $arg = shift; ++ my $paths = shift; ++ ++ my $count = 0; ++ my $curdir = File::Spec->curdir(); ++ my $updir = File::Spec->updir(); ++ ++ my (@files, $root); ++ ROOT_DIR: ++ foreach $root (@$paths) { ++ # since we chdir into each directory, it may not be obvious ++ # to figure out where we are if we generate a message about ++ # a file name. We therefore construct a semi-canonical ++ # filename, anchored from the directory being unlinked (as ++ # opposed to being truly canonical, anchored from the root (/). ++ ++ my $canon = $arg->{prefix} ++ ? File::Spec->catfile($arg->{prefix}, $root) ++ : $root ++ ; ++ ++ my ($ldev, $lino, $perm) = (lstat $root)[0,1,2] or next ROOT_DIR; ++ ++ if ( -d _ ) { ++ $root = VMS::Filespec::pathify($root) if $Is_VMS; ++ if (!chdir($root)) { ++ # see if we can escalate privileges to get in ++ # (e.g. funny protection mask such as -w- instead of rwx) ++ $perm &= 07777; ++ my $nperm = $perm | 0700; ++ if (!($arg->{safe} or $nperm == $perm or chmod($nperm, $root))) { ++ _error($arg, "cannot make child directory read-write-exec", $canon); ++ next ROOT_DIR; ++ } ++ elsif (!chdir($root)) { ++ _error($arg, "cannot chdir to child", $canon); ++ next ROOT_DIR; ++ } ++ } ++ ++ my ($cur_dev, $cur_inode, $perm) = (stat $curdir)[0,1,2] or do { ++ _error($arg, "cannot stat current working directory", $canon); ++ next ROOT_DIR; ++ }; ++ ++ ($ldev eq $cur_dev and $lino eq $cur_inode) ++ or _croak("directory $canon changed before chdir, expected dev=$ldev ino=$lino, actual dev=$cur_dev ino=$cur_inode, aborting."); ++ ++ $perm &= 07777; # don't forget setuid, setgid, sticky bits ++ my $nperm = $perm | 0700; ++ ++ # notabene: 0700 is for making readable in the first place, ++ # it's also intended to change it to writable in case we have ++ # to recurse in which case we are better than rm -rf for ++ # subtrees with strange permissions ++ ++ if (!($arg->{safe} or $nperm == $perm or chmod($nperm, $curdir))) { ++ _error($arg, "cannot make directory read+writeable", $canon); ++ $nperm = $perm; ++ } ++ ++ my $d; ++ $d = gensym() if $] < 5.006; ++ if (!opendir $d, $curdir) { ++ _error($arg, "cannot opendir", $canon); ++ @files = (); ++ } ++ else { ++ no strict 'refs'; ++ if (!defined ${"\cTAINT"} or ${"\cTAINT"}) { ++ # Blindly untaint dir names if taint mode is ++ # active, or any perl < 5.006 ++ @files = map { /\A(.*)\z/s; $1 } readdir $d; ++ } ++ else { ++ @files = readdir $d; ++ } ++ closedir $d; ++ } ++ ++ if ($Is_VMS) { ++ # Deleting large numbers of files from VMS Files-11 ++ # filesystems is faster if done in reverse ASCIIbetical order. ++ # include '.' to '.;' from blead patch #31775 ++ @files = map {$_ eq '.' ? '.;' : $_} reverse @files; ++ ($root = VMS::Filespec::unixify($root)) =~ s/\.dir\z//; ++ } ++ @files = grep {$_ ne $updir and $_ ne $curdir} @files; ++ ++ if (@files) { ++ # remove the contained files before the directory itself ++ my $narg = {%$arg}; ++ @{$narg}{qw(device inode cwd prefix depth)} ++ = ($cur_dev, $cur_inode, $updir, $canon, $arg->{depth}+1); ++ $count += _rmtree($narg, \@files); ++ } ++ ++ # restore directory permissions of required now (in case the rmdir ++ # below fails), while we are still in the directory and may do so ++ # without a race via '.' ++ if ($nperm != $perm and not chmod($perm, $curdir)) { ++ _error($arg, "cannot reset chmod", $canon); ++ } ++ ++ # don't leave the client code in an unexpected directory ++ chdir($arg->{cwd}) ++ or _croak("cannot chdir to $arg->{cwd} from $canon: $!, aborting."); ++ ++ # ensure that a chdir upwards didn't take us somewhere other ++ # than we expected (see CVE-2002-0435) ++ ($cur_dev, $cur_inode) = (stat $curdir)[0,1] ++ or _croak("cannot stat prior working directory $arg->{cwd}: $!, aborting."); ++ ++ ($arg->{device} eq $cur_dev and $arg->{inode} eq $cur_inode) ++ or _croak("previous directory $arg->{cwd} changed before entering $canon, expected dev=$ldev ino=$lino, actual dev=$cur_dev ino=$cur_inode, aborting."); ++ ++ if ($arg->{depth} or !$arg->{keep_root}) { ++ if ($arg->{safe} && ++ ($Is_VMS ? !&VMS::Filespec::candelete($root) : !-w $root)) { ++ print "skipped $root\n" if $arg->{verbose}; ++ next ROOT_DIR; ++ } ++ if ($Force_Writeable and !chmod $perm | 0700, $root) { ++ _error($arg, "cannot make directory writeable", $canon); ++ } ++ print "rmdir $root\n" if $arg->{verbose}; ++ if (rmdir $root) { ++ push @{${$arg->{result}}}, $root if $arg->{result}; ++ ++$count; ++ } ++ else { ++ _error($arg, "cannot remove directory", $canon); ++ if ($Force_Writeable && !chmod($perm, ($Is_VMS ? VMS::Filespec::fileify($root) : $root)) ++ ) { ++ _error($arg, sprintf("cannot restore permissions to 0%o",$perm), $canon); ++ } ++ } ++ } ++ } ++ else { ++ # not a directory ++ $root = VMS::Filespec::vmsify("./$root") ++ if $Is_VMS ++ && !File::Spec->file_name_is_absolute($root) ++ && ($root !~ m/(?<!\^)[\]>]+/); # not already in VMS syntax ++ ++ if ($arg->{safe} && ++ ($Is_VMS ? !&VMS::Filespec::candelete($root) ++ : !(-l $root || -w $root))) ++ { ++ print "skipped $root\n" if $arg->{verbose}; ++ next ROOT_DIR; ++ } ++ ++ my $nperm = $perm & 07777 | 0600; ++ if ($Force_Writeable and $nperm != $perm and not chmod $nperm, $root) { ++ _error($arg, "cannot make file writeable", $canon); ++ } ++ print "unlink $canon\n" if $arg->{verbose}; ++ # delete all versions under VMS ++ for (;;) { ++ if (unlink $root) { ++ push @{${$arg->{result}}}, $root if $arg->{result}; ++ } ++ else { ++ _error($arg, "cannot unlink file", $canon); ++ $Force_Writeable and chmod($perm, $root) or ++ _error($arg, sprintf("cannot restore permissions to 0%o",$perm), $canon); ++ last; ++ } ++ ++$count; ++ last unless $Is_VMS && lstat $root; ++ } ++ } ++ } ++ return $count; ++} ++ ++sub _slash_lc { ++ # fix up slashes and case on MSWin32 so that we can determine that ++ # c:\path\to\dir is underneath C:/Path/To ++ my $path = shift; ++ $path =~ tr{\\}{/}; ++ return lc($path); ++} ++ ++1; ++__END__ ++ + =head1 NAME + +-File::Path - create or remove directory trees ++File::Path - Create or remove directory trees ++ ++=head1 VERSION ++ ++This document describes version 2.07 of File::Path, released ++2008-11-09. + + =head1 SYNOPSIS + +- use File::Path; ++ use File::Path qw(make_path remove_tree); ++ ++ make_path('foo/bar/baz', '/zug/zwang'); ++ make_path('foo/bar/baz', '/zug/zwang', { ++ verbose => 1, ++ mode => 0711, ++ }); ++ ++ remove_tree('foo/bar/baz', '/zug/zwang'); ++ remove_tree('foo/bar/baz', '/zug/zwang', { ++ verbose => 1, ++ error => \my $err_list, ++ }); ++ ++ # legacy (interface promoted before v2.00) ++ mkpath('/foo/bar/baz'); ++ mkpath('/foo/bar/baz', 1, 0711); ++ mkpath(['/foo/bar/baz', 'blurfl/quux'], 1, 0711); ++ rmtree('foo/bar/baz', 1, 1); ++ rmtree(['foo/bar/baz', 'blurfl/quux'], 1, 1); + +- mkpath(['/foo/bar/baz', 'blurfl/quux'], 1, 0711); +- rmtree(['foo/bar/baz', 'blurfl/quux'], 1, 1); ++ # legacy (interface promoted before v2.06) ++ mkpath('foo/bar/baz', '/zug/zwang', { verbose => 1, mode => 0711 }); ++ rmtree('foo/bar/baz', '/zug/zwang', { verbose => 1, mode => 0711 }); + + =head1 DESCRIPTION + +-The C<mkpath> function provides a convenient way to create directories, even +-if your C<mkdir> kernel call won't create more than one level of directory at +-a time. C<mkpath> takes three arguments: ++This module provide a convenient way to create directories of ++arbitrary depth and to delete an entire directory subtree from the ++filesystem. + +-=over 4 ++The following functions are provided: + +-=item * ++=over + +-the name of the path to create, or a reference +-to a list of paths to create, ++=item make_path( $dir1, $dir2, .... ) + +-=item * ++=item make_path( $dir1, $dir2, ...., \%opts ) + +-a boolean value, which if TRUE will cause C<mkpath> +-to print the name of each directory as it is created +-(defaults to FALSE), and ++The C<make_path> function creates the given directories if they don't ++exists before, much like the Unix command C<mkdir -p>. + +-=item * ++The function accepts a list of directories to be created. Its ++behaviour may be tuned by an optional hashref appearing as the last ++parameter on the call. ++ ++The function returns the list of directories actually created during ++the call; in scalar context the number of directories created. ++ ++The following keys are recognised in the option hash: ++ ++=over ++ ++=item mode => $num ++ ++The numeric permissions mode to apply to each created directory ++(defaults to 0777), to be modified by the current C<umask>. If the ++directory already exists (and thus does not need to be created), ++the permissions will not be modified. ++ ++C<mask> is recognised as an alias for this parameter. ++ ++=item verbose => $bool ++ ++If present, will cause C<make_path> to print the name of each directory ++as it is created. By default nothing is printed. + +-the numeric mode to use when creating the directories +-(defaults to 0777), to be modified by the current umask. ++=item error => \$err ++ ++If present, it should be a reference to a scalar. ++This scalar will be made to reference an array, which will ++be used to store any errors that are encountered. See the L</"ERROR ++HANDLING"> section for more information. ++ ++If this parameter is not used, certain error conditions may raise ++a fatal error that will cause the program will halt, unless trapped ++in an C<eval> block. + + =back + +-It returns a list of all directories (including intermediates, determined +-using the Unix '/' separator) created. ++=item mkpath( $dir ) + +-If a system error prevents a directory from being created, then the +-C<mkpath> function throws a fatal error with C<Carp::croak>. This error +-can be trapped with an C<eval> block: ++=item mkpath( $dir, $verbose, $mode ) + +- eval { mkpath($dir) }; +- if ($@) { +- print "Couldn't create $dir: $@"; +- } ++=item mkpath( [$dir1, $dir2,...], $verbose, $mode ) + +-Similarly, the C<rmtree> function provides a convenient way to delete a +-subtree from the directory structure, much like the Unix command C<rm -r>. +-C<rmtree> takes three arguments: ++=item mkpath( $dir1, $dir2,..., \%opt ) + +-=over 4 ++The mkpath() function provide the legacy interface of make_path() with ++a different interpretation of the arguments passed. The behaviour and ++return value of the function is otherwise identical to make_path(). + +-=item * ++=item remove_tree( $dir1, $dir2, .... ) + +-the root of the subtree to delete, or a reference to +-a list of roots. All of the files and directories +-below each root, as well as the roots themselves, +-will be deleted. ++=item remove_tree( $dir1, $dir2, ...., \%opts ) + +-=item * ++The C<remove_tree> function deletes the given directories and any ++files and subdirectories they might contain, much like the Unix ++command C<rm -r> or C<del /s> on Windows. + +-a boolean value, which if TRUE will cause C<rmtree> to +-print a message each time it examines a file, giving the +-name of the file, and indicating whether it's using C<rmdir> +-or C<unlink> to remove it, or that it's skipping it. +-(defaults to FALSE) ++The function accepts a list of directories to be ++removed. Its behaviour may be tuned by an optional hashref ++appearing as the last parameter on the call. + +-=item * ++The functions returns the number of files successfully deleted. ++ ++The following keys are recognised in the option hash: ++ ++=over ++ ++=item verbose => $bool ++ ++If present, will cause C<remove_tree> to print the name of each file as ++it is unlinked. By default nothing is printed. ++ ++=item safe => $bool ++ ++When set to a true value, will cause C<remove_tree> to skip the files ++for which the process lacks the required privileges needed to delete ++files, such as delete privileges on VMS. In other words, the code ++will make no attempt to alter file permissions. Thus, if the process ++is interrupted, no filesystem object will be left in a more ++permissive mode. ++ ++=item keep_root => $bool ++ ++When set to a true value, will cause all files and subdirectories ++to be removed, except the initially specified directories. This comes ++in handy when cleaning out an application's scratch directory. ++ ++ remove_tree( '/tmp', {keep_root => 1} ); ++ ++=item result => \$res ++ ++If present, it should be a reference to a scalar. ++This scalar will be made to reference an array, which will ++be used to store all files and directories unlinked ++during the call. If nothing is unlinked, the array will be empty. ++ ++ remove_tree( '/tmp', {result => \my $list} ); ++ print "unlinked $_\n" for @$list; ++ ++This is a useful alternative to the C<verbose> key. ++ ++=item error => \$err ++ ++If present, it should be a reference to a scalar. ++This scalar will be made to reference an array, which will ++be used to store any errors that are encountered. See the L</"ERROR ++HANDLING"> section for more information. ++ ++Removing things is a much more dangerous proposition than ++creating things. As such, there are certain conditions that ++C<remove_tree> may encounter that are so dangerous that the only ++sane action left is to kill the program. ++ ++Use C<error> to trap all that is reasonable (problems with ++permissions and the like), and let it die if things get out ++of hand. This is the safest course of action. ++ ++=back ++ ++=item rmtree( $dir ) ++ ++=item rmtree( $dir, $verbose, $safe ) ++ ++=item rmtree( [$dir1, $dir2,...], $verbose, $safe ) + +-a boolean value, which if TRUE will cause C<rmtree> to +-skip any files to which you do not have delete access +-(if running under VMS) or write access (if running +-under another OS). This will change in the future when +-a criterion for 'delete permission' under OSs other +-than VMS is settled. (defaults to FALSE) ++=item rmtree( $dir1, $dir2,..., \%opt ) ++ ++The rmtree() function provide the legacy interface of remove_tree() ++with a different interpretation of the arguments passed. The behaviour ++and return value of the function is otherwise identical to ++remove_tree(). + + =back + +-It returns the number of files successfully deleted. Symlinks are +-simply deleted and not followed. ++=head2 ERROR HANDLING ++ ++=over 4 + +-B<NOTE:> There are race conditions internal to the implementation of +-C<rmtree> making it unsafe to use on directory trees which may be +-altered or moved while C<rmtree> is running, and in particular on any +-directory trees with any path components or subdirectories potentially +-writable by untrusted users. ++=item B<NOTE:> ++ ++The following error handling mechanism is considered ++experimental and is subject to change pending feedback from ++users. ++ ++=back ++ ++If C<make_path> or C<remove_tree> encounter an error, a diagnostic ++message will be printed to C<STDERR> via C<carp> (for non-fatal ++errors), or via C<croak> (for fatal errors). ++ ++If this behaviour is not desirable, the C<error> attribute may be ++used to hold a reference to a variable, which will be used to store ++the diagnostics. The variable is made a reference to an array of hash ++references. Each hash contain a single key/value pair where the key ++is the name of the file, and the value is the error message (including ++the contents of C<$!> when appropriate). If a general error is ++encountered the diagnostic key will be empty. ++ ++An example usage looks like: ++ ++ remove_tree( 'foo/bar', 'bar/rat', {error => \my $err} ); ++ if (@$err) { ++ for my $diag (@$err) { ++ my ($file, $message) = %$diag; ++ if ($file eq '') { ++ print "general error: $message\n"; ++ } ++ else { ++ print "problem unlinking $file: $message\n"; ++ } ++ } ++ } ++ else { ++ print "No error encountered\n"; ++ } + +-Additionally, if the third parameter is not TRUE and C<rmtree> is +-interrupted, it may leave files and directories with permissions altered +-to allow deletion (and older versions of this module would even set +-files and directories to world-read/writable!) ++Note that if no errors are encountered, C<$err> will reference an ++empty array. This means that C<$err> will always end up TRUE; so you ++need to test C<@$err> to determine if errors occured. + +-Note also that the occurrence of errors in C<rmtree> can be determined I<only> +-by trapping diagnostic messages using C<$SIG{__WARN__}>; it is not apparent +-from the return value. ++=head2 NOTES ++ ++C<File::Path> blindly exports C<mkpath> and C<rmtree> into the ++current namespace. These days, this is considered bad style, but ++to change it now would break too much code. Nonetheless, you are ++invited to specify what it is you are expecting to use: ++ ++ use File::Path 'rmtree'; ++ ++The routines C<make_path> and C<remove_tree> are B<not> exported ++by default. You must specify which ones you want to use. ++ ++ use File::Path 'remove_tree'; ++ ++Note that a side-effect of the above is that C<mkpath> and C<rmtree> ++are no longer exported at all. This is due to the way the C<Exporter> ++module works. If you are migrating a codebase to use the new ++interface, you will have to list everything explicitly. But that's ++just good practice anyway. ++ ++ use File::Path qw(remove_tree rmtree); ++ ++=head3 SECURITY CONSIDERATIONS ++ ++There were race conditions 1.x implementations of File::Path's ++C<rmtree> function (although sometimes patched depending on the OS ++distribution or platform). The 2.0 version contains code to avoid the ++problem mentioned in CVE-2002-0435. ++ ++See the following pages for more information: ++ ++ http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905 ++ http://www.nntp.perl.org/group/perl.perl5.porters/2005/01/msg97623.html ++ http://www.debian.org/security/2005/dsa-696 ++ ++Additionally, unless the C<safe> parameter is set (or the ++third parameter in the traditional interface is TRUE), should a ++C<remove_tree> be interrupted, files that were originally in read-only ++mode may now have their permissions set to a read-write (or "delete ++OK") mode. + + =head1 DIAGNOSTICS + ++FATAL errors will cause the program to halt (C<croak>), since the ++problem is so severe that it would be dangerous to continue. (This ++can always be trapped with C<eval>, but it's not a good idea. Under ++the circumstances, dying is the best thing to do). ++ ++SEVERE errors may be trapped using the modern interface. If the ++they are not trapped, or the old interface is used, such an error ++will cause the program will halt. ++ ++All other errors may be trapped using the modern interface, otherwise ++they will be C<carp>ed about. Program execution will not be halted. ++ ++=over 4 ++ ++=item mkdir [path]: [errmsg] (SEVERE) ++ ++C<make_path> was unable to create the path. Probably some sort of ++permissions error at the point of departure, or insufficient resources ++(such as free inodes on Unix). ++ ++=item No root path(s) specified ++ ++C<make_path> was not given any paths to create. This message is only ++emitted if the routine is called with the traditional interface. ++The modern interface will remain silent if given nothing to do. ++ ++=item No such file or directory ++ ++On Windows, if C<make_path> gives you this warning, it may mean that ++you have exceeded your filesystem's maximum path length. ++ ++=item cannot fetch initial working directory: [errmsg] ++ ++C<remove_tree> attempted to determine the initial directory by calling ++C<Cwd::getcwd>, but the call failed for some reason. No attempt ++will be made to delete anything. ++ ++=item cannot stat initial working directory: [errmsg] ++ ++C<remove_tree> attempted to stat the initial directory (after having ++successfully obtained its name via C<getcwd>), however, the call ++failed for some reason. No attempt will be made to delete anything. ++ ++=item cannot chdir to [dir]: [errmsg] ++ ++C<remove_tree> attempted to set the working directory in order to ++begin deleting the objects therein, but was unsuccessful. This is ++usually a permissions issue. The routine will continue to delete ++other things, but this directory will be left intact. ++ ++=item directory [dir] changed before chdir, expected dev=[n] ino=[n], actual dev=[n] ino=[n], aborting. (FATAL) ++ ++C<remove_tree> recorded the device and inode of a directory, and then ++moved into it. It then performed a C<stat> on the current directory ++and detected that the device and inode were no longer the same. As ++this is at the heart of the race condition problem, the program ++will die at this point. ++ ++=item cannot make directory [dir] read+writeable: [errmsg] ++ ++C<remove_tree> attempted to change the permissions on the current directory ++to ensure that subsequent unlinkings would not run into problems, ++but was unable to do so. The permissions remain as they were, and ++the program will carry on, doing the best it can. ++ ++=item cannot read [dir]: [errmsg] ++ ++C<remove_tree> tried to read the contents of the directory in order ++to acquire the names of the directory entries to be unlinked, but ++was unsuccessful. This is usually a permissions issue. The ++program will continue, but the files in this directory will remain ++after the call. ++ ++=item cannot reset chmod [dir]: [errmsg] ++ ++C<remove_tree>, after having deleted everything in a directory, attempted ++to restore its permissions to the original state but failed. The ++directory may wind up being left behind. ++ ++=item cannot remove [dir] when cwd is [dir] ++ ++The current working directory of the program is F</some/path/to/here> ++and you are attempting to remove an ancestor, such as F</some/path>. ++The directory tree is left untouched. ++ ++The solution is to C<chdir> out of the child directory to a place ++outside the directory tree to be removed. ++ ++=item cannot chdir to [parent-dir] from [child-dir]: [errmsg], aborting. (FATAL) ++ ++C<remove_tree>, after having deleted everything and restored the permissions ++of a directory, was unable to chdir back to the parent. The program ++halts to avoid a race condition from occurring. ++ ++=item cannot stat prior working directory [dir]: [errmsg], aborting. (FATAL) ++ ++C<remove_tree> was unable to stat the parent directory after have returned ++from the child. Since there is no way of knowing if we returned to ++where we think we should be (by comparing device and inode) the only ++way out is to C<croak>. ++ ++=item previous directory [parent-dir] changed before entering [child-dir], expected dev=[n] ino=[n], actual dev=[n] ino=[n], aborting. (FATAL) ++ ++When C<remove_tree> returned from deleting files in a child directory, a ++check revealed that the parent directory it returned to wasn't the one ++it started out from. This is considered a sign of malicious activity. ++ ++=item cannot make directory [dir] writeable: [errmsg] ++ ++Just before removing a directory (after having successfully removed ++everything it contained), C<remove_tree> attempted to set the permissions ++on the directory to ensure it could be removed and failed. Program ++execution continues, but the directory may possibly not be deleted. ++ ++=item cannot remove directory [dir]: [errmsg] ++ ++C<remove_tree> attempted to remove a directory, but failed. This may because ++some objects that were unable to be removed remain in the directory, or ++a permissions issue. The directory will be left behind. ++ ++=item cannot restore permissions of [dir] to [0nnn]: [errmsg] ++ ++After having failed to remove a directory, C<remove_tree> was unable to ++restore its permissions from a permissive state back to a possibly ++more restrictive setting. (Permissions given in octal). ++ ++=item cannot make file [file] writeable: [errmsg] ++ ++C<remove_tree> attempted to force the permissions of a file to ensure it ++could be deleted, but failed to do so. It will, however, still attempt ++to unlink the file. ++ ++=item cannot unlink file [file]: [errmsg] ++ ++C<remove_tree> failed to remove a file. Probably a permissions issue. ++ ++=item cannot restore permissions of [file] to [0nnn]: [errmsg] ++ ++After having failed to remove a file, C<remove_tree> was also unable ++to restore the permissions on the file to a possibly less permissive ++setting. (Permissions given in octal). ++ ++=back ++ ++=head1 SEE ALSO ++ + =over 4 + + =item * + +-On Windows, if C<mkpath> gives you the warning: B<No such file or +-directory>, this may mean that you've exceeded your filesystem's +-maximum path length. ++L<File::Remove> ++ ++Allows files and directories to be moved to the Trashcan/Recycle ++Bin (where they may later be restored if necessary) if the operating ++system supports such functionality. This feature may one day be ++made available directly in C<File::Path>. ++ ++=item * ++ ++L<File::Find::Rule> ++ ++When removing directory trees, if you want to examine each file to ++decide whether to delete it (and possibly leaving large swathes ++alone), F<File::Find::Rule> offers a convenient and flexible approach ++to examining directory trees. + + =back + +-=head1 AUTHORS ++=head1 BUGS + +-Tim Bunce <F<Tim.Bunce@ig.co.uk>> and +-Charles Bailey <F<bailey@newman.upenn.edu>> ++Please report all bugs on the RT queue: + +-=cut ++L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=File-Path> + +-use 5.006; +-use Carp; +-use File::Basename (); +-use Exporter (); +-use strict; +-use warnings; ++=head1 ACKNOWLEDGEMENTS + +-our $VERSION = "1.08"; +-our @ISA = qw( Exporter ); +-our @EXPORT = qw( mkpath rmtree ); ++Paul Szabo identified the race condition originally, and Brendan ++O'Dea wrote an implementation for Debian that addressed the problem. ++That code was used as a basis for the current code. Their efforts ++are greatly appreciated. + +-my $Is_VMS = $^O eq 'VMS'; +-my $Is_MacOS = $^O eq 'MacOS'; ++Gisle Aas made a number of improvements to the documentation for ++2.07 and his advice and assistance is also greatly appreciated. + +-# These OSes complain if you want to remove a file that you have no +-# write permission to: +-my $force_writeable = ($^O eq 'os2' || $^O eq 'dos' || $^O eq 'MSWin32' || +- $^O eq 'amigaos' || $^O eq 'MacOS' || $^O eq 'epoc'); ++=head1 AUTHORS + +-sub mkpath { +- my($paths, $verbose, $mode) = @_; +- # $paths -- either a path string or ref to list of paths +- # $verbose -- optional print "mkdir $path" for each directory created +- # $mode -- optional permissions, defaults to 0777 +- local($")=$Is_MacOS ? ":" : "/"; +- $mode = 0777 unless defined($mode); +- $paths = [$paths] unless ref $paths; +- my(@created,$path); +- foreach $path (@$paths) { +- $path .= '/' if $^O eq 'os2' and $path =~ /^\w:\z/s; # feature of CRT +- # Logic wants Unix paths, so go with the flow. +- if ($Is_VMS) { +- next if $path eq '/'; +- $path = VMS::Filespec::unixify($path); +- if ($path =~ m:^(/[^/]+)/?\z:) { +- $path = $1.'/000000'; +- } +- } +- next if -d $path; +- my $parent = File::Basename::dirname($path); +- unless (-d $parent or $path eq $parent) { +- push(@created,mkpath($parent, $verbose, $mode)); +- } +- print "mkdir $path\n" if $verbose; +- unless (mkdir($path,$mode)) { +- my $e = $!; +- # allow for another process to have created it meanwhile +- $! = $e, croak ("mkdir $path: $e") unless -d $path; +- } +- push(@created, $path); +- } +- @created; +-} ++Tim Bunce and Charles Bailey. Currently maintained by David Landgren ++<F<david@landgren.net>>. + +-sub rmtree { +- my($roots, $verbose, $safe) = @_; +- my(@files); +- my($count) = 0; +- $verbose ||= 0; +- $safe ||= 0; +- +- if ( defined($roots) && length($roots) ) { +- $roots = [$roots] unless ref $roots; +- } +- else { +- carp "No root path(s) specified\n"; +- return 0; +- } ++=head1 COPYRIGHT + +- my($root); +- foreach $root (@{$roots}) { +- if ($Is_MacOS) { +- $root = ":$root" if $root !~ /:/; +- $root =~ s#([^:])\z#$1:#; +- } else { +- $root =~ s#/\z##; +- } +- (undef, undef, my $rp) = lstat $root or next; +- $rp &= 07777; # don't forget setuid, setgid, sticky bits +- if ( -d _ ) { +- # notabene: 0700 is for making readable in the first place, +- # it's also intended to change it to writable in case we have +- # to recurse in which case we are better than rm -rf for +- # subtrees with strange permissions +- chmod($rp | 0700, ($Is_VMS ? VMS::Filespec::fileify($root) : $root)) +- or carp "Can't make directory $root read+writeable: $!" +- unless $safe; +- +- if (opendir my $d, $root) { +- no strict 'refs'; +- if (!defined ${"\cTAINT"} or ${"\cTAINT"}) { +- # Blindly untaint dir names +- @files = map { /^(.*)$/s ; $1 } readdir $d; +- } else { +- @files = readdir $d; +- } +- closedir $d; +- } +- else { +- carp "Can't read $root: $!"; +- @files = (); +- } +- +- # Deleting large numbers of files from VMS Files-11 filesystems +- # is faster if done in reverse ASCIIbetical order +- @files = reverse @files if $Is_VMS; +- ($root = VMS::Filespec::unixify($root)) =~ s#\.dir\z## if $Is_VMS; +- if ($Is_MacOS) { +- @files = map("$root$_", @files); +- } else { +- @files = map("$root/$_", grep $_!~/^\.{1,2}\z/s,@files); +- } +- $count += rmtree(\@files,$verbose,$safe); +- if ($safe && +- ($Is_VMS ? !&VMS::Filespec::candelete($root) : !-w $root)) { +- print "skipped $root\n" if $verbose; +- next; +- } +- chmod $rp | 0700, $root +- or carp "Can't make directory $root writeable: $!" +- if $force_writeable; +- print "rmdir $root\n" if $verbose; +- if (rmdir $root) { +- ++$count; +- } +- else { +- carp "Can't remove directory $root: $!"; +- chmod($rp, ($Is_VMS ? VMS::Filespec::fileify($root) : $root)) +- or carp("and can't restore permissions to " +- . sprintf("0%o",$rp) . "\n"); +- } +- } +- else { +- if ($safe && +- ($Is_VMS ? !&VMS::Filespec::candelete($root) +- : !(-l $root || -w $root))) +- { +- print "skipped $root\n" if $verbose; +- next; +- } +- chmod $rp | 0600, $root +- or carp "Can't make file $root writeable: $!" +- if $force_writeable; +- print "unlink $root\n" if $verbose; +- # delete all versions under VMS +- for (;;) { +- unless (unlink $root) { +- carp "Can't unlink file $root: $!"; +- if ($force_writeable) { +- chmod $rp, $root +- or carp("and can't restore permissions to " +- . sprintf("0%o",$rp) . "\n"); +- } +- last; +- } +- ++$count; +- last unless $Is_VMS && lstat $root; +- } +- } +- } ++This module is copyright (C) Charles Bailey, Tim Bunce and ++David Landgren 1995-2008. All rights reserved. + +- $count; +-} ++=head1 LICENSE + +-1; ++This library is free software; you can redistribute it and/or modify ++it under the same terms as Perl itself. ++ ++=cut +diff --git a/lib/File/Path.t b/lib/File/Path.t +index 84575d7..f1b5928 100755 +--- lib/File/Path.t ++++ lib/File/Path.t +@@ -1,18 +1,18 @@ +-#!./perl -wT ++# Path.t -- tests for module File::Path ++ ++use strict; ++ ++use Test::More tests => 99; + + BEGIN { +- chdir 't' if -d 't'; +- @INC = '../lib'; ++ use_ok('File::Path'); ++ use_ok('File::Spec::Functions'); + } + +-use File::Path; +-use File::Spec::Functions; +-use strict; +- +-my $count = 0; +-use warnings; ++eval "use Test::Output"; ++my $has_Test_Output = $@ ? 0 : 1; + +-print "1..4\n"; ++my $Is_VMS = $^O eq 'VMS'; + + # first check for stupid permissions second for full, so we clean up + # behind ourselves +@@ -21,10 +21,411 @@ for my $perm (0111,0777) { + mkpath($path); + chmod $perm, "mhx", $path; + +- print "not " unless -d "mhx" && -d $path; +- print "ok ", ++$count, "\n"; ++ my $oct = sprintf('0%o', $perm); ++ ok(-d "mhx", "mkdir parent dir $oct"); ++ ok(-d $path, "mkdir child dir $oct"); + + rmtree("mhx"); +- print "not " if -e "mhx"; +- print "ok ", ++$count, "\n"; ++ ok(! -e "mhx", "mhx does not exist $oct"); ++} ++ ++# find a place to work ++my ($error, $list, $file, $message); ++my $tmp_base = catdir( ++ curdir(), ++ sprintf( 'test-%x-%x-%x', time, $$, rand(99999) ), ++); ++ ++# invent some names ++my @dir = ( ++ catdir($tmp_base, qw(a b)), ++ catdir($tmp_base, qw(a c)), ++ catdir($tmp_base, qw(z b)), ++ catdir($tmp_base, qw(z c)), ++); ++ ++# create them ++my @created = mkpath(@dir); ++ ++is(scalar(@created), 7, "created list of directories"); ++ ++# pray for no race conditions blowing them out from under us ++@created = mkpath([$tmp_base]); ++is(scalar(@created), 0, "skipped making existing directory") ++ or diag("unexpectedly recreated @created"); ++ ++# create a file ++my $file_name = catfile( $tmp_base, 'a', 'delete.me' ); ++my $file_count = 0; ++if (open OUT, "> $file_name") { ++ print OUT "this file may be deleted\n"; ++ close OUT; ++ ++$file_count; + } ++else { ++ diag( "Failed to create file $file_name: $!" ); ++} ++ ++SKIP: { ++ skip "cannot remove a file we failed to create", 1 ++ unless $file_count == 1; ++ my $count = rmtree($file_name); ++ is($count, 1, "rmtree'ed a file"); ++} ++ ++@created = mkpath(''); ++is(scalar(@created), 0, "Can't create a directory named ''"); ++ ++my $dir; ++my $dir2; ++ ++SKIP: { ++ $dir = catdir($tmp_base, 'B'); ++ $dir2 = catdir($dir, updir()); ++ # IOW: File::Spec->catdir( qw(foo bar), File::Spec->updir ) eq 'foo' ++ # rather than foo/bar/.. ++ skip "updir() canonicalises path on this platform", 2 ++ if $dir2 eq $tmp_base ++ or $^O eq 'cygwin'; ++ ++ @created = mkpath($dir2, {mask => 0700}); ++ is(scalar(@created), 1, "make directory with trailing parent segment"); ++ is($created[0], $dir, "made parent"); ++}; ++ ++my $count = rmtree({error => \$error}); ++is( $count, 0, 'rmtree of nothing, count of zero' ); ++is( scalar(@$error), 0, 'no diagnostic captured' ); ++ ++@created = mkpath($tmp_base, 0); ++is(scalar(@created), 0, "skipped making existing directories (old style 1)") ++ or diag("unexpectedly recreated @created"); ++ ++$dir = catdir($tmp_base,'C'); ++# mkpath returns unix syntax filespecs on VMS ++$dir = VMS::Filespec::unixify($dir) if $Is_VMS; ++@created = mkpath($tmp_base, $dir); ++is(scalar(@created), 1, "created directory (new style 1)"); ++is($created[0], $dir, "created directory (new style 1) cross-check"); ++ ++@created = mkpath($tmp_base, 0, 0700); ++is(scalar(@created), 0, "skipped making existing directories (old style 2)") ++ or diag("unexpectedly recreated @created"); ++ ++$dir2 = catdir($tmp_base,'D'); ++# mkpath returns unix syntax filespecs on VMS ++$dir2 = VMS::Filespec::unixify($dir2) if $Is_VMS; ++@created = mkpath($tmp_base, $dir, $dir2); ++is(scalar(@created), 1, "created directory (new style 2)"); ++is($created[0], $dir2, "created directory (new style 2) cross-check"); ++ ++$count = rmtree($dir, 0); ++is($count, 1, "removed directory unsafe mode"); ++ ++$count = rmtree($dir2, 0, 1); ++my $removed = $Is_VMS ? 0 : 1; ++is($count, $removed, "removed directory safe mode"); ++ ++# mkdir foo ./E/../Y ++# Y should exist ++# existence of E is neither here nor there ++$dir = catdir($tmp_base, 'E', updir(), 'Y'); ++@created =mkpath($dir); ++cmp_ok(scalar(@created), '>=', 1, "made one or more dirs because of .."); ++cmp_ok(scalar(@created), '<=', 2, "made less than two dirs because of .."); ++ok( -d catdir($tmp_base, 'Y'), "directory after parent" ); ++ ++@created = mkpath(catdir(curdir(), $tmp_base)); ++is(scalar(@created), 0, "nothing created") ++ or diag(@created); ++ ++$dir = catdir($tmp_base, 'a'); ++$dir2 = catdir($tmp_base, 'z'); ++ ++rmtree( $dir, $dir2, ++ { ++ error => \$error, ++ result => \$list, ++ keep_root => 1, ++ } ++); ++ ++is(scalar(@$error), 0, "no errors unlinking a and z"); ++is(scalar(@$list), 4, "list contains 4 elements") ++ or diag("@$list"); ++ ++ok(-d $dir, "dir a still exists"); ++ok(-d $dir2, "dir z still exists"); ++ ++$dir = catdir($tmp_base,'F'); ++# mkpath returns unix syntax filespecs on VMS ++$dir = VMS::Filespec::unixify($dir) if $Is_VMS; ++ ++@created = mkpath($dir, undef, 0770); ++is(scalar(@created), 1, "created directory (old style 2 verbose undef)"); ++is($created[0], $dir, "created directory (old style 2 verbose undef) cross-check"); ++is(rmtree($dir, undef, 0), 1, "removed directory 2 verbose undef"); ++ ++@created = mkpath($dir, undef); ++is(scalar(@created), 1, "created directory (old style 2a verbose undef)"); ++is($created[0], $dir, "created directory (old style 2a verbose undef) cross-check"); ++is(rmtree($dir, undef), 1, "removed directory 2a verbose undef"); ++ ++@created = mkpath($dir, 0, undef); ++is(scalar(@created), 1, "created directory (old style 3 mode undef)"); ++is($created[0], $dir, "created directory (old style 3 mode undef) cross-check"); ++is(rmtree($dir, 0, undef), 1, "removed directory 3 verbose undef"); ++ ++$dir = catdir($tmp_base,'G'); ++$dir = VMS::Filespec::unixify($dir) if $Is_VMS; ++ ++@created = mkpath($dir, undef, 0200); ++is(scalar(@created), 1, "created write-only dir"); ++is($created[0], $dir, "created write-only directory cross-check"); ++is(rmtree($dir), 1, "removed write-only dir"); ++ ++# borderline new-style heuristics ++if (chdir $tmp_base) { ++ pass("chdir to temp dir"); ++} ++else { ++ fail("chdir to temp dir: $!"); ++} ++ ++$dir = catdir('a', 'd1'); ++$dir2 = catdir('a', 'd2'); ++ ++@created = mkpath( $dir, 0, $dir2 ); ++is(scalar @created, 3, 'new-style 3 dirs created'); ++ ++$count = rmtree( $dir, 0, $dir2, ); ++is($count, 3, 'new-style 3 dirs removed'); ++ ++@created = mkpath( $dir, $dir2, 1 ); ++is(scalar @created, 3, 'new-style 3 dirs created (redux)'); ++ ++$count = rmtree( $dir, $dir2, 1 ); ++is($count, 3, 'new-style 3 dirs removed (redux)'); ++ ++@created = mkpath( $dir, $dir2 ); ++is(scalar @created, 2, 'new-style 2 dirs created'); ++ ++$count = rmtree( $dir, $dir2 ); ++is($count, 2, 'new-style 2 dirs removed'); ++ ++if (chdir updir()) { ++ pass("chdir parent"); ++} ++else { ++ fail("chdir parent: $!"); ++} ++ ++# see what happens if a file exists where we want a directory ++SKIP: { ++ my $entry = catdir($tmp_base, "file"); ++ skip "Cannot create $entry", 4 unless open OUT, "> $entry"; ++ print OUT "test file, safe to delete\n", scalar(localtime), "\n"; ++ close OUT; ++ ok(-e $entry, "file exists in place of directory"); ++ ++ mkpath( $entry, {error => \$error} ); ++ is( scalar(@$error), 1, "caught error condition" ); ++ ($file, $message) = each %{$error->[0]}; ++ is( $entry, $file, "and the message is: $message"); ++ ++ eval {@created = mkpath($entry, 0, 0700)}; ++ $error = $@; ++ chomp $error; # just to remove silly # in TAP output ++ cmp_ok( $error, 'ne', "", "no directory created (old-style) err=$error" ) ++ or diag(@created); ++} ++ ++my $extra = catdir(curdir(), qw(EXTRA 1 a)); ++ ++SKIP: { ++ skip "extra scenarios not set up, see eg/setup-extra-tests", 14 ++ unless -e $extra; ++ ++ my ($list, $err); ++ $dir = catdir( 'EXTRA', '1' ); ++ rmtree( $dir, {result => \$list, error => \$err} ); ++ is(scalar(@$list), 2, "extra dir $dir removed"); ++ is(scalar(@$err), 1, "one error encountered"); ++ ++ $dir = catdir( 'EXTRA', '3', 'N' ); ++ rmtree( $dir, {result => \$list, error => \$err} ); ++ is( @$list, 1, q{remove a symlinked dir} ); ++ is( @$err, 0, q{with no errors} ); ++ ++ $dir = catdir('EXTRA', '3', 'S'); ++ rmtree($dir, {error => \$error}); ++ is( scalar(@$error), 1, 'one error for an unreadable dir' ); ++ eval { ($file, $message) = each %{$error->[0]}}; ++ is( $file, $dir, 'unreadable dir reported in error' ) ++ or diag($message); ++ ++ $dir = catdir('EXTRA', '3', 'T'); ++ rmtree($dir, {error => \$error}); ++ is( scalar(@$error), 1, 'one error for an unreadable dir T' ); ++ eval { ($file, $message) = each %{$error->[0]}}; ++ is( $file, $dir, 'unreadable dir reported in error T' ); ++ ++ $dir = catdir( 'EXTRA', '4' ); ++ rmtree($dir, {result => \$list, error => \$err} ); ++ is( scalar(@$list), 0, q{don't follow a symlinked dir} ); ++ is( scalar(@$err), 2, q{two errors when removing a symlink in r/o dir} ); ++ eval { ($file, $message) = each %{$err->[0]} }; ++ is( $file, $dir, 'symlink reported in error' ); ++ ++ $dir = catdir('EXTRA', '3', 'U'); ++ $dir2 = catdir('EXTRA', '3', 'V'); ++ rmtree($dir, $dir2, {verbose => 0, error => \$err, result => \$list}); ++ is( scalar(@$list), 1, q{deleted 1 out of 2 directories} ); ++ is( scalar(@$error), 1, q{left behind 1 out of 2 directories} ); ++ eval { ($file, $message) = each %{$err->[0]} }; ++ is( $file, $dir, 'first dir reported in error' ); ++} ++ ++{ ++ $dir = catdir($tmp_base, 'ZZ'); ++ @created = mkpath($dir); ++ is(scalar(@created), 1, "create a ZZ directory"); ++ ++ local @ARGV = ($dir); ++ rmtree( [grep -e $_, @ARGV], 0, 0 ); ++ ok(!-e $dir, "blow it away via \@ARGV"); ++} ++ ++SKIP: { ++ skip 'Test::Output not available', 14 ++ unless $has_Test_Output; ++ ++ SKIP: { ++ $dir = catdir('EXTRA', '3'); ++ skip "extra scenarios not set up, see eg/setup-extra-tests", 3 ++ unless -e $dir; ++ ++ $dir = catdir('EXTRA', '3', 'U'); ++ stderr_like( ++ sub {rmtree($dir, {verbose => 0})}, ++ qr{\Acannot make child directory read-write-exec for [^:]+: .* at \S+ line \d+}, ++ q(rmtree can't chdir into root dir) ++ ); ++ ++ $dir = catdir('EXTRA', '3'); ++ stderr_like( ++ sub {rmtree($dir, {})}, ++ qr{\Acannot make child directory read-write-exec for [^:]+: .* at (\S+) line (\d+) ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot remove directory for [^:]+: .* at \1 line \2}, ++ 'rmtree with file owned by root' ++ ); ++ ++ stderr_like( ++ sub {rmtree('EXTRA', {})}, ++ qr{\Acannot remove directory for [^:]+: .* at (\S+) line (\d+) ++cannot remove directory for [^:]+: .* at \1 line \2 ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot remove directory for [^:]+: .* at \1 line \2 ++cannot unlink file for [^:]+: .* at \1 line \2 ++cannot restore permissions to \d+ for [^:]+: .* at \1 line \2 ++cannot make child directory read-write-exec for [^:]+: .* at \1 line \2 ++cannot remove directory for [^:]+: .* at \1 line \2 ++cannot restore permissions to \d+ for [^:]+: .* at \1 line \2}, ++ 'rmtree with insufficient privileges' ++ ); ++ } ++ ++ my $base = catdir($tmp_base,'output'); ++ $dir = catdir($base,'A'); ++ $dir2 = catdir($base,'B'); ++ ++ stderr_like( ++ sub { rmtree( undef, 1 ) }, ++ qr/\ANo root path\(s\) specified\b/, ++ "rmtree of nothing carps sensibly" ++ ); ++ ++ stderr_like( ++ sub { rmtree( '', 1 ) }, ++ qr/\ANo root path\(s\) specified\b/, ++ "rmtree of empty dir carps sensibly" ++ ); ++ ++ stderr_is( sub { mkpath() }, '', "mkpath no args does not carp" ); ++ stderr_is( sub { rmtree() }, '', "rmtree no args does not carp" ); ++ ++ stdout_is( ++ sub {@created = mkpath($dir, 1)}, ++ "mkdir $base\nmkdir $dir\n", ++ 'mkpath verbose (old style 1)' ++ ); ++ ++ stdout_is( ++ sub {@created = mkpath([$dir2], 1)}, ++ "mkdir $dir2\n", ++ 'mkpath verbose (old style 2)' ++ ); ++ ++ stdout_is( ++ sub {$count = rmtree([$dir, $dir2], 1, 1)}, ++ "rmdir $dir\nrmdir $dir2\n", ++ 'rmtree verbose (old style)' ++ ); ++ ++ stdout_is( ++ sub {@created = mkpath($dir, {verbose => 1, mask => 0750})}, ++ "mkdir $dir\n", ++ 'mkpath verbose (new style 1)' ++ ); ++ ++ stdout_is( ++ sub {@created = mkpath($dir2, 1, 0771)}, ++ "mkdir $dir2\n", ++ 'mkpath verbose (new style 2)' ++ ); ++ ++ SKIP: { ++ $file = catdir($dir2, "file"); ++ skip "Cannot create $file", 2 unless open OUT, "> $file"; ++ print OUT "test file, safe to delete\n", scalar(localtime), "\n"; ++ close OUT; ++ ++ ok(-e $file, "file created in directory"); ++ ++ stdout_is( ++ sub {$count = rmtree($dir, $dir2, {verbose => 1, safe => 1})}, ++ "rmdir $dir\nunlink $file\nrmdir $dir2\n", ++ 'rmtree safe verbose (new style)' ++ ); ++ } ++} ++ ++SKIP: { ++ skip "extra scenarios not set up, see eg/setup-extra-tests", 11 ++ unless -d catdir(qw(EXTRA 1)); ++ ++ rmtree 'EXTRA', {safe => 0, error => \$error}; ++ is( scalar(@$error), 11, 'seven deadly sins' ); # well there used to be 7 ++ ++ rmtree 'EXTRA', {safe => 1, error => \$error}; ++ is( scalar(@$error), 9, 'safe is better' ); ++ for (@$error) { ++ ($file, $message) = each %$_; ++ if ($file =~ /[123]\z/) { ++ is(index($message, 'cannot remove directory: '), 0, "failed to remove $file with rmdir") ++ or diag($message); ++ } ++ else { ++ like($message, qr(\Acannot (?:restore permissions to \d+|chdir to child|unlink file): ), "failed to remove $file with unlink") ++ or diag($message) ++ } ++ } ++} ++ ++rmtree($tmp_base, {result => \$list} ); ++is(ref($list), 'ARRAY', "received a final list of results"); ++ok( !(-d $tmp_base), "test base directory gone" ); -- 1.6.0.4 --- perl5.8-fix-CVE-2005-0448-and-related-ones.diff ends here --- I had tested it with the above scripts and they are not giving me either setuid binary or removal of the file. The following VuXML entry should be evaluated and added: --- vuln.xml begins here --- <vuln vid="4f692cb2-bf2e-11dd-a708-001fc66e7203"> <topic>perl5.8 -- multiple vulnerabilities in File::Path rmtree function</topic> <affects> <package> <name>perl</name> <range><ge>5.8.0</ge><lt>5.8.8_2</lt></range> </package> </affects> <description> <body xmlns="http://www.w3.org/1999/xhtml"> <p>Niko Tyni from Debian reports:</p> <blockquote cite="http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905#85"> <p>CVE-2005-0448 (File::Path::rmtree races) has resurfaced and is present in all of etch, lenny, and sid.</p> <p>To be precise, CVE-2005-0448 was about two bugs (#286922 and #286905). Both of those apply to the etch package (perl-5.8.8), while only #286905 applies to the lenny/sid package (perl-5.10.0).</p> </blockquote> </body> </description> <references> <cvename>CVE-2005-0448</cvename> <mlist>http://www.openwall.com/lists/oss-security/2008/11/28/1</mlist> <mlist>http://www.gossamer-threads.com/lists/perl/porters/233699#233699</mlist> <url>http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286905</url> <url>http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=286922</url> </references> <dates> <discovery>27-11-2008</discovery> <entry>TODAY</entry> </dates> </vuln> --- vuln.xml ends here --- I had traced the vulnerability at least to the 5.8.0. 5.8.4 (that was said to be the fixed version) has all of these, because of missing 'if $force_writeable' for the second chmod call (setuid case) and the checks for the directory substituted by the symlink are missing too (presumably some inode and mount device comparisons should be done). NB: all this stuff is already public, so I am not feeling myself guilty to report this to the public PR database ;))
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?20081130230350.96F5DB8019>