windows: Fix potential use-after-free when joining a thread

Because the flag was set before running the TLS cleanup, a thread
waiting in `join()` could exit the loop and destroy the thread object
before `docleanup()` is called in `end_thread()`.

This change removes the `terminated` flag and instead properly waits for
the thread to exit in `join()`.  By always removing the threads from the
hashtable in `end_thread()`, we also avoid requiring to check any flags
in `cleanup_tls()`, as it now only finds an object for external threads.

We now also make sure to call `docleanup()` before removing the thread
from the hashtable.  Otherwise, if a TLS cleanup callback calls
`thread_current[_id]()`, a new thread object would get created that is
never cleaned up.

Fixes: 0fa9c95811 ("windows: Provide a complete native Windows threading backend")
This commit is contained in:
Tobias Brunner
2026-07-24 08:47:39 +02:00
parent 677b915efe
commit f00b85dbf4
+18 -33
View File
@@ -1,4 +1,5 @@
/* /*
* Copyright (C) 2014-2026 Tobias Brunner
* Copyright (C) 2013 Martin Willi * Copyright (C) 2013 Martin Willi
* *
* Copyright (C) secunet Security Networks AG * Copyright (C) secunet Security Networks AG
@@ -72,11 +73,6 @@ struct private_thread_t {
*/ */
hashtable_t *tls; hashtable_t *tls;
/**
* Thread terminated?
*/
bool terminated;
/** /**
* Thread detached? * Thread detached?
*/ */
@@ -287,14 +283,12 @@ static void docleanup(private_thread_t *this)
} }
/** /**
* Clean up and destroy a thread * Destroy a thread
*/ */
static void destroy(private_thread_t *this) static void destroy(private_thread_t *this)
{ {
bool old; bool old;
docleanup(this);
old = set_leak_detective(FALSE); old = set_leak_detective(FALSE);
array_destroy(this->cleanup); array_destroy(this->cleanup);
@@ -309,20 +303,17 @@ static void destroy(private_thread_t *this)
} }
/** /**
* End a thread, destroy when detached * End and cleanup a thread, destroy when detached
*/ */
static void end_thread(private_thread_t *this) static void end_thread(private_thread_t *this)
{ {
docleanup(this);
remove_thread(this);
if (this->detached) if (this->detached)
{ {
remove_thread(this);
destroy(this); destroy(this);
} }
else
{
this->terminated = TRUE;
docleanup(this);
}
} }
/** /**
@@ -398,15 +389,14 @@ METHOD(thread_t, join, void*,
return NULL; return NULL;
} }
while (!this->terminated) /* join is a cancellation point, use alertable wait until thread exited */
while (WaitForSingleObjectEx(this->handle, INFINITE,
TRUE) == WAIT_IO_COMPLETION)
{ {
/* join is a cancellation point, use alertable wait */
WaitForSingleObjectEx(this->handle, INFINITE, TRUE);
} }
ret = this->ret; ret = this->ret;
remove_thread(this);
destroy(this); destroy(this);
return ret; return ret;
@@ -631,7 +621,6 @@ void thread_exit(void *val)
static void cleanup_tls() static void cleanup_tls()
{ {
private_thread_t *this; private_thread_t *this;
bool old;
/* ignore this if called for the thread that called threads_deinit() */ /* ignore this if called for the thread that called threads_deinit() */
if (!threads_lock) if (!threads_lock)
@@ -639,23 +628,17 @@ static void cleanup_tls()
return; return;
} }
old = set_leak_detective(FALSE); /* we target externally spawned threads only here, our threads remove
* themselves in end_thread(), which runs before DLL_THREAD_DETACH */
threads_lock->lock(threads_lock); threads_lock->lock(threads_lock);
this = threads->get(threads, (void*)(uintptr_t)GetCurrentThreadId());
this = threads->remove(threads, (void*)(uintptr_t)GetCurrentThreadId());
threads_lock->unlock(threads_lock); threads_lock->unlock(threads_lock);
set_leak_detective(old);
if (this) if (this)
{ {
/* If the thread exited, but has not been joined, it is in terminated docleanup(this);
* state. We must not mangle it, as we target externally spawned remove_thread(this);
* threads only. */ destroy(this);
if (!this->terminated && !this->detached)
{
destroy(this);
}
} }
} }
@@ -696,7 +679,9 @@ void threads_deinit()
{ {
private_thread_t *this; private_thread_t *this;
this = threads->remove(threads, (void*)(uintptr_t)GetCurrentThreadId()); this = get_current_thread();
docleanup(this);
remove_thread(this);
destroy(this); destroy(this);
threads_lock->destroy(threads_lock); threads_lock->destroy(threads_lock);