su: Fix never alarmed SIGKILL when session terminates

The buggy code was introduced nearly 5 years ago at the
commit 08fd4b69e8. The
desired behavior is that SIGKILL will be sent to the
child if it does not exit within 2 seconds after it
receives SIGTERM. However, SIGALRM is masked while
waiting for the child so it cannot wake the program
up after 2 seconds to send SIGKILL.

An example shows the buggy behavior, which exists in
Ubuntu 18.04 LTS (with login 1:4.5-1ubuntu2).
```bash
user1@localhost:~$ su user2 -c '
_term() {
  echo SIGTERM received
}
trap _term TERM

while true; do
  sleep 1
  echo still alive
done'
Password:
still alive

Session terminated, terminating shell...Terminated
SIGTERM received
still alive
still alive
still alive
still alive
```
(SIGTERM is sent in another user1's terminal by
executing `killall su`.)

Here is the desired behavior, which shows what the
commit fixes.
```bash
user1@localhost:~$ su user2 -c '
_term() {
  echo SIGTERM received
}
trap _term TERM

while true; do
  sleep 1
  echo still alive
done'
Password:
still alive

Session terminated, terminating shell...Terminated
SIGTERM received
still alive
still alive
 ...killed.
user1@localhost:~$ echo $?
255
```
This commit is contained in:
Ruihan Li 2021-10-09 19:54:36 +08:00 committed by lrh2000
parent cdc8c1e25b
commit 5b4082d007

View File

@ -397,22 +397,28 @@ static void prepare_pam_close_session (void)
snprintf (kill_msg, sizeof kill_msg, _(" ...killed.\n"));
snprintf (wait_msg, sizeof wait_msg, _(" ...waiting for child to terminate.\n"));
/* Any signals other than SIGCHLD and SIGALRM will no longer have any effect,
* so it's time to block all of them. */
sigfillset (&ourset);
if (sigprocmask (SIG_BLOCK, &ourset, NULL) != 0) {
fprintf (stderr, _("%s: signal masking malfunction\n"), Prog);
kill_child (0);
/* Never reach (_exit called). */
}
/* Send SIGKILL to the child if it doesn't
* exit within 2 seconds (after SIGTERM) */
(void) signal (SIGALRM, kill_child);
(void) signal (SIGCHLD, catch_signals);
(void) alarm (2);
sigemptyset (&ourset);
if ((sigaddset (&ourset, SIGALRM) != 0)
|| (sigprocmask (SIG_BLOCK, &ourset, NULL) != 0)) {
fprintf (stderr, _("%s: signal masking malfunction\n"), Prog);
kill_child (0);
} else {
while (0 == waitpid (pid_child, &status, WNOHANG)) {
sigsuspend (&ourset);
}
pid_child = 0;
(void) sigprocmask (SIG_UNBLOCK, &ourset, NULL);
(void) sigdelset (&ourset, SIGALRM);
(void) sigdelset (&ourset, SIGCHLD);
while (0 == waitpid (pid_child, &status, WNOHANG)) {
sigsuspend (&ourset);
}
pid_child = 0;
(void) fputs (_(" ...terminated.\n"), stderr);
}