Unix Programming - fork/exec race condition with signals

This is Interesting: Free IT Magazines  
Home > Archive > Unix Programming > July 2006 > fork/exec race condition with signals





You are viewing an archived Text-only version of the thread. To view this thread in it's original format and/or if you want to reply to this thread please [click here]

Author fork/exec race condition with signals
skillzero@gmail.com

2006-07-19, 8:04 am

I'm seeing a race condition when fork/exec'ing a process. My process
catches SIGTERM to do some cleanup, but also sends SIGTERM to processes
that it fork/exec's (to quit them as needed). The problem is that my
process (the parent) calls fork and then continues its normal
processing. The child of the fork does an exec, but if its parent (my
process) sends that child a SIGTERM in the window between fork and
exec, the child is still running the code of my process so it ends up
running the SIGTERM handler of the child instance of my process. My
SIGTERM handler writes to an internal IPC pipe to tell a thread to
quit. Because file descriptors are inherited, this ends up causing the
thread in the parent process to think it needs to quit when the child
get a SIGTERM in this window (some signal handlers are in libraries so
I can't just check the pid).

I thought I could just block all signals before fork and restore them
in the parent after fork, but it seems sigprocmask is preserved even
across exec so any signals ignored in the parent would end up also
being ignored in the newly exec'd process, which I don't want because
that process may want the default behavior for those signals.

The best I could come up with is to block all signals before fork then
from the child, reset signals I know the parent has ignored and unblock
all signals in the child before calling exec, like this:

sigfillset( &newSet );
sigprocmask( SIG_BLOCK, &newSet, &oldSet );
pid = fork();
if( pid == 0 ) // Child
{
signal( SIGPIPE, SIG_DFL ); // SIGPIPE was ignored in parent

sigfillset( &newSet );
sigprocmask( SIG_UNBLOCK, &newSet, NULL );
exec...
}
else
{
sigprocmask( SIG_SETMASK, &oldSet, NULL );
}

Is this good enough? Is there an easier way? I don't like that I have
to just "know" which signals I've ignored and reset them explicitly
because my process uses lots of libraries that may ignore signals
behind my back. I didn't see any sigset iteration functions. I was
hoping for some way to say "exec this process, but restore all signals
to their defaults".

William Ahern

2006-07-19, 7:25 pm

On Wed, 19 Jul 2006 03:27:56 -0700, skillzero@gmail.com wrote:

> I'm seeing a race condition when fork/exec'ing a process. My process
> catches SIGTERM to do some cleanup, but also sends SIGTERM to processes
> that it fork/exec's (to quit them as needed). The problem is that my
> process (the parent) calls fork and then continues its normal processing.
> The child of the fork does an exec, but if its parent (my process) sends
> that child a SIGTERM in the window between fork and exec, the child is
> still running the code of my process so it ends up running the SIGTERM
> handler of the child instance of my process. My SIGTERM handler writes to
> an internal IPC pipe to tell a thread to quit. Because file descriptors
> are inherited, this ends up causing the thread in the parent process to
> think it needs to quit when the child get a SIGTERM in this window (some
> signal handlers are in libraries so I can't just check the pid).
>
> I thought I could just block all signals before fork and restore them in
> the parent after fork, but it seems sigprocmask is preserved even across
> exec so any signals ignored in the parent would end up also being ignored
> in the newly exec'd process, which I don't want because that process may
> want the default behavior for those signals.
>
> The best I could come up with is to block all signals before fork then
> from the child, reset signals I know the parent has ignored and unblock
> all signals in the child before calling exec, like this:
>


That's fine if you're okay missing any SIGTERMs in the interim.

Another solution is to do have the parent pause until the child notifies
that it's in a stable state:

int fd[2]
int n;
char c;

if (0 != pipe(fd]))
/* HANDLE ERROR */;

switch (fork()) {
case -1:
/* HANDLE ERROR */;
case 0: /* CHILD */
while (0 != close(fd[0]) && errno == EINTR)
;;

/*
* Install default signal handlers and masks
*/

/*
* Notify parent. Technically we could skip
* this and simply rely on EOF when we do the
* close.
*/
do {
n = write(fd[1], 'a', 1)
} while (n == -1 && errno == EINTR);

while (0 != close(fd[1] && errno == EINTR)
;;

exec(...);

/* NOT REACHED */
default: /* PARENT */
while (0 != close(fd[1]) && errno == EINTR)
;;

do {
n = read(fd[0], &c, 1);
} while (n == -1 && errno == EINTR);

break;
}


skillzero@gmail.com

2006-07-19, 7:25 pm

> That's fine if you're okay missing any SIGTERMs in the interim.

I'm not sure I see how SIGTERMs would be missed? If I block signals
then unblock them, any that occur for the parent during that window
should be pending and delivered when the unblock occurs, right?

> while (0 != close(fd[1] && errno == EINTR)
> ;;
>
> exec(...);


It looks like this would have the same race condition since the child
could still be sent a signal after fork and before exec. Or maybe I'm
misunderstanding something?

After doing some more reading and experimenting, I don't think I can
solve this with sigprocmask because my app is multi-threaded.
sigprocmask doesn't seem to block the signals except maybe in the
current current thread (POSIX says its undefined behavior and my tests
show that I still get the signal even when blocked with
sigprocmask...only from a different thread).

I hope I'm just misunderstanding something, but it appears that
blocking signals is basically broken for multi-threaded apps because
even pthread_sigmask only blocks the calling thread and there's no way
I know of to get all the threads to block signals in all of them.

So what I came up with is to essentially do this:

oldSIGTERM = signal( SIGTERM, SIG_DFL );
pid = fork();
if( pid == 0 ) // Child
{
signal( SIGPIPE, SIG_DFL ); // Parent was ignoring SIGPIPE
exec...
}
else
{
signal( SIGTERM, oldSIGTERM );
}

This has a race condition such that if I receive a SIGTERM in the
parent after setting it to SIG_DFL, it's going to terminate the process
without calling my signal handler, but I couldn't figure out any other
way to temporarily block signals for the parent process. Luckily in my
case, it's a closed system and SIGTERM is only sent manually or when
something serious is wrong such that graceful cleanup isn't critical.

Sponsored Links






Free braindumps | Software forum | Database administration forum

Copyright 2003 - 2008 webservertalk.com