fix(lsp): bound structured-binding field reads and TS method heritage walk (#1743)

Two deterministic indexing-worker crashes reported on #1743.

C/C++ (the original LLVM report, "segfault at 9"): structured-binding
decomposition read rt->field_types[binding_idx] guarded only by
`rt->field_types[binding_idx] != NULL`. field_types[] is parallel to
field_names[] and NULL-terminated, so a binding list longer than the
struct's field list (ill-formed, but present in compiler test suites,
e.g. llvm clang/test/CXX/dcl.decl/dcl.decomp/p3.cpp) read past the
terminator into the next arena object. That object is a CBMType whose
first word is its kind (9), so the name was bound to the pointer 0x9 and
decltype(name) dereferenced it in c_resolve_name_to_type. All three
decomposition sites now go through c_struct_field_type_at(), which bounds
the index by field_names[] and yields NULL (unknown) past the end.

TypeScript (kilocode report on v0.11.0): lookup_method walked the
extends/implements chain recursively with no depth bound. The Effect idiom
`class Service extends Context.Service<Service, I>()(...)` gets its base
resolved to the class itself, so `Service.of(...)` (or a sibling class
whose base resolves to that Service) recursed until the resolver thread's
stack overflowed. lookup_method now has the same depth-guarded entry as
lookup_member_type (TS_LSP_MAX_MEMBER_DEPTH); past the cap the method is
simply not found.

Proof on real input (macOS arm64, fresh cache each run):
- llvm/llvm-project llvmorg-15.0.7: before, 5 worker SIGSEGVs at 0x9 in
  c_resolve_name_to_type and 3 files quarantined; after, 0 crashes,
  0 quarantined, 1,375,933 nodes / 4,257,770 edges.
- Kilo-Org/kilocode main: before, the resolve worker died every attempt
  (SIGBUS, lookup_method recursion depth ~65k); after, indexed cleanly,
  90,745 nodes / 440,654 edges.

Tests: c_lsp::clsp_structured_binding_more_names_than_fields and
ts_lsp::tslsp_nocrash_cyclic_extends_method_call (both crash RED without
the fix).

Fixes #1743

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-09-25 18:37:16 +02:00
1 parent 5958f546dc
commit a8c4040e8a
5 files changed
+96 -8

No files matched your search

