ABu
ABu

Reputation: 12279

Using static objects from signal handler

According to cppreference.com (I haven't search it in the standard), it's UB to use static objects from signal handlers.

Why is UB to do such thing? What are the potential issues of that?

If the signal handler is called NOT as a result of std::abort or std::raise (asynchronous signal), the behavior is undefined if [...] the signal handler refers to any object with static storage duration that is not std::atomic (since C++11) or volatile std::sig_atomic_t.

Upvotes: 0

Views: 1027

Answers (2)

supercat
supercat

Reputation: 81179

This issue goes back to the C Standard, which uses the term UB to characterize general situations which it might sometimes be expensive for implementations to process code in sequentially-consistent fashion, even if most implementations should process such situations meaningfully when practical. Consider a function like:

extern int x,y,z;

void test(int a)
{
  int i;
  for (i=0; i<a; i++)
  {
    x=a*i;
    y=a*a;
    z=a*i;
  }
}

Should a compiler be required to store the value a*a to y between the writes to x and z, or should it be allowed to at its leisure either hoist the assignment to y ahead of the loop or defer it until after the loop's completion. If the compiler is allowed to hoist or defer the assignment to y, would there be any simple and clean way of describing program behavior if a signal happens to occur during the execution of the loop, and the signal handler reads the values of x, y, and z? The cost and value of offering various behavioral guarantees in such cases would depend upon a variety of factors which the authors of the Standard would have no way of knowing. Rather than try to write rules for what should be guaranteed, the authors of the Standard expected that compiler writers would be better able than the Committee to judge the needs of their customers.

Upvotes: 0

Sander De Dycker
Sander De Dycker

Reputation: 16243

The C++ standard has this to say about it in [intro.execution] :

19 If a signal handler is executed as a result of a call to the std::raise function, then the execution of the handler is sequenced after the invocation of the std::raise function and before its return. [ Note: When a signal is received for another reason, the execution of the signal handler is usually unsequenced with respect to the rest of the program. — end note ]

The meaning of "unsequenced" is clarified earlier :

15 ...SNIP... [ Note: The execution of unsequenced evaluations can overlap. — end note ]

Then in [intro.races] :

20 Two actions are potentially concurrent if

(20.1) — they are performed by different threads, or

(20.2) — they are unsequenced, at least one is performed by a signal handler, and they are not both performed by the same signal handler invocation.

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

The special case referred to is :

21 Two accesses to the same object of type volatile std::sig_atomic_t do not result in a data race if both occur in the same thread, even if one or more occurs in a signal handler.

To sum it all up : when a signal handler (called for an asynchronous signal) accesses an object with static storage duration that is not atomic, that access is unsequenced, and when it's happening concurrently with a conflicting access (to the same object eg.), then there's a data race, resulting in undefined behavior.

Note that this can happen just as well in a single-threaded application as in a multi-threaded application. Example (substitute int with any other type that is more obviously non-atomic if desired) :

#include <csignal>

int global = 0;

void signal_handler(int signal) {
    global = 0;  // OOPS : this access is (typically) unsequenced
                 // and might happen concurrently with the access
                 // in main, when the interrupt happens right in
                 // the middle of that access
}

int main(void) {
    std::signal(SIGINT, signal_handler);

    while (true) {
        ++global;  // potentially concurrent access
    }

    return 0;
}

Upvotes: 2

Related Questions