From 0bf1b68e33beef547c8e6c49c185fbdad8694a3d Mon Sep 17 00:00:00 2001 From: Evgeniy Gorbanyov Date: Tue, 20 Oct 2026 12:38:39 +0600 Subject: [PATCH] fix: reject open-token nesting past the fixed stack Parentheses and braces were written into fixed-size stacks without checking STACK_DEPTH, so a deep nest overflowed the lexer object. Signed-off-by: Evgeniy Gorbanyov --- include/merve/parser.h | 1 + include/merve_c.h | 1 + rust/deps/merve.cpp | 28 +++++++++++++++++++++++----- rust/deps/merve.h | 1 + rust/deps/merve_c.h | 1 + rust/src/lib.rs | 12 +++++++++++- src/parser.cpp | 27 ++++++++++++++++++++++----- tests/c_api_compile_test.c | 1 + tests/c_api_tests.cpp | 10 ++++++++++ tests/real_world_tests.cpp | 28 ++++++++++++++++++++++++++++ 10 files changed, 99 insertions(+), 11 deletions(-) diff --git a/include/merve/parser.h b/include/merve/parser.h index 6458872..86056e9 100644 --- a/include/merve/parser.h +++ b/include/merve/parser.h @@ -36,6 +36,7 @@ enum lexer_error { // Resource limit errors TEMPLATE_NEST_OVERFLOW, ///< Template literal nesting too deep + OPEN_TOKEN_OVERFLOW, ///< Parenthesis or brace nesting too deep }; /** diff --git a/include/merve_c.h b/include/merve_c.h index 035ce21..496d698 100644 --- a/include/merve_c.h +++ b/include/merve_c.h @@ -66,6 +66,7 @@ typedef struct { #define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10 #define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11 #define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12 +#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13 #ifdef __cplusplus extern "C" { diff --git a/rust/deps/merve.cpp b/rust/deps/merve.cpp index c1ccdbe..a44fa0e 100644 --- a/rust/deps/merve.cpp +++ b/rust/deps/merve.cpp @@ -520,6 +520,20 @@ class CJSLexer { } // Parsing utilities + + // STACK_DEPTH slots. A deeper '(' or '{' is a parse error. + bool pushOpenToken(char type, const char* tokenPos, bool isClass = false) { + if (openTokenDepth >= STACK_DEPTH) { + syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW, tokenPos); + return false; + } + if (type == '{') + openClassPosStack[openTokenDepth] = isClass; + openTokenTypeStack_[openTokenDepth] = type; + openTokenPosStack_[openTokenDepth++] = tokenPos; + return true; + } + void syntaxError(lexer_error code, const char* at = nullptr) { if (!last_error) { last_error = code; @@ -633,6 +647,10 @@ class CJSLexer { syntaxError(lexer_error::TEMPLATE_NEST_OVERFLOW); return; } + if (openTokenDepth >= STACK_DEPTH) { + syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW); + return; + } templateStack_[templateStackDepth++] = templateDepth; templateDepth = ++openTokenDepth; return; @@ -1682,8 +1700,8 @@ class CJSLexer { tryParseObjectDefineOrKeys(openTokenDepth == 0); break; case '(': - openTokenTypeStack_[openTokenDepth] = '('; - openTokenPosStack_[openTokenDepth++] = lastTokenPos; + if (!pushOpenToken('(', lastTokenPos)) + return false; break; case ')': if (openTokenDepth == 0) { @@ -1693,10 +1711,9 @@ class CJSLexer { openTokenDepth--; break; case '{': - openClassPosStack[openTokenDepth] = nextBraceIsClass; + if (!pushOpenToken('{', lastTokenPos, nextBraceIsClass)) + return false; nextBraceIsClass = false; - openTokenTypeStack_[openTokenDepth] = '{'; - openTokenPosStack_[openTokenDepth++] = lastTokenPos; break; case '}': if (openTokenDepth == 0) { @@ -1880,6 +1897,7 @@ typedef struct { #define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10 #define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11 #define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12 +#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13 #ifdef __cplusplus extern "C" { diff --git a/rust/deps/merve.h b/rust/deps/merve.h index 61a4da8..f9debec 100644 --- a/rust/deps/merve.h +++ b/rust/deps/merve.h @@ -62,6 +62,7 @@ enum lexer_error { // Resource limit errors TEMPLATE_NEST_OVERFLOW, ///< Template literal nesting too deep + OPEN_TOKEN_OVERFLOW, ///< Parenthesis or brace nesting too deep }; /** diff --git a/rust/deps/merve_c.h b/rust/deps/merve_c.h index 035ce21..496d698 100644 --- a/rust/deps/merve_c.h +++ b/rust/deps/merve_c.h @@ -66,6 +66,7 @@ typedef struct { #define MERVE_ERROR_UNEXPECTED_ESM_IMPORT 10 #define MERVE_ERROR_UNEXPECTED_ESM_EXPORT 11 #define MERVE_ERROR_TEMPLATE_NEST_OVERFLOW 12 +#define MERVE_ERROR_OPEN_TOKEN_OVERFLOW 13 #ifdef __cplusplus extern "C" { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 841fe01..d344274 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -52,6 +52,7 @@ pub enum LexerError { UnexpectedEsmImport, UnexpectedEsmExport, TemplateNestOverflow, + OpenTokenOverflow, /// An error code not recognized by these bindings. Unknown(i32), } @@ -74,6 +75,7 @@ impl LexerError { 10 => Self::UnexpectedEsmImport, 11 => Self::UnexpectedEsmExport, 12 => Self::TemplateNestOverflow, + 13 => Self::OpenTokenOverflow, other => Self::Unknown(other), } } @@ -95,6 +97,7 @@ impl LexerError { Self::UnexpectedEsmImport => "unexpected ESM import", Self::UnexpectedEsmExport => "unexpected ESM export", Self::TemplateNestOverflow => "template nesting overflow", + Self::OpenTokenOverflow => "open token nesting overflow", Self::Unknown(_) => "unknown error", } } @@ -530,6 +533,13 @@ mod tests { assert_eq!(err.kind, LexerError::UnexpectedEsmExport); } + #[test] + fn open_token_overflow() { + let source = "(".repeat(2048 * 4); + let err = parse_commonjs(&source).unwrap_err(); + assert_eq!(err.kind, LexerError::OpenTokenOverflow); + } + #[test] fn empty_input() { let result = parse_commonjs(""); @@ -694,7 +704,7 @@ mod tests { #[test] fn error_from_code_roundtrip() { - for code in 0..=12 { + for code in 0..=13 { let err = LexerError::from_code(code); assert_ne!(err, LexerError::Unknown(code)); } diff --git a/src/parser.cpp b/src/parser.cpp index da67699..e2c74ef 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -518,6 +518,20 @@ class CJSLexer { } // Parsing utilities + + // STACK_DEPTH slots. A deeper '(' or '{' is a parse error. + bool pushOpenToken(char type, const char* tokenPos, bool isClass = false) { + if (openTokenDepth >= STACK_DEPTH) { + syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW, tokenPos); + return false; + } + if (type == '{') + openClassPosStack[openTokenDepth] = isClass; + openTokenTypeStack_[openTokenDepth] = type; + openTokenPosStack_[openTokenDepth++] = tokenPos; + return true; + } + void syntaxError(lexer_error code, const char* at = nullptr) { if (!last_error) { last_error = code; @@ -631,6 +645,10 @@ class CJSLexer { syntaxError(lexer_error::TEMPLATE_NEST_OVERFLOW); return; } + if (openTokenDepth >= STACK_DEPTH) { + syntaxError(lexer_error::OPEN_TOKEN_OVERFLOW); + return; + } templateStack_[templateStackDepth++] = templateDepth; templateDepth = ++openTokenDepth; return; @@ -1680,8 +1698,8 @@ class CJSLexer { tryParseObjectDefineOrKeys(openTokenDepth == 0); break; case '(': - openTokenTypeStack_[openTokenDepth] = '('; - openTokenPosStack_[openTokenDepth++] = lastTokenPos; + if (!pushOpenToken('(', lastTokenPos)) + return false; break; case ')': if (openTokenDepth == 0) { @@ -1691,10 +1709,9 @@ class CJSLexer { openTokenDepth--; break; case '{': - openClassPosStack[openTokenDepth] = nextBraceIsClass; + if (!pushOpenToken('{', lastTokenPos, nextBraceIsClass)) + return false; nextBraceIsClass = false; - openTokenTypeStack_[openTokenDepth] = '{'; - openTokenPosStack_[openTokenDepth++] = lastTokenPos; break; case '}': if (openTokenDepth == 0) { diff --git a/tests/c_api_compile_test.c b/tests/c_api_compile_test.c index e9419de..7608f8d 100644 --- a/tests/c_api_compile_test.c +++ b/tests/c_api_compile_test.c @@ -47,6 +47,7 @@ static void check_types(void) { MERVE_ERROR_UNEXPECTED_ESM_IMPORT, MERVE_ERROR_UNEXPECTED_ESM_EXPORT, MERVE_ERROR_TEMPLATE_NEST_OVERFLOW, + MERVE_ERROR_OPEN_TOKEN_OVERFLOW, }; (void)errors; } diff --git a/tests/c_api_tests.cpp b/tests/c_api_tests.cpp index ebc7386..ad0c1fb 100644 --- a/tests/c_api_tests.cpp +++ b/tests/c_api_tests.cpp @@ -2,6 +2,7 @@ #include "gtest/gtest.h" #include +#include // Helper: compare merve_string to a C string literal. static bool merve_string_eq(merve_string s, const char* expected) { @@ -482,3 +483,12 @@ TEST(c_api_tests, spread_reexports) { ASSERT_TRUE(merve_string_eq(merve_get_reexport_name(result, 1), "dep2")); merve_free(result); } + +TEST(c_api_tests, open_token_overflow) { + const std::string source(2048 * 4, '('); + merve_analysis result = merve_parse_commonjs(source.data(), source.size()); + ASSERT_NE(result, nullptr); + ASSERT_FALSE(merve_is_valid(result)); + ASSERT_EQ(merve_get_last_error(), MERVE_ERROR_OPEN_TOKEN_OVERFLOW); + merve_free(result); +} diff --git a/tests/real_world_tests.cpp b/tests/real_world_tests.cpp index f320bc3..72780d7 100644 --- a/tests/real_world_tests.cpp +++ b/tests/real_world_tests.cpp @@ -1306,6 +1306,34 @@ TEST(real_world_tests, require_at_start_of_input) { ASSERT_TRUE(result2.has_value()); } +// Matches lexer::STACK_DEPTH in src/parser.cpp. +constexpr size_t kOpenTokenStackDepth = 2048; + +TEST(real_world_tests, open_token_stack_depth_still_parses) { + std::string source(kOpenTokenStackDepth, '('); + source.append(kOpenTokenStackDepth, ')'); + source += "exports.depth = 1;"; + auto result = lexer::parse_commonjs(source); + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result->exports.size(), 1); + ASSERT_EQ(lexer::get_string_view(result->exports[0]), "depth"); +} + +TEST(real_world_tests, open_token_overflow) { + std::string source(kOpenTokenStackDepth * 4, '('); + auto result = lexer::parse_commonjs(source); + ASSERT_FALSE(result.has_value()); + ASSERT_EQ(lexer::get_last_error(), lexer::lexer_error::OPEN_TOKEN_OVERFLOW); +} + +TEST(real_world_tests, open_token_overflow_inside_template) { + std::string source(kOpenTokenStackDepth, '('); + source += "`${"; + auto result = lexer::parse_commonjs(source); + ASSERT_FALSE(result.has_value()); + ASSERT_EQ(lexer::get_last_error(), lexer::lexer_error::OPEN_TOKEN_OVERFLOW); +} + TEST(real_world_tests, line_numbers_after_block_comment) { auto result = lexer::parse_commonjs( "/*\n"