Skip to content

Commit ff68247

Browse files
fix(perl-lsp): guard pathologically nested Perl before tree-sitter parse
A deeply nested Perl call chain f(f(f(...))) overflowed the stack while parsing on small-stack platforms: tree-sitter's GLR parser recurses once per nesting level in stack_node_add_link (vendored ts_runtime/src/stack.c) merging the ambiguous parse-stack heads that Perl's f(...) grammar produces. The overflow happens during ts_parser_parse, before any LSP walk runs, so the CBM_LSP_PERL_MAX_WALK_DEPTH guards can never fire. This crashed test-windows (CLANG64) with an AddressSanitizer stack-overflow and hung test-unix (ubuntu-24.04-arm) to a 4h timeout on lsp_perl_deep_expression_no_crash. Add cbm_source_nesting_exceeds(): an O(n) early-exit bracket-depth scan, and skip extraction (clean has_error, zero edges) for Perl input whose nesting exceeds CBM_PERL_MAX_PARSE_NESTING (128) before it reaches tree-sitter. Scoped to Perl because only its ambiguous call grammar drives the GLR recursion to the nesting depth; C/Java/Python parse the same shape with a single stack head. 128 is ~10x above any real Perl nesting and well below the depth where the parser overflows a 1MB stack. Correct the lsp_perl_deep_expression_no_crash comment to describe the actual mechanism (parser recursion, not the LSP walk). Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
1 parent 123afbe commit ff68247

2 files changed

Lines changed: 56 additions & 5 deletions

File tree

internal/cbm/cbm.c

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,43 @@ static void cbm_test_fault_inject(const char *rel_path) {
684684
}
685685
}
686686

687+
/* Pre-parse nesting guard for pathologically nested input. tree-sitter's GLR
688+
* parser recurses once per nesting level inside stack_node_add_link
689+
* (vendored ts_runtime/src/stack.c) while merging ambiguous parse-stack heads.
690+
* The Perl grammar is genuinely ambiguous for `f(...)` (function call vs.
691+
* bareword), so a deeply nested call chain `f(f(f(...)))` drives that recursion
692+
* as deep as the nesting and overflows a small (1 MB Windows) stack *during the
693+
* parse* — before any of the LSP walk-depth guards can fire. Unambiguous
694+
* grammars (C/Java/Python) keep a single stack head and don't hit this, which is
695+
* why only Perl crashed on the Windows/ARM CI runners.
696+
*
697+
* This is a workaround: the proper fix is bounding the GLR stack-merge recursion
698+
* inside the vendored tree-sitter runtime, tracked upstream as #913. Remove this
699+
* guard once that lands.
700+
*
701+
* cbm_source_nesting_exceeds scans the raw bytes for the maximum bracket-nesting
702+
* depth and returns true as soon as it passes the cap (early-exit, O(n)). Real
703+
* source never nests brackets this deep, so a file that does is skipped as a
704+
* parse error (zero edges — graceful degradation, never a crash). Brackets in
705+
* strings/comments are counted too: the only consequence of a false positive is
706+
* skipping one absurd file, so string-awareness is not worth the cost. */
707+
#define CBM_PERL_MAX_PARSE_NESTING 128
708+
709+
static bool cbm_source_nesting_exceeds(const char *source, int source_len, int cap) {
710+
int depth = 0;
711+
for (int i = 0; i < source_len; i++) {
712+
char c = source[i];
713+
if (c == '(' || c == '[' || c == '{') {
714+
if (++depth > cap) {
715+
return true;
716+
}
717+
} else if ((c == ')' || c == ']' || c == '}') && depth > 0) {
718+
depth--;
719+
}
720+
}
721+
return false;
722+
}
723+
687724
static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len,
688725
CBMLanguage language, const char *project,
689726
const char *rel_path, int64_t timeout_micros,
@@ -747,6 +784,17 @@ static CBMFileResult *cbm_extract_file_impl(const char *source, int source_len,
747784
return result;
748785
}
749786

787+
// Skip pathologically nested Perl before tree-sitter's recursive GLR stack
788+
// merge overflows a small stack during the parse (see
789+
// cbm_source_nesting_exceeds). Scoped to Perl: its ambiguous call grammar is
790+
// the only one that drives that recursion to the nesting depth.
791+
if (language == CBM_LANG_PERL &&
792+
cbm_source_nesting_exceeds(source, source_len, CBM_PERL_MAX_PARSE_NESTING)) {
793+
result->has_error = true;
794+
result->error_msg = cbm_arena_strdup(a, "perl source nesting too deep; skipped");
795+
return result;
796+
}
797+
750798
// Get thread-local parser (reused across files on same thread)
751799
TSParser *parser = get_thread_parser(ts_lang, language);
752800
if (!parser) {

tests/test_stack_overflow.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -513,11 +513,14 @@ TEST(lsp_python_deep_expression_no_crash) {
513513
}
514514

515515
TEST(lsp_perl_deep_expression_no_crash) {
516-
/* Deeply nested Perl call expressions f(f(f(...f(1)...))). The Perl AST
517-
* walkers (perl_resolve_calls_in_node / perl_pass1_scan) recurse per child
518-
* with no depth bound before QA round 1 — the same SIGSEGV-prone shape as
519-
* the Java/C++ walkers. The CBM_LSP_PERL_MAX_WALK_DEPTH guard must cap the
520-
* recursion so this does not overflow the stack. See
516+
/* Deeply nested Perl call expressions f(f(f(...f(1)...))). Unlike the
517+
* Java/C++ cases, the overflow here is NOT in the LSP walk — it is in
518+
* tree-sitter's own GLR parser: stack_node_add_link (vendored
519+
* ts_runtime/src/stack.c) recurses once per nesting level while merging the
520+
* ambiguous parse-stack heads that Perl's `f(...)` grammar produces, blowing
521+
* a small (1 MB Windows) stack during the parse, before any LSP walk runs.
522+
* The CBM_PERL_MAX_PARSE_NESTING pre-parse guard in cbm_extract_file skips
523+
* such input so it never reaches tree-sitter. See
521524
* lsp_java_deep_nesting_no_crash on the depth choice. */
522525
const int DEPTH = 30000;
523526
size_t sz = (size_t)DEPTH * 3 + 256;

0 commit comments

Comments
 (0)