ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
mutex.c File Reference

Per-thread mutex lock stack for deadlock detection. More...

Go to the source code of this file.

Data Structures

struct  thread_lock_stack_t
 
struct  thread_registry_entry_t
 
struct  deadlock_state_t
 

Macros

#define MUTEX_STACK_MAX_DEPTH   64
 
#define MAX_THREADS   256
 
#define MAX_CYCLE_MUTEXES   16
 
#define MAX_CYCLE_LEN   64
 DFS-based cycle detection in the waits-for graph Returns cycle start index if found, -1 otherwise Fills cycle_path with indices of threads in the cycle (if found)
 
#define COND_DEADLOCK_THRESHOLD_NS   (5ULL * 1000000000ULL)
 

Functions

void mutex_stack_push_pending (uintptr_t mutex_key, const char *mutex_name)
 Push a mutex onto the current thread's lock stack (PENDING state)
 
void mutex_stack_mark_locked (uintptr_t mutex_key)
 Mark the top of the current thread's lock stack as LOCKED (transitions PENDING -> LOCKED)
 
void mutex_stack_pop (uintptr_t mutex_key)
 Pop the top mutex from the current thread's lock stack (unlock)
 
int mutex_stack_get_current (mutex_stack_entry_t *out_entries, int max_entries)
 Get the current thread's lock stack.
 
int mutex_stack_get_all_threads (mutex_stack_entry_t ***out_stacks, int **out_stack_counts, int *out_thread_count)
 Get all threads' lock stacks for deadlock analysis.
 
void mutex_stack_free_all_threads (mutex_stack_entry_t **stacks, int *stack_counts, int thread_count)
 Free memory allocated by mutex_stack_get_all_threads()
 
void mutex_stack_detect_deadlocks (void)
 Detect circular wait deadlocks using DFS-based cycle detection.
 
bool debug_sync_is_cleanup_in_progress (void)
 
void debug_sync_check_cond_deadlocks (void)
 Check all condition variables for deadlocks.
 
int mutex_stack_init (void)
 Initialize mutex stack system.
 
void mutex_stack_cleanup_current_thread (void)
 Cleanup TLS stack for current thread Explicitly frees the thread-local mutex stack. Used to prevent leaks when TLS destructors might not run reliably (e.g., before TLS key deletion).
 
void mutex_stack_cleanup (void)
 Cleanup mutex stack system.
 

Detailed Description

Per-thread mutex lock stack for deadlock detection.

Date
February 2026

Tracks which mutexes are held and pending per thread. Detects circular wait patterns (classic deadlock condition).

Definition in file debug/mutex.c.

Macro Definition Documentation

◆ COND_DEADLOCK_THRESHOLD_NS

#define COND_DEADLOCK_THRESHOLD_NS   (5ULL * 1000000000ULL)

Definition at line 665 of file debug/mutex.c.

◆ MAX_CYCLE_LEN

#define MAX_CYCLE_LEN   64

DFS-based cycle detection in the waits-for graph Returns cycle start index if found, -1 otherwise Fills cycle_path with indices of threads in the cycle (if found)

Definition at line 447 of file debug/mutex.c.

◆ MAX_CYCLE_MUTEXES

#define MAX_CYCLE_MUTEXES   16

Definition at line 76 of file debug/mutex.c.

◆ MAX_THREADS

#define MAX_THREADS   256

Definition at line 46 of file debug/mutex.c.

◆ MUTEX_STACK_MAX_DEPTH

#define MUTEX_STACK_MAX_DEPTH   64

Definition at line 27 of file debug/mutex.c.

Function Documentation

◆ debug_sync_is_cleanup_in_progress()

bool debug_sync_is_cleanup_in_progress ( void  )
extern

Definition at line 653 of file sync.c.

653 {
654 return atomic_load_bool(&g_cleanup_in_progress);
655}
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169

References atomic_load_bool().

Referenced by debug_sync_check_cond_deadlocks().

◆ mutex_stack_cleanup()

