system() with timeout

Aaron Crane perl at
Tue Apr 21 13:12:06 BST 2009

Abigail writes:
> On Thu, Apr 16, 2009 at 08:34:16PM +0100, David Cantrell wrote:
> >     eval {
> >         if(my $pid = fork()) { # parent
> >             local $SIG{ALRM} = sub {
> >                 kill 9, $pid; # quit RIGHT FUCKING NOW
> >                 die("Child process timed out\n");
> >             };
> >             alarm(5);
> >             waitpid($pid, 0);
> >             alarm(0);
> >         } else {
> >             exec(@blah);
> >         }
> >     };
> I wonder whether that has a race condition. Could it be possible that
> the following happens:
>   - child finishes.
>   - waitpid reaps the child.
>   - another process is started, with PID equal to $pid.
>   - 5 seconds are up and $SIG{ALRM} kicks in.

Yes, I agree.

I also think there's no way of closing the race window.  Whatever you
do to determine that the process $pid is what you expect it to be, you
can be rescheduled between that point and when you actually deliver
the SIGKILL to $pid.

If you can guarantee that the process to be timed out won't change its
process group, I think this would work:

- Parent process forks a child C and waits for it
- C creates a new process group with setpgrp(0, 0)
- C forks a grandchild G which will exec the desired process
- C sets up a SIGALRM handler that will SIGKILL the newly-created
  process group; if the SIGKILL gets delivered, it will kill C also
- C waits for G, and disables the alarm() when done
- C exits with the status it found by waiting for G

There's still a race there, but the consequence is now merely that C
exits with a SIGKILL status even though G exited successfully.  Since
that would have happened anyway if G had taken just a little longer to
complete, I consider that acceptable.

This approach also has the advantage of killing any children that G
itself has forked (although it might be preferable anyway for G to
setrlimit(RLIMIT_NPROC) with both soft and hard limits set to 0 before
executing the desired process).

It may be possible to deal with arbitrary processes that try to change
their process group by using ptrace(2) (or an equivalent mechanism on
your preferred OS).  This would count as a heroic measure, in my book.

In the absence of something like that, I can't think of a way to avoid
the race for an arbitrary process that might change its process group.
Fortunately, the window is normally going to be extremely small; it
relies on the pid being reused very quickly compared to the reschedule
interval.  In practice, I suspect it would be hard to trigger the race
in the absence of a malicious attacker who isn't subject to ulimits
and who's playing tricks like filling up the process table.  It may be
that the risk of encountering the race is sufficiently low as to make
it not worth worrying about.

There's another problem for an arbitrary process: if it's setuid, you
may not have permission to send it a signal, on some systems.  That
doesn't seem to be a problem given what David's trying to do here,

Aaron Crane **

More information about the mailing list