Skip to content

Commit 976598d

Browse files
bpo-11105: Do not crash when compiling recursive ASTs (GH-20594)
When compiling an AST object with a direct / indirect reference cycles, on the conversion phase because of exceeding amount of calls, a segfault was raised. This patch adds recursion guards to places for preventing user inputs to not to crash AST but instead raise a RecursionError. (cherry picked from commit f349124) Co-authored-by: Batuhan Taskaya <[email protected]>
1 parent d2ab15f commit 976598d

File tree

4 files changed

+840
-4
lines changed

4 files changed

+840
-4
lines changed

Lib/test/test_ast.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,20 @@ def test_level_as_none(self):
10971097
exec(code, ns)
10981098
self.assertIn('sleep', ns)
10991099

1100+
def test_recursion_direct(self):
1101+
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
1102+
e.operand = e
1103+
with self.assertRaises(RecursionError):
1104+
compile(ast.Expression(e), "<test>", "eval")
1105+
1106+
def test_recursion_indirect(self):
1107+
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
1108+
f = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
1109+
e.operand = f
1110+
f.operand = e
1111+
with self.assertRaises(RecursionError):
1112+
compile(ast.Expression(e), "<test>", "eval")
1113+
11001114

11011115
class ASTValidatorTests(unittest.TestCase):
11021116

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When compiling :class:`ast.AST` objects with recursive references
2+
through :func:`compile`, the interpreter doesn't crash anymore instead
3+
it raises a :exc:`RecursionError`.

Parser/asdl_c.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import textwrap
77

88
from argparse import ArgumentParser
9+
from contextlib import contextmanager
910
from pathlib import Path
1011

1112
import asdl
@@ -421,6 +422,14 @@ def visitProduct(self, prod, name):
421422

422423

423424
class Obj2ModVisitor(PickleVisitor):
425+
@contextmanager
426+
def recursive_call(self, node, level):
427+
self.emit('if (Py_EnterRecursiveCall(" while traversing \'%s\' node")) {' % node, level, reflow=False)
428+
self.emit('goto failed;', level + 1)
429+
self.emit('}', level)
430+
yield
431+
self.emit('Py_LeaveRecursiveCall();', level)
432+
424433
def funcHeader(self, name):
425434
ctype = get_c_type(name)
426435
self.emit("int", 0)
@@ -596,8 +605,9 @@ def visitField(self, field, name, sum=None, prod=None, depth=0):
596605
self.emit("%s val;" % ctype, depth+2)
597606
self.emit("PyObject *tmp2 = PyList_GET_ITEM(tmp, i);", depth+2)
598607
self.emit("Py_INCREF(tmp2);", depth+2)
599-
self.emit("res = obj2ast_%s(state, tmp2, &val, arena);" %
600-
field.type, depth+2, reflow=False)
608+
with self.recursive_call(name, depth+2):
609+
self.emit("res = obj2ast_%s(state, tmp2, &val, arena);" %
610+
field.type, depth+2, reflow=False)
601611
self.emit("Py_DECREF(tmp2);", depth+2)
602612
self.emit("if (res != 0) goto failed;", depth+2)
603613
self.emit("if (len != PyList_GET_SIZE(tmp)) {", depth+2)
@@ -610,8 +620,9 @@ def visitField(self, field, name, sum=None, prod=None, depth=0):
610620
self.emit("asdl_seq_SET(%s, i, val);" % field.name, depth+2)
611621
self.emit("}", depth+1)
612622
else:
613-
self.emit("res = obj2ast_%s(state, tmp, &%s, arena);" %
614-
(field.type, field.name), depth+1)
623+
with self.recursive_call(name, depth+1):
624+
self.emit("res = obj2ast_%s(state, tmp, &%s, arena);" %
625+
(field.type, field.name), depth+1)
615626
self.emit("if (res != 0) goto failed;", depth+1)
616627

617628
self.emit("Py_CLEAR(tmp);", depth+1)

0 commit comments

Comments
 (0)