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

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

* Set C recursion limit to 1500, set cost of eval loop to 2 frames, and compiler mutliply to 2.
(cherry picked from commit fa45958450)

Co-authored-by: Mark Shannon <mark@hotpy.org>
This commit is contained in:
Miss Islington (bot) 2023-08-04 03:25:51 -07:00 committed by GitHub
parent 58af2293c5
commit 98902d6c05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 57 additions and 45 deletions

View file

@ -802,6 +802,11 @@ sys
exception instance, rather than to a ``(typ, exc, tb)`` tuple. exception instance, rather than to a ``(typ, exc, tb)`` tuple.
(Contributed by Irit Katriel in :gh:`103176`.) (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 tempfile
-------- --------

View file

@ -255,7 +255,8 @@ struct _ts {
# ifdef __wasi__ # ifdef __wasi__
# define C_RECURSION_LIMIT 500 # define C_RECURSION_LIMIT 500
# else # else
# define C_RECURSION_LIMIT 800 // This value is duplicated in Lib/test/support/__init__.py
# define C_RECURSION_LIMIT 1500
# endif # endif
#endif #endif

View file

@ -6,7 +6,7 @@ import sys
from functools import cmp_to_key from functools import cmp_to_key
from test import seq_tests 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): class CommonTest(seq_tests.CommonTest):
@ -61,7 +61,7 @@ class CommonTest(seq_tests.CommonTest):
def test_repr_deep(self): def test_repr_deep(self):
a = self.type2test([]) a = self.type2test([])
for i in range(sys.getrecursionlimit() + 100): for i in range(C_RECURSION_LIMIT + 1):
a = self.type2test([a]) a = self.type2test([a])
self.assertRaises(RecursionError, repr, a) self.assertRaises(RecursionError, repr, a)

View file

@ -2,6 +2,7 @@
import unittest import unittest
import collections import collections
import sys import sys
from test.support import C_RECURSION_LIMIT
class BasicTestMappingProtocol(unittest.TestCase): class BasicTestMappingProtocol(unittest.TestCase):
@ -624,7 +625,7 @@ class TestHashMappingProtocol(TestMappingProtocol):
def test_repr_deep(self): def test_repr_deep(self):
d = self._empty_mapping() d = self._empty_mapping()
for i in range(sys.getrecursionlimit() + 100): for i in range(C_RECURSION_LIMIT + 1):
d0 = d d0 = d
d = self._empty_mapping() d = self._empty_mapping()
d[1] = d0 d[1] = d0

View file

@ -64,7 +64,8 @@ __all__ = [
"run_with_tz", "PGO", "missing_compiler_executable", "run_with_tz", "PGO", "missing_compiler_executable",
"ALWAYS_EQ", "NEVER_EQ", "LARGEST", "SMALLEST", "ALWAYS_EQ", "NEVER_EQ", "LARGEST", "SMALLEST",
"LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT", "LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT",
"Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "C_RECURSION_LIMIT",
"skip_on_s390x",
] ]
@ -2460,3 +2461,10 @@ def adjust_int_max_str_digits(max_digits):
#For recursion tests, easily exceeds default recursion limit #For recursion tests, easily exceeds default recursion limit
EXCEEDS_RECURSION_LIMIT = 5000 EXCEEDS_RECURSION_LIMIT = 5000
# The default C recursion limit (from Include/cpython/pystate.h).
C_RECURSION_LIMIT = 1500
#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',
'skipped on s390x')

View file

@ -1084,6 +1084,7 @@ class AST_Tests(unittest.TestCase):
return self return self
enum._test_simple_enum(_Precedence, ast._Precedence) enum._test_simple_enum(_Precedence, ast._Precedence)
@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.EXCEEDS_RECURSION_LIMIT

View file

@ -1,5 +1,5 @@
import unittest import unittest
from test.support import cpython_only, requires_limited_api from test.support import cpython_only, requires_limited_api, skip_on_s390x
try: try:
import _testcapi import _testcapi
except ImportError: except ImportError:
@ -931,6 +931,7 @@ class TestErrorMessagesUseQualifiedName(unittest.TestCase):
@cpython_only @cpython_only
class TestRecursion(unittest.TestCase): class TestRecursion(unittest.TestCase):
@skip_on_s390x
def test_super_deep(self): def test_super_deep(self):
def recurse(n): def recurse(n):

View file

