Skip to content

Commit 4e7a69b

Browse files
authored
bpo-42500: Fix recursion in or after except (GH-23568)
* Use counter, rather boolean state when handling soft overflows.
1 parent 93a0ef7 commit 4e7a69b

File tree

9 files changed

+76
-72
lines changed

9 files changed

+76
-72
lines changed

Include/cpython/pystate.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ struct _ts {
5454
/* Borrowed reference to the current frame (it can be NULL) */
5555
PyFrameObject *frame;
5656
int recursion_depth;
57-
char overflowed; /* The stack has overflowed. Allow 50 more calls
58-
to handle the runtime error. */
57+
int recursion_headroom; /* Allow 50 more calls to handle any errors. */
5958
int stackcheck_counter;
6059

6160
/* 'tracing' keeps track of the execution depth when tracing/profiling.

Include/internal/pycore_ceval.h

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,8 @@ static inline int _Py_EnterRecursiveCall_inline(const char *where) {
9292

9393
#define Py_EnterRecursiveCall(where) _Py_EnterRecursiveCall_inline(where)
9494

95-
/* Compute the "lower-water mark" for a recursion limit. When
96-
* Py_LeaveRecursiveCall() is called with a recursion depth below this mark,
97-
* the overflowed flag is reset to 0. */
98-
static inline int _Py_RecursionLimitLowerWaterMark(int limit) {
99-
if (limit > 200) {
100-
return (limit - 50);
101-
}
102-
else {
103-
return (3 * (limit >> 2));
104-
}
105-
}
106-
10795
static inline void _Py_LeaveRecursiveCall(PyThreadState *tstate) {
10896
tstate->recursion_depth--;
109-
int limit = tstate->interp->ceval.recursion_limit;
110-
if (tstate->recursion_depth < _Py_RecursionLimitLowerWaterMark(limit)) {
111-
tstate->overflowed = 0;
112-
}
11397
}
11498

11599
static inline void _Py_LeaveRecursiveCall_inline(void) {

Lib/test/test_exceptions.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ def gen():
10461046
# tstate->recursion_depth is equal to (recursion_limit - 1)
10471047
# and is equal to recursion_limit when _gen_throw() calls
10481048
# PyErr_NormalizeException().
1049-
recurse(setrecursionlimit(depth + 2) - depth - 1)
1049+
recurse(setrecursionlimit(depth + 2) - depth)
10501050
finally:
10511051
sys.setrecursionlimit(recursionlimit)
10521052
print('Done.')
@@ -1076,6 +1076,54 @@ def test_recursion_normalizing_infinite_exception(self):
10761076
b'while normalizing an exception', err)
10771077
self.assertIn(b'Done.', out)
10781078

1079+
1080+
def test_recursion_in_except_handler(self):
1081+
1082+
def set_relative_recursion_limit(n):
1083+
depth = 1
1084+
while True:
1085+
try:
1086+
sys.setrecursionlimit(depth)
1087+
except RecursionError:
1088+
depth += 1
1089+
else:
1090+
break
1091+
sys.setrecursionlimit(depth+n)
1092+
1093+
def recurse_in_except():
1094+
try:
1095+
1/0
1096+
except:
1097+
recurse_in_except()
1098+
1099+
def recurse_after_except():
1100+
try:
1101+
1/0
1102+
except:
1103+
pass
1104+
recurse_after_except()
1105+
1106+
def recurse_in_body_and_except():
1107+
try:
1108+
recurse_in_body_and_except()
1109+
except:
1110+
recurse_in_body_and_except()
1111+
1112+
recursionlimit = sys.getrecursionlimit()
1113+
try:
1114+
set_relative_recursion_limit(10)
1115+
for func in (recurse_in_except, recurse_after_except, recurse_in_body_and_except):
1116+
with self.subTest(func=func):
1117+
try:
1118+
func()
1119+
except RecursionError:
1120+
pass
1121+
else:
1122+
self.fail("Should have raised a RecursionError")
1123+
finally:
1124+
sys.setrecursionlimit(recursionlimit)
1125+
1126+
10791127
@cpython_only
10801128
def test_recursion_normalizing_with_no_memory(self):
10811129
# Issue #30697. Test that in the abort that occurs when there is no
@@ -1112,7 +1160,7 @@ def raiseMemError():
11121160
except MemoryError as e:
11131161
tb = e.__traceback__
11141162
else:
1115-
self.fail("Should have raises a MemoryError")
1163+
self.fail("Should have raised a MemoryError")
11161164
return traceback.format_tb(tb)
11171165

11181166
tb1 = raiseMemError()

Lib/test/test_sys.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def test_recursionlimit_recovery(self):
221221
def f():
222222
f()
223223
try:
224-
for depth in (10, 25, 50, 75, 100, 250, 1000):
224+
for depth in (50, 75, 100, 250, 1000):
225225
try:
226226
sys.setrecursionlimit(depth)
227227
except RecursionError:
@@ -231,17 +231,17 @@ def f():
231231

232232
# Issue #5392: test stack overflow after hitting recursion
233233
# limit twice
234-
self.assertRaises(RecursionError, f)
235-
self.assertRaises(RecursionError, f)
234+
with self.assertRaises(RecursionError):
235+
f()
236+
with self.assertRaises(RecursionError):
237+
f()
236238
finally:
237239
sys.setrecursionlimit(oldlimit)
238240

239241
@test.support.cpython_only
240242
def test_setrecursionlimit_recursion_depth(self):
241243
# Issue #25274: Setting a low recursion limit must be blocked if the
242-
# current recursion depth is already higher than the "lower-water
243-
# mark". Otherwise, it may not be possible anymore to
244-
# reset the overflowed flag to 0.
244+
# current recursion depth is already higher than limit.
245245

246246
from _testinternalcapi import get_recursion_depth
247247

@@ -262,42 +262,10 @@ def set_recursion_limit_at_depth(depth, limit):
262262
sys.setrecursionlimit(1000)
263263

264264
for limit in (10, 25, 50, 75, 100, 150, 200):
265-
# formula extracted from _Py_RecursionLimitLowerWaterMark()
266-
if limit > 200:
267-
depth = limit - 50
268-
else:
269-
depth = limit * 3 // 4
270-
set_recursion_limit_at_depth(depth, limit)
265+
set_recursion_limit_at_depth(limit, limit)
271266
finally:
272267
sys.setrecursionlimit(oldlimit)
273268

274-
# The error message is specific to CPython
275-
@test.support.cpython_only
276-
def test_recursionlimit_fatalerror(self):
277-
# A fatal error occurs if a second recursion limit is hit when recovering
278-
# from a first one.
279-
code = textwrap.dedent("""
280-
import sys
281-
282-
def f():
283-
try:
284-
f()
285-
except RecursionError:
286-
f()
287-
288-
sys.setrecursionlimit(%d)
289-
f()""")
290-
with test.support.SuppressCrashReport():
291-
for i in (50, 1000):
292-
sub = subprocess.Popen([sys.executable, '-c', code % i],
293-
stderr=subprocess.PIPE)
294-
err = sub.communicate()[1]
295-
self.assertTrue(sub.returncode, sub.returncode)
296-
self.assertIn(
297-
b"Fatal Python error: _Py_CheckRecursiveCall: "
298-
b"Cannot recover from stack overflow",
299-
err)
300-
301269
def test_getwindowsversion(self):
302270
# Raise SkipTest if sys doesn't have getwindowsversion attribute
303271
test.support.get_attribute(sys, "getwindowsversion")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve handling of exceptions near recursion limit. Converts a number of
2+
Fatal Errors in RecursionErrors.

Python/ceval.c

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -857,20 +857,22 @@ _Py_CheckRecursiveCall(PyThreadState *tstate, const char *where)
857857
return -1;
858858
}
859859
#endif
860-
if (tstate->overflowed) {
860+
if (tstate->recursion_headroom) {
861861
if (tstate->recursion_depth > recursion_limit + 50) {
862862
/* Overflowing while handling an overflow. Give up. */
863863
Py_FatalError("Cannot recover from stack overflow.");
864864
}
865-
return 0;
866865
}
867-
if (tstate->recursion_depth > recursion_limit) {
868-
--tstate->recursion_depth;
869-
tstate->overflowed = 1;
870-
_PyErr_Format(tstate, PyExc_RecursionError,
871-
"maximum recursion depth exceeded%s",
872-
where);
873-
return -1;
866+
else {
867+
if (tstate->recursion_depth > recursion_limit) {
868+
tstate->recursion_headroom++;
869+
_PyErr_Format(tstate, PyExc_RecursionError,
870+
"maximum recursion depth exceeded%s",
871+
where);
872+
tstate->recursion_headroom--;
873+
--tstate->recursion_depth;
874+
return -1;
875+
}
874876
}
875877
return 0;
876878
}

Python/errors.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,14 @@ _PyErr_NormalizeException(PyThreadState *tstate, PyObject **exc,
290290
PyObject **val, PyObject **tb)
291291
{
292292
int recursion_depth = 0;
293+
tstate->recursion_headroom++;
293294
PyObject *type, *value, *initial_tb;
294295

295296
restart:
296297
type = *exc;
297298
if (type == NULL) {
298299
/* There was no exception, so nothing to do. */
300+
tstate->recursion_headroom--;
299301
return;
300302
}
301303

@@ -347,6 +349,7 @@ _PyErr_NormalizeException(PyThreadState *tstate, PyObject **exc,
347349
}
348350
*exc = type;
349351
*val = value;
352+
tstate->recursion_headroom--;
350353
return;
351354

352355
error:

Python/pystate.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ new_threadstate(PyInterpreterState *interp, int init)
605605

606606
tstate->frame = NULL;
607607
tstate->recursion_depth = 0;
608-
tstate->overflowed = 0;
608+
tstate->recursion_headroom = 0;
609609
tstate->stackcheck_counter = 0;
610610
tstate->tracing = 0;
611611
tstate->use_tracing = 0;

Python/sysmodule.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,6 @@ static PyObject *
11811181
sys_setrecursionlimit_impl(PyObject *module, int new_limit)
11821182
/*[clinic end generated code: output=35e1c64754800ace input=b0f7a23393924af3]*/
11831183
{
1184-
int mark;
11851184
PyThreadState *tstate = _PyThreadState_GET();
11861185

11871186
if (new_limit < 1) {
@@ -1199,8 +1198,7 @@ sys_setrecursionlimit_impl(PyObject *module, int new_limit)
11991198
Reject too low new limit if the current recursion depth is higher than
12001199
the new low-water mark. Otherwise it may not be possible anymore to
12011200
reset the overflowed flag to 0. */
1202-
mark = _Py_RecursionLimitLowerWaterMark(new_limit);
1203-
if (tstate->recursion_depth >= mark) {
1201+
if (tstate->recursion_depth >= new_limit) {
12041202
_PyErr_Format(tstate, PyExc_RecursionError,
12051203
"cannot set the recursion limit to %i at "
12061204
"the recursion depth %i: the limit is too low",

0 commit comments

Comments
 (0)