The use base.pm and SIGDIE problem, still not fixed in Perl 5.8.8

This weekend I’ve been playing around with a “from scratch” idea for an app/framework thingy. I’ve been assembling the pieces in my head, in notes and in reading around. While I doubt its going to become the next “IT” framework, even if it gets released to the public, I’m hoping it will give me a clearer understanding of how to make a modern web app in Perl.

On of the things I’ve been trying to do is make the core of the app fairly sensible in terms of setting up lots of stuff by default. So, for example, to default to using UTF8 encoding and Localization as well as basic stuff like signal handlers. I cheerily set up my SIGDIE handler in my initApp() method to handle stuff like checking if headers need sending before dumping the error message back to the browser and moved on.

After getting the basic thing working a little I decided to approach Localization (L10N). I’ve never tried this before and I have to be honest the suggestions on how to do this in Perl … err.. lack. I found myself hitting the same few articles time and time again. Heck even Bugzilla’s roadmap uses the same list of articles quoted else where (TJP13, Locale::Maketext::Lexicon, and the Cpanel.net roadmap for the future of Cpanel), but as with most things Perl trying and poking seems to get you somewhere.

In my case it got me more than I’d bargained for because in trying to make my app flexible the localization is optional, and more so, tried to make the use of Locale::Maketext::Lexicon optional so you can use other modules if you prefer. This meant that I was a prime candidate for the “use base” bug.

If you haven’t come across this bug then its pretty simple. There’s a bug in base.pm in which the die signal handler entry in %SIG is local()ized for part of import(). This is great because its localized and the author makes an attempt to restore it afterward to ensure it was reset correctly. Unfortunately it goes horribly wrong. And I’ve just wasted another couple of hours tracking this crap down only to discover that because this is a relatively new machine I hadn’t patched base.pm on it unlike my other servers … *grr*.

If you don’t know the bug a solution is documented on the perl5 mailing list. The code block in question is this:

 85             my $sigdie;
 86             {
 87                 local $SIG{__DIE__};
 88                 eval "require $base";
 89                 # Only ignore "Can't locate" errors from our eval require.
 90                 # Other fatal errors (syntax etc) must be reported.
 91                 die if $@ && $@ !~ /^Can't locate .*? at \(eval /;
 92                 unless (%{"$base\::"}) {
 93                     require Carp;
 94                     Carp::croak(<<ERROR);
 95 Base class package "$base" is empty.
 96     (Perhaps you need to 'use' the module which defines that package first.)
 97 ERROR
 98                 }
 99                 $sigdie = $SIG{__DIE__};
100             }
101             # Make sure a global $SIG{__DIE__} makes it out of the localization.
102             $SIG{__DIE__} = $sigdie if defined $sigdie;

The general idea here is that base.pm doesn’t want a custom SIGDIE handler interfering with what’s going on and so overrides it and tries to reset it. The bug is that this doesn’t happen. What happens is that your signal handler is always removed. Why? Well that’s because line 87 causes SIGDIE to be defined, but to be defined with no value. It seems that this is the default for signal handlers, even though it doesn’t appear to happen for other variables, including hashes:

#!/usr/bin/perl
use strict;
use warnings;
use vars qw(%A);

print "\$A{B} def: ", defined($A{B}) ? 'Yes' : 'No', "\n";
$A{B} = 1;
print "\$A{B} def: ", defined($A{B}) ? 'Yes' : 'No', "\n";
local($A{B});
print "\$A{B} def: ", defined($A{B}) ? 'Yes' : 'No', "\n";

print "\$SIG{__DIE__} def: ", defined($SIG{__DIE__}) ? 'Yes' : 'No', "\n";
$SIG{__DIE__} = 'IGNORE';
print "\$SIG{__DIE__} def: ", defined($SIG{__DIE__}) ? 'Yes' : 'No', "\n";
local($SIG{__DIE__});
print "\$SIG{__DIE__} def: ", defined($SIG{__DIE__}) ? 'Yes' : 'No', "\n";

This outputs the following. Can you spot the problem?

$A{B} def: No
$A{B} def: Yes
$A{B} def: No
$SIG{__DIE__} def: No
$SIG{__DIE__} def: Yes
$SIG{__DIE__} def: Yes

That’s right. In a normal hash once we localize a hash variable then we end up with it not being defined, irregardless of whether or not the original var was defined. This doesn’t hold true for %SIG. In the case of %SIG localizing it causes the elements to be defined. ALWAYS.

Normally this isn’t a problem because you don’t use these, and besides which the value is defined but zero. But in the way this is handled in base.pm, its a problem because of the strange way that SIGDIE is restored from the var $sigdie, but $sigdie is assigned to after the call to local():

 99                 $sigdie = $SIG{__DIE__};
100             }
101             # Make sure a global $SIG{__DIE__} makes it out of the localization.
102             $SIG{__DIE__} = $sigdie if defined $sigdie;

Thanks to above quirk with %SIG and local in this situation $sigdie (line 99) is ALWAYS defined, but because $sigdie is assigned to after local() is used, $sigdie doesn’t contain your signal handler, but instead contains “”. So we hit line 102 where the block is conditional on $sigdie being defined, which it always is, remember, and thus the code always assigns “” to $SIG{__DIE__} after loading the given module. As line 102 is outside of the block in which local() is used at this point %SIG is the actual %SIG (the global copy if you prefer). Therefore this assignation has the effect of wiping out any signal handler you may have installed despite the fact that it is trying to achieve exactly the opposite and restore the original signal handler.

The solution mentioned above is to use || undef on line 99. This has the effect of assigning undef to $sigdie because the signal handler is “”, or false, thus line 102 doesn’t pass the conditional and install this “” as the SIGDIE handler. It works, but I have to ask, why bother? It seems like the better way is to simply remove all of the $sigdie stuff and localize %SIG resetting it to its default, and then let the code exit the block. When it does the localized version of %SIG is removed and there’s no risk of clobbering an existing handler.

An alternative, if you must use $sigdie, is to assign the current SIGDIE handler as you’re declaring it, thus:

 85             my $sigdie = $SIG{__DIE__};
 86             {
 87                 local $SIG{__DIE__};

Then remove line 99 where the value is messed up:

 99                 $sigdie = $SIG{__DIE__};

Having just checked for updated modules the other day, I have to say, I’m very surprised that there isn’t a patched version of this which has been sent out to versions of Perl with broken versions. As I understand it 5.10 doesn’t suffer this problem, but since the stable version is still considered to be 5.8.8 it seems like it should be updated too. It would save people like me wasting a few hours trying to track down what’s going on with their code….

Colin.

Advertisements

One Response to The use base.pm and SIGDIE problem, still not fixed in Perl 5.8.8

  1. Milhomem says:

    Great article, this works for me.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: