A Question of Location

The great detective was staring at the door, as he had done for the past two weeks. He needed a case to occupy that mind. Thankfully today the door opened and Holmes had a case.

A man stepped in and introduced himself as Mr. Mokko.

“Watson, here is a man having troubles with Perl.”

“Holmes, how can you know that?”

“We can tell from the lines on his face, the stiff wrists and the pads of his fingers that he a much maligned sysadmin; from the Hawaiian shirt and the hat that he’s into the Perl scene. The scowl and the fact that he’s here on our doorstep tells me that he’s having troubles.”

Holmes looked back to the man and said, “Come now, you must tell me the tale.”

“Mr. Holmes I’m at my wits end. I would like to install Alien::Base, but it depends on File::chdir which will not install on Windows using cygwin. You may recall that the module provides a convenient mechanism for changing the working directory in a local way, but this does me no good if I cannot install it.

“When I make the attempt, a test dies with the message

t/newline.t ....... Cannot chdir back to /home/maurice/projects/File-chdir-0.1007/t/testdir3492: No such file or directory at /home/maurice/projects/File-chdir-0.1007/blib/lib/File/chdir.pm line 21.

“Thinking that you might need a little more I have brought you the relevant test. The closing curly brace is line 21.” He handed Holmes a sheet that contained the following:

use File::chdir;
use Cwd qw(abs_path);

my $Orig_Cwd = $CWD;

my $Test_Dir = "t/testdir$$\ntest";
mkdir $Test_Dir;

{
    local $CWD = $Test_Dir;
    is $CWD, abs_path;
}

“I have been to the Yard and talked to Chief Inspector Solomon Silver, and he has advised me to consult the author of Alien::Base and urge him to move to File::pushd instead. Should I do this? I hear that its author is rather fond of the offending module.”

Holmes looked amused, “Silver is an accomplished detective, yet you’ve done well to come to me. Tell me, have you looked at the source for File::chdir? Surely it must contain some clues to this mystery.”

“Why yes, it does contain this which I thought might be useful.” He produced another sheet, containing:

package File::chdir;
use 5.004;
use strict;
use vars qw($VERSION @ISA @EXPORT $CWD @CWD);

require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(*CWD);

use Cwd 3.16;
use File::Spec::Functions 3.27 qw/canonpath splitpath catpath splitdir catdir/;

tie $CWD, 'File::chdir::SCALAR' or die "Can't tie \$CWD";

package File::chdir::SCALAR;

sub _abs_path {
    # Otherwise we'll never work under taint mode.
    my($cwd) = Cwd::abs_path =~ /(.*)/s;
    return canonpath($cwd);
}

sub _chdir {
    my($new_dir) = @_;

    if ( ! CORE::chdir($new_dir) ) {
        croak "Failed to change directory to '$new_dir': $!";
    };
    return 1;
}

sub TIESCALAR { bless [], $_[0] }

# To be safe, in case someone chdir'd out from under us, we always
# check the Cwd explicitly.
sub FETCH {
    return _abs_path;
}

sub STORE {
    return unless defined $_[1];
    _chdir($_[1]);
}

I looked at it and exclaimed, “Holmes, its extraordinary! Such simplicity! But I cannot see what should cause the problem. Ack, what shall we do!”

“My dear Watson, the evidence is overwhelming! Can you really not see it? But you have started down the road to a solution already I see. Thank you, Mr. Mokko for bringing this interesting case to my attention. Watson, we shall have to canvas the surrounding area for clues, come, there’s not a moment to lose!” Holmes grabbed his hat and fled the room, leaving our guest to the care of Mrs. Hudson.

When I reached him, he had flagged a hansom and was waiting for me impatiently as I entered. I told him I had no idea what his last remark had meant, but my friend had slipped into one of his trances. His eyes were on his hands, the fingers pressed together firmly. Once I thought I heard him mutter “ack” under his breath, but I couldn’t be sure.

When the carriage came to a halt I noticed we were outside a strange pub named “The Dancing Pet.” We entered and the room was empty but for the barkeep, drying some glasses. Holmes approached exclaimed, “Andy, I’m so very glad to see you!”

The proprietor, it seems, had once been a detective in his own right. Holmes wasted no time.

“Andy I need you to look for the phrase ‘Cannot chdir back’ throughout the Perl source.”

