Skip to content

Commit da046da

Browse files
committed
fix(ts-runtime): bound GLR stack-merge recursion to prevent stack overflow
tree-sitter's GLR parser merges ambiguous parse-stack heads recursively in stack_node_add_link (internal/cbm/vendored/ts_runtime/src/stack.c), once per nesting level. On pathologically nested input whose grammar is ambiguous (Perl's paren-optional calls, f(f(f(...)))), that recursion overflows the native stack during the parse — a small ~1 MB Windows thread stack, and even an 8 MB POSIX stack at extreme depth — before any extraction runs. Grammars that are unambiguous here (Java, C++) never trigger the recursive merge. Cap the recursive merge at CBM_TS_STACK_MERGE_MAX_DEPTH (512) via a depth-tracked inner worker; the public stack_node_add_link is a thin wrapper seeding depth 0, so every existing call site is unchanged. Past the cap the ambiguity is left on the GLR stack instead of merged — a valid parse, never a wrong one. 512 frames is well within a 1 MB stack while far exceeding any realistic source nesting. Add perl_glr_deep_parse_recursion_capped: a crash-isolating regression that parses deep ambiguous Perl directly (a forked child on POSIX, in-process on Windows), bypassing cbm_extract_file's CBM_PERL_MAX_PARSE_NESTING pre-parse guard so the vendored cap itself is exercised rather than the guard. Narrow vendored-runtime change; no other parsing behavior is affected. The higher-level Perl pre-parse guard (issue DeusData#913 context) stays in place and can be retired in a follow-up once this cap is proven in the field. Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
1 parent 6b57db2 commit da046da

2 files changed

Lines changed: 119 additions & 4 deletions

File tree

‎internal/cbm/vendored/ts_runtime/src/stack.c‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@
1212
#define MAX_NODE_POOL_SIZE 50
1313
#define MAX_ITERATOR_COUNT 64
1414

15+
// CBM patch: cap the recursive ambiguity-merge in stack_node_add_link.
16+
// On a deeply ambiguous parse (e.g. Perl's optional-paren function calls in a
17+
// f(f(f(...))) chain ~30k deep), the "merge them recursively" path below
18+
// recurses once per nesting level on the C stack (~260 B/frame), overflowing
19+
// the small default thread stack on Windows (~1 MB) and even the 8 MB POSIX
20+
// stack at extreme depth — a SIGSEGV during ts_parser_parse, before any
21+
// language extractor runs. Past this cap we stop eagerly merging and leave the
22+
// ambiguity on the GLR stack, exactly as the existing
23+
// `self->link_count == MAX_LINK_COUNT` bail-out already does: the parse still
24+
// produces a valid tree (graceful degradation), never a wrong one. 512 mirrors
25+
// CBM's CBM_LSP_*_MAX_WALK_DEPTH walk caps; 512*~260 B ~= 130 KB, safe with
26+
// wide headroom on a 1 MB stack, while far exceeding any realistic source
27+
// nesting.
28+
#define CBM_TS_STACK_MERGE_MAX_DEPTH 512
29+
1530
#if defined _WIN32 && !defined __GNUC__
1631
#define forceinline __forceinline
1732
#else
@@ -197,10 +212,15 @@ static bool stack__subtree_is_equivalent(Subtree left, Subtree right) {
197212
);
198213
}
199214