@ -11,10 +11,9 @@ import textwrap
import warnings import warnings
from test import support from test import support
from test.support import (script_helper, requires_debug_ranges, from test.support import (script_helper, requires_debug_ranges,
requires_specialization) requires_specialization, C_RECURSION_LIMIT)
from test.support.os_helper import FakePath from test.support.os_helper import FakePath
class TestSpecifics(unittest.TestCase): class TestSpecifics(unittest.TestCase):
def compile_single(self, source): def compile_single(self, source):
@ -112,7 +111,7 @@ class TestSpecifics(unittest.TestCase):
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_extended_arg(self): def test_extended_arg(self):
repeat = 2000 repeat = int(C_RECURSION_LIMIT * 0.9)
longexpr = 'x = x or ' + '-x' * repeat longexpr = 'x = x or ' + '-x' * repeat
g = {} g = {}
code = textwrap.dedent(''' code = textwrap.dedent('''
@ -558,16 +557,12 @@ class TestSpecifics(unittest.TestCase):
@support.cpython_only @support.cpython_only
@unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI") @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
def test_compiler_recursion_limit(self): def test_compiler_recursion_limit(self):
# Expected limit is sys.getrecursionlimit() * the scaling factor # Expected limit is C_RECURSION_LIMIT * 2
# in symtable.c (currently 3) # Duplicating the limit here is a little ugly.
# We expect to fail *at* that limit, because we use up some of # Perhaps it should be exposed somewhere...
# the stack depth limit in the test suite code fail_depth = C_RECURSION_LIMIT * 2 + 1
# So we check the expected limit and 75% of that crash_depth = C_RECURSION_LIMIT * 100
# XXX (ncoghlan): duplicating the scaling factor here is a little success_depth = int(C_RECURSION_LIMIT * 1.8)
# ugly. Perhaps it should be exposed somewhere...
fail_depth = sys.getrecursionlimit() * 3
crash_depth = sys.getrecursionlimit() * 300
success_depth = int(fail_depth * 0.75)
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

@ -8,7 +8,7 @@ import sys
import unittest import unittest
import weakref import weakref
from test import support from test import support
from test.support import import_helper from test.support import import_helper, C_RECURSION_LIMIT
class DictTest(unittest.TestCase): class DictTest(unittest.TestCase):
@ -596,7 +596,7 @@ class DictTest(unittest.TestCase):
def test_repr_deep(self): def test_repr_deep(self):
d = {} d = {}
for i in range(sys.getrecursionlimit() + 100): for i in range(C_RECURSION_LIMIT + 1):
d = {1: d} d = {1: d}
self.assertRaises(RecursionError, repr, d) self.assertRaises(RecursionError, repr, d)

View file

@ -3,6 +3,7 @@ import copy
import pickle import pickle
import sys import sys
import unittest import unittest
from test.support import C_RECURSION_LIMIT
class DictSetTest(unittest.TestCase): class DictSetTest(unittest.TestCase):
@ -279,7 +280,7 @@ class DictSetTest(unittest.TestCase):
def test_deeply_nested_repr(self): def test_deeply_nested_repr(self):
d = {} d = {}
for i in range(sys.getrecursionlimit() + 100): for i in range(C_RECURSION_LIMIT//2 + 100):
d = {42: d.values()} d = {42: d.values()}
self.assertRaises(RecursionError, repr, d) self.assertRaises(RecursionError, repr, d)

View file

@ -1,7 +1,7 @@
import collections.abc import collections.abc
import types import types
import unittest import unittest
from test.support import C_RECURSION_LIMIT
class TestExceptionGroupTypeHierarchy(unittest.TestCase): class TestExceptionGroupTypeHierarchy(unittest.TestCase):
def test_exception_group_types(self): def test_exception_group_types(self):
@ -433,7 +433,7 @@ class ExceptionGroupSplitTests(ExceptionGroupTestBase):
class DeepRecursionInSplitAndSubgroup(unittest.TestCase): class DeepRecursionInSplitAndSubgroup(unittest.TestCase):
def make_deep_eg(self): def make_deep_eg(self):
e = TypeError(1) e = TypeError(1)
for i in range(2000): for i in range(C_RECURSION_LIMIT + 1):
e = ExceptionGroup('eg', [e]) e = ExceptionGroup('eg', [e])
return e return e

View file

@ -7,7 +7,7 @@ import os
import pickle import pickle
import random import random
import sys import sys
from test.support import bigmemtest, _1G, _4G from test.support import bigmemtest, _1G, _4G, skip_on_s390x
zlib = import_helper.import_module('zlib') zlib = import_helper.import_module('zlib')
@ -44,10 +44,7 @@ requires_Decompress_copy = unittest.skipUnless(
# zlib.decompress(func1(data)) == zlib.decompress(func2(data)) == data # zlib.decompress(func1(data)) == zlib.decompress(func2(data)) == data
# #
# Make the assumption that s390x always has an accelerator to simplify the skip # Make the assumption that s390x always has an accelerator to simplify the skip
# condition. Windows doesn't have os.uname() but it doesn't support s390x. # condition.
skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
'skipped on s390x')
class VersionTestCase(unittest.TestCase): class VersionTestCase(unittest.TestCase):

View file

@ -0,0 +1,3 @@
Increase C recursion limit for functions other than the main interpreter
from 800 to 1500. This should allow functions like ``list.__repr__`` and
``json.dumps`` to handle all the inputs that they could prior to 3.12

View file

@ -1393,7 +1393,7 @@ 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 = 3; int COMPILER_STACK_FRAME_SCALE = 2;
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) { if (!tstate) {
return 0; return 0;

2
Python/Python-ast.c generated
View file

@ -13074,7 +13074,7 @@ 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 = 3; int COMPILER_STACK_FRAME_SCALE = 2;
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
if (!tstate) { if (!tstate) {
return 0; return 0;

View file

@ -1029,7 +1029,7 @@ validate_type_params(struct validator *state, asdl_type_param_seq *tps)
/* See comments in symtable.c. */ /* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 3 #define COMPILER_STACK_FRAME_SCALE 2
int int
_PyAST_Validate(mod_ty mod) _PyAST_Validate(mod_ty mod)

View file

@ -1103,7 +1103,7 @@ astfold_type_param(type_param_ty node_, PyArena *ctx_, _PyASTOptimizeState *stat
#undef CALL_SEQ #undef CALL_SEQ
/* See comments in symtable.c. */ /* See comments in symtable.c. */
#define COMPILER_STACK_FRAME_SCALE 3 #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)

View file

@ -635,7 +635,7 @@ dummy_func(
tstate->cframe = cframe.previous; tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous); assert(tstate->cframe->current_frame == frame->previous);
assert(!_PyErr_Occurred(tstate)); assert(!_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallTstate(tstate); tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return retval; return retval;
} }

View file

@ -637,6 +637,11 @@ static inline void _Py_LeaveRecursiveCallPy(PyThreadState *tstate) {
# pragma warning(disable:4102) # pragma warning(disable:4102)
#endif #endif
/* _PyEval_EvalFrameDefault() is a *big* function,
* so consume 3 units of C stack */
#define PY_EVAL_C_STACK_UNITS 2
PyObject* _Py_HOT_FUNCTION PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag) _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
{ {
@ -691,6 +696,7 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
frame->previous = &entry_frame; frame->previous = &entry_frame;
cframe.current_frame = frame; cframe.current_frame = frame;
tstate->c_recursion_remaining -= (PY_EVAL_C_STACK_UNITS - 1);
if (_Py_EnterRecursiveCallTstate(tstate, "")) { if (_Py_EnterRecursiveCallTstate(tstate, "")) {
tstate->c_recursion_remaining--; tstate->c_recursion_remaining--;
tstate->py_recursion_remaining--; tstate->py_recursion_remaining--;
@ -990,7 +996,7 @@ exit_unwind:
/* Restore previous cframe and exit */ /* Restore previous cframe and exit */
tstate->cframe = cframe.previous; tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous); assert(tstate->cframe->current_frame == frame->previous);
_Py_LeaveRecursiveCallTstate(tstate); tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return NULL; return NULL;
} }

View file

@ -922,7 +922,7 @@
tstate->cframe = cframe.previous; tstate->cframe = cframe.previous;
assert(tstate->cframe->current_frame == frame->previous); assert(tstate->cframe->current_frame == frame->previous);
assert(!_PyErr_Occurred(tstate)); assert(!_PyErr_Occurred(tstate));
_Py_LeaveRecursiveCallTstate(tstate); tstate->c_recursion_remaining += PY_EVAL_C_STACK_UNITS;
return retval; return retval;
#line 928 "Python/generated_cases.c.h" #line 928 "Python/generated_cases.c.h"
} }

View file

@ -282,17 +282,10 @@ symtable_new(void)
return NULL; return NULL;
} }
/* When compiling the use of C stack is probably going to be a lot /* Using a scaling factor means this should automatically adjust when
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
the recursion limit is adjusted for small or large C stack allocations. the recursion limit is adjusted for small or large C stack allocations.
*/ */
#define COMPILER_STACK_FRAME_SCALE 3 #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)