Safe async/reentrant closure (e.g. signal handler)


#1

An interruption handler is difficult to code properly to ensure that the interrupted thread stay in a consistent and safe state.

If Rust want to be able to safely handle signals (as it safely handle threads) we probably need to create a new trait for closure (e.g. FnAsync). This function type must be reentrant and so, should only be allowed to mutate external/global objects wrapped in a dedicated type (i.e. volatile sig_atomic_t), something similar as Arc but for safe interruptions, not thread. Another way is not to allow global object modification (i.e. pure function), but this only move the problem to the raw interrupt handler (calling the closure). To be consistent, this new function type should only be allowed to call reentrant functions as well.

This also impact the memory manager (i.e. jemalloc). Heap allocation should be prohibited in this type of function to avoid inconsistent memory layout (when malloc/free is interrupted). The core lib should help here.

A safe interruption handler seems to be hard to achieve as a generic way because of the async-signal-safe FFI (cf. signal(7)) and some special cases as the errno variable, but I think Rust have the potential to code safely, even this kind of asynchronous function call.

Other problems remains (stack allocation, EINTR error handling…) but this FnAsync seems to be the more tricky.

This safety problem may impact some Rust internals and should be discuss to be able to get a future-proof Rust 1.0.


Priorities after 1.0
#2

Rust’s ownership system already mostly prevents you from making a mess via signals. Forcing the handlers to be Send would mostly work (except it would at some times force using mp-atomics instead of signal-atomics), except for the problem of thread-locals.


#3

Is there any way we can give thread locals some distinguishing lifetime? The inability to differentiate them from globals is unfortunate.

I think there probably isn’t a reason to reuse the Send trait. You could create a new OIBIT for this purpose and just make Send a subtype (mostly to gobble up all the existing implementations).


#4

Why can’t every signal handler simply be its own thread that gets awakened when the interrupt occurs? Of course this won’t work if the interrupt re-occurs before the previous interrupt is processed. But that is a bug anyway, and should panic imho.

Or is it too much overhead to register a thread per interrupt?


#5

This new closure type could help write safe green threads as well :slight_smile:


#6

cc @carllerche, @reem


#7

@l0kod it is already unsafe to access a global in a non-reentrant way in a safe fn, since all fn items implement Fn and can therefore be called concurrently or recursively.

Note that when I say reentrant I mean that no undefined behavior can be caused by the function being called recursively or concurrently, not that it will produce the same result or not panic.


#8

@reem From how I understand it, this is not true. Consider a RefCell in thread-local storage:

  1. The RefCell is initially not borrowed.
  2. A function tries to borrow it mutably, but doesn’t set the borrow flag as the optimizer determined that no code can run during the modification.
  3. The thread is interrupted during a modification, the content of the RefCell is in an inconsistent state. The signal handler is executed, It also tries to borrow the RefCell mutably. Since the borrow state had not been written by the former access, it can do so.

This yields memory-unsafety (or “undefined behavior”) with thread-locals and signal handlers.


#9

@tbu your example relies on a bug in the optimizer (step 2). RefCell uses Cell for its borrow state, which uses UnsafeCell internally.

UnsafeCell exists exactly so that incorrect optimizations like the one you are describing do not happen.


#10

You’re right, I missed that UnsafeCell.


#11

cc Make sigaction unsafe