bpo-42500: Fix recursion in or after except (GH-23568)

* Use counter, rather boolean state when handling soft overflows.
This commit is contained in:
Mark Shannon 2020-12-02 13:30:55 +00:00 committed by GitHub
parent 93a0ef7647
commit 4e7a69bdb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 76 additions and 72 deletions

View File

@ -54,8 +54,7 @@ struct _ts {
/* Borrowed reference to the current frame (it can be NULL) */ /* Borrowed reference to the current frame (it can be NULL) */
PyFrameObject *frame; PyFrameObject *frame;
int recursion_depth; int recursion_depth;
char overflowed; /* The stack has overflowed. Allow 50 more calls int recursion_headroom; /* Allow 50 more calls to handle any errors. */
to handle the runtime error. */
int stackcheck_counter; int stackcheck_counter;
/* 'tracing' keeps track of the execution depth when tracing/profiling. /* 'tracing' keeps track of the execution depth when tracing/profiling.

View File

@ -92,24 +92,8 @@ static inline int _Py_EnterRecursiveCall_inline(const char *where) {
#define Py_EnterRecursiveCall(where) _Py_EnterRecursiveCall_inline(where) #define Py_EnterRecursiveCall(where) _Py_EnterRecursiveCall_inline(where)
/* Compute the "lower-water mark" for a recursion limit. When
* Py_LeaveRecursiveCall() is called with a recursion depth below this mark,
* the overflowed flag is reset to 0. */
static inline int _Py_RecursionLimitLowerWaterMark(int limit) {
if (limit > 200) {
return (limit - 50);
}
else {
return (3 * (limit >> 2));
}
}
static inline void _Py_LeaveRecursiveCall(PyThreadState *tstate) { static inline void _Py_LeaveRecursiveCall(PyThreadState *tstate) {
tstate->recursion_depth--; tstate->recursion_depth--;
int limit = tstate->interp->ceval.recursion_limit;
if (tstate->recursion_depth < _Py_RecursionLimitLowerWaterMark(limit)) {
tstate->overflowed = 0;
}
} }
static inline void _Py_LeaveRecursiveCall_inline(void) { static inline void _Py_LeaveRecursiveCall_inline(void) {

View File

@ -1046,7 +1046,7 @@ class ExceptionTests(unittest.TestCase):
# tstate->recursion_depth is equal to (recursion_limit - 1) # tstate->recursion_depth is equal to (recursion_limit - 1)
# and is equal to recursion_limit when _gen_throw() calls # and is equal to recursion_limit when _gen_throw() calls
# PyErr_NormalizeException(). # PyErr_NormalizeException().
recurse(setrecursionlimit(depth + 2) - depth - 1) recurse(setrecursionlimit(depth + 2) - depth)
finally: finally:
sys.setrecursionlimit(recursionlimit) sys.setrecursionlimit(recursionlimit)
print('Done.') print('Done.')
@ -1076,6 +1076,54 @@ class ExceptionTests(unittest.TestCase):
b'while normalizing an exception', err) b'while normalizing an exception', err)
self.assertIn(b'Done.', out) self.assertIn(b'Done.', out)
def test_recursion_in_except_handler(self):
def set_relative_recursion_limit(n):
depth = 1
while True:
try:
sys.setrecursionlimit(depth)
except RecursionError:
depth += 1
else:
break
sys.setrecursionlimit(depth+n)
def recurse_in_except():
try:
1/0
except:
recurse_in_except()
def recurse_after_except():
try:
1/0
except:
pass
recurse_after_except()
def recurse_in_body_and_except():
try:
recurse_in_body_and_except()
except:
recurse_in_body_and_except()
recursionlimit = sys.getrecursionlimit()
try:
set_relative_recursion_limit(10)
for func in (recurse_in_except, recurse_after_except, recurse_in_body_and_except):
with self.subTest(func=func):
try:
func()
except RecursionError:
pass
else:
self.fail("Should have raised a RecursionError")
finally:
sys.setrecursionlimit(recursionlimit)
@cpython_only @cpython_only
def test_recursion_normalizing_with_no_memory(self): def test_recursion_normalizing_with_no_memory(self):
# Issue #30697. Test that in the abort that occurs when there is no # Issue #30697. Test that in the abort that occurs when there is no
@ -1112,7 +1160,7 @@ class ExceptionTests(unittest.TestCase):
except MemoryError as e: except MemoryError as e:
tb = e.__traceback__ tb = e.__traceback__
else: else:
self.fail("Should have raises a MemoryError") self.fail("Should have raised a MemoryError")
return traceback.format_tb(tb) return traceback.format_tb(tb)
tb1 = raiseMemError() tb1 = raiseMemError()

View File

@ -221,7 +221,7 @@ class SysModuleTest(unittest.TestCase):
def f(): def f():
f() f()
try: try:
for depth in (10, 25, 50, 75, 100, 250, 1000): for depth in (50, 75, 100, 250, 1000):
try: try:
sys.setrecursionlimit(depth) sys.setrecursionlimit(depth)
except RecursionError: except RecursionError:
@ -231,17 +231,17 @@ class SysModuleTest(unittest.TestCase):
# Issue #5392: test stack overflow after hitting recursion # Issue #5392: test stack overflow after hitting recursion
# limit twice # limit twice
self.assertRaises(RecursionError, f) with self.assertRaises(RecursionError):
self.assertRaises(RecursionError, f) f()
with self.assertRaises(RecursionError):
f()
finally: finally:
sys.setrecursionlimit(oldlimit) sys.setrecursionlimit(oldlimit)
@test.support.cpython_only @test.support.cpython_only
def test_setrecursionlimit_recursion_depth(self): def test_setrecursionlimit_recursion_depth(self):
# Issue #25274: Setting a low recursion limit must be blocked if the # Issue #25274: Setting a low recursion limit must be blocked if the
# current recursion depth is already higher than the "lower-water # current recursion depth is already higher than limit.
# mark". Otherwise, it may not be possible anymore to
# reset the overflowed flag to 0.
from _testinternalcapi import get_recursion_depth from _testinternalcapi import get_recursion_depth
@ -262,42 +262,10 @@ class SysModuleTest(unittest.TestCase):
sys.setrecursionlimit(1000) sys.setrecursionlimit(1000)
for limit in (10, 25, 50, 75, 100, 150, 200): for limit in (10, 25, 50, 75, 100, 150, 200):
# formula extracted from _Py_RecursionLimitLowerWaterMark() set_recursion_limit_at_depth(limit, limit)
if limit > 200:
depth = limit - 50
else:
depth = limit * 3 // 4
set_recursion_limit_at_depth(depth, limit)
finally: finally:
sys.setrecursionlimit(oldlimit) sys.setrecursionlimit(oldlimit)
# The error message is specific to CPython
@test.support.cpython_only
def test_recursionlimit_fatalerror(self):
# A fatal error occurs if a second recursion limit is hit when recovering
# from a first one.
code = textwrap.dedent("""
import sys
def f():
try:
f()
except RecursionError:
f()
sys.setrecursionlimit(%d)
f()""")
with test.support.SuppressCrashReport():
for i in (50, 1000):
sub = subprocess.Popen([sys.executable, '-c', code % i],
stderr=subprocess.PIPE)
err = sub.communicate()[1]
self.assertTrue(sub.returncode, sub.returncode)
self.assertIn(
b"Fatal Python error: _Py_CheckRecursiveCall: "
b"Cannot recover from stack overflow",
err)
def test_getwindowsversion(self): def test_getwindowsversion(self):
# Raise SkipTest if sys doesn't have getwindowsversion attribute # Raise SkipTest if sys doesn't have getwindowsversion attribute
test.support.get_attribute(sys, "getwindowsversion") test.support.get_attribute(sys, "getwindowsversion")

View File

@ -0,0 +1,2 @@
Improve handling of exceptions near recursion limit. Converts a number of
Fatal Errors in RecursionErrors.

View File

@ -857,21 +857,23 @@ _Py_CheckRecursiveCall(PyThreadState *tstate, const char *where)
return -1; return -1;
} }
#endif #endif
if (tstate->overflowed) { if (tstate->recursion_headroom) {
if (tstate->recursion_depth > recursion_limit + 50) { if (tstate->recursion_depth > recursion_limit + 50) {
/* Overflowing while handling an overflow. Give up. */ /* Overflowing while handling an overflow. Give up. */
Py_FatalError("Cannot recover from stack overflow."); Py_FatalError("Cannot recover from stack overflow.");
} }
return 0;
} }
else {
if (tstate->recursion_depth > recursion_limit) { if (tstate->recursion_depth > recursion_limit) {
--tstate->recursion_depth; tstate->recursion_headroom++;
tstate->overflowed = 1;
_PyErr_Format(tstate, PyExc_RecursionError, _PyErr_Format(tstate, PyExc_RecursionError,
"maximum recursion depth exceeded%s", "maximum recursion depth exceeded%s",
where); where);
tstate->recursion_headroom--;
--tstate->recursion_depth;
return -1; return -1;
} }
}
return 0; return 0;
} }

