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;
