Skip to content

Commit bbf3206

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 c8a2089 commit bbf3206

2 files changed

Lines changed: 52 additions & 5 deletions

File tree

internal/cbm/cbm.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,39 @@ static void cbm_test_fault_inject(const char *rel_path) {
668668
}
669669
}
670670

671+
/* Pre-parse nesting guard for pathologically nested input. tree-sitter's GLR
672+
* parser recurses once per nesting level inside stack_node_add_link
673+
* (vendored ts_runtime/src/stack.c) while merging ambiguous parse-stack heads.
674+
* The Perl grammar is genuinely ambiguous for `f(...)` (function call vs.
675+
* bareword), so a deeply nested call chain `f(f(f(...)))` drives that recursion
676+
* as deep as the nesting and overflows a small (1 MB Windows) stack *during the
677+
* parse* — before any of the LSP walk-depth guards can fire. Unambiguous
678+
* grammars (C/Java/Python) keep a single stack head and don't hit this, which is
679+
* why only Perl crashed on the Windows/ARM CI runners.
680+
*
681+
* cbm_source_nesting_exceeds scans the raw bytes for the maximum bracket-nesting
682+
* depth and returns true as soon as it passes the cap (early-exit, O(n)). Real
683+
* source never nests brackets this deep, so a file that does is skipped as a
684+
* parse error (zero edges — graceful degradation, never a crash). Brackets in
685+
* strings/comments are counted too: the only consequence of a false positive is
686+
* skipping one absurd file, so string-awareness is not worth the cost. */
687+
#define CBM_PERL_MAX_PARSE_NESTING 128
688+
689+
static bool cbm_source_nesting_exceeds(const char *source, int source_len, int cap) {
690+
int depth = 0;
691+
for (int i = 0; i < source_len; i++) {
692+
char c = source[i];
693+
if (c == '(' || c == '[' || c == '{') {
694+
if (++depth > cap) {
695+
return true;
696+
}
697+
} else if ((c == ')' || c == ']' || c == '}') && depth > 0) {
698+
depth--;
699+
}
700+
}
701+
return false;
702+
}
703+
671704
CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage language,
672705
const char *project, const char *rel_path, int64_t timeout_micros,
673706
const char **extra_defines, const char **include_paths) {
@@ -711,6 +744,17 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
711744
return result;
712745
}
713746

747+
// Skip pathologically nested Perl before tree-sitter's recursive GLR stack
748+
// merge overflows a small stack during the parse (see
749+
// cbm_source_nesting_exceeds). Scoped to Perl: its ambiguous call grammar is
750+
// the only one that drives that recursion to the nesting depth.
751+
if (language == CBM_LANG_PERL &&
752+
cbm_source_nesting_exceeds(source, source_len, CBM_PERL_MAX_PARSE_NESTING)) {
753+
result->has_error = true;
754+
result->error_msg = cbm_arena_strdup(a, "perl source nesting too deep; skipped");
755+
return result;
756+
}
757+
714758
// Get thread-local parser (reused across files on same thread)
715759
TSParser *parser = get_thread_parser(ts_lang, language);
716760
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)