Re: PR lib/59751: dlclose is not MT-safe depending on the libraries unloaded
Taylor R Campbell <[email protected]>
| Newsgroups | gmane.os.netbsd.bugs |
|---|---|
| Message-ID | <[email protected]> |
> Date: Thu, 16 Jul 2026 10:47:57 +0200 > From: Anthony Mallet <[email protected]> > > On Thursday 16 Jul 2026, at 00:03, Taylor R Campbell wrote: > > Can you try the attached patch and see if it works in your complex > > application? > > Thanks ! It fixes the recursive behaviour yes. > At least with my quick testing, the application does not deadlock anymore. That patch was hopelessly broken, sorry! It just pushed a deeper underlying problem around. > Now, I'm back to my original problem though (before your changes, > ld.elf_so around Jan 22 this year), which is a segfault in dclose(), > deterministic, although the backtrace can vary depending on the code or > if I prevent some libraries from dlclos'ing. So it was looking really > similar to the 59751 issue, but it's actually not. > > I don't think the application does something wrong. It works under > linux (which is not a reason, but I also inspected the code a > bit). The problem is that it's a really huge code base ... > > I not sure how to trace this. Next step is probably compiling > ld.elf_so with debug and step through... Can you try updating ld.elf_so from HEAD and see if that makes a difference? The latest big change (https://mail-index.netbsd.org/source-changes/2026/07/18/msg163193.html) fixes a long-standing bug in the single-threaded ordering of constructors and destructors when you call dlopen/dlclose from inside a constructor or destructor. (Also fixes more multithreaded races.) If that doesn't work, can you isolate a reproducer from your application? It is likely that creating a set of libraries that matches the DT_NEEDED graph structure (including the ordering of DT_NEEDED entries in each ELF object) and dlopen/dlclose call graph is enough to reproduce the issue. Attached is an example of an isolated reproducer for the single-threaded recursive dlopen/dlclose bug that the latest change fixes.
recurdl.t
(text/plain, 3 KB)
#!/usr/bin/env cram
Dependency relations:
- A is needed by B is needed by C
- Y is dlopened by B's constructor and dlclosed by B's destructor
- X is needed by Y
In what order do constructors get called by a program that links
against C?
Surely, A should run first, because it is needed by B which is needed
by C, and no requirement for X or Y is known yet.
Then, B should _begin_ to run first, at which point X should run and Y
should run, before B finishes. Then, B having finished, C should run
last.
(Destructor order during finalization should be the reverse of the
above, of course.)
$ cat >A.c <<EOF
> #include <stdio.h>
>
> __attribute__((constructor))
> static void init_A(void) { fprintf(stderr, "%s\n", __func__); }
>
> __attribute__((destructor))
> static void fini_A(void) { fprintf(stderr, "%s\n", __func__); }
> EOF
$ cat >B.c <<EOF
> #include <dlfcn.h>
> #include <stdio.h>
>
> static void *Y_handle;
> __attribute__((constructor))
> static void init_B(void)
> {
> fprintf(stderr, "%s enter\n", __func__);
> Y_handle = dlopen("libY.so", RTLD_LAZY|RTLD_LOCAL);
> fprintf(stderr, "%s exit\n", __func__);
> }
>
> __attribute__((destructor))
> static void fini_B(void)
> {
> fprintf(stderr, "%s enter\n", __func__);
> if (Y_handle)
> dlclose(Y_handle);
> fprintf(stderr, "%s exit\n", __func__);
> }
> EOF
$ cat >C.c <<EOF
> #include <stdio.h>
>
> __attribute__((constructor))
> static void init_C(void) { fprintf(stderr, "%s\n", __func__); }
>
> __attribute__((destructor))
> static void fini_C(void) { fprintf(stderr, "%s\n", __func__); }
> EOF
$ cat >X.c <<EOF
> #include <stdio.h>
>
> __attribute__((constructor))
> static void init_X(void) { fprintf(stderr, "%s\n", __func__); }
>
> __attribute__((destructor))
> static void fini_X(void) { fprintf(stderr, "%s\n", __func__); }
> EOF
$ cat >Y.c <<EOF
> #include <stdio.h>
>
> __attribute__((constructor))
> static void init_Y(void) { fprintf(stderr, "%s\n", __func__); }
>
> __attribute__((destructor))
> static void fini_Y(void) { fprintf(stderr, "%s\n", __func__); }
> EOF
$ cat >main.c <<EOF
> #include <stdio.h>
>
> int main(void) { fprintf(stderr, "hello world\n"); return 0; }
> EOF
$ cc -shared -fPIC -o libA.so A.c
$ cc -shared -fPIC -o libB.so B.c -L. -Wl,-R. -lA
$ cc -shared -fPIC -o libC.so C.c -L. -Wl,-R. -lB
$ cc -shared -fPIC -o libX.so X.c
$ cc -shared -fPIC -o libY.so Y.c -L. -Wl,-R. -lX
$ cc -o test main.c -L. -Wl,-R. -lC
So we _should_ get:
$ ./test
init_A
init_B enter
init_X
init_Y
init_B exit
init_C
hello world
fini_C
fini_B enter
fini_Y
fini_X
fini_B exit
fini_A
Except, that's not how it plays out! Instead, we get:
$ ./test
init_A
init_B enter
init_C
init_X
init_Y
init_B exit
hello world
fini_Y
fini_X
fini_C
fini_B enter
fini_B exit
fini_A
It's wrong in both directions!
1. init_C runs before init_B has returned.
2. fini_Y and fini_X run before fini_B has dlclosed fini_Y.