NixOS 25.11/LLVM 21.1.2: SIGSEGV: invalid permissions for mapped object from pthread_exit()

This C function being called from a C++ test function and SIGSEGVing. my_thread_exit() and it’s accompanying C++ test were written over 20 years ago.

It works on aix-ppc64 , linux-armv8 (Asahi, RHEL10), linux-ppc64le , linux-s390x (RHEL8), macos-armv8 , macos-x86_64 , windows-x86_64 (25H2), solaris-sparc , solaris-x86_64 , hpux-itanium , linux-x86_64 (Debian, Void, Fedora, Asahi, RHEL (6 to 10)), but fails on nixos-x86_64/25.11

void
my_thread_exit(
    int retval)
{
#ifdef _WIN32
    ExitThread(retval);
#else
    int ret = retval;
    pthread_exit((void *) ((intptr_t) ret) );  // SIGSEGV here when ret = 0...
#endif
}
// C++ caller...
int main() {
  my_thread_exit(0);
}
Process 500465 stopped
* thread #4, name = 'test', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address=0x7bfff1ffbad0)
    frame #0: 0x00007bfff1ffbad0
->  0x7bfff1ffbad0: lock
    0x7bfff1ffbad1: movl   $0x7bfff1ff, %edx ; imm = 0x7BFFF1FF
    0x7bfff1ffbad6: addb   %al, (%rax)
    0x7bfff1ffbad8: notb   -0x15(%rbp)
(lldb) bt
* thread #4, name = 'test', stop reason = signal SIGSEGV: invalid permissions for mapped object (fault address=0x7bfff1ffbad0)
  * frame #0: 0x00007bfff1ffbad0
    frame #1: 0x00007ffff71a59d5 libunwind.so.1`unw_get_proc_info + 117
    frame #2: 0x00007ffff71ab0c6 libunwind.so.1`_Unwind_GetLanguageSpecificData + 38
    frame #3: 0x00007ffff72d0bb9 libc++abi.so.1`__gxx_personality_v0 + 265
    frame #4: 0x00007ffff7197be0 libgcc_s.so.1`_Unwind_ForcedUnwind_Phase2 + 176
    frame #5: 0x00007ffff71982c0 libgcc_s.so.1`_Unwind_ForcedUnwind + 304
    frame #6: 0x00007ffff6ea3a14 libc.so.6`__pthread_unwind + 68
    frame #7: 0x00007ffff6e9bd02 libc.so.6`pthread_exit + 66
    frame #8: 0x0000555555eb55f6 test`my_thread_exit(retval=0) at mythread.c:182:5 [opt]
    frame #9: 0x0000555555cd833a test`_client(param=<unavailable>) at test.cpp:169:5

This is how it’s being compiled.

/nix/store/wp328h9diq2ybqyml065qyflwzx14iry-clang-wrapper-21.1.2/bin/clang++ -m64 -stdlib=libc++ -fno-ms-extensions -U_WIN32 -mtune=native -march=native -static-libasan -fuse-ld=lld -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-address-use-after-return=always -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize=function -fsanitize=local-bounds -g -glldb -gcolumn-info -m64 -fuse-ld=lld -Wl,-rpath,/nix/store/wp328h9diq2ybqyml065qyflwzx14iry-clang-wrapper-21.1.2/lib64/clang/21/lib -L/nix/store/wp328h9diq2ybqyml065qyflwzx14iry-clang-wrapper-21.1.2/lib64/clang/21/lib -Wl,-rpath,/nix/store/1x19prv42yswnkfpc6a0wcx4gg17hpnn-compiler-rt-libc-21.1.2/lib/linux -L/nix/store/1x19prv42yswnkfpc6a0wcx4gg17hpnn-compiler-rt-libc-21.1.2/lib/linux -shared-libasan -Wl,--no-as-needed -fsanitize=address -fsanitize=undefined -fno-sanitize=function -fno-omit-frame-pointer -fsanitize=local-bounds -rdynamic -Xlinker --dependency-file=CMakeFiles/link.d CMakeFiles/test.cpp.o 

Is it an international null dereference only on linux? Note that on windows you pass an integer value. My bet would be (based on the SEGVing address) on some hardening options on libunwindto try to walk the already destroyed part of the stack. While in other environments libunwindmight do less and might happen not to touch the stack. Ah, retval is supposed to be opaque. Should be fine as is.

Consider writing a minimal self-contained example, preferably in the form of a derivation and without unrelated compiler flags. Pasting your example as is does not quite compile:

clang++ a.cc -o a && ./a
a.c:9:29: error: use of undeclared identifier 'intptr_t'
    9 |     pthread_exit((void *) ((intptr_t) ret) );  // SIGSEGV here when ret = 0...
      |                             ^~~~~~~~
1 error generated.

And when tweaked with missing headers does not crash:

NIX_ENFORCE_NO_NATIVE=0 x86_64-unknown-linux-gnu-clang++ a.cc -o a -stdlib=libc++ -mtune=native -march=native -static-libasan -fuse-ld=lld -fsanitize=address -fsanitize-address-use-after-scope -fsanitize-address-use-after-return=always -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize=function -fsanitize=local-bounds -g -glldb -gcolumn-info -m64 -fuse-ld=lld -Wl,-rpath,/nix/store/wp328h9diq2ybqyml065qyflwzx14iry-clang-wrapper-21.1.2/lib64/clang/21/lib -L/nix/store/wp328h9diq2ybqyml065qyflwzx14iry-clang-wrapper-21.1.2/lib64/clang/21/lib -Wl,-rpath,/nix/store/1x19prv42yswnkfpc6a0wcx4gg17hpnn-compiler-rt-libc-21.1.2/lib/linux -L/nix/store/1x19prv42yswnkfpc6a0wcx4gg17hpnn-compiler-rt-libc-21.1.2/lib/linux -shared-libasan -Wl,--no-as-needed -fsanitize=address -fsanitize=undefined -fno-sanitize=function -fno-omit-frame-pointer -fsanitize=local-bounds -rdynamic && ./a

Looking at the backtrace. it’s a crash in the stack unwinder.

Turns out, the twenty year old test was wrong. It did run on every possible OS except NixOS though… I had previously suppressed clang-analyzer-unix.StdCLibraryFunctions for that test, and that was the issue… The test was exiting a thread and attempting to join it later. I’m wanting to say it was some kind of stress test on fd_select()… The solution was just to remove the pthread_exit() for the thread function…