Skip to content

Commit

Permalink
[3.9] bpo-11105: Do not crash when compiling recursive ASTs (GH-20594) (
Browse files Browse the repository at this point in the history
GH-26522)

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 <batuhan@python.org>
  • Loading branch information
isidentical authored Jun 3, 2021
1 parent 5a8ddcc commit de58b31
Show file tree
Hide file tree
Showing 4 changed files with 732 additions and 4 deletions.
14 changes: 14 additions & 0 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,20 @@ def test_level_as_none(self):
exec(code, ns)
self.assertIn('sleep', ns)

def test_recursion_direct(self):
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
e.operand = e
with self.assertRaises(RecursionError):
compile(ast.Expression(e), "<test>", "eval")

def test_recursion_indirect(self):
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
f = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
e.operand = f
f.operand = e
with self.assertRaises(RecursionError):
compile(ast.Expression(e), "<test>", "eval")


class ASTValidatorTests(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
When compiling :class:`ast.AST` objects with recursive references
through :func:`compile`, the interpreter doesn't crash anymore instead
it raises a :exc:`RecursionError`.
19 changes: 15 additions & 4 deletions Parser/asdl_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys

from argparse import ArgumentParser
from contextlib import contextmanager
from pathlib import Path

import asdl
Expand Down Expand Up @@ -394,6 +395,14 @@ def visitProduct(self, prod, name):


class Obj2ModVisitor(PickleVisitor):
@contextmanager
def recursive_call(self, node, level):
self.emit('if (Py_EnterRecursiveCall(" while traversing \'%s\' node")) {' % node, level, reflow=False)
self.emit('goto failed;', level + 1)
self.emit('}', level)
yield
self.emit('Py_LeaveRecursiveCall();', level)

def funcHeader(self, name):
ctype = get_c_type(name)
self.emit("int", 0)
Expand Down Expand Up @@ -568,8 +577,9 @@ def visitField(self, field, name, sum=None, prod=None, depth=0):
self.emit("%s val;" % ctype, depth+2)
self.emit("PyObject *tmp2 = PyList_GET_ITEM(tmp, i);", depth+2)
self.emit("Py_INCREF(tmp2);", depth+2)
self.emit("res = obj2ast_%s(state, tmp2, &val, arena);" %
field.type, depth+2, reflow=False)
with self.recursive_call(name, depth+2):
self.emit("res = obj2ast_%s(state, tmp2, &val, arena);" %
field.type, depth+2, reflow=False)
self.emit("Py_DECREF(tmp2);", depth+2)
self.emit("if (res != 0) goto failed;", depth+2)
self.emit("if (len != PyList_GET_SIZE(tmp)) {", depth+2)
Expand All @@ -582,8 +592,9 @@ def visitField(self, field, name, sum=None, prod=None, depth=0):
self.emit("asdl_seq_SET(%s, i, val);" % field.name, depth+2)
self.emit("}", depth+1)
else:
self.emit("res = obj2ast_%s(state, tmp, &%s, arena);" %
(field.type, field.name), depth+1)
with self.recursive_call(name, depth+1):
self.emit("res = obj2ast_%s(state, tmp, &%s, arena);" %
(field.type, field.name), depth+1)
self.emit("if (res != 0) goto failed;", depth+1)

self.emit("Py_CLEAR(tmp);", depth+1)
Expand Down
Loading

0 comments on commit de58b31

Please sign in to comment.