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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.