Tackle async signal safety in Swift

How do you safely interrupt a command-line program written in Swift? This question was posed to me this week by a reader who got in touch to point out that boilerplate code included in my How to write macOS command line tools in Swift post might not be totally safe: it could leave a program and system in an undefined state, which is never a good thing. So I took a closer look.

What does the particular code do? It traps the ctrlc key combo to cancel processing more politely than just dumping ^c to the terminal and quitting. The code looks like this:

signal(SIGINT) {
    _ in writeToStderr("\(BSP)\(BSP)\rimageprep interrupted -- halting")
    exit(EXIT_CTRL_C_CODE)
}

How does it work? When you hit ctrlc in Terminal, it sends, via the OS, the Unix interrupt signal, aka SIGINT, to the currently running process. Signals are low-level notification messages sent from one process to another. They cause the target program to be interrupted, or ‘pre-empted’. This triggers a registered handler, or a default one, which performs actions according to the received signal. SIGINT tells the receiving process — in this case my imageprep app — that the user wants it to quit.

The point raised by reader Jeremy Pereira is that this broad approach is potentially unsafe. Specifically, it does not provide ‘async signal safety’. What does that mean?

The code above uses Foundation’s signal() function to register a block which will be run when a specified signal — in this case, SIGINT — is received. The block has a single parameter, the integer value identifying the signal that triggered its execution. I know it’s SIGINT, so I’ve used an underscore to ignore the input value. It writes a polite message string to the STDERR output and exits. The exit() function is passed the standard return code for ctrlc, 130.

That seems straightforward, but there’s a problem: if the asynchronously called block calls a function that had just been called by the pre-empted code, there’s a very strong risk of intermingling data in a bad way. Here’s how the Linux signal safety man page puts it:

“Suppose that the main program is in the middle of a call to a stdio function such as printf() where the buffer and associated variables have been partially updated. If, at that moment, the program is interrupted by a signal handler that also calls printf(), then the second call to printf() will operate on inconsistent data, with unpredictable results.”

This is because printf() is not ‘re-entrant’: it can’t be interrupted in the middle of its execution and safely be called again before its previous invocations complete execution. This is because both calls work with the same static data allocation established by the first call. They are not independent.

“Ensure that (a) the signal handler calls only async-signal-safe functions, and (b) the signal handler itself is re-entrant with respect to global variables in the main program,” advises the man page.

My code doesn’t explicitly use printf() and works only with constants, so you might think that’s OK. But the thing about re-entrancy is that it’s dependent on all the calls that the function being considered makes. A function is only re-entrant if the functions it calls are re-entrant. And the functions they call are re-entrant. And the functions they call are re-entrant… and on and on.

Most functions in a program work with and modify global data, or use statically allocated data structures, so there’s a very good chance they are not re-entrant.

My writeToStderr() function doesn’t call Swift’s print() function, but that’s a common call for a Swift programmer to make in this context. Does print(), deep within its code, call printf()? If it does, it will not be re-entrant and therefore shouldn’t be called in a signal handler. In any case, my code is doing string interpolation, so it’s not a straightforward case of ‘write out static string’. Jeremy is rightly concerned that a programmer might take my example and add to it print(), printf() or some other function that isn’t re-entrant.

Async-signal safely is limited. The POSIX Unix compatibility standard specifies only 191 functions that ought to be async-signal safe. Darwin, macOS’ Unix core, aims for conformance with the Unix 03 standard, but it’s not completely POSIX compliant.

exit() is supposed to be compliant and probably is in Darwin. writeToStderr() uses the Foundation framework’s FileHandle.standardError.write() function to output the received string after first converting it to data. Does that use the async-signal safe write() function? Even if it does, is Darwin’s version of write() actually async-signal safe, as POSIX mandates, or one of Darwin’s functions that aren’t POSIX compliant?

You see the problem…

Is there a solution? Apple’s recommended, but caveated, approach is to use Grand Central Dispatch to brush the problem under the carpet. CGD provides a special, signal-oriented event source which you can hook up to a dispatch queue, including the main queue. You them provide a handler for those signals if and when they arrive:

