NetBSD-Bugs archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
Re: PR lib/59751: dlclose is not MT-safe depending on the libraries unloaded
> Date: Thu, 16 Jul 2026 10:47:57 +0200
> From: Anthony Mallet <anthony.mallet%laas.fr@localhost>
>
> 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.
#!/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.
Home |
Main Index |
Thread Index |
Old Index