Skip to content

Make signal handler registration thread-safe#375

Merged
bodono merged 5 commits intomasterfrom
thread-safe-ctrlc
Apr 7, 2026
Merged

Make signal handler registration thread-safe#375
bodono merged 5 commits intomasterfrom
thread-safe-ctrlc

Conversation

@bodono
Copy link
Copy Markdown
Member

@bodono bodono commented Apr 7, 2026

Summary

When multiple threads call scs_solve concurrently, each thread calls scs_start_interrupt_listener which writes to global int_detected and struct sigaction oact without synchronization. This causes data races detected by Thread Sanitizer (TSan) when testing free-threaded Python (3.13t/3.14t) with the scs-python wrapper.

Fix:

  • Unix: Add pthread_mutex_t + reference count. Only the first concurrent solver installs the SIGINT handler, only the last restores it. Change int_detected to volatile sig_atomic_t (POSIX-correct type for signal handler communication).
  • Windows: Add CRITICAL_SECTION (initialized via InitOnceExecuteOnce) + reference count. Use InterlockedExchange / InterlockedCompareExchange for atomic int_detected access.
  • Build: Link pthreads via find_package(Threads) in CMake and -lpthread in the Makefile (Linux only — macOS has pthreads built-in).

No API changes. The scs_start_interrupt_listener / scs_end_interrupt_listener / scs_is_interrupted signatures and behavior are unchanged from the caller's perspective.

Context

TSan report from scs-python free-threading CI:

WARNING: ThreadSanitizer: data race (pid=...)
  Write of size 4 at ... by thread T2:
    #0 scs_start_interrupt_listener scs_source/src/ctrlc.c:69
    #1 scs_solve scs_source/src/scs.c:1216
  Previous write of size 4 at ... by thread T1:
    #0 scs_start_interrupt_listener scs_source/src/ctrlc.c:69
    #1 scs_solve scs_source/src/scs.c:1216

Test plan

  • scs-python test suite passes (282 tests)
  • scs-python free-threading tests pass (27 threading tests)
  • TSan CI should pass with this fix (currently suppressed in scs-python)

🤖 Generated with Claude Code

When multiple threads call scs_solve concurrently, each thread calls
scs_start_interrupt_listener which writes to global state (int_detected,
oact/sigaction) without synchronization, causing data races detected by
Thread Sanitizer.

Fix by adding reference counting with a mutex (pthread_mutex on Unix,
CRITICAL_SECTION on Windows). Only the first concurrent solver installs
the signal handler, and only the last one restores it. Use
volatile sig_atomic_t for int_detected (POSIX signal safety).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bodono and others added 4 commits April 8, 2026 00:01
Move scs_start_interrupt_listener() after the input validation checks
so that early-return paths (NULL data/cone, failed validation) don't
increment listener_count without a matching decrement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
InitOnceExecuteOnce/INIT_ONCE caused segfaults on MSYS2/MinGW-w64.
Replace with a simple InterlockedCompareExchange spinlock, which is
universally available on all Windows toolchains.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The failure() helper calls scs_end_interrupt_listener() unconditionally,
including when scs_init fails validation before calling scs_start.
Guard against decrementing listener_count below 0, and on Windows call
InitOnceExecuteOnce in scs_end too so the CRITICAL_SECTION is always
initialized before use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bodono bodono merged commit 92428c1 into master Apr 7, 2026
64 checks passed
@bodono bodono deleted the thread-safe-ctrlc branch April 7, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant