Skip to content

Commit 1385f83

Browse files
[3.9] bpo-43406: Fix possible race condition where PyErr_CheckSignals tries to execute a non-Python signal handler (GH-24756) (GH-24761)
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. (cherry picked from commit 68245b7) Co-authored-by: Antoine Pitrou <[email protected]>
1 parent 65f3a0d commit 1385f83

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
@@ -1243,6 +1244,55 @@ def handler(signum, frame):
12431244
# Python handler
12441245
self.assertEqual(len(sigs), N, "Some signals were lost")
12451246

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

12481298
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
@@ -1714,10 +1714,34 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
17141714
}
17151715
_Py_atomic_store_relaxed(&Handlers[i].tripped, 0);
17161716

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

0 commit comments

Comments
 (0)