Skip to content

Commit b1a646e

Browse files
authored
Merge pull request #677 from DeusData/fix/c-lsp-shared-registry-on2
fix(lsp): seal shared Tier-2 cross-registry against O(n^2) resolve hang
2 parents 9169f46 + d10701d commit b1a646e

8 files changed

Lines changed: 190 additions & 2 deletions

File tree

internal/cbm/lsp/c_lsp.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4432,8 +4432,15 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) {
44324432
!strstr(func_qn, ctx->current_namespace))
44334433
func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s",
44344434
ctx->current_namespace, fname);
4435-
// Only register if not already registered
4436-
if (!cbm_registry_lookup_func(ctx->registry, func_qn)) {
4435+
// Only register if not already registered. On the
4436+
// shared Tier-2 registry, skip entirely: it is
4437+
// finalized + read-only, the def is already present
4438+
// from the project-wide build, and the lookup + add
4439+
// would otherwise hit/grow the post-finalize tail ->
4440+
// O(files*defs) on large C codebases (the Linux-kernel
4441+
// full-index hang) plus a cross-worker heap race.
4442+
if (!ctx->registry_shared &&
4443+
!cbm_registry_lookup_func(ctx->registry, func_qn)) {
44374444
const CBMType **rets = (const CBMType **)cbm_arena_alloc(
44384445
ctx->arena, 2 * sizeof(const CBMType *));
44394446
rets[0] = ret_type;
@@ -5315,6 +5322,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
53155322
c_register_lsp_defs(arena, reg, "", d, 1);
53165323
}
53175324
cbm_registry_finalize(reg);
5325+
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
53185326
return reg;
53195327
}
53205328

internal/cbm/lsp/cs_lsp.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2954,6 +2954,7 @@ CBMTypeRegistry *cbm_cs_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
29542954
cs_register_lsp_defs(arena, reg, &defs[i], 1);
29552955
}
29562956
cbm_registry_finalize(reg);
2957+
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
29572958
return reg;
29582959
}
29592960

internal/cbm/lsp/go_lsp.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2835,6 +2835,7 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
28352835
}
28362836

28372837
cbm_registry_finalize(reg);
2838+
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
28382839
return reg;
28392840
}
28402841

internal/cbm/lsp/py_lsp.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3607,6 +3607,7 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
36073607
}
36083608

36093609
cbm_registry_finalize(reg);
3610+
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
36103611
return reg;
36113612
}
36123613

internal/cbm/lsp/ts_lsp.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4986,6 +4986,7 @@ CBMTypeRegistry *cbm_ts_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
49864986
ts_register_lsp_defs(arena, reg, d, 1);
49874987
}
49884988
cbm_registry_finalize(reg);
4989+
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
49894990
return reg;
49904991
}
49914992

internal/cbm/lsp/type_registry.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ void cbm_registry_init(CBMTypeRegistry *reg, CBMArena *arena) {
135135
}
136136

