|
11 | 11 | */ |
12 | 12 | #include "test_framework.h" |
13 | 13 | #include "cbm.h" |
| 14 | +#include "lang_specs.h" /* cbm_ts_language — direct-parse GLR cap regression (#913) */ |
14 | 15 |
|
15 | 16 | #include <stdio.h> |
16 | 17 | #include <stdlib.h> |
@@ -440,6 +441,53 @@ static bool so_extract_crashes(const char *content, CBMLanguage lang, const char |
440 | 441 | #endif |
441 | 442 | } |
442 | 443 |
|
| 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 | + |
443 | 491 | TEST(lsp_java_deep_nesting_no_crash) { |
444 | 492 | /* Deeply nested call expressions — the same shape as the elasticsearch |
445 | 493 | * crash (fast SIGSEGV under recursive java_resolve_calls_in_node frames; |
@@ -541,6 +589,39 @@ TEST(lsp_perl_deep_expression_no_crash) { |
541 | 589 | PASS(); |
542 | 590 | } |
543 | 591 |
|
| 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 | + |
544 | 625 | TEST(lsp_java_lambda_args_exceed_params_no_crash) { |
545 | 626 | /* A call with MORE arguments than the resolved method's declared params: |
546 | 627 | * bind_lambda_args indexed the NULL-terminated signature param_types array |
@@ -679,6 +760,7 @@ SUITE(stack_overflow) { |
679 | 760 | RUN_TEST(lsp_cpp_deep_expression_no_crash); |
680 | 761 | RUN_TEST(lsp_python_deep_expression_no_crash); |
681 | 762 | RUN_TEST(lsp_perl_deep_expression_no_crash); |
| 763 | + RUN_TEST(perl_glr_deep_parse_recursion_capped); |
682 | 764 | RUN_TEST(lsp_ts_cyclic_types_no_crash); |
683 | 765 | RUN_TEST(lsp_python_deep_nesting_no_crash); |
684 | 766 | RUN_TEST(lsp_go_deep_nesting_no_crash); |
|
0 commit comments