Bug#787734: libstdc++ breaks dynamic unloading
Package: libstdc++6
Version: 4.9.2-10
Severity: normal
Where libstdc++ is dynamically loaded, in some cases it prevents an
unrelated dynamic library that was loaded earlier from being unloaded.
It also, incidentally, can't itself be unloaded, but that's much less
of a problem. When I say that a library can't be unloaded, I mean that
dlclose() indicates success, but the library actually remains mapped.
I've been probing this with a test program, which just dlopens a sequence
of libraries and then dlcloses them in reverse sequence, looking at
what's actually mapped at each point:
$ cat try_dl.c
#include <stdio.h>
#include <dlfcn.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
setbuf(stdout, NULL);
if(argc <= 1) exit(2);
argv++; argc--;
void **handles = malloc(sizeof(void*) * argc);
if(!handles) exit(1);
char maps_cmd[100];
sprintf(maps_cmd, "sed -n 's:.* /: /:p' /proc/%ld/maps | sort -u", (long)getpid());
system(maps_cmd);
for(int i = 0; i != argc; i++) {
handles[i] = dlopen(argv[i], RTLD_LAZY);
printf("dlopen(\"%s\", RTLD_LAZY) = %p\n", argv[i], handles[i]);
system(maps_cmd);
if(!handles[i]) exit(1);
}
for(int i = argc; i--; ) {
int ret = dlclose(handles[i]);
printf("dlclose(%p /*\"%s\"*/) = %d\n", handles[i], argv[i], ret);
system(maps_cmd);
if(ret) exit(1);
}
exit(0);
}
$ gcc -std=c99 try_dl.c -ldl -o try_dl
$ ./try_dl /usr/lib/x86_64-linux-gnu/libperl.so.5.20 /usr/lib/x86_64-linux-gnu/libstdc++.so.6
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
dlopen("/usr/lib/x86_64-linux-gnu/libperl.so.5.20", RTLD_LAZY) = 0x1761070
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libcrypt-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libm-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
/usr/lib/x86_64-linux-gnu/libperl.so.5.20.2
dlopen("/usr/lib/x86_64-linux-gnu/libstdc++.so.6", RTLD_LAZY) = 0x1762ae0
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libcrypt-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libm-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
/usr/lib/x86_64-linux-gnu/libperl.so.5.20.2
/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.20
dlclose(0x1762ae0 /*"/usr/lib/x86_64-linux-gnu/libstdc++.so.6"*/) = 0
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libcrypt-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libm-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
/usr/lib/x86_64-linux-gnu/libperl.so.5.20.2
/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.20
dlclose(0x1761070 /*"/usr/lib/x86_64-linux-gnu/libperl.so.5.20"*/) = 0
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libcrypt-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libm-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
/usr/lib/x86_64-linux-gnu/libperl.so.5.20.2
/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.20
Observe that both libperl and libstdc++ remain loaded after the dlcloses,
along with all their dependencies. But if libperl is loaded without
libstdc++, it unloads as one would expect:
$ ./try_dl /usr/lib/x86_64-linux-gnu/libperl.so.5.20
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
dlopen("/usr/lib/x86_64-linux-gnu/libperl.so.5.20", RTLD_LAZY) = 0xd48070
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libcrypt-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libm-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
/usr/lib/x86_64-linux-gnu/libperl.so.5.20.2
dlclose(0xd48070 /*"/usr/lib/x86_64-linux-gnu/libperl.so.5.20"*/) = 0
/home/zefram/tmp/try_dl
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libpthread-2.19.so
In this case libpthread, loaded as one of libperl's dependencies, won't
unload, but this doesn't seem to be a problem.
I'll spare you the full try_dl traces for other combinations; here's what
I've learned from experimenting with it. Most shared libraries unload
as they ought to. libpthread and libstdc++ share the unusual feature
seen above that once loaded (either directly or as a dependency) they
won't unload, and this seems to be something inherent in those libraries.
A handful of other libraries also won't unload, but have either libpthread
or libstdc++ as a dependency, so maybe their stickiness is caused by
those two: for example librt (depending on libpthread) and libjack
(depending on libstdc++). Not all libraries depending on those two get
this behaviour, however.
Sometimes, where a library depends on libstdc++ and others, the main
library will unload, but libstdc++ and some other dependency will not.
For example, libxatracker depends on libstdc++ and libffi (and some
others), and can itself be unloaded along with some of its dependencies,
but leaves both libstdc++ and libffi loaded. If libffi and libstdc++ are
loaded without libxatracker, libstdc++ stays loaded but libffi unloads OK.
libstdc++ alone, not libpthread, has this behaviour of keeping loaded a
library that does not depend on it. Only a small number of libraries
are affected by this: libperl (as seen above), libtcl, and libtk.
The stickiness only occurs if libstdc++ is loaded after the victim.
If libstdc++ is loaded first, then the victim is able to unload (though
libstdc++ itself still won't unload).
On a system I have access to with older packages, including libstdc++
4.4.3, libstdc++ does not have the effect of preventing library unloading,
either of itself or of other libraries. I do see libpthread and librt
failing to unload, but not all of libpthread's dependents that have that
behaviour on the newer system.
The real problem that I run into due to this unloading problem is
concerned with running Perl code under Apache, for which I have custom
builds of both. Apache likes to process its configuration twice in
each run, unloading all of its modules between runs. When mod_perl
is shut down, it tries to unload everything that Perl loaded. It so
happens that one of the Perl XS modules incorporates some C++ code and
so brings in libstdc++ as a dependency. libperl, having been loaded as
a dependency of mod_perl, ought to unload when Apache unloads mod_perl,
but due to this problem with libstdc++ it does not. In the absence of
unloading, the second time through the configuration has stale values in
Perl global variables, and the process quickly crashes when Perl follows a
hook pointing at a module that was successfully unloaded. I need libperl
to unload properly in order to get a fresh set of its global variables.
I have a potential workaround of modifying Apache to explicitly load
libstdc++ during startup, because (as noted above) having it loaded
first seems to prevent it interfering with the unloading of other modules.
-zefram
Reply to: