Commit 188b7ff70b for strongswan.org

commit 188b7ff70bfbd4a94aa70b8abd44b4c6c67be58f
Author: Tobias Brunner <tobias@strongswan.org>
Date:   Mon Sep 7 19:09:45 2026 +0200

    windows: Fix race when canceling threads in timed_wait()

    If a thread is canceled that's in `timed_wait()` and has already called
    `thread_set_active_condvar()` but has not yet started waiting on the
    condvar, the wake could get lost (it is not queued on the condvar).
    To avoid that, we signal the condvar until the woken thread resets it
    again, either when exiting `timed_wait()` or when the APC is delivered
    via the `SleepEx()` cancellation point in `thread_set_active_condvar()`.
    Because that requires the target to reacquire the mutex/rwlock, the
    thread that calls `cancel()` can't hold that same lock to avoid a
    deadlock.  Note that `processor_t` technically does this, but since the
    threads are not cancelable while waiting for jobs, it doesn't trigger
    this issue.

    An alternative option would have been to avoid waiting indefinitely in
    `timed_wait()` (i.e. implement a kind of polling there).  But since
    `cancel()` is called way less often, this seemed the better option.

    Fixes: 0fa9c958114f ("windows: Provide a complete native Windows threading backend")

diff --git a/src/libstrongswan/threading/thread.h b/src/libstrongswan/threading/thread.h
index 83ba42f41c..af3a25a235 100644
--- a/src/libstrongswan/threading/thread.h
+++ b/src/libstrongswan/threading/thread.h
@@ -51,6 +51,10 @@ struct thread_t {

 	/**
 	 * Cancel this thread.
+	 *
+	 * @warning On Windows, this blocks until the thread has left a condvar
+	 * wait, so it must not be called while holding the same mutex/rwlock the
+	 * thread might be waiting on in cancelable state.
 	 */
 	void (*cancel)(thread_t *this);

diff --git a/src/libstrongswan/threading/windows/thread.c b/src/libstrongswan/threading/windows/thread.c
index a2c8dfe194..2713c4299f 100644
--- a/src/libstrongswan/threading/windows/thread.c
+++ b/src/libstrongswan/threading/windows/thread.c
@@ -338,6 +338,24 @@ void thread_set_active_condvar(CONDITION_VARIABLE *condvar)
 	SleepEx(0, TRUE);
 }

+/**
+ * Wake the thread waiting on the condvar and return whether there
+ * currently is an active condvar.
+ */
+static bool wake_active_condvar(private_thread_t *this)
+{
+	CONDITION_VARIABLE *cv;
+
+	condvar_lock->lock(condvar_lock);
+	cv = this->condvar;
+	if (cv)
+	{
+		WakeAllConditionVariable(cv);
+	}
+	condvar_lock->unlock(condvar_lock);
+	return cv != NULL;
+}
+
 /**
  * APC to cancel a thread
  */
@@ -345,6 +363,12 @@ static void WINAPI docancel(ULONG_PTR dwParam)
 {
 	private_thread_t *this = (private_thread_t*)dwParam;

+	/* if canceled in thread_set_active_condvar() before waiting on the condvar,
+	 * we clear this so cancel() doesn't wait for it */
+	condvar_lock->lock(condvar_lock);
+	this->condvar = NULL;
+	condvar_lock->unlock(condvar_lock);
+
 	end_thread(this);
 	ExitThread(0);
 }
@@ -356,16 +380,27 @@ METHOD(thread_t, cancel, void,
 	if (atomic_get_bool(&this->cancelability) &&
 		cas_bool(&this->cancel_pending, FALSE, TRUE))
 	{
-		CONDITION_VARIABLE *cv;
-
-		condvar_lock->lock(condvar_lock);
-		cv = this->condvar;
 		QueueUserAPC(docancel, this->handle, (uintptr_t)this);
-		if (cv)
+
+		/* SleepConditionVariableCS() is not alertable, so the APC can only be
+		 * delivered after the thread returns from the wait. waking the condvar
+		 * normally achieves that, but the wake may get lost if it races with
+		 * the thread entering the wait (after setting the condvar).  so we keep
+		 * waking it until the thread cleared the pointer when leaving the wait
+		 * (docancel() does the same if the thread is canceled before actually
+		 * waiting).  however, because the target thread can only clear the
+		 * condvar by leaving the wait and reacquiring the mutex/rwlock,
+		 * cancel() can't be called while holding the same lock as that would
+		 * prevent that.  on the other hand, there wouldn't be a lost wake as
+		 * the target is definitely waiting on the condvar if we hold the lock.
+		 * unfortunately, we can't determine whether the calling thread holds
+		 * the same lock, so doing this must be avoided.  note that it's safe to
+		 * access `this` as calling cancel() on a detached thread doesn't make
+		 * sense as the thread object could get destroyed at any moment */
+		while (wake_active_condvar(this))
 		{
-			WakeAllConditionVariable(cv);
+			Sleep(1);
 		}
-		condvar_lock->unlock(condvar_lock);
 	}
 }