mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
* Bound AST nesting so deep source raises instead of crashing Deeply nested source exhausted the native stack and killed the process with SIGSEGV, which no Python-level `except` can catch. The parser grows its own stack when it runs low on one, so it returns trees deeper than the passes after it can walk. Those passes recurse without a limit of their own, and the symbol table, which does apply one, runs last. `compile(..., PyCF_ONLY_AST)` returns before reaching it at all. Apply that same limit to the tree right after it is parsed, and count bracket nesting before parsing rather than after, as CPython's tokenizer does. Checking afterwards built the tree first, and one nested that deep exhausts the stack when it is dropped. Fixes #7655 Assisted-by: Claude Code:Opus 5 * Return Result from too_deeply_nested_error Lets the four call sites use `?` instead of matching on `Some` and returning the error by hand, which drops 11 lines. `Result` is already `#[must_use]`, so the attribute on the function goes away with it. Assisted-by: Claude Code:Opus 5 * Return Result from pre_parse_source_error too Same shape as the previous commit, for the other check this PR adds, so the two are consistent with each other. Assisted-by: Claude Code:Opus 5 * Drop exceeds_max_nesting for the check beside it The escape-warning pass needs the bracket check but had no `SourceFile` to build a `CompileError` from, so it got a `bool` wrapper of its own. Building the `SourceFile` there lets it call `pre_parse_source_error` instead, which is shaped like the other checks in that file, and costs nothing measurable. Assisted-by: Claude Code:Opus 5
52 lines
2.0 KiB
Python
52 lines
2.0 KiB
Python
import ast
|
|
|
|
from testutils import assert_raises
|
|
|
|
# Deeply nested source used to exhaust the native stack, killing the process
|
|
# with a signal no `except` here could catch. What is asserted below is mostly
|
|
# that this file finishes at all: reaching the end means each case raised a
|
|
# Python exception instead of taking the interpreter down.
|
|
#
|
|
# Two limits keep that from happening, and each has to apply before a tree that
|
|
# deep is built. The parser grows its own stack when it runs low on one, and a
|
|
# tree it returns costs stack both to walk and to drop, so checking afterwards
|
|
# is already too late.
|
|
|
|
|
|
def nested_binop(depth):
|
|
return "x" + "+1" * depth
|
|
|
|
|
|
def nested_brackets(depth):
|
|
return "[" * depth + "1" + "]" * depth
|
|
|
|
|
|
# Brackets are counted in the source text before it is parsed, the way CPython
|
|
# counts them in its tokenizer, so no tree is built for these at all.
|
|
assert_raises(SyntaxError, compile, nested_brackets(200000), "<test>", "exec")
|
|
assert_raises(SyntaxError, ast.parse, nested_brackets(200000))
|
|
assert_raises(SyntaxError, compile, nested_brackets(5000), "<test>", "exec")
|
|
assert_raises(SyntaxError, ast.parse, nested_brackets(5000))
|
|
|
|
# Nesting that reaches the parser is bounded by the compiler's recursion limit,
|
|
# applied to the tree it returns. CPython bounds the same walks by how much
|
|
# stack is left rather than by a count, so it accepts depths rejected here;
|
|
# either outcome is fine, crashing is not.
|
|
try:
|
|
compile(nested_binop(20000), "<test>", "exec")
|
|
except RecursionError:
|
|
pass
|
|
|
|
# `compile(..., PyCF_ONLY_AST)`, which `ast.parse` uses, returns before the
|
|
# symbol table runs, so it used to reach the limit of no pass at all.
|
|
try:
|
|
ast.parse(nested_binop(5000))
|
|
except RecursionError:
|
|
pass
|
|
|
|
# Nesting within both limits still compiles and runs.
|
|
assert compile(nested_binop(100), "<test>", "exec")
|
|
assert compile(nested_brackets(100), "<test>", "exec")
|
|
assert ast.parse(nested_binop(100))
|
|
assert eval(compile(nested_binop(100), "<test>", "eval"), {"x": 0}) == 100
|