Skip to content

Fix log mutex initialization TOCTOU race condition#194

Closed
map588 wants to merge 2 commits intoDeusData:mainfrom
map588:fix/log-mutex-init-toctou
Closed

Fix log mutex initialization TOCTOU race condition#194
map588 wants to merge 2 commits intoDeusData:mainfrom
map588:fix/log-mutex-init-toctou

Conversation

@map588
Copy link
Copy Markdown
Contributor

@map588 map588 commented Apr 2, 2026

Two threads calling cbm_ui_log_append() simultaneously before the mutex was initialized could both pass the atomic_load check and double-initialize the mutex.

Replace with atomic CAS (0→1→2 state machine) so exactly one thread initializes.

Two threads calling cbm_ui_log_append() simultaneously before the
mutex was initialized could both pass the atomic_load check and
double-initialize the mutex. Replace with atomic CAS (0→1→2 state
machine) so exactly one thread initializes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 2, 2026 06:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a TOCTOU race during lazy initialization of the log ring-buffer mutex when multiple threads call cbm_ui_log_append() concurrently at startup.

Changes:

  • Introduces log_mutex_ensure_init() using an atomic CAS-based 0→1→2 initialization state machine.
  • Updates cbm_ui_log_append() and handle_logs() to call the new ensure-init helper before taking the log mutex.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +138 to +148
/* CAS from 0 → 1: exactly one thread initializes the mutex */
int expected = 0;
if (atomic_compare_exchange_strong(&g_log_mutex_init, &expected, 1)) {
cbm_mutex_init(&g_log_mutex);
atomic_store(&g_log_mutex_init, 2); /* 2 = fully initialized */
} else {
/* Another thread is initializing — spin until done */
while (atomic_load(&g_log_mutex_init) != 2) {
/* tight spin is fine — init takes nanoseconds */
}
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After initialization (state==2), this still executes an atomic compare-exchange on every log append / log fetch. Consider adding a fast path (e.g., load state first and return immediately when it's already fully initialized) so the steady-state cost is just a single atomic load.

Suggested change
/* CAS from 0 → 1: exactly one thread initializes the mutex */
int expected = 0;
if (atomic_compare_exchange_strong(&g_log_mutex_init, &expected, 1)) {
cbm_mutex_init(&g_log_mutex);
atomic_store(&g_log_mutex_init, 2); /* 2 = fully initialized */
} else {
/* Another thread is initializing — spin until done */
while (atomic_load(&g_log_mutex_init) != 2) {
/* tight spin is fine — init takes nanoseconds */
}
}
/* Fast path: already fully initialized? Just return. */
int state = atomic_load(&g_log_mutex_init);
if (state == 2) {
return;
}
/* CAS from 0 → 1: exactly one thread initializes the mutex */
if (state == 0) {
int expected = 0;
if (atomic_compare_exchange_strong(&g_log_mutex_init, &expected, 1)) {
cbm_mutex_init(&g_log_mutex);
atomic_store(&g_log_mutex_init, 2); /* 2 = fully initialized */
return;
}
}
/* Another thread is initializing — spin until done */
while (atomic_load(&g_log_mutex_init) != 2) {
/* tight spin is fine — init takes nanoseconds */
}

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
/* Another thread is initializing — spin until done */
while (atomic_load(&g_log_mutex_init) != 2) {
/* tight spin is fine — init takes nanoseconds */
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wait loop is a tight spin with no backoff/yield; if mutex init ever takes longer than expected (e.g., OS contention, debug builds), this can burn CPU. Consider adding a small pause/backoff (e.g., cbm_usleep(0/1) or cbm_nanosleep) inside the loop, and updating the comment that assumes init always takes “nanoseconds”.

Suggested change
/* Another thread is initializing — spin until done */
while (atomic_load(&g_log_mutex_init) != 2) {
/* tight spin is fine — init takes nanoseconds */
/* Another thread is initializing — wait until done (avoid tight spinning) */
while (atomic_load(&g_log_mutex_init) != 2) {
/* Small sleep/backoff to avoid burning CPU if init is slower than expected */
cbm_usleep(1);

Copilot uses AI. Check for mistakes.
Add early return when state==2 (avoids CAS on every log call).
Replace tight spin with cbm_usleep(1) yield while waiting for
another thread to finish initialization.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@DeusData DeusData added bug Something isn't working stability/performance Server crashes, OOM, hangs, high CPU/memory labels Apr 3, 2026
@map588 map588 closed this Apr 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working stability/performance Server crashes, OOM, hangs, high CPU/memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants