Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions crates/vm/src/stdlib/_ast/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ impl<T: Node> Node for Vec<T> {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
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)
})
})
}
}

Expand All @@ -45,7 +52,13 @@ impl<T: Node> Node for Box<T> {
source_file: &SourceFile,
object: PyObjectRef,
) -> PyResult<Self> {
T::ast_from_object(vm, source_file, object).map(Self::new)
// Recursion guard: every descent through a Box<AstNode> 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 {
Expand Down
51 changes: 51 additions & 0 deletions extra_tests/snippets/stdlib_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,54 @@ def foo():
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<T> descents (UnaryOp, BinOp,
# Call, Attribute) and Vec<T> descents (List, Tuple).
import warnings


def _cyclic_cases():
# Box<Expr> 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<Expr> 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), "<cyclic>", "eval")
raise AssertionError(f"cyclic {name} should raise RecursionError")
except RecursionError:
pass # expected; matches CPython
Loading