Commit 1bad10de authored by jan.koester's avatar jan.koester
Browse files

memleak

parent 25bcc33f
Loading
Loading
Loading
Loading
+4 −13
Changes for include/cxx/thread: 4 added lines, 13 removed lines.
Original line number Diff line number Diff line
@@ -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); }
+45 −0
Changes for src/linux/ld/tls.cpp: 45 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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;
+9 −0
Changes for src/linux/ld/tls.h: 9 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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);

+65 −26
Changes for src/linux/thread.cpp: 65 added lines, 26 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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;
@@ -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
@@ -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
@@ -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;
}
+22 −0
Changes for src/test/e2e/CMakeLists.txt: 22 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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