From 4e9d68a00594d1c55a599a3c00468ebe4766aec6 Mon Sep 17 00:00:00 2001 From: Sumi Jeong <125195487+sigmaith@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:39:34 +0900 Subject: [PATCH] Limit bracket nesting in the lexer like CPython Source like `'[' * 5000 + '1' + ']' * 5000` makes the parser recurse until the native stack runs out, killing the process with SIGSEGV instead of raising a Python error. CPython stops this in the tokenizer, not the parser: it rejects an opening bracket once `tok->level` hits `MAXLEVEL` (200), so the parser never recurses that deep. Do the same here using the nesting counter the lexer already keeps. As in CPython, `(`, `[` and `{` share one counter and report the same message. Checked against CPython 3.14.6: depth 200 parses, depth 201 raises `SyntaxError: too many nested parentheses`, and brackets inside strings, comments and f-strings are ignored. Deep recursion without brackets, such as long operator chains, still overflows and needs a separate fix. Refs RustPython/RustPython#7655 CPython reference: https://github.com/python/cpython/blob/main/Parser/lexer/lexer.c#L582-L600 Assisted-by: Claude Code:claude-opus-5 --- crates/ruff_python_parser/src/error.rs | 3 +++ crates/ruff_python_parser/src/lexer.rs | 27 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 2ea046d29c262..9d1849a53e969 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -419,6 +419,8 @@ pub enum LexicalErrorType { LineContinuationError, /// An unexpected end of file was encountered. Eof, + /// Parentheses, brackets, or braces nested past the lexer's nesting limit. + TooManyNestedParentheses, /// An unexpected error occurred. OtherError(Box), } @@ -457,6 +459,7 @@ impl std::fmt::Display for LexicalErrorType { write!(f, "Expected a newline after line continuation character") } Self::Eof => write!(f, "unexpected EOF while parsing"), + Self::TooManyNestedParentheses => write!(f, "too many nested parentheses"), Self::OtherError(msg) => write!(f, "{msg}"), Self::UnclosedStringError => { write!(f, "missing closing quote in string literal") diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index b78f8bf55baf1..8c25252bee19a 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -35,6 +35,10 @@ mod interpolated_string; const BOM: char = '\u{feff}'; +/// Maximum depth of nested parentheses, brackets, and braces. +/// Mirrors CPython's `MAXLEVEL` (`Parser/lexer/state.h`). +const MAX_LEVEL: u32 = 200; + /// A lexer for Python source code. #[derive(Debug)] pub struct Lexer<'src> { @@ -145,6 +149,20 @@ impl<'src> Lexer<'src> { std::mem::take(&mut self.current_value) } + /// Returns an error if opening one more bracket would exceed [`MAX_LEVEL`]. + /// + /// CPython rejects over-nested source in its tokenizer (`Parser/lexer/lexer.c`) + /// rather than in the parser, so the recursive descent never gets deep enough + /// to exhaust the native stack. + fn nesting_limit_error(&self) -> Option { + (self.nesting >= MAX_LEVEL).then(|| { + LexicalError::new( + LexicalErrorType::TooManyNestedParentheses, + self.token_range(), + ) + }) + } + /// Helper function to push the given error, updating the current range with the error location /// and return the [`TokenKind::Unknown`] token. fn push_error(&mut self, error: LexicalError) -> TokenKind { @@ -522,6 +540,9 @@ impl<'src> Lexer<'src> { } '~' => TokenKind::Tilde, '(' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lpar } @@ -530,6 +551,9 @@ impl<'src> Lexer<'src> { TokenKind::Rpar } '[' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lsqb } @@ -538,6 +562,9 @@ impl<'src> Lexer<'src> { TokenKind::Rsqb } '{' => { + if let Some(error) = self.nesting_limit_error() { + return self.push_error(error); + } self.nesting += 1; TokenKind::Lbrace }