sys.setrecursionlimit() now raises RecursionError · pythoncapi/cpython@50856d5 · GitHub
Skip to content

Commit 50856d5

Browse files
committed
sys.setrecursionlimit() now raises RecursionError
Issue python#25274: sys.setrecursionlimit() now raises a RecursionError if the new recursion limit is too low depending at the current recursion depth. Modify also the "lower-water mark" formula to make it monotonic. This mark is used to decide when the overflowed flag of the thread state is reset.
1 parent 60f2669 commit 50856d5

6 files changed

Lines changed: 102 additions & 13 deletions

File tree

Doc/library/sys.rst

Lines changed: 7 additions & 0 deletions

Include/ceval.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,16 @@ PyAPI_DATA(int) _Py_CheckRecursionLimit;
9494
# define _Py_MakeRecCheck(x) (++(x) > _Py_CheckRecursionLimit)
9595
#endif
9696

97+
/* Compute the "lower-water mark" for a recursion limit. When
98+
* Py_LeaveRecursiveCall() is called with a recursion depth below this mark,
99+
* the overflowed flag is reset to 0. */
100+
#define _Py_RecursionLimitLowerWaterMark(limit) \
101+
(((limit) > 200) \
102+
? ((limit) - 50) \
103+
: (3 * ((limit) >> 2)))
104+
97105
#define _Py_MakeEndRecCheck(x) \
98-
(--(x) < ((_Py_CheckRecursionLimit > 100) \
99-
? (_Py_CheckRecursionLimit - 50) \
100-
: (3 * (_Py_CheckRecursionLimit >> 2))))
106+
(--(x) < _Py_RecursionLimitLowerWaterMark(_Py_CheckRecursionLimit))
101107

102108
#define Py_ALLOW_RECURSION \
103109
do { unsigned char _old = PyThreadState_GET()->recursion_critical;\

Lib/test/test_sys.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -201,22 +201,60 @@ def test_recursionlimit_recovery(self):
201201
if hasattr(sys, 'gettrace') and sys.gettrace():
202202
self.skipTest('fatal error if run with a trace function')
203203

204-
# NOTE: this test is slightly fragile in that it depends on the current
205-
# recursion count when executing the test being low enough so as to
206-
# trigger the recursion recovery detection in the _Py_MakeEndRecCheck
207-
# macro (see ceval.h).
208204
oldlimit = sys.getrecursionlimit()
209205
def f():
210206
f()
211207
try:
212-
for i in (50, 1000):
213-
# Issue #5392: stack overflow after hitting recursion limit twice
214-
sys.setrecursionlimit(i)
208+
for depth in (10, 25, 50, 75, 100, 250, 1000):
209+
try:
210+
sys.setrecursionlimit(depth)
211+
except RecursionError:
212+
# Issue #25274: The recursion limit is too low at the
213+
# current recursion depth
214+
continue
215+
216+
# Issue #5392: test stack overflow after hitting recursion
217+
# limit twice
215218
self.assertRaises(RecursionError, f)
216219
self.assertRaises(RecursionError, f)
217220
finally:
218221
sys.setrecursionlimit(oldlimit)
219222

223+
@test.support.cpython_only
224+
def test_setrecursionlimit_recursion_depth(self):
225+
# Issue #25274: Setting a low recursion limit must be blocked if the
226+
# current recursion depth is already higher than the "lower-water
227+
# mark". Otherwise, it may not be possible anymore to
228+
# reset the overflowed flag to 0.
229+
230+
from _testcapi import get_recursion_depth
231+
232+
def set_recursion_limit_at_depth(depth, limit):
233+
recursion_depth = get_recursion_depth()
234+
if recursion_depth >= depth:
235+
with self.assertRaises(RecursionError) as cm:
236+
sys.setrecursionlimit(limit)
237+
self.assertRegex(str(cm.exception),
238+
"cannot set the recursion limit to [0-9]+ "
239+
"at the recursion depth [0-9]+: "
240+
"the limit is too low")
241+
else:
242+
set_recursion_limit_at_depth(depth, limit)
243+
244+
oldlimit = sys.getrecursionlimit()
245+
try:
246+
sys.setrecursionlimit(1000)
247+
248+
for limit in (10, 25, 50, 75, 100, 150, 200):
249+
# formula extracted from _Py_RecursionLimitLowerWaterMark()
250+
if limit > 200:
251+
depth = limit - 50
252+
else:
253+
depth = limit * 3 // 4
254+
set_recursion_limit_at_depth(depth, limit)
255+
finally:
256+
sys.setrecursionlimit(oldlimit)
257+
220258
def test_recursionlimit_fatalerror(self):
221259
# A fatal error occurs if a second recursion limit is hit when recovering
222260
# from a first one.

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Release date: TBA
1111
Core and Builtins
1212
-----------------
1313

14+
- Issue #25274: sys.setrecursionlimit() now raises a RecursionError if the new
15+
recursion limit is too low depending at the current recursion depth. Modify
16+
also the "lower-water mark" formula to make it monotonic. This mark is used
17+
to decide when the overflowed flag of the thread state is reset.
18+
1419
- Issue #24402: Fix input() to prompt to the redirected stdout when
1520
sys.stdout.fileno() fails.
1621

Modules/_testcapimodule.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3518,6 +3518,15 @@ test_PyTime_AsMicroseconds(PyObject *self, PyObject *args)
35183518
return _PyTime_AsNanosecondsObject(ms);
35193519
}
35203520

3521+
static PyObject*
3522+
get_recursion_depth(PyObject *self, PyObject *args)
3523+
{
3524+
PyThreadState *tstate = PyThreadState_GET();
3525+
3526+
/* substract one to ignore the frame of the get_recursion_depth() call */
3527+
return PyLong_FromLong(tstate->recursion_depth - 1);
3528+
}
3529+
35213530

35223531
static PyMethodDef TestMethods[] = {
35233532
{"raise_exception", raise_exception, METH_VARARGS},
@@ -3694,6 +3703,7 @@ static PyMethodDef TestMethods[] = {
36943703
#endif
36953704
{"PyTime_AsMilliseconds", test_PyTime_AsMilliseconds, METH_VARARGS},
36963705
{"PyTime_AsMicroseconds", test_PyTime_AsMicroseconds, METH_VARARGS},
3706+
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
36973707
{NULL, NULL} /* sentinel */
36983708
};
36993709

Python/sysmodule.c

Lines changed: 26 additions & 3 deletions

0 commit comments

Comments
 (0)