Change vfork_and_run implementation to inspect the kernel's process tables to determine whether the child has really exec'd or exited.

This should avoid strange behavior due to races when the parent has resumed but the child is still running the exec* code in libc, which mainly manifests itself when running at low speed.

We also change to signaling the child's completion with SIGALRM, and setting an extra alarm in the parent in case the child doesn't actually do it.
This commit is contained in:
Stephen Heumann 2014-11-10 16:08:24 -06:00
parent 6d5e65b9df
commit c37475dcbc
5 changed files with 84 additions and 40 deletions

View File

@ -1636,7 +1636,8 @@ static ALWAYS_INLINE unsigned char bb_ascii_tolower(unsigned char a)
/* Signal to the parent that it can resume executing after a fork,
* because the child is about to exec or terminate.
*/
int signal_parent_to_resume(void);
void signal_parent_to_resume(void);
int is_forked_child(void);
/* Get path of current executable */
char *get_exec_path(void);

View File

@ -49,6 +49,7 @@ void FAST_FUNC kill_myself_with_sig(int sig)
{
signal(sig, SIG_DFL);
sig_unblock(sig);
signal_parent_to_resume();
raise(sig);
_exit(sig | 128); /* Should not reach it */
}

View File

@ -27,10 +27,23 @@ pid_t vfork_and_run(void (*fn)(void*) NORETURN, void *arg) {
#else
# include <signal.h>
# include <gno/kvm.h>
# include <orca.h>
/* Turn off all ORCA/C stack repair code to avoid corruption. */
#ifdef __ORCAC__
# pragma optimize 72
#endif
# ifdef __ORCAC__
# pragma optimize 72
# endif
# pragma databank 1
void fork_thunk(void (*fn)(void*) NORETURN, void *arg, long sigmask) {
sigsetmask(sigmask);
fn(arg);
}
# pragma databank 0
const char * forked_child_name = "hush (forked)";
pid_t vfork_and_run(void (*fn)(void*) NORETURN, void *arg) {
/* GNO's fork2 call will return immediately and allow the parent and
@ -39,31 +52,60 @@ pid_t vfork_and_run(void (*fn)(void*) NORETURN, void *arg) {
* behavior like a traditional vfork() implementation, where the
* parent blocks until the child terminates or execs.
*
* Our approach will be to have the child send SIGUSR2 to the parent
* Our approach will be to have the child send SIGALRM to the parent
* just before it terminates or execs, and block waiting for that here.
*
* It's tempting to use waitpid(). That would have the advantage of
* catching cases where the child process terminates abruptly, but
* GNO's implementation of waitpid() is a wrapper around wait() and
* therefore is buggy: it may swallow the information about termination
* of other child processes (ones that forked earlier and have already
* exec'd), which we want to avoid.
* We also set an alarm in case the child doesn't signal.
*
* When we get SIGALRM, we check the process tables to make sure the
* child has actually finished or exec'd. If not, we loop and try again.
* We can't just rely on the fact that the child signaled us, because
* it may still be running in libc's implementation of exec*.
*/
long oldmask;
sig_t prev_alarm_sig;
pid_t pid;
kvmt *kvm_context;
struct pentry *proc_entry;
bool done = 0;
/* Block all signals for now */
oldmask = sigblock(-1);
pid = fork2((fn), 1024, 0, "hush (forked)", 2, (arg));
pid = fork2(fork_thunk, 1024, 0, forked_child_name,
(sizeof(fn) + sizeof(arg) + sizeof(oldmask) + 1) / 2,
fn, arg, oldmask);
if (pid < 0)
goto ret;
/* Now wait until we get SIGUSR2 */
sigpause(~sigmask(SIGUSR2));
prev_alarm_sig = signal(SIGALRM, SIG_IGN);
/* Restore original signal mask */
while (!done) {
/* Set alarm. This is a backup in case the child dies without signaling us. */
alarm10(1);
/* Wait until we get SIGALRM */
sigpause(~sigmask(SIGALRM));
/* Check if the child is really dead or forked by inspecting
* the kernel's process entry for it. */
kvm_context = kvm_open();
if (kvm_context == NULL)
break;
proc_entry = kvmgetproc(kvm_context, pid);
if (proc_entry == NULL
|| (proc_entry->args != NULL
&& strcmp(forked_child_name, proc_entry->args + 8) != 0))
done = 1;
kvm_close(kvm_context);
}
alarm10(0);
sigsetmask(~sigmask(SIGALRM));
signal(SIGALRM, prev_alarm_sig);
ret:
sigsetmask(oldmask);
return pid;
}

View File

@ -38,8 +38,7 @@ void FAST_FUNC xfunc_die(void)
}
sleep(die_sleep);
}
forked = signal_parent_to_resume();
if (!forked)
if (!is_forked_child())
exit(xfunc_error_retval);
else
_exit(xfunc_error_retval);

