Skip to content

Commit fdb49d8

Browse files
Fix segfault on cyclic or deeply-nested AST in compile() (#7630)
1 parent 3770708 commit fdb49d8

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

crates/vm/src/stdlib/_ast/node.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ impl<T: Node> Node for Vec<T> {
3131
source_file: &SourceFile,
3232
object: PyObjectRef,
3333
) -> PyResult<Self> {
34-
vm.extract_elements_with(&object, |obj| Node::ast_from_object(vm, source_file, obj))
34+
// Recursion guard for each element: prevents stack overflow when a
35+
// sequence element transitively references the sequence itself
36+
// (e.g. `l = ast.List(...); l.elts = [l]`). See issue #4862.
37+
vm.extract_elements_with(&object, |obj| {
38+
vm.with_recursion("while traversing AST node", || {
39+
Node::ast_from_object(vm, source_file, obj)
40+
})
41+
})
3542
}
3643
}
3744

@@ -45,7 +52,13 @@ impl<T: Node> Node for Box<T> {
4552
source_file: &SourceFile,
4653
object: PyObjectRef,
4754
) -> PyResult<Self> {
48-
T::ast_from_object(vm, source_file, object).map(Self::new)
55+
// Recursion guard: every descent through a Box<AstNode> increments the
56+
// VM's recursion depth so cyclic or pathologically deep ASTs raise
57+
// RecursionError instead of overflowing the native stack.
58+
// See issue #4862.
59+
vm.with_recursion("while traversing AST node", || {
60+
T::ast_from_object(vm, source_file, object).map(Self::new)
61+
})
4962
}
5063

5164
fn is_none(&self) -> bool {

extra_tests/snippets/stdlib_ast.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,54 @@ def foo():
3737
assert i.module is None
3838
assert i.names[0].name == "a"
3939
assert i.names[0].asname is None
40+
41+
42+
# Regression test for issue #4862:
43+
# A cyclic AST fed to compile() used to overflow the Rust stack and SIGSEGV.
44+
# After the fix, the recursion guard in ast_from_object raises RecursionError,
45+
# matching CPython's behavior. Covers both Box<T> descents (UnaryOp, BinOp,
46+
# Call, Attribute) and Vec<T> descents (List, Tuple).
47+
import warnings
48+
49+
50+
def _cyclic_cases():
51+
# Box<Expr> descents
52+
u = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
53+
u.operand = u
54+
yield "UnaryOp", u
55+
56+
b = ast.BinOp(
57+
op=ast.Add(),
58+
right=ast.Constant(value=0, lineno=0, col_offset=0),
59+
lineno=0,
60+
col_offset=0,
61+
)
62+
b.left = b
63+
yield "BinOp", b
64+
65+
c = ast.Call(args=[], keywords=[], lineno=0, col_offset=0)
66+
c.func = c
67+
yield "Call", c
68+
69+
a = ast.Attribute(attr="x", ctx=ast.Load(), lineno=0, col_offset=0)
70+
a.value = a
71+
yield "Attribute", a
72+
73+
# Vec<Expr> descents
74+
lst = ast.List(ctx=ast.Load(), lineno=0, col_offset=0)
75+
lst.elts = [lst]
76+
yield "List", lst
77+
78+
tup = ast.Tuple(ctx=ast.Load(), lineno=0, col_offset=0)
79+
tup.elts = [tup]
80+
yield "Tuple", tup
81+
82+
83+
with warnings.catch_warnings():
84+
warnings.simplefilter("ignore")
85+
for name, node in _cyclic_cases():
86+
try:
87+
compile(ast.Expression(node), "<cyclic>", "eval")
88+
raise AssertionError(f"cyclic {name} should raise RecursionError")
89+
except RecursionError:
90+
pass # expected; matches CPython

0 commit comments

Comments
 (0)