Attached is a separate Windows ECPG pthread_once ordering fix that keeps pthread_once_t as bool and preserves the exported helper signature.
The public macro currently reads the bool without acquire ordering and can bypass the helper entirely. A source-extracted two-thread stress test on native ARM64 observes the initialized flag before callback data is visible. It preinitializes the mutex to isolate this from the separate mutex initialization issue. Saved baseline runs fail under both /volatile:iso and /volatile:ms, since the latter doesn't make the macro's ordinary bool read volatile. The patch uses acquire reads in the macro and both helper checks, then release publication after the callback. MSVC ARM64 uses the SDK byte acquire/release operations. MSVC x86/x64 adds compiler barriers for /volatile:iso; GNU uses atomic builtins. The compiler barrier is deprecated, so feedback on that choice would be useful. The exact candidate passes 4 million initializations and 8 million checks per configuration on ARM64 /volatile:iso, x64 /volatile:iso and /volatile:ms, and MinGW GCC 13.2 x64. C/C++ header checks pass. Full ECPG suites pass all 66 subtests on native ARM64 and emulated x64, with the separate mutex and Meson patches in the integration checkout. This is standalone red/green evidence, not a reproduced complete ECPG query failure. The patch is based on master 8e82d6610e7 and applies cleanly to the checked REL_17/18/19_STABLE heads; those branches weren't built. pgindent checks pass. Let me know your thoughts. Best, Harrison
From 9c6e968e37e1c67eaaddd7b033fbd068ee1d021a Mon Sep 17 00:00:00 2001 From: Harrison Booth <[email protected]> Date: Mon, 12 Oct 2026 20:01:15 -0500 Subject: [PATCH v1] Fix Windows ECPG pthread_once publication ordering Use acquire reads in both the public macro fast path and the once helper, and publish the completed initialization with a release store. A volatile read in the helper cannot protect callers that bypass it through the ordinary Boolean read in the macro. Keep pthread_once_t as bool and retain the existing function signature. Use byte acquire/release operations with MSVC and GNU atomic operations, and constrain compiler reordering on MSVC x86/x64 even with volatile:iso. A source-extracted ARM64 stress probe observes initialized flags before callback data becomes visible on the original code. The revised protocol passes native ARM64, emulated x64, and MinGW controls. Full native ARM64 and emulated x64 ECPG regression suites pass with the validation setup. --- src/interfaces/ecpg/ecpglib/misc.c | 13 ++++++++--- .../ecpg/include/ecpg-pthread-win32.h | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/interfaces/ecpg/ecpglib/misc.c b/src/interfaces/ecpg/ecpglib/misc.c index 354a2e731fc..34d42b37ae1 100644 --- a/src/interfaces/ecpg/ecpglib/misc.c +++ b/src/interfaces/ecpg/ecpglib/misc.c @@ -465,13 +465,20 @@ static pthread_mutex_t win32_pthread_once_lock = PTHREAD_MUTEX_INITIALIZER; void win32_pthread_once(volatile pthread_once_t *once, void (*fn) (void)) { - if (!*once) + if (!win32_pthread_once_done(once)) { pthread_mutex_lock(&win32_pthread_once_lock); - if (!*once) + if (!win32_pthread_once_done(once)) { fn(); - *once = true; +#ifdef _MSC_VER +#if defined(_M_IX86) || defined(_M_X64) + _ReadWriteBarrier(); +#endif + WriteRelease8((volatile CHAR *) once, 1); +#else + __atomic_store_n(once, true, __ATOMIC_RELEASE); +#endif } pthread_mutex_unlock(&win32_pthread_once_lock); } diff --git a/src/interfaces/ecpg/include/ecpg-pthread-win32.h b/src/interfaces/ecpg/include/ecpg-pthread-win32.h index 7b6ba46b349..d933f07420d 100644 --- a/src/interfaces/ecpg/include/ecpg-pthread-win32.h +++ b/src/interfaces/ecpg/include/ecpg-pthread-win32.h @@ -10,6 +10,10 @@ #include <pthread.h> #else +#ifdef _MSC_VER +#include <intrin.h> +#endif + typedef struct pthread_mutex_t { /* initstate = 0: not initialized; 1: init done; 2: init in progress */ @@ -29,6 +33,23 @@ int pthread_mutex_unlock(pthread_mutex_t *); void win32_pthread_once(volatile pthread_once_t *once, void (*fn) (void)); +/* Acquire the initialization function's effects before using its result. */ +static inline bool +win32_pthread_once_done(volatile pthread_once_t *once) +{ +#ifdef _MSC_VER + bool done = ReadAcquire8((const volatile CHAR *) once) != 0; + +#if defined(_M_IX86) || defined(_M_X64) + /* Also constrain compiler reordering with /volatile:iso on x86/x64. */ + _ReadWriteBarrier(); +#endif + return done; +#else + return __atomic_load_n(once, __ATOMIC_ACQUIRE); +#endif +} + #define pthread_getspecific(key) \ TlsGetValue((key)) @@ -41,7 +62,7 @@ void win32_pthread_once(volatile pthread_once_t *once, void (*fn) (void)); #define pthread_once(once, fn) \ do { \ - if (!*(once)) \ + if (!win32_pthread_once_done((once))) \ win32_pthread_once((once), (fn)); \ } while(0) #endif /* WIN32 */ -- 2.53.0.windows.2
