Fix data race in mju_getLogConfigPtr lazy init

The lazy init of log_config used a single atomic flag (env_checked) with
a load-then-store pattern, allowing two threads to both enter the init
path and concurrently write to the non-atomic log_config.topics field.

Replace with a two-phase atomic init: an atomic exchange on
env_init_claimed ensures exactly one thread enters the init, while
env_init_done (with acquire/release semantics) signals completion and
provides the happens-before edge that makes log_config writes visible to
other threads.

Also adds mj_atomic_exchange_bool to engine_crossplatform.h (both MSVC
and GCC/Clang variants).

PiperOrigin-RevId: 941202181
Change-Id: I59b6801c7b4b8b0e1647d82d2aeb534a90fb66fb
This commit is contained in:
Georg Ostrovski
2026-07-01 11:23:55 -07:00
committed by Copybara-Service
parent 6e8a79c657
commit bb80b55ae1
2 changed files with 31 additions and 6 deletions
+14
View File
@@ -85,6 +85,20 @@
__atomic_fetch_add(ptr, val, __ATOMIC_RELAXED)
#endif
// Atomics helpers for mjtBool (1-byte _Bool) with acquire/release semantics.
#if defined(_MSC_VER) && !defined(__clang__)
#define mj_atomic_load_bool(ptr) \
(mjtBool) _InterlockedCompareExchange8((volatile char*)(ptr), 0, 0)
#define mj_atomic_store_bool(ptr, val) \
(void)_InterlockedExchange8((volatile char*)(ptr), (char)(val))
#define mj_atomic_exchange_bool(ptr, val) \
(mjtBool) _InterlockedExchange8((volatile char*)(ptr), (char)(val))
#else
#define mj_atomic_load_bool(ptr) __atomic_load_n(ptr, __ATOMIC_ACQUIRE)
#define mj_atomic_store_bool(ptr, val) __atomic_store_n(ptr, val, __ATOMIC_RELEASE)
#define mj_atomic_exchange_bool(ptr, val) __atomic_exchange_n(ptr, val, __ATOMIC_ACQ_REL)
#endif
#ifdef __cplusplus
extern "C" {
#endif
+17 -6
View File
@@ -104,7 +104,8 @@ static mjLogConfig log_config = {.logto_console = true,
.logto_file = true,
.logfile = "MUJOCO_LOG.TXT",
.topics = 0};
static mjtBool env_checked = 0;
static mjtBool env_init_claimed = 0; // claimed by first thread to enter init
static mjtBool env_init_done = 0; // set after log_config init completes
// parse MUJOCO_LOG_TOPICS env var to seed initial topic bitmask
// example: MUJOCO_LOG_TOPICS="time_stp,sleep"
@@ -143,9 +144,17 @@ static void mju_initLogTopicsFromEnv(void) {
// private pointer getter encapsulates lazy init with zero copy overhead
static const mjLogConfig* mju_getLogConfigPtr(void) {
if (!env_checked) {
mju_initLogTopicsFromEnv();
env_checked = 1;
if (!mj_atomic_load_bool(&env_init_done)) {
// atomically claim right to initialize; only one thread gets old value 0
if (!mj_atomic_exchange_bool(&env_init_claimed, 1)) {
mju_initLogTopicsFromEnv();
mj_atomic_store_bool(&env_init_done, 1);
} else {
// another thread is initializing; spin until it completes
while (!mj_atomic_load_bool(&env_init_done)) {
// empty
}
}
}
return &log_config;
}
@@ -171,8 +180,9 @@ mjLogConfig mju_getLogConfig(void) {
// set default handler configuration
void mju_setLogConfig(mjLogConfig config) {
env_checked = 1;
log_config = config;
mj_atomic_store_bool(&env_init_claimed, 1);
mj_atomic_store_bool(&env_init_done, 1);
}
// restore default processing
@@ -182,8 +192,9 @@ void mju_clearHandlers(void) {
.logto_file = true,
.logfile = "MUJOCO_LOG.TXT",
.topics = 0};
env_checked = 1;
mj_atomic_store_bool(&env_init_claimed, 1);
mju_initLogTopicsFromEnv();
mj_atomic_store_bool(&env_init_done, 1);
mju_user_error = 0;
mju_user_warning = 0;