200-
static void stack_node_add_link(
215+
// CBM patch: depth-tracked inner worker. `depth` counts the recursive
216+
// ambiguity-merge nesting; past CBM_TS_STACK_MERGE_MAX_DEPTH we stop merging
217+
// (see the macro comment). The public stack_node_add_link below is a thin
218+
// wrapper that seeds depth = 0, so all existing call sites are unchanged.
219+
static void stack_node_add_link_inner(
201220
StackNode *self,
202221
StackLink link,
203-
SubtreePool *subtree_pool
222+
SubtreePool *subtree_pool,
223+
unsigned depth
204224
) {
205225
if (link.node == self) return;
206226

@@ -231,8 +251,12 @@ static void stack_node_add_link(
231251
existing_link->node->position.bytes == link.node->position.bytes &&
232252
existing_link->node->error_cost == link.node->error_cost
233253
) {
234-
for (int j = 0; j < link.node->link_count; j++) {
235-
stack_node_add_link(existing_link->node, link.node->links[j], subtree_pool);
254+
// CBM patch: bound the recursive merge to keep the C stack finite.
255+
if (depth < CBM_TS_STACK_MERGE_MAX_DEPTH) {
256+
for (int j = 0; j < link.node->link_count; j++) {
257+
stack_node_add_link_inner(existing_link->node, link.node->links[j],
258+
subtree_pool, depth + 1);
259+
}
236260
}
237261
int32_t dynamic_precedence = link.node->dynamic_precedence;
238262
if (link.subtree.ptr) {
@@ -263,6 +287,15 @@ static void stack_node_add_link(
263287
if (dynamic_precedence > self->dynamic_precedence) self->dynamic_precedence = dynamic_precedence;
264288
}
265289

290+
// CBM patch: public entry point — unchanged signature, seeds recursion depth.
291+
static void stack_node_add_link(
292+
StackNode *self,
293+
StackLink link,
294+
SubtreePool *subtree_pool
295+
) {
296+
stack_node_add_link_inner(self, link, subtree_pool, 0);
297+
}
298+
266299
static void stack_head_delete(
267300
StackHead *self,
268301
StackNodeArray *pool,

‎tests/test_stack_overflow.c‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212
#include "test_framework.h"
1313
#include "cbm.h"
14+
#include "lang_specs.h" /* cbm_ts_language — direct-parse GLR cap regression (#913) */
1415

1516
#include <stdio.h>
1617
#include <stdlib.h>
@@ -440,6 +441,53 @@ static bool so_extract_crashes(const char *content, CBMLanguage lang, const char
440441
#endif
441442
}
442443

444+
/* Parse `content` with tree-sitter DIRECTLY in a forked child — bypassing
445+
* cbm_extract_file's Perl pre-parse nesting guard — returning true if the child
446+
* died by signal. This is the crash-isolating regression for the vendored GLR
447+
* stack-merge recursion cap (CBM_TS_STACK_MERGE_MAX_DEPTH, ts_runtime/src/stack.c):
448+
* the extract-level guard skips pathologically nested Perl before it reaches the
449+
* parser, so only a direct parse exercises the cap. Windows runs in-process (a
450+
* real crash aborts the runner — a visible failure), mirroring so_extract_crashes. */
451+
static bool so_parse_crashes(const char *content, CBMLanguage lang) {
452+
const TSLanguage *ts_lang = cbm_ts_language(lang);
453+
if (!ts_lang) {
454+
return false;
455+
}
456+
#if defined(_WIN32)
457+
TSParser *parser = ts_parser_new();
458+
if (parser) {
459+
ts_parser_set_language(parser, ts_lang);
460+
TSTree *tree = ts_parser_parse_string(parser, NULL, content, (uint32_t)strlen(content));
461+
if (tree) {
462+
ts_tree_delete(tree);
463+
}
464+
ts_parser_delete(parser);
465+
}
466+
return false;
467+
#else
468+
fflush(NULL);
469+
pid_t pid = fork();
470+
if (pid < 0) {
471+
return false;
472+
}
473+
if (pid == 0) {
474+
TSParser *parser = ts_parser_new();
475+
if (parser) {
476+
ts_parser_set_language(parser, ts_lang);
477+
TSTree *tree = ts_parser_parse_string(parser, NULL, content, (uint32_t)strlen(content));
478+
if (tree) {
479+
ts_tree_delete(tree);
480+
}
481+
ts_parser_delete(parser);
482+
}
483+
_exit(0);
484+
}
485+
int status = 0;
486+
(void)waitpid(pid, &status, 0);
487+
return WIFSIGNALED(status);
488+
#endif
489+
}
490+
443491
TEST(lsp_java_deep_nesting_no_crash) {
444492
/* Deeply nested call expressions — the same shape as the elasticsearch
445493
* crash (fast SIGSEGV under recursive java_resolve_calls_in_node frames;
@@ -541,6 +589,39 @@ TEST(lsp_perl_deep_expression_no_crash) {
541589
PASS();
542590
}
543591

592+
TEST(perl_glr_deep_parse_recursion_capped) {
593+
/* Issue #913 — the proper fix for what lsp_perl_deep_expression_no_crash's
594+
* pre-parse guard only works around. Parse deeply nested ambiguous Perl
595+
* f(f(f(...f(1)...))) DIRECTLY (past the CBM_PERL_MAX_PARSE_NESTING guard,
596+
* which would otherwise skip it). Perl's paren-optional call grammar makes
597+
* each level ambiguous, so tree-sitter's GLR parser merges the ambiguous
598+
* parse-stack heads recursively — stack_node_add_link in
599+
* ts_runtime/src/stack.c, once per nesting level — overflowing the native
600+
* stack (a ~1 MB Windows stack, and even an 8 MB POSIX stack at this depth)
601+
* during the parse, before any extraction runs. The
602+
* CBM_TS_STACK_MERGE_MAX_DEPTH cap stops merging past the bound: the
603+
* ambiguity is left on the GLR stack instead of merged — a valid parse,
604+
* never a wrong one — so the parse returns cleanly instead of crashing.
605+
* Depth mirrors lsp_perl_deep_expression_no_crash. */
606+
const int DEPTH = 30000;
607+
size_t sz = (size_t)DEPTH * 3 + 256;
608+
char *src = malloc(sz);
609+
ASSERT_NOT_NULL(src);
610+
char *p = src;
611+
p += snprintf(p, sz, "sub f { return $_[0]; }\nsub g { return ");
612+
for (int i = 0; i < DEPTH; i++) {
613+
*p++ = 'f';
614+
*p++ = '(';
615+
}
616+
*p++ = '1';
617+
memset(p, ')', DEPTH);
618+
p += DEPTH;
619+
snprintf(p, sz - (size_t)(p - src), "; }\n");
620+
ASSERT_FALSE(so_parse_crashes(src, CBM_LANG_PERL));
621+
free(src);
622+
PASS();
623+
}
624+
544625
TEST(lsp_java_lambda_args_exceed_params_no_crash) {
545626
/* A call with MORE arguments than the resolved method's declared params:
546627
* bind_lambda_args indexed the NULL-terminated signature param_types array
@@ -679,6 +760,7 @@ SUITE(stack_overflow) {
679760
RUN_TEST(lsp_cpp_deep_expression_no_crash);
680761
RUN_TEST(lsp_python_deep_expression_no_crash);
681762
RUN_TEST(lsp_perl_deep_expression_no_crash);
763+
RUN_TEST(perl_glr_deep_parse_recursion_capped);
682764
RUN_TEST(lsp_ts_cyclic_types_no_crash);
683765
RUN_TEST(lsp_python_deep_nesting_no_crash);
684766
RUN_TEST(lsp_go_deep_nesting_no_crash);

0 commit comments

Comments
 (0)