init: improve handling of signals racing with each other

Before this change, a request to reboot could be "overwritten" by e.g.
SIGHUP.

function                                             old     new   delta
init_main                                            709     793     +84
packed_usage                                       33273   33337     +64
run_actions                                          109     117      +8
stop_handler                                          87      88      +1
check_delayed_sigs                                   340     335      -5
run                                                  214     198     -16
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 4/2 up/down: 157/-21)           Total: 136 bytes

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/init/init.c b/init/init.c
index db1d99a..28775a6 100644
--- a/init/init.c
+++ b/init/init.c
@@ -210,6 +210,8 @@
 #if !ENABLE_FEATURE_INIT_SYSLOG
 	const char *log_console;
 #endif
+	sigset_t delayed_sigset;
+	struct timespec zero_ts;
 } FIX_ALIASING;
 #define G (*(struct globals*)bb_common_bufsiz1)
 #define INIT_G() do { \
@@ -411,14 +413,8 @@
 static void reset_sighandlers_and_unblock_sigs(void)
 {
 	bb_signals(0
-		+ (1 << SIGUSR1)
-		+ (1 << SIGUSR2)
-		+ (1 << SIGTERM)
-		+ (1 << SIGQUIT)
-		+ (1 << SIGINT)
-		+ (1 << SIGHUP)
-		+ (1 << SIGTSTP)
-		+ (1 << SIGSTOP)
+		| (1 << SIGTSTP)
+		| (1 << SIGSTOP)
 		, SIG_DFL);
 	sigprocmask_allsigs(SIG_UNBLOCK);
 }
@@ -484,16 +480,13 @@
 {
 	pid_t pid;
 
-	/* Careful: don't be affected by a signal in vforked child */
-	sigprocmask_allsigs(SIG_BLOCK);
 	if (BB_MMU && (a->action_type & ASKFIRST))
 		pid = fork();
 	else
 		pid = vfork();
-	if (pid < 0)
-		message(L_LOG | L_CONSOLE, "can't fork");
 	if (pid) {
-		sigprocmask_allsigs(SIG_UNBLOCK);
+		if (pid < 0)
+			message(L_LOG | L_CONSOLE, "can't fork");
 		return pid; /* Parent or error */
 	}
 
@@ -587,9 +580,11 @@
 	while (1) {
 		pid_t wpid = wait(NULL);
 		mark_terminated(wpid);
-		/* Unsafe. SIGTSTP handler might have wait'ed it already */
-		/*if (wpid == pid) break;*/
-		/* More reliable: */
+		if (wpid == pid) /* this was the process we waited for */
+			break;
+		/* The above is not reliable enough: SIGTSTP handler might have
+		 * wait'ed it already. Double check, exit if process is gone:
+		 */
 		if (kill(pid, 0))
 			break;
 	}
@@ -798,23 +793,17 @@
  * Delayed handlers just set a flag variable. The variable is checked
  * in the main loop and acted upon.
  *
- * halt/poweroff/reboot and restart have immediate handlers.
- * They only traverse linked list of struct action's, never modify it,
- * this should be safe to do even in signal handler. Also they
- * never return.
- *
  * SIGSTOP and SIGTSTP have immediate handlers. They just wait
  * for SIGCONT to happen.
  *
+ * halt/poweroff/reboot and restart have delayed handlers.
+ *
  * SIGHUP has a delayed handler, because modifying linked list
  * of struct action's from a signal handler while it is manipulated
  * by the program may be disastrous.
  *
  * Ctrl-Alt-Del has a delayed handler. Not a must, but allowing
  * it to happen even somewhere inside "sysinit" would be a bit awkward.
- *
- * There is a tiny probability that SIGHUP and Ctrl-Alt-Del will collide
- * and only one will be remembered and acted upon.
  */
 
 /* The SIGPWR/SIGUSR[12]/SIGTERM handler */
@@ -898,11 +887,9 @@
  */
 static void stop_handler(int sig UNUSED_PARAM)
 {
-	smallint saved_bb_got_signal;
-	int saved_errno;
+	int saved_errno = errno;
 
-	saved_bb_got_signal = bb_got_signal;
-	saved_errno = errno;
+	bb_got_signal = 0;
 	signal(SIGCONT, record_signo);
 
 	while (1) {
@@ -916,12 +903,12 @@
 		 */
 		wpid = wait_any_nohang(NULL);
 		mark_terminated(wpid);
-		sleep(1);
+		if (wpid <= 0) /* no processes exited? sleep a bit */
+			sleep(1);
 	}
 
 	signal(SIGCONT, SIG_DFL);
 	errno = saved_errno;
-	bb_got_signal = saved_bb_got_signal;
 }
 
 #if ENABLE_FEATURE_USE_INITTAB
@@ -986,43 +973,39 @@
 }
 #endif
 
