From 7d2b37ddd64ba4d3d4b8b5efdbca146a4a13807d Mon Sep 17 00:00:00 2001 From: Changjoon Date: Sun, 19 Apr 2026 22:35:09 +0900 Subject: [PATCH] Fix segfault on cyclic or deeply-nested AST in compile() (#7630) --- crates/vm/src/stdlib/_ast/node.rs | 17 ++++++++-- extra_tests/snippets/stdlib_ast.py | 51 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/vm/src/stdlib/_ast/node.rs b/crates/vm/src/stdlib/_ast/node.rs index 6f74e7151..4ee3893b6 100644 --- a/crates/vm/src/stdlib/_ast/node.rs +++ b/crates/vm/src/stdlib/_ast/node.rs @@ -31,7 +31,14 @@ impl Node for Vec { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - vm.extract_elements_with(&object, |obj| Node::ast_from_object(vm, source_file, obj)) + // Recursion guard for each element: prevents stack overflow when a + // sequence element transitively references the sequence itself + // (e.g. `l = ast.List(...); l.elts = [l]`). See issue #4862. + vm.extract_elements_with(&object, |obj| { + vm.with_recursion("while traversing AST node", || { + Node::ast_from_object(vm, source_file, obj) + }) + }) } } @@ -45,7 +52,13 @@ impl Node for Box { source_file: &SourceFile, object: PyObjectRef, ) -> PyResult { - T::ast_from_object(vm, source_file, object).map(Self::new) + // Recursion guard: every descent through a Box increments the + // VM's recursion depth so cyclic or pathologically deep ASTs raise + // RecursionError instead of overflowing the native stack. + // See issue #4862. + vm.with_recursion("while traversing AST node", || { + T::ast_from_object(vm, source_file, object).map(Self::new) + }) } fn is_none(&self) -> bool { diff --git a/extra_tests/snippets/stdlib_ast.py b/extra_tests/snippets/stdlib_ast.py index dc626506f..7b5c69df4 100644 --- a/extra_tests/snippets/stdlib_ast.py +++ b/extra_tests/snippets/stdlib_ast.py @@ -37,3 +37,54 @@ assert i.level == 3 assert i.module is None assert i.names[0].name == "a" assert i.names[0].asname is None + + +# Regression test for issue #4862: +# A cyclic AST fed to compile() used to overflow the Rust stack and SIGSEGV. +# After the fix, the recursion guard in ast_from_object raises RecursionError, +# matching CPython's behavior. Covers both Box descents (UnaryOp, BinOp, +# Call, Attribute) and Vec descents (List, Tuple). +import warnings + + +def _cyclic_cases(): + # Box descents + u = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0) + u.operand = u + yield "UnaryOp", u + + b = ast.BinOp( + op=ast.Add(), + right=ast.Constant(value=0, lineno=0, col_offset=0), + lineno=0, + col_offset=0, + ) + b.left = b + yield "BinOp", b + + c = ast.Call(args=[], keywords=[], lineno=0, col_offset=0) + c.func = c + yield "Call", c + + a = ast.Attribute(attr="x", ctx=ast.Load(), lineno=0, col_offset=0) + a.value = a + yield "Attribute", a + + # Vec descents + lst = ast.List(ctx=ast.Load(), lineno=0, col_offset=0) + lst.elts = [lst] + yield "List", lst + + tup = ast.Tuple(ctx=ast.Load(), lineno=0, col_offset=0) + tup.elts = [tup] + yield "Tuple", tup + + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + for name, node in _cyclic_cases(): + try: + compile(ast.Expression(node), "", "eval") + raise AssertionError(f"cyclic {name} should raise RecursionError") + except RecursionError: + pass # expected; matches CPython