Signal semantics for Rust

Hello everyone!

Some operating systems have a concept of asynchronous signals -- an external events that can interrupt the control flow of a thread and cause it to execute a special function, called a signal handler. A control flow can be redirected into a signal handler from any point at which the signal is not blocked, which makes installing signal handlers very unsafe and requires caution while implementing a signal handler. C and C++ standards, as well as POSIX, specify what a signal is and what semantics they have, and what a signal handler can safely do.

I wonder, are there signal semantics in Rust? Specifically, regarding:

  • What functions from the standard library can be called from a signal handler? The Rust standard library is separated into three parts: core (language features), alloc (high-level interface to the memory allocator) and std (interaction with the operating system). As far as I understand, alloc and std shouldn't (generally) be assumed to be async-signal-safe. I wonder, which parts of core are safe for signal handlers and which are not? As far as I understand, panicking is not async-signal-safe because a panic handler may be async-signal-unsafe. I wonder, what about other parts, and specifically the formatting system (core::fmt)?
  • Allowed interactions with global objects. C and C++ allow signal handlers to only interact with global lock-free atomics and volatile sig_atomic_t objects. As far as I understand, the latter is a workaround for standards before C11 and C++11 (because such standards do not describe what an actual atomic variable is) and is thread-unsafe (it's still data race and undefined behavior if a signal handler from one thread assigns a global volatile sig_atomic_t and the other thread reads it at the same time). The proper way is to use atomics (core::sync::atomic). In Rust, there is no such thing as "non-lock-free atomics" (if there is no hardware support for atomic operations on certain type, then C or C++ compiler may emulate it with locking, while this is not allowed in Rust -- if there is no support for atomic operations on a certain integer type, then the corresponding atomic type is not provided)
  • I also wonder about libc implementations that provide async-signal-safe thread-local variables (such as musl). Can Rust thread_local! variable be accessed from a signal handler if it's built with musl? What happens if a signal arrives while a thread-local Cell is in the process of being modified?

I don't recall Rust explicitly specifying signal safety, but some of it can be deduced:

You can't use thread-locals nor any !Send or !Sync types. Rust constructs that are not thread-safe usually assume that they have full control over the thread, and won't be re-entrant or interrupted outside of their control. So your signal handlers must act like they're a different thread (require Send + Sync). Cell is not safe. Mutex won't be compatible.Atomic* types should be safe.

no-std core should be safe. It doesn't do any heap allocations (that's alloc/std) and shouldn't be using locks.

core::fmt could be compatible. It's a fancy abstraction, but conceptually it's just &mut Formatter. The output going through fmt::Write is under your control and in a no-std no-alloc program you'll be forced to use something primitive for it that won't lock or allocate. Of course std's formatting like println! isn't going to be compatible.

You may need to be paranoid about panics in the handlers. Rust can't statically guarantee absence of panics. I imagine that an attempt to unwind from a signal handler is going to fail spectacularly, and at best it will abort the process.