Skip to content

Share interrupt state across all SCS extensions (fixes swallowed SIGINT) - #231

Merged
bodono merged 2 commits into
masterfrom
fix/shared-interrupt
Sep 5, 2026
Merged

Share interrupt state across all SCS extensions (fixes swallowed SIGINT)#231
bodono merged 2 commits into
masterfrom
fix/shared-interrupt

Conversation

@bodono

@bodono bodono commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Fixes the highest-severity scs-python finding from the 3.3.0 review, reproduced exactly as described: after overlapping QDLDL and CPU_INDIRECT solves, a self-sent SIGINT was permanently swallowed.

Mechanism: each extension compiles its own ctrlc.c, so saved-handler/refcount state is per-extension. Non-LIFO overlap makes extension B save A's transient handler as "previous" and restore it last — a dead SCS handler stays installed forever.

Fix: Python builds swap scs_source/src/ctrlc.c for scs/py_ctrlc.c (same three-function interface, vendored source untouched) operating on one process-shared state struct, published as a PyCapsule via atomic PyDict_SetDefault insert-if-absent (race-safe under free-threading), resolved once per extension at import with the GIL held. One shared refcount ⟹ the real previous handler is saved once and restored once, however solves interleave. POSIX + Windows.

Test (subprocess-based, POSIX): one barrier-synchronised reproduction overlaps a QDLDL and a CPU_INDIRECT solve, sends SIGINT during the overlap, and checks that both solves stop and the original handler is back afterwards; before the fix the handler was swallowed.

Stacked on #230 (shares meson.build).

🤖 Generated with Claude Code

@bodono
bodono force-pushed the fix/shared-interrupt branch 7 times, most recently from 3ce285d to 7769fcd Compare August 31, 2026 11:01
bodono added a commit that referenced this pull request Aug 31, 2026
- legacy_setup.py rejects --blas64 outright: the legacy build cannot
  verify an ILP64 BLAS provider (even --mkl may resolve an LP64
  mkl_rt), and 64-bit prototypes against LP64 BLAS corrupt memory. The
  meson build selects and verifies the ILP64 interface; point there.
- legacy_setup.py no longer references scs/py_ctrlc.c, which this
  branch does not contain -- that hunk moves to #231 where the file is
  introduced.
- The pinned-negative regularization test now requires construction
  plus a solved status; a rejection by the pinned core skips loudly
  (TODO: becomes a hard failure at the post-#421 re-pin) and a
  rejection by the wrapper itself still fails.
- The ILP64 guard step asserts the core's specific 'MKL interface
  layer mismatch' diagnostic instead of accepting any ValueError, and
  runs under set -euo pipefail (bash -l drops GitHub's errexit).
@bodono
bodono force-pushed the fix/shared-interrupt branch 4 times, most recently from bced0a3 to 0f4d214 Compare August 31, 2026 19:51
bodono added a commit that referenced this pull request Aug 31, 2026
- legacy_setup.py rejects --blas64 outright: the legacy build cannot
  verify an ILP64 BLAS provider (even --mkl may resolve an LP64
  mkl_rt), and 64-bit prototypes against LP64 BLAS corrupt memory. The
  meson build selects and verifies the ILP64 interface; point there.
- legacy_setup.py no longer references scs/py_ctrlc.c, which this
  branch does not contain -- that hunk moves to #231 where the file is
  introduced.
- The pinned-negative regularization test now requires construction
  plus a solved status; a rejection by the pinned core skips loudly
  (TODO: becomes a hard failure at the post-#421 re-pin) and a
  rejection by the wrapper itself still fails.
- The ILP64 guard step asserts the core's specific 'MKL interface
  layer mismatch' diagnostic instead of accepting any ValueError, and
  runs under set -euo pipefail (bash -l drops GitHub's errexit).
@bodono
bodono force-pushed the fix/shared-interrupt branch 2 times, most recently from 26cf495 to ce901bb Compare September 1, 2026 07:36
@bodono
bodono force-pushed the fix/review-batch branch 2 times, most recently from 73d35e1 to 1b416a2 Compare September 3, 2026 19:53
@bodono
bodono force-pushed the fix/shared-interrupt branch 2 times, most recently from 62730e2 to ae8b57a Compare September 3, 2026 19:53
@bodono
bodono force-pushed the fix/shared-interrupt branch from ae8b57a to d22c26c Compare September 3, 2026 20:10
@bodono
bodono force-pushed the fix/shared-interrupt branch from d22c26c to d074a0f Compare September 3, 2026 21:07
@bodono
bodono force-pushed the fix/shared-interrupt branch from d074a0f to 951b8d0 Compare September 3, 2026 21:17
@bodono
bodono force-pushed the fix/shared-interrupt branch from 951b8d0 to 228d2f0 Compare September 3, 2026 21:29
@bodono
bodono force-pushed the fix/shared-interrupt branch from 228d2f0 to dbb2f75 Compare September 4, 2026 11:37
@bodono
bodono force-pushed the fix/shared-interrupt branch from dbb2f75 to 4540245 Compare September 4, 2026 16:49
bodono and others added 2 commits September 5, 2026 14:51
Every extension compiles its own copy of the solver, so file-static
interrupt state gave each one a private saved handler and refcount.
Overlapping solves from two different extensions then saved and restored
the SIGINT handler non-LIFO and left a dead SCS handler installed after
both finished: SIGINT was swallowed for the rest of the process.

Python builds now compile scs/py_ctrlc.c instead of the upstream ctrlc.c:
one state struct per process, published as a PyCapsule in a registry
module's dict with PyDict_SetDefault so racing imports agree on it, and
resolved once by each extension at import time. The interrupt flag is a
C11 atomic (signal- and thread-safe); the solver-facing functions touch no
Python API. Module init releases the module on every failure exit.

The test runs the real scenario in a subprocess with barrier-synchronized
threads: two overlapping solves from different backends are both
interrupted by one SIGINT, and Python's handler is back afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bodono
bodono force-pushed the fix/shared-interrupt branch from 4540245 to debf896 Compare September 5, 2026 13:59
@bodono
bodono changed the base branch from fix/review-batch to master September 5, 2026 14:51
@bodono
bodono merged commit f9432c1 into master Sep 5, 2026
42 checks passed
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