[3.12] GH-112215: Backport C recursion changes (GH-115083)

This commit is contained in:
Mark Shannon 2024-02-13 09:45:59 +00:00 committed by GitHub
parent a30bb080dc
commit 4d87832d87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 65 additions and 46 deletions

View file

@ -251,12 +251,24 @@ struct _ts {
/* WASI has limited call stack. Python's recursion limit depends on code /* WASI has limited call stack. Python's recursion limit depends on code
layout, optimization, and WASI runtime. Wasmtime can handle about 700 layout, optimization, and WASI runtime. Wasmtime can handle about 700
recursions, sometimes less. 500 is a more conservative limit. */ recursions, sometimes less. 500 is a more conservative limit. */
#ifndef C_RECURSION_LIMIT #ifdef Py_DEBUG
# ifdef __wasi__ # if defined(__wasi__)
# define C_RECURSION_LIMIT 500 # define C_RECURSION_LIMIT 150
# else # else
// This value is duplicated in Lib/test/support/__init__.py # define C_RECURSION_LIMIT 500
# define C_RECURSION_LIMIT 1500 # 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 10000
# endif # endif
#endif #endif

View file

@ -2112,13 +2112,13 @@ def set_recursion_limit(limit):
finally: finally:
sys.setrecursionlimit(original_limit) sys.setrecursionlimit(original_limit)
def infinite_recursion(max_depth=100): def infinite_recursion(max_depth=None):
"""Set a lower limit for tests that interact with infinite recursions if max_depth is None:
(e.g test_ast.ASTHelpers_Test.test_recursion_direct) since on some # Pick a number large enough to cause problems
debug windows builds, due to not enough functions being inlined the # but not take too long for code that can handle
stack size might not handle the default recursion limit (1000). See # very deep recursion.
bpo-11105 for details.""" max_depth = 20_000
if max_depth < 3: elif max_depth < 3:
raise ValueError("max_depth must be at least 3, got {max_depth}") raise ValueError("max_depth must be at least 3, got {max_depth}")
depth = get_recursion_depth() depth = get_recursion_depth()
depth = max(depth - 1, 1) # Ignore infinite_recursion() frame. depth = max(depth - 1, 1) # Ignore infinite_recursion() frame.
@ -2362,7 +2362,22 @@ def adjust_int_max_str_digits(max_digits):
EXCEEDS_RECURSION_LIMIT = 5000 EXCEEDS_RECURSION_LIMIT = 5000
# The default C recursion limit (from Include/cpython/pystate.h). # 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. #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', skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',

View file

@ -1087,9 +1087,9 @@ class AST_Tests(unittest.TestCase):
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
@support.cpython_only @support.cpython_only
def test_ast_recursion_limit(self): def test_ast_recursion_limit(self):
fail_depth = support.EXCEEDS_RECURSION_LIMIT fail_depth = support.C_RECURSION_LIMIT + 1
crash_depth = 100_000 crash_depth = 100_000
success_depth = 1200 success_depth = int(support.C_RECURSION_LIMIT * 0.9)
def check_limit(prefix, repeated): def check_limit(prefix, repeated):
expect_ok = prefix + repeated * success_depth expect_ok = prefix + repeated * success_depth

View file

@ -1,5 +1,5 @@
import unittest 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: try:
import _testcapi import _testcapi
except ImportError: except ImportError:
@ -932,6 +932,7 @@ class TestErrorMessagesUseQualifiedName(unittest.TestCase):
class TestRecursion(unittest.TestCase): class TestRecursion(unittest.TestCase):
@skip_on_s390x @skip_on_s390x
@unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
def test_super_deep(self): def test_super_deep(self):
def recurse(n): def recurse(n):

View file

@ -607,9 +607,9 @@ class TestSpecifics(unittest.TestCase):
# Expected limit is C_RECURSION_LIMIT * 2 # Expected limit is C_RECURSION_LIMIT * 2
# Duplicating the limit here is a little ugly. # Duplicating the limit here is a little ugly.
# Perhaps it should be exposed somewhere... # 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 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"): def check_limit(prefix, repeated, mode="single"):
expect_ok = prefix + repeated * success_depth expect_ok = prefix + repeated * success_depth

View file

@ -352,7 +352,7 @@ def blowstack(fxn, arg, compare_to):
# Make sure that calling isinstance with a deeply nested tuple for its # Make sure that calling isinstance with a deeply nested tuple for its
# argument will raise RecursionError eventually. # argument will raise RecursionError eventually.
tuple_arg = (compare_to,) 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,) tuple_arg = (tuple_arg,)
fxn(arg, tuple_arg) fxn(arg, tuple_arg)

View file

