0

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.

7
  • 2
    Most likey reason: Thread safety. A non-atomic global variable isn't safe to be used by multiple threads unless you use a mutex or some other thread synchronization method. Commented Mar 13, 2020 at 13:29
  • 1
    The same issue for POSIX/C was answered here. Commented Mar 13, 2020 at 13:45
  • @NathanOliver : a (regular) mutex wouldn't quite help for a signal handler, due to the risk of deadlock. std::mutex isn't signal safe for a reason. Commented Mar 13, 2020 at 14:42
  • 1
    @Peregring-lk : malloc is not signal safe - you should not call it from a signal handler. Commented Mar 13, 2020 at 14:46
  • 1
    @SanderDeDycker: The Standard treats such issues as a quality of implementation outside its jurisdiction. Implementations intended for tasks that would be impossible without a signal-safe malloc would need to make it signal-safe in order to be suitable for such tasks; those not intended for such tasks would not need to make it signal-safe. Commented Mar 16, 2020 at 16:49

2 Answers 2

2

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;
}
Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.