Php – Why won’t an alarm handler fire in a process run from a php process

apache-2.2PHP

I don't have much experience with PHP/mod_php administration, so I apologize if this is a really simple question.

My question is this – why won't a process that I've spawned from a PHP script via the exec() call receive an alarm interrupt properly?

The long version of my question:

This morning I was handed a bug in an existing php script. After some investigation, I've traced it down to the (apparent) fact that the php was using exec() to run a subprocess, the subprocess was relying on a SIGALRM to escape a loop, and it never received the alarm.

I don't think it matters, but the specific subprocess was /bin/ping. When pinging a device that doesn't return any packets (such as a device w/ a firewall that discards ICMP echo requests instead of returning destination host unreachable), you have to use the -w option to set a timer to allow the program to exit (because -c counts return packets – if the target never returns packets and you don't use -w, you're stuck in and endless loop). When called from the php, the alarm handler that ping -w relies on doesn't fire.

Here're a few interesting lines from using strace to follow the ping call from the command line (where the alarm handler does work):

(snip)
setitimer(ITIMER_REAL, {it_interval={0, 0}, it_value={1, 0}}, NULL) = 0
(snip)
--- SIGALRM (Alarm clock) @ 0 (0) ---
rt_sigreturn(0xe)                       = -1 EINTR (Interrupted system call)

When I inserted a shell wrapper to allow me to run strace on the ping when called from the web, I found that the setitimer call is present (and appears to run successfully), but that the SIGALRM line and rt_sigreturn() lines aren't present. The ping then continues to run sendmsg() and recvmsg() forever until I kill it by hand.

Trying to reduce variables, I then cut ping out of it and wrote the following perl:

[jj33@g3 t]# cat /tmp/toperl 
#!/usr/bin/perl

$SIG{ALRM} = sub { print scalar(localtime()), " ALARM, leaving\n"; exit; };

alarm(5);

print scalar(localtime()), " Starting sleep...\n";

sleep (10);

print scalar(localtime()), " Exiting normally...\n";

It works as expected when run from the command line, the alarm handler fires successfully:

[jj33@g3 t]# /tmp/toperl 
Mon May  2 14:49:04 2011 Starting sleep...
Mon May  2 14:49:09 2011 ALARM, leaving

Then I tried running /tmp/toperl via the same PHP page (via both exec() and backticks) that was having problems calling ping. Here's the php wrapper I wrote for the test:

<?
print "Running /tmp/toperl via PHP\n";

$v = `/tmp/toperl`;

print "Output:\n$v\n";
?>

As with ping, /tmp/toperl did not receive its alarm interrupt:

Running /tmp/toperl via PHP
Output:
Mon May  2 14:52:19 2011 Starting sleep...
Mon May  2 14:52:29 2011 Exiting normally...

Then I wrote a quick cgi wrapper in perl to execute in the same Apache, but under mod_cgi instead of mod_php. Here's the wrapper for reference:

[jj33@g3 t]# cat tt.cgi
#!/usr/bin/perl

print "Content-type: text/plain\n\n";

print "Running /tmp/toperl\n";

my $v = `/tmp/toperl`;

print "Output:\n$v\n";

And, lo and behold, the alarm handler worked:

Running /tmp/toperl
Output:
Mon May  2 14:55:34 2011 Starting sleep...
Mon May  2 14:55:39 2011 ALARM, leaving

So, back to my original question – why won't a process I've spawned via exec() in a mod_php controlled PHP script receive an alarm signal when the same spawned process will do so when called from the command line and perl/mod_cgi?

Apache 2.2.17, PHP 5.3.5.

Thanks for any thoughts.

EDIT – DerfK was correct, mod_php is masking out SIGALRM before calling the sub process. I don't have any interest in recompiling ping so I'll end up writing a wrapper for it. Since I already wrote so much text for this question I thought I would also drop in a revision to my toy program /tmp/toperl that tests to see if SIGALRM is being masked out and unblocking it if so.

#!/usr/bin/perl

use POSIX qw(:signal_h);

my $sigset_new = POSIX::SigSet->new();
my $sigset_old = POSIX::SigSet->new();

sigprocmask(SIG_BLOCK, $sigset_new, $sigset_old);

if ($sigset_old->ismember(SIGALRM)) {
  print "SIGALRM is being blocked!\n";
  $sigset_new->addset(SIGALRM);
  sigprocmask(SIG_UNBLOCK, $sigset_new);
} else {
  print "SIGALRM NOT being blocked\n";
}

$SIG{ALRM} = sub { print scalar(localtime()), " ALARM, leaving\n"; sigprocmask(SIG_BLOCK, $sigset_new, $sigset_old); exit; };

alarm(5);

print scalar(localtime()), " Starting sleep...\n";

sleep (10);

print scalar(localtime()), " Exiting normally...\n";

Now this test works correctly (meaning it exits after 5 seconds with the "ALARM, leaving" line) in all instances (perl/command line, php/command line, perl/mod_cgi, php/mod_php). In the first three instances it prints the 'SIGALRM NOT being blocked' line, in the latter it prints 'SIGALRM is being blocked!' and correctly unblocks it.

Best Answer

Mod_php probably blocks this (using sigprocmask() I presume, masks are maintained through fork() and execve()) in order to prevent signals from mangling Apache (since mod_php runs PHP in apache's process).

If it's due to sigprocmask(), then I think you should be able to use perl's POSIX module to undo it in the exec()'d script, but I'm not exactly sure how it works. The Perl Cookbook has an example of blocking then unblocking SIGINT. I think it should be something like

use POSIX qw(:signal_h);
$sigset=POSIX::SigSet->new(SIGALRM);
sigprocmask(SIG_UNBLOCK,$sigset);

If that doesn't work, then maybe try installing php5-cgi, setting it up as a Handler in Apache for a different extension (say, .phpc) then renaming the script to ping.phpc and updating the links. Since CGI executes in its own process, the CGI version of PHP may not lock down the signals.

Related Topic