Skip Menu |

This queue is for tickets about the PathTools CPAN distribution.

Report information
The Basics
Id: 13851
Status: open
Priority: 0/
Queue: PathTools

People
Owner: Nobody in particular
Requestors: baldvin [...] cs.elte.hu
jkeenan [...] cpan.org
Cc: ether [...] cpan.org
AdminCc:

Bug Information
Severity: Normal
Broken in: (no value)
Fixed in: (no value)



Subject: abs_path fails if no rights for current directory
Try the following: chmod 000 . perl -w -e 'use Cwd qw(abs_path); print abs_path("/"), "\n"' It fails, because abs_path tries to chdir and come back in order to do its job -- however, it cannot come back. The problem is quite interesting for me, since I develop a tool for system administration, and the following common conditions are resulting in failure: 1. the home direcotries are on nfs 2. the user homes of the sysadmins are not readable nor executable for anybody else 3. the nfs is mounted with root_squash. Now "su", and there you go with a directory that you cannot chdir back in...
From: dan.harbin [...] gmail.com
This is also a problem for me. This bug is duplicated in ticket 27294 and bug 4593. I noticed that this was a problem for XS abs_path and fast_abs_path, but not _perl_abs_path. I tested on version 3.05 of pathtools.
From: mschwern [...] cpan.org
Here's a similar situation. Dreamhost sets /home 771, so I can't read /home but I can cd into it. This results in abs_path() (and thus getcwd) to fail while building perl. The problem appears to be in _perl_abs_path(), which is why it only effects the build before Cwd.xs has been built. It can be simulated like so... ~/tmp$ mkdir -p home/username/foo ~/tmp$ chmod 771 home ~/tmp$ sudo chown root:staff home ~/tmp$ ls home ls: cannot open directory home: Permission denied ~/tmp$ ls -ld home drwxrwx--x 3 root staff 102 Dec 24 02:21 home ~/tmp$ cd home/username/foo ~/tmp/home/username/foo$ perl -MCwd -wle 'print Cwd::abs_path(".")'/Users/schwern/tmp/home/username/foo ~/tmp/home/username/foo$ perl -MCwd -wle 'print Cwd::_perl_abs_path(".")' opendir(./../..): Permission denied at -e line 1 This is with 5.8.8 and Cwd 3.25, 5.10.0 and Cwd 3.2501. 5.6.2 w/Cwd 2.04 and 5.5.5 w/Cwd 2.01 fail with abs_path() because it is the same code as _perl_abs_path(). It's a very old bug.
Attached is a test.
#!./perl -w BEGIN { if ($ENV{PERL_CORE}) { chdir 't'; @INC = '../lib'; } } use Cwd; chdir 't'; use strict; use Config; use File::Spec; use File::Path; use lib File::Spec->catdir('t', 'lib'); require VMS::Filespec if $^O eq 'VMS'; use Test::More tests => 9; my $starting_wd = Cwd::abs_path(); my $x_but_no_r = 'x_but_no_r'; my $test_dir = 'foo'; my $test_path = File::Spec->catdir($x_but_no_r, $test_dir); ok mkpath($test_path); ok chmod 0100, $x_but_no_r; ok !opendir(my $dh, $x_but_no_r), "directory unreadable as desired"; ok chdir $x_but_no_r, " but we can cd into it"; ok chdir $test_dir; like Cwd::abs_path("."), qr{\Q$test_path\E$}, 'abs_path'; like Cwd::_perl_abs_path("."), qr{\Q$test_path\E$}, 'abs_path no-XS'; END { chdir $starting_wd; ok chmod 0700, $x_but_no_r; ok rmtree $x_but_no_r; }
I've figured out the basic problem. You're supposed to cheat and run a setuid executable. IEEE even recommends it. http://www.opengroup.org/onlinepubs/009695399/functions/getcwd.html ------------- If a program is operating in a directory where some (grand)parent directory does not permit reading, getcwd() may fail, as in most implementations it must read the directory to determine the name of the file. This can occur if search, but not read, permission is granted in an intermediate directory, or if the program is placed in that directory by some more privileged process (for example, login). Including the [EACCES] error condition makes the reporting of the error consistent and warns the application writer that getcwd() can fail for reasons beyond the control of the application writer or user. Some implementations can avoid this occurrence (for example, by implementing getcwd() using pwd, where pwd is a set-user-root process), thus the error was made optional. Since this volume of IEEE Std 1003.1-2001 permits the addition of other errors, this would be a common addition and yet one that applications could not be expected to deal with without this addition. ------------- So that pretty much boils down to if the opendir() fails it should fall back to _backtick_pwd().
Attached is a fix. It just calls _backtick_pwd() if the opendir() fails. This is, alas, a bit Unix centric but then again so are filesystem permissions. It might make sense to check the exact errno returned from opendir (ie. EACCES) or to see if the directory in question is unreadable, but I don't think it's necessary to get too specific and trying the native pwd on failure seems a good general recovery mechanism. The fix is still evolving. In particular I need to check when the cwd itself is unreadable, I think all the stats in _perl_abs_path() will be trouble. In the process of fixing this I discovered another problem with _perl_abs_path. realpath (which is abs_path) should work even when given a subdir that doesn't exist and _perl_abs_path doesn't. I'll post that in another bug, in this test it's inside a TODO.
--- t/permissions.t (/vendor/PathTools) (revision 41190) +++ t/permissions.t (/local/PathTools) (revision 41190) @@ -0,0 +1,76 @@ +#!./perl -w + +BEGIN { + if ($ENV{PERL_CORE}) { + chdir 't'; + @INC = '../lib'; + } +} +use Cwd; +chdir 't'; + +use strict; +use Config; +use File::Spec; +use File::Path; + +use lib File::Spec->catdir('t', 'lib'); +require VMS::Filespec if $^O eq 'VMS'; + +use Test::More; + +# Set up the environment. +my $starting_wd = Cwd::abs_path(); +my $x_but_no_r = 'x_but_no_r'; +my $test_dir = 'testdir'; +my $sub_dir = 'subdir'; +my $test_path = File::Spec->catdir($x_but_no_r, $test_dir); +my $sub_path = File::Spec->catdir($test_path, $sub_dir); + +eval { + mkpath($sub_path) or die; + chmod 0100, $x_but_no_r or die; + + !opendir(my $dh, $x_but_no_r) or die; + chdir $x_but_no_r or die; + chdir $test_dir or die; +}; +if( $@ ) { + plan skip_all => + "restricted permission test environment could not be made" +} +else { + plan tests => 8; +} + + +my %funcs = ( + abs_path => \&Cwd::abs_path, + _perl_abs_path => \&Cwd::_perl_abs_path, +); +for my $name (sort keys %funcs) { + my $func = $funcs{$name}; + + like $func->(), qr{\Q$test_path\E$}, "$name w/no args"; + like $func->($sub_dir), qr{\Q$sub_path\E$}, "$name w/subdir"; + + # All but the last component of pathname must exist when realpath() + # is called. -- BSD realpath man page. + my $dne_path = File::Spec->catdir($test_path, "dne"); + TODO: { + local $TODO = 'Perl abs_path() does not work with a bogus sub file' + if $name =~ /perl/; + like $func->("dne"), qr{\Q$dne_path\E$}, + "$name w/non-existant filename"; + } + + my $double_dne = File::Spec->catdir("dne", "sub_dne"); + ok !$func->($double_dne), "$name w/non-existant sub dir"; +} + + +END { + chdir $starting_wd; + chmod 0700, $x_but_no_r; + rmtree $x_but_no_r; +} \ No newline at end of file --- MANIFEST (/vendor/PathTools) (revision 41190) +++ MANIFEST (/local/PathTools) (revision 41190) @@ -23,6 +23,7 @@ t/lib/Test/More.pm t/lib/Test/Simple.pm t/lib/Test/Tutorial.pod +t/permissions.t t/rel2abs2rel.t t/Spec.t t/taint.t --- Cwd.pm (/vendor/PathTools) (revision 41190) +++ Cwd.pm (/local/PathTools) (revision 41190) @@ -540,8 +540,8 @@ local *PARENT; unless (opendir(PARENT, $dotdots)) { - _carp("opendir($dotdots): $!"); - return ''; + # probably a permissions issue. Try the native command. + return File::Spec->rel2abs( $start, _backtick_pwd() ); } unless (@cst = stat($dotdots)) {
From: mschwern [...] cpan.org
Ok, so that's not a fix for this problem, it's a fix for a different problem. The XS version of abs_path() is failing if it has no rights for the current directory. Ironicly, the _perl_abs_path() works. Anyhow, abs_path() uses the copy of OpenBSD's realpath() in Cwd.xs. That's code over 9 years old. On a hunch I grabbed the latest version and slapped it in. http://www.openbsd.org/cgi-bin/cvsweb/src/lib/libc/stdlib/realpath.c As it doesn't use chdir(), and no gotos, it required a lot less modification. This fixes the problem with abs_path() and probably a small pile of others. Since this version of realpath() is a lot easier to follow than the old one, I can probably duplicate it in _perl_abs_path(). More to come, but the latest patch is attached.

