Skip to content

Free side module TLS when a thread exits - #28051

Merged
sbc100 merged 5 commits into
emscripten-core:mainfrom
clementperon:side-module-tls-free
Oct 29, 2026
Merged

sbc100 merged 5 commits into
emscripten-core:mainfrom
clementperon:side-module-tls-free

Conversation

@clementperon

Copy link
Copy Markdown
Contributor

With dynamic linking, _emscripten_tls_init allocates a TLS block for each side module on every new thread, but _emscripten_tls_free is 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 KiB thread_local).

Side modules now export _emscripten_tls_free, registerTLSInit collects them, and _emscripten_thread_exit runs them after the thread's TSD destructors (so C++ thread_local destructors still see their data).

The new test_pthread_dylink_tls_free checks heap growth over 100 threads after dlopen() 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

_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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/libpthread.js
Comment thread src/lib/libpthread.js
@clementperon
clementperon marked this pull request as draft October 26, 2026 10:21
_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>
@clementperon
clementperon marked this pull request as ready for review October 26, 2026 14:10

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

high

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)

high

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
  },

@clementperon

Copy link
Copy Markdown
Contributor Author

@sbc100 leak found when brute forcing side module

Comment thread src/lib/libpthread.js
Comment thread system/lib/pthread/pthread_create.c Outdated
Comment thread test/core/pthread/test_pthread_dylink_tls_free.c
Comment thread test/core/pthread/test_pthread_dylink_tls_free.c
Comment thread test/core/pthread/test_pthread_dylink_tls_free.c
Comment thread test/core/pthread/test_pthread_dylink_tls_free_robust_side.c Outdated
Comment thread test/core/pthread/test_pthread_dylink_tls_free_robust_side.c
Comment thread test/test_core.py Outdated
Comment thread test/test_core.py
@clementperon

Copy link
Copy Markdown
Contributor Author

@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.
Else we can advocate to add one test per allocation which doesn't make sense.
What do you think?

@sbc100

sbc100 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

@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. Else we can advocate to add one test per allocation which doesn't make sense. What do you think?

We do support ASAN and we run the whole test_core.py suite in asan mode, but sadly emscripten does not (yet) support TSAN.

- 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>
@clementperon

Copy link
Copy Markdown
Contributor Author

@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. Else we can advocate to add one test per allocation which doesn't make sense. What do you think?

We do support ASAN and we run the whole test_core.py suite in asan mode, but sadly emscripten does not (yet) support TSAN.

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 :(

@sbc100

sbc100 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

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 ?

@clementperon

Copy link
Copy Markdown
Contributor Author

I tried to challenge those tests but they guard two different bugs.
tls_free checks whether side-module TLS gets freed (the leak). tls_free_robust checks when a robust mutex can live in that TLS, and freeing it before the thread-exit robust-list walk corrupts the heap.

Neither catches the other.

Comment thread system/lib/pthread/pthread_create.c

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm % comment

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>
Comment thread src/lib/libpthread.js
Comment thread src/lib/libpthread.js
@sbc100
sbc100 enabled auto-merge (squash) October 29, 2026 17:52
@sbc100
sbc100 merged commit 9f276ef into emscripten-core:main Oct 29, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants