Attached is a mutex-only v2 rebased on master at 8e82d6610e7. It
leaves pthread_once and the existing type layouts unchanged.

Only a thread that claims state 0 initializes the critical section and
publishes state 1. The lock fast path uses an acquire load instead of
an interlocked read-modify-write; unlock uses a relaxed load after a
successful lock.

The deterministic reproducer returns EINVAL on the original code and
passes with v2. Stress testing passed 800,000 lock/unlock pairs per
configuration on native ARM64 MSVC, emulated x64 MSVC, and emulated
x64 MinGW.

Full ARM64 and x64 builds passed. On each architecture, ECPG passed 66
subtests, libpq passed 241, and pg_basebackup passed 151. The ARM64
ECPG rerun passed after adding the missing diff tool to PATH.

The patch applies cleanly to REL_17_STABLE and REL_18_STABLE; neither
stable branch was built.

Alexander, could you test this revision against your reproducer?
Heikki, do the acquire loads and separate pthread_once work address
your concerns?

A separate GitHub-hosted Windows x64 build and all nine test groups
passed with all three patches applied. ECPG passed 66 subtests, libpq
passed 241, and pg_basebackup passed 152.

Best,
Harrison
From 7d9583f19d019ad65e68e6da4b09d4f1a59a3e45 Mon Sep 17 00:00:00 2001
From: Harrison Booth <[email protected]>
Date: Mon, 12 Oct 2026 15:54:29 -0500
Subject: [PATCH v2] Fix races in Windows pthread mutex initialization

Claim initialization only when initstate is zero. Publish state one only
from the initializer, so a waiter cannot overwrite initialized state.

Use an acquire load on the lock fast path and a relaxed load after locking,
avoiding read-modify-write operations on already initialized mutexes.

Keep pthread_once unchanged and preserve the existing type layouts.
---
 src/interfaces/ecpg/ecpglib/misc.c   | 23 +++++++++++++++++------
 src/interfaces/libpq/pthread-win32.c | 23 +++++++++++++++++------
 2 files changed, 34 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/ecpg/ecpglib/misc.c b/src/interfaces/ecpg/ecpglib/misc.c
index 354a2e731fc..09d44164f47 100644
--- a/src/interfaces/ecpg/ecpglib/misc.c
+++ b/src/interfaces/ecpg/ecpglib/misc.c
@@ -436,16 +436,22 @@ pthread_mutex_init(pthread_mutex_t *mp, void *attr)
 int
 pthread_mutex_lock(pthread_mutex_t *mp)
 {
-	/* Initialize the csection if not already done */
-	if (mp->initstate != 1)
+	/* Acquire the initialization state without a read-modify-write. */
+#ifdef _MSC_VER
+	if (ReadAcquire(&mp->initstate) != 1)
+#else
+	if (__atomic_load_n(&mp->initstate, __ATOMIC_ACQUIRE) != 1)
+#endif
 	{
 		LONG		istate;
 
-		while ((istate = InterlockedExchange(&mp->initstate, 2)) == 2)
+		while ((istate = InterlockedCompareExchange(&mp->initstate, 2, 0)) == 2)
 			Sleep(0);			/* wait, another thread is doing this */
-		if (istate != 1)
+		if (istate == 0)
+		{
 			InitializeCriticalSection(&mp->csection);
-		InterlockedExchange(&mp->initstate, 1);
+			InterlockedExchange(&mp->initstate, 1);
+		}
 	}
 	EnterCriticalSection(&mp->csection);
 	return 0;
@@ -454,7 +460,12 @@ pthread_mutex_lock(pthread_mutex_t *mp)
 int
 pthread_mutex_unlock(pthread_mutex_t *mp)
 {
-	if (mp->initstate != 1)
+	/* The successful lock already acquired the initialized critical section. */
+#ifdef _MSC_VER
+	if (ReadNoFence(&mp->initstate) != 1)
+#else
+	if (__atomic_load_n(&mp->initstate, __ATOMIC_RELAXED) != 1)
+#endif
 		return EINVAL;
 	LeaveCriticalSection(&mp->csection);
 	return 0;
diff --git a/src/interfaces/libpq/pthread-win32.c b/src/interfaces/libpq/pthread-win32.c
index cf66284f007..da5d6c9e420 100644
--- a/src/interfaces/libpq/pthread-win32.c
+++ b/src/interfaces/libpq/pthread-win32.c
@@ -41,16 +41,22 @@ pthread_mutex_init(pthread_mutex_t *mp, void *attr)
 int
 pthread_mutex_lock(pthread_mutex_t *mp)
 {
-	/* Initialize the csection if not already done */
-	if (mp->initstate != 1)
+	/* Acquire the initialization state without a read-modify-write. */
+#ifdef _MSC_VER
+	if (ReadAcquire(&mp->initstate) != 1)
+#else
+	if (__atomic_load_n(&mp->initstate, __ATOMIC_ACQUIRE) != 1)
+#endif
 	{
 		LONG		istate;
 
-		while ((istate = InterlockedExchange(&mp->initstate, 2)) == 2)
+		while ((istate = InterlockedCompareExchange(&mp->initstate, 2, 0)) == 2)
 			Sleep(0);			/* wait, another thread is doing this */
-		if (istate != 1)
+		if (istate == 0)
+		{
 			InitializeCriticalSection(&mp->csection);
-		InterlockedExchange(&mp->initstate, 1);
+			InterlockedExchange(&mp->initstate, 1);
+		}
 	}
 	EnterCriticalSection(&mp->csection);
 	return 0;
@@ -59,7 +65,12 @@ pthread_mutex_lock(pthread_mutex_t *mp)
 int
 pthread_mutex_unlock(pthread_mutex_t *mp)
 {
-	if (mp->initstate != 1)
+	/* The successful lock already acquired the initialized critical section. */
+#ifdef _MSC_VER
+	if (ReadNoFence(&mp->initstate) != 1)
+#else
+	if (__atomic_load_n(&mp->initstate, __ATOMIC_RELAXED) != 1)
+#endif
 		return EINVAL;
 	LeaveCriticalSection(&mp->csection);
 	return 0;

Reply via email to