Search code examples
clinuxsignalsasync-safe

Is it always unsafe when I call a non-async-safe function from a signal handler?


I am just figuring out whether I can call a non-async-safe function in a signal handler.
Quotes from Linux man page signal(7):

If a signal interrupts the execution of an unsafe function, and handler calls an unsafe function, then the behavior of the program is undefined.

and TLPI :

SUSv3 notes that all functions not listed in Table 21-1 (list of async-safe functions) are considered to be unsafe with respect to signals, but points out that a function is unsafe only when invocation of a signal handler interrupts the execution of an unsafe function, and the handler itself also calls an unsafe function.

My interpretation of above quotes is that it's safe to call a non-async-safe function from a signal handler only if the signal handler did not interrupt a non-async-safe function.

For example I install a handler for SIGINT which calls an unsafe function suppose to be crypt(3) which is non-reentrant namely unsafe.

sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = handler;
sigaction(SIGINT, &sa, NULL);

And I also call printf() in an infinite loop in main(), and I only have the main thread running.

The question is for this simple example, I don't see any bad things happen when the handler interrupted the execution of printf() and it calls an unsafe function. AFAK, printf() will acquire a console lock and has an internal buffer to perform buffered I/O, but its state is consistent in this example. And although crypt() returns a statically allocated string, but it doesn't shared with other function or threads.

Am I misunderstanding something? I want someone to clarify me that is it always unsafe to have a signal handler interrupted the execution of an unsafe function in main program and itself also calls an unsafe function or it is safe to do so in some situation (e.g the simple example above)?


Solution

  • Yes indeed, it is unsafe to call a non-async-signal-safe function from inside a signal handler in all cases (unless you dive into your implementation code -e.g. libc and perhaps your compiler generating code for it); then you could perhaps prove that calling such an such function is in fact safe; but such a proof might be impossible or take months or years of your time, even with the help of static analyzers à la Frama-C, ... and require studying all the implementation details.

    Concretely, it is probable that crypt is internally calling malloc (for some internal data, etc...). And the standard malloc function has obviously some global state (e.g. the vector of linked lists of buckets related to previously free-d memory zone, to be reused by future calls to malloc).

    Remember that Unix signals can occur at every machine instruction (not only at C sequence points, which have some well defined semantics) in user space (a system call is then a single SYSENTER instruction). With bad luck, a signal might occur in the few machine instructions which are updating the global state of malloc. Then future calls to malloc -e.g. indirectly from your signal handler, might break havoc (i.e. undefined behavior). Such a misfortune might be unlikely (but evaluating its probability is practically impossible), but you should code against it.

    Details are heavily implementation specific, depending on your compiler and optimization flags, libc, kernel, processor architecture, etc...

    You might not care about async-signal-safe functions, by betting that disaster won't happen. This might be acceptable for debugging purposes (e.g. very often, but not always, a printf inside a signal handler would practically work most of the time; and the GCC compiler is internally using its "async-unsafe" libbacktrace library in signal handlers) for code wrapped with #ifndef NDEBUG, but it won't be good for production code; if you really have to add such code in a handler, mention in a comment that you know that you are doing wrongly a call to a non-async-signal-safe function, and be prepared to be cursed by future colleagues working on the same code base.

    A typical trick to handle such situations is to simply set a volatile sig_atomic_t flag in the signal handler (read POSIX signal.h documentation) and check that flag in some safe place in some loop -outside of the handler-, or to write(2) one -or a few- bytes to a pipe(7) previously setup at application initialization, and have the read end of that pipe be periodically poll(2)-ed and later read by your event loop -or some other thread-).

    (I've taken malloc as an example, but you could think of other widely used non-async-signal-safe functions, or even implementation specific routines, e.g. 64 bits arithmetic on a 32 bits processor, etc...).