// Make sure SIGINT does not terminate the application
signal(SIGINT, SIG_IGN)

// Set up a SIGINT-specific signal dispatch source, feeding the main queue
let sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: DispatchQueue.main)

// Add the event handler to the source
sigintSource.setEventHandler {
    writeToStderr("\(BSP)\(BSP)\rimageprep interrupted -- halting")
    exit(EXIT_CTRL_C_CODE)
}

// Start the event flow
sigintSource.resume()

The downside to this approach is its asynchronicity: events are queued, whereas event handlers set up using signal() are synchronous: they’re called immediately. Says Apple:

“Signal dispatch sources are not a replacement for the synchronous signal handlers you install using the sigaction() function. Synchronous signal handlers can actually catch a signal and prevent it from terminating your application. Signal dispatch sources allow you to monitor only the arrival of the signal. In addition, you cannot use signal dispatch sources to retrieve all types of signals. Specifically, you cannot use them to monitor the SIGILL, SIGBUS and SIGSEGV signals.”

However, “because signal dispatch sources are executed asynchronously on a dispatch queue, they do not suffer from some of the same limitations as synchronous signal handlers. For example, there are no restrictions on the functions you can call from your signal dispatch source’s event handler. The tradeoff for this increased flexibility is the fact that there may be some increased latency between the time a signal arrives and the time your dispatch source’s event handler is called.”

If you’re wondering about sigaction(), it’s the key event handler installation function for which signal() is just a simplified wrapper.

As you can see, my version of the Apple approach does terminate the application, and for my code I’m not seeing any noticeable lag between hitting ctrlc and the app quitting. You may get better — or worse — mileage, of course.

A third option is Jeremy’s: sample C code that puts in place a handler which sets a flag on a given signal; your code polls the flag. Again, it’s trading latency to sidestep the async signal safety issue.

Finally, you can also mask off interrupts whenever you call a non re-entrant function so that no handler is called. Once the call has returned, you re-enable the interrupt. But this is an extreme solution that doesn’t suit all signals: if the user wants the app to quit, your app should respond, not ignore the request because it just happened to come while non re-entrant code was running.

The question I’m left with, is which should I use? The safe but potentially slower approach, or the possibly unsafe version which handles ctrlc immediately and may therefore ensure fewer files are processed with unwanted settings? Is there any way to test for this?

1 thought on “Tackle async signal safety in Swift

  1. jeremy pereira

    Tricky isn’t it!

    I think the right answer depends on what you are trying to achieve. For example, I first started investigating how to handle signals in Swift because I have a project in progress to write a lambda calculus interpreter. For this, I need a loop that iteratively “beta reduces” a lambda expression until it can’t be beta reduced anymore. However, there’s no guarantee that any particular will ever get to the point where it can’t be beta reduced any further. Hence, the loop may never terminate. So, all I wanted was to know if ctrl-c had been pressed while the iterative loop was running. Thus, my approach of setting a flag works absolutely fine because each iteration of the loop is quite short and I can just use “has ctrl-c been pressed” as one of the loop conditions.

    If you just want to terminate a long running program, I’d be inclined not to intercept ctrl-c at all and just accept you can’t write a nice message. If you really must have the nice message, I’d go with the dispatch source method. Yes, there’s more latency, but how much is that compared to the latency of your brain realising it’s done the wrong thing, signalling to your fingers to press ctrl-c and then all the mechanical links in the chain before your program gets the signal?

    One or two extras:

    – exit(3) is not guaranteed to be async signal safe. Before it actually exits, it calls all the functions registered with atexit(3). If any of those are not async signal safe, there’s a problem. _Exit(3) is safe and so is _exit(2).

    – Disabling interrupts is not an option in Swift because memory allocation is not async signal safe. Swift does memory allocation all over the place and it’s not obvious where it happens. For example, even modifying an element in an array may cause a memory allocation because Array is a value type with copy on write semantics. If you assign one array to another, it is copied, but the data is not copied until you modify an element in either the original or the copy.

    Reply

Leave a Reply to jeremy pereira Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s