[Bug 3547] shell read is maybe too safe

Denys Vlasenko vda.linux at googlemail.com
Mon May 9 11:25:57 UTC 2011


On Mon, May 9, 2011 at 11:19 AM, Laurent Bercot
<ska-dietlibc at skarnet.org> wrote:
>> Although I think the fix revealed a deeper problem:
>> hush's way of handling signals isn't good for read
>> and for reading commands from stdin: since we block
>> signals we are interested in, we cannot detect them
>> while we wait for input on a fd.
>
>> Changing signal handling to a different scheme,
>> where they are caught and recorded, will somewhat help,
>> because now poll/read syscalls will be interruptible,
>> but this creates another problem: many other sycalls
>> may start failing with EINTR too, and we don't check
>> for that. The problem is, EINTR case happens rarely.
>> IOW: we may have many rarely-triggered bugs, one
>> per every syscall we don't wrap with "repeat on EINTR"
>> code, or with "if we got EINTR, propagate EINTR".
>
>  Indeed.
>  http://www.skarnet.org/tmp/doc/libstddjb/safewrappers.html
>  (non-permanent URL)
>  Safe wrappers should be used everywhere, *even* when SIGINT is
> meant to be detected: the handling part must be done in another
> place than the chunk of code containing your interrupted system
> call. See below.

> For instance, you cannot trap SIGSTOP; SIGSTOP does not kill your process, which should resume flawlessly at the next SIGCONT; and according to the specification, it is valid for SIGSTOP and SIGCONT to not have SA_RESTART behaviour.

I don't think that such SIGSTOP behavior isn't a kernel bug.
It would break a lot of programs, because no one stress tests
their apps with massive SIGSTOP/SIGCONT storms, so many apps
are bound to contain bugs wrt this.
Practicality dictates that the kernel-side fix is a better place for this
than auditing quadrazillion of syscalls in userspace code.

SIGCONT is just another signal - you can even have a handler for it.
Try it if you don't believe me - it works:

#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static void sig(int n)
{
      char buf[128];
      int e = errno;
      sprintf(buf, "sig: %d %s\n", n, strsignal(n));
      write(1, buf, strlen(buf));
      errno = e;
}
int main()
{
      signal(SIGSTOP, sig);
      signal(SIGCONT, sig);
      signal(SIGWINCH, sig);
      signal(SIGABRT, sig);
      printf("PID: %d\n", getpid());
      fflush(NULL);
      sleep(30);
      return 0;
}

$ gcc t.c
$ ./a.out
PID: 3770
<-- kill -STOP 3770
<-- kill -ABRT 3770
<-- kill -WINCH 3770
<-- kill -CONT 3770
sig: 28 Window changed
sig: 18 Continued  <========NOTE THIS!
sig: 6 Aborted


Because it's an "ordinary" signal (unlike SIGSTOP/SIGKILL),
you definitely can set SA_RESTART on SIGCONT.
The fact that SIGCONT wakes up SIGSTOPed task
is a special side effect which is orthogonal to SA_RESTARTness.


>> Ideas anyone?
>
>  As always when you need to mix signals and I/O: use a self-pipe.
>  http://www.skarnet.org/tmp/doc/libstddjb/selfpipe.html
>  (non-permanent URL)
>  and http://cr.yp.to/docs/selfpipe.html
>
>  Create a pipe p, trap SIGINT with a "write(p[1], &c, 1)" handler,
> and loop over a poll() on stdin and p[0] (and maybe other stuff you need).
>
> Use safe wrappers for your (asynchronous) reads and your (preferably
> asynchronous, but it doesn't really matter) writes.
>  When you get a SIGINT, it will be momentarily ignored if you're in the
> middle of performing some operation ; but there will be a byte in p, and
> as soon as you come back to the poll(), p[0] will be readable, you will
> know you got a SIGINT, and can handle it in a normal (i.e.
> non-interrupt-handler) context.
>
>  The self-pipe trick is the ultimate solution to mixing I/O and signals,
> because it integrates signal handling into the generic select/poll
> asynchronous I/O event framework. There is no case it cannot handle.
> Teaching it should be a mandatory part of every Unix course. :)

Yep, I know the technique.
But.
This is  *shell*. In shell, people may use fd's *directly*:

echo yo! >&23

What will happen if they'd write stuff to the write end
of the signal pipe?!

It is already a huge PITA to prevent users from seeing/reading from
the fd's opened to the currently running script! Think about this:

script.sh:
====
. ./foo.sh
foo.sh:
====
cat <&4
...

If script.sh is opened to fd 3 and foo.sh is opened to fd 4 by shell,
what will happen?


Therefore, signal pipe isn't an attractive solution.

Will it work if I'll set signal handlers to SA_RESTART, but perform
all reads by checking data availability via poll() beforehand?
The trick is that poll() is interrupted by SA_RESTART signals!
(Unlike read(), which isn't).

-- 
vda


More information about the busybox mailing list