View File

@ -290,12 +290,14 @@ _PyErr_NormalizeException(PyThreadState *tstate, PyObject **exc,
PyObject **val, PyObject **tb) PyObject **val, PyObject **tb)
{ {
int recursion_depth = 0; int recursion_depth = 0;
tstate->recursion_headroom++;
PyObject *type, *value, *initial_tb; PyObject *type, *value, *initial_tb;
restart: restart:
type = *exc; type = *exc;
if (type == NULL) { if (type == NULL) {
/* There was no exception, so nothing to do. */ /* There was no exception, so nothing to do. */
tstate->recursion_headroom--;
return; return;
} }
@ -347,6 +349,7 @@ _PyErr_NormalizeException(PyThreadState *tstate, PyObject **exc,
} }
*exc = type; *exc = type;
*val = value; *val = value;
tstate->recursion_headroom--;
return; return;
error: error:

View File

@ -605,7 +605,7 @@ new_threadstate(PyInterpreterState *interp, int init)
tstate->frame = NULL; tstate->frame = NULL;
tstate->recursion_depth = 0; tstate->recursion_depth = 0;
tstate->overflowed = 0; tstate->recursion_headroom = 0;
tstate->stackcheck_counter = 0; tstate->stackcheck_counter = 0;
tstate->tracing = 0; tstate->tracing = 0;
tstate->use_tracing = 0; tstate->use_tracing = 0;

View File

@ -1181,7 +1181,6 @@ static PyObject *
sys_setrecursionlimit_impl(PyObject *module, int new_limit) sys_setrecursionlimit_impl(PyObject *module, int new_limit)
/*[clinic end generated code: output=35e1c64754800ace input=b0f7a23393924af3]*/ /*[clinic end generated code: output=35e1c64754800ace input=b0f7a23393924af3]*/
{ {
int mark;
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
if (new_limit < 1) { if (new_limit < 1) {
@ -1199,8 +1198,7 @@ sys_setrecursionlimit_impl(PyObject *module, int new_limit)
Reject too low new limit if the current recursion depth is higher than Reject too low new limit if the current recursion depth is higher than
the new low-water mark. Otherwise it may not be possible anymore to the new low-water mark. Otherwise it may not be possible anymore to
reset the overflowed flag to 0. */ reset the overflowed flag to 0. */
mark = _Py_RecursionLimitLowerWaterMark(new_limit); if (tstate->recursion_depth >= new_limit) {
if (tstate->recursion_depth >= mark) {
_PyErr_Format(tstate, PyExc_RecursionError, _PyErr_Format(tstate, PyExc_RecursionError,
"cannot set the recursion limit to %i at " "cannot set the recursion limit to %i at "
"the recursion depth %i: the limit is too low", "the recursion depth %i: the limit is too low",