void mutex_stack_cleanup ( void  )

Cleanup mutex stack system.

Definition at line 788 of file debug/mutex.c.

788 {
789 // Signal shutdown to prevent new allocations from threads still running
790 atomic_store_bool(&g_shutting_down, true);
791
792 // Manually free all stacks in the registry
793 // This must happen BEFORE deleting the TLS key to avoid double-free
794 // (destructor won't run on still-running threads until they exit, by which time
795 // these stacks will already be freed)
796 int count = atomic_load_int(&g_thread_registry_count);
797 for (int i = 0; i < count; i++) {
798 if (g_thread_registry[i].stack) {
799 // Use raw free() - stacks are allocated with raw malloc(), not SAFE_CALLOC()
800 free(g_thread_registry[i].stack);
801 g_thread_registry[i].stack = NULL;
802 }
803 }
804
805 // Now delete the TLS key - destructors will see NULL in registry and skip freeing
806 // For any threads that haven't been registered yet, destructor will free directly
807 if (atomic_load_bool(&g_tls_initialized)) {
808 ascii_tls_key_delete(g_tls_mutex_stack);
809 atomic_store_bool(&g_tls_initialized, false);
810 }
811
812 // Clear registry on cleanup using atomic operations
813 atomic_store_int(&g_thread_registry_count, 0);
814}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
void atomic_store_int(atomic_t *a, int value)
Atomically store an int value.
Definition atomic.c:202
int atomic_load_int(atomic_t *a)
Atomically load an int value.
Definition atomic.c:194
int ascii_tls_key_delete(tls_key_t key)
Delete a thread-local storage key.
Definition threading.c:101
thread_lock_stack_t * stack
Definition debug/mutex.c:49

References ascii_tls_key_delete(), atomic_load_bool(), atomic_load_int(), atomic_store_bool(), atomic_store_int(), and thread_registry_entry_t::stack.

Referenced by asciichat_shared_destroy().

◆ mutex_stack_cleanup_current_thread()

void mutex_stack_cleanup_current_thread ( void  )

Cleanup TLS stack for current thread Explicitly frees the thread-local mutex stack. Used to prevent leaks when TLS destructors might not run reliably (e.g., before TLS key deletion).

Definition at line 755 of file debug/mutex.c.

755 {
756 // Explicitly free the current thread's TLS stack
757 // This is used to prevent leaks when TLS destructors might not run reliably
758 // (e.g., debug threads exiting before mutex_stack_cleanup() deletes the TLS key)
759
760 if (!atomic_load_bool(&g_tls_initialized)) {
761 return; // TLS not initialized, nothing to clean up
762 }
763
764 thread_lock_stack_t *stack = (thread_lock_stack_t *)ascii_tls_get(g_tls_mutex_stack);
765 if (!stack) {
766 return; // No stack allocated for this thread
767 }
768
769 // Clear from TLS
770 ascii_tls_set(g_tls_mutex_stack, NULL);
771
772 // Update registry if this thread is registered
773 thread_id_t current_thread = asciichat_thread_self();
774 registry_lock();
775 int count = atomic_load_int(&g_thread_registry_count);
776 for (int i = 0; i < count; i++) {
777 if (asciichat_thread_equal(g_thread_registry[i].thread_id, current_thread) && g_thread_registry[i].stack == stack) {
778 g_thread_registry[i].stack = NULL; // Mark as freed in registry
779 break;
780 }
781 }
782
783 // Free the stack (raw free to match raw malloc above)
784 free(stack);
785 registry_unlock();
786}
int thread_id
int ascii_tls_set(tls_key_t key, void *value)
Set thread-local value for a key.
Definition threading.c:109
void * ascii_tls_get(tls_key_t key)
Get thread-local value for a key.
Definition threading.c:105
#define asciichat_thread_self()
#define asciichat_thread_equal(t1, t2)

References ascii_tls_get(), ascii_tls_set(), asciichat_thread_equal, asciichat_thread_self, atomic_load_bool(), atomic_load_int(), thread_registry_entry_t::stack, and thread_id.

