-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Fix: Prevent reallocation of TLS during thread exit on iOS #5575
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: master
Are you sure you want to change the base?
Conversation
10e75ee to
3dc5c7c
Compare
|
The _tlv_get_addr function, which is called when a thread_local variable is accessed, operates on the DATA.__thread_data section. Accessing this section maybe trigger a page fault, resulting in blocking disk I/O. This becomes critical during thread termination.
|
|
Hi, @xuezhulian! Thank you for your pull request! I feel a bit uncertain about using |
|
@haitaka Thanks for the reply! Using standard C++ features on iOS works fine. Initially, I attempted to indirectly trigger the deinit runtime using the destructor of a C++ TLS variable. However, debugging revealed that on iOS, C++ TLS variables use _tlv_atexit to register the callback for thread destruction. Since _tlv_atexit is a public API (and its full implementation can be found in the dyld library), I ended up using _tlv_atexit directly instead of the C++ TLS variable. My primary focus is iOS development, so I am uncertain if this approach is applicable to other platforms. |
haitaka
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thread_local std::unique_ptr threadExit;
That's exactly what I had in mind! We even have this idea somewhere in our back log. Let's go this way.
Let's try replacing troublesome onThreadExit call with the setup of a thread_local cleaner that will call Kotlin_deinitRuntimeCallback on destruction, ensuring that runtimeState is still alive and accessible. If everything works out we will resolve an old annoying issue.
Problem:
The previous implementation used pthread_key_create(&terminationKey, onThreadExitCallback) to listen for thread exit and execute a cleanup callback.
The core issue was that this onThreadExitCallback function accessed a statically declared thread_local variable: THREAD_LOCAL_VARIABLE RuntimeState* runtimeState = kInvalidRuntime;.
Due to the order of operations during thread termination, the cleanup for our custom terminationKey was executed after the thread_local variable destructor. This meant that by the time our callback was called, the TLV for runtimeState had already been destroyed.
Consequently, accessing the destroyed runtimeState inside the callback triggered a new allocation for it. Because the thread's TSD cleanup loop was already complete, this newly allocated memory was never freed, leading to a memory leak.
Solution:
The fix is to change the callback registration mechanism to align with the C++ runtime's intended process for thread_local cleanup.
Instead of creating a custom key, the new implementation uses _tlv_atexit(&onThreadExitCallback, destructorRecord) to register the thread exit callback.
This works because _tlv_atexit indirectly registers our callback with the main cleanup list managed by dyld. During process initialization, dyld creates a system-level key, _terminatorsKey, and associates it with a master cleanup function, finalizeListTLV. The _tlv_atexit function essentially adds our callback to the list that finalizeListTLV will process.
Crucially, the execution of finalizeListTLV is guaranteed to happen before the individual thread_local variables like runtimeState are destroyed.
As a result, when onThreadExitCallback now accesses runtimeState, the variable is still valid, which prevents the TLV reallocation and resolves the memory leak.