Skip to content

Commit

Permalink
[3.12] GH-112215: Backport C recursion changes (GH-115083)
Browse files Browse the repository at this point in the history
  • Loading branch information
markshannon authored Feb 13, 2024
1 parent a30bb08 commit 4d87832
Show file tree
Hide file tree
Showing 14 changed files with 64 additions and 45 deletions.
20 changes: 16 additions & 4 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,24 @@ struct _ts {
/* WASI has limited call stack. Python's recursion limit depends on code
layout, optimization, and WASI runtime. Wasmtime can handle about 700
recursions, sometimes less. 500 is a more conservative limit. */
#ifndef C_RECURSION_LIMIT
# ifdef __wasi__
#ifdef Py_DEBUG
# if defined(__wasi__)
# define C_RECURSION_LIMIT 150
# else
# define C_RECURSION_LIMIT 500
# endif
#else
# if defined(__wasi__)
# define C_RECURSION_LIMIT 500
# elif defined(__s390x__)
# define C_RECURSION_LIMIT 800
# elif defined(_WIN32)
# define C_RECURSION_LIMIT 3000
# elif defined(_Py_ADDRESS_SANITIZER)
# define C_RECURSION_LIMIT 4000
# else
// This value is duplicated in Lib/test/support/__init__.py
# define C_RECURSION_LIMIT 1500
// This value is duplicated in Lib/test/support/__init__.py
# define C_RECURSION_LIMIT 10000
# endif
#endif

Expand Down
31 changes: 23 additions & 8 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2112,13 +2112,13 @@ def set_recursion_limit(limit):
finally:
sys.setrecursionlimit(original_limit)

def infinite_recursion(max_depth=100):
"""Set a lower limit for tests that interact with infinite recursions
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some
debug windows builds, due to not enough functions being inlined the
stack size might not handle the default recursion limit (1000). See
bpo-11105 for details."""
if max_depth < 3:
def infinite_recursion(max_depth=None):
if max_depth is None:
# Pick a number large enough to cause problems
# but not take too long for code that can handle
# very deep recursion.
max_depth = 20_000
elif max_depth < 3:
raise ValueError("max_depth must be at least 3, got {max_depth}")
depth = get_recursion_depth()
depth = max(depth - 1, 1) # Ignore infinite_recursion() frame.
Expand Down Expand Up @@ -2362,7 +2362,22 @@ def adjust_int_max_str_digits(max_digits):
EXCEEDS_RECURSION_LIMIT = 5000

# The default C recursion limit (from Include/cpython/pystate.h).
C_RECURSION_LIMIT = 1500
if Py_DEBUG:
if is_wasi:
C_RECURSION_LIMIT = 150
else:
C_RECURSION_LIMIT = 500
else:
if is_wasi:
C_RECURSION_LIMIT = 500
elif hasattr(os, 'uname') and os.uname().machine == 's390x':
C_RECURSION_LIMIT = 800
elif sys.platform.startswith('win'):
C_RECURSION_LIMIT = 3000
elif check_sanitizer(address=True):
C_RECURSION_LIMIT = 4000
else:
C_RECURSION_LIMIT = 10000

#Windows doesn't have os.uname() but it doesn't support s390x.
skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,9 +1087,9 @@ def next(self):
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
@support.cpython_only
def test_ast_recursion_limit(self):
fail_depth = support.EXCEEDS_RECURSION_LIMIT
fail_depth = support.C_RECURSION_LIMIT + 1
crash_depth = 100_000
success_depth = 1200
success_depth = int(support.C_RECURSION_LIMIT * 0.9)

def check_limit(prefix, repeated):
expect_ok = prefix + repeated * success_depth
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_call.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from test.support import cpython_only, requires_limited_api, skip_on_s390x
from test.support import cpython_only, requires_limited_api, skip_on_s390x, is_wasi, Py_DEBUG
try:
import _testcapi
except ImportError:
Expand Down Expand Up @@ -932,6 +932,7 @@ def test_multiple_values(self):
class TestRecursion(unittest.TestCase):

@skip_on_s390x
@unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
def test_super_deep(self):

def recurse(n):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,9 +607,9 @@ def test_compiler_recursion_limit(self):
# Expected limit is C_RECURSION_LIMIT * 2
# Duplicating the limit here is a little ugly.
# Perhaps it should be exposed somewhere...
fail_depth = C_RECURSION_LIMIT * 2 + 1
fail_depth = C_RECURSION_LIMIT + 1
crash_depth = C_RECURSION_LIMIT * 100
success_depth = int(C_RECURSION_LIMIT * 1.8)
success_depth = int(C_RECURSION_LIMIT * 0.9)

def check_limit(prefix, repeated, mode="single"):
expect_ok = prefix + repeated * success_depth
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_isinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def blowstack(fxn, arg, compare_to):
# Make sure that calling isinstance with a deeply nested tuple for its
# argument will raise RecursionError eventually.
tuple_arg = (compare_to,)
for cnt in range(support.EXCEEDS_RECURSION_LIMIT):
for cnt in range(support.C_RECURSION_LIMIT * 2):
tuple_arg = (tuple_arg,)
fxn(arg, tuple_arg)

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_plistlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ def test_cycles(self):
self.assertIs(b['x'], b)

def test_deep_nesting(self):
tests = [50, 100_000] if support.is_wasi else [50, 300, 100_000]
tests = [50, 100_000] if support.is_wasi else [50, 600, 100_000]
for N in tests:
chunks = [b'\xa1' + (i + 1).to_bytes(4, 'big') for i in range(N)]
try:
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_sys_settrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -2965,16 +2965,18 @@ def test_trace_unpack_long_sequence(self):
self.assertEqual(counts, {'call': 1, 'line': 301, 'return': 1})

def test_trace_lots_of_globals(self):
count = min(1000, int(support.C_RECURSION_LIMIT * 0.8))

code = """if 1:
def f():
return (
{}
)
""".format("\n+\n".join(f"var{i}\n" for i in range(1000)))
ns = {f"var{i}": i for i in range(1000)}
""".format("\n+\n".join(f"var{i}\n" for i in range(count)))
ns = {f"var{i}": i for i in range(count)}
exec(code, ns)
counts = self.count_traces(ns["f"])
self.assertEqual(counts, {'call': 1, 'line': 2000, 'return': 1})
self.assertEqual(counts, {'call': 1, 'line': count * 2, 'return': 1})


class TestEdgeCases(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Change the C recursion limits to more closely reflect the underlying
platform limits.
5 changes: 2 additions & 3 deletions Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,15 +1393,14 @@ class PartingShots(StaticVisitor):
int starting_recursion_depth;
/* Be careful here to prevent overflow. */
int COMPILER_STACK_FRAME_SCALE = 2;
PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) {
return NULL;
}
struct validator vstate;
vstate.recursion_limit = C_RECURSION_LIMIT * COMPILER_STACK_FRAME_SCALE;
vstate.recursion_limit = C_RECURSION_LIMIT;
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining;
starting_recursion_depth = recursion_depth * COMPILER_STACK_FRAME_SCALE;
starting_recursion_depth = recursion_depth;
vstate.recursion_depth = starting_recursion_depth;
PyObject *result = ast2obj_mod(state, &vstate, t);
Expand Down
5 changes: 2 additions & 3 deletions Python/Python-ast.c

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

7 changes: 2 additions & 5 deletions Python/ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -1038,9 +1038,6 @@ validate_type_params(struct validator *state, asdl_type_param_seq *tps)
}


/* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 2

int
_PyAST_Validate(mod_ty mod)
{
Expand All @@ -1057,9 +1054,9 @@ _PyAST_Validate(mod_ty mod)
}
/* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining;
starting_recursion_depth = recursion_depth * COMPILER_STACK_FRAME_SCALE;
starting_recursion_depth = recursion_depth;
state.recursion_depth = starting_recursion_depth;
state.recursion_limit = C_RECURSION_LIMIT * COMPILER_STACK_FRAME_SCALE;
state.recursion_limit = C_RECURSION_LIMIT;

switch (mod->kind) {
case Module_kind:
Expand Down
7 changes: 2 additions & 5 deletions Python/ast_opt.c
Original file line number Diff line number Diff line change
Expand Up @@ -1102,9 +1102,6 @@ astfold_type_param(type_param_ty node_, PyArena *ctx_, _PyASTOptimizeState *stat
#undef CALL_OPT
#undef CALL_SEQ

/* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 2

int
_PyAST_Optimize(mod_ty mod, PyArena *arena, _PyASTOptimizeState *state)
{
Expand All @@ -1118,9 +1115,9 @@ _PyAST_Optimize(mod_ty mod, PyArena *arena, _PyASTOptimizeState *state)
}
/* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining;
starting_recursion_depth = recursion_depth * COMPILER_STACK_FRAME_SCALE;
starting_recursion_depth = recursion_depth;
state->recursion_depth = starting_recursion_depth;
state->recursion_limit = C_RECURSION_LIMIT * COMPILER_STACK_FRAME_SCALE;
state->recursion_limit = C_RECURSION_LIMIT;

int ret = astfold_mod(mod, arena, state);
assert(ret || PyErr_Occurred());
Expand Down
9 changes: 2 additions & 7 deletions Python/symtable.c
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,6 @@ symtable_new(void)
return NULL;
}

/* 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 2

struct symtable *
_PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
{
Expand All @@ -312,9 +307,9 @@ _PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
}
/* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining;
starting_recursion_depth = recursion_depth * COMPILER_STACK_FRAME_SCALE;
starting_recursion_depth = recursion_depth;
st->recursion_depth = starting_recursion_depth;
st->recursion_limit = C_RECURSION_LIMIT * COMPILER_STACK_FRAME_SCALE;
st->recursion_limit = C_RECURSION_LIMIT;

/* Make the initial symbol information gathering pass */
if (!symtable_enter_block(st, &_Py_ID(top), ModuleBlock, (void *)mod, 0, 0, 0, 0)) {
Expand Down

0 comments on commit 4d87832

Please sign in to comment.