Referenced by debug_sync_final_cleanup().

◆ mutex_stack_detect_deadlocks()

void mutex_stack_detect_deadlocks ( void  )

Detect circular wait deadlocks using DFS-based cycle detection.

Detect and log circular wait deadlocks.

Detects both same-thread and multi-thread deadlock patterns of any length:

Same-thread deadlock:

  • Thread tries to acquire a mutex it already holds (recursive lock on non-recursive mutex)

Multi-thread circular wait (2-way, 3-way, N-way):

  • Uses DFS to detect cycles in the "waits-for" graph
  • Reports all threads involved in the cycle

Definition at line 559 of file debug/mutex.c.

559 {
560 mutex_stack_entry_t **all_stacks = NULL;
561 int *stack_counts = NULL;
562 int thread_count = 0;
563
564 if (mutex_stack_get_all_threads(&all_stacks, &stack_counts, &thread_count) != 0) {
565 return;
566 }
567
568 // Analyze copies instead of stacks changing concurrently on other threads.
569 thread_lock_stack_t *snapshots = SAFE_CALLOC(thread_count, sizeof(thread_lock_stack_t), thread_lock_stack_t *);
570 for (int i = 0; i < thread_count; i++) {
571 snapshots[i].depth = stack_counts[i];
572 if (stack_counts[i] > 0)
573 memcpy(snapshots[i].stack, all_stacks[i], stack_counts[i] * sizeof(mutex_stack_entry_t));
574 }
575
576 // Check each thread for deadlock conditions
577 for (int i = 0; i < thread_count; i++) {
578 thread_lock_stack_t *stack_a = &snapshots[i];
579 uintptr_t waiting_for = thread_waiting_for_mutex(stack_a);
580
581 if (waiting_for == 0)
582 continue; // Thread not waiting
583
584 // Same-thread deadlock: thread trying to acquire a mutex it already holds
585 if (thread_holds_mutex(stack_a, waiting_for)) {
586 log_error("%s", colored_string(LOG_COLOR_ERROR, "╔═══════════════════════════════════════════════════════════╗"));
587 log_error("%s", colored_string(LOG_COLOR_ERROR, "║ ⚠️ DEADLOCK DETECTED: Same-thread Recursive Lock ⚠️ ║"));
588 log_error("%s", colored_string(LOG_COLOR_ERROR, "╚═══════════════════════════════════════════════════════════╝"));
589 log_error(" Thread Address: 0x%lx", (unsigned long)g_thread_registry[i].thread_id);
590 log_error(" Mutex: 0x%lx", waiting_for);
591 log_error(" Issue: Thread attempts recursive lock on non-recursive mutex");
592 continue;
593 }
594
595 // Multi-thread circular wait: use DFS to detect cycles
596 int cycle_path[MAX_CYCLE_LEN];
597 int cycle_len = 0;
598 int cycle_start = detect_cycle_dfs(snapshots, thread_count, i, cycle_path, &cycle_len);
599
600 if (cycle_start >= 0 && cycle_len > 1) {
601 // Collect mutexes involved in this deadlock
602 uintptr_t cycle_mutexes[MAX_CYCLE_MUTEXES];
603 int mutex_count = 0;
604 for (int k = 0; k < cycle_len && mutex_count < MAX_CYCLE_MUTEXES; k++) {
605 int thread_idx = cycle_path[k];
606 thread_lock_stack_t *stack = &snapshots[thread_idx];
607 uintptr_t waiting_for = thread_waiting_for_mutex(stack);
608 if (waiting_for != 0) {
609 cycle_mutexes[mutex_count++] = waiting_for;
610 }
611 }
612
613 // Check if mutexes are different from last deadlock
614 bool is_new_deadlock = deadlock_mutexes_changed(cycle_mutexes, mutex_count);
615 if (is_new_deadlock) {
616 update_deadlock_state(cycle_mutexes, mutex_count);
617 }
618
619 // Cycle detected! Build complete message in one string
620 char cycle_msg[4096];
621 int msg_len = 0;
622
623 // Leading newline and header box
624 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "\n%s\n",
625 colored_string(LOG_COLOR_ERROR, "╔═════════════════════════════════╗"));
626 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
627 colored_string(LOG_COLOR_ERROR, "║ DEADLOCK: Circular Wait Cycle ║"));
628 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
629 colored_string(LOG_COLOR_ERROR, "╚═════════════════════════════════╝"));
630
631 // Print each thread in the cycle
632 for (int k = 0; k < cycle_len; k++) {
633 int thread_idx = cycle_path[k];
634 int next_thread_idx = cycle_path[(k + 1) % cycle_len];
635
636 thread_lock_stack_t *current_stack = &snapshots[thread_idx];
637 uintptr_t current_waiting = thread_waiting_for_mutex(current_stack);
638
639 char thread_name[256], mutex_name[256], held_by_name[256];
640 NAMED_GET_BY_PTR((uintptr_t)g_thread_registry[thread_idx].thread_id, thread_name, sizeof(thread_name));
641 NAMED_GET_BY_PTR((uintptr_t)current_waiting, mutex_name, sizeof(mutex_name));
642 NAMED_GET_BY_PTR((uintptr_t)g_thread_registry[next_thread_idx].thread_id, held_by_name, sizeof(held_by_name));
643
644 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, " T%d: %s waits for %s (held by %s)%s",
645 k + 1, thread_name, mutex_name, held_by_name, k < cycle_len - 1 ? "\n" : "");
646 }
647
648 // Log repeated deadlock detections (skip first call, throttle subsequent ones)
649 if (!is_new_deadlock) {
650 log_error_every(1000000, "%s", cycle_msg); // 1000000 µs = 1 second
651 }
652 }
653 }
654
655 SAFE_FREE(snapshots);
656 mutex_stack_free_all_threads(all_stacks, stack_counts, thread_count);
657}
#define MAX_CYCLE_MUTEXES
Definition debug/mutex.c:76
int mutex_stack_get_all_threads(mutex_stack_entry_t ***out_stacks, int **out_stack_counts, int *out_thread_count)
Get all threads' lock stacks for deadlock analysis.
void mutex_stack_free_all_threads(mutex_stack_entry_t **stacks, int *stack_counts, int thread_count)
Free memory allocated by mutex_stack_get_all_threads()
#define MAX_CYCLE_LEN
DFS-based cycle detection in the waits-for graph Returns cycle start index if found,...
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
#define NAMED_GET_BY_PTR(key, buffer, size)
Get name of a pointer/key or format as address fallback.
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
@ LOG_COLOR_ERROR
Definition log/log.h:135
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:711
Definition debug/mutex.h:30