Message body is not shown because it is too large.

From: kwilliams [...] cpan.org
Excellent, I like the way this is going. I applied the patch and all previous tests pass; for the new t/abs_path.t, tests 1-2 & 9-11 fail, `pwd` says "permission denied". Weird that they don't use fchdir() anymore, I haven't looked over the new algorithm but I thought that would be a part of it. -Ken
Those tests are still failing and I haven't had the time to look into why. For the last couple releases, there have been some urgent things necessitating other fixes to be pushed out, so I've just removed t/abs_path.t from the MANIFEST. If we can get it working, great. -Ken
Here's some improvements to _perl_abs_path() and another shot at abs_path.t. This is a diff against 3.2701. This fixes _perl_abs_path() with a filename that doesn't exist.
--- t/abs_path.t (/vendor/PathTools) (revision 54975) +++ t/abs_path.t (/local/PathTools) (revision 54975) @@ -0,0 +1,108 @@ +#!./perl -w + +BEGIN { + if ($ENV{PERL_CORE}) { + chdir 't'; + @INC = '../lib'; + } +} +use Cwd; +chdir 't'; + +use strict; +use Carp; +use Carp::Heavy; # because we chdir around later and it won't be found +use Config; +use File::Spec; +BEGIN { package FS; our @ISA = qw(File::Spec) } +use File::Path; + +use lib FS->catdir('t', 'lib'); +require VMS::Filespec if $^O eq 'VMS'; + +use Test::More; + +sub cmp_path { + my($got, $expected, $name) = @_; + + $expected = File::Spec->catdir(@$expected) if defined $expected; + + local $Test::Builder::Level = $Test::Builder::Level + 1; + return is lc $got, lc $expected, $name; +} + +# Set up the environment. +my $starting_wd = Cwd::abs_path(); +my $x_but_no_r = 'x_but_no_r'; +my $test_dir = 'testdir'; +my $sub_dir = 'subdir'; +my $test_path = FS->catdir($x_but_no_r, $test_dir); +my $sub_path = FS->catdir($test_path, $sub_dir); + +my $build_environment = eval { + mkpath($sub_path) or die; + chmod 0100, $x_but_no_r or die; + + !opendir(my $dh, $x_but_no_r) or die; + chdir $x_but_no_r or die; + chdir $test_dir or die; +}; +END { + chdir $starting_wd; + chmod 0700, $x_but_no_r; + rmtree $x_but_no_r; +} + +if( !$build_environment ) { + plan skip_all => + "restricted permission test environment could not be made" +} +else { + plan 'no_plan'; +} + + +my %funcs = ( + abs_path => \&Cwd::abs_path, + _perl_abs_path => \&Cwd::_perl_abs_path, +); +for my $name (sort keys %funcs) { + my $func = $funcs{$name}; + + cmp_path $func->(), [$starting_wd, $test_path], "$name w/no args"; + cmp_path $func->($sub_dir), [$starting_wd, $sub_path], "$name w/subdir"; + + # All but the last component of pathname must exist when realpath() + # is called. -- BSD realpath man page. + my $dne_path = FS->catdir($test_path, "dne"); + cmp_path $func->("dne"), [$starting_wd, $dne_path], + "$name w/non-existant filename"; + + my $double_dne = FS->catdir("dne", "sub_dne"); + cmp_path $func->($double_dne), undef, "$name w/non-existant sub dir"; + + ok chdir $starting_wd; + cmp_path $func->("../t"), [$starting_wd]; + cmp_path $func->("../t/../t"), [$starting_wd]; + cmp_path $func->("../t/dne"), [$starting_wd, 'dne']; + cmp_path $func->('abs_path.t'), [$starting_wd, 'abs_path.t'], + "$name works on files"; + + ok chdir $x_but_no_r, 'chdir to the unreadable directory'; + cmp_path $func->(), [$starting_wd, $x_but_no_r], + "$name when cwd unreadable"; + ok chdir $test_dir, 'chdir back to the test dir'; + + cmp_path $func->(".."), [$starting_wd, $x_but_no_r]; + + TODO: { + local $TODO = '_perl_abs_path does not fold .. yet' + if $name eq '_perl_abs_path'; + + cmp_path $func->("../$test_dir"), + [$starting_wd, $x_but_no_r, $test_dir]; + } + cmp_path $func->("/"), ["/"]; + cmp_path $func->("/does_not_exist"), ["/does_not_exist"]; + cmp_path $func->("/foo/"), undef; +} --- MANIFEST (/vendor/PathTools) (revision 54975) +++ MANIFEST (/local/PathTools) (revision 54975) @@ -16,16 +16,17 @@ MANIFEST This list of files META.yml ppport.h +t/abs_path.t t/crossplatform.t t/cwd.t t/Functions.t t/lib/Test/Builder.pm t/lib/Test/More.pm t/lib/Test/Simple.pm t/rel2abs2rel.t t/Spec.t t/taint.t t/tmpdir.t t/win32.t --- Cwd.pm (/vendor/PathTools) (revision 54975) +++ Cwd.pm (/local/PathTools) (revision 54975) @@ -498,41 +498,38 @@ } +use File::Basename; sub _perl_abs_path { my $start = @_ ? shift : '.'; - my($dotdots, $cwd, @pst, @cst, $dir, @tst); - unless (@cst = stat( $start )) - { - _carp("stat($start): $!"); - return ''; + # The last element of the path is allowed to not exist. + my $start_dir = $start; + my $start_file = ''; + if( !-e $start_dir or !-d $start_dir ) { + ($start_file, $start_dir) = fileparse($start); + $start_file = '' if $start_file eq '.'; } - unless (-d _) { - # Make sure we can be invoked on plain files, not just directories. - # NOTE that this routine assumes that '/' is the only directory separator. - - my ($dir, $file) = $start =~ m{^(.*)/(.+)$} - or return cwd() . '/' . $start; - - # Can't use "-l _" here, because the previous stat was a stat(), not an lstat(). if (-l $start) { - my $link_target = readlink($start); - die "Can't resolve link $start: $!" unless defined $link_target; - require File::Spec; - $link_target = $dir . '/' . $link_target - unless File::Spec->file_name_is_absolute($link_target); + my $link_target = readlink( File::Spec->canonpath($start) ); + die "Can't resolve link $start: $!" unless defined $link_target; return abs_path($link_target); } - return $dir ? abs_path($dir) . "/$file" : "/$file"; + my(@pst, @cst, @tst); + + unless (@cst = stat( $start_dir )) + { + _carp("stat($start_dir): $!"); + return undef; } - $cwd = ''; - $dotdots = $start; + my $cwd = ''; + my $dotdots = $start_dir; + my $dir; do { $dotdots .= '/..'; @@ -541,7 +538,8 @@ unless (opendir(PARENT, $dotdots)) { # probably a permissions issue. Try the native command. - return File::Spec->rel2abs( $start, _backtick_pwd() ); + require File::Spec; + return File::Spec->rel2abs( $start, _backtick_pwd() ) } unless (@cst = stat($dotdots)) { @@ -571,8 +569,10 @@ $cwd = (defined $dir ? "$dir" : "" ) . "/$cwd" ; closedir(PARENT); } while (defined $dir); - chop($cwd) unless $cwd eq '/'; # drop the trailing / - $cwd; + + chop($cwd) if $cwd ne '/' and !$start_file; + $cwd .= "$start_file" if $start_file; + return $cwd; }
Still not happy on abs_path.t: t/abs_path....pwd: : Permission denied Use of uninitialized value in join or string at /Users/ken/src/PathTools/blib/lib/File/Spec/Unix.pm line 82. not ok 1 - _perl_abs_path w/no args # Failed test (t/abs_path.t at line 72) # got: '/' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir' pwd: : Permission denied Use of uninitialized value in join or string at /Users/ken/src/PathTools/blib/lib/File/Spec/Unix.pm line 82. not ok 2 - _perl_abs_path w/subdir # Failed test (t/abs_path.t at line 73) # got: '/subdir' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir/subdir' pwd: : Permission denied Use of uninitialized value in join or string at /Users/ken/src/PathTools/blib/lib/File/Spec/Unix.pm line 82. not ok 3 - _perl_abs_path w/non-existant filename # Failed test (t/abs_path.t at line 78) # got: '/dne' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir/dne' stat(dne/): No such file or directory at t/abs_path.t line 82 ok 4 - _perl_abs_path w/non-existant sub dir ok 5 ok 6 ok 7 ok 8 ok 9 - _perl_abs_path works on files ok 10 - chdir to the unreadable directory ok 11 - _perl_abs_path when cwd unreadable ok 12 - chdir back to the test dir ok 13 pwd: : Permission denied Use of uninitialized value in join or string at /Users/ken/src/PathTools/blib/lib/File/Spec/Unix.pm line 82. not ok 14 # TODO _perl_abs_path does not fold .. yet # Failed (TODO) test (t/abs_path.t at line 102) # got: '/testdir' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir' ok 15 ok 16 stat(/foo/): No such file or directory at t/abs_path.t line 107 ok 17 not ok 18 - abs_path w/no args # Failed test (t/abs_path.t at line 72) # got: '' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir' not ok 19 - abs_path w/subdir # Failed test (t/abs_path.t at line 73) # got: '' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir/subdir' not ok 20 - abs_path w/non-existant filename # Failed test (t/abs_path.t at line 78) # got: '' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir/dne' ok 21 - abs_path w/non-existant sub dir ok 22 ok 23 ok 24 ok 25 ok 26 - abs_path works on files ok 27 - chdir to the unreadable directory ok 28 - abs_path when cwd unreadable ok 29 - chdir back to the test dir not ok 30 # Failed test (t/abs_path.t at line 96) # got: '' # expected: '/users/ken/src/pathtools/t/x_but_no_r' not ok 31 # Failed test (t/abs_path.t at line 102) # got: '' # expected: '/users/ken/src/pathtools/t/x_but_no_r/testdir' ok 32 ok 33 ok 34 1..34 # Looks like you failed 8 tests of 34. dubious Test returned status 8 (wstat 2048, 0x800) DIED. FAILED tests 1-3, 18-20, 30-31 Failed 8/34 tests, 76.47% okay Failed Test Stat Wstat Total Fail List of Failed ------------------------------------------------------------------------------- t/abs_path.t 8 2048 34 8 1-3 18-20 30-31 Failed 1/1 test scripts. 8/34 subtests failed. Files=1, Tests=34, 1 wallclock secs ( 0.23 cusr + 0.13 csys = 0.36 CPU) Failed 1/1 test programs. 8/34 subtests failed.
Turns out it's failing because /bin/pwd on OS X fails in this situation too: % mkdir -p t/x_but_no_r/testdir/subdir % chmod 100 t/x_but_no_r % cd t/x_but_no_r/testdir % /bin/pwd pwd: : Permission denied The built-in zsh command 'pwd' succeeds, but of course we can't use that. The closest we can get is something like the $ENV{PWD} memory we get with Cwd's chdir() thingy. -Ken
On Mon Mar 31 21:51:02 2008, KWILLIAMS wrote: Show quoted text
> Turns out it's failing because /bin/pwd on OS X fails in this > situation too: > > % mkdir -p t/x_but_no_r/testdir/subdir > % chmod 100 t/x_but_no_r > % cd t/x_but_no_r/testdir > % /bin/pwd > pwd: : Permission denied > > The built-in zsh command 'pwd' succeeds, but of course we can't use > that. The closest we can > get is something like the $ENV{PWD} memory we get with Cwd's chdir() > thingy. > > -Ken
Michael, Ken: Would it be possible to get an update on the status of the issues discussed in this ticket? Thank you very much. Jim Keenan