http://www.perlmonks.org?node_id=132862

I've just found an extremely obscure bug in perl. This is the tale of that bug - where it came from, and how it was found. I hope you find it entertaining.

Introduction

Yesterday, blakem posted a question about while loops. A while loop in Perl looks like this:
while (CONDITION) { BLOCK }
or like this:
STATEMENT while CONDITION;

The body of the loop (the STATEMENT or BLOCK) is repeatedly executed as long as the condition is true. In Perl, a scalar is false if it is equal, as a string, to either "" or "0". Otherwise it's true. (The undefined value undef has a string value of "", so undef is false.)

But this being Perl, that's not the end of it. Overloading causes one complication, but that's a story for another day. When you're processing a text file, you will often use a loop like this:

while (my $line = <FILE>) { # Do something with the $line }

The problem might come if the file has a line which is blank, or just "0". Ordinarily that wouldn't matter, because the line would be terminated with "\n". So the value of $line would be "\n" or "0\n", which are both true. But what if it's the last line of the file, and it isn't terminated with a newline? Or what if you used the -l command-line switch to perl, which tells it to remove the line terminator automatically? We have to think about these things. The loop condition will then be false, and the loop will terminate before the file's been completely read.

Or will it? Actually, no. Perl is one step ahead, as usual. If Perl sees a loop like the one above, then it will secretly transform it into:

while (defined(my $line = <FILE>)) { # Do something with the $line }
and all is well. Your program works the way that you intended, and peace reigns in the Monastery. You can see this transformation by using the B::Deparse module:

$ perl -MO=Deparse -e 'while(my $line=<FILE>) { print; }' while (defined(my $line = <FILE>)) { print $_; } -e syntax OK

There is one other kind of loop which gets the same magic treatment, and that's if you use the glob operator. The glob operator returns the names of all the files which match a particular wildcard pattern: for example, you can print the names of all the Perl modules in the current directory like this:

while (my $module = glob("*.pm")) { print "Found a module: $module\n"; }
Now, you might have a file called "0", which would cause the same problem as before. So Perl uses the magic defined test here as well.

The old bug

What blakem noticed yesterday is that this magic wasn't working properly in a while loop of the second kind (STATEMENT while CONDITION;). You can test it easily enough: run this code:
my $ok=0; $ok=1 while my $zero = glob("0"); print $ok ? "ok\n" : "not ok\n"
I looked into the source code for perl, and I found a mistake in the code which was testing for that kind of condition. (The code was wrongly checking for a NULL opcode rather than a GLOB opcode.) I wrote up the correction as a patch, and sent it to the perl development mailing list, perl5-porters.

Hugo, one of the porters, replied to my message saying

Interesting - is there any situation in which that test could have succeeded, and so tested definedness when it should have tested truth?
This is starting to get confusing... Let me try and sum up the story up to this point:
  1. In some situations, perl magically puts a defined(...) test around the condition in a while loop.
  2. But the magic wasn't always working properly.
  3. The reason it wasn't working is that there was a mistake in the perl source code...
  4. ... meaning that a different condition was being checked for. (Instead of checking for a GLOB opcode, it was checking for a NULL opcode)
What Hugo asked was whether that condition could ever arise. The question surprised me: I hadn't even considered the possibility of it. So I thought a bit, and poked around a bit, and I realised that the answer was "yes". It can happen!

I won't go too deep into the details, because I don't want this post to turn into a book on the internals of perl. But I'll tell you what I found.

The new bug

I found that the broken condition is triggered by an assignment statement which has a logical operator on its right-hand side. For example,
$foo = $bar || $baz

If a statement like that is used as the condition of a while loop, then perl will (wrongly) insert a defined(...) test. For example,

my $x = 1; die("Oh dear!") while my $foo = $x && 0;
will be silently turned into
my $x = 1; die("Oh dear!") while defined(my $foo = $x && 0);
and so it will die, even though the condition seems to be false!

I wonder if this is the most obscure bug found in perl to date.

Edit Masem 2001-12-18 - Fixed amp; entity to real & in last code blocks.

Replies are listed 'Best First'.
Re: A most obscure bug
by blakem (Monsignor) on Dec 19, 2001 at 02:46 UTC
    Very nice explanation. Perhaps I should fill in the details leading up to my original post:

    While rereading Effective Perl Programming I noticed that it warned against constructs such as: while($line = <FILE>) and recommended using while(defined($line = <FILE>)) instead. The book is several years old ('97 I think??) and I knew that this particular issue had been dealt with in later versions of perl. I had also noticed a "real" erratum in the book and decided to track down the books errata page so I could keep track of such things as I read the book.

    Well, it turns out that I couldn't find a list of errata... The best I could come up with was this empty page which looks lilke it should contain a list of errata but it doesnt. A one line '* this issue has been fixed in later versions' would have ended the story right there.

    I then went looking for the documentation covering this automatic definedness feature. The trivial 'perldoc -f while' doesn't work, because while isn't a function. 'perldoc -q while | grep defined' didn't turn up anything, and searching for 'while and defined' over at perldoc.com or Super Search turned up way too much. I checked a few online docs around here, including perlop to no avail. It turns out that perlop is so long, the Monastery's copy is split in two... the info I was looking for was on the second page of perlop which would have ended my search had I found it.

    Fine, the docs had failed me... I'd just have to figure it out on my own. B::Deparse to the rescue! After deparsing a dozen constructs, I was more confused than ever... <FILEHANDLE> seemed well behaved, I could make a few conclusions about how it would play with definedness and while. However, globbing didn't seem to play by the same rules. Sometimes it added the defined() sometimes it didn't. Hindsight shows that there was really only one case that didn't play nicely, but at the time it confused the hell out of me.

    Frustrated, I posted to perlmonks, and the rest is covered quite nicely above.

    So, this bug would have gone unnoticed had we not a missing errata page, a split man page and an intrepid new monk (i.e. robin) who happens to be a "sometime perl5-porter."

    P.S. Thanks robin, you made my day.

    -Blake

Re: A most obscure bug
by chip (Curate) on Dec 19, 2001 at 12:46 UTC
    Nice work, robin!

    But now your meditation has made me curious about the origin of the bug you found. Was the implied-defined() feature broken from the beginning -- which seems unlikely -- or was it broken later and nobody noticed, and if so, how and when?

    sigh So many intriguing bugs, so little time.

        -- Chip Salzenberg, Free-Floating Agent of Chaos

      As far as I can tell, it was broken from the beginning. I haven't managed to track down the original patch which added the implicit defined() feature though.

      Can you remember when it was added? Was it during your reign, or later than that?

        The automatic implied defined() was added after my time as pumpking. When I discovered the danger of while (<>) {}, I didn't feel I could change the language without breaking things, so I added a warning instead. That warning was unsatisfactory to a lot of people, so the issue came up again, this time during one of Larry's more available periods. Somebody proposed the language change we have today, Larry OK'd it, and the patch was installed almost immediately.

            -- Chip Salzenberg, Free-Floating Agent of Chaos

Re: A most obscure bug
by hagen (Friar) on Dec 19, 2001 at 03:09 UTC

    Thanks for a very well crafted technical insight into Perl's smarts!

    As a newbie this gave me an idea on what it is Perl does "behind the scenes" for its users.

    Even more interestingly it gives and example the process of research, review and repair working in the Open Source arena. I can use this to show others and calm their "but who supports it?" fears.

    Thanks again for taking the time to post.

    robin++ of course.

Re: A most obscure bug
by Aristotle (Chancellor) on Dec 18, 2001 at 22:35 UTC
    robin++, great detective work! And thumbs up to Masem also for spotting the while inconsistency.
Re: A most obscure bug
by dragonchild (Archbishop) on Dec 18, 2001 at 20:43 UTC
    Ummm .. Robin?
    my $x = 1; die "Oh dear!\n" while my $foo = $x && 0; print "ok!\n"; ------ Oh dear!
    It does die under 5.6.1 ... unpatched.

    ------
    We are the carpenters and bricklayers of the Information Age.

    Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

      Exactly! It shouldn't die, because the condition is false and so the loop body should never be executed.
      c:\>perl -v This is perl, v5.8.4 built for MSWin32-x86-multi-thread (with 3 registered patches, see perl -V for more detail) c:\>perl my $x = 1; die "Oh dear!\n" while my $foo = $x && 0; print "ok!\n"; ^D ok!