-
Notifications
You must be signed in to change notification settings - Fork 114
handle reentrancy during initialization #762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
62b2b96
to
29d2562
Compare
Ooops, still cannot make it work. The following code will stuck forever: #include <snmalloc/snmalloc.h>
#include <stdlib.h>
void do_nothing() {}
extern "C" void * calloc(size_t num, size_t size) {
return snmalloc::libc::calloc(num, size);
}
extern "C" void free(void * p) {
return snmalloc::libc::free(p);
}
int main() {
for (int i = 0; i < 8192; ++i)
atexit(do_nothing);
} |
cfc1f25
to
91e2b6f
Compare
Interesting, I think snmalloc is correctly reentrant: snmalloc/src/snmalloc/global/threadalloc.h Lines 118 to 128 in 80bdcd9
However, atexit is not reentrant from the perspective of calloc calling back into atexit. This means we should not be calling atexit in register clean up? |
I am still debugging a bit. |
I wonder if you could fix this using the constructor attribute? __attribute__((constructor))
void startup_init()
{
ThreadAlloc::CheckInit::check_init([](){}, [](){});
} This would trigger initialisation outside the atexit call. As long as glibc itself didn't call atexit, we wouldn't have a problem? |
Good luck. I'm signing off for the night, but will look more tomorrow. |
The The remaining problem is So according to #762 (comment), do we need to handle |
I think the safety check is no longer needed, removing them. |
cb1cd3f
to
28b043d
Compare
So I have been looking at this today, and unfortunately #include <thread>
#include <array>
#ifndef __has_feature
# define __has_feature(x) 0
#endif
#if defined(__linux__) && !__has_feature(address_sanitizer) && \
!defined(__SANITIZE_ADDRESS__)
# define RUN_TEST
#endif
#ifdef RUN_TEST
# include <snmalloc/snmalloc.h>
# include <stdlib.h>
// A key in the second "second level" block of the pthread key table.
// First second level block is statically allocated.
// This is be 33.
pthread_key_t key;
void thread_setspecific()
{
// If the following line is uncommented then the test will pass.
// free(calloc(1, 1));
pthread_setspecific(key, (void*)1); // (A)
}
// We only selectively override these functions. Otherwise, malloc may be called
// before atexit triggers the first initialization attempt.
extern "C" void* calloc(size_t num, size_t size)
{
snmalloc::message("calloc({}, {})", num, size);
return snmalloc::libc::calloc(num, size);
}
extern "C" void free(void* p)
{
snmalloc::message("free({})", p);
// Just leak it
if (snmalloc::is_owned(p))
return snmalloc::libc::free(p);
}
#endif
void callback(void*)
{
snmalloc::message("callback");
}
int main()
{
// The first 32 keys are statically allocated, so we need to create 33 keys
// to create a key for which pthread_setspecific will call the calloc.
for (size_t i = 0; i < 33; i++)
{
pthread_key_create(&key, callback);
}
// The first calloc occurs here, after the first [0, 32] keys have been created
// thus snmalloc will choose the key 33, `key` contains the key `32` and snmalloc `33`.
// Both of these keys are not in the statically allocated part of the pthread key space.
std::thread(thread_setspecific).join();
// There should be a single allocator that can be extracted.
if (snmalloc::AllocPool<snmalloc::Config>::extract() == nullptr)
{
// The thread has not torn down its allocator.
snmalloc::report_fatal_error("Teardown of thread allocator has not occurred.");
return 1;
}
return 0;
} The re-entrant call to
This overwrite means that the |
I also investigated |
The function to use is actually |
I meant that the C++ destructors version (SNMALLOC_CLEANUP=CXX11_DESTRUCTORS) I think will be okay in all cases for glibc's implementation of |
I see. On the other hand, is only allocating a single cell each time, which is free of the issue. |
So I have pushed my experiments based on this PR: There are two tests, and a possible new mode for doing TLS that works nicely if the libc provides Really good find on this. This is super subtle, great work. |
During initialisation, snmalloc will need to call
atexit
to install a cleanup hook. This atexit function may call malloc on themselves while holding some mutexes, thus leading to deadlocks.This is only a very rare situation depending on libc implementations. Normally, malloc is called at least once during early cruntime startup. However, as shown in the test, if the implementation happen to skip early initialisation of the allocator and the application happen to install a bunch of exiting hooks, then one will run into a deadlock.
This patch instead installs the main thread finalisation hook to static .fini arrays to avoid those bad situations.