No sooner than he asked, Andy set to work. He cloned the Perl source then ran

$ ack 'Cannot chdir back'
dist/Cwd/Cwd.pm
660:    _croak("Cannot chdir back to $cwd: $!");

“It is as I suspected, File::chdir did not throw that warning. The message, though similar, was not exactly right.”

“Why Holmes, you’re right! But now there’s a problem. That line comes from Cwd::fast_abs_path which is never called anywhere in the test program.”

“Indeed that is troubling. What else do you see in that error message, Watson?”

“How did I miss it before? The folder name in the warning doesn’t contain a newline, nor anything after it! But where did it go?”

“Look again, Watson; consider the untainting code.”

sub fast_abs_path {
    local $ENV{PWD} = $ENV{PWD} || ''; # Guard against clobberage
    my $cwd = getcwd();
    require File::Spec;
    my $path = @_ ? shift : ($Curdir ||= File::Spec->curdir);

    # Detaint else we'll explode in taint mode.  This is safe because
    # we're not doing anything dangerous with it.
    ($path) = $path =~ /(.*)/;
    ($cwd)  = $cwd  =~ /(.*)/;

    unless (-e $path) {
        _croak("$path: No such file or directory");
    }

    if (!CORE::chdir($path)) {
        _croak("Cannot chdir to $path: $!");
    }
    my $realpath = getcwd();
    if (! ((-d $cwd) && (CORE::chdir($cwd)))) {
        _croak("Cannot chdir back to $cwd: $!");
    }
    $realpath;
}

“Well there it is, the untainting code doesn’t have the /s flag and thus stops matching at a newline.”

“It is devious Watson, fast_abs_path gets the full path to a directory by chdir-ing to it, getting the working directory by getcwd then chdir-ing back. However if either path contains a newline, the path is truncated and the chdir-ing fails, with an unexpected, and in this case very misleading warning!”

“Misleading indeed. But why is this function called at all?”

“Remember that this message was generated on cygwin Doctor. Why should this happen there and not elsewhere?”

“Perhaps I can help there too, gentlemen,” said Andy from across the bar. “On cygwin it seems that *Cwd::abs_path is aliased to \&Cwd::fast_abs_path. It must be that the xs version of abs_path doesn’t function correctly on cygwin, putting that platform in the firing path of this bug.”

“Great scott! But Holmes this would mean that all platforms are vulnerable.”

“Watson we must do more tests!”

We bid Andy farewell and thanked him. Back at Baker Street, Holmes went straight to his test bench.

“We can do a quick experiment here on our Linux system. Create a directory with a newline, chdir into it then run this one-liner.”

Holmes handed me a scrap of paper with a command on it. I executed these statements.

$ mkdir 'hello
world'
$ cd 'hello
world'
$ perl -MCwd -E 'say Cwd::fast_abs_path'

And to my amazement, it died saying

Cannot chdir back to /home/watson/hello: No such file or directory at -e line 1

“By Jove! Any code that uses fast_abs_path is subtly broken!”

“Worse, Watson, any code that uses abs_path and might be executed on cygwin is similarly broken. And do you know what is the saddest part of this case Watson? File::chdir needn’t have used Cwd::abs_path in the first place. When called without argument, that function performs the same task as Cwd::getcwd which is not broken as far as I can tell! Be sure to tell your readers when you publish this tale Watson and perhaps some good can come from it all!

“This case is made most interesting from the fact that the module in question deals in chdir-ing and yet it turns out that it was not its _chdir function that caused the problems (nor its tied setter STORE), but rather the tied class’ accessor (FETCH) and the test suite calling abs_path that threw the ‘Cannot chdir’ warning!”

A bug has been filed on Perl: RT#115962 and a pull-request has been opened on File::chdir. Note that some code shown has been simplified slightly for space considerations. With stylistic thanks to Tom Wyant and apologies to Sir Arthur Conan Doyle.

5 Comments

Lovely piece of writing. Thanks :)

Wonderful! A piece of deduction worthy of the character in whose mouth you have placed it.

I await the next with bated breath.

Loved it!

Can’t wait for “A study() in RedHat” or “The round()s of Posixville” :D

Leave a comment

About Joel Berger

user-pic As I delve into the deeper Perl magic I like to share what I can.