On Mon, Nov 9, 2026 at 11:37 AM Pepper Gray <[email protected]> wrote:
> Both hanging and crashing can be diagnosed under a debugger, but I > think crashing has a practical advantage in CI: a crash is detected > promptly, whereas a hang typically results in a timeout and requires > further investigation. > > I checked how other pthread implementations handle self-join > (https://github.com/peppergrayxyz/pthread_join_self), and the > overwhelming majority detects it rather than wait indefinitely: > - Return `EDEADLK`: glibc, uClibc-ng, Bionic, LLVM libc, the BSDs, > Apple, Solaris, GNU Hurd, etc. > yeah, fwiw we're usually on the "abort with a clear message if caller seems broken" side of the fence for the same reason (but opposite variant) of rich felker's "it's easier to debug if you don't just return a failure [because far too much c code doesn't check for failures anyway]". a hang is great for a _local_ failure where you the author are debugging your own code and can reproduce the failure, but an abort is what you need if that's not normally the case: because the people finding these bugs are real users in the field and all you have to go on are the crash dumps and stack traces. (so why isn't this an abort? probably because of the libc++ test. a big part of how i ended up maintaining a c library is "because developers needed better c++ support, and making the llvm libc++ tests was a high priority for us". we do crash on a lot of pthread_t misuse, but when i added the self-check to pthread_join() in 2012, i was probably just trying to pass a test [since my commit message doesn't have any more detailed "why?", annoyingly] and it was a year or two before we really started the whole "abort with a clear message, because that's the only way to debug issues from the field" thing.) > - Assert (unless compiled with `NDEBUG`): CloudABI cloudlibc > - Hang: musl, mlibc, threaded wasi-libc > > Explicitly detecting a self-join is a widely adopted choice, although > that doesn't necessarily argue for returning `EDEADLK` in musl. You > also make a good point that a hang allows the thread to be canceled, > whereas a crash terminates the entire process. > > Since self-join doesn't necessarily leave the application permanently > deadlocked as long as another thread can cancel the joiner, I wondered > whether another alternative would be possible: let the self-joining > thread block while other threads exist, but crash once it becomes the > last remaining thread. Musl already tracks the number of live threads, > although this would presumably require some mechanism to notify the > blocked thread when the others exit. > > I'm not sure though, if the benefit would justify the implementation > and maintenance complexity. Simplicity seems like a good reason to > prefer an immediate crash over more elaborate handling, at least > without a concrete use case where preserving cancellation matters. > > On Mon, 9 Nov 2026 at 02:44, Rich Felker <[email protected]> wrote: > > > > On Sun, Nov 08, 2026 at 06:47:52PM +0100, Pepper Gray wrote: > > > I understand the reasoning against returning an error on UB, but I > > > think hanging indefinitely is worse because it's much harder to > > > diagnose. There's already a precedent in musl's pthread_join() > > > implementation where it explicitly calls a_crash() when it detects > > > certain invalid thread states. For example, Box64's tests/test11.c > > > joins the same thread twice. glibc returns an error on the second > > > join, which the test ignores, while musl crashes because the thread's > > > resources have already been unmapped. > > > > > > So why not deliberately crash on self-join as well, rather than hang > > > indefinitely? > > > That would allow for both: avoid allowing execution on UB and making > > > it easier to diagnose. > > > > Yes, it's something I'd considered, but I'm not sure whether it's > > better. It's a matter of whether it's easier to attach a debugger and > > see what's happening in a stuck process or re-run under debugger and > > wait for crash (or enable core files). > > > > The cases where it crashes now are UAF bugs (memory unsafety) but > > self-join is a case where nothing inherently unsafe has happened; > > semantically a self-join could be well-defined as an infinite hang (or > > hang until cancel) for the thread. So there's not really a safety > > mandate to prefer crash. > > > > If crash is deemed the better behavior though I see no reason it > > couldn't be changed. > > > > > > > > > On Sat, 7 Nov 2026 at 14:41, Rich Felker <[email protected]> wrote: > > > > > > > > On Sat, Nov 07, 2026 at 01:09:03PM +0100, Pepper Gray wrote: > > > > > According to POSIX, self-joining a thread is undefined behavior: > > > > > > > > > > > The behavior is undefined if the value specified by the thread > argument to > > > > > > pthread_join() refers to the calling thread. > > > > > > > > > > ```C > > > > > #include <pthread.h> > > > > > #include <stdio.h> > > > > > #include <string.h> > > > > > > > > > > int main(void) { > > > > > int err = pthread_join(pthread_self(), NULL); // glibc: EDEADLK, > > > > > musl: never returns > > > > > printf("returned: %s\n", strerror(err)); > > > > > } > > > > > ``` > > > > > > > > > > but the rationale recommends detecting it: > > > > > > > > > > > If an implementation detects that the value specified by the > thread argument > > > > > > to pthread_join() refers to the calling thread, it is > recommended that the > > > > > > function should fail and report an [EDEADLK] error. > > > > > > > > > > This is the case in `glibc`, which reports `EDEADLK` while musl > just hangs. > > > > > > > > > > While hanging is technically correct, the behavior is undesirable > in practice, > > > > > and a behavior like returning `EDEADLK` or even crashing would > make it easier to > > > > > deal with such an app than when it just hangs. > > > > > > > > It's very intentional that we do not facilitate forward progress when > > > > a program has invoked undefined behavior. There is no good reason to > > > > expect that a programmer has ignored the fact that the operation is > UB > > > > will have taken the effort to check the return value, so just > > > > returning an error is likely to result in the flow of execution > > > > continuing to run in an inconsistent and possibly dangerous state. > > > > > > > > Self-join suggests a confusion about who owns resources, so this may > > > > involve data races, etc. But this is a pretty broad principle in > musl, > > > > not specific to pthread_join. At one point my write-up of the > > > > principle was included in some glibc guidelines too; I can't find the > > > > link to that at the moment, but FWIW there was consensus that "return > > > > an error on UB" is bad behavior. > > > > > > > > > In C++ this case is defined for `std::thread::join()` > > > > > ([thread.thread.member]( > https://eel.is/c++draft/thread.thread.member)): > > > > > > > > > > > *Throws:* `system_error` when an exception is required > ([thread.req.exception]). > > > > > > > > > > > > *Error conditions:* > > > > > > - `resource_deadlock_would_occur` — if deadlock is detected or > `get_id() == this_thread::get_id()`. > > > > > > > > > > However this doesn't work on `libstdc++` nor `libc++` because they > both wrap > > > > > `pthread_join`, which just hangs on musl. > > > > > > > > If the C++ std::thread::join has a contract to throw on self-join > then > > > > it needs to call pthread_equal before pthread_join to check that > > > > condition. >
