Hi hackers, I've found a bug that causes PostgreSQL to crash during startup 
when built with ThreadSanitizer (-fsanitize=thread).

My environment
Ubuntu 24.04.1 LTS (kernel 6.14.0-29-generic)
clang 18
PostgreSQL 17.2
Build Configuration: ./configure --enable-debug --enable-cassert 
CFLAGS="-fsanitize=thread -g"

PostgreSQL compiled with ThreadSanitizer (-fsanitize=thread) crashes with 
SIGSEGV during program initialization, before reaching main().

Steps to Reproduce

1. Configure PostgreSQL with ThreadSanitizer
2.  ./configure --enable-debug CFLAGS="-fsanitize=thread -g"
3. make
4. Run any PostgreSQL command:  ./postgres --version

Expected Behavior: Program should start normally and display version 
information.
Actual Behavior: Segmentation fault during early initialization

Root Cause: The __ubsan_default_options() function in main.c is compiled with 
TSan instrumentation, creating a circular dependency during sanitizer runtime 
initialization.
1. TSan initialization calls __ubsan_default_options()
2. TSan tries to instrument the function
3. Instrumentation requires initialized ThreadState
4. ThreadState isn't ready because TSan init isn't complete
5. Segfault/crash occurs

Proposed Fix: Move __ubsan_default_options() to a separate compilation unit 
built without sanitizer instrumentation.
The below attached patch moves the function to a separate compilation unit with 
a custom Makefile rule that uses -fno-sanitize=thread,address,undefined. The 
reached_main check is preserved to avoid calling getenv() before libc is fully 
initialized and to handle cases where set_ps_display() breaks 
/proc/$pid/environ.

Please let me know if you have any questions or would like further details.
Thanks & Regards,
Emmanuel Sibi
diff --git a/src/backend/main/Makefile b/src/backend/main/Makefile
index 6d34072624b..e49b10fe0c4 100644
--- a/src/backend/main/Makefile
+++ b/src/backend/main/Makefile
@@ -13,6 +13,11 @@ top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
 OBJS = \
-	main.o
+	main.o \
+	sanitizer_hook.o
+
+# Custom rule to build sanitizer_hook.o without sanitizer instrumentation
+sanitizer_hook.o: sanitizer_hook.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) -fno-sanitize=thread,address,undefined -c -o $@ $<
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/main/main.c b/src/backend/main/main.c
index 4672aab8378..be61d8db0a6 100644
--- a/src/backend/main/main.c
+++ b/src/backend/main/main.c
@@ -42,7 +42,7 @@
 
 
 const char *progname;
-static bool reached_main = false;
+bool reached_main = false;
 
 
 static void startup_hacks(const char *progname);
@@ -415,29 +415,3 @@ check_root(const char *progname)
 #endif							/* WIN32 */
 }
 
-/*
- * At least on linux, set_ps_display() breaks /proc/$pid/environ. The
- * sanitizer library uses /proc/$pid/environ to implement getenv() as it wants
- * to work independent of libc. When just using undefined and alignment
- * sanitizers, the sanitizer library is only initialized when the first error
- * occurs, by which time we've often already called set_ps_display(),
- * preventing the sanitizer libraries from seeing the options.
- *
- * We can work around that by defining __ubsan_default_options, a weak symbol
- * libsanitizer uses to get defaults from the application, and return
- * getenv("UBSAN_OPTIONS"). But only if main already was reached, so that we
- * don't end up relying on a not-yet-working getenv().
- *
- * As this function won't get called when not running a sanitizer, it doesn't
- * seem necessary to only compile it conditionally.
- */
-const char *__ubsan_default_options(void);
-const char *
-__ubsan_default_options(void)
-{
-	/* don't call libc before it's guaranteed to be initialized */
-	if (!reached_main)
-		return "";
-
-	return getenv("UBSAN_OPTIONS");
-}
diff --git a/src/backend/main/sanitizer_hook.c b/src/backend/main/sanitizer_hook.c
new file mode 100644
index 00000000000..61302de0ead
--- /dev/null
+++ b/src/backend/main/sanitizer_hook.c
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * sanitizer_hook.c
+ *	  UBSan options hook to avoid initialization timing issues
+ *
+ * This file provides __ubsan_default_options() without sanitizer
+ * instrumentation to prevent circular dependencies during early
+ * program startup when set_ps_display() breaks /proc/$pid/environ.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/main/sanitizer_hook.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <signal.h>
+#include <stdlib.h>
+
+
+extern bool reached_main;
+
+
+/*
+ * At least on linux, set_ps_display() breaks /proc/$pid/environ. The
+ * sanitizer library uses /proc/$pid/environ to implement getenv() as it wants
+ * to work independent of libc. When just using undefined and alignment
+ * sanitizers, the sanitizer library is only initialized when the first error
+ * occurs, by which time we've often already called set_ps_display(),
+ * preventing the sanitizer libraries from seeing the options.
+ *
+ * We can work around that by defining __ubsan_default_options, a weak symbol
+ * libsanitizer uses to get defaults from the application, and return
+ * getenv("UBSAN_OPTIONS"). But only if main already was reached, so that we
+ * don't end up relying on a not-yet-working getenv().
+ *
+ * As this function won't get called when not running a sanitizer, it doesn't
+ * seem necessary to only compile it conditionally.
+ */
+const char *
+__ubsan_default_options(void)
+{
+	/*
+	 * Avoid calling libc until program startup is complete (reached_main).
+	 * This prevents sanitizer initialization issues when set_ps_display()
+	 * breaks /proc/$pid/environ before sanitizer can read UBSAN_OPTIONS.
+	 */
+	if (!reached_main)
+		return "";
+
+	return getenv("UBSAN_OPTIONS");
+}

Reply via email to