The problem here is the weak declaration: $ eu-readelf --symbols=.dynsym /lib64/libcap-ng.so.0.0.0 | grep pthread_atfork 28: 0000000000000000 0 NOTYPE WEAK DEFAULT UNDEF pthread_atfork
In the Fedora 29 build, the constructor looks like this: Dump of assembler code for function init_lib: 0x00000000000025d0 <+0>: endbr64 0x00000000000025d4 <+4>: cmpq $0x0,0x4a0c(%rip) # 0x6fe8 0x00000000000025dc <+12>: je 0x25ee <init_lib+30> 0x00000000000025de <+14>: lea 0xcb(%rip),%rdx # 0x26b0 <deinit> 0x00000000000025e5 <+21>: xor %esi,%esi 0x00000000000025e7 <+23>: xor %edi,%edi 0x00000000000025e9 <+25>: jmpq 0x24f0 <pthread_atfork@plt> 0x00000000000025ee <+30>: retq src/cap-ng.c has this: /* * The pthread_atfork function is being made weak so that we can use it * if the program is linked with pthreads and not requiring it for * everything that uses libcap-ng. */ extern int __attribute__((weak)) pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); … static void init_lib(void) __attribute__ ((constructor)); static void init_lib(void) { if (pthread_atfork) pthread_atfork(NULL, NULL, deinit); } This is wrong. pthread_atfork needs to be *strong* reference, otherwise the implementation in libc_nonshared.a is not used. This implementation provides the correct __dso_handle argument, allowing unregistration at dlclose. For glibc 2.28 and later, the fix should be simple: Just delete the weak declaration. For older glibc versions, you need to call __register_atfork directly, with an explicit __dso_handle argument. (I believe systemd has an example of this which looks correct.) This is a stable glibc ABI, despite all those glibc internals. We cannot fix this in libpthread because of the tail call in init_lib. It destroys the caller's stack frame, so the identity of the calling DSO is not available to pthread_atfork. (Without the tail call, we could use __builtin_return_address (0) and the internal variant of dladdr to figure out the caller.) Thanks, Florian