Skip to content

Commit 86eb500

Browse files
[3.11] gh-95185: Check recursion depth in the AST constructor (GH-95186) (GH-95208)
Co-authored-by: Serhiy Storchaka <[email protected]> (cherry picked from commit 0047447) Co-authored-by: Pablo Galindo Salgado <[email protected]>
1 parent 5a9920f commit 86eb500

File tree

6 files changed

+614
-443
lines changed

6 files changed

+614
-443
lines changed

Doc/data/python3.11.abi

Lines changed: 446 additions & 441 deletions
Large diffs are not rendered by default.

Include/internal/pycore_ast_state.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_ast.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,27 @@ def next(self):
793793
return self
794794
enum._test_simple_enum(_Precedence, ast._Precedence)
795795

796+
@support.cpython_only
797+
def test_ast_recursion_limit(self):
798+
fail_depth = sys.getrecursionlimit() * 3
799+
crash_depth = sys.getrecursionlimit() * 300
800+
success_depth = int(fail_depth * 0.75)
801+
802+
def check_limit(prefix, repeated):
803+
expect_ok = prefix + repeated * success_depth
804+
ast.parse(expect_ok)
805+
for depth in (fail_depth, crash_depth):
806+
broken = prefix + repeated * depth
807+
details = "Compiling ({!r} + {!r} * {})".format(
808+
prefix, repeated, depth)
809+
with self.assertRaises(RecursionError, msg=details):
810+
ast.parse(broken)
811+
812+
check_limit("a", "()")
813+
check_limit("a", ".b")
814+
check_limit("a", "[0]")
815+
check_limit("a", "*a")
816+
796817

797818
class ASTHelpers_Test(unittest.TestCase):
798819
maxDiff = None
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Prevented crashes in the AST constructor when compiling some absurdly long
2+
expressions like ``"+0"*1000000``. :exc:`RecursionError` is now raised
3+
instead. Patch by Pablo Galindo

Parser/asdl_c.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,8 @@ def visitModule(self, mod):
11131113
for dfn in mod.dfns:
11141114
self.visit(dfn)
11151115
self.file.write(textwrap.dedent('''
1116+
state->recursion_depth = 0;
1117+
state->recursion_limit = 0;
11161118
state->initialized = 1;
11171119
return 1;
11181120
}
@@ -1260,8 +1262,14 @@ def func_begin(self, name):
12601262
self.emit('if (!o) {', 1)
12611263
self.emit("Py_RETURN_NONE;", 2)
12621264
self.emit("}", 1)
1265+
self.emit("if (++state->recursion_depth > state->recursion_limit) {", 1)
1266+
self.emit("PyErr_SetString(PyExc_RecursionError,", 2)
1267+
self.emit('"maximum recursion depth exceeded during ast construction");', 3)
1268+
self.emit("return 0;", 2)
1269+
self.emit("}", 1)
12631270

12641271
def func_end(self):
1272+
self.emit("state->recursion_depth--;", 1)
12651273
self.emit("return result;", 1)
12661274
self.emit("failed:", 0)
12671275
self.emit("Py_XDECREF(value);", 1)
@@ -1372,7 +1380,32 @@ class PartingShots(StaticVisitor):
13721380
if (state == NULL) {
13731381
return NULL;
13741382
}
1375-
return ast2obj_mod(state, t);
1383+
1384+
int recursion_limit = Py_GetRecursionLimit();
1385+
int starting_recursion_depth;
1386+
/* Be careful here to prevent overflow. */
1387+
int COMPILER_STACK_FRAME_SCALE = 3;
1388+
PyThreadState *tstate = _PyThreadState_GET();
1389+
if (!tstate) {
1390+
return 0;
1391+
}
1392+
state->recursion_limit = (recursion_limit < INT_MAX / COMPILER_STACK_FRAME_SCALE) ?
1393+
recursion_limit * COMPILER_STACK_FRAME_SCALE : recursion_limit;
1394+
int recursion_depth = tstate->recursion_limit - tstate->recursion_remaining;
1395+
starting_recursion_depth = (recursion_depth < INT_MAX / COMPILER_STACK_FRAME_SCALE) ?
1396+
recursion_depth * COMPILER_STACK_FRAME_SCALE : recursion_depth;
1397+
state->recursion_depth = starting_recursion_depth;
1398+
1399+
PyObject *result = ast2obj_mod(state, t);
1400+
1401+
/* Check that the recursion depth counting balanced correctly */
1402+
if (result && state->recursion_depth != starting_recursion_depth) {
1403+
PyErr_Format(PyExc_SystemError,
1404+
"AST constructor recursion depth mismatch (before=%d, after=%d)",
1405+
starting_recursion_depth, state->recursion_depth);
1406+
return 0;
1407+
}
1408+
return result;
13761409
}
13771410
13781411
/* mode is 0 for "exec", 1 for "eval" and 2 for "single" input */
@@ -1438,6 +1471,8 @@ def visit(self, object):
14381471
def generate_ast_state(module_state, f):
14391472
f.write('struct ast_state {\n')
14401473
f.write(' int initialized;\n')
1474+
f.write(' int recursion_depth;\n')
1475+
f.write(' int recursion_limit;\n')
14411476
for s in module_state:
14421477
f.write(' PyObject *' + s + ';\n')
14431478
f.write('};')

0 commit comments

Comments
 (0)