Loading include/cxx/thread +4 −13 Changes for include/cxx/thread: 4 added lines, 13 removed lines. Original line number Diff line number Diff line Loading @@ -88,20 +88,11 @@ public: } void detach() { /* systempp has no pthread_detach(): CLONE_CHILD_CLEARTID + a * futex join() is how this runtime reclaims a thread's kernel * resources, and a detached thread's join() must still run * eventually to avoid leaking the sys::posixthread object. */ pthread_t t = _t; /* pthread_detach() frees the thread once it has exited. This * used to start a reaper thread to join it -- which was never * joined itself, so every detach leaked that thread instead. */ _started = false; struct detacher { static void* run(void* arg) { pthread_join((pthread_t)arg, nullptr); return nullptr; } }; pthread_t reaper; pthread_create(&reaper, nullptr, &detacher::run, t); pthread_detach(_t); } id get_id() const noexcept { return id(_t); } Loading src/linux/ld/tls.cpp +45 −0 Changes for src/linux/ld/tls.cpp: 45 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -172,6 +172,51 @@ tcb* init_tls_block(void* mem, void* thread_obj) { return t; } /* released blocks, linked through tcb::thread_obj (unused while a block is * free); a spinlock rather than a lock-free stack, whose pop has ABA */ static tcb* g_free_blocks = nullptr; static int g_free_lock = 0; static void free_lock() { while (__atomic_exchange_n(&g_free_lock, 1, __ATOMIC_ACQUIRE)) __builtin_ia32_pause(); } static void free_unlock() { __atomic_store_n(&g_free_lock, 0, __ATOMIC_RELEASE); } tcb* acquire_tls_block(void* thread_obj) { free_lock(); tcb* t = g_free_blocks; if (t) g_free_blocks = (tcb*)t->thread_obj; free_unlock(); if (!t) { void* mem = mmap(nullptr, tls_block_size(), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mem == MAP_FAILED) return nullptr; return init_tls_block(mem, thread_obj); } /* already on g_blocks: only reset it, like init_tls_block() would */ t->self = t; t->thread_obj = thread_obj; t->errno_val = 0; size_t count = __atomic_load_n(&g_module_count, __ATOMIC_ACQUIRE); for (size_t i = 0; i < count; i++) init_module_in_block(t, g_modules[i]); return t; } void release_tls_block(tcb* t) { if (!t) return; free_lock(); t->thread_obj = g_free_blocks; g_free_blocks = t; free_unlock(); } long tls_module_offset(size_t id) { if (id == 0 || id > g_module_count) return 0; return g_modules[id - 1].offset; Loading src/linux/ld/tls.h +9 −0 Changes for src/linux/ld/tls.h: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -73,6 +73,15 @@ size_t tls_block_size(); * (which lives at the high end of the block, i.e. what %fs must point at). */ tcb* init_tls_block(void* mem, void* thread_obj); /* A worker thread's block: a released one re-initialized if there is one, * else a freshly mmap'd one (nullptr if that fails). Blocks stay on the * g_blocks list for good (see register_tls_module()), so they are * recycled instead of unmapped -- every thread used to leak its whole * block (static TLS of every module plus the 32 KiB surplus). */ tcb* acquire_tls_block(void* thread_obj); /* Hands back the block of a thread that has really exited. */ void release_tls_block(tcb* t); /* Allocates + installs the initial thread's TLS block via arch_prctl. */ void install_main_thread_tls(void* thread_obj); Loading src/linux/thread.cpp +65 −26 Changes for src/linux/thread.cpp: 65 added lines, 26 removed lines. Original line number Diff line number Diff line Loading @@ -85,8 +85,20 @@ void sys::__internal::thread::addins(sys::thread *thrd,size_t tid){ unlock(_mutex); }; /* Was empty, so the list held a node for every thread ever created. */ void sys::__internal::thread::delins(sys::thread *thrd){ lock(_mutex); ins *prev = nullptr; for (ins *cur = _firstins; cur; prev = cur, cur = cur->nextins) { if (cur->threadins != thrd) continue; if (prev) prev->nextins = cur->nextins; else _firstins = cur->nextins; if (_lastins == cur) _lastins = prev; cur->nextins = nullptr; /* ~ins() deletes the rest of the chain */ delete cur; break; } unlock(_mutex); }; /* The calling thread's own object straight from its TCB (nullptr for the Loading Loading @@ -191,18 +203,49 @@ sys::thread::thread(void *args,threadattr *attr){ _joinable=false; _detached=false; _reclaim=0; _ParentPid=0; _ThreadPid=1; for (size_t i = 0; i < PTHREAD_KEYS_MAX; i++) _specific[i] = nullptr; void* tlsmem = mmap(nullptr, sys::ld::tls_block_size(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (tlsmem == MAP_FAILED) { _tls = sys::ld::acquire_tls_block(this); if (!_tls) { SystemException excep; throw excep[SystemException::Error] << "Clone failed can't allocate thread local space !"; } _tls = sys::ld::init_tls_block(tlsmem, this); } static sys::thread* g_detached_done = nullptr; void sys::thread::_markReclaim(int bit) { int other = bit == RECLAIM_FINISHED ? RECLAIM_DETACHED : RECLAIM_FINISHED; if (!(__atomic_fetch_or(&_reclaim, bit, __ATOMIC_ACQ_REL) & other)) return; sys::thread* head = __atomic_load_n(&g_detached_done, __ATOMIC_ACQUIRE); do { _nextThread = head; } while (!__atomic_compare_exchange_n(&g_detached_done, &head, this, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)); } void sys::thread::_reapDetached() { /* take the whole list, so concurrent sweeps never see the same entry */ sys::thread* list = __atomic_exchange_n(&g_detached_done, nullptr, __ATOMIC_ACQ_REL); while (list) { sys::thread* t = list; list = t->_nextThread; if (__atomic_load_n(&t->_ThreadPid, __ATOMIC_ACQUIRE) == 0) { delete t; continue; } /* still on its way out of the kernel: next sweep */ sys::thread* head = __atomic_load_n(&g_detached_done, __ATOMIC_ACQUIRE); do { t->_nextThread = head; } while (!__atomic_compare_exchange_n(&g_detached_done, &head, t, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)); } } extern "C" void __systempp_warmup_allocator() noexcept; Loading @@ -212,6 +255,8 @@ void sys::thread::start(){ | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID; _reapDetached(); /* Both getInstance() (alloc.cpp) and getins() below are Meyer's * singletons whose lazy-init guard has no thread-safety (see * __cxa_guard_acquire's comment in cxa_exception.cpp) -- touching Loading Loading @@ -241,8 +286,9 @@ void sys::thread::start(){ } void* sys::thread::_start(void *ins) noexcept{ try { sys::thread *mythread=(sys::thread*)((__ThreadArguments*)ins)->ins; void* ret; try { mythread->_Retval = mythread->run(((__ThreadArguments*)ins)->args); /* Real POSIX requirement: pthread_key destructors (which is * also what __cxa_thread_atexit's real thread_local destructor Loading @@ -251,35 +297,28 @@ void* sys::thread::_start(void *ins) noexcept{ * reclaims it -- not later, e.g. from whoever calls join(). */ mythread->runKeyDestructors(); mythread->_joinable=true; /* Deliberately NOT `delete mythread` here for a detached thread * (a first version of this fix did, and this codebase's own * e2e suite caught the real crash that causes): this function * is running ON the very stack that ~threadattr() (via * ~thread()) would munmap -- _attr->_stack is exactly what * start() told __clone() to use as this thread's stack. Freeing * it while still executing on it unmaps the CPU's own * in-flight stack; a few instructions (this return included) * can still succeed against stale TLB entries before a real * fault hits, which is why that crash showed up at a different, * seemingly unrelated point on every run. A real self-safe * detached-thread reclaim needs a dedicated tiny asm trampoline * that switches off the doomed stack before unmapping it and * exiting (real musl's __unmapself is exactly this) -- out of * scope for what any real caller here evidences needing; a * detached thread's sys::posixthread (and its stack mapping) * is simply leaked instead, a small, bounded, deliberate * trade-off against the alternative of crashing. */ return mythread->_Retval; ret = mythread->_Retval; }catch(SystemException &e){ return (void*)&e; ret = (void*)&e; } /* Never `delete mythread` here for a detached thread (a first * version did, and the e2e suite caught the crash): this function * runs ON the stack ~threadattr() would munmap. Queue it instead; * another thread's start() frees it once the kernel has cleared * _ThreadPid. mythread stays valid until then, so the return below * is safe. */ mythread->_markReclaim(RECLAIM_FINISHED); return ret; } void sys::thread::detach() { _detached = true; _markReclaim(RECLAIM_DETACHED); } sys::thread::~thread(){ sys::__internal::thread::getins().delins(this); sys::ld::release_tls_block((sys::ld::tcb*)_tls); if(_selfattr && _attr) delete _attr; } Loading src/test/e2e/CMakeLists.txt +22 −0 Changes for src/test/e2e/CMakeLists.txt: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3732,3 +3732,25 @@ target_link_libraries(e2e_blogi_stdlib_gaps PRIVATE add_dependencies(e2e_blogi_stdlib_gaps crti Scrt1 crtbeginS crtn systempp) add_test(NAME e2e_blogi_stdlib_gaps COMMAND e2e_blogi_stdlib_gaps) # Thread resource reclaim (2026-10-27): TLS blocks, detached threads, # std::thread::detach()'s reaper and std::async no longer leak. add_executable(e2e_thread_reclaim thread_reclaim_test.cpp) set_target_properties(e2e_thread_reclaim PROPERTIES OUTPUT_NAME "thread_reclaim_test") target_compile_options(e2e_thread_reclaim PRIVATE -fPIE) target_link_options(e2e_thread_reclaim PRIVATE -nostdlib -pie -Wl,--hash-style=sysv -Wl,--dynamic-linker,$<TARGET_FILE_DIR:systempp>/ld-linux-x86_64.so -Wl,-rpath,$<TARGET_FILE_DIR:systempp> ) target_link_libraries(e2e_thread_reclaim PRIVATE $<TARGET_OBJECTS:crti> $<TARGET_OBJECTS:Scrt1> $<TARGET_OBJECTS:crtbeginS> $<TARGET_OBJECTS:crtn> systempp ) add_dependencies(e2e_thread_reclaim crti Scrt1 crtbeginS crtn systempp) add_test(NAME e2e_thread_reclaim COMMAND e2e_thread_reclaim) Loading
include/cxx/thread +4 −13 Changes for include/cxx/thread: 4 added lines, 13 removed lines. Original line number Diff line number Diff line Loading @@ -88,20 +88,11 @@ public: } void detach() { /* systempp has no pthread_detach(): CLONE_CHILD_CLEARTID + a * futex join() is how this runtime reclaims a thread's kernel * resources, and a detached thread's join() must still run * eventually to avoid leaking the sys::posixthread object. */ pthread_t t = _t; /* pthread_detach() frees the thread once it has exited. This * used to start a reaper thread to join it -- which was never * joined itself, so every detach leaked that thread instead. */ _started = false; struct detacher { static void* run(void* arg) { pthread_join((pthread_t)arg, nullptr); return nullptr; } }; pthread_t reaper; pthread_create(&reaper, nullptr, &detacher::run, t); pthread_detach(_t); } id get_id() const noexcept { return id(_t); } Loading
src/linux/ld/tls.cpp +45 −0 Changes for src/linux/ld/tls.cpp: 45 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -172,6 +172,51 @@ tcb* init_tls_block(void* mem, void* thread_obj) { return t; } /* released blocks, linked through tcb::thread_obj (unused while a block is * free); a spinlock rather than a lock-free stack, whose pop has ABA */ static tcb* g_free_blocks = nullptr; static int g_free_lock = 0; static void free_lock() { while (__atomic_exchange_n(&g_free_lock, 1, __ATOMIC_ACQUIRE)) __builtin_ia32_pause(); } static void free_unlock() { __atomic_store_n(&g_free_lock, 0, __ATOMIC_RELEASE); } tcb* acquire_tls_block(void* thread_obj) { free_lock(); tcb* t = g_free_blocks; if (t) g_free_blocks = (tcb*)t->thread_obj; free_unlock(); if (!t) { void* mem = mmap(nullptr, tls_block_size(), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mem == MAP_FAILED) return nullptr; return init_tls_block(mem, thread_obj); } /* already on g_blocks: only reset it, like init_tls_block() would */ t->self = t; t->thread_obj = thread_obj; t->errno_val = 0; size_t count = __atomic_load_n(&g_module_count, __ATOMIC_ACQUIRE); for (size_t i = 0; i < count; i++) init_module_in_block(t, g_modules[i]); return t; } void release_tls_block(tcb* t) { if (!t) return; free_lock(); t->thread_obj = g_free_blocks; g_free_blocks = t; free_unlock(); } long tls_module_offset(size_t id) { if (id == 0 || id > g_module_count) return 0; return g_modules[id - 1].offset; Loading
src/linux/ld/tls.h +9 −0 Changes for src/linux/ld/tls.h: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -73,6 +73,15 @@ size_t tls_block_size(); * (which lives at the high end of the block, i.e. what %fs must point at). */ tcb* init_tls_block(void* mem, void* thread_obj); /* A worker thread's block: a released one re-initialized if there is one, * else a freshly mmap'd one (nullptr if that fails). Blocks stay on the * g_blocks list for good (see register_tls_module()), so they are * recycled instead of unmapped -- every thread used to leak its whole * block (static TLS of every module plus the 32 KiB surplus). */ tcb* acquire_tls_block(void* thread_obj); /* Hands back the block of a thread that has really exited. */ void release_tls_block(tcb* t); /* Allocates + installs the initial thread's TLS block via arch_prctl. */ void install_main_thread_tls(void* thread_obj); Loading
src/linux/thread.cpp +65 −26 Changes for src/linux/thread.cpp: 65 added lines, 26 removed lines. Original line number Diff line number Diff line Loading @@ -85,8 +85,20 @@ void sys::__internal::thread::addins(sys::thread *thrd,size_t tid){ unlock(_mutex); }; /* Was empty, so the list held a node for every thread ever created. */ void sys::__internal::thread::delins(sys::thread *thrd){ lock(_mutex); ins *prev = nullptr; for (ins *cur = _firstins; cur; prev = cur, cur = cur->nextins) { if (cur->threadins != thrd) continue; if (prev) prev->nextins = cur->nextins; else _firstins = cur->nextins; if (_lastins == cur) _lastins = prev; cur->nextins = nullptr; /* ~ins() deletes the rest of the chain */ delete cur; break; } unlock(_mutex); }; /* The calling thread's own object straight from its TCB (nullptr for the Loading Loading @@ -191,18 +203,49 @@ sys::thread::thread(void *args,threadattr *attr){ _joinable=false; _detached=false; _reclaim=0; _ParentPid=0; _ThreadPid=1; for (size_t i = 0; i < PTHREAD_KEYS_MAX; i++) _specific[i] = nullptr; void* tlsmem = mmap(nullptr, sys::ld::tls_block_size(), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (tlsmem == MAP_FAILED) { _tls = sys::ld::acquire_tls_block(this); if (!_tls) { SystemException excep; throw excep[SystemException::Error] << "Clone failed can't allocate thread local space !"; } _tls = sys::ld::init_tls_block(tlsmem, this); } static sys::thread* g_detached_done = nullptr; void sys::thread::_markReclaim(int bit) { int other = bit == RECLAIM_FINISHED ? RECLAIM_DETACHED : RECLAIM_FINISHED; if (!(__atomic_fetch_or(&_reclaim, bit, __ATOMIC_ACQ_REL) & other)) return; sys::thread* head = __atomic_load_n(&g_detached_done, __ATOMIC_ACQUIRE); do { _nextThread = head; } while (!__atomic_compare_exchange_n(&g_detached_done, &head, this, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)); } void sys::thread::_reapDetached() { /* take the whole list, so concurrent sweeps never see the same entry */ sys::thread* list = __atomic_exchange_n(&g_detached_done, nullptr, __ATOMIC_ACQ_REL); while (list) { sys::thread* t = list; list = t->_nextThread; if (__atomic_load_n(&t->_ThreadPid, __ATOMIC_ACQUIRE) == 0) { delete t; continue; } /* still on its way out of the kernel: next sweep */ sys::thread* head = __atomic_load_n(&g_detached_done, __ATOMIC_ACQUIRE); do { t->_nextThread = head; } while (!__atomic_compare_exchange_n(&g_detached_done, &head, t, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)); } } extern "C" void __systempp_warmup_allocator() noexcept; Loading @@ -212,6 +255,8 @@ void sys::thread::start(){ | CLONE_THREAD | CLONE_SYSVSEM | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID; _reapDetached(); /* Both getInstance() (alloc.cpp) and getins() below are Meyer's * singletons whose lazy-init guard has no thread-safety (see * __cxa_guard_acquire's comment in cxa_exception.cpp) -- touching Loading Loading @@ -241,8 +286,9 @@ void sys::thread::start(){ } void* sys::thread::_start(void *ins) noexcept{ try { sys::thread *mythread=(sys::thread*)((__ThreadArguments*)ins)->ins; void* ret; try { mythread->_Retval = mythread->run(((__ThreadArguments*)ins)->args); /* Real POSIX requirement: pthread_key destructors (which is * also what __cxa_thread_atexit's real thread_local destructor Loading @@ -251,35 +297,28 @@ void* sys::thread::_start(void *ins) noexcept{ * reclaims it -- not later, e.g. from whoever calls join(). */ mythread->runKeyDestructors(); mythread->_joinable=true; /* Deliberately NOT `delete mythread` here for a detached thread * (a first version of this fix did, and this codebase's own * e2e suite caught the real crash that causes): this function * is running ON the very stack that ~threadattr() (via * ~thread()) would munmap -- _attr->_stack is exactly what * start() told __clone() to use as this thread's stack. Freeing * it while still executing on it unmaps the CPU's own * in-flight stack; a few instructions (this return included) * can still succeed against stale TLB entries before a real * fault hits, which is why that crash showed up at a different, * seemingly unrelated point on every run. A real self-safe * detached-thread reclaim needs a dedicated tiny asm trampoline * that switches off the doomed stack before unmapping it and * exiting (real musl's __unmapself is exactly this) -- out of * scope for what any real caller here evidences needing; a * detached thread's sys::posixthread (and its stack mapping) * is simply leaked instead, a small, bounded, deliberate * trade-off against the alternative of crashing. */ return mythread->_Retval; ret = mythread->_Retval; }catch(SystemException &e){ return (void*)&e; ret = (void*)&e; } /* Never `delete mythread` here for a detached thread (a first * version did, and the e2e suite caught the crash): this function * runs ON the stack ~threadattr() would munmap. Queue it instead; * another thread's start() frees it once the kernel has cleared * _ThreadPid. mythread stays valid until then, so the return below * is safe. */ mythread->_markReclaim(RECLAIM_FINISHED); return ret; } void sys::thread::detach() { _detached = true; _markReclaim(RECLAIM_DETACHED); } sys::thread::~thread(){ sys::__internal::thread::getins().delins(this); sys::ld::release_tls_block((sys::ld::tcb*)_tls); if(_selfattr && _attr) delete _attr; } Loading
src/test/e2e/CMakeLists.txt +22 −0 Changes for src/test/e2e/CMakeLists.txt: 22 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -3732,3 +3732,25 @@ target_link_libraries(e2e_blogi_stdlib_gaps PRIVATE add_dependencies(e2e_blogi_stdlib_gaps crti Scrt1 crtbeginS crtn systempp) add_test(NAME e2e_blogi_stdlib_gaps COMMAND e2e_blogi_stdlib_gaps) # Thread resource reclaim (2026-10-27): TLS blocks, detached threads, # std::thread::detach()'s reaper and std::async no longer leak. add_executable(e2e_thread_reclaim thread_reclaim_test.cpp) set_target_properties(e2e_thread_reclaim PROPERTIES OUTPUT_NAME "thread_reclaim_test") target_compile_options(e2e_thread_reclaim PRIVATE -fPIE) target_link_options(e2e_thread_reclaim PRIVATE -nostdlib -pie -Wl,--hash-style=sysv -Wl,--dynamic-linker,$<TARGET_FILE_DIR:systempp>/ld-linux-x86_64.so -Wl,-rpath,$<TARGET_FILE_DIR:systempp> ) target_link_libraries(e2e_thread_reclaim PRIVATE $<TARGET_OBJECTS:crti> $<TARGET_OBJECTS:Scrt1> $<TARGET_OBJECTS:crtbeginS> $<TARGET_OBJECTS:crtn> systempp ) add_dependencies(e2e_thread_reclaim crti Scrt1 crtbeginS crtn systempp) add_test(NAME e2e_thread_reclaim COMMAND e2e_thread_reclaim)