137137
void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func) {
138+
if (reg->read_only) {
139+
return; /* sealed Tier-2 shared registry: refuse post-finalize mutation (O(n^2)+race guard)
140+
*/
141+
}
138142
if (reg->func_count >= reg->func_cap) {
139143
int new_cap = reg->func_cap == 0 ? 64 : reg->func_cap * 2;
140144
CBMRegisteredFunc *new_items = (CBMRegisteredFunc *)cbm_arena_alloc(
@@ -151,6 +155,10 @@ void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func) {
151155
}
152156

153157
void cbm_registry_add_type(CBMTypeRegistry *reg, CBMRegisteredType type) {
158+
if (reg->read_only) {
159+
return; /* sealed Tier-2 shared registry: refuse post-finalize mutation (O(n^2)+race guard)
160+
*/
161+
}
154162
if (reg->type_count >= reg->type_cap) {
155163
int new_cap = reg->type_cap == 0 ? 64 : reg->type_cap * 2;
156164
CBMRegisteredType *new_items = (CBMRegisteredType *)cbm_arena_alloc(

internal/cbm/lsp/type_registry.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "type_rep.h"
55
#include "../arena.h"
6+
#include <stdbool.h>
67

78
// Decorator-derived flags (Python). Added at struct tail so existing
89
// callers that memset to zero before populating other fields keep working.
@@ -99,6 +100,16 @@ typedef struct CBMTypeRegistry {
99100
CBMRegistryHashEntry *method_entries;
100101
int method_bucket_count;
101102
int method_entry_count;
103+
104+
/* Sealed / read-only. Set true by the cbm_X_build_cross_registry builders
105+
* (c/cpp, python, c#, ts, go) right after finalize: a Tier-2 cross-registry
106+
* is built ONCE and shared READ-ONLY across the parallel resolve workers.
107+
* cbm_registry_add_func/_type no-op on a sealed registry, so a per-file
108+
* resolver can never mutate the shared, finalized registry. Without this,
109+
* post-finalize adds accumulate in a tail the hash index does not cover ->
110+
* every lookup linear-scans it -> O(files*defs) (the Linux-kernel full-index
111+
* hang) plus a heap data race across workers. */
112+
bool read_only;
102113
} CBMTypeRegistry;
103114

104115
// Initialize a registry.

tests/test_c_lsp.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
*/
2121
#include "test_framework.h"
2222
#include "cbm.h"
23+
#include "lsp/c_lsp.h"
24+
#include "lsp/py_lsp.h"
25+
#include "lsp/cs_lsp.h"
26+
#include "lsp/ts_lsp.h"
27+
#include "lsp/go_lsp.h"
28+
#include "lsp/type_registry.h"
29+
#include "arena.h"
2330
#include <stdio.h>
2431
#include <stdlib.h>
2532

@@ -15182,9 +15189,159 @@ TEST(clsp_easy_win_sfinaeconditional_return) {
1518215189
PASS();
1518315190
}
1518415191

15192+
/* Reproduce-first guard for the Linux-kernel full-index O(n^2) hang.
15193+
*
15194+
* In `full` mode the pipeline builds ONE project-wide C cross-registry
15195+
* (cbm_c_build_cross_registry), FINALIZES it (O(1) hash lookups), then shares it
15196+
* READ-ONLY across the parallel resolve workers (ctx.registry_shared = true).
15197+
* cbm_run_c_lsp_cross_with_registry must therefore NOT mutate it.
15198+
*
15199+
* Bug: c_lsp.c:4323 (and 4628/4201/4426) ignore registry_shared and add_func into
15200+
* the shared, already-finalized registry. Each post-finalize add lands in a tail
15201+
* the hash index does not cover, so lookup_func_self/lookup_method_self linear-scan
15202+
* that ever-growing tail on every lookup (type_registry.c:280-286, 186-195). Across
15203+
* 89k kernel files doing millions of lookups => O(files * defs) (the >6-min hang),
15204+
* plus an 11-thread heap race (c_lsp.c:4146-4152 warns of the SIGSEGV).
15205+
*
15206+
* INVARIANT (green <=> fixed): resolve leaves the finalized shared registry's
15207+
* func_count/type_count unchanged. RED on the unguarded code; GREEN once every
15208+
* mutation site honors !registry_shared. */
15209+
TEST(clsp_tier2_shared_registry_readonly_c) {
15210+
CBMArena arena;
15211+
cbm_arena_init(&arena);
15212+
/* stdlib-only project registry, finalized inside the builder */
15213+
CBMTypeRegistry *reg = cbm_c_build_cross_registry(&arena, NULL, 0);
15214+
ASSERT_NOT_NULL(reg);
15215+
int funcs_before = reg->func_count;
15216+
int types_before = reg->type_count;
15217+
/* A C translation unit defining functions absent from the shared registry; a
15218+
* correct read-only resolve must not register them into it. */
15219+
const char *src = "struct Node { int v; };\n"
15220+
"struct Node *make_node(int v);\n"
15221+
"int helper(int x) { return x + 1; }\n"
15222+
"int caller(void) { return helper(make_node(7)->v); }\n";
15223+
CBMResolvedCallArray out = {0};
15224+
cbm_run_c_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod",
15225+
/*cpp_mode=*/false, reg, /*include_paths=*/NULL,
15226+
/*include_ns_qns=*/NULL, /*include_count=*/0,
15227+
/*cached_tree=*/NULL, &out);
15228+
ASSERT_EQ(reg->func_count, funcs_before);
15229+
ASSERT_EQ(reg->type_count, types_before);
15230+
cbm_arena_destroy(&arena);
15231+
PASS();
15232+
}
15233+
1518515234
/* ── Suite ─────────────────────────────────────────────────────── */
1518615235

15236+
/* C++ sibling guard: the same shared-registry read-only invariant for the
15237+
* C++-only mutation sites — method registration (c_lsp.c:4760), template
15238+
* type-param scan (4558), default-arg min_params scan (4333). RED while those
15239+
* sites ignore registry_shared; GREEN once guarded. (Latent O(n^2)+race for large
15240+
* C++ codebases like LLVM/bitcoin — not the kernel, which is C.) */
15241+
TEST(clsp_tier2_shared_registry_readonly_cpp) {
15242+
CBMArena arena;
15243+
cbm_arena_init(&arena);
15244+
CBMTypeRegistry *reg = cbm_c_build_cross_registry(&arena, NULL, 0);
15245+
ASSERT_NOT_NULL(reg);
15246+
int funcs_before = reg->func_count;
15247+
int types_before = reg->type_count;
15248+
const char *src = "template <typename T> struct Holder { T get(); };\n"
15249+
"struct Box { int unwrap(); };\n"
15250+
"int Box::unwrap() { return 0; }\n"
15251+
"int with_default(int a, int b = 2) { return a + b; }\n"
15252+
"int caller(Box *b) { return b->unwrap() + with_default(1); }\n";
15253+
CBMResolvedCallArray out = {0};
15254+
cbm_run_c_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod",
15255+
/*cpp_mode=*/true, reg, /*include_paths=*/NULL,
15256+
/*include_ns_qns=*/NULL, /*include_count=*/0,
15257+
/*cached_tree=*/NULL, &out);
15258+
ASSERT_EQ(reg->func_count, funcs_before);
15259+
ASSERT_EQ(reg->type_count, types_before);
15260+
cbm_arena_destroy(&arena);
15261+
PASS();
15262+
}
15263+
15264+
/* Cross-language check of the same Tier-2 shared-registry read-only invariant for
15265+
* the other languages that build a shared cross-registry (py, c#, ts, go). The
15266+
* registry-level seal (type_registry.c: add_func/_type no-op when reg->read_only)
15267+
* guards all of them at one chokepoint. Each test: build+finalize the shared
15268+
* registry, resolve a source, assert func_count/type_count are unchanged. (TS
15269+
* deliberately mutates a per-file OVERLAY chained to the base — the base must
15270+
* still be untouched.) */
15271+
TEST(seal_py_shared_registry_readonly) {
15272+
CBMArena arena;
15273+
cbm_arena_init(&arena);
15274+
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, NULL, 0);
15275+
ASSERT_NOT_NULL(reg);
15276+
int fb = reg->func_count, tb = reg->type_count;
15277+
const char *src = "def helper(x):\n return x + 1\n\ndef caller():\n return helper(1)\n";
15278+
CBMResolvedCallArray out = {0};
15279+
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
15280+
0, NULL, &out);
15281+
ASSERT_EQ(reg->func_count, fb);
15282+
ASSERT_EQ(reg->type_count, tb);
15283+
cbm_arena_destroy(&arena);
15284+
PASS();
15285+
}
15286+
15287+
TEST(seal_cs_shared_registry_readonly) {
15288+
CBMArena arena;
15289+
cbm_arena_init(&arena);
15290+
CBMTypeRegistry *reg = cbm_cs_build_cross_registry(&arena, NULL, 0);
15291+
ASSERT_NOT_NULL(reg);
15292+
int fb = reg->func_count, tb = reg->type_count;
15293+
const char *src = "namespace N { class Box { int Unwrap() { return 0; }\n"
15294+
" int Caller() { return Unwrap(); } } }\n";
15295+
CBMResolvedCallArray out = {0};
15296+
cbm_run_cs_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, 0,
15297+
NULL, &out);
15298+
ASSERT_EQ(reg->func_count, fb);
15299+
ASSERT_EQ(reg->type_count, tb);
15300+
cbm_arena_destroy(&arena);
15301+
PASS();
15302+
}
15303+
15304+
TEST(seal_ts_shared_registry_readonly) {
15305+
CBMArena arena;
15306+
cbm_arena_init(&arena);
15307+
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, NULL, 0);
15308+
ASSERT_NOT_NULL(reg);
15309+
int fb = reg->func_count, tb = reg->type_count;
15310+
const char *src = "class Box { unwrap(): number { return 0; } }\n"
15311+
"function caller(b: Box): number { return b.unwrap(); }\n";
15312+
CBMResolvedCallArray out = {0};
15313+
cbm_run_ts_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", false, false,
15314+
false, reg, NULL, 0, NULL, NULL, 0, NULL, &out);
15315+
ASSERT_EQ(reg->func_count, fb);
15316+
ASSERT_EQ(reg->type_count, tb);
15317+
cbm_arena_destroy(&arena);
15318+
PASS();
15319+
}
15320+
15321+
TEST(seal_go_shared_registry_readonly) {
15322+
CBMArena arena;
15323+
cbm_arena_init(&arena);
15324+
CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, NULL, 0);
15325+
ASSERT_NOT_NULL(reg);
15326+
int fb = reg->func_count, tb = reg->type_count;
15327+
const char *src = "package main\nfunc helper(x int) int { return x + 1 }\n"
15328+
"func caller() int { return helper(1) }\n";
15329+
CBMResolvedCallArray out = {0};
15330+
cbm_run_go_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
15331+
0, NULL, &out);
15332+
ASSERT_EQ(reg->func_count, fb);
15333+
ASSERT_EQ(reg->type_count, tb);
15334+
cbm_arena_destroy(&arena);
15335+
PASS();
15336+
}
15337+
1518715338
SUITE(c_lsp) {
15339+
RUN_TEST(clsp_tier2_shared_registry_readonly_c);
15340+
RUN_TEST(clsp_tier2_shared_registry_readonly_cpp);
15341+
RUN_TEST(seal_py_shared_registry_readonly);
15342+
RUN_TEST(seal_cs_shared_registry_readonly);
15343+
RUN_TEST(seal_ts_shared_registry_readonly);
15344+
RUN_TEST(seal_go_shared_registry_readonly);
1518815345
RUN_TEST(clsp_simple_var_decl);
1518915346
RUN_TEST(clsp_pointer_arrow);
1519015347
RUN_TEST(clsp_dot_access);

0 commit comments

Comments
 (0)