Skip to content

Commit 6153369

Browse files
author
cxzhong
committed
Fix Python 3.14 compatibility: defer exception raising from signal handlers
Python 3.14 changed recursion checking to use actual C stack pointers instead of counters. The old implementation called Python code from within signal handlers before siglongjmp(), causing _Py_CheckRecursiveCall() to see inconsistent stack pointers and report 'Unrecoverable stack overflow'. Solution: Never call Python code from within signal handlers. Changes: - Added signal_to_raise field to cysigs_t to store deferred signal - Modified signal handlers to only set flag and longjmp (no Python calls) - Added _sig_raise_deferred() to raise exceptions after longjmp - Updated _sig_on_postjmp() to call _sig_raise_deferred() in safe context This maintains async-signal-safety and ensures Python's stack tracking remains consistent. All 65 tests pass on Python 3.14.0rc3. Also updates: - pyproject.toml: requires-python = '>=3.9,<3.15' (was <3.14) - CI/CD: Add Python 3.14 to test matrix
1 parent 5f35844 commit 6153369

File tree

8 files changed

+47
-9
lines changed

8 files changed

+47
-9
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
fail-fast: false
2323
matrix:
2424
os: ['macos-13', 'macos-latest', 'ubuntu-latest', 'windows-latest']
25-
python-version: ['3.10', '3.11', '3.12', '3.13']
25+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
2626
steps:
2727
- name: Set up the repository
2828
uses: actions/checkout@v4

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ classifiers = [
3030
"Topic :: Software Development :: Debuggers",
3131
]
3232
urls = { Homepage = "https://github.com/sagemath/cysignals" }
33-
requires-python = ">=3.9,<3.14"
33+
requires-python = ">=3.9,<3.15"
3434

3535
[project.entry-points.pkg_config]
3636
cysignals = 'cysignals'

src/cysignals/implementation.c

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,9 @@ static void cysigs_interrupt_handler(int sig)
392392
{
393393
if (!cysigs.block_sigint && !custom_signal_is_blocked())
394394
{
395-
/* Raise an exception so Python can see it */
396-
do_raise_exception(sig);
395+
/* Store signal number to raise exception after longjmp.
396+
* Do NOT call Python code from signal handler! */
397+
cysigs.signal_to_raise = sig;
397398

398399
#if !_WIN32
399400
/* Jump back to sig_on() (the first one if there is a stack) */
@@ -449,8 +450,9 @@ static void cysigs_signal_handler(int sig)
449450
}
450451
#endif
451452

452-
/* Raise an exception so Python can see it */
453-
do_raise_exception(sig);
453+
/* Store signal number to raise exception after longjmp.
454+
* Do NOT call Python code from signal handler! */
455+
cysigs.signal_to_raise = sig;
454456
#if !_WIN32
455457
/* Jump back to sig_on() (the first one if there is a stack) */
456458
siglongjmp(trampoline, sig);
@@ -616,6 +618,20 @@ static void _sig_on_recover(void)
616618
cysigs.inside_signal_handler = 0;
617619
}
618620

621+
/* Raise exception for signal that was deferred from signal handler.
622+
* This MUST be called from a safe context (after longjmp), NOT from
623+
* inside the signal handler itself. */
624+
static void _sig_raise_deferred(void)
625+
{
626+
int sig = cysigs.signal_to_raise;
627+
if (sig != 0)
628+
{
629+
cysigs.signal_to_raise = 0;
630+
/* Now safe to call Python code - we're back in normal context */
631+
do_raise_exception(sig);
632+
}
633+
}
634+
619635
/* Give a warning that sig_off() was called without sig_on() */
620636
static void _sig_off_warning(const char* file, int line)
621637
{

src/cysignals/macros.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ static inline int _sig_on_postjmp(int jmpret)
148148
{
149149
if (unlikely(jmpret > 0))
150150
{
151-
/* An exception occurred */
151+
/* A signal occurred and we jumped back via longjmp.
152+
* Now we're back in a safe context (not in signal handler),
153+
* so it's safe to call Python code to raise the exception. */
154+
_sig_raise_deferred();
152155
_sig_on_recover();
153156
return 0;
154157
}

src/cysignals/signals.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ cdef nogil:
9191
cysigs_t cysigs "cysigs"
9292
void _sig_on_interrupt_received "_sig_on_interrupt_received"() noexcept
9393
void _sig_on_recover "_sig_on_recover"() noexcept
94+
void _sig_raise_deferred "_sig_raise_deferred"() noexcept
9495
void _sig_off_warning "_sig_off_warning"(const char*, int) noexcept
9596
void print_backtrace "print_backtrace"() noexcept
9697

@@ -99,5 +100,6 @@ cdef inline void __generate_declarations() noexcept:
99100
cysigs
100101
_sig_on_interrupt_received
101102
_sig_on_recover
103+
_sig_raise_deferred
102104
_sig_off_warning
103105
print_backtrace

src/cysignals/signals.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ cdef extern from "implementation.c":
5656
void print_backtrace() nogil
5757
void _sig_on_interrupt_received() nogil
5858
void _sig_on_recover() nogil
59+
void _sig_raise_deferred() nogil
5960
void _sig_off_warning(const char*, int) nogil
6061

6162
# Python library functions for raising exceptions without "except"

src/cysignals/struct_signals.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ typedef struct
9191
* been received. This is set by sig_on(). */
9292
cyjmp_buf env;
9393

94+
/* Signal number to be raised as exception after longjmp.
95+
* Set by signal handler, checked and cleared by _sig_on_postjmp.
96+
* This allows us to defer calling Python code until we're back
97+
* in a safe context (not inside signal handler). */
98+
cy_atomic_int signal_to_raise;
99+
94100
/* An optional string (in UTF-8 encoding) to be used as text for
95101
* the exception raised by sig_raise_exception(). If this is NULL,
96102
* use some default string depending on the type of signal. This can

uv.lock

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)