-static int check_delayed_sigs(void)
+static void check_delayed_sigs(struct timespec *ts)
 {
-	int sigs_seen = 0;
+	int sig = sigtimedwait(&G.delayed_sigset, /* siginfo_t */ NULL, ts);
+	if (sig <= 0)
+		return;
 
-	while (1) {
-		smallint sig = bb_got_signal;
+	/* The signal "sig" was caught */
 
-		if (!sig)
-			return sigs_seen;
-		bb_got_signal = 0;
-		sigs_seen = 1;
 #if ENABLE_FEATURE_USE_INITTAB
-		if (sig == SIGHUP)
-			reload_inittab();
+	if (sig == SIGHUP)
+		reload_inittab();
 #endif
-		if (sig == SIGINT)
-			run_actions(CTRLALTDEL);
-		if (sig == SIGQUIT) {
-			exec_restart_action();
-			/* returns only if no restart action defined */
-		}
-		if ((1 << sig) & (0
-#ifdef SIGPWR
-		    + (1 << SIGPWR)
-#endif
-		    + (1 << SIGUSR1)
-		    + (1 << SIGUSR2)
-		    + (1 << SIGTERM)
-		)) {
-			halt_reboot_pwoff(sig);
-		}
+	if (sig == SIGINT)
+		run_actions(CTRLALTDEL);
+	if (sig == SIGQUIT) {
+		exec_restart_action();
+		/* returns only if no restart action defined */
 	}
+	if ((1 << sig) & (0
+#ifdef SIGPWR
+	    | (1 << SIGPWR)
+#endif
+	    | (1 << SIGUSR1)
+	    | (1 << SIGUSR2)
+	    | (1 << SIGTERM)
+	)) {
+		halt_reboot_pwoff(sig);
+	}
+	/* if (sig == SIGCHLD) do nothing */
 }
 
 #if DEBUG_SEGV_HANDLER
-static
-void handle_sigsegv(int sig, siginfo_t *info, void *ucontext)
+static void handle_sigsegv(int sig, siginfo_t *info, void *ucontext)
 {
 	long ip;
 	ucontext_t *uc;
@@ -1049,50 +1032,62 @@
 
 static void sleep_much(void)
 {
-        sleep(30 * 24*60*60);
+	sleep(30 * 24*60*60);
 }
 
 int init_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int init_main(int argc UNUSED_PARAM, char **argv)
 {
+	struct sigaction sa;
+
 	INIT_G();
 
+	/* Some users send poweroff signals to init VERY early.
+	 * To handle this, mask signals early.
+	 */
+	/* sigemptyset(&G.delayed_sigset); - done by INIT_G() */
+	sigaddset(&G.delayed_sigset, SIGINT);  /* Ctrl-Alt-Del */
+	sigaddset(&G.delayed_sigset, SIGQUIT); /* re-exec another init */
+#ifdef SIGPWR
+	sigaddset(&G.delayed_sigset, SIGPWR);  /* halt */
+#endif
+	sigaddset(&G.delayed_sigset, SIGUSR1); /* halt */
+	sigaddset(&G.delayed_sigset, SIGTERM); /* reboot */
+	sigaddset(&G.delayed_sigset, SIGUSR2); /* poweroff */
+#if ENABLE_FEATURE_USE_INITTAB
+	sigaddset(&G.delayed_sigset, SIGHUP);  /* reread /etc/inittab */
+#endif
+	sigaddset(&G.delayed_sigset, SIGCHLD); /* make sigtimedwait() exit on SIGCHLD */
+	sigprocmask(SIG_BLOCK, &G.delayed_sigset, NULL);
+
+#if DEBUG_SEGV_HANDLER
+	memset(&sa, 0, sizeof(sa));
+	sa.sa_sigaction = handle_sigsegv;
+	sa.sa_flags = SA_SIGINFO;
+	sigaction_set(SIGSEGV, &sa);
+	sigaction_set(SIGILL, &sa);
+	sigaction_set(SIGFPE, &sa);
+	sigaction_set(SIGBUS, &sa);
+#endif
+
 	if (argv[1] && strcmp(argv[1], "-q") == 0) {
 		return kill(1, SIGHUP);
 	}
 
-#if DEBUG_SEGV_HANDLER
-	{
-		struct sigaction sa;
-		memset(&sa, 0, sizeof(sa));
-		sa.sa_sigaction = handle_sigsegv;
-		sa.sa_flags = SA_SIGINFO;
-		sigaction(SIGSEGV, &sa, NULL);
-		sigaction(SIGILL, &sa, NULL);
-		sigaction(SIGFPE, &sa, NULL);
-		sigaction(SIGBUS, &sa, NULL);
+#if !DEBUG_INIT
+	/* Expect to be invoked as init with PID=1 or be invoked as linuxrc */
+	if (getpid() != 1
+	 && (!ENABLE_LINUXRC || applet_name[0] != 'l') /* not linuxrc? */
+	) {
+		bb_simple_error_msg_and_die("must be run as PID 1");
 	}
-#endif
 
-	if (!DEBUG_INIT) {
-		/* Some users send poweroff signals to init VERY early.
-		 * To handle this, mask signals early,
-		 * and unmask them only after signal handlers are installed.
-		 */
-		sigprocmask_allsigs(SIG_BLOCK);
-
-		/* Expect to be invoked as init with PID=1 or be invoked as linuxrc */
-		if (getpid() != 1
-		 && (!ENABLE_LINUXRC || applet_name[0] != 'l') /* not linuxrc? */
-		) {
-			bb_simple_error_msg_and_die("must be run as PID 1");
-		}
-#ifdef RB_DISABLE_CAD
-		/* Turn off rebooting via CTL-ALT-DEL - we get a
-		 * SIGINT on CAD so we can shut things down gracefully... */
-		reboot(RB_DISABLE_CAD); /* misnomer */
+# ifdef RB_DISABLE_CAD
+	/* Turn off rebooting via CTL-ALT-DEL - we get a
+	 * SIGINT on CAD so we can shut things down gracefully... */
+	reboot(RB_DISABLE_CAD); /* misnomer */
+# endif
 #endif
-	}
 
 	/* If, say, xmalloc would ever die, we don't want to oops kernel
 	 * by exiting.
@@ -1156,106 +1151,65 @@
 	}
 #endif
 
-	if (ENABLE_FEATURE_INIT_MODIFY_CMDLINE) {
-		/* Make the command line just say "init"  - that's all, nothing else */
-		strncpy(argv[0], "init", strlen(argv[0]));
-		/* Wipe argv[1]-argv[N] so they don't clutter the ps listing */
-		while (*++argv)
-			nuke_str(*argv);
-	}
-
-	/* Set up signal handlers */
-	if (!DEBUG_INIT) {
-		struct sigaction sa;
-
-		/* Stop handler must allow only SIGCONT inside itself */
-		memset(&sa, 0, sizeof(sa));
-		sigfillset(&sa.sa_mask);
-		sigdelset(&sa.sa_mask, SIGCONT);
-		sa.sa_handler = stop_handler;
-		/* NB: sa_flags doesn't have SA_RESTART.
-		 * It must be able to interrupt wait().
-		 */
-		sigaction_set(SIGTSTP, &sa); /* pause */
-		/* Does not work as intended, at least in 2.6.20.
-		 * SIGSTOP is simply ignored by init:
-		 */
-		sigaction_set(SIGSTOP, &sa); /* pause */
-
-		/* These signals must interrupt wait(),
-		 * setting handler without SA_RESTART flag.
-		 */
-		bb_signals_recursive_norestart(0
-			+ (1 << SIGINT)  /* Ctrl-Alt-Del */
-			+ (1 << SIGQUIT) /* re-exec another init */
-#ifdef SIGPWR
-			+ (1 << SIGPWR)  /* halt */
+#if ENABLE_FEATURE_INIT_MODIFY_CMDLINE
+	/* Make the command line just say "init"  - that's all, nothing else */
+	strncpy(argv[0], "init", strlen(argv[0]));
+	/* Wipe argv[1]-argv[N] so they don't clutter the ps listing */
+	while (*++argv)
+		nuke_str(*argv);
 #endif
-			+ (1 << SIGUSR1) /* halt */
-			+ (1 << SIGTERM) /* reboot */
-			+ (1 << SIGUSR2) /* poweroff */
-#if ENABLE_FEATURE_USE_INITTAB
-			+ (1 << SIGHUP)  /* reread /etc/inittab */
-#endif
-			, record_signo);
 
-		sigprocmask_allsigs(SIG_UNBLOCK);
-	}
+	/* Set up STOP signal handlers */
+	/* Stop handler must allow only SIGCONT inside itself */
+	memset(&sa, 0, sizeof(sa));
+	sigfillset(&sa.sa_mask);
+	sigdelset(&sa.sa_mask, SIGCONT);
+	sa.sa_handler = stop_handler;
+	sa.sa_flags = SA_RESTART;
+	sigaction_set(SIGTSTP, &sa); /* pause */
+	/* Does not work as intended, at least in 2.6.20.
+	 * SIGSTOP is simply ignored by init
+	 * (NB: behavior might differ under strace):
+	 */
+	sigaction_set(SIGSTOP, &sa); /* pause */
 
 	/* Now run everything that needs to be run */
 	/* First run the sysinit command */
 	run_actions(SYSINIT);
-	check_delayed_sigs();
+	check_delayed_sigs(&G.zero_ts);
 	/* Next run anything that wants to block */
 	run_actions(WAIT);
-	check_delayed_sigs();
+	check_delayed_sigs(&G.zero_ts);
 	/* Next run anything to be run only once */
 	run_actions(ONCE);
 
-	/* Now run the looping stuff for the rest of forever.
-	 */
+	/* Now run the looping stuff for the rest of forever */
 	while (1) {
-		int maybe_WNOHANG;
-
-		maybe_WNOHANG = check_delayed_sigs();
-
 		/* (Re)run the respawn/askfirst stuff */
 		run_actions(RESPAWN | ASKFIRST);
-		maybe_WNOHANG |= check_delayed_sigs();
 
-		/* Don't consume all CPU time - sleep a bit */
-		sleep(1);
-		maybe_WNOHANG |= check_delayed_sigs();
+		/* Wait for any signal (typically it's SIGCHLD) */
+		check_delayed_sigs(NULL); /* NULL timespec makes it wait */
 
-		/* Wait for any child process(es) to exit.
-		 *
-		 * If check_delayed_sigs above reported that a signal
-		 * was caught, wait will be nonblocking. This ensures
-		 * that if SIGHUP has reloaded inittab, respawn and askfirst
-		 * actions will not be delayed until next child death.
-		 */
-		if (maybe_WNOHANG)
-			maybe_WNOHANG = WNOHANG;
+		/* Wait for any child process(es) to exit */
 		while (1) {
 			pid_t wpid;
 			struct init_action *a;
 
-			/* If signals happen _in_ the wait, they interrupt it,
-			 * bb_signals_recursive_norestart set them up that way
-			 */
-			wpid = waitpid(-1, NULL, maybe_WNOHANG);
+			wpid = waitpid(-1, NULL, WNOHANG);
 			if (wpid <= 0)
 				break;
 
 			a = mark_terminated(wpid);
 			if (a) {
-				message(L_LOG, "process '%s' (pid %d) exited. "
+				message(L_LOG, "process '%s' (pid %u) exited. "
 						"Scheduling for restart.",
-						a->command, wpid);
+						a->command, (unsigned)wpid);
 			}
-			/* See if anyone else is waiting to be reaped */
-			maybe_WNOHANG = WNOHANG;
 		}
+
+		/* Don't consume all CPU time - sleep a bit */
+		sleep(1);
 	} /* while (1) */
 }
 
@@ -1268,11 +1222,17 @@
 //usage:       "Init is the first process started during boot. It never exits."
 //usage:	IF_FEATURE_USE_INITTAB(
 //usage:   "\n""It (re)spawns children according to /etc/inittab."
+//usage:   "\n""Signals:"
+//usage:   "\n""HUP: reload /etc/inittab"
 //usage:	)
 //usage:	IF_NOT_FEATURE_USE_INITTAB(
 //usage:   "\n""This version of init doesn't use /etc/inittab,"
 //usage:   "\n""has fixed set of processed to run."
+//usage:   "\n""Signals:"
 //usage:	)
+//usage:   "\n""TSTP: stop respawning until CONT"
+//usage:   "\n""QUIT: re-exec another init"
+//usage:   "\n""USR1/TERM/USR2/INT: run halt/reboot/poweroff/Ctrl-Alt-Del script"
 //usage:
 //usage:#define init_notes_usage
 //usage:	"This version of init is designed to be run only by the kernel.\n"