Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-107263: Increase C stack limit for most functions, except _PyEval_EvalFrameDefault() #107535

5 changes: 5 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,11 @@ sys
exception instance, rather than to a ``(typ, exc, tb)`` tuple.
(Contributed by Irit Katriel in :gh:`103176`.)

* :func:`sys.setrecursionlimit` and :func:`sys.getrecursionlimit`.
The recursion limit now applies only to Python code. Builtin functions do
not use the recursion limit, but are protected by a different mechanism
that prevents recursion from causing a virtual machine crash.

tempfile
--------

Expand Down
3 changes: 2 additions & 1 deletion Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ struct _ts {
# ifdef __wasi__
# define C_RECURSION_LIMIT 500
# else
# define C_RECURSION_LIMIT 800
// This value is duplicated in Lib/test/support/__init__.py
# define C_RECURSION_LIMIT 2000
# endif
#endif

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/list_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from functools import cmp_to_key

from test import seq_tests
from test.support import ALWAYS_EQ, NEVER_EQ
from test.support import ALWAYS_EQ, NEVER_EQ, C_RECURSION_LIMIT


class CommonTest(seq_tests.CommonTest):
Expand Down Expand Up @@ -61,7 +61,7 @@ def test_repr(self):

def test_repr_deep(self):
a = self.type2test([])
for i in range(sys.getrecursionlimit() + 100):
for i in range(C_RECURSION_LIMIT + 1):
a = self.type2test([a])
self.assertRaises(RecursionError, repr, a)

Expand Down
3 changes: 2 additions & 1 deletion Lib/test/mapping_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
import collections
import sys
from test.support import C_RECURSION_LIMIT


class BasicTestMappingProtocol(unittest.TestCase):
Expand Down Expand Up @@ -624,7 +625,7 @@ def __repr__(self):

def test_repr_deep(self):
d = self._empty_mapping()
for i in range(sys.getrecursionlimit() + 100):
for i in range(C_RECURSION_LIMIT + 1):
d0 = d
d = self._empty_mapping()
d[1] = d0
Expand Down
5 changes: 4 additions & 1 deletion Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"run_with_tz", "PGO", "missing_compiler_executable",
"ALWAYS_EQ", "NEVER_EQ", "LARGEST", "SMALLEST",
"LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT",
"Py_DEBUG", "EXCEEDS_RECURSION_LIMIT",
"Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "C_RECURSION_LIMIT",
]


Expand Down Expand Up @@ -2460,3 +2460,6 @@ def adjust_int_max_str_digits(max_digits):

#For recursion tests, easily exceeds default recursion limit
EXCEEDS_RECURSION_LIMIT = 5000

# The default C recursion limit (from Include/cpython/pystate.h).
C_RECURSION_LIMIT = 2000
21 changes: 8 additions & 13 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
import warnings
from test import support
from test.support import (script_helper, requires_debug_ranges,
requires_specialization)
requires_specialization, C_RECURSION_LIMIT)
from test.support.os_helper import FakePath


class TestSpecifics(unittest.TestCase):

def compile_single(self, source):
Expand Down Expand Up @@ -112,7 +111,7 @@ def __getitem__(self, key):

