Telephone +44(0)1524 64544
Email: info(at)shadowcat.co.uk

Taint mode, testing, system() and stupid $^X hacks

Wed Jun 3 22:00:00 2009

Digg! submit to reddit Delicious RSS Feed Readers

Tainted Love

So. Taint mode. Great idea. Not something I generally use because my code has already got several layers of validation before it does anything dangerous. Maybe this makes me a bad person. Never mind.

In fact, my primary interaction with taint mode is when people fsck up their test suites.

Here's the usual way they mess this up:

#!perl -T

...

system($^X, '-T', 'other_taint_script.pl', @args);

See anything obviously wrong here yet? Well, neither did the author. But your module will not install on a local::lib using build system.

Using $^X is good, that means you get the same binary that the current script was invoked with. But. Taint mode ignores PERL5LIB. Now, Test::Harness fixes this for your top level t/ files by basically doing:

system($^X, (map "-I$_", split /:/, $ENV{PERL5LIB}), ...);

but once you're in a test script, if you want things to work, you have to do that yourself:

my @command = ($^X);
# must unroll PERL5LIB into -I when calling -T test script
if ($ENV{$PERL5LIB}) {
  push @command, map "-I$_", split /:/, $ENV{PERL5LIB};
}
push @command, '-T', 'other_taint_script.pl', @args;
system(@command);

Note that, yes, there are more elegant ways to put the command line together. I was aiming for "simple" here since I have a horrible feeling somebody's going to cargo cult it so I'd rather it was obvious if not ideally beautiful.

Anyway ... this isn't actually what I was trying to do when I started going "argh, taint mode!" this time. What I was trying to do was something significantly crazier: Get a module loaded at interpreter startup for every test. Now, in the absence of taint, PERL5OPT='-MFoo' will work fine. But taint, correctly, ignores this env var.

So I thought "well, I can just mess with the Test::Harness options". Then I remembered the "chained -T script" thing and cried for a bit. Then I tried overriding CORE::GLOBAL::system.

Turns out you can't do that. Well, you sort of could, in theory, but you'd need a lot of Devel::Declare because at some point somebody thought it'd be clever to use indirect object style syntax to extend system's capabilities:

system { $command } $argv_0, @args;

And while I'm sure I could inject Devel::Declare into the process and make that work ... I don't want to. I really don't want to confuse the test script, so minimalism is the name of the game.

So then I thought, well what if I override the op construction for system and exec and if the first arg is $^X then ... oh my god no that's horrible.

So then I thought, maybe I could wrap the perl binary and inject stuff. Except that would also be horrible, since It Isn't Designed To Work Like That.

Except ... taint mode -doesn't- stop command line args adding -I lines - that's how we manage to get things to work at all in the normal case. So ... ah, it doesn't stop you adding -M lines either. Which means ... if I create a fakeperl script like so:

#!/usr/bin/perl

exec($^X, '-I/path/where/my/madness/lives', '-MMadness', @ARGV);

and create a module Madness.pm that does:

$^X = '/path/to/fakeperl';

and I run:

./fakeperl Makefile.PL

Then Makefile.PL sees the $^X, checks it smells like a perl interpreter, and uses that as the perl for the test invocations, which means that the invoked t/ script has $^X set, which means when the final system runs, our module still gets invoked!

Of course, it doesn't work with #! lines since the #! line can't be a script. But I haven't needed to care about that yet, so long as I remember that for Module::Build using stuff I have to do "perl Build test" rather than "./Build test". Which, honestly, isn't a big deal right now.

The real solution is probably to make fakeperl a tiny C program that does that exec, but until I actually need to do that, I'm not going to bother.

Oh, and if you're wondering why the hell I'm doing this? Watch this space, all will be revealed in future posts ...