@ -908,7 +908,7 @@ class TestBinaryPlistlib(unittest.TestCase):
self.assertIs(b['x'], b) self.assertIs(b['x'], b)
def test_deep_nesting(self): 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: for N in tests:
chunks = [b'\xa1' + (i + 1).to_bytes(4, 'big') for i in range(N)] chunks = [b'\xa1' + (i + 1).to_bytes(4, 'big') for i in range(N)]
try: try:

View file

@ -2965,16 +2965,18 @@ class TestExtendedArgs(unittest.TestCase):
self.assertEqual(counts, {'call': 1, 'line': 301, 'return': 1}) self.assertEqual(counts, {'call': 1, 'line': 301, 'return': 1})
def test_trace_lots_of_globals(self): def test_trace_lots_of_globals(self):
count = min(1000, int(support.C_RECURSION_LIMIT * 0.8))
code = """if 1: code = """if 1:
def f(): def f():
return ( return (
{} {}
) )
""".format("\n+\n".join(f"var{i}\n" 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(1000)} ns = {f"var{i}": i for i in range(count)}
exec(code, ns) exec(code, ns)
counts = self.count_traces(ns["f"]) 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): class TestEdgeCases(unittest.TestCase):

View file

@ -0,0 +1,2 @@
Change the C recursion limits to more closely reflect the underlying
platform limits.

View file

@ -1393,15 +1393,14 @@ PyObject* PyAST_mod2obj(mod_ty t)
int starting_recursion_depth; int starting_recursion_depth;
/* Be careful here to prevent overflow. */ /* Be careful here to prevent overflow. */
int COMPILER_STACK_FRAME_SCALE = 2;
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) { if (!tstate) {
return NULL; return NULL;
} }
struct validator vstate; 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; 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; vstate.recursion_depth = starting_recursion_depth;
PyObject *result = ast2obj_mod(state, &vstate, t); PyObject *result = ast2obj_mod(state, &vstate, t);

5
Python/Python-ast.c generated
View file

@ -13152,15 +13152,14 @@ PyObject* PyAST_mod2obj(mod_ty t)
int starting_recursion_depth; int starting_recursion_depth;
/* Be careful here to prevent overflow. */ /* Be careful here to prevent overflow. */
int COMPILER_STACK_FRAME_SCALE = 2;
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) { if (!tstate) {
return NULL; return NULL;
} }
struct validator vstate; 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; 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; vstate.recursion_depth = starting_recursion_depth;
PyObject *result = ast2obj_mod(state, &vstate, t); PyObject *result = ast2obj_mod(state, &vstate, t);

View file

@ -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 int
_PyAST_Validate(mod_ty mod) _PyAST_Validate(mod_ty mod)
{ {
@ -1057,9 +1054,9 @@ _PyAST_Validate(mod_ty mod)
} }
/* Be careful here to prevent overflow. */ /* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining; 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_depth = starting_recursion_depth;
state.recursion_limit = C_RECURSION_LIMIT * COMPILER_STACK_FRAME_SCALE; state.recursion_limit = C_RECURSION_LIMIT;
switch (mod->kind) { switch (mod->kind) {
case Module_kind: case Module_kind:

View file

@ -1102,9 +1102,6 @@ astfold_type_param(type_param_ty node_, PyArena *ctx_, _PyASTOptimizeState *stat
#undef CALL_OPT #undef CALL_OPT
#undef CALL_SEQ #undef CALL_SEQ
/* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 2
int int
_PyAST_Optimize(mod_ty mod, PyArena *arena, _PyASTOptimizeState *state) _PyAST_Optimize(mod_ty mod, PyArena *arena, _PyASTOptimizeState *state)
{ {
@ -1118,9 +1115,9 @@ _PyAST_Optimize(mod_ty mod, PyArena *arena, _PyASTOptimizeState *state)
} }
/* Be careful here to prevent overflow. */ /* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining; 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_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); int ret = astfold_mod(mod, arena, state);
assert(ret || PyErr_Occurred()); assert(ret || PyErr_Occurred());

View file

@ -281,11 +281,6 @@ symtable_new(void)
return NULL; 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 * struct symtable *
_PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future) _PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
{ {
@ -312,9 +307,9 @@ _PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)
} }
/* Be careful here to prevent overflow. */ /* Be careful here to prevent overflow. */
int recursion_depth = C_RECURSION_LIMIT - tstate->c_recursion_remaining; 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_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 */ /* Make the initial symbol information gathering pass */
if (!symtable_enter_block(st, &_Py_ID(top), ModuleBlock, (void *)mod, 0, 0, 0, 0)) { if (!symtable_enter_block(st, &_Py_ID(top), ModuleBlock, (void *)mod, 0, 0, 0, 0)) {