Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Lib/test/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import threading
import time
import unittest
import weakref
from test import support
from test.support import (
force_not_colorized, is_apple, is_apple_mobile, os_helper, threading_helper
Expand Down Expand Up @@ -1441,6 +1442,42 @@ def handler(a, b):
signal.raise_signal(signal.SIGINT)
self.assertTrue(is_ok)

def test_handler_replacing_itself_stays_alive(self):
events = []
def handler(signum, frame):
events.append("enter")
signal.signal(signum, signal.SIG_DFL)
support.gc_collect()
events.append("exit")
handler = functools.partial(handler)
wr = weakref.ref(handler, lambda ref: events.append("freed"))
old_signal = signal.signal(signal.SIGINT, handler)
self.addCleanup(signal.signal, signal.SIGINT, old_signal)
del handler

signal.raise_signal(signal.SIGINT)
support.gc_collect()
self.assertEqual(events, ["enter", "exit", "freed"])

def test_handler_replacing_itself_and_raising_is_freed(self):
events = []
def handler(signum, frame):
events.append("enter")
signal.signal(signum, signal.SIG_DFL)
support.gc_collect()
events.append("exit")
raise ZeroDivisionError
handler = functools.partial(handler)
wr = weakref.ref(handler, lambda ref: events.append("freed"))
old_signal = signal.signal(signal.SIGINT, handler)
self.addCleanup(signal.signal, signal.SIGINT, old_signal)
del handler

with self.assertRaises(ZeroDivisionError):
signal.raise_signal(signal.SIGINT)
support.gc_collect()
self.assertEqual(events, ["enter", "exit", "freed"])

def test__thread_interrupt_main(self):
# See https://github.com/python/cpython/issues/102397
code = """if 1:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a possible crash in :func:`signal.getsignal` on a free-threaded build when
another thread replaces the signal handler at the same time. A Python signal
handler that replaces itself with :func:`signal.signal` now stays alive until
it returns.
32 changes: 17 additions & 15 deletions Modules/signalmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "pycore_fileutils.h" // _Py_BEGIN_SUPPRESS_IPH
#include "pycore_interpframe.h" // _PyThreadState_GetFrame()
#include "pycore_moduleobject.h" // _PyModule_GetState()
#include "pycore_object.h" // _PyObject_XDecRefDelayed()
#include "pycore_pyerrors.h" // _PyErr_SetString()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_signal.h" // _Py_RestoreSignals()
Expand Down Expand Up @@ -135,12 +136,16 @@ get_handler(int i)
return (PyObject *)_Py_atomic_load_ptr(&Handlers[i].func);
}

Py_LOCAL_INLINE(void)
set_handler(int i, PyObject* func)
Py_LOCAL_INLINE(PyObject *)
swap_handler(int i, PyObject *func)
{
/* Store func with atomic operation to ensure
that PyErr_SetInterrupt is async-signal-safe. */
_Py_atomic_store_ptr(&Handlers[i].func, func);
/* The slot is updated with an atomic operation so that
PyErr_SetInterrupt() stays async-signal-safe. */
PyObject *old = _Py_atomic_exchange_ptr(&Handlers[i].func,
Py_XNewRef(func));
PyObject *result = Py_XNewRef(old);
_PyObject_XDecRefDelayed(old);
return result;
}


Expand Down Expand Up @@ -532,9 +537,7 @@ signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
return NULL;
}

old_handler = get_handler(signalnum);
set_handler(signalnum, Py_NewRef(handler));

old_handler = swap_handler(signalnum, handler);
if (old_handler != NULL) {
return old_handler;
}
Expand Down Expand Up @@ -1595,9 +1598,7 @@ signal_get_set_handlers(signal_state_t *state, PyObject *mod_dict)
}
// If signal_module_exec() is called more than one, we must
// clear the strong reference to the previous function.
PyObject* old_func = get_handler(signum);
set_handler(signum, Py_NewRef(func));
Py_XDECREF(old_func);
Py_XDECREF(swap_handler(signum, func));
}

// Install Python SIGINT handler which raises KeyboardInterrupt
Expand All @@ -1609,8 +1610,8 @@ signal_get_set_handlers(signal_state_t *state, PyObject *mod_dict)
return -1;
}

set_handler(SIGINT, int_handler);
Py_DECREF(sigint_func);
Py_XDECREF(swap_handler(SIGINT, int_handler));
Py_DECREF(int_handler);
PyOS_setsig(SIGINT, signal_handler);
}
return 0;
Expand Down Expand Up @@ -1744,9 +1745,8 @@ _PySignal_Fini(void)

// Restore default signals and clear handlers
for (int signum = 1; signum < Py_NSIG; signum++) {
PyObject *func = get_handler(signum);
_Py_atomic_store_int_relaxed(&Handlers[signum].tripped, 0);
set_handler(signum, NULL);
PyObject *func = swap_handler(signum, NULL);
if (func != NULL
&& func != Py_None
&& !compare_handler(func, state->default_handler)
Expand Down Expand Up @@ -1864,6 +1864,7 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
"calling signal handler");
continue;
}
Py_INCREF(func);
PyObject *arglist = NULL;
if (frame == NULL) {
arglist = Py_BuildValue("(iO)", i, Py_None);
Expand All @@ -1882,6 +1883,7 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
else {
result = NULL;
}
Py_DECREF(func);
if (!result) {
/* On error, re-schedule a call to _PyErr_CheckSignalsTstate() */
_Py_atomic_store_int(&is_tripped, 1);
Expand Down
Loading