Skip to content

Commit 68245b7

Browse files
authored
bpo-43406: Fix possible race condition where PyErr_CheckSignals tries to execute a non-Python signal handler (GH-24756)
We can receive signals (at the C level, in `trip_signal()` in signalmodule.c) while `signal.signal` is being called to modify the corresponding handler. Later when `PyErr_CheckSignals()` is called to handle the given signal, the handler may be a non-callable object and would raise a cryptic asynchronous exception.
1 parent 02ac6f4 commit 68245b7

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

Lib/test/test_signal.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import statistics
77
import subprocess
88
import sys
9+
import threading
910
import time
1011
import unittest
1112
from test import support
@@ -1251,6 +1252,55 @@ def handler(signum, frame):
12511252
# Python handler
12521253
self.assertEqual(len(sigs), N, "Some signals were lost")
12531254

1255+
@unittest.skipUnless(hasattr(signal, "SIGUSR1"),
1256+
"test needs SIGUSR1")
1257+
def test_stress_modifying_handlers(self):
1258+
# bpo-43406: race condition between trip_signal() and signal.signal
1259+
signum = signal.SIGUSR1
1260+
num_sent_signals = 0
1261+
num_received_signals = 0
1262+
do_stop = False
1263+
1264+
def custom_handler(signum, frame):
1265+
nonlocal num_received_signals
1266+
num_received_signals += 1
1267+
1268+
def set_interrupts():
1269+
nonlocal num_sent_signals
1270+
while not do_stop:
1271+
signal.raise_signal(signum)
1272+
num_sent_signals += 1
1273+
1274+
def cycle_handlers():
1275+
while num_sent_signals < 100:
1276+
for i in range(20000):
1277+
# Cycle between a Python-defined and a non-Python handler
1278+
for handler in [custom_handler, signal.SIG_IGN]:
1279+
signal.signal(signum, handler)
1280+
1281+
old_handler = signal.signal(signum, custom_handler)
1282+
self.addCleanup(signal.signal, signum, old_handler)
1283+
t = threading.Thread(target=set_interrupts)
1284+
t.start()
1285+
try:
1286+
with support.catch_unraisable_exception() as cm:
1287+
cycle_handlers()
1288+
if cm.unraisable is not None:
1289+
# An unraisable exception may be printed out when
1290+
# a signal is ignored due to the aforementioned
1291+
# race condition, check it.
1292+
self.assertIsInstance(cm.unraisable.exc_value, OSError)
1293+
self.assertIn(
1294+
f"Signal {signum} ignored due to race condition",
1295+
str(cm.unraisable.exc_value))
1296+
# Sanity check that some signals were received, but not all
1297+
self.assertGreater(num_received_signals, 0)
1298+
self.assertLess(num_received_signals, num_sent_signals)
1299+
finally:
1300+
do_stop = True
1301+
t.join()
1302+
1303+
12541304
class RaiseSignalTest(unittest.TestCase):
12551305

12561306
def test_sigint(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a possible race condition where ``PyErr_CheckSignals`` tries to execute a
2+
non-Python signal handler.

Modules/signalmodule.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1706,10 +1706,34 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
17061706
}
17071707
_Py_atomic_store_relaxed(&Handlers[i].tripped, 0);
17081708

1709+
/* Signal handlers can be modified while a signal is received,
1710+
* and therefore the fact that trip_signal() or PyErr_SetInterrupt()
1711+
* was called doesn't guarantee that there is still a Python
1712+
* signal handler for it by the time PyErr_CheckSignals() is called
1713+
* (see bpo-43406).
1714+
*/
1715+
PyObject *func = Handlers[i].func;
1716+
if (func == NULL || func == Py_None || func == IgnoreHandler ||
1717+
func == DefaultHandler) {
1718+
/* No Python signal handler due to aforementioned race condition.
1719+
* We can't call raise() as it would break the assumption
1720+
* that PyErr_SetInterrupt() only *simulates* an incoming
1721+
* signal (i.e. it will never kill the process).
1722+
* We also don't want to interrupt user code with a cryptic
1723+
* asynchronous exception, so instead just write out an
1724+
* unraisable error.
1725+
*/
1726+
PyErr_Format(PyExc_OSError,
1727+
"Signal %i ignored due to race condition",
1728+
i);
1729+
PyErr_WriteUnraisable(Py_None);
1730+
continue;
1731+
}
1732+
17091733
PyObject *arglist = Py_BuildValue("(iO)", i, frame);
17101734
PyObject *result;
17111735
if (arglist) {
1712-
result = _PyObject_Call(tstate, Handlers[i].func, arglist, NULL);
1736+
result = _PyObject_Call(tstate, func, arglist, NULL);
17131737
Py_DECREF(arglist);
17141738
}
17151739
else {

0 commit comments

Comments
 (0)