Document number: N1874
Submitter: Martin Sebor
Submission Date: September 29, 2014
Subject: problems with references to objects in signal handlers

Changes From N1812

During a discussion of the paper at the April 2014 meeting it was suggested that "Allowing access to const qualified objects would be a feature and cannot be accomplished by the mechanism of a defect report." In response, the author agreed to modify the paper and limit the scope of the proposed corrigendum. Upon reflection, however, the author realized that such a change would only constitute a new feature if the current standard unambiguously prevented it. However, since the the standard is ambiguous, it can reasonably be interpreted to already allow such a feature. Therefore, this updated revision of the paper merely clarifies this point without modifying the suggested corrigendum.

Summary

There are two problems in section 7.14.1.1 The signal function, paragraph 5, which specifies the constraints under which signal handlers can access objects declared in other scopes. The problems are summarized in the following two subsections. The section titled Suggested Technical Corrigendum then proposes a correction to both.

Section 7.14.1.1 The signal function, paragraph 5, specifies the following constraints. Note, in particular, to use of the word "refers," and the reference to objects with "static or thread storage duration" underscored in the text below. They are the subject of this defect report.

If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers to any object with static or thread storage duration that is not a lock-free atomic object other than by assigning a value to an object declared as volatile sig_atomic_t, or the signal handler calls any function in the standard library other than the abort function, the _Exit function, the quick_exit function, or the signal function with the first argument equal to the signal number corresponding to the signal that caused the invocation of the handler.

Underspecification of referring to objects

The standard doesn't formally define the term refer but its uses in other parts of the text suggest that it might denote any use of an object, including one that doesn't involve accessing it, or accessing a read-only object without modifying it. This section explains why such an interpretation would be undesrirable and provides a motivation for using more precise language in order to avoid it.

Preventing signal handlers from accessing (either reading or writing) modifiable objects is necessary in order to avoid data races with other accesses (reads and writes) to the same object in the rest of the program that are in progress but not completed at the time the signal is delivered.

Interpreting the word refer to include even mentioning the name of an object in an unevaluated context such as the sizeof expression, or taking its address is undefined in a signal handler. However, such a restriction would be unnecessary since such references cannot introduce a data race between the signal handler and the rest of the program. Thus, such an interpretation of the word would be undesirable and needlessly constrain safe programs.

Interpreting the word refer to mean access (which is defined in 3.1 as reading or modifying the value of an object) would imply that reading a const object that cannot be written to by the rest of the program would be undefied in a signal handler. Such a restriction would also be unnecessary since reading const objects cannot introduce a data race. Thus, this interpretation would also be undesirable.

Note that const objects are those that are declared const. In particular, reading an object that was not declared const via a pointer to a const-qualified type does not constitute an access to a const object.

The comments in the following example should make this distinction clear:

const int safe = (1 << SIGINT) | (1 << SIGQUIT);
      int unsafe = (1 << SIGHUP) | (1 << SIGTERM);

volatile sig_atomic_t sigcount [2];

void handler (int signo) {

    const int *pmask;   // pointer to const int

    // taking the address of any object is safe and should be allowed
    pmask = &safe;

    // access to safe should be allowed since it's a const object
    if ((1 << signo) & *pmask)
        ++sigcount [0];

    // safe and should be allowed
    pmask = &unsafe;

    // access to unsafe remains undefined since it's not a const object
    if ((1 << signo) & *pmask)
        ++sigcount [1];
}

Missing restriction to access other functions' local objects

The sentence from paragraph 5 quoted above specifically singles out objects with static or thread storage duration, but permits signal handlers to access objects with automatic storage duration without a similar restriction. However, a signal handler that has access to a local variable defined in another function whose execution is interrupted by the delivery of a signal resulting in the invocation of the signal handler contains the same potential data race as if the two functions both accessed the same object with static storage duration.

To see how this condition could arise, consider the following program which, when atomic_intptr_t is a lock-free type, is strictly conforming according to the letter of the standard despite the data race.

atomic_intptr_t p;   // assume atomic_intptr_t is lock-free

void handler (int signo) {
    // the following write access should be undefined since it modifies
    // an object with automatic storage duration declared in f
    ++*(int*)p;
}

void f (void) {
    int i = 0;
    p = (atomic_intptr_t)&i;

    signal (SIGINT, handler);

    while (i < 7)
        printf ("%i\n", i);
}

Suggested Technical Corrigendum

The proposed corrigendum below changes the standard to avoid either of the two undesirable interpretations discussed above, and to add the missing restriction to prevent accessing local variables defined elsewhere in the program. The reference to the lifetime of automatic objects makes sure that accesses to local variables defined in signal handlers themselves as well as in functions called from them remain well defined.

In section 7.14.1.1, modify the first sentence of paragraph 5 as indicated below:

If the signal occurs other than as the result of calling the abort or raise function, the behavior is undefined if the signal handler refers toaccesses any non-const object with static or thread storage duration, or any non-const object with automatic storage duration whose lifetime started before the signal handler has been entered, that is not a lock-free atomic object other than by...

In addition, make the corresponding change to section J.2 Undefined behavior.