Skip to content
/ cpython Public
  • Rate limit · GitHub

    Whoa there!

    You have triggered an abuse detection mechanism.

    Please wait a few minutes before you try again;
    in some cases this may take up to an hour.

  • Notifications You must be signed in to change notification settings
  • Fork 31.4k
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7126cc1

Browse files
committedApr 23, 2017
bpo-30039: Don't run signal handlers while resuming a yield from stack
If we have a chain of generators/coroutines that are 'yield from'ing each other, then resuming the stack works like: - call send() on the outermost generator - this enters _PyEval_EvalFrameDefault, which re-executes the YIELD_FROM opcode - which calls send() on the next generator - which enters _PyEval_EvalFrameDefault, which re-executes the YIELD_FROM opcode - ...etc. However, every time we enter _PyEval_EvalFrameDefault, the first thing we do is to check for pending signals, and if there are any then we run the signal handler. And if it raises an exception, then we immediately propagate that exception *instead* of starting to execute bytecode. This means that e.g. a SIGINT at the wrong moment can "break the chain" – it can be raised in the middle of our yield from chain, with the bottom part of the stack abandoned for the garbage collector. The fix is pretty simple: there's already a special case in _PyEval_EvalFrameEx where it skips running signal handlers if the next opcode is SETUP_FINALLY. (I don't see how this accomplishes anything useful, but that's another story.) If we extend this check to also skip running signal handlers when the next opcode is YIELD_FROM, then that closes the hole – now the exception can only be raised at the innermost stack frame. This shouldn't have any performance implications, because the opcode check happens inside the "slow path" after we've already determined that there's a pending signal or something similar for us to process; the vast majority of the time this isn't true and the new check doesn't run at all. The included test fails before this patch, but passes afterwards.
1 parent 8312fba commit 7126cc1

File tree

4 files changed

+71
-3
lines changed

4 files changed

+71
-3
lines changed
 

‎Lib/test/test_generators.py

+29
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,35 @@
99

1010
from test import support
1111

12+
_testcapi = support.import_module('_testcapi')
13+
14+
15+
# This tests to make sure that if a SIGINT arrives just before we send into a
16+
# yield from chain, the KeyboardInterrupt is raised in the innermost
17+
# generator (see bpo-30039).
18+
class SignalAndYieldFromTest(unittest.TestCase):
19+
20+
def generator1(self):
21+
return (yield from self.generator2())
22+
23+
def generator2(self):
24+
try:
25+
yield
26+
except KeyboardInterrupt:
27+
return "PASSED"
28+
else:
29+
return "FAILED"
30+
31+
def test_raise_and_yield_from(self):
32+
gen = self.generator1()
33+
gen.send(None)
34+
try:
35+
_testcapi.raise_SIGINT_then_send_None(gen)
36+
except BaseException as _exc:
37+
exc = _exc
38+
self.assertIs(type(exc), StopIteration)
39+
self.assertEqual(exc.value, "PASSED")
40+
1241

1342
class FinalizationTest(unittest.TestCase):
1443

‎Misc/NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ What's New in Python 3.7.0 alpha 1?
1010
Core and Builtins
1111
-----------------
1212

13+
- bpo-30039: If a KeyboardInterrupt happens when the interpreter is in
14+
the middle of resuming a chain of nested 'yield from' or 'await'
15+
calls, it's now correctly delivered to the innermost frame.
16+
1317
- bpo-29839: len() now raises ValueError rather than OverflowError if
1418
__len__() returned a large negative integer.
1519

‎Modules/_testcapimodule.c

+24
Original file line numberDiff line numberDiff line change
@@ -4028,6 +4028,29 @@ dict_get_version(PyObject *self, PyObject *args)
40284028
}
40294029

40304030

4031+
static PyObject *
4032+
raise_SIGINT_then_send_None(PyObject *self, PyObject *args)
4033+
{
4034+
PyGenObject *gen;
4035+
4036+
if (!PyArg_ParseTuple(args, "O!", &PyGen_Type, &gen))
4037+
return NULL;
4038+
4039+
/* This is used in a test to check what happens if a signal arrives just
4040+
as we're in the process of entering a yield from chain (see
4041+
bpo-30039).
4042+
4043+
Needs to be done in C, because:
4044+
- we don't have a Python wrapper for raise()
4045+
- we need to make sure that the Python-level signal handler doesn't run
4046+
*before* we enter the generator frame, which is impossible in Python
4047+
because we check for signals before every bytecode operation.
4048+
*/
4049+
raise(SIGINT);
4050+
return _PyGen_Send(gen, Py_None);
4051+
}
4052+
4053+
40314054
static PyMethodDef TestMethods[] = {
40324055
{"raise_exception", raise_exception, METH_VARARGS},
40334056
{"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS},
@@ -4232,6 +4255,7 @@ static PyMethodDef TestMethods[] = {
42324255
{"tracemalloc_untrack", tracemalloc_untrack, METH_VARARGS},
42334256
{"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS},
42344257
{"dict_get_version", dict_get_version, METH_VARARGS},
4258+
{"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS},
42354259
{NULL, NULL} /* sentinel */
42364260
};
42374261

‎Python/ceval.c

+14-3
Original file line numberDiff line numberDiff line change
@@ -1064,9 +1064,20 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
10641064
Py_MakePendingCalls() above. */
10651065

10661066
if (_Py_atomic_load_relaxed(&eval_breaker)) {
1067-
if (_Py_OPCODE(*next_instr) == SETUP_FINALLY) {
1068-
/* Make the last opcode before
1069-
a try: finally: block uninterruptible. */
1067+
if (_Py_OPCODE(*next_instr) == SETUP_FINALLY ||
1068+
_Py_OPCODE(*next_instr) == YIELD_FROM) {
1069+
/* Two cases where we skip running signal handlers and other
1070+
pending calls:
1071+
- If we're about to enter the try: of a try/finally (not
1072+
*very* useful, but might help in some cases and it's
1073+
traditional)
1074+
- If we're resuming a chain of nested 'yield from' or
1075+
'await' calls, then each frame is parked with YIELD_FROM
1076+
as its next opcode. If the user hit control-C we want to
1077+
wait until we've reached the innermost frame before
1078+
running the signal handler and raising KeyboardInterrupt
1079+
(see bpo-30039).
1080+
*/
10701081
goto fast_next_opcode;
10711082
}
10721083
if (_Py_atomic_load_relaxed(&pendingcalls_to_do)) {

0 commit comments

Comments
 (0)
Failed to load comments.