Skip to content

Commit 8b795ab

Browse files
authored
bpo-42500: Fix recursion in or after except (GH-23568) (#24501)
* Use counter, rather boolean state when handling soft overflows. (cherry picked from commit 4e7a69b)
1 parent f836e5f commit 8b795ab

File tree

9 files changed

+76
-75
lines changed

9 files changed

+76
-75
lines changed

Include/cpython/pystate.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ struct _ts {
5858
/* Borrowed reference to the current frame (it can be NULL) */
5959
PyFrameObject *frame;
6060
int recursion_depth;
61-
char overflowed; /* The stack has overflowed. Allow 50 more calls
62-
to handle the runtime error. */
61+
int recursion_headroom; /* Allow 50 more calls to handle any errors. */
6362
char recursion_critical; /* The current calls must not cause
6463
a stack overflow. */
6564
int stackcheck_counter;

Include/internal/pycore_ceval.h

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

9191
#define Py_EnterRecursiveCall(where) _Py_EnterRecursiveCall_inline(where)
9292

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

11397
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
@@ -1043,7 +1043,7 @@ def gen():
10431043
# tstate->recursion_depth is equal to (recursion_limit - 1)
10441044
# and is equal to recursion_limit when _gen_throw() calls
10451045
# PyErr_NormalizeException().
1046-
recurse(setrecursionlimit(depth + 2) - depth - 1)
1046+
recurse(setrecursionlimit(depth + 2) - depth)
10471047
finally:
10481048
sys.setrecursionlimit(recursionlimit)
10491049
print('Done.')
@@ -1073,6 +1073,54 @@ def test_recursion_normalizing_infinite_exception(self):
10731073
b'while normalizing an exception', err)
10741074
self.assertIn(b'Done.', out)
10751075

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

11151163
tb1 = raiseMemError()

Lib/test/test_sys.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def test_recursionlimit_recovery(self):
219219
def f():
220220
f()
221221
try:
222-
for depth in (10, 25, 50, 75, 100, 250, 1000):
222+
for depth in (50, 75, 100, 250, 1000):
223223
try:
224224
sys.setrecursionlimit(depth)
225225
except RecursionError:
@@ -229,17 +229,17 @@ def f():
229229

230230
# Issue #5392: test stack overflow after hitting recursion
231231
# limit twice
232-
self.assertRaises(RecursionError, f)
233-
self.assertRaises(RecursionError, f)
232+
with self.assertRaises(RecursionError):
233+
f()
234+
with self.assertRaises(RecursionError):
235+
f()
234236
finally:
235237
sys.setrecursionlimit(oldlimit)
236238

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

244244
from _testinternalcapi import get_recursion_depth
245245

@@ -260,42 +260,10 @@ def set_recursion_limit_at_depth(depth, limit):
260260
sys.setrecursionlimit(1000)
261261

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

272-
# The error message is specific to CPython
273-
@test.support.cpython_only
274-
def test_recursionlimit_fatalerror(self):
275-
# A fatal error occurs if a second recursion limit is hit when recovering
276-
# from a first one.
277-
code = textwrap.dedent("""
278-
import sys
279-
280-
def f():
281-
try:
282-
f()
283-
except RecursionError:
284-
f()
285-
286-
sys.setrecursionlimit(%d)
287-
f()""")
288-
with test.support.SuppressCrashReport():
289-
for i in (50, 1000):
290-
sub = subprocess.Popen([sys.executable, '-c', code % i],
291-
stderr=subprocess.PIPE)
292-
err = sub.communicate()[1]
293-
self.assertTrue(sub.returncode, sub.returncode)
294-
self.assertIn(
295-
b"Fatal Python error: _Py_CheckRecursiveCall: "
296-
b"Cannot recover from stack overflow",
297-
err)
298-
299267
def test_getwindowsversion(self):
300268
# Raise SkipTest if sys doesn't have getwindowsversion attribute
301269
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 & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -793,23 +793,22 @@ _Py_CheckRecursiveCall(PyThreadState *tstate, const char *where)
793793
_Py_CheckRecursionLimit = recursion_limit;
794794
}
795795
#endif
796-
if (tstate->recursion_critical)
797-
/* Somebody asked that we don't check for recursion. */
798-
return 0;
799-
if (tstate->overflowed) {
796+
if (tstate->recursion_headroom) {
800797
if (tstate->recursion_depth > recursion_limit + 50) {
801798
/* Overflowing while handling an overflow. Give up. */
802799
Py_FatalError("Cannot recover from stack overflow.");
803800
}
804-
return 0;
805801
}
806-
if (tstate->recursion_depth > recursion_limit) {
807-
--tstate->recursion_depth;
808-
tstate->overflowed = 1;
809-
_PyErr_Format(tstate, PyExc_RecursionError,
810-
"maximum recursion depth exceeded%s",
811-
where);
812-
return -1;
802+
else {
803+
if (tstate->recursion_depth > recursion_limit) {
804+
tstate->recursion_headroom++;
805+
_PyErr_Format(tstate, PyExc_RecursionError,
806+
"maximum recursion depth exceeded%s",
807+
where);
808+
tstate->recursion_headroom--;
809+
--tstate->recursion_depth;
810+
return -1;
811+
}
813812
}
814813
return 0;
815814
}

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
@@ -576,7 +576,7 @@ new_threadstate(PyInterpreterState *interp, int init)
576576

577577
tstate->frame = NULL;
578578
tstate->recursion_depth = 0;
579-
tstate->overflowed = 0;
579+
tstate->recursion_headroom = 0;
580580
tstate->recursion_critical = 0;
581581
tstate->stackcheck_counter = 0;
582582
tstate->tracing = 0;

Python/sysmodule.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,6 @@ static PyObject *
11601160
sys_setrecursionlimit_impl(PyObject *module, int new_limit)
11611161
/*[clinic end generated code: output=35e1c64754800ace input=b0f7a23393924af3]*/
11621162
{
1163-
int mark;
11641163
PyThreadState *tstate = _PyThreadState_GET();
11651164

11661165
if (new_limit < 1) {
@@ -1178,8 +1177,7 @@ sys_setrecursionlimit_impl(PyObject *module, int new_limit)
11781177
Reject too low new limit if the current recursion depth is higher than
11791178
the new low-water mark. Otherwise it may not be possible anymore to
11801179
reset the overflowed flag to 0. */
1181-
mark = _Py_RecursionLimitLowerWaterMark(new_limit);
1182-
if (tstate->recursion_depth >= mark) {
1180+
if (tstate->recursion_depth >= new_limit) {
11831181
_PyErr_Format(tstate, PyExc_RecursionError,
11841182
"cannot set the recursion limit to %i at "
11851183
"the recursion depth %i: the limit is too low",

0 commit comments

Comments
 (0)