From 11f2eae31551c0322b3daf4ce8ba516266a1cd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Andr=C3=A9=20Reuter?= Date: Fri, 9 Jan 2026 11:33:40 +0100 Subject: [PATCH 1/5] gh-143460: Skip infinite recusion tests for inf. stack size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid tests being killed due to OOM on Linux if a system is configured with 'ulimit -s unlimited' by skipping tests relying on infinite recursion. While unclear if Python should support 'ulimit -s unlimited', we should at least try to avoid failing a PGO build running tests due to an unlimited stack size being set. Signed-off-by: Jan André Reuter --- Lib/test/pickletester.py | 1 + Lib/test/support/__init__.py | 17 +++++++++++++++++ Lib/test/test_ast/test_ast.py | 6 +++++- Lib/test/test_functools.py | 2 ++ Lib/test/test_isinstance.py | 2 ++ Lib/test/test_json/test_recursion.py | 3 +++ Lib/test/test_support.py | 1 + Lib/test/test_tomllib/test_misc.py | 2 ++ ...26-01-09-13-52-10.gh-issue-143460._nW2jt.rst | 1 + 9 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Tests/2026-01-09-13-52-10.gh-issue-143460._nW2jt.rst diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 09bfb374732e86..4522df3632d7c1 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2438,6 +2438,7 @@ def test_reduce_None(self): with self.assertRaises(TypeError): self.dumps(c) + @support.skip_if_unlimited_stack_size() @no_tracing def test_bad_getattr(self): # Issue #3514: crash when there is an infinite loop in __getattr__ diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 847d9074eb82cd..1237d6eb42f97f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -45,6 +45,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", # sys "MS_WINDOWS", "is_jython", "is_android", "is_emscripten", "is_wasi", "is_apple_mobile", "check_impl_detail", "unix_shell", "setswitchinterval", @@ -1771,6 +1772,22 @@ def skip_if_pgo_task(test): return test if ok else unittest.skip(msg)(test) +def skip_if_unlimited_stack_size(): + """ + Skip decorator for tests not run when an unlimited stack size is configured. + + Tests using support.infinite_recursion([...]) may otherwise run into an infinite loop, + running until the memory on the system is filled and crashing due to OOM. + + See gh-143460: Python 3.14/3.15a build aborting due to OOM during test_functools / test_json + """ + import resource + stack_size = resource.getrlimit(resource.RLIMIT_STACK) + reason = "Not run due to unlimited stack size" + unlimited_stack_size_cond = stack_size == (-1, -1) or stack_size == (0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + return unittest.skipIf(unlimited_stack_size_cond, reason) + + def detect_api_mismatch(ref_api, other_api, *, ignore=()): """Returns the set of items in ref_api not in other_api, except for a defined list of items to be ignored in this check. diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index d2b76b46dbe2eb..d6b629f9ef014f 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -25,7 +25,7 @@ from test import support from test.support import os_helper -from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow +from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow, skip_if_unlimited_stack_size from test.support.ast_helper import ASTTestMixin from test.support.import_helper import ensure_lazy_imports from test.test_ast.utils import to_tuple @@ -989,6 +989,7 @@ def next(self): enum._test_simple_enum(_Precedence, _ast_unparse._Precedence) @support.cpython_only + @skip_if_unlimited_stack_size() @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_ast_recursion_limit(self): @@ -1127,6 +1128,7 @@ def test_pickling(self): ast2 = pickle.loads(pickle.dumps(tree, protocol)) self.assertEqual(to_tuple(ast2), to_tuple(tree)) + @skip_if_unlimited_stack_size() def test_copy_with_parents(self): # gh-120108 code = """ @@ -1974,6 +1976,7 @@ def test_level_as_none(self): exec(code, ns) self.assertIn('sleep', ns) + @skip_if_unlimited_stack_size() @skip_emscripten_stack_overflow() def test_recursion_direct(self): e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1)) @@ -1982,6 +1985,7 @@ def test_recursion_direct(self): with support.infinite_recursion(): compile(ast.Expression(e), "", "eval") + @skip_if_unlimited_stack_size() @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_functools.py b/Lib/test/test_functools.py index 090926fd8d8b61..188569f0ced158 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -438,6 +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_emscripten_stack_overflow() def test_recursive_pickle(self): with replaced_module('functools', self.module): @@ -2139,6 +2140,7 @@ def orig(a: int) -> nonexistent: ... @support.skip_on_s390x @unittest.skipIf(support.is_wasi, "WASI has limited C stack") @support.skip_if_sanitizer("requires deep stack", ub=True, thread=True) + @support.skip_if_unlimited_stack_size() @support.skip_emscripten_stack_overflow() def test_lru_recursion(self): diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py index f440fc28ee7b7d..d9d732508fa8ff 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -317,6 +317,7 @@ def __bases__(self): self.assertRaises(RecursionError, issubclass, int, X()) self.assertRaises(RecursionError, isinstance, 1, X()) + @support.skip_if_unlimited_stack_size() @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_infinite_recursion_via_bases_tuple(self): @@ -328,6 +329,7 @@ def __getattr__(self, attr): with self.assertRaises(RecursionError): issubclass(Failure(), int) + @support.skip_if_unlimited_stack_size() @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 40a0baa53f0c3b..1437f25d6bba3f 100644 --- a/Lib/test/test_json/test_recursion.py +++ b/Lib/test/test_json/test_recursion.py @@ -68,6 +68,7 @@ def default(self, o): self.fail("didn't raise ValueError on default recursion") + @support.skip_if_unlimited_stack_size() @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_highly_nested_objects_decoding(self): @@ -84,6 +85,7 @@ def test_highly_nested_objects_decoding(self): with support.infinite_recursion(): self.loads('[' * very_deep + '1' + ']' * very_deep) + @support.skip_if_unlimited_stack_size() @support.skip_wasi_stack_overflow() @support.skip_emscripten_stack_overflow() @support.requires_resource('cpu') @@ -99,6 +101,7 @@ def test_highly_nested_objects_encoding(self): with support.infinite_recursion(5000): self.dumps(d) + @support.skip_if_unlimited_stack_size() @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_endless_recursion(self): diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 667fcc81d8e378..7661b43e84af21 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -672,6 +672,7 @@ def test_recursive(depth, limit): """) script_helper.assert_python_ok("-c", code) + @support.skip_if_unlimited_stack_size() def test_recursion(self): # Test infinite_recursion() and get_recursion_available() functions. def recursive_function(depth): diff --git a/Lib/test/test_tomllib/test_misc.py b/Lib/test/test_tomllib/test_misc.py index 59116afa1f36ad..772fdec9661889 100644 --- a/Lib/test/test_tomllib/test_misc.py +++ b/Lib/test/test_tomllib/test_misc.py @@ -93,6 +93,7 @@ def test_deepcopy(self): } self.assertEqual(obj_copy, expected_obj) + @support.skip_if_unlimited_stack_size() def test_inline_array_recursion_limit(self): with support.infinite_recursion(max_depth=100): available = support.get_recursion_available() @@ -104,6 +105,7 @@ def test_inline_array_recursion_limit(self): recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]" tomllib.loads(recursive_array_toml) + @support.skip_if_unlimited_stack_size() def test_inline_table_recursion_limit(self): with support.infinite_recursion(max_depth=100): available = support.get_recursion_available() diff --git a/Misc/NEWS.d/next/Tests/2026-01-09-13-52-10.gh-issue-143460._nW2jt.rst b/Misc/NEWS.d/next/Tests/2026-01-09-13-52-10.gh-issue-143460._nW2jt.rst new file mode 100644 index 00000000000000..b0df9917d62655 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-01-09-13-52-10.gh-issue-143460._nW2jt.rst @@ -0,0 +1 @@ +Skip tests relying on infinite recusion if stack size is unlimited. From c9c8c9fdf4a7d46d1ba3b6ae518fedd3f0f7fc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Andr=C3=A9=20Reuter?= Date: Fri, 9 Jan 2026 14:25:08 +0100 Subject: [PATCH 2/5] skip stack size check on Windows and WASI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan André Reuter --- Lib/test/pickletester.py | 2 +- Lib/test/support/__init__.py | 11 ++++++++--- Lib/test/test_ast/test_ast.py | 8 ++++---- Lib/test/test_functools.py | 4 ++-- Lib/test/test_isinstance.py | 4 ++-- Lib/test/test_json/test_recursion.py | 6 +++--- Lib/test/test_support.py | 2 +- Lib/test/test_tomllib/test_misc.py | 4 ++-- 8 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 4522df3632d7c1..2e362ac1b02ac9 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2438,7 +2438,7 @@ def test_reduce_None(self): with self.assertRaises(TypeError): self.dumps(c) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size @no_tracing def test_bad_getattr(self): # Issue #3514: crash when there is an infinite loop in __getattr__ diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 1237d6eb42f97f..fd8f3d66d4171f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1772,7 +1772,7 @@ def skip_if_pgo_task(test): return test if ok else unittest.skip(msg)(test) -def skip_if_unlimited_stack_size(): +def skip_if_unlimited_stack_size(test): """ Skip decorator for tests not run when an unlimited stack size is configured. @@ -1781,11 +1781,16 @@ def skip_if_unlimited_stack_size(): See gh-143460: Python 3.14/3.15a build aborting due to OOM during test_functools / test_json """ + if is_wasi: + return test + if sys.platform.startswith('win'): + return test + import resource stack_size = resource.getrlimit(resource.RLIMIT_STACK) - reason = "Not run due to unlimited stack size" unlimited_stack_size_cond = stack_size == (-1, -1) or stack_size == (0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) - return unittest.skipIf(unlimited_stack_size_cond, reason) + reason = "Not run due to unlimited stack size" + return unittest.skipIf(unlimited_stack_size_cond, reason)(test) def detect_api_mismatch(ref_api, other_api, *, ignore=()): diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index d6b629f9ef014f..3917407fb37d9e 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -989,7 +989,7 @@ def next(self): enum._test_simple_enum(_Precedence, _ast_unparse._Precedence) @support.cpython_only - @skip_if_unlimited_stack_size() + @skip_if_unlimited_stack_size @skip_wasi_stack_overflow() @skip_emscripten_stack_overflow() def test_ast_recursion_limit(self): @@ -1128,7 +1128,7 @@ def test_pickling(self): ast2 = pickle.loads(pickle.dumps(tree, protocol)) self.assertEqual(to_tuple(ast2), to_tuple(tree)) - @skip_if_unlimited_stack_size() + @skip_if_unlimited_stack_size def test_copy_with_parents(self): # gh-120108 code = """ @@ -1976,7 +1976,7 @@ def test_level_as_none(self): exec(code, ns) self.assertIn('sleep', ns) - @skip_if_unlimited_stack_size() + @skip_if_unlimited_stack_size @skip_emscripten_stack_overflow() def test_recursion_direct(self): e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1)) @@ -1985,7 +1985,7 @@ def test_recursion_direct(self): with support.infinite_recursion(): compile(ast.Expression(e), "", "eval") - @skip_if_unlimited_stack_size() + @skip_if_unlimited_stack_size @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_functools.py b/Lib/test/test_functools.py index 188569f0ced158..459d56f82d6820 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_unlimited_stack_size @support.skip_emscripten_stack_overflow() def test_recursive_pickle(self): with replaced_module('functools', self.module): @@ -2140,7 +2140,7 @@ def orig(a: int) -> nonexistent: ... @support.skip_on_s390x @unittest.skipIf(support.is_wasi, "WASI has limited C stack") @support.skip_if_sanitizer("requires deep stack", ub=True, thread=True) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size @support.skip_emscripten_stack_overflow() def test_lru_recursion(self): diff --git a/Lib/test/test_isinstance.py b/Lib/test/test_isinstance.py index d9d732508fa8ff..d97535ba46e677 100644 --- a/Lib/test/test_isinstance.py +++ b/Lib/test/test_isinstance.py @@ -317,7 +317,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_unlimited_stack_size @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_infinite_recursion_via_bases_tuple(self): @@ -329,7 +329,7 @@ def __getattr__(self, attr): with self.assertRaises(RecursionError): issubclass(Failure(), int) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size @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 1437f25d6bba3f..ffd3404e6f77a0 100644 --- a/Lib/test/test_json/test_recursion.py +++ b/Lib/test/test_json/test_recursion.py @@ -68,7 +68,7 @@ def default(self, o): self.fail("didn't raise ValueError on default recursion") - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_highly_nested_objects_decoding(self): @@ -85,7 +85,7 @@ def test_highly_nested_objects_decoding(self): with support.infinite_recursion(): self.loads('[' * very_deep + '1' + ']' * very_deep) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size @support.skip_wasi_stack_overflow() @support.skip_emscripten_stack_overflow() @support.requires_resource('cpu') @@ -101,7 +101,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_unlimited_stack_size @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_endless_recursion(self): diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 7661b43e84af21..be7e307b4f1111 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -672,7 +672,7 @@ def test_recursive(depth, limit): """) script_helper.assert_python_ok("-c", code) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size def test_recursion(self): # Test infinite_recursion() and get_recursion_available() functions. def recursive_function(depth): diff --git a/Lib/test/test_tomllib/test_misc.py b/Lib/test/test_tomllib/test_misc.py index 772fdec9661889..118fde24d88521 100644 --- a/Lib/test/test_tomllib/test_misc.py +++ b/Lib/test/test_tomllib/test_misc.py @@ -93,7 +93,7 @@ def test_deepcopy(self): } self.assertEqual(obj_copy, expected_obj) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size def test_inline_array_recursion_limit(self): with support.infinite_recursion(max_depth=100): available = support.get_recursion_available() @@ -105,7 +105,7 @@ def test_inline_array_recursion_limit(self): recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]" tomllib.loads(recursive_array_toml) - @support.skip_if_unlimited_stack_size() + @support.skip_if_unlimited_stack_size def test_inline_table_recursion_limit(self): with support.infinite_recursion(max_depth=100): available = support.get_recursion_available() From c58d297627c20fe0cfbc4ac109ee1f3f8d375bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Andr=C3=A9=20Reuter?= Date: Fri, 9 Jan 2026 14:28:25 +0100 Subject: [PATCH 3/5] cleanup skip_if_unlimited_stack_size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan André Reuter --- Lib/test/support/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index fd8f3d66d4171f..b7d7e1352f848a 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1787,8 +1787,8 @@ def skip_if_unlimited_stack_size(test): return test import resource - stack_size = resource.getrlimit(resource.RLIMIT_STACK) - unlimited_stack_size_cond = stack_size == (-1, -1) or stack_size == (0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF) + curlim, maxlim = resource.getrlimit(resource.RLIMIT_STACK) + unlimited_stack_size_cond = curlim == maxlim and curlim in (-1, 0xFFFFFFFFFFFFFFFF) reason = "Not run due to unlimited stack size" return unittest.skipIf(unlimited_stack_size_cond, reason)(test) From 44b7c6aa5a91ad94e2ee20deb6571b776f5cdee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Andr=C3=A9=20Reuter?= Date: Fri, 9 Jan 2026 14:33:54 +0100 Subject: [PATCH 4/5] apply docstring suggestion and shorten Windows / WASI check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan André Reuter --- Lib/test/support/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index b7d7e1352f848a..54bd34ddea9bfd 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1773,17 +1773,15 @@ def skip_if_pgo_task(test): def skip_if_unlimited_stack_size(test): - """ - Skip decorator for tests not run when an unlimited stack size is configured. + """Skip decorator for tests not run when an unlimited stack size is configured. - Tests using support.infinite_recursion([...]) may otherwise run into an infinite loop, - running until the memory on the system is filled and crashing due to OOM. + Tests using support.infinite_recursion([...]) may otherwise run into + an infinite loop, running until the memory on the system is filled and + crashing due to OOM. - See gh-143460: Python 3.14/3.15a build aborting due to OOM during test_functools / test_json + See https://github.com/python/cpython/issues/143460. """ - if is_wasi: - return test - if sys.platform.startswith('win'): + if is_wasi or os.name == "nt": return test import resource From 2877729a36cc618c75e78ba7ce7c86f8f980cfd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Andr=C3=A9=20Reuter?= Date: Fri, 9 Jan 2026 16:23:03 +0100 Subject: [PATCH 5/5] make hex value for stack limit check more readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan André Reuter --- Lib/test/support/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 54bd34ddea9bfd..7bc2e1f3150035 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1786,7 +1786,7 @@ def skip_if_unlimited_stack_size(test): import resource curlim, maxlim = resource.getrlimit(resource.RLIMIT_STACK) - unlimited_stack_size_cond = curlim == maxlim and curlim in (-1, 0xFFFFFFFFFFFFFFFF) + unlimited_stack_size_cond = curlim == maxlim and curlim in (-1, 0xFFFF_FFFF_FFFF_FFFF) reason = "Not run due to unlimited stack size" return unittest.skipIf(unlimited_stack_size_cond, reason)(test)