From 68741e3629966b668901e309aca6307d09d1e1f7 Mon Sep 17 00:00:00 2001 From: Nate Bracy Date: Wed, 16 Sep 2026 23:33:25 -0400 Subject: [PATCH 1/3] Exit proxied main with its status when keepalives run out With -pthread -sPROXY_TO_PTHREAD -sEXIT_RUNTIME, a main() that returns while a runtime keepalive is held now exits with its status once the count reaches zero, instead of exiting 0 with atexit handlers skipped. Fixes #27721 --- src/lib/libcore.js | 20 ++++++++ src/lib/libpthread.js | 23 +++++++++ system/lib/libc/crt1_proxy_main.c | 2 + system/lib/pthread/threading_internal.h | 2 + test/other/test_proxied_main_keepalive_exit.c | 47 +++++++++++++++++++ test/test_other.py | 15 ++++++ 6 files changed, 109 insertions(+) create mode 100644 test/other/test_proxied_main_keepalive_exit.c diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 928777d174877..5be61f8984b79 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -113,6 +113,9 @@ addToLibrary({ #if PTHREADS '$exitOnMainThread', #endif +#if PROXY_TO_PTHREAD + '$proxiedMainDone', +#endif #if PTHREADS_DEBUG || ASSERTIONS '$runtimeKeepaliveCounter', #endif @@ -132,6 +135,10 @@ addToLibrary({ #endif #if PTHREADS_DEBUG dbg(`Pthread ${ptrToString(_pthread_self())} called exit(${status}), posting exitOnMainThread.`); +#endif +#if PROXY_TO_PTHREAD + // Forget a waiting main return. + proxiedMainDone = false; #endif // When running in a pthread we propagate the exit back to the main thread // where it can decide if the whole process should be shut down or not. @@ -2169,6 +2176,11 @@ addToLibrary({ #if PTHREADS '_emscripten_thread_exit', #endif +#if PROXY_TO_PTHREAD + '$proxiedMainDone', + '$proxiedMainExitCode', + '$exitOnMainThread', +#endif #if RUNTIME_DEBUG >= 2 '$runtimeKeepaliveCounter', #endif @@ -2192,6 +2204,14 @@ addToLibrary({ // exit the current thread, but only if there is one active. // TODO(https://github.com/emscripten-core/emscripten/issues/25076): // Unify this check with the runtimeExited check above +#if PROXY_TO_PTHREAD && EXIT_RUNTIME + // Run a waiting main return once. + if (proxiedMainDone) { + proxiedMainDone = false; + exitOnMainThread(proxiedMainExitCode); + return; + } +#endif if (_pthread_self()) __emscripten_thread_exit(EXITSTATUS); return; } diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 7e68b6e14d7d8..38367fc030b87 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -986,6 +986,20 @@ var LibraryPThread = { _exit(returnCode); }, +#if PROXY_TO_PTHREAD + // Main's return, saved for maybeExit. + $proxiedMainDone__internal: true, + $proxiedMainDone: false, + $proxiedMainExitCode__internal: true, + $proxiedMainExitCode: 0, + + __emscripten_proxied_main_done__deps: ['$proxiedMainDone', '$proxiedMainExitCode'], + __emscripten_proxied_main_done: (status) => { + proxiedMainDone = true; + proxiedMainExitCode = status; + }, +#endif + #if MEMORY64 // Calls proxyToMainThread but returns a bigint rather than a number $proxyToMainThreadPtr__deps: ['$proxyToMainThread'], @@ -1144,6 +1158,10 @@ var LibraryPThread = { #if !MINIMAL_RUNTIME '$keepRuntimeAlive', '$runtimeKeepaliveCounter', +#endif +#if PROXY_TO_PTHREAD + '$proxiedMainDone', + '$proxiedMainExitCode', #endif ], $invokeEntryPoint: {{{ asyncIf(ASYNCIFY == 2) }}}(ptr, arg) => { @@ -1166,6 +1184,11 @@ var LibraryPThread = { noExitRuntime = 0; #endif #endif +#if PROXY_TO_PTHREAD + // No main return waiting yet. + proxiedMainDone = false; + proxiedMainExitCode = 0; +#endif #if MAIN_MODULE // Before we call the thread entry point, make sure any shared libraries diff --git a/system/lib/libc/crt1_proxy_main.c b/system/lib/libc/crt1_proxy_main.c index 2d178ca2e473a..e21c062dc7e11 100644 --- a/system/lib/libc/crt1_proxy_main.c +++ b/system/lib/libc/crt1_proxy_main.c @@ -32,6 +32,8 @@ static void* _main_thread(void* param) { if (!emscripten_runtime_keepalive_check()) { exit(rtn); } + // Wait for keepalives, then exit with main's status. + __emscripten_proxied_main_done(rtn); return NULL; } diff --git a/system/lib/pthread/threading_internal.h b/system/lib/pthread/threading_internal.h index 097595e3ba6b9..0e62c97081ac0 100644 --- a/system/lib/pthread/threading_internal.h +++ b/system/lib/pthread/threading_internal.h @@ -63,6 +63,8 @@ void _emscripten_init_main_thread_js(void* tb); void _emscripten_thread_profiler_enable(); void _emscripten_thread_cleanup(pthread_t thread); +void __emscripten_proxied_main_done(int status); + hidden void* _emscripten_tls_init(void); hidden void _emscripten_tls_free(void); diff --git a/test/other/test_proxied_main_keepalive_exit.c b/test/other/test_proxied_main_keepalive_exit.c new file mode 100644 index 0000000000000..949a89bde444a --- /dev/null +++ b/test/other/test_proxied_main_keepalive_exit.c @@ -0,0 +1,47 @@ +// Proxied main must exit with its status after keepalives run. +#include +#include +#include +#include + +void at_exit(void) { printf("done\n"); } + +#ifdef MODE_CLEARED +int long_id; + +void never(void* arg) { + printf("cleared callback ran\n"); + abort(); +} +#endif + +void fired(void* arg) { + printf("fired\n"); +#ifdef MODE_CLEARED + emscripten_clear_timeout(long_id); +#elif defined(MODE_FORCE_EXIT) + // exit() beats a waiting return. + emscripten_force_exit(7); +#elif defined(MODE_EXIT) + exit(7); +#endif +} + +int main(void) { +#ifdef MODE_NEGATIVE + // Check the value, not just the code. + MAIN_THREAD_EM_ASM({ Module['onExit'] = (c) => { out('exited:' + c); }; }); +#else + MAIN_THREAD_EM_ASM({ Module['onExit'] = () => { out('exited'); }; }); +#endif + atexit(at_exit); +#ifdef MODE_CLEARED + long_id = emscripten_set_timeout(never, 10000, NULL); +#endif + emscripten_set_timeout(fired, 10, NULL); +#ifdef MODE_NEGATIVE + return -1; +#else + return 3; +#endif +} diff --git a/test/test_other.py b/test/test_other.py index bd63374a7c13b..7eba2116a1916 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -11603,6 +11603,21 @@ def test_proxy_to_pthread_stack(self): '-sSTACK_SIZE=128kb', '-sEXIT_RUNTIME', '--profiling-funcs']) + @requires_pthreads + @parameterized({ + '': ([], 3, 'fired\ndone\nexited\n'), + 'cleared': (['-DMODE_CLEARED'], 3, 'fired\ndone\nexited\n'), + 'force_exit': (['-DMODE_FORCE_EXIT'], 7, 'fired\ndone\nexited\n'), + 'exit': (['-DMODE_EXIT'], 7, 'fired\ndone\nexited\n'), + 'negative': (['-DMODE_NEGATIVE'], NON_ZERO, 'fired\ndone\nexited:-1\n'), + }) + def test_proxied_main_keepalive_exit(self, cflags, returncode, expected): + # See https://github.com/emscripten-core/emscripten/issues/27721 + # Proxied main that returns with a keepalive must exit with its status. + self.do_runf('other/test_proxied_main_keepalive_exit.c', expected, + cflags=['-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME'] + cflags, + assert_returncode=returncode) + @crossplatform @no_windows('ptys and select are not available on windows') def test_color_diagnostics(self): From 7e5e091e8272636c8e9b4fa7bde8874885cf14d6 Mon Sep 17 00:00:00 2001 From: Nate Bracy Date: Thu, 17 Sep 2026 21:06:56 -0400 Subject: [PATCH 2/3] nit: simplify main exit status --- src/lib/libcore.js | 3 +-- src/lib/libpthread.js | 9 ++------- system/lib/libc/crt1_proxy_main.c | 4 ++-- system/lib/pthread/threading_internal.h | 2 +- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 5be61f8984b79..8efacb26ca958 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2178,7 +2178,6 @@ addToLibrary({ #endif #if PROXY_TO_PTHREAD '$proxiedMainDone', - '$proxiedMainExitCode', '$exitOnMainThread', #endif #if RUNTIME_DEBUG >= 2 @@ -2208,7 +2207,7 @@ addToLibrary({ // Run a waiting main return once. if (proxiedMainDone) { proxiedMainDone = false; - exitOnMainThread(proxiedMainExitCode); + exitOnMainThread(EXITSTATUS); return; } #endif diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 38367fc030b87..598a6da0028a1 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -990,13 +990,10 @@ var LibraryPThread = { // Main's return, saved for maybeExit. $proxiedMainDone__internal: true, $proxiedMainDone: false, - $proxiedMainExitCode__internal: true, - $proxiedMainExitCode: 0, - __emscripten_proxied_main_done__deps: ['$proxiedMainDone', '$proxiedMainExitCode'], - __emscripten_proxied_main_done: (status) => { + __emscripten_proxied_main_done__deps: ['$proxiedMainDone'], + __emscripten_proxied_main_done: () => { proxiedMainDone = true; - proxiedMainExitCode = status; }, #endif @@ -1161,7 +1158,6 @@ var LibraryPThread = { #endif #if PROXY_TO_PTHREAD '$proxiedMainDone', - '$proxiedMainExitCode', #endif ], $invokeEntryPoint: {{{ asyncIf(ASYNCIFY == 2) }}}(ptr, arg) => { @@ -1187,7 +1183,6 @@ var LibraryPThread = { #if PROXY_TO_PTHREAD // No main return waiting yet. proxiedMainDone = false; - proxiedMainExitCode = 0; #endif #if MAIN_MODULE diff --git a/system/lib/libc/crt1_proxy_main.c b/system/lib/libc/crt1_proxy_main.c index e21c062dc7e11..d71a44d4d48b4 100644 --- a/system/lib/libc/crt1_proxy_main.c +++ b/system/lib/libc/crt1_proxy_main.c @@ -33,8 +33,8 @@ static void* _main_thread(void* param) { exit(rtn); } // Wait for keepalives, then exit with main's status. - __emscripten_proxied_main_done(rtn); - return NULL; + __emscripten_proxied_main_done(); + return (void*)(intptr_t)rtn; } EMSCRIPTEN_KEEPALIVE int _emscripten_proxy_main(int argc, char** argv) { diff --git a/system/lib/pthread/threading_internal.h b/system/lib/pthread/threading_internal.h index 0e62c97081ac0..f568d6d1bfbc8 100644 --- a/system/lib/pthread/threading_internal.h +++ b/system/lib/pthread/threading_internal.h @@ -63,7 +63,7 @@ void _emscripten_init_main_thread_js(void* tb); void _emscripten_thread_profiler_enable(); void _emscripten_thread_cleanup(pthread_t thread); -void __emscripten_proxied_main_done(int status); +void __emscripten_proxied_main_done(void); hidden void* _emscripten_tls_init(void); hidden void _emscripten_tls_free(void); From d49fcc78b984b0ce3f0cdd88baff273fe6063445 Mon Sep 17 00:00:00 2001 From: Nate Bracy Date: Sat, 19 Sep 2026 21:30:11 -0400 Subject: [PATCH 3/3] nit: drop proxiedMainDone in favor of isProxiedMainThread --- src/lib/libcore.js | 24 ++++++------------------ src/lib/libpthread.js | 24 ++++++++++++++---------- system/lib/libc/crt1_proxy_main.c | 9 +++------ system/lib/pthread/threading_internal.h | 2 +- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 8efacb26ca958..2e93d64553d15 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -113,9 +113,6 @@ addToLibrary({ #if PTHREADS '$exitOnMainThread', #endif -#if PROXY_TO_PTHREAD - '$proxiedMainDone', -#endif #if PTHREADS_DEBUG || ASSERTIONS '$runtimeKeepaliveCounter', #endif @@ -135,10 +132,6 @@ addToLibrary({ #endif #if PTHREADS_DEBUG dbg(`Pthread ${ptrToString(_pthread_self())} called exit(${status}), posting exitOnMainThread.`); -#endif -#if PROXY_TO_PTHREAD - // Forget a waiting main return. - proxiedMainDone = false; #endif // When running in a pthread we propagate the exit back to the main thread // where it can decide if the whole process should be shut down or not. @@ -2177,8 +2170,7 @@ addToLibrary({ '_emscripten_thread_exit', #endif #if PROXY_TO_PTHREAD - '$proxiedMainDone', - '$exitOnMainThread', + '$isProxiedMainThread', #endif #if RUNTIME_DEBUG >= 2 '$runtimeKeepaliveCounter', @@ -2199,18 +2191,14 @@ addToLibrary({ #endif try { #if PTHREADS - if (ENVIRONMENT_IS_PTHREAD) { + if (ENVIRONMENT_IS_PTHREAD +#if PROXY_TO_PTHREAD + && !isProxiedMainThread +#endif + ) { // exit the current thread, but only if there is one active. // TODO(https://github.com/emscripten-core/emscripten/issues/25076): // Unify this check with the runtimeExited check above -#if PROXY_TO_PTHREAD && EXIT_RUNTIME - // Run a waiting main return once. - if (proxiedMainDone) { - proxiedMainDone = false; - exitOnMainThread(EXITSTATUS); - return; - } -#endif if (_pthread_self()) __emscripten_thread_exit(EXITSTATUS); return; } diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js index 598a6da0028a1..2fb3349cf0419 100644 --- a/src/lib/libpthread.js +++ b/src/lib/libpthread.js @@ -987,13 +987,12 @@ var LibraryPThread = { }, #if PROXY_TO_PTHREAD - // Main's return, saved for maybeExit. - $proxiedMainDone__internal: true, - $proxiedMainDone: false, + $isProxiedMainThread__internal: true, + $isProxiedMainThread: false, - __emscripten_proxied_main_done__deps: ['$proxiedMainDone'], - __emscripten_proxied_main_done: () => { - proxiedMainDone = true; + __emscripten_set_proxied_main_thread__deps: ['$isProxiedMainThread'], + __emscripten_set_proxied_main_thread: () => { + isProxiedMainThread = true; }, #endif @@ -1157,7 +1156,8 @@ var LibraryPThread = { '$runtimeKeepaliveCounter', #endif #if PROXY_TO_PTHREAD - '$proxiedMainDone', + '$isProxiedMainThread', + 'exit', #endif ], $invokeEntryPoint: {{{ asyncIf(ASYNCIFY == 2) }}}(ptr, arg) => { @@ -1181,8 +1181,7 @@ var LibraryPThread = { #endif #endif #if PROXY_TO_PTHREAD - // No main return waiting yet. - proxiedMainDone = false; + isProxiedMainThread = false; #endif #if MAIN_MODULE @@ -1208,13 +1207,18 @@ var LibraryPThread = { #endif function finish(result) { #if !MINIMAL_RUNTIME + EXITSTATUS = result; // In MINIMAL_RUNTIME the noExitRuntime concept does not apply to // pthreads. To exit a pthread with live runtime, use the function // emscripten_unwind_to_js_event_loop() in the pthread body. if (keepRuntimeAlive()) { - EXITSTATUS = result; return; } +#endif +#if PROXY_TO_PTHREAD + if (isProxiedMainThread) { + _exit(result); + } #endif __emscripten_thread_exit(result); } diff --git a/system/lib/libc/crt1_proxy_main.c b/system/lib/libc/crt1_proxy_main.c index d71a44d4d48b4..37d5008beee8f 100644 --- a/system/lib/libc/crt1_proxy_main.c +++ b/system/lib/libc/crt1_proxy_main.c @@ -8,10 +8,11 @@ #include #include +#include + #include #include #include -#include #include "threading_internal.h" @@ -27,13 +28,9 @@ weak int __main_void(void) { static void* _main_thread(void* param) { // This is the main runtime thread for the application. emscripten_set_thread_name(pthread_self(), "Application main thread"); + __emscripten_set_proxied_main_thread(); // Will either call user's __main_void or weak version above. int rtn = __main_void(); - if (!emscripten_runtime_keepalive_check()) { - exit(rtn); - } - // Wait for keepalives, then exit with main's status. - __emscripten_proxied_main_done(); return (void*)(intptr_t)rtn; } diff --git a/system/lib/pthread/threading_internal.h b/system/lib/pthread/threading_internal.h index f568d6d1bfbc8..73df3a0ab057b 100644 --- a/system/lib/pthread/threading_internal.h +++ b/system/lib/pthread/threading_internal.h @@ -63,7 +63,7 @@ void _emscripten_init_main_thread_js(void* tb); void _emscripten_thread_profiler_enable(); void _emscripten_thread_cleanup(pthread_t thread); -void __emscripten_proxied_main_done(void); +void __emscripten_set_proxied_main_thread(void); hidden void* _emscripten_tls_init(void); hidden void _emscripten_tls_free(void);