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
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,13 @@ always available.
975975
that supports a higher limit. This should be done with care, because a too-high
976976
limit can lead to a crash.
977977

978+
If the new limit is too low at the current recursion depth, a
979+
:exc:`RecursionError` exception is raised.
980+
981+
.. versionchanged:: 3.5.1
982+
A :exc:`RecursionError` exception is now raised if the new limit is too
983+
low at the current recursion depth.
984+
978985

979986
.. function:: setswitchinterval(interval)
980987

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
Original file line numberDiff line numberDiff line change
@@ -632,14 +632,37 @@ processor's time-stamp counter."
632632
static PyObject *
633633
sys_setrecursionlimit(PyObject *self, PyObject *args)
634634
{
635-
int new_limit;
635+
int new_limit, mark;
636+
PyThreadState *tstate;
637+
636638
if (!PyArg_ParseTuple(args, "i:setrecursionlimit", &new_limit))
637639
return NULL;
638-
if (new_limit <= 0) {
640+
641+
if (new_limit < 1) {
639642
PyErr_SetString(PyExc_ValueError,
640-
"recursion limit must be positive");
643+
"recursion limit must be greater or equal than 1");
641644
return NULL;
642645
}
646+
647+
/* Issue #25274: When the recursion depth hits the recursion limit in
648+
_Py_CheckRecursiveCall(), the overflowed flag of the thread state is
649+
set to 1 and a RecursionError is raised. The overflowed flag is reset
650+
to 0 when the recursion depth goes below the low-water mark: see
651+
Py_LeaveRecursiveCall().
652+
653+
Reject too low new limit if the current recursion depth is higher than
654+
the new low-water mark. Otherwise it may not be possible anymore to
655+
reset the overflowed flag to 0. */
656+
mark = _Py_RecursionLimitLowerWaterMark(new_limit);
657+
tstate = PyThreadState_GET();
658+
if (tstate->recursion_depth >= mark) {
659+
PyErr_Format(PyExc_RecursionError,
660+
"cannot set the recursion limit to %i at "
661+
"the recursion depth %i: the limit is too low",
662+
new_limit, tstate->recursion_depth);
663+
return NULL;
664+
}
665+
643666
Py_SetRecursionLimit(new_limit);
644667
Py_INCREF(Py_None);
645668
return Py_None;

0 commit comments

Comments
 (0)