pthread_exit() might cause leaks
| Vulnerability potential | Medium |
| DDoS potential | Medium |
pthread_exit() might cause leaks
Impact
pthread_exit terminates the calling thread immediately, unwinding only what
the POSIX cleanup machinery knows about. Any resource the thread owns that is not
registered with a cleanup handler is simply abandoned: heap blocks it allocated,
file descriptors and sockets it opened, mutexes it still holds, and — crucially —
C++ automatic objects whose destructors would have run on a normal return.
Because pthread_exit does a non-local exit, local variables go out of scope
without their destructors firing on many implementations, so RAII-managed memory,
locks and handles leak. A long-running process that spawns and exits many threads
this way accumulates leaks until it exhausts memory, descriptors or lock state.
A held mutex that is never unlocked is worse than a memory leak: it can deadlock
every other thread that needs it.
Vulnerability potential
The defect contributes to denial of service and, through leaked locks, to correctness/safety failures.
- Steady resource leakage (memory, file descriptors, thread-stack space for detached threads) under attacker-driven request volume drives the process to exhaustion and crash — a denial-of-service vector.
- A mutex abandoned while held deadlocks all contending threads, hanging the subsystem or process.
- Leaked sensitive buffers that are never freed (and thus never scrubbed) linger in memory longer than intended, a minor information-exposure risk; abandoned file descriptors can also keep privileged resources open.
Technical details
pthread_exit runs the thread’s cleanup-handler stack
(pthread_cleanup_push/pop) and thread-specific-data destructors, then
frees the thread’s own stack only if the thread was joined or detached
correctly. What it does not do is run arbitrary unwinding for resources the
program forgot to register.
C vs C++
In C, only resources tied to pthread_cleanup_push handlers and TSD
destructors are released; raw malloc blocks and open descriptors leak. In
C++, whether automatic-object destructors run during pthread_exit is
implementation-defined — glibc implements pthread_exit via forced unwinding
so destructors often do run, but this is not guaranteed by POSIX and does not
hold on every platform, so relying on it is non-portable. Mixing pthread_exit
with C++ objects is therefore fragile.
Detached vs joinable
A joinable thread that exits keeps its stack and exit status reserved until some
thread calls pthread_join; if no one ever joins, that memory leaks for the
life of the process. Detached threads free their resources automatically but
leave any joiner with a dangling reference.
main()
Calling pthread_exit from main keeps the process alive until all other
threads finish (instead of terminating them via the normal return), which can
strand resources and is a frequent source of confusion.
Catching the issue
Runtime
Valgrind/Memcheck and LeakSanitizer (-fsanitize=leak, bundled with ASan)
report blocks still reachable/lost at thread or process exit. Run leak detection
in a loop that repeatedly spawns and pthread_exits threads to surface
per-thread growth.
Static analysis and design
Coverity, Clang Static Analyzer and PVS-Studio flag resources that escape a
function via pthread_exit without release. Prefer returning normally from the
thread function so RAII and the natural control flow free everything; register
every unguarded resource with pthread_cleanup_push if an early
pthread_exit is unavoidable, and always unlock mutexes before exiting.
How to reproduce
Run under Valgrind (valgrind --leak-check=full); the block allocated before
pthread_exit is reported as definitely lost.
#include <pthread.h>
#include <stdlib.h>
static void *worker(void *arg)
{
(void)arg;
char *buf = malloc(1024); /* not registered with any cleanup handler */
(void)buf;
pthread_exit(NULL); /* buf is leaked: never freed */
}
int main(void)
{
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
pthread_join(t, NULL);
return 0;
}