References colored_string(), thread_lock_stack_t::depth, LOG_COLOR_ERROR, log_error, log_error_every, MAX_CYCLE_LEN, MAX_CYCLE_MUTEXES, mutex_stack_free_all_threads(), mutex_stack_get_all_threads(), NAMED_GET_BY_PTR, SAFE_CALLOC, SAFE_FREE, and thread_id.

◆ mutex_stack_free_all_threads()

void mutex_stack_free_all_threads ( mutex_stack_entry_t **  stacks,
int *  stack_counts,
int  thread_count 
)

Free memory allocated by mutex_stack_get_all_threads()

Definition at line 381 of file debug/mutex.c.

381 {
382
383 if (!stacks || !stack_counts)
384 return;
385
386 for (int i = 0; i < thread_count; i++) {
387 SAFE_FREE(stacks[i]);
388 }
389
390 SAFE_FREE(stacks);
391 SAFE_FREE(stack_counts);
392}

References SAFE_FREE.

Referenced by mutex_stack_detect_deadlocks().

◆ mutex_stack_get_all_threads()

int mutex_stack_get_all_threads ( mutex_stack_entry_t ***  out_stacks,
int **  out_stack_counts,
int *  out_thread_count 
)

Get all threads' lock stacks for deadlock analysis.

Allocates memory for all thread stacks. Caller must free with mutex_stack_free_all_threads().

