Okay, this is a complex one.
Say you have a shared library written in D which exports the symbol test_fun. Now in a C application, you create a thread, which you detach, and then load the shared library and execute test_fun. Meanwhile, the main thread waits for the auxiliary thread to complete with pthread_join, and then simply terminate the application.
This will lead to the D runtime throwing asserts, and the default assert handler attempts to allocate memory with the GC, which makes the application segfault because the GC is not enabled at this stage and the asserts happen in a @nogc application anyway. If we replace the assert handler with something that doesn't do GC allocations, we can see two places in the D runtime that throw asserts; attempts to lock and unlock a pthread mutex.
Interestingly, if one calls dlclose() after executing test_fun, the application terminates without asserts. Even more interestingly, using dlopen with the RTLD_NODELETE flag, even dlclose() can't save us from druntime's assertiveness.
Here's an example application:
main.c:
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <pthread.h>
int run_lib() {
printf("run_lib()\n");
void *lib = dlopen("./libdtest.so", RTLD_NOW | RTLD_LOCAL);
if (!lib) {
fprintf(stderr, "dlopen failed: '%s'\n", dlerror());
}
int (*fun)() = dlsym(lib, "test_fun");
char *error = dlerror();
if (error) {
fprintf(stderr, "dlsym failed: '%s'\n", error);
exit(1);
}
(*fun)();
// uncomment for no asserts!
// dlclose(lib);
return 0;
}
static void *object_thread(void *p) {
pthread_detach(pthread_self());
run_lib();
}
int main() {
printf("main()\n");
pthread_t thread;
pthread_create(&thread, NULL, object_thread, NULL);
pthread_join(thread, NULL);
printf("main() done\n");
fflush(stdout);
return 0;
}
dtest.d:
import core.stdc.stdio;
import core.exception;
void dumb_assert_handler(string file, ulong line, string msg) nothrow {
printf("Someone in %s on line %d is complaining.\n", file.ptr, line);
}
extern (C) int test_fun() {
assertHandler(&dumb_assert_handler);
printf("can confirm this is fun\n");
return 0;
}
build.sh:
rm -rf dtest.o main.o libdtest.so main
dmd -c dtest.d -fPIC
dmd -oflibdtest.so dtest.o -shared -defaultlib=libphobos2.so -L-rpath=/usr/lib/
gcc -c main.c
gcc -rdynamic main.o -o main -ldl -lpthread
run ./build.sh, then run ./main
Interestingly, if you call test_fun directly from main() and not from a detached thread, druntime doesn't throw asserts.
Comment #1 by aliloko — 2017-11-02T14:10:55Z
A major problem I see is that you use pthread_detach which you are not supposed to.
People are talking about "attachment" in the context of the D runtime which maintains a list of "attached" threads (thread_attachThis/thread_detachThis), nothing to do with pthreads.
In your case, you don't want the runtime enabled so don't have to worry about this attachment to the D runtime anyway.
Comment #2 by ajidala — 2017-11-02T15:03:44Z
(In reply to ponce from comment #1)
> A major problem I see is that you use pthread_detach which you are not
> supposed to.
>
> People are talking about "attachment" in the context of the D runtime which
> maintains a list of "attached" threads
> (thread_attachThis/thread_detachThis), nothing to do with pthreads.
>
> In your case, you don't want the runtime enabled so don't have to worry
> about this attachment to the D runtime anyway.
This is just a minimal test application which shows exactly the behaviour that is causing an issue. The place I ran onto this in the real world is when trying to write a SO plugin for mpv. mpv spawns SO plugins in a detached thread, and I have no control over this. Ideally I'd use the runtime within my thread in that case so I can get things like the GC, but this issue happens both with the runtime initialised and without.
There really is no reason for D to segfault or throw asserts at all if the parent application doesn't call dlclose() before shutting down.
Comment #3 by ajidala — 2017-11-12T15:20:50Z
fwiw, you can get rid of the naughty pthread_join call which would result in undefined behaviour for detached threads by replacing it with a simple sleep(3), which will result in the same segfault inside the D runtime, effectively proving that a completely valid C program can call into completely valid D code and cause a crash because of the D runtime.
Comment #4 by stanislav.blinov — 2021-12-08T22:13:49Z
With 2.098, I observer no mentioned asserts. However, this does lead to an abort IFF the shared library isn't dlclosed.
main()
run_lib()
can confirm this is fun
main() done
Aborting from src/rt/sections_elf_shared.d(523) _handleToDSO not in sync with _loadedDSOs.[1] 75008 abort (core dumped) ./main
Comment #5 by robert.schadek — 2024-12-07T13:37:41Z