https://github.com/python/cpython/commit/a739f8061b5456643c1a65bab9813555c2612829
commit: a739f8061b5456643c1a65bab9813555c2612829
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-20T09:23:42Z
summary:

[3.14] gh-154211: Skip C-stack exhaustion tests when the C stack is huge 
(GH-154212) (GH-154231)

(cherry picked from commit 863b31268c3160af1febcee4c79ff6ed380f328f)

Co-authored-by: Claude Fable 5 <[email protected]>

files:
A Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
M Lib/test/list_tests.py
M Lib/test/mapping_tests.py
M Lib/test/support/__init__.py
M Lib/test/test_ast/test_ast.py
M Lib/test/test_call.py
M Lib/test/test_class.py
M Lib/test/test_compile.py
M Lib/test/test_copy.py
M Lib/test/test_ctypes/test_as_parameter.py
M Lib/test/test_descr.py
M Lib/test/test_dict.py
M Lib/test/test_dictviews.py
M Lib/test/test_exception_group.py
M Lib/test/test_exceptions.py
M Lib/test/test_functools.py
M Lib/test/test_isinstance.py
M Lib/test/test_json/test_recursion.py
M Lib/test/test_pyexpat.py
M Lib/test/test_xml_etree.py

diff --git a/Lib/test/list_tests.py b/Lib/test/list_tests.py
index e76f79c274e744a..ec2aa59f2cb8728 100644
--- a/Lib/test/list_tests.py
+++ b/Lib/test/list_tests.py
@@ -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, skip_if_huge_c_stack
 from test.support import skip_emscripten_stack_overflow, 
skip_wasi_stack_overflow
 
 
@@ -60,6 +60,7 @@ def test_repr(self):
         self.assertEqual(str(a2), "[0, 1, 2, [...], 3]")
         self.assertEqual(repr(a2), "[0, 1, 2, [...], 3]")
 
+    @skip_if_huge_c_stack(200_000)
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_repr_deep(self):
diff --git a/Lib/test/mapping_tests.py b/Lib/test/mapping_tests.py
index 20306e1526d7b84..b95affd9f78479b 100644
--- a/Lib/test/mapping_tests.py
+++ b/Lib/test/mapping_tests.py
@@ -622,6 +622,7 @@ def __repr__(self):
         d = self._full_mapping({1: BadRepr()})
         self.assertRaises(Exc, repr, d)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     @support.skip_if_sanitizer("requires deep stack", ub=True)
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index c401e18247e6017..005ab19a93d941c 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -44,7 +44,7 @@
     "check__all__", "skip_if_buggy_ucrt_strfptime",
     "check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
     "requires_limited_api", "requires_specialization", "thread_unsafe",
-    "skip_if_unlimited_stack_size",
+    "skip_if_unlimited_stack_size", "skip_if_huge_c_stack",
     # sys
     "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi",
     "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval",
@@ -2743,6 +2743,32 @@ def exceeds_recursion_limit():
     return 150_000
 
 
+def skip_if_huge_c_stack(depth=150_000):
+    """Skip decorator for tests which cannot overflow the C stack.
+
+    Tests exhausting the C stack with *depth* recursive calls cannot
+    trigger the recursion protection if the C stack is too large (e.g.
+    with a large or unlimited RLIMIT_STACK), and either fail, or run
+    for a very long time, or crash, or consume all memory.
+    """
+    try:
+        from _testinternalcapi import get_c_recursion_remaining
+    except ImportError:
+        # Fall back to checking for an unlimited stack size.
+        huge = False
+        if not (is_emscripten or is_wasi) and os.name != "nt":
+            import resource
+            soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
+            huge = soft == hard and soft in (-1, 0xFFFF_FFFF_FFFF_FFFF)
+    else:
+        remaining = get_c_recursion_remaining()
+        # A negative value means integer overflow in the estimate
+        # (e.g. with an unlimited RLIMIT_STACK).
+        huge = remaining >= depth or remaining < 0
+    return unittest.skipIf(
+        huge, f"the C stack is large enough for {depth} recursive calls")
+
+
 # Windows doesn't have os.uname() but it doesn't support s390x.
 is_s390x = hasattr(os, 'uname') and os.uname().machine == 's390x'
 skip_on_s390x = unittest.skipIf(is_s390x, 'skipped on s390x')
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index ef12ce0ce37de8e..3f607a00d2508e0 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -24,7 +24,9 @@
 
 from test import support
 from test.support import os_helper
