fix(ts): bound recursive member and method lookup

Signed-off-by: dianshu <dianshu@123.com>
This commit is contained in:
dianshu committed 2026-10-13 17:50:13 +08:00
1 parent 56700bdb71
commit e5c3ddd573
3 files changed
+245 -56

No files matched your search

+131 -39
View File
@@ -59,6 +59,10 @@ void cbm_ts_full_registry_builds_reset(void) {
* constructs it does not model. -1 = unlimited (no entry point armed it). */
static _Thread_local long g_ts_type_budget = -1;
static _Thread_local bool g_ts_type_budget_warned;
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local int g_ts_max_member_lookup_steps;
static _Thread_local int g_ts_max_method_lookup_steps;
#endif
static void ts_type_budget_reset(size_t source_len) {
const char *e = cbm_runtime_getenv("CBM_TS_TYPE_BUDGET");
@@ -170,6 +174,12 @@ long cbm_ts_lsp_test_budget_remaining(void) {
bool cbm_ts_lsp_test_budget_warned(void) {
return g_ts_type_budget_warned;
}
int cbm_ts_lsp_test_max_member_lookup_steps(void) {
return g_ts_max_member_lookup_steps;
}
int cbm_ts_lsp_test_max_method_lookup_steps(void) {
return g_ts_max_method_lookup_steps;
}
#endif
#define TS_LSP_MAX_EVAL_DEPTH 64
@@ -190,6 +200,10 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name);
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name);
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name);
typedef struct TSLookupState TSLookupState;
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state);
static char *node_text(TSLSPContext *ctx, TSNode node);
// Collect a node's children into an arena array via a single O(n) cursor pass.
@@ -1569,28 +1583,77 @@ static const char *builtin_wrapper_class(const char *builtin_name) {
}
// Look up a property `name` on a receiver type. Returns the property type or UNKNOWN.
static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name);
#define TS_LSP_MAX_MEMBER_DEPTH 64
#define TS_LSP_INLINE_VISITED_TYPES 16
/* Depth-guarded entry: member lookup recurses through wrapper classes, union
* members and registered-type expansion; cyclic type graphs in real-world TS
* (microsoft/TypeScript reallyLargeFile.ts) recursed without bound — SIGBUS
* stack overflow under endless lookup_member_type frames. Past the cap the
* member resolves as unknown — graceful degradation, not a crash. */
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) {
if (!ctx || ctx->member_depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
ctx->member_depth++;
const CBMType *r = lookup_member_type_inner(ctx, recv, name);
ctx->member_depth--;
return r;
typedef struct {
const char *qn;
int depth;
} TSVisitedType;
struct TSLookupState {
TSVisitedType inline_visited[TS_LSP_INLINE_VISITED_TYPES];
TSVisitedType *visited;
int visited_count;
int visited_capacity;
#ifdef CBM_ENABLE_TEST_SEAMS
int steps;
#endif
};
/* A named type needs another visit only when reached along a shallower path.
* Grow the visited table rather than silently dropping entries at a fixed cap:
* the depth bound then limits each named type to at most 64 admitted visits. */
static bool ts_lookup_visit_named(CBMArena *arena, TSLookupState *state, const char *qn,
int depth) {
for (int i = 0; i < state->visited_count; i++) {
if (strcmp(state->visited[i].qn, qn) == 0) {
if (state->visited[i].depth <= depth)
return false;
state->visited[i].depth = depth;
return true;
}
}
if (!state->visited) {
state->visited = state->inline_visited;
state->visited_capacity = TS_LSP_INLINE_VISITED_TYPES;
} else if (state->visited_count == state->visited_capacity) {
int capacity = state->visited_capacity * 2;
TSVisitedType *larger =
(TSVisitedType *)cbm_arena_alloc(arena, (size_t)capacity * sizeof(*larger));
if (!larger) {
fprintf(stderr, "[tslsp] lookup visited allocation failed\n");
return false;
}
memcpy(larger, state->visited, (size_t)state->visited_count * sizeof(*larger));
state->visited = larger;
state->visited_capacity = capacity;
}
state->visited[state->visited_count++] = (TSVisitedType){.qn = qn, .depth = depth};
return true;
}
static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv,
const char *name) {
if (!ctx || !recv || !name)
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state);
/* Depth and shallower-only named-type visits bound work on cyclic inheritance. */
static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) {
TSLookupState state = {0};
const CBMType *result = lookup_member_type_at(ctx, recv, name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_member_lookup_steps)
g_ts_max_member_lookup_steps = state.steps;
#endif
return result;
}
static const CBMType *lookup_member_type_at(TSLSPContext *ctx, const CBMType *recv,
const char *name, int depth, TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return cbm_type_unknown();
const CBMType *base = simplify_type(ctx, recv);
if (!base)
@@ -1604,7 +1667,7 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_member_type(ctx, wrapped, name);
return lookup_member_type_at(ctx, wrapped, name, depth + 1, state);
}
return cbm_type_unknown();
}
@@ -1659,7 +1722,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// first hit instead of building a union of results.)
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(ctx, base->data.union_type.members[i],
name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1670,7 +1736,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
if (base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *m = lookup_member_type_at(ctx, base->data.union_type.members[i],
name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1682,6 +1751,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
return cbm_type_unknown();
const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return cbm_type_unknown();
if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return cbm_type_unknown();
const CBMRegisteredType *rt = cbm_registry_resolve_alias(ctx->registry, recv_qn);
if (!rt) {
rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
@@ -1711,8 +1784,10 @@ static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType
// Walk extends/implements.
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMType *m = lookup_member_type(ctx, parent, name);
const CBMType *m = lookup_member_type_at(ctx, parent, name, depth + 1, state);
if (!cbm_type_is_unknown(m))
return m;
}
@@ -1795,9 +1870,6 @@ static const CBMType *eval_indexed_access(TSLSPContext *ctx, const CBMType *obj,
return cbm_type_unknown();
}
static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBMType *recv,
const char *method_name);
/* Depth-guarded entry, same contract as lookup_member_type: the method walk
* recurses through wrapper classes, union members and the extends/implements
* chain. A cyclic chain (#1743: the Effect idiom `class Service extends
@@ -1806,18 +1878,24 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
* cap the method is simply not found. */
static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv,
const char *method_name) {
if (!ctx || ctx->method_depth >= TS_LSP_MAX_MEMBER_DEPTH)
return NULL;
ctx->method_depth++;
const CBMRegisteredFunc *f = lookup_method_inner(ctx, recv, method_name);
ctx->method_depth--;
return f;
TSLookupState state = {0};
const CBMRegisteredFunc *result = lookup_method_at(ctx, recv, method_name, 0, &state);
#ifdef CBM_ENABLE_TEST_SEAMS
if (state.steps > g_ts_max_method_lookup_steps)
g_ts_max_method_lookup_steps = state.steps;
#endif
return result;
}
// Look up a method on a receiver type — returns the registered func.
static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBMType *recv,
const char *method_name) {
if (!ctx || !recv || !method_name)
/* Depth and shallower-only named-type visits bound cyclic inheritance work. */
static const CBMRegisteredFunc *lookup_method_at(TSLSPContext *ctx, const CBMType *recv,
const char *method_name, int depth,
TSLookupState *state) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (state)
state->steps++;
#endif
if (!ctx || !recv || !method_name || !state || depth >= TS_LSP_MAX_MEMBER_DEPTH)
return NULL;
const CBMType *base = simplify_type(ctx, recv);
if (!base)
@@ -1831,7 +1909,7 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
const char *wrap = builtin_wrapper_class(base->data.builtin.name);
if (wrap) {
const CBMType *wrapped = cbm_type_named(ctx->arena, wrap);
return lookup_method(ctx, wrapped, method_name);
return lookup_method_at(ctx, wrapped, method_name, depth + 1, state);
}
return NULL;
}
@@ -1860,8 +1938,10 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
if (base->kind == CBM_TYPE_UNION || base->kind == CBM_TYPE_INTERSECTION) {
if (base->data.union_type.members) {
for (int i = 0; i < base->data.union_type.count; i++) {
const CBMRegisteredFunc *f =
lookup_method(ctx, base->data.union_type.members[i], method_name);
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMRegisteredFunc *f = lookup_method_at(ctx, base->data.union_type.members[i],
method_name, depth + 1, state);
if (f)
return f;
}
@@ -1872,6 +1952,11 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
if (base->kind != CBM_TYPE_NAMED)
return NULL;
const char *recv_qn = base->data.named.qualified_name;
if (!recv_qn)
return NULL;
if (!ts_lookup_visit_named(ctx->arena, state, recv_qn, depth))
return NULL;
const CBMRegisteredFunc *f =
cbm_registry_lookup_method_aliased(ctx->registry, recv_qn, method_name);
@@ -1882,8 +1967,11 @@ static const CBMRegisteredFunc *lookup_method_inner(TSLSPContext *ctx, const CBM
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn);
if (rt && rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
if (depth + 1 >= TS_LSP_MAX_MEMBER_DEPTH)
break;
const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]);
const CBMRegisteredFunc *pf = lookup_method(ctx, parent, method_name);
const CBMRegisteredFunc *pf =
lookup_method_at(ctx, parent, method_name, depth + 1, state);
if (pf)
return pf;
}
@@ -3838,6 +3926,10 @@ void ts_lsp_init(TSLSPContext *ctx, CBMArena *arena, const char *source, int sou
bool jsx_mode, bool dts_mode, CBMResolvedCallArray *out) {
if (!ctx)
return;
#ifdef CBM_ENABLE_TEST_SEAMS
g_ts_max_member_lookup_steps = 0;
g_ts_max_method_lookup_steps = 0;
#endif
memset(ctx, 0, sizeof(TSLSPContext));
ctx->arena = arena;
ctx->source = source;
+4 -7
View File
@@ -65,13 +65,6 @@ typedef struct {
// first completed eval; see TsEvalMemo in ts_lsp.c). Kills the exponential
// re-evaluation of shared subexpressions under overload resolution.
struct TsEvalMemo *eval_memo;
// Recursion guard for lookup_member_type: cyclic type graphs (mutually
// recursive unions/wrappers across registered types) otherwise recurse
// without bound — stack overflow on real repos.
int member_depth;
// Same guard for lookup_method: its extends/implements walk recursed
// without bound on a cyclic heritage chain (#1743).
int method_depth;
} TSLSPContext;
#ifdef CBM_ENABLE_TEST_SEAMS
@@ -79,6 +72,10 @@ typedef struct {
// for the calling thread, valid after a cbm_run_ts_lsp on the same thread.
long cbm_ts_lsp_test_budget_remaining(void);
bool cbm_ts_lsp_test_budget_warned(void);
// Maximum recursive calls in one member/method lookup during the last TS LSP run,
// including attempts rejected by the depth or visited-type guard.
int cbm_ts_lsp_test_max_member_lookup_steps(void);
int cbm_ts_lsp_test_max_method_lookup_steps(void);
#endif
// --- Initialization ---
+110 -10
View File
@@ -12,6 +12,7 @@
#include "test_framework.h"
#include "cbm.h"
#include "lang_specs.h" /* cbm_ts_language — direct-parse GLR cap regression (#913) */
#include "lsp/ts_lsp.h"
#include <stdio.h>
#include <stdlib.h>
@@ -399,11 +400,11 @@ TEST(cpp_large_templated_header_no_crash_issue424) {
* java_resolve_calls_in_node frames (bind_lambda_args), bitcoin → SIGSEGV
* under deep c_resolve_calls_in_node frames (cbm_type_substitute via
* c_adl_resolve), microsoft/TypeScript → SIGBUS under an unbounded
* lookup_member_type cycle. The walks now carry depth guards; these
* reproductions fork a child so a regression cannot kill the test runner
* (the TS cyclic-type shape is only reachable with a real cross-file
* registry, so that one is verified at the real-repo tier; the synthetic
* cyclic fixture here guards the in-file path).
* lookup_member_type cycle. The walks now carry depth guards. Non-TS crash
* fixtures fork on POSIX and run in-process on Windows. TypeScript cycle work
* runs in-process on both platforms to assert test-only lookup counters.
* The synthetic single-file fixtures here guard the local inheritance path;
* real-repo scale extraction is checked separately.
* ═══════════════════════════════════════════════════════════════════ */
#if !defined(_WIN32)
@@ -670,9 +671,106 @@ TEST(lsp_ts_cyclic_types_no_crash) {
"interface D extends C { d: number; }\n"
"declare const a: A;\n"
"declare const c: C;\n"
"function useIt(p: C) { return p.missing_member; }\n"
/* Both lookup paths must traverse the inheritance cycle. */
"function useIt(p: C) { p.missing(); return p.missing_member; }\n"
"const y = c.also_missing;\n";
ASSERT_FALSE(so_extract_crashes(src, CBM_LANG_TYPESCRIPT, "cycle.ts"));
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "cycle.ts");
ASSERT_NOT_NULL(r);
/* C and D each have one parent: <= 64 admitted visits per type, plus
* one attempted (possibly rejected) parent visit per admitted visit. */
ASSERT_GT(cbm_ts_lsp_test_max_member_lookup_steps(), 0);
ASSERT_GT(cbm_ts_lsp_test_max_method_lookup_steps(), 0);
ASSERT_LTE(cbm_ts_lsp_test_max_member_lookup_steps(), 2 * 64 * 2);
ASSERT_LTE(cbm_ts_lsp_test_max_method_lookup_steps(), 2 * 64 * 2);
cbm_free_result(r);
PASS();
}
TEST(lsp_ts_branching_cycle_has_bounded_work) {
const char *src =
"interface A extends B, C {}\n"
"interface B extends A, C {}\n"
"interface C extends A, B {}\n"
"function useIt(value: A) { value.missing(); return value.missing_member; }\n";
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "branching-cycle.ts");
ASSERT_NOT_NULL(r);
/* A, B, C each have two parents: <= 64 admitted visits per type, plus
* two attempted (possibly rejected) parent visits per admitted visit. */
ASSERT_GT(cbm_ts_lsp_test_max_member_lookup_steps(), 0);
ASSERT_GT(cbm_ts_lsp_test_max_method_lookup_steps(), 0);
ASSERT_LTE(cbm_ts_lsp_test_max_member_lookup_steps(), 3 * 64 * 3);
ASSERT_LTE(cbm_ts_lsp_test_max_method_lookup_steps(), 3 * 64 * 3);
cbm_free_result(r);
PASS();
}
TEST(lsp_ts_shallower_revisit_preserves_inherited_method) {
enum { CHAIN = 62, CAP = 16384 };
char *src = malloc(CAP);
ASSERT_NOT_NULL(src);
size_t used =
(size_t)snprintf(src, CAP,
"interface Service { ping(): void; }\n"
"interface Target { inherited(): void; inheritedField: Service; }\n"
"interface Shared extends Target {}\n"
"interface Root extends D0, Shared {}\n");
for (int i = 0; i < CHAIN; i++) {
const char *parent = i + 1 < CHAIN ? "D" : "Shared";
int wrote = i + 1 < CHAIN
? snprintf(src + used, CAP - used, "interface D%d extends %s%d {}\n", i,
parent, i + 1)
: snprintf(src + used, CAP - used, "interface D%d extends Shared {}\n", i);
ASSERT_TRUE(wrote > 0 && (size_t)wrote < CAP - used);
used += (size_t)wrote;
}
int wrote = snprintf(
src + used, CAP - used,
"function useIt(value: Root) { value.inherited(); value.inheritedField.ping(); }\n");
ASSERT_TRUE(wrote > 0 && (size_t)wrote < CAP - used);
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "shallower.ts");
free(src);
ASSERT_NOT_NULL(r);
int inherited_method_found = 0;
int inherited_field_method_found = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &r->resolved_calls.items[i];
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "inherited")) {
inherited_method_found = 1;
}
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "ping")) {
inherited_field_method_found = 1;
}
}
cbm_free_result(r);
ASSERT_TRUE(inherited_method_found);
ASSERT_TRUE(inherited_field_method_found);
PASS();
}
TEST(lsp_ts_inherited_method_lookup) {
const char *src = "interface Base { inherited(): void; }\n"
"interface Child extends Base {}\n"
"function useIt(c: Child) { c.inherited(); }\n";
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "inherited.ts");
ASSERT_NOT_NULL(r);
int found = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &r->resolved_calls.items[i];
if (resolved->confidence > 0 && resolved->caller_qn &&
strstr(resolved->caller_qn, "useIt") && resolved->callee_qn &&
strstr(resolved->callee_qn, "inherited")) {
found = 1;
break;
}
}
cbm_free_result(r);
ASSERT_TRUE(found);
PASS();
}
@@ -777,9 +875,8 @@ TEST(lsp_kotlin_deep_nesting_no_crash) {
/* Split into three sub-suites so parallel/sharded runs are not serialized
* behind one ~4-minute suite (it was the wall-clock critical path: every
* other suite finished underneath it). Pure re-registration — the 20
* RUN_TEST entries are exactly the ones the single suite carried; the
* before/after test-count parity is asserted in the shard runner. */
* other suite finished underneath it). New regressions stay in the matching
* shard; the shard runner asserts aggregate test-count parity. */
SUITE(stack_overflow_a) {
cbm_init();
@@ -799,6 +896,9 @@ SUITE(stack_overflow_b) {
RUN_TEST(perl_glr_deep_parse_recursion_capped);
RUN_TEST(lsp_ts_cyclic_types_no_crash);
RUN_TEST(lsp_ts_branching_cycle_has_bounded_work);
RUN_TEST(lsp_ts_shallower_revisit_preserves_inherited_method);
RUN_TEST(lsp_ts_inherited_method_lookup);
RUN_TEST(lsp_python_deep_nesting_no_crash);
RUN_TEST(lsp_go_deep_nesting_no_crash);
RUN_TEST(lsp_php_deep_nesting_no_crash);