Fix data race in concurrent lazy initialization (#396)

The first allocation on any thread lazily calls rpmalloc_initialize(0), so when
several threads make their first allocation simultaneously they all enter
rpmalloc_initialize concurrently, racing on the plain-int initialized flag and on
global_config - a thread could be handed a block backed by half-written global
state. Drive initialization as an atomic state machine (UNINIT -> RUNNING -> DONE):
exactly one thread performs the global setup while the others wait for it to finish
before allocating, with release/acquire ordering publishing the global_config writes
to threads that observe completion.
diff --git a/.gitignore b/.gitignore
index 5f51b75..6897419 100644
--- a/.gitignore
+++ b/.gitignore
@@ -238,3 +238,6 @@
 
 # Stress test helper output (test/stress.sh)
 /rpmalloc-stress-*
+
+# Init race test helper output (test/init-race.sh)
+/rpmalloc-init-race-*
diff --git a/CHANGELOG b/CHANGELOG
index 50b71e6..6886504 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,10 @@
+2.0.1
+
+Fixed a data race in concurrent lazy initialization where simultaneous first allocations from
+multiple threads could race on the global init state and return a block backed by a partially
+written configuration. Initialization is now serialized through an atomic state machine.
+
+
 2.0.0
 
 Major redesign of the allocator core. The most significant changes in design from the 1.4 series:
diff --git a/rpmalloc/rpmalloc.c b/rpmalloc/rpmalloc.c
index e1e525c..f0cc485 100644
--- a/rpmalloc/rpmalloc.c
+++ b/rpmalloc/rpmalloc.c
@@ -641,8 +641,16 @@
 static atomic_uintptr_t global_heap_lock;
 //! Heap ID counter
 static atomic_uint global_heap_id = 1;
-//! Initialized flag
-static int global_rpmalloc_initialized;
+//! Global initialization state. rpmalloc_initialize can be called concurrently from multiple
+//! threads (the first allocation on any thread may trigger it), so it is driven as an atomic
+//! state machine: exactly one thread performs the global setup while the others wait for it to
+//! complete before allocating - they must never observe, or allocate against, a partially
+//! written global_config. Release/acquire ordering on the DONE transition publishes all of the
+//! global_config writes to threads that observe completion.
+#define RPMALLOC_INIT_UNINIT 0
+#define RPMALLOC_INIT_RUNNING 1
+#define RPMALLOC_INIT_DONE 2
+static atomic_int global_rpmalloc_init_state;
 //! Memory interface
 static rpmalloc_interface_t* global_memory_interface;
 //! Default memory interface
@@ -2666,7 +2674,7 @@
 
 extern int
 rpmalloc_initialize_config(rpmalloc_interface_t* memory_interface, rpmalloc_config_t* config) {
-	if (global_rpmalloc_initialized) {
+	if (atomic_load_explicit(&global_rpmalloc_init_state, memory_order_acquire) == RPMALLOC_INIT_DONE) {
 		rpmalloc_thread_initialize();
 		if (config)
 			*config = global_config;
@@ -2686,13 +2694,25 @@
 
 extern int
 rpmalloc_initialize(rpmalloc_interface_t* memory_interface) {
-	if (global_rpmalloc_initialized) {
-		rpmalloc_thread_initialize();
-		return 0;
+	for (;;) {
+		int state = atomic_load_explicit(&global_rpmalloc_init_state, memory_order_acquire);
+		if (state == RPMALLOC_INIT_DONE) {
+			rpmalloc_thread_initialize();
+			return 0;
+		}
+		if (state == RPMALLOC_INIT_RUNNING) {
+			while (atomic_load_explicit(&global_rpmalloc_init_state, memory_order_acquire) ==
+			       RPMALLOC_INIT_RUNNING)
+				wait_spin();
+			continue;
+		}
+		int expected = RPMALLOC_INIT_UNINIT;
+		if (atomic_compare_exchange_strong_explicit(&global_rpmalloc_init_state, &expected,
+		                                            RPMALLOC_INIT_RUNNING, memory_order_acq_rel,
+		                                            memory_order_acquire))
+			break;
 	}
 
-	global_rpmalloc_initialized = 1;
-
 	// Remember whether the caller explicitly requested huge pages, the detection below
 	// overwrites global_config.enable_huge_pages with what is actually available.
 	const int huge_pages_requested = global_config.enable_huge_pages;
@@ -2850,7 +2870,7 @@
 		// huge page size. Leave the allocator uninitialized (and the requested flag cleared)
 		// so the caller can detect this and re-initialize without huge pages if desired.
 		global_config.enable_huge_pages = 0;
-		global_rpmalloc_initialized = 0;
+		atomic_store_explicit(&global_rpmalloc_init_state, RPMALLOC_INIT_UNINIT, memory_order_release);
 		return -1;
 	}
 
@@ -2908,6 +2928,8 @@
 
 	global_main_thread_id = get_thread_id();
 
+	atomic_store_explicit(&global_rpmalloc_init_state, RPMALLOC_INIT_DONE, memory_order_release);
+
 	rpmalloc_thread_initialize();
 
 	return 0;
@@ -3055,7 +3077,7 @@
 
 	global_heap_creating = 0;
 	global_main_thread_id = 0;
-	global_rpmalloc_initialized = 0;
+	atomic_store_explicit(&global_rpmalloc_init_state, RPMALLOC_INIT_UNINIT, memory_order_release);
 }
 
 extern void
diff --git a/rpmalloc/rpmalloc.h b/rpmalloc/rpmalloc.h
index 7d4c86e..565c511 100644
--- a/rpmalloc/rpmalloc.h
+++ b/rpmalloc/rpmalloc.h
@@ -19,10 +19,10 @@
 //! rpmalloc version. RPMALLOC_VERSION is a human-readable string and may carry a pre-release
 //  suffix such as "-rc1". RPMALLOC_VERSION_NUMBER is a monotonic integer for comparisons,
 //  computed as major*10000 + minor*100 + patch (pre-release suffixes are not encoded).
-#define RPMALLOC_VERSION "2.0.0"
+#define RPMALLOC_VERSION "2.0.1"
 #define RPMALLOC_VERSION_MAJOR 2
 #define RPMALLOC_VERSION_MINOR 0
-#define RPMALLOC_VERSION_PATCH 0
+#define RPMALLOC_VERSION_PATCH 1
 #define RPMALLOC_VERSION_NUMBER \
 	(RPMALLOC_VERSION_MAJOR * 10000 + RPMALLOC_VERSION_MINOR * 100 + RPMALLOC_VERSION_PATCH)