Repository navigation
Free side module TLS when a thread exits - #28051
Conversation
_emscripten_tls_init allocates a TLS block for each side module on every thread, but nothing called _emscripten_tls_free, so each thread leaked the TLS of every loaded side module. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to free Thread Local Storage (TLS) blocks allocated by side modules when a thread exits, preventing memory leaks in dynamic linking scenarios. It adds _emscripten_thread_free_tls to clean up these blocks and integrates it into the thread exit sequence. The review feedback correctly identifies potential runtime TypeError issues in non-MAIN_MODULE builds where PThread.tlsFreeFunctions is undefined. It is recommended to wrap both the registration and execution of these TLS free functions in #if MAIN_MODULE preprocessor guards to ensure safety.
_emscripten_thread_exit freed side module TLS before processing the robust mutex list. A robust mutex living in side module TLS would then have its userspace cleanup write into the freed block, corrupting the heap. Move _emscripten_thread_free_tls() after __tl_unlock(), once the robust list has been processed and the thread unlinked, keeping the thread identity valid until the TLS blocks are gone. Add a regression test that exits a thread holding a robust mutex in side module TLS and then exercises the allocator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements the freeing of side module thread-local storage (TLS) blocks upon thread exit in Emscripten. It registers the _emscripten_tls_free functions of side modules and invokes them during thread cleanup. Feedback on the changes highlights potential runtime TypeError crashes when MAIN_MODULE is disabled, as PThread.tlsFreeFunctions is only defined under that preprocessor guard. It is recommended to add checks or guards when pushing to and iterating over tlsFreeFunctions.
I am having trouble creating individual review comments. Click here to see my feedback.
src/lib/libpthread.js (701-704)
If MAIN_MODULE is disabled, PThread.tlsFreeFunctions is not defined (as its declaration on line 116 is guarded by #if MAIN_MODULE). In this scenario, if tlsFreeFunc is somehow resolved to a truthy value, attempting to call .push() on PThread.tlsFreeFunctions will throw a TypeError. Adding a guard check for PThread.tlsFreeFunctions ensures robustness.
var tlsFreeFunc = moduleExports?.['_emscripten_tls_free'];
if (tlsFreeFunc && PThread.tlsFreeFunctions) {
PThread.tlsFreeFunctions.push(tlsFreeFunc);
}src/lib/libpthread.js (1229)
Since PThread.tlsFreeFunctions is only defined when MAIN_MODULE is enabled, calling _emscripten_thread_free_tls when MAIN_MODULE is disabled will result in a TypeError: Cannot read properties of undefined (reading 'forEach'). We should guard the body of _emscripten_thread_free_tls with #if MAIN_MODULE to prevent runtime crashes in non-main-module builds.
_emscripten_thread_free_tls: () => {
#if MAIN_MODULE
PThread.tlsFreeFunctions.forEach((f) => f());
#endif
},|
@sbc100 leak found when brute forcing side module |
|
@sbc100 Thanks for the review, I have commited the test for proof but IMO we should no add such kind of test no? We should more compile with ASAN / TSAN and just run a normal usage. |
We do support ASAN and we run the whole |
- Simplify the comment on the free in _emscripten_thread_exit. - Drop the MAIN_MODULE guard on PThread.tlsFreeFunctions so its declaration matches tlsInitFunctions; the push and the free of its entries already live inside MAIN_MODULE-only code. - Rely on the linked side module for dlopen instead of embedding it, so the --embed-file lines are no longer needed. - Drop unnecessary static qualifiers in the leak test and remove the redundant mutexattr_destroy in the robust test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
I have dig a bit into it and currently it can't detect it, because the side-module TLS is allocated with emscripten_builtin_memalign, which aliases raw dlmalloc and sits below the sanitizer allocator :( |
|
Yes, that makes sense to me. These allocations a very low level, and maybe cannot be tracked like normal allocation using ASan. I'm happy to land this with the tests you wrote ? Or maybe just one of them ? |
|
I tried to challenge those tests but they guard two different bugs. Neither catches the other. |
Keep the TLS-free JS code uniformly MAIN_MODULE-only: the push in $registerTLSInit and _emscripten_thread_free_tls already live in MAIN_MODULE blocks, so guard the declaration too rather than leaving it unconditional. Matches the EMSCRIPTEN_DYNAMIC_LINKING guard on the call site in pthread_create.c. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With dynamic linking,
_emscripten_tls_initallocates a TLS block for each side module on every new thread, but_emscripten_tls_freeis never called. Every thread that exits leaks the TLS of every loaded side module (4 KiB per thread for a side module with a 4 KiBthread_local).Side modules now export
_emscripten_tls_free,registerTLSInitcollects them, and_emscripten_thread_exitruns them after the thread's TSD destructors (so C++thread_localdestructors still see their data).The new
test_pthread_dylink_tls_freechecks heap growth over 100 threads afterdlopen()of such a side module; it fails on main and passes with this change. Over 1000 threads the heap grew by 4,108,000 bytes before and 0 after.🤖 Generated with Claude Code