svn commit: r327495 - head/usr.sbin/rpcbind

Bruce Evans brde at optusnet.com.au
Sun Feb 4 05:15:23 UTC 2018


On Sat, 3 Feb 2018, Conrad Meyer wrote:

> On Sat, Feb 3, 2018 at 6:46 AM, Bruce Evans <brde at optusnet.com.au> wrote:
>> On Tue, 2 Jan 2018, Conrad Meyer wrote:
>>> ...
>> Today I learned from the POSIX list that the pselect() function is designed
>> to avoid this bug.  It takes a signal mask arg (like msleep() in the
>> kernel).
>>
>> I haven't noticed any function related to poll() or kevent that takes a
>> signal mask.
>
> There is the similar function ppoll(), although it complies only with
> the Linux standard, not POSIX.

ppoll() is relatively new in FreeBSD (2014-11-13).  Its man page says
that it is first appeared in 11.0, but it is now in 10.x, and according
to bsd-family-tree it actually appeared in the 10.2 release more than a
year before it appeared in the 11.0 release:
- 2014-11-13  pre-11.0 source tree commit
- 2014-12-21  10.x source tree commit
- 2015-08-13  10.2 release
- 2016-10-10  11.0 release.

> With kevent, you can simply mask all (or most) signals and watch on a
> signal event.  Conversion to kevent is more complicated, though.
>
>> Programs broken by buggy conversion:
>> ...
>> - rpcbind(8).  This is not interactive and normally doesn't use ttys
>>   which might block.  However, the -d flag makes it do fprintf() to
>>   stderr.  This may block forever (e.g., for flow control), and you
>>   would like to be able to kill it using a signal.  But write() will
>>   restart.  rpcbind also uses plain signal() and doesn't know anything
>>   about SA_RESTART.
>
> This was not broken by conversion -- it was already broken in this
> case.  If the signal delivery raced with an fprintf, we ended up
> invoking the stdio atexit handlers via exit(3) call in terminate(),
> which of course encountered corrupted state.  Now it is broken in a
> slightly different way, yes, if signal delivery races fprintf *and*
> fprintf is blocked in flow control.  This might happen with a slow
> serial for stderr but seems extraordinarily unlikely otherwise.

Unsafe signal handlers are broken by their existence, but sometimes work.

It is unlikely for ttys too.  I forgot that stdio is clueless about ttys
so it doesn't wait for output to drain even for stderr.  So the output is
normally buffered in the kernel and write() returns.

> ...
>> perror()
>> is broken as designed since it uses stdio, so it is unsafe in signal
>> handlers.  The err() family copies this bug.  Even *s*printf() is not
>> required to be safe in signal handlers.  I would fix the latter first.
>
> It does seem like the printf family of routines should be
> signal-handler safe.  Unfortunately, they are implemented in terms of
> the unsafe stdio streams :-(.

Even sprintf() uses the generic function __vfprintf() on a fake FILE.  I
think it has problems with locales.  No ctype functions are in the list
of async-signal-safe functions in at least the 2001 version of POSIX.  It
is easy to see why they might not be safe.  They access lots of static
storage, and setlocale() or a simpler function that changes state might
run in another or just a nested signal handler, so locking is required...

So the safe sprintf() would probably have to use the C locale.  The locale
is already passed to __vfprintf(), as needed for sprintf_l().

>...
>>> Modified: head/usr.sbin/rpcbind/rpcbind.c
>>> ...
>>> terminate(int signum)
>>> ...
>>> +       wr = write(terminate_wfd, &c, 1);
>>
>>
>> Not async-signal-safe.  Acccesses in signal handlers to objects with
>> static storage duration give undefined behaviour except for assignment to
>> objects of type volatile sig_atomic_t, but the access to terminate_wfd is
>> a read and the type is plain int.
>
> The type can be changed to volatile sig_atomic_t if you believe plain
> int will trigger nonsensical compiler behavior.  The value is
> initialized once, before the signal handler is registered, so unless
> the compiler does something totally insane it should be fine on all
> architectures FreeBSD runs on.

sig_atomic_t is no better than plain int.  This behaviour now makes complete
sense.  It is just like the undefined behaviour with the ctype functions,
except since we own terminate_wfd we can guarantee that it doesn't change
while the handler is active (and is valid when the handler is entered).
We could also use atomic ops.  However, the C standard doesn't require
anything that we do to work (except maybe in C11, atomic ops might be
explicitly or implicitly specifed to work for things like this).

>>> +       if (wr < 1)
>>> +               _exit(2);
>>
>>
>> Best to not check for write errors, since the error handling of using
>> _exit()
>> is worse than none.  It loses stdio flushing to handle an almost-harmless
>> error.  The main problem with keeping everything in a safe handler is that
>> it
>> is impossible to keep stdio flushing there and we would prefer to not lost
>> the stdio flushing.
>
> I don't necessarily agree.  If the write fails, we missed the signal
> telling us to terminate the program and will never exit.  That said,
> how would the write ever fail?

We still set the flag, so can only fail to exit() in the race case,
but the write() failure affects all cases.

I can't see how the write() can fail.  That was part of the excuse for
removing the check.

Bruce


More information about the svn-src-head mailing list