Parameters
out_stacksPointer to array of stacks (one per thread)
out_stack_countsArray of stack sizes corresponding to out_stacks
out_thread_countNumber of threads with lock stacks
Returns
0 on success, -1 on error

Definition at line 331 of file debug/mutex.c.

331 {
332
333 if (!out_stacks || !out_stack_counts || !out_thread_count) {
334 return -1;
335 }
336
337 // Read the thread count with acquire semantics (lock-free, no mutex needed)
338 // Use memory_order_seq_cst to ensure we get a consistent snapshot
339 int thread_count = atomic_load_int(&g_thread_registry_count);
340
341 // Allocate arrays for threads in the registry
342 // Note: SAFE_MALLOC takes bytes as first parameter, not count
343 *out_stacks = SAFE_CALLOC(thread_count, sizeof(mutex_stack_entry_t *), mutex_stack_entry_t **);
344 *out_stack_counts = SAFE_CALLOC(thread_count, sizeof(int), int *);
345
346 if (!*out_stacks || !*out_stack_counts) {
347 SAFE_FREE(*out_stacks);
348 SAFE_FREE(*out_stack_counts);
349 return -1;
350 }
351
352 // Copy each thread's stack from the registry
353 // Note: the thread count can change concurrently, so we re-read it in the loop
354 // to avoid accessing out-of-bounds memory if threads exit during iteration
355 for (int i = 0; i < thread_count; i++) {
356 // Re-check thread count in case registry shrank
357 int current_registry_count = atomic_load_int(&g_thread_registry_count);
358 if (i >= current_registry_count) {
359 break;
360 }
361
362 // Allocate before locking because tracked allocation updates the lock stack.
364 (*out_stack_counts)[i] = 0;
365 registry_lock();
366 thread_lock_stack_t *src = g_thread_registry[i].stack;
367 if (src) {
368 stack_lock(src);
369 int depth = src->depth;
370 (*out_stack_counts)[i] = depth;
371 memcpy((*out_stacks)[i], src->stack, depth * sizeof(mutex_stack_entry_t));
372 stack_unlock(src);
373 }
374 registry_unlock();
375 }
376
377 *out_thread_count = thread_count;
378 return 0;
379}
#define MUTEX_STACK_MAX_DEPTH
Definition debug/mutex.c:27
#define SAFE_MALLOC(size, cast)
Definition common.h:264
mutex_stack_entry_t stack[64]
Definition debug/mutex.c:30

References atomic_load_int(), thread_lock_stack_t::depth, MUTEX_STACK_MAX_DEPTH, SAFE_CALLOC, SAFE_FREE, SAFE_MALLOC, thread_lock_stack_t::stack, and thread_registry_entry_t::stack.

Referenced by mutex_stack_detect_deadlocks().

◆ mutex_stack_get_current()

int mutex_stack_get_current ( mutex_stack_entry_t *  out_entries,
int  max_entries 
)

Get the current thread's lock stack.

Parameters
out_entriesPointer to array where entries will be stored
max_entriesMaximum number of entries to return
Returns
Number of entries in the stack (may be > max_entries if truncated)

Definition at line 313 of file debug/mutex.c.

313 {
314 thread_lock_stack_t *stack = get_thread_local_stack();
315 if (!stack || !out_entries) {
316 return 0;
317 }
318
319 stack_lock(stack);
320 int depth = stack->depth;
321 int count = (depth < max_entries) ? depth : max_entries;
322 memcpy(out_entries, stack->stack, count * sizeof(mutex_stack_entry_t));
323 stack_unlock(stack);
324 return depth; // Return actual depth even if truncated
325}

References thread_lock_stack_t::depth, and thread_lock_stack_t::stack.

◆ mutex_stack_init()

int mutex_stack_init ( void  )

Initialize mutex stack system.

Returns
0 on success, -1 on error