+27 -7
View File
@@ -2880,6 +2880,23 @@ static bool c_base_declares_member(CLSPContext *ctx, const char *type_qn, const
return false;
}
/* Type of the idx-th declared field, for structured-binding decomposition.
* field_types[] is parallel to field_names[] and may hold NULL entries, so
* only field_names[] bounds it: a binding list longer than the field list
* (ill-formed, but present in compiler test suites) must yield NULL, never
* read past the array into the next arena object (#1743). */
static const CBMType *c_struct_field_type_at(const CBMRegisteredType *rt, int idx) {
if (!rt || !rt->field_names || !rt->field_types || idx < 0) {
return NULL;
}
for (int i = 0; i < idx; i++) {
if (!rt->field_names[i]) {
return NULL;
}
}
return rt->field_names[idx] ? rt->field_types[idx] : NULL;
}
// Field type lookup
static const CBMType *c_lookup_field_type(CLSPContext *ctx, const char *type_qn,
const char *field_name, int depth) {
@@ -3055,9 +3072,10 @@ void c_process_statement(CLSPContext *ctx, TSNode node) {
ctx->registry,
cbm_arena_sprintf(ctx->arena, "%s.%s",
ctx->module_qn, rhs_qn));
if (rt && rt->field_types &&
rt->field_types[binding_idx])
elem_type = rt->field_types[binding_idx];
const CBMType *ft =
c_struct_field_type_at(rt, binding_idx);
if (ft)
elem_type = ft;
}
cbm_scope_bind(ctx->current_scope, bname, elem_type);
binding_idx++;
@@ -3140,8 +3158,9 @@ void c_process_statement(CLSPContext *ctx, TSNode node) {
ctx->registry,
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn,
rhs_qn));
if (rt && rt->field_types && rt->field_types[binding_idx]) {
elem_type = rt->field_types[binding_idx];
const CBMType *ft = c_struct_field_type_at(rt, binding_idx);
if (ft) {
elem_type = ft;
}
}
cbm_scope_bind(ctx->current_scope, bname, elem_type);
@@ -3551,8 +3570,9 @@ void c_process_statement(CLSPContext *ctx, TSNode node) {
rt = cbm_registry_lookup_type(
ctx->registry,
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, eq));
if (rt && rt->field_types && rt->field_types[binding_idx])
bt = rt->field_types[binding_idx];
const CBMType *ft = c_struct_field_type_at(rt, binding_idx);
if (ft)
bt = ft;
}
cbm_scope_bind(ctx->current_scope, bname, bt);
binding_idx++;
+20 -1
View File
@@ -1785,9 +1785,28 @@ static const CBMType *eval_indexed_access(TSLSPContext *ctx, const CBMType *obj,
return cbm_type_unknown();
}
// 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);
/* 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
* Context.Service<Service, I>()(...)` whose base resolved to the class itself)
* recursed without bound and overflowed the resolver thread's stack. Past the
* 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;
}
// 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)
return NULL;
const CBMType *base = simplify_type(ctx, recv);
+3
View File
@@ -69,6 +69,9 @@ typedef struct {
// 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
+23
View File
@@ -2086,6 +2086,28 @@ TEST(clsp_structured_binding_struct) {
PASS();
}
/* #1743: a structured binding with more names than the struct has fields
* (ill-formed C++, but common in compiler test suites such as LLVM's
* dcl.decomp tests) indexed field_types[] past its NULL terminator. The read
* landed in the next arena object and bound a garbage pointer (0x9) as the
* type, which decltype() then dereferenced: a deterministic SIGSEGV at
* address 9 in c_resolve_name_to_type. Extra names must bind as unknown. */
TEST(clsp_structured_binding_more_names_than_fields) {
CBMFileResult *r = extract_cpp("struct A { int x, y; };\n"
"const auto &[acr0, acr1, acr2] = A();\n"
"using ConstFloatRef = decltype(acr2);\n"
"void test() {\n"
" auto [b0, b1, b2, b3] = A();\n"
" using T = decltype(b3);\n"
" for (auto [c0, c1, c2, c3] : (A *)0) {\n"
" using U = decltype(c3);\n"
" }\n"
"}\n");
ASSERT_NOT_NULL(r);
cbm_free_result(r);
PASS();
}
TEST(clsp_ternary_type) {
CBMFileResult *r = extract_cpp("\n"
"class Widget {\n"
@@ -16486,6 +16508,7 @@ SUITE(c_lsp) {
RUN_TEST(clsp_tad_make_pair_like);
RUN_TEST(clsp_structured_binding_pair);
RUN_TEST(clsp_structured_binding_struct);
RUN_TEST(clsp_structured_binding_more_names_than_fields);
RUN_TEST(clsp_ternary_type);
RUN_TEST(clsp_chained_method_calls);
RUN_TEST(clsp_std_vector_push_back);
+23
View File
@@ -952,6 +952,28 @@ TEST(tslsp_nocrash_circular_extends) {
PASS();
}
/* #1743 (kilocode report): the Effect idiom `class Service extends
* Context.Service<Service, I>()(...)` resolved its base to the class itself,
* so a static call `Service.of(...)` walked a self-cycle in the extends chain
* with no depth bound and overflowed the resolver thread's stack. Method and
* member lookups over cyclic heritage must terminate. */
TEST(tslsp_nocrash_cyclic_extends_method_call) {
CBMFileResult *r =
extract_ts("export class Loader extends Context.Service<Loader, LoaderI>()(\n"
") {}\n"
"export class Service extends Context.Service<Service, I>()(\"@x/S\") {}\n"
"export function fromSDK(sdk: SDK): LoaderI {\n"
" return Loader.of({\n"
" })\n"
"}\n"
"class A extends B {}\n"
"class B extends A {}\n"
"function go(a: A) { a.missing(); return a.field.other(); }\n");
ASSERT_NOT_NULL(r);
cbm_free_result(r);
PASS();
}
TEST(tslsp_nocrash_recursive_type) {
CBMFileResult *r = extract_ts("interface List { next?: List; value: number; }\n"
"function go(l: List) { l.next?.value; }\n");
@@ -4365,6 +4387,7 @@ SUITE(ts_lsp) {
/* Category 26: more crash safety */
RUN_TEST(tslsp_nocrash_circular_extends);
RUN_TEST(tslsp_nocrash_cyclic_extends_method_call);
RUN_TEST(tslsp_nocrash_recursive_type);
RUN_TEST(tslsp_nocrash_unicode_identifier);
RUN_TEST(tslsp_nocrash_template_with_call);