Fix: Prevent reallocation of TLS during thread exit on iOS #5575
+31
−1
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.