ash: eval: Reap zombies after built-in commands and functions

Upstream commit:

    Date: Mon, 26 Mar 2018 23:55:50 +0800
    eval: Reap zombies after built-in commands and functions

    Currently dash does not reap dead children after built-in commands
    or functions.  This means that if you construct a loop consisting
    of solely built-in commands and functions, then zombies can hang
    around indefinitely.

    This patch fixes this by reaping when necessary after each built-in
    command and function.

    Reported-by: Denys Vlasenko <vda.linux@googlemail.com>
    Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au>

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
diff --git a/shell/ash.c b/shell/ash.c
index 389db3c..8047cf9 100644
--- a/shell/ash.c
+++ b/shell/ash.c
@@ -5355,10 +5355,10 @@
 {
 	int st;
 
-	TRACE(("waitforjob(%%%d) called\n", jobno(jp)));
+	TRACE(("waitforjob(%%%d) called\n", jp ? jobno(jp) : 0));
 
 	INT_OFF;
-	while (jp->state == JOBRUNNING) {
+	while ((jp && jp->state == JOBRUNNING) || got_sigchld) {
 		/* In non-interactive shells, we _can_ get
 		 * a keyboard signal here and be EINTRed,
 		 * but we just loop back, waiting for command to complete.
@@ -5393,6 +5393,8 @@
 	}
 	INT_ON;
 
+	if (!jp)
+		return exitstatus;
 	st = getstatus(jp);
 #if JOBS
 	if (jp->jobctl) {
@@ -10311,6 +10313,8 @@
 		goto out;
 	}
 
+	jp = NULL;
+
 	/* Execute the command. */
 	switch (cmdentry.cmdtype) {
 	default: {
@@ -10365,7 +10369,6 @@
 			jp = makejob(/*cmd,*/ 1);
 			if (forkshell(jp, cmd, FORK_FG) != 0) {
 				/* parent */
-				status = waitforjob(jp);
 				INT_ON;
 				TRACE(("forked child exited with %d\n", status));
 				break;
@@ -10384,33 +10387,24 @@
 			if (cmd_is_exec && argc > 1)
 				listsetvar(varlist.list, VEXPORT);
 		}
-
-		/* Tight loop with builtins only:
-		 * "while kill -0 $child; do true; done"
-		 * will never exit even if $child died, unless we do this
-		 * to reap the zombie and make kill detect that it's gone: */
-		dowait(DOWAIT_NONBLOCK, NULL);
-
 		if (evalbltin(cmdentry.u.cmd, argc, argv, flags)) {
 			if (exception_type == EXERROR && spclbltin <= 0) {
 				FORCE_INT_ON;
-				goto readstatus;
+				break;
 			}
  raise:
 			longjmp(exception_handler->loc, 1);
 		}
-		goto readstatus;
+		break;
 
 	case CMDFUNCTION:
-		/* See above for the rationale */
-		dowait(DOWAIT_NONBLOCK, NULL);
 		if (evalfun(cmdentry.u.func, argc, argv, flags))
 			goto raise;
- readstatus:
-		status = exitstatus;
 		break;
 	} /* switch */
 
+	status = waitforjob(jp);
+
  out:
 	if (cmd->ncmd.redirect)
 		popredir(/*drop:*/ cmd_is_exec);