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