-from test.support import skip_emscripten_stack_overflow, 
skip_wasi_stack_overflow, skip_if_unlimited_stack_size
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow,
+                          skip_if_unlimited_stack_size, skip_if_huge_c_stack)
 from test.support.ast_helper import ASTTestMixin
 from test.support.import_helper import ensure_lazy_imports
 from test.test_ast.utils import to_tuple
@@ -988,7 +990,7 @@ def next(self):
         enum._test_simple_enum(_Precedence, _ast_unparse._Precedence)
 
     @support.cpython_only
-    @skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 
500_000)
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_ast_recursion_limit(self):
@@ -1960,7 +1962,7 @@ def test_level_as_none(self):
         exec(code, ns)
         self.assertIn('sleep', ns)
 
-    @skip_if_unlimited_stack_size
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursion_direct(self):
         e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, 
operand=ast.Constant(1))
@@ -1969,7 +1971,7 @@ def test_recursion_direct(self):
             with support.infinite_recursion():
                 compile(ast.Expression(e), "<test>", "eval")
 
-    @skip_if_unlimited_stack_size
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursion_indirect(self):
         e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, 
operand=ast.Constant(1))
diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py
index b18f16a6efcf84a..d3af0eed893874e 100644
--- a/Lib/test/test_call.py
+++ b/Lib/test/test_call.py
@@ -3,7 +3,7 @@
                           set_recursion_limit, skip_on_s390x,
                           skip_emscripten_stack_overflow,
                           skip_wasi_stack_overflow, skip_if_sanitizer,
-                          import_helper)
+                          skip_if_huge_c_stack, import_helper)
 try:
     import _testcapi
 except ImportError:
@@ -1062,6 +1062,7 @@ def get_sp():
     @unittest.skipIf(is_wasi and Py_DEBUG, "requires deep stack")
     @skip_if_sanitizer("requires deep stack", thread=True)
     @unittest.skipIf(_testcapi is None, "requires _testcapi")
+    @skip_if_huge_c_stack(90_000)
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_super_deep(self):
diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py
index 30e959189d68afc..df46c0de00d60aa 100644
--- a/Lib/test/test_class.py
+++ b/Lib/test/test_class.py
@@ -555,6 +555,7 @@ class Custom:
         self.assertFalse(hasattr(o, "__call__"))
         self.assertFalse(hasattr(c, "__call__"))
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def testSFBug532646(self):
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index a6542b396ccfcfa..db1b4c6aa95186d 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -724,6 +724,7 @@ def test_yet_more_evil_still_undecodable(self):
 
     @support.cpython_only
     @unittest.skipIf(support.is_wasi, "exhausts limited stack on WASI")
+    @support.skip_if_huge_c_stack(100_000 if sys.platform == "android" else 
500_000)
     @support.skip_emscripten_stack_overflow()
     def test_compiler_recursion_limit(self):
         # Compiler frames are small
diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py
index cfef24727e8c82e..b452ddbef5ccbee 100644
--- a/Lib/test/test_copy.py
+++ b/Lib/test/test_copy.py
@@ -371,6 +371,7 @@ def test_deepcopy_list(self):
         self.assertIsNot(x, y)
         self.assertIsNot(x[0], y[0])
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_list(self):
@@ -400,6 +401,7 @@ def test_deepcopy_tuple_of_immutables(self):
         y = copy.deepcopy(x)
         self.assertIs(x, y)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_tuple(self):
@@ -419,6 +421,7 @@ def test_deepcopy_dict(self):
         self.assertIsNot(x, y)
         self.assertIsNot(x["foo"], y["foo"])
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deepcopy_reflexive_dict(self):
@@ -579,6 +582,7 @@ def __eq__(self, other):
         self.assertIsNot(y, x)
         self.assertIsNot(y.foo, x.foo)
 
+    @support.skip_if_huge_c_stack()
     def test_deepcopy_reflexive_inst(self):
         class C:
             pass
