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

Reply via email to