Why is it accepted by the compiler, then?
Why is it accepted by the compiler, then?
I think the reason must be that the problem lies in the intersection of three areas of development (the C programming language, the C standard library, and the POSIX operating system definition) and so requires coordination to solve.
Think about how you would implement warnings for failures of async-signal-safety. One approach would go like this:
1. In compiler front-ends, introduce a new attribute that can be attached to function declarations to indicate that they are async-signal-safe, for example __attribute__((async_signal_safe)), and a new attribute that can be attached to function parameters and struct members to indicate that the passed value must be async-signal-safe, for example __attribute__((require_async_signal_safe)).
2. In C library headers, apply the async_signal_safe attribute to all the async-signal-safe function declarations, and apply the require_async_signal_safe attribute to the parameter to the signal function and to the sa_handler member of struct sigaction.
3. In compilers, propagate the async_signal_safe attribute, so that any function that only calls async_signal_safe functions is also marked with the attribute.
4. In compilers, check that when a function is passed to a parameter or assigned to a member with the require_async_signal_safe attribute, the function is marked with the async_signal_safe attribute, and issue a warning if not.
This doesn't solve the whole problem (sometimes the compiler will not be able to know which function is going to be passed as the parameter to signal, because this is determined at runtime), and it might have false positives in obscure situations (you might in theory use a struct sigaction for some other purpose and never pass it to sigaction) but it would catch many cases, including the one in beep.
But notice the amount of coordination required. It would require input from several people with different areas of expertise.
Signal is basically the crappiest form of IPC available on a modern operating system short of emulating a mouse and keyboard and typing into the other application's terminal window.
There was a "talk"-like application for BBC Micro Econet called `*NOTIFY` which worked like that (minus the mouse, no mice in those days). You could message people, but also type commands as them. Happy days ...
- write(2) to STDERR_FILENO
- write(2) to a "self-pipe" (i.e., a pipe where the same
process is waiting on in its event loop), thus turning
the async signal event into an async *I/O* event that
can be handled without any constraints regarding
async-signal-safety
- _exit()
Yes, there are other async-signal-safe functions that can be called from a signal handler, but it's generally not worth it. Adhering to my more constrained approach will keep your code safe and will make it easier to always get it right.ALSO, while we're at it, the only global or thread-local variables you can read from or write to from a signal handler must be of type volatile sig_atomic_t (or else volatile of any other integral or pointer type that you can use with atomic operations). This is very important. E.g., imagine using SIGUSR1/2 to manage verbosity levels...
A write(2) to STDERR_FILENO for verbosity/debugging is fine, but mostly you don't want to do this because it will interleave with any non-line-buffered stdio writes to it... An _exit(2) is also OK if you really want to do that, but generally you want to do some cleanup, so might as well do the self-pipe thing every time.
The only tricky thing is when you use SA_SIGINFO and you want to pass the siginfo_t data to the event loop. You can write(2) that to the self-pipe, but you have to be careful of the possibility that it will fill up. You can always create a new pipe(2), write(2) the siginfo_t to it, close(2) the write end, and send the read side fd via a socketpair(2) that the event loop listens to.
kevent() is another way to handle signals. It puts handling them into the program's main event loop, which is done synchronously with normal event-dispatching mechanisms and so does not have worries about asynchronous signal safety, because with kevent() they are just another type of filter.
The nice thing about the write-a-byte-to-the-pipe thing is that it works virtually everywhere.
I'd rather recommend C11 atomics without volatile. They use memory barriers to guarantee visibility, and getting the idea that "volatile" should be used in the context of atomics is a bad idea, as it allows reordering (unlike memory barriers).
(EDIT: The compiler doesn't even know that you're using POSIX. It can figure it out contextually. It doesn't know which C library you'll be linking with though. Bottom line: we need some C standards extensions, or failing that, GCC/Clang C extensions in order to best handle this, though there are some heuristics a compiler could implement even without those, at some mild risk.)
(E.g., suppose there was a thread-local counter of signal handlers on the stack... then stdio functions might be able to handle async-signal reentrance. It's not too farfetched, though it's obviously a lot easier to not bother at all and just leave the set of async-signal-safe functions being just the set of system calls that have sufficiently thin stubs in the C library.)
Now, the C standard could have additional keywords, much like, say, 'volatile' and friends, to describe async-signal-safety, reentrance, and other characteristics of functions. And if the C library and your programs used these then the compiler absolutely could warn or refuse to compile your program when you break the standard's rules.
Incidentally, Unix/POSIX signals are horrible. The only sane and portable way to handle them in programs that do I/O is to have an event loop and a "self-pipe" that the signal handler can write into so that the event loop can pick up and handle the signal as a normal async event in a context where there are no constraints on calling async-signal-unsafe functions. This is what I always do in my programs. I strongly recommend it. This means you don't need ppoll(2), pselect(2), signalfd(2), epoll_pwait(2), and so on -- you don't because you're always turning signals into normal I/O events, so you don't need to worry about signal blocking, and you don't need to use less widely available functions.
(That's what I was saying, except the bit that programmers cannot reasonably be expected to understand what to do in a signal handler. That you should usually only set a (volatile) flag and return immediately, and should take extreme care if you really need to do more, is basically the first thing you learn when you read up on signal handlers. That's also completely obvious if you take a look at how they are implemented.)
But on the other hand, the signals API might be a problem. Signals aren't exactly praised as a marvel of engineering. There probably is a need for something as low-level as that in some cases. But in the common case an API that just asynchronously sets a volatile flag when a signal is received, should be sufficient. Calls to "slow" devices are interrupted and return EINTR anyway.
Signal handlers were always meant to be "top-half" interrupt handlers -- set a flag or put something in a queue for the main thread to process. signalfd(2) formalized this usage.
I find this very useful, a lot more useful than to say "If you don't even read the manpages there is no help in sight".
Maybe the compiler should not do it but perhaps you could run a linter against packages that are shipped with an os to find issues like this easier.
When the signal handler is compiled, there’s nothing to say “this will be used as a signal handler,” so while the compiler has the source it doesn’t have a reason to complain about calling any particular functions.
Then, when that function is used as a signal handler, the compiler has the source to the calling code (well, the code setting up the callback) but may not have the source for the handler itself. So now it knows which rules apply but may not have the code it needs to enforce those rules. If they’re in the same file, it could. And while that is a common case, it’s not the only possibility.
How much should the compiler or linter know about the platform’s API? How can I tell the compiler about any arbitrary rule my own code must follow?
As someone else suggested, you could do it with annotations, but that’s nonstandard.
For example: the signal handler would be a function that accepts an argument of some kind, and the argument is the key to reaching all apis allowed from that point (e.g. the argument must be passed on to allowed apis, or the argument contains function pointers to all allowed functions, that is "OO-style").
I'm not saying this would be possible by any stretch of the imagination for POSIX.
Edit: to be clear -- I'm not asking this just to be snarky :-). Inferring functional information from nothing but semantic information with no functional implication is a very complicated problem that has very error-prone solutions. In C's case, in fact, I think C99 defines signal handlers as taking a single argument, an int. A compiler that doesn't let me call free from a function that takes a single integer argument wouldn't be too useful.
IRL, there are analysis tools that can help catch (a subset of) this sort of problem, but they are environment-specific and don't rely on just the function definition. They either look at the signal handler installation calls (but then they're restricted to information that's known at compile time!), or require some sort of annotation (e.g. via special comments a la Doxygen, or via macros etc.).
And even then, the sort of problems that they catch are specific to each environment. The restriction isn't that you shouldn't call <these functions> under POSIX, the restriction is that you shouldn't call functions that are not async-signal-safe (i.e. they're not re-entrant or they're not atomic with respect to signals). There are a few POSIX functions that are safe to call from signals (see man 7 signal-safety on a Linux box), but that list obviously doesn't cover user-defined functions -- some of which may be OK to call, others not so much.
You can probably determine if a function is async-call-safe with some static analysis but at this point, it's the sort of stuff that really doesn't belong in a compiler anymore...
void handle_signal(struct signal_handler_arg *arg)
which does signal handlingand this:
void check_signal_info(struct signal_handler_arg *arg)
which validates a struct signal_handler, is called outside a signal handler, and doesn't do any signal handling.Similarly, you would not be able to verify that this is okay:
void handle_signalstruct signal_handler_arg *arg)
{
...
some_global_state->some_cb(...);
}
If some_cb is populated at runtime, how do you know (at compile-time) that it's been populated with a function that's safe to call inside a signal?Let's use a pseudo OO syntax:
void foo(arg: argtype)
{
arg.do_thing(); // ok
frob(arg); // not possible - if frob might be forbidden anywhere then it's not callable like this
}
That frob is forbidden isn't because the compiler detected that foo is a signal handler, but because no forbidden functions exist. I'm not sure this would be practical in this context - but it's the only way I can imagine to have a compiler detect that you don't call a particular function from a particular context. Basically, very few global/static functoins would have to exist, and no/very little global state as well.> That frob is forbidden isn't because the compiler detected that foo is a signal handler, but because no forbidden functions exist.
or this:
> not possible - if frob might be forbidden anywhere then it's not callable like this
So basically: say that putchar is the forbidden function. Then there exists no global function "putchar". There is no way to just "import" a header and call putchar in our language/api.
Instead, everything available to a function is what's passed to it (an interface to available API). So for example for a particular type of context (a handler, say) - a very restricted api is passed in as argument, meaning the code inside that function has very little api surface area to play with.
It's not a certain set of functions that you want to forbid, it's a certain behaviour (e.g. being interruptible by signals, or being non-reentrancy). Forbidding functions won't get you anywhere. You can always forbid write(), but you won't be able to forbid functions that dynamically dispatch to write() at runtime, or user-defined functions that aren't reentrant.
Edit: i.e. when we say you shouldn't call write(2) from a signal handler, that's not because there's a list somewhere in a POSIX standard with functions that you shouldn't call in a signal handler and write(2) is on it \. That's because the POSIX spec allows write(2) to be interrupted by a signal and that's not okay inside a signal handler.
___
\ There might be one, I haven't looked at a POSIX spec in a long time now -- but either way, the point is that such a list is open :-).
Classifying functions into more categories like "interruptible", "uninterruptible" seems doable with enough language support. It doesn't look to far from what e.g. Rust and C# does with "unsafe", i.e. a status of functions that bubbles up so that your function may be tainted as unsafe by calling unsafe functions. If the number of classifications of functions is small as is the case with unsafe/safe, or uninterruptible/interruptible then it would seem possible to sort this out via compiler support and keywords for example. So for example a signal handler is then an uninterruptible function. It's a compiler error to call even indirectly, or dynamically dispatch to interruptible functions.
For any type of safety, dynamic dispatch obviously needs to be done in a type safe manner, so no pointer arithmetic/function pointer invocation can be allowed. Doing so would make the called code be assumed to be the worst of all categories: unsafe, interruptible etc). This is how unsafe/safe works too.
So while it's cumbersome I'm sure it would be possible to achieve at least to some extent, given an expressive enough language and a clever enough compiler.