Signals are one of the things that I think UNIX (and later POSIX) got horribly, horribly wrong. POSIX makes it hard to do things that people would love to do with signals. And their safety issues are hard to encapsulate even in a language like Rust (because you can only call async-signal-safe functions from a signal handler, and those are pretty far and few between).

And sadly, Linux manages to do some of its own custom things here that makes it even worse. One thing I find helpful sometimes is being able to inspect the actual hardware trap number, which can help distinguish between a SIGSEGV caused by a pointer to memory that doesn't exist (which is the normal cause) or an unaligned memory access in an instruction that requires aligned access [1]. There's a field for it in siginfo_t, si_trapno... but Linux doesn't fill in si_trapno for x86 nor even provide it as a field to be provided, unlike the BSDs in both regards. However, Linux does helpfully provide the trap number in the mccontext_t struct... which is only accessible via the third argument of a signal handler, and not via any ptrace access for a debugger.

If I were to redesign signals, I'd start by dividing them up into two categories: synchronous signals and asynchronous signals. This division actually already exists, but only in terms of how a signal is dispatched; I'd extend it so that the details of what the signals look like and how they are handled are completely different.

Synchronous signals are those that are caused by processor interrupts (think a page fault), or some set of user-custom dispatchable signals (e.g., being able to request thread cancellation). These would be handled like a regular try/catch scope, although with additional opportunities for first-ditch and last-ditch handling--Windows SEH is the kind of programming model I'd go for. What this allows is being able to do something like "I'm going to call some custom code, and if it causes a segmentation fault, I can report an error and unwind the stack to code that's unaffected" (and possibly report a full stacktrace in the meantime, maybe even as the default handler if uninstalled). This kind of handling allows it to be scoped on a per-thread basis, unlike the current per-process signal handlers.

Asynchronous signals would instead be handled by inserting them into a per-process queue of unhandled signals. There would be some syscall for getting the next unhandled signal, and naturally something that could feed into whatever syscall you use for "wait until something interesting happens" (which on Linux would mean something akin to signalfd). At this point, dequeuing signals is now a relatively benign step, and you can handle this with a simple thread that does nothing but dequeues signals and sticks them into a thread-safe queue for other threads to check at their leisure. No more need to worry about async-signal-safety!

[1] Side side rant: On x86, Linux maps the #AC (alignment check) exception to SIGBUS. But #AC isn't used for SSE aligned vector move instructions, instead #GP is used. #GP is instead mapped to SIGSEGV, but Linux doesn't fill in many of the fields. (Not that I think x86 fills in the faulted address for a #GP exception, but still, no si_trapno field to easily distinguish a #PF SIGSEGV versus #GP SIGSEGV). It took me several hours of pulling my hair out before I realized I was getting #GP and not #PF...