@@ -641,6 +645,7 @@ def __eq__(self, other):
         self.assertEqual(y, x)
         self.assertIsNot(y.foo, x.foo)
 
+    @support.skip_if_huge_c_stack()
     def test_reconstruct_reflexive(self):
         class C(object):
             pass
diff --git a/Lib/test/test_ctypes/test_as_parameter.py 
b/Lib/test/test_ctypes/test_as_parameter.py
index c9d728e9ae2f9cb..abbfb6efdcea55b 100644
--- a/Lib/test/test_ctypes/test_as_parameter.py
+++ b/Lib/test/test_ctypes/test_as_parameter.py
@@ -5,7 +5,8 @@
                     c_short, c_int, c_long, c_longlong,
                     c_byte, c_wchar, c_float, c_double,
                     ArgumentError)
-from test.support import import_helper, skip_if_sanitizer, 
skip_emscripten_stack_overflow
+from test.support import (import_helper, skip_if_sanitizer,
+                          skip_emscripten_stack_overflow, skip_if_huge_c_stack)
 _ctypes_test = import_helper.import_module("_ctypes_test")
 
 
@@ -193,6 +194,7 @@ class S8I(Structure):
                              (9*2, 8*3, 7*4, 6*5, 5*6, 4*7, 3*8, 2*9))
 
     @skip_if_sanitizer('requires deep stack', thread=True)
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     def test_recursive_as_param(self):
         class A:
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 626e05f4a085235..00e5386b1d56b98 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -3692,6 +3692,7 @@ def f(a): return a
                            encoding='latin1', errors='replace')
         self.assertEqual(ba, b'abc\xbd?')
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_recursive_call(self):
@@ -4899,6 +4900,7 @@ class Thing:
                 # CALL_METHOD_DESCRIPTOR_O
                 deque.append(thing, thing)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_repr_as_str(self):
diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index c1e64a5d99b9e95..7cc6dc7715e2fdf 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -678,6 +678,7 @@ def __repr__(self):
         d = {1: BadRepr()}
         self.assertRaises(Exc, repr, d)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_repr_deep(self):
diff --git a/Lib/test/test_dictviews.py b/Lib/test/test_dictviews.py
index 691fc5dfb5e8ab9..9816ae6c033ec74 100644
--- a/Lib/test/test_dictviews.py
+++ b/Lib/test/test_dictviews.py
@@ -2,7 +2,9 @@
 import copy
 import pickle
 import unittest
-from test.support import skip_emscripten_stack_overflow, 
skip_wasi_stack_overflow, exceeds_recursion_limit
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow, skip_if_huge_c_stack,
+                          exceeds_recursion_limit)
 
 class DictSetTest(unittest.TestCase):
 
@@ -277,6 +279,7 @@ def test_recursive_repr(self):
         # Again.
         self.assertIsInstance(r, str)
 
+    @skip_if_huge_c_stack()
     @skip_wasi_stack_overflow()
     @skip_emscripten_stack_overflow()
     def test_deeply_nested_repr(self):
diff --git a/Lib/test/test_exception_group.py b/Lib/test/test_exception_group.py
index db6ff9d33b44aa9..7d690ca59738ebb 100644
--- a/Lib/test/test_exception_group.py
+++ b/Lib/test/test_exception_group.py
@@ -1,7 +1,9 @@
 import collections
 import types
 import unittest
-from test.support import skip_emscripten_stack_overflow, 
skip_wasi_stack_overflow, exceeds_recursion_limit
+from test.support import (skip_emscripten_stack_overflow,
+                          skip_wasi_stack_overflow, skip_if_huge_c_stack,
+                          exceeds_recursion_limit)
 
 class TestExceptionGroupTypeHierarchy(unittest.TestCase):
     def test_exception_group_types(self):
@@ -535,6 +537,7 @@ def make_deep_eg(self):
             e = ExceptionGroup('eg', [e])
         return e
 
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_deep_split(self):
@@ -542,6 +545,7 @@ def test_deep_split(self):
         with self.assertRaises(RecursionError):
             e.split(TypeError)
 