Definition at line 750 of file debug/mutex.c.

750 {
751 // No initialization needed - registry uses lock-free atomic operations
752 return 0;
753}

◆ mutex_stack_mark_locked()

void mutex_stack_mark_locked ( uintptr_t  mutex_key)

Mark the top of the current thread's lock stack as LOCKED (transitions PENDING -> LOCKED)

Parameters
mutex_keyPointer to mutex (must match most recent pending push)

Definition at line 268 of file debug/mutex.c.

268 {
269 thread_lock_stack_t *stack = get_thread_local_stack();
270 if (!stack) {
271 return;
272 }
273
274 // Mark the top of the stack as locked
275 stack_lock(stack);
276 if (stack->depth == 0) {
277 stack_unlock(stack);
278 return;
279 }
280 int top = stack->depth - 1;
281 if (stack->stack[top].mutex_key == mutex_key) {
283 stack->stack[top].timestamp_ns = time_get_ns();
284 }
285 stack_unlock(stack);
286
287 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
288}
@ MUTEX_STACK_STATE_LOCKED
Definition debug/mutex.h:24
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
mutex_stack_state_t state
Definition debug/mutex.h:33
uint64_t timestamp_ns
Definition debug/mutex.h:34
uintptr_t mutex_key
Definition debug/mutex.h:31

References thread_lock_stack_t::depth, mutex_stack_entry_t::mutex_key, MUTEX_STACK_STATE_LOCKED, thread_lock_stack_t::stack, mutex_stack_entry_t::state, time_get_ns(), and mutex_stack_entry_t::timestamp_ns.

◆ mutex_stack_pop()

void mutex_stack_pop ( uintptr_t  mutex_key)

Pop the top mutex from the current thread's lock stack (unlock)

Parameters
mutex_keyPointer to mutex (used for validation)

Definition at line 290 of file debug/mutex.c.

290 {
291 thread_lock_stack_t *stack = get_thread_local_stack();
292 if (!stack) {
293 return;
294 }
295
296 // Validate top matches
297 stack_lock(stack);
298 if (stack->depth == 0) {
299 stack_unlock(stack);
300 return;
301 }
302 int top = stack->depth - 1;
303 if (stack->stack[top].mutex_key == mutex_key) {
304 stack->depth--;
305 }
306 stack_unlock(stack);
307
308 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
309}

References thread_lock_stack_t::depth, mutex_stack_entry_t::mutex_key, and thread_lock_stack_t::stack.

◆ mutex_stack_push_pending()

void mutex_stack_push_pending ( uintptr_t  mutex_key,
const char *  mutex_name 
)

Push a mutex onto the current thread's lock stack (PENDING state)

Parameters
mutex_keyPointer to mutex (used as unique ID)
mutex_nameHuman-readable name of mutex

Definition at line 246 of file debug/mutex.c.

246 {
247 thread_lock_stack_t *stack = get_thread_local_stack();
248 if (!stack) {
249 return;
250 }
251
252 // Register this thread in the global registry on first mutex use
253 register_thread_if_needed();
254
255 stack_lock(stack);
256 if (stack->depth >= MUTEX_STACK_MAX_DEPTH) {
257 stack_unlock(stack);
258 return;
259 }
260 stack->stack[stack->depth].mutex_key = mutex_key;
261 stack->stack[stack->depth].mutex_name = mutex_name;
263 stack->stack[stack->depth].timestamp_ns = time_get_ns();
264 stack->depth++;
265 stack_unlock(stack);
266}
@ MUTEX_STACK_STATE_PENDING
Definition debug/mutex.h:23
const char * mutex_name
Definition debug/mutex.h:32

References thread_lock_stack_t::depth, mutex_stack_entry_t::mutex_key, mutex_stack_entry_t::mutex_name, MUTEX_STACK_MAX_DEPTH, MUTEX_STACK_STATE_PENDING, thread_lock_stack_t::stack, mutex_stack_entry_t::state, time_get_ns(), and mutex_stack_entry_t::timestamp_ns.