Doc. No.: WG21/N3618
Date: 2013-3-17
Reply to: Hans-J. Boehm
Phone: +1-650-857-3406
Email: Hans.Boehm@hp.com

N3618: What can signal handlers do? (CWG 1441)

This is an attempt to summarize the current state of discussions around CWG issue 1441. Much of this discussion has occurred within SG1, and it has moved significantly past the original issue, so it seemed appropriate to turn it into a separate paper. This attempts to reflect the contributions of many people, especially Lawrence Crowl, Jens Maurer, Clark Nelson, and Detlef Vollmann.

Background

CWG Issue 1441 points out that in the process of relaxing the restrictions on asynchronous signal handlers to allow use of atomics, we inadvertently made it impossible to use even local variables of non-volatile, non-atomic type. As a result of an initial discussion within CWG, Jens Maurer generated a proposed resolution, which addresses that specific issue.

Later discussion in SG1, both in Portland and during the February 2013 SG1 teleconference, raised a number of additional issues. Both Jens' solution and all prior versions of the standard still give undefined behavior to code involving signal handlers which we believe should clearly be legal. For example, a signal handler should be allowed to access "read-only" data that has not been modified since the signal handler was installed. Our goal is to correct such oversights, and allow some realistic signal handlers to be portable, while preserving a significant amount of implementation freedom with respect to what is allowable in a signal handler. In particular, we do not want to reinvent Posix' notion of async-signal-safe functions here.

Proposed resolution and discussion

We give several proposed changes and summarize the reasoning behind the change as well as some of the past discussion:

Replace 1.9p6, the paragraph imposing the restrictions on signal handlers

Replace 1.9p6 [intro.execution]:

When the processing of the abstract machine is interrupted by receipt of a signal, the values of objects which are neither
  1. of type volatile std::sig_atomic_t nor
  2. lock-free atomic objects (29.4)
are unspecified during the execution of the signal handler, and the value of any object not in either of these two categories that is modified by the handler becomes undefined.

with

If a signal handler is executed as a result of a call to the raise function, then the execution of the handler is sequenced after the invocation of the raise function and before its return. When a signal is received for another reason, the execution of the signal handler is unsequenced with respect to the rest of the program.

The original restriction would now be expressed elsewhere (see below) in terms of data races. This means that signal handlers can now access variables also accessed in mainline code, so long as the required happens-before orders are established.

We concluded during the February discussion that the old "interrupted by a signal" phrase referred to an asynchronous signal, and was basically OK. But after reading the C standard I'm not sure, and it makes sense to me to be more explicit. This is my latest attempt to do so.

Expand the discussion of data races to cover signal handler invocations we want to prohibit

Change the normative part of 1.10p21 [intro.multithread] as follows:

Two actions are potentially concurrent if they are performed by different threads, or 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 in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

Discussion:

There was some discussion during the February phone call as to whether we should view signal handlers as being performed by a specific thread at all, and I think we were moving towards removing that notion. A signal handler probably cannot portably tell which thread it's running on. But after thinking about this more, I don't know how to reconcile this change with atomic_signal_fence, so I am once again inclined to leave things more like they are.

There was some earlier discussion about the difference in treatment between "data races" and undefined behavior due to unsequenced operations in the same full expression (1.9p15). The conclusion appears to be that behavior is in fact the same, though for reasons that appear to apply only to C++, not C. In more detail:

Question 1: Do unsequenced atomic operations on the same object cause undefined behavior? Is {atomic<int> i; i = i++;} legal?

Answer: Yes. The operations on i are all function calls, and hence indeterminately sequenced, not unsequenced. Hence there is no undefined behavior. The answer for C may be different.

Question 2: Is { atomic<int *>p = 0; int i; (i = 17, p = &i, 1) + (p? *p : 0)} legal?

Answer: After some false starts, the answer appears to be yes. The store to p and the initial test of p are indeterminately sequenced. If the latter occurs first, the potentially unsequenced access to *p doesn't occur. In the other case, the store to i is sequenced before the store to p, which is sequenced before the test on p, which is sequenced before the questionable load from *p. This again relies heavily on the fact that atomic operations are function calls in C++.

Thus there appears to be no real difference between the treatment of unsequenced conflicting operations and data races, and we could model races between a signal handler and mainline code in the same thread using either mechanism. We could possibly even remove the distinction entirely. The situation in C is potentially (and accidentally) different.

Ensure that signal handler invocation happens after signal handler installation

Insert in 18.10 [support.runtime] after p7:

The function signal defined in <csignal> shall ensure that a call to signal synchronizes with any resulting invocation of the newly installed signal handler.

Discussion:

Note that 29.8p6 already talks about synchronizes-with relationships between a thread and a signal handler in the same thread, so I don't think this is a very fundamental change in perspective.

This does have the effect of allowing full atomics to be used in communicating with a signal handler. I can now allocate an object, assign it to an atomic pointer variable, and have a signal handler access the non-atomic objects through that variable, just as another thread could. Since signal handlers obey strictly more scheduling constraints than threads, I think this is entirely expected, and what we had in mind all the time.

Clarify which C-like functions can be used in a signal handler

Change 18.10 [support.runtime] p9 as follows:

The common subset of the C and C++ languages consists of all declarations, definitions, and expressions that may appear in a well formed C++ program and also in a conforming C program. A plain lock-free atomic operation is an invocation of a function f from clause 29, such that f is not a member function, and either f is the function is_lock_free, or for any atomic argument a passed to f, is_lock_free(a) yields true. A POF ("plain old function") is a function that uses only features from this common subset, and that does not directly or indirectly use any function that is not a POF, except that it may use functions defined in Clause 29 that are not member functions plain lock-free atomic operations. All signal handlers shall have C linkage. A POF that could be used as a signal handler in a conforming C program does not produce undefined behavior when used as a signal handler in a C++ program. The behavior of any other function other than a POF used as a signal handler in a C++ program is implementation-defined.

Discussion:

Some of the phone call discussion seems to have overlooked the existing clause 29 exemption which, for example, makes calls to is_atomic legal.

That exemption was too broad, since it allowed non-lock-free calls. Calls that acquire locks need to be prohibited in signal handlers, since they typcailly deadlock if the mainline thread already holds the lock.

I don't understand the meaning of a normative sentence that says "X does not have undefined behavior". We otherwise define its meaning, so why would it possibly have undefined behavior without this sentence? Hence I'm proposing to rephrase.

(This paragraph removes the need for a 1.10p5 change I previously proposed.)