+    @skip_if_huge_c_stack()
     @skip_emscripten_stack_overflow()
     @skip_wasi_stack_overflow()
     def test_deep_subgroup(self):
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 3a0f94e44f292f1..e79e52911f66aba 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -1509,6 +1509,7 @@ def gen():
     @cpython_only
     @unittest.skipIf(_testcapi is None, "requires _testcapi")
     @force_not_colorized
+    @support.skip_if_huge_c_stack()
     def test_recursion_normalizing_infinite_exception(self):
         # Issue #30697. Test that a RecursionError is raised when
         # maximum recursion depth has been exceeded when creating
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index 16adb8dad7d8bc3..6531bd2bca33c70 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -438,7 +438,7 @@ def test_setstate_subclasses(self):
         self.assertIs(type(r[0]), tuple)
 
     @support.skip_if_sanitizer("thread sanitizer crashes in 
__tsan::FuncEntry", thread=True)
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     def test_recursive_pickle(self):
         with replaced_module('functools', self.module):
diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py
index d97535ba46e677f..16d879a3831bc65 100644
--- a/Lib/test/test_isinstance.py
+++ b/Lib/test/test_isinstance.py
@@ -263,6 +263,7 @@ def test_subclass_tuple(self):
         self.assertEqual(True, issubclass(int, (int, (float, int))))
         self.assertEqual(True, issubclass(str, (str, (Child, str))))
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_subclass_recursion_limit(self):
@@ -270,6 +271,7 @@ def test_subclass_recursion_limit(self):
         # blown
         self.assertRaises(RecursionError, blowstack, issubclass, str, str)
 
+    @support.skip_if_huge_c_stack()
     @support.skip_wasi_stack_overflow()
     @support.skip_emscripten_stack_overflow()
     def test_isinstance_recursion_limit(self):
@@ -317,7 +319,7 @@ def __bases__(self):
             self.assertRaises(RecursionError, issubclass, int, X())
             self.assertRaises(RecursionError, isinstance, 1, X())
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_infinite_recursion_via_bases_tuple(self):
@@ -329,7 +331,7 @@ def __getattr__(self, attr):
             with self.assertRaises(RecursionError):
                 issubclass(Failure(), int)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_infinite_cycle_in_bases(self):
diff --git a/Lib/test/test_json/test_recursion.py 
b/Lib/test/test_json/test_recursion.py
index d732fc80cf1cf30..cbae9fbb4d624ba 100644
--- a/Lib/test/test_json/test_recursion.py
+++ b/Lib/test/test_json/test_recursion.py
@@ -69,7 +69,7 @@ def default(self, o):
 
 
     @support.skip_if_pgo_task  # fails during PGO training w/ some stack sizes
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(500_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_highly_nested_objects_decoding(self):
@@ -102,7 +102,7 @@ def test_highly_nested_objects_encoding(self):
             with support.infinite_recursion(5000):
                 self.dumps(d)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack()
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_endless_recursion(self):
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index 37dbb313b014e85..10e2e5f79908b0b 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -815,7 +815,7 @@ def test_trigger_leak(self):
         parser.ElementDeclHandler = lambda _1, _2: None
         self.assertRaises(TypeError, parser.Parse, data, True)
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(800_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deeply_nested_content_model(self):
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 8c693bfbdb39d92..e268ed7f1ebef3d 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -3123,7 +3123,7 @@ def __deepcopy__(self, memo):
         self.assertEqual([c.tag for c in children[3:]],
                          [a.tag, b.tag, a.tag, b.tag])
 
-    @support.skip_if_unlimited_stack_size
+    @support.skip_if_huge_c_stack(500_000)
     @support.skip_emscripten_stack_overflow()
     @support.skip_wasi_stack_overflow()
     def test_deeply_nested_deepcopy(self):
diff --git 
a/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst 
b/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
new file mode 100644
index 000000000000000..8030db869c7ba6e
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-07-20-11-30-00.gh-issue-154211.hugestack.rst
@@ -0,0 +1,3 @@
+Add ``test.support.skip_if_huge_c_stack()`` and use it to skip tests
+that exhaust the C stack if the stack limit is very large
+(e.g. on DragonFly BSD or with ``ulimit -s unlimited``).

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to