@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_extended_arg(self):
repeat = 2000
repeat = int(C_RECURSION_LIMIT * 0.9)
longexpr = 'x = x or ' + '-x' * repeat
g = {}
code = textwrap.dedent('''
Expand Down Expand Up @@ -558,16 +557,12 @@ def test_yet_more_evil_still_undecodable(self):
@support.cpython_only
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_compiler_recursion_limit(self):
# Expected limit is sys.getrecursionlimit() * the scaling factor
# in symtable.c (currently 3)
# We expect to fail *at* that limit, because we use up some of
# the stack depth limit in the test suite code
# So we check the expected limit and 75% of that
# XXX (ncoghlan): duplicating the scaling factor here is a little
# ugly. Perhaps it should be exposed somewhere...
fail_depth = sys.getrecursionlimit() * 3
crash_depth = sys.getrecursionlimit() * 300
success_depth = int(fail_depth * 0.75)
# Expected limit is C_RECURSION_LIMIT
# Duplicating the limit here is a little ugly.
# Perhaps it should be exposed somewhere...
fail_depth = C_RECURSION_LIMIT + 1
crash_depth = C_RECURSION_LIMIT * 100
success_depth = int(C_RECURSION_LIMIT * 0.9)

def check_limit(prefix, repeated, mode="single"):
expect_ok = prefix + repeated * success_depth
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import unittest
import weakref
from test import support
from test.support import import_helper
from test.support import import_helper, C_RECURSION_LIMIT


class DictTest(unittest.TestCase):
Expand Down Expand Up @@ -596,7 +596,7 @@ def __repr__(self):

def test_repr_deep(self):
d = {}
for i in range(sys.getrecursionlimit() + 100):
for i in range(C_RECURSION_LIMIT + 1):
d = {1: d}
self.assertRaises(RecursionError, repr, d)

Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_dictviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pickle
import sys
import unittest
from test.support import C_RECURSION_LIMIT

class DictSetTest(unittest.TestCase):

Expand Down Expand Up @@ -279,7 +280,7 @@ def test_recursive_repr(self):

def test_deeply_nested_repr(self):
d = {}
for i in range(sys.getrecursionlimit() + 100):
for i in range(C_RECURSION_LIMIT//2 + 100):
d = {42: d.values()}
self.assertRaises(RecursionError, repr, d)

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_exception_group.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import collections.abc
import types
import unittest

from test.support import C_RECURSION_LIMIT

class TestExceptionGroupTypeHierarchy(unittest.TestCase):
def test_exception_group_types(self):
Expand Down Expand Up @@ -460,7 +460,7 @@ def test_basics_split_by_predicate__match(self):
class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
def make_deep_eg(self):
e = TypeError(1)
for i in range(2000):
for i in range(C_RECURSION_LIMIT + 1):
e = ExceptionGroup('eg', [e])
return e

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Increase C recursion limit for functions other than the main interpreter
from 800 to 2500. This should allow functions like ``list.__repr__`` and
markshannon marked this conversation as resolved.
Show resolved Hide resolved
``json.dumps`` to handle all the inputs that they could prior to 3.12
2 changes: 1 addition & 1 deletion Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ class PartingShots(StaticVisitor):

int starting_recursion_depth;
/* Be careful here to prevent overflow. */
int COMPILER_STACK_FRAME_SCALE = 3;
int COMPILER_STACK_FRAME_SCALE = 1;
PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) {
return 0;
Expand Down
2 changes: 1 addition & 1 deletion Python/Python-ast.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Python/ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ validate_type_params(struct validator *state, asdl_type_param_seq *tps)


/* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 3
#define COMPILER_STACK_FRAME_SCALE 1

int
_PyAST_Validate(mod_ty mod)
Expand Down
2 changes: 1 addition & 1 deletion Python/ast_opt.c
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ astfold_type_param(type_param_ty node_, PyArena *ctx_, _PyASTOptimizeState *stat
#undef CALL_SEQ

/* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 3
#define COMPILER_STACK_FRAME_SCALE 1

int
_PyAST_Optimize(mod_ty mod, PyArena *arena, int optimize, int ff_features)
Expand Down
2 changes: 1 addition & 1 deletion Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ dummy_func(
tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous);
assert(!_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallTstate(tstate);
tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return retval;
}

Expand Down
8 changes: 7 additions & 1 deletion Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,11 @@ extern const struct _PyCode_DEF(8) _Py_InitCleanup;
# pragma warning(disable:4102)
#endif


/* _PyEval_EvalFrameDefault() is a *big* function,
* so consume 3 units of C stack */
#define PY_EVAL_C_STACK_UNITS 3

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{
Expand Down Expand Up @@ -676,6 +681,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
frame->previous = &entry_frame;
cframe.current_frame = frame;

tstate->c_recursion_remaining -= (PY_EVAL_C_STACK_UNITS - 1);
if (_Py_EnterRecursiveCallTstate(tstate, "")) {
tstate->c_recursion_remaining--;
tstate->py_recursion_remaining--;
Expand Down Expand Up @@ -907,7 +913,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
/* Restore previous cframe and exit */
tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous);
_Py_LeaveRecursiveCallTstate(tstate);
tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return NULL;
}

Expand Down
2 changes: 1 addition & 1 deletion Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 2 additions & 9 deletions Python/symtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -282,17 +282,10 @@ symtable_new(void)
return NULL;
}

/* When compiling the use of C stack is probably going to be a lot
lighter than when executing Python code but still can overflow
and causing a Python crash if not checked (e.g. eval("()"*300000)).
Using the current recursion limit for the compiler seems too
restrictive (it caused at least one test to fail) so a factor is
used to allow deeper recursion when compiling an expression.

Using a scaling factor means this should automatically adjust when
/* Using a scaling factor means this should automatically adjust when
the recursion limit is adjusted for small or large C stack allocations.
*/
#define COMPILER_STACK_FRAME_SCALE 3
#define COMPILER_STACK_FRAME_SCALE 1

struct symtable *
_PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
Expand Down
Loading