]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-107263: Increase C stack limit for most functions, except `_PyEval_EvalFrameDefaul...
authorMark Shannon <mark@hotpy.org>
Fri, 4 Aug 2023 09:10:29 +0000 (10:10 +0100)
committerGitHub <noreply@github.com>
Fri, 4 Aug 2023 09:10:29 +0000 (10:10 +0100)
* Set C recursion limit to 1500, set cost of eval loop to 2 frames, and compiler mutliply to 2.

21 files changed:
Doc/whatsnew/3.12.rst
Include/cpython/pystate.h
Lib/test/list_tests.py
Lib/test/mapping_tests.py
Lib/test/support/__init__.py
Lib/test/test_ast.py
Lib/test/test_call.py
Lib/test/test_compile.py
Lib/test/test_dict.py
Lib/test/test_dictviews.py
Lib/test/test_exception_group.py
Lib/test/test_zlib.py
Misc/NEWS.d/next/Core and Builtins/2023-07-30-05-20-16.gh-issue-107263.q0IU2M.rst [new file with mode: 0644]
Parser/asdl_c.py
Python/Python-ast.c
Python/ast.c
Python/ast_opt.c
Python/bytecodes.c
Python/ceval.c
Python/generated_cases.c.h
Python/symtable.c

index 3c39476ced9f48f9fc9c4a69c17b3f1aaa6062e5..6553013552d00381eb36b91e813762e3f658400f 100644 (file)
@@ -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
 --------
 
index 30de4ee4b6c58cfe20743dc4e66289589cec8c33..56e473cc5e42d55aee6823f7853a73deb97cbd77 100644 (file)
@@ -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 1500
 #  endif
 #endif
 
index fe3ee80b8d461f547e8501ac3b07fee480585e8d..b1ef332522d2ce5cfefb1fb3638d7c326ea77f5c 100644 (file)
@@ -6,7 +6,7 @@ import sys
 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):
@@ -61,7 +61,7 @@ class CommonTest(seq_tests.CommonTest):
 
     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)
 
index 613206a0855aea858819afc51becb874f558a4bc..5492bbf86d1f879fba9000a58d36d085e0a2f7cb 100644 (file)
@@ -2,6 +2,7 @@
 import unittest
 import collections
 import sys
+from test.support import C_RECURSION_LIMIT
 
 
 class BasicTestMappingProtocol(unittest.TestCase):
@@ -624,7 +625,7 @@ class TestHashMappingProtocol(TestMappingProtocol):
 
     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
index 2a59911e54d6b9ad419043718d645949e0a0f5ca..64c66d8e25d9cdc918dacdcbd6ace1a7fd5d4030 100644 (file)
@@ -64,7 +64,8 @@ __all__ = [
     "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",
+    "skip_on_s390x",
     ]
 
 
@@ -2460,3 +2461,10 @@ 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 = 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')
index a03fa4c7187b05295d6a34423d2fa5cb10691957..5346b39043f0f5c156e36887a7fd0368eba700eb 100644 (file)
@@ -1084,6 +1084,7 @@ class AST_Tests(unittest.TestCase):
                     return self
         enum._test_simple_enum(_Precedence, ast._Precedence)
 
+    @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
index 09a531f8cc627b044bdbcd0a9ef38891be20b3ee..c3c3b1853b573666db568534900f4d74e4dc73c4 100644 (file)
@@ -1,5 +1,5 @@
 import unittest
-from test.support import cpython_only, requires_limited_api
+from test.support import cpython_only, requires_limited_api, skip_on_s390x
 try:
     import _testcapi
 except ImportError:
@@ -918,6 +918,7 @@ class TestErrorMessagesUseQualifiedName(unittest.TestCase):
 @cpython_only
 class TestRecursion(unittest.TestCase):
 
+    @skip_on_s390x
     def test_super_deep(self):
 
         def recurse(n):
index 85ce0a4b39d8547da2d79e547da0d7c2210a4b95..770184c5ef9a91b2e3c16658f57e207e5786e550 100644 (file)
@@ -11,10 +11,9 @@ import textwrap
 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):
@@ -112,7 +111,7 @@ class TestSpecifics(unittest.TestCase):
 
     @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('''
@@ -558,16 +557,12 @@ class TestSpecifics(unittest.TestCase):
     @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 * 2
+        # Duplicating the limit here is a little ugly.
+        # Perhaps it should be exposed somewhere...
+        fail_depth = C_RECURSION_LIMIT * 2 + 1
+        crash_depth = C_RECURSION_LIMIT * 100
+        success_depth = int(C_RECURSION_LIMIT * 1.8)
 
         def check_limit(prefix, repeated, mode="single"):
             expect_ok = prefix + repeated * success_depth
index 79638340059f650d03dba441d26ae8bbf75832a0..fbc6ce8282de3cefbbb2d92ebec35b0060d9f792 100644 (file)
@@ -8,7 +8,7 @@ import sys
 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):
@@ -596,7 +596,7 @@ class DictTest(unittest.TestCase):
 
     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)
 
index 924f4a6829e19c3f16cfad7bba70135611bdff23..2bd9d6eef8cfc617407932faf6228b478caa6f77 100644 (file)
@@ -3,6 +3,7 @@ import copy
 import pickle
 import sys
 import unittest
+from test.support import C_RECURSION_LIMIT
 
 class DictSetTest(unittest.TestCase):
 
@@ -279,7 +280,7 @@ class DictSetTest(unittest.TestCase):
 
     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)
 
index 2658e027ff3e2b34042f4bbab24117329e54d8dd..a02d54da35e948ab54932dd6899610058e88a8c8 100644 (file)
@@ -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):
@@ -460,7 +460,7 @@ class ExceptionGroupSplitTests(ExceptionGroupTestBase):
 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
 
index 3dac70eb12852cd82fe032f51ea9f0014d92ccfc..2113757254c0ed920db133f9107399a914a9f62d 100644 (file)
@@ -7,7 +7,7 @@ import os
 import pickle
 import random
 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')
@@ -44,10 +44,7 @@ requires_Decompress_copy = unittest.skipUnless(
 #   zlib.decompress(func1(data)) == zlib.decompress(func2(data)) == data
 #
 # 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.
-skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
-                                'skipped on s390x')
-
+# condition.
 
 class VersionTestCase(unittest.TestCase):
 
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-07-30-05-20-16.gh-issue-107263.q0IU2M.rst b/Misc/NEWS.d/next/Core and Builtins/2023-07-30-05-20-16.gh-issue-107263.q0IU2M.rst
new file mode 100644 (file)
index 0000000..fb0940b
--- /dev/null
@@ -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
index f159b573ce47274a27ad12e07d4a86f65f7ae1ed..2a36610527f8987b66c9e80a570c10086c8aff78 100755 (executable)
@@ -1393,7 +1393,7 @@ PyObject* PyAST_mod2obj(mod_ty t)
 
     int starting_recursion_depth;
     /* Be careful here to prevent overflow. */
-    int COMPILER_STACK_FRAME_SCALE = 3;
+    int COMPILER_STACK_FRAME_SCALE = 2;
     PyThreadState *tstate = _PyThreadState_GET();
     if (!tstate) {
         return 0;
index 412de79397477b7d2cafab51eac49b0b45345438..8047b1259c5d8660a1268cd4298049600449884d 100644 (file)
@@ -13073,7 +13073,7 @@ PyObject* PyAST_mod2obj(mod_ty t)
 
     int starting_recursion_depth;
     /* Be careful here to prevent overflow. */
-    int COMPILER_STACK_FRAME_SCALE = 3;
+    int COMPILER_STACK_FRAME_SCALE = 2;
     PyThreadState *tstate = _PyThreadState_GET();
     if (!tstate) {
         return 0;
index 68600ce683b97498a6d524ee9c037d317b1095d8..74c97f948d15e63d5b02fade7b09d7188218adc4 100644 (file)
@@ -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 2
 
 int
 _PyAST_Validate(mod_ty mod)
index ad1e312b084b747ff610de1a208aae62f178106d..82e7559e5b629a670ff47e6fa7fe4ec34e37dea3 100644 (file)
@@ -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 2
 
 int
 _PyAST_Optimize(mod_ty mod, PyArena *arena, int optimize, int ff_features)
index 0bea7b59597a26c6a29fd17e1e67ac35e7f73735..90e26d3c86b380a57238509c7d9dddc3319b8794 100644 (file)
@@ -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;
         }
 
index 369b9a69152a5c14613eb93040080bdf82f0dd2c..b85e9677747afb53d23cbea5d454642b6808fe76 100644 (file)
@@ -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 2
+
 PyObject* _Py_HOT_FUNCTION
 _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int throwflag)
 {
@@ -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--;
@@ -907,7 +913,7 @@ exit_unwind:
         /* 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;
     }
 
index e43b3dedf14f5d14d679ee40f1bc84f17f278f5e..9fa549a1203245eb5ae66915d9200ecffddc247c 100644 (file)
             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;
         }
 
index 04be3192d6c7c41aaf4639ecaab798ddf019c258..e9adbd5d29b1f91d40560d02128470ee7c1311e4 100644 (file)
@@ -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 2
 
 struct symtable *
 _PySymtable_Build(mod_ty mod, PyObject *filename, PyFutureFeatures *future)