View File

@ -1343,19 +1343,26 @@ static void restore_G_args(save_arg_t *sv, char **argv)
}
/* Signal to the parent that it can resume executing after a fork,
* because the child is about to exec or terminate.
*/
int signal_parent_to_resume(void) {
int is_forked_child(void) {
#ifdef __GNO__
if (getpid() != G.last_execed_pid) {
kill(getppid(), SIGUSR2);
return 1;
}
#endif
return 0;
}
/* Signal to the parent that it can resume executing after a fork,
* because the child is about to exec or terminate.
*/
void signal_parent_to_resume(void) {
#ifdef __GNO__
if (getpid() != G.last_execed_pid) {
kill(getppid(), SIGALRM);
}
#endif
}
#ifdef __GNO__
#undef _exit
@ -1374,8 +1381,12 @@ void _exit_wrapper(int status) {
// Call regular _exit()
_exit(status);
} else {
// We're a forked child. Just call QuitGS. We do it in
// assembly so we can push the return value on the stack.
// We're a forked child.
fflush(stdout);
fflush(stderr);
signal_parent_to_resume();
// Call QuitGS in assembly so we can push the return value on the stack.
while (1) {
asm {
lda status
@ -1627,7 +1638,6 @@ static void sigexit(int sig)
/* Not a signal, just exit */
if (sig <= 0) {
signal_parent_to_resume();
_exit(- sig);
}
@ -6105,8 +6115,11 @@ static void xforked_child(void *args_struct) {
) {
static const char *const argv[] = { NULL, NULL };
builtin_trap((char**)argv);
signal_parent_to_resume();
# ifdef __GNO__
_exit(0);
# else
exit(0); /* not _exit() - we need to fflush */
# endif
}
# if BB_MMU
reset_traps_to_defaults();
@ -6264,7 +6277,6 @@ static void xvforked_child(void *grandchild_args) {
pid = xvfork_and_run(xforked_grandchild, grandchild_args);
#endif
if (pid != 0) {
signal_parent_to_resume();
_exit(0);
}
xforked_grandchild(grandchild_args); // Only get here in BB_MMU case
@ -6706,7 +6718,6 @@ static NOINLINE void pseudo_exec_argv(nommu_save_t *nommu_save,
* expand_assignments(): think about ... | var=`sleep 1` | ...
*/
free_strings(new_env);
signal_parent_to_resume();
_exit(EXIT_SUCCESS);
}
@ -6830,7 +6841,6 @@ static void pseudo_exec(nommu_save_t *nommu_save,
/* Case when we are here: ... | >file */
debug_printf_exec(("pseudo_exec'ed null command\n"));
signal_parent_to_resume();
_exit(EXIT_SUCCESS);
}
@ -7570,7 +7580,6 @@ static void forked_child(void *args_struct) {
/* Like bash, explicit redirects override pipes,
* and the pipe fd is available for dup'ing. */
if (setup_redirects(*args->command_p, NULL)) {
signal_parent_to_resume();
_exit(1);
}
@ -8494,14 +8503,6 @@ int hush_main(int argc, char **argv)
install_special_sighandlers();
#endif
#ifdef __GNO__
/* Don't terminate on SIGUSR2, because we'll be using it to signal when
* it's safe to resume after a fork. We don't need to actually do any
* other handling for it, so just set it to SIG_IGN.
*/
install_sighandler(SIGUSR2, SIG_IGN);
#endif
/* bash:
* if interactive but not a login shell, sources ~/.bashrc
* (--norc turns this off, --rcfile <file> overrides)