2815. quick_exit can deadlock

Section: 17.5 [support.start.term] Status: New Submitter: Jean-François Bastien Opened: 2016-11-07 Last modified: 2020-09-06

Priority: 3

View other active issues in [support.start.term].

View all other issues in [support.start.term].

View all issues with New status.

Discussion:

While SG1 was processing NB comments CA1 and LATE2 regarding P0270R1, we decided to remove the proposed guarantee that quick_exit be made signal safe.

Our reasoning is that functions registered with at_quick_exit aren't forbidden from calling quick_exit, but quick_exit implementations likely acquire some form of a lock before processing all registered functions (because a note forbids the implementation from introducing data races).

The following code can therefore deadlock:

#include <cstdlib>

int main() 
{
  std::at_quick_exit([] () { std::quick_exit(0); });
  std::quick_exit(1);
  return 0;
}

The same applies if a function registered in at_quick_exit handles a signal, and that signal calls quick_exit. SG1 believes that both issues (same thread deadlock, and signal deadlock) can be resolved in the same manner. Either:

  1. Specify that calling quick_exit while servicing quick_exit is undefined; or
  2. Specifying that calling quick_exit while servicing quick_exit is defined to not deadlock, and instead calls _Exit without calling further registered functions.

Option 2. seems preferable, and can be implemented along the lines of:

#include <array>
#include <atomic>
#include <cstddef>

namespace {

  typedef void (*func)();
  
  std::array<func, 32> quick_exit_functions;
  
  const auto* quick_exit_functions_ptr = &quick_exit_functions;
  
  std::atomic_flag lock = ATOMIC_FLAG_INIT;
  
  struct scope 
  {
    scope() { while (lock.test_and_set(std::memory_order_acquire)) ; }
    ~scope() { lock.clear(std::memory_order_release); }
  };
  
}

namespace std {

  extern "C" void quick_exit(int status) noexcept
  {
    decltype(quick_exit_functions_ptr) f;
    {
      scope s;
      f = quick_exit_functions_ptr;
      quick_exit_functions_ptr = nullptr;
    }
    if (f) {
      size_t pos = f->size();
      while (pos > 0)
        (*f)[--pos]();
    }
    _Exit(status);
  }
  
  extern "C++" int at_quick_exit(func f) noexcept
  {
    scope s;
    if (!quick_exit_functions_ptr || quick_exit_functions.size() == quick_exit_functions.max_size())
      return -1;
    quick_exit_functions[quick_exit_functions.size()] = f;
    return 0;
  }

}

Ideally, the resolution would also add back the wording which SG1 dropped from P0270R1:

Add at new element to the end of 17.5 [support.start.term] p13 (quick_exit()):

Remarks: The function quick_exit() is signal-safe (17.13.4 [csignal.syn]). [Note: It might still be unsafe to call quick_exit() from a handler, because the functions registered with at_quick_exit() might not be signal-safe. — end note]

[Issues Telecon 16-Dec-2016]

Priority 3

Proposed resolution:

This wording is relative to N4606.

  1. Add at new element to the end of 17.5 [support.start.term] p13 (quick_exit()):

    [[noreturn]] void quick_exit(int status) noexcept;
    

    -13- Effects: Functions registered by calls to at_quick_exit are called in the reverse order of their registration, except that a function shall be called after any previously registered functions that had already been called at the time it was registered. Objects shall not be destroyed as a result of calling quick_exit. If control leaves a registered function called by quick_exit because the function does not provide a handler for a thrown exception, std::terminate() shall be called. [Note: at_quick_exit may call a registered function from a different thread than the one that registered it, so registered functions should not rely on the identity of objects with thread storage duration. — end note] After calling registered functions, quick_exit shall call _Exit(status). [Note: The standard file buffers are not flushed. See: ISO C 7.22.4.5. — end note]

    -?- Remarks: The function quick_exit() is signal-safe (17.13.4 [csignal.syn]). [Note: It might still be unsafe to call quick_exit() from a handler, because the functions registered with at_quick_exit() might not be signal-safe. — end note]