mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-28 07:03:20 +08:00
Merge pull request #1002 from halindrome/p913-glr-recursion
fix(ts-runtime): bound GLR stack-merge recursion to prevent stack overflow
This commit is contained in:
@@ -13,6 +13,17 @@ The tree-sitter C runtime is vendored in `internal/cbm/vendored/ts_runtime/`.
|
||||
- **License:** MIT
|
||||
- **Copyright:** (c) 2018–2024 Max Brunsfeld
|
||||
|
||||
**Local modification** (`internal/cbm/vendored/ts_runtime/src/stack.c`, #913): a
|
||||
single CBM patch bounds the recursive ambiguity-merge in `stack_node_add_link`
|
||||
at `CBM_TS_STACK_MERGE_MAX_DEPTH` (512). Deeply nested grammar-ambiguous input
|
||||
(e.g. Perl `f(f(f(...)))`) otherwise recurses once per level on the native C
|
||||
stack and overflows it during parsing (SIGSEGV on the ~1 MB Windows thread
|
||||
stack, and even the 8 MB POSIX stack at extreme depth) before any extractor
|
||||
runs. Past the cap the ambiguity is left on the GLR stack instead of merged —
|
||||
still a valid parse, never a wrong one — mirroring the existing
|
||||
`MAX_LINK_COUNT` bail-out. The change is clearly marked `// CBM patch:` inline.
|
||||
**On re-vendor (e.g. ts_runtime → 0.26.x): re-apply this bound.**
|
||||
|
||||
The shared scanner helpers in `internal/cbm/vendored/common/` (`scanner.h`,
|
||||
`tag.h`) originate from
|
||||
[tree-sitter-html](https://github.com/tree-sitter/tree-sitter-html) (MIT,
|
||||
|
||||
@@ -12,6 +12,21 @@
|
||||
#define MAX_NODE_POOL_SIZE 50
|
||||
#define MAX_ITERATOR_COUNT 64
|
||||
|
||||
// CBM patch: cap the recursive ambiguity-merge in stack_node_add_link.
|
||||
// On a deeply ambiguous parse (e.g. Perl's optional-paren function calls in a
|
||||
// f(f(f(...))) chain ~30k deep), the "merge them recursively" path below
|
||||
// recurses once per nesting level on the C stack (~260 B/frame), overflowing
|
||||
// the small default thread stack on Windows (~1 MB) and even the 8 MB POSIX
|
||||
// stack at extreme depth — a SIGSEGV during ts_parser_parse, before any
|
||||
// language extractor runs. Past this cap we stop eagerly merging and leave the
|
||||
// ambiguity on the GLR stack, exactly as the existing
|
||||
// `self->link_count == MAX_LINK_COUNT` bail-out already does: the parse still
|
||||
// produces a valid tree (graceful degradation), never a wrong one. 512 mirrors
|
||||
// CBM's CBM_LSP_*_MAX_WALK_DEPTH walk caps; 512*~260 B ~= 130 KB, safe with
|
||||
// wide headroom on a 1 MB stack, while far exceeding any realistic source
|
||||
// nesting.
|
||||
#define CBM_TS_STACK_MERGE_MAX_DEPTH 512
|
||||
|
||||
#if defined _WIN32 && !defined __GNUC__
|
||||
#define forceinline __forceinline
|
||||
#else
|
||||
@@ -197,10 +212,15 @@ static bool stack__subtree_is_equivalent(Subtree left, Subtree right) {
|
||||
);
|
||||
}
|
||||
|
||||
static void stack_node_add_link(
|
||||
// CBM patch: depth-tracked inner worker. `depth` counts the recursive
|
||||
// ambiguity-merge nesting; past CBM_TS_STACK_MERGE_MAX_DEPTH we stop merging
|
||||
// (see the macro comment). The public stack_node_add_link below is a thin
|
||||
// wrapper that seeds depth = 0, so all existing call sites are unchanged.
|
||||
static void stack_node_add_link_inner(
|
||||
StackNode *self,
|
||||
StackLink link,
|
||||
SubtreePool *subtree_pool
|
||||
SubtreePool *subtree_pool,
|
||||
unsigned depth
|
||||
) {
|
||||
if (link.node == self) return;
|
||||
|
||||
@@ -231,8 +251,12 @@ static void stack_node_add_link(
|
||||
existing_link->node->position.bytes == link.node->position.bytes &&
|
||||
existing_link->node->error_cost == link.node->error_cost
|
||||
) {
|
||||
for (int j = 0; j < link.node->link_count; j++) {
|
||||
stack_node_add_link(existing_link->node, link.node->links[j], subtree_pool);
|
||||
// CBM patch: bound the recursive merge to keep the C stack finite.
|
||||
if (depth < CBM_TS_STACK_MERGE_MAX_DEPTH) {
|
||||
for (int j = 0; j < link.node->link_count; j++) {
|
||||
stack_node_add_link_inner(existing_link->node, link.node->links[j],
|
||||
subtree_pool, depth + 1);
|
||||
}
|
||||
}
|
||||
int32_t dynamic_precedence = link.node->dynamic_precedence;
|
||||
if (link.subtree.ptr) {
|
||||
@@ -263,6 +287,15 @@ static void stack_node_add_link(
|
||||
if (dynamic_precedence > self->dynamic_precedence) self->dynamic_precedence = dynamic_precedence;
|
||||
}
|
||||
|
||||
// CBM patch: public entry point — unchanged signature, seeds recursion depth.
|
||||
static void stack_node_add_link(
|
||||
StackNode *self,
|
||||
StackLink link,
|
||||
SubtreePool *subtree_pool
|
||||
) {
|
||||
stack_node_add_link_inner(self, link, subtree_pool, 0);
|
||||
}
|
||||
|
||||
static void stack_head_delete(
|
||||
StackHead *self,
|
||||
StackNodeArray *pool,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
#include "lang_specs.h" /* cbm_ts_language — direct-parse GLR cap regression (#913) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -440,6 +441,53 @@ static bool so_extract_crashes(const char *content, CBMLanguage lang, const char
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Parse `content` with tree-sitter DIRECTLY in a forked child — bypassing
|
||||
* cbm_extract_file's Perl pre-parse nesting guard — returning true if the child
|
||||
* died by signal. This is the crash-isolating regression for the vendored GLR
|
||||
* stack-merge recursion cap (CBM_TS_STACK_MERGE_MAX_DEPTH, ts_runtime/src/stack.c):
|
||||
* the extract-level guard skips pathologically nested Perl before it reaches the
|
||||
* parser, so only a direct parse exercises the cap. Windows runs in-process (a
|
||||
* real crash aborts the runner — a visible failure), mirroring so_extract_crashes. */
|
||||
static bool so_parse_crashes(const char *content, CBMLanguage lang) {
|
||||
const TSLanguage *ts_lang = cbm_ts_language(lang);
|
||||
if (!ts_lang) {
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (parser) {
|
||||
ts_parser_set_language(parser, ts_lang);
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, content, (uint32_t)strlen(content));
|
||||
if (tree) {
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
fflush(NULL);
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (parser) {
|
||||
ts_parser_set_language(parser, ts_lang);
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, content, (uint32_t)strlen(content));
|
||||
if (tree) {
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
(void)waitpid(pid, &status, 0);
|
||||
return WIFSIGNALED(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(lsp_java_deep_nesting_no_crash) {
|
||||
/* Deeply nested call expressions — the same shape as the elasticsearch
|
||||
* crash (fast SIGSEGV under recursive java_resolve_calls_in_node frames;
|
||||
@@ -541,6 +589,39 @@ TEST(lsp_perl_deep_expression_no_crash) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(perl_glr_deep_parse_recursion_capped) {
|
||||
/* Issue #913 — the proper fix for what lsp_perl_deep_expression_no_crash's
|
||||
* pre-parse guard only works around. Parse deeply nested ambiguous Perl
|
||||
* f(f(f(...f(1)...))) DIRECTLY (past the CBM_PERL_MAX_PARSE_NESTING guard,
|
||||
* which would otherwise skip it). Perl's paren-optional call grammar makes
|
||||
* each level ambiguous, so tree-sitter's GLR parser merges the ambiguous
|
||||
* parse-stack heads recursively — stack_node_add_link in
|
||||
* ts_runtime/src/stack.c, once per nesting level — overflowing the native
|
||||
* stack (a ~1 MB Windows stack, and even an 8 MB POSIX stack at this depth)
|
||||
* during the parse, before any extraction runs. The
|
||||
* CBM_TS_STACK_MERGE_MAX_DEPTH cap stops merging past the bound: the
|
||||
* ambiguity is left on the GLR stack instead of merged — a valid parse,
|
||||
* never a wrong one — so the parse returns cleanly instead of crashing.
|
||||
* Depth mirrors lsp_perl_deep_expression_no_crash. */
|
||||
const int DEPTH = 30000;
|
||||
size_t sz = (size_t)DEPTH * 3 + 256;
|
||||
char *src = malloc(sz);
|
||||
ASSERT_NOT_NULL(src);
|
||||
char *p = src;
|
||||
p += snprintf(p, sz, "sub f { return $_[0]; }\nsub g { return ");
|
||||
for (int i = 0; i < DEPTH; i++) {
|
||||
*p++ = 'f';
|
||||
*p++ = '(';
|
||||
}
|
||||
*p++ = '1';
|
||||
memset(p, ')', DEPTH);
|
||||
p += DEPTH;
|
||||
snprintf(p, sz - (size_t)(p - src), "; }\n");
|
||||
ASSERT_FALSE(so_parse_crashes(src, CBM_LANG_PERL));
|
||||
free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(lsp_java_lambda_args_exceed_params_no_crash) {
|
||||
/* A call with MORE arguments than the resolved method's declared params:
|
||||
* bind_lambda_args indexed the NULL-terminated signature param_types array
|
||||
@@ -679,6 +760,7 @@ SUITE(stack_overflow) {
|
||||
RUN_TEST(lsp_cpp_deep_expression_no_crash);
|
||||
RUN_TEST(lsp_python_deep_expression_no_crash);
|
||||
RUN_TEST(lsp_perl_deep_expression_no_crash);
|
||||
RUN_TEST(perl_glr_deep_parse_recursion_capped);
|
||||
RUN_TEST(lsp_ts_cyclic_types_no_crash);
|
||||
RUN_TEST(lsp_python_deep_nesting_no_crash);
|
||||
RUN_TEST(lsp_go_deep_nesting_no_crash);
|
||||
|
||||
Reference in New Issue
Block a user