> Doesn't matter, because the signal handler in this case is strictly called "during" invocation of epoll_pwait, so there's no risk of it interrupting the initialization of a TLS object. The usual rules about async signal safety do not need to be followed here; it's as if epoll_wait()'s implementation made a plain old function call to the signal handler.
And if dlopen() is called in another thread, needing to (re)allocate TLS space and/or rebuild the index? You're relying on implementation details.
TLS is async-signal safe in musl libc, but musl libc also fudges (arguably) dlclose--it leaks memory as its async-signal safe data structures prevents it from deallocating per-module TLS space.
glibc may work by accident, or in the past 10 years or so they may have refactored things to work for your case. But, again, strictly speaking you're definitely relying on implementation details. (Which is perfectly acceptable as long as the dependency is transparent.)
> It does not play well with signals that target a specific thread (pthread_kill()) -- on FreeBSD, all threads will get the kqueue event, while on MacOS, none of them do.
That's because file descriptors are independent of threads--a thread doesn't "own" a descriptor--so the semantics of per-thread signals cannot cleanly map. (Multiple threads can wait on kqueue/epoll/signalfd descriptor, and moreover on both BSD and Linux you can install a kqueue/epoll descriptor as an event on another kqueue/epoll descriptor.) That Linux tries to fit a square peg into a round-hole with signalfd in this regard is probably related to the horrendous locking issues you encountered.
How are EVFILT_SIGNAL semantics better as a general matter? Among other things, it permits different libraries or components to listen for a signal without stepping on the toes of any other component. That's something that is simply impossible on Linux, period, where even signalfd is basically implemented as a signal handler--which is why you can't have both a signal handler and signalfd responding to a signal, not to mention multiple signalfd's responding to a signal.
There really aren't a lot of great solutions, here. OTOH, there aren't many use cases for per-thread signals, at least as originally conceived on Unix. EPIPE should be masked and handled in-band for anything with an event loop. SIGBUS and SIGSEGV must be handled by a signal handler, if at all. I'm sure you have your reasons, but at the end of the day it sounds like you're dealing with an issue at the nexus of signals, threading, and process semantics that cannot be correctly[1] resolved without a new, dedicated kernel API.
[1] Not strictly true. You could install a dedicated stack using sigaltstack for each thread, which could then be used to smuggle per-thread data to the signal handler, e.g. placing the data at a fixed offset from the stack given to sigaltstack. I did this once for an app that needed to longjmp from SIGSEGV--the SIGSEGV occurred by design when indexing off the end of an mmap'd array. Bounds checking and growing of the array inline was too slow as the constant inlined conditional checks destroyed pipelining. Catching SIGSEGV and longjmp'ing back to a safe point to regrow the array and then restarting the operation improved performance by some multiple > 2. Using the sigaltstack trick meant the library could be safely used from multiple threads, and without the rest of the application needing to know anything.