Skip to content

Commit fcd8bfc

Browse files
authored
Merge pull request DeusData#1937 from ilyabrykau-orca/fix/go-usage-writes-cross-language
fix(pipeline): guard USAGE/WRITES/READS against cross-language binds
2 parents 9b24332 + d6417ad commit fcd8bfc

6 files changed

Lines changed: 252 additions & 7 deletions

File tree

‎src/pipeline/pass_parallel.c‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2654,6 +2654,12 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
26542654
continue;
26552655
}
26562656
tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
2657+
/* #1928: the registry fallback is a bare-name guess — never let it
2658+
* bind a reference across a language boundary. Mirrors the
2659+
* sequential twin (pass_usages.c). */
2660+
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
2661+
continue;
2662+
}
26572663
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
26582664
cbm_pipeline_node_is_callable_target(tgt))) {
26592665
continue;
@@ -2713,7 +2719,7 @@ static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws,
27132719
/* Resolve reads/writes for one file. */
27142720
static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
27152721
const char *rel, const char *module_qn, const char **imp_keys,
2716-
const char **imp_vals, int imp_count) {
2722+
const char **imp_vals, int imp_count, CBMLanguage lang) {
27172723
for (int r = 0; r < result->rw.count; r++) {
27182724
CBMReadWrite *rw = &result->rw.items[r];
27192725
if (!rw->var_name) {
@@ -2733,6 +2739,12 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
27332739
if (!tgt || src->id == tgt->id) {
27342740
continue;
27352741
}
2742+
/* #1928: every resolution here is a bare-name registry guess — never
2743+
* let it bind a read/write across a language boundary. Mirrors the
2744+
* sequential twin (pass_usages.c). */
2745+
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
2746+
continue;
2747+
}
27362748
const char *etype = rw->is_write ? "WRITES" : "READS";
27372749
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
27382750
}
@@ -3179,7 +3191,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {
31793191

31803192
/* ── READS / WRITES ────────────────────────────────────── */
31813193
_ph_t0 = extract_now_ns();
3182-
resolve_file_rw(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count);
3194+
resolve_file_rw(rc, ws, result, rel, module_qn, imp_keys, imp_vals, imp_count, lang);
31833195
atomic_fetch_add_explicit(&rc->time_ns_rw, extract_now_ns() - _ph_t0, memory_order_relaxed);
31843196

31853197
/* ── INHERITS + DECORATES + IMPLEMENTS ──────────────────── */

‎src/pipeline/pass_usages.c‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
210210
continue;
211211
}
212212
tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
213+
/* #1928: the registry fallback is a bare-name guess — never let it
214+
* bind a reference across a language boundary (the LSP-backed
215+
* semantic branch above is not affected). */
216+
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
217+
continue;
218+
}
213219
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
214220
cbm_pipeline_node_is_callable_target(tgt))) {
215221
continue;
@@ -270,7 +276,7 @@ static int resolve_throw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
270276
/* Resolve READS/WRITES edges for one file's extracted read/write accesses. */
271277
static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, const char *rel,
272278
const char *module_qn, const char **imp_keys, const char **imp_vals,
273-
int imp_count) {
279+
int imp_count, CBMLanguage lang) {
274280
int resolved = 0;
275281
for (int r = 0; r < result->rw.count; r++) {
276282
CBMReadWrite *rw = &result->rw.items[r];
@@ -293,6 +299,11 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
293299
if (!tgt || src->id == tgt->id) {
294300
continue;
295301
}
302+
/* #1928: every resolution here is a bare-name registry guess — never
303+
* let it bind a read/write across a language boundary. */
304+
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
305+
continue;
306+
}
296307

297308
const char *edge_type = rw->is_write ? "WRITES" : "READS";
298309
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}");
@@ -359,7 +370,8 @@ int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fil
359370
imp_count, files[i].language);
360371
throw_resolved +=
361372
resolve_throw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
362-
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count);
373+
rw_resolved += resolve_rw_edges(ctx, result, rel, module_qn, imp_keys, imp_vals, imp_count,
374+
files[i].language);
363375

364376
free(module_qn);
365377
free_import_map(imp_keys, imp_vals, imp_count);

‎src/pipeline/pipeline.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
288288
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
289289
const char *strategy);
290290

291+
/* #1928: USAGE/WRITES/READS analog of the CALLS guard above. Reference edges
292+
* resolved by the short-name registry carry no import-closure evidence, so a
293+
* cross-language binding is a bare-name collision for EVERY strategy — drop
294+
* it whenever the caller's language and the target file's language disagree
295+
* (JS/TS family members and the C/C++ header family excepted). Pure;
296+
* unit-tested in test_registry.c. */
297+
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);
298+
291299
/* Get the label of a qualified name, or NULL if not found. */
292300
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
293301

‎src/pipeline/registry.c‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ static bool js_ts_family(CBMLanguage lang) {
469469
lang == CBM_LANG_ARKTS;
470470
}
471471

472+
/* C and C++ are one family for cross-language checks: .h maps to CBM_LANG_CPP
473+
* in the extension table, so a .c file referencing a symbol declared in its
474+
* own header would otherwise read as a language boundary. */
475+
static bool c_cpp_family(CBMLanguage lang) {
476+
return lang == CBM_LANG_C || lang == CBM_LANG_CPP;
477+
}
478+
472479
static const char *path_basename(const char *path) {
473480
if (!path || !path[0]) {
474481
return path;
@@ -508,6 +515,37 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
508515
return true;
509516
}
510517

518+
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path) {
519+
/* #1928: USAGE / WRITES / READS analog of the CALLS guard above. A
520+
* variable or field reference resolved by the short-name registry must
521+
* not cross a language boundary: unlike CALLS, a reference edge carries
522+
* no import-closure evidence at all — a Go test's local `event` and an
523+
* eBPF C probe's automatic `event` share nothing but the spelling, so
524+
* EVERY registry strategy is a bare-name guess here and none is exempt.
525+
* LSP-backed semantic references resolve before the registry fallback
526+
* and never reach this predicate, which is where a genuine cross-language
527+
* binding (a future cgo resolver) would live. The JS/TS family keeps its
528+
* exemption (.js/.ts/.d.ts pairs legitimately share symbols), and C/C++
529+
* count as one family (.h maps to CBM_LANG_CPP). */
530+
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
531+
return false;
532+
}
533+
CBMLanguage target_lang = cbm_language_for_filename(path_basename(target_file_path));
534+
if (target_lang == CBM_LANG_COUNT) {
535+
return false;
536+
}
537+
if (caller_lang == target_lang) {
538+
return false;
539+
}
540+
if (js_ts_family(caller_lang) && js_ts_family(target_lang)) {
541+
return false;
542+
}
543+
if (c_cpp_family(caller_lang) && c_cpp_family(target_lang)) {
544+
return false;
545+
}
546+
return true;
547+
}
548+
511549
/* ── Lifecycle ──────────────────────────────────────────────────── */
512550

513551
cbm_registry_t *cbm_registry_new(void) {

‎tests/test_pipeline.c‎

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,8 +824,8 @@ TEST(pipeline_calls_resolution) {
824824

825825
/* True iff a CALLS edge exists from a node named src_name to a node named
826826
* tgt_name. Used to assert cross-file call resolution survives a reindex. */
827-
static bool cross_file_call_exists(cbm_store_t *s, const char *project, const char *src_name,
828-
const char *tgt_name) {
827+
static bool cross_file_edge_exists(cbm_store_t *s, const char *project, const char *src_name,
828+
const char *tgt_name, const char *edge_type) {
829829
cbm_node_t *srcs = NULL;
830830
cbm_node_t *tgts = NULL;
831831
int sc = 0;
@@ -836,7 +836,7 @@ static bool cross_file_call_exists(cbm_store_t *s, const char *project, const ch
836836
for (int i = 0; i < sc && !found; i++) {
837837
cbm_edge_t *edges = NULL;
838838
int ec = 0;
839-
cbm_store_find_edges_by_source_type(s, srcs[i].id, "CALLS", &edges, &ec);
839+
cbm_store_find_edges_by_source_type(s, srcs[i].id, edge_type, &edges, &ec);
840840
for (int j = 0; j < ec && !found; j++) {
841841
for (int k = 0; k < tc; k++) {
842842
if (edges[j].target_id == tgts[k].id) {
@@ -858,6 +858,11 @@ static bool cross_file_call_exists(cbm_store_t *s, const char *project, const ch
858858
return found;
859859
}
860860

861+
static bool cross_file_call_exists(cbm_store_t *s, const char *project, const char *src_name,
862+
const char *tgt_name) {
863+
return cross_file_edge_exists(s, project, src_name, tgt_name, "CALLS");
864+
}
865+
861866
/* True iff the exact named CALLS edge exists and its serialized strategy
862867
* contains `strategy_fragment`. Parallel synthetic-carrier regressions use
863868
* this on a separate ordinary-call control: it proves the cross-file LSP ran
@@ -4688,6 +4693,145 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
46884693
PASS();
46894694
}
46904695

4696+
/* Fixture for the #1928 cross-language reference-guard probes (sequential and
4697+
* parallel twins). pad_files > 0 adds filler files to push the index over the
4698+
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
4699+
* path and both must consult the guard. */
4700+
static void write_go_c_ref_guard_fixture(const char *tmp, int pad_files) {
4701+
write_temp_file(tmp, "go.mod", "module example.com/fxguard\n\ngo 1.22\n");
4702+
/* The C probe: a local named `event` and a file-scope function `handle`,
4703+
* the two shapes Go identifiers collide with. */
4704+
write_temp_file(tmp, "probe/probe.c",
4705+
"static int total_events = 0;\n"
4706+
"\n"
4707+
"static int handle(void) {\n"
4708+
" int event = 0;\n"
4709+
" total_events += event;\n"
4710+
" return event;\n"
4711+
"}\n");
4712+
/* Go: a local write named like the C local, and a value use named like
4713+
* the C function. Neither can touch anything in a C translation unit. */
4714+
write_temp_file(tmp, "app/app.go",
4715+
"package app\n"
4716+
"\n"
4717+
"func TrackEvent() int {\n"
4718+
"\tevent := 1\n"
4719+
"\treturn event\n"
4720+
"}\n"
4721+
"\n"
4722+
"func UsesHandle() int {\n"
4723+
"\th := handle\n"
4724+
"\t_ = h\n"
4725+
"\treturn 4\n"
4726+
"}\n"
4727+
"\n"
4728+
"func WriteTotal() {\n"
4729+
"\ttotal_events := 5\n"
4730+
"\t_ = total_events\n"
4731+
"}\n");
4732+
/* Same-language control: a Go package-level var written from another
4733+
* file must keep its WRITES edge. */
4734+
write_temp_file(tmp, "state/vars.go",
4735+
"package state\n"
4736+
"\n"
4737+
"var Counter int\n");
4738+
write_temp_file(tmp, "state/state.go",
4739+
"package state\n"
4740+
"\n"
4741+
"func Bump() {\n"
4742+
"\tCounter = 2\n"
4743+
"}\n");
4744+
for (int i = 0; i < pad_files; i++) {
4745+
char name[64];
4746+
char body[128];
4747+
snprintf(name, sizeof(name), "pad/filler%d.go", i);
4748+
snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i);
4749+
write_temp_file(tmp, name, body);
4750+
}
4751+
}
4752+
4753+
TEST(pipeline_go_rw_usage_never_cross_into_c) {
4754+
/* #1928: USAGE and WRITES/READS resolve through the same short-name
4755+
* registry as CALLS but never consulted the #725 cross-language guard.
4756+
* On a Go tree with eBPF C probes every Go identifier spelled like a C
4757+
* one produced an edge into the C file — 31.5% of all WRITES on the
4758+
* measured repo crossed the Go/C boundary. Go code cannot write a C
4759+
* automatic variable, so every edge in that class is false. This is the
4760+
* SEQUENTIAL-path twin; the parallel twin follows. */
4761+
char tmp[256];
4762+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rw_XXXXXX");
4763+
if (!cbm_mkdtemp(tmp)) {
4764+
FAIL("tmpdir");
4765+
}
4766+
write_go_c_ref_guard_fixture(tmp, 0);
4767+
4768+
char db_path[512];
4769+
snprintf(db_path, sizeof(db_path), "%s/go_rw.db", tmp);
4770+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
4771+
ASSERT_NOT_NULL(p);
4772+
ASSERT_EQ(cbm_pipeline_run(p), 0);
4773+
const char *project = cbm_pipeline_project_name(p);
4774+
4775+
cbm_store_t *s = cbm_store_open_path(db_path);
4776+
ASSERT_NOT_NULL(s);
4777+
4778+
/* Reproduce-first: RED before the fix — the Go local write binds the C
4779+
* probe's global, and the Go value use binds the C `handle`. */
4780+
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES"));
4781+
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS"));
4782+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES"));
4783+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS"));
4784+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE"));
4785+
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES"));
4786+
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE"));
4787+
/* Same-language reference edges survive the guard. */
4788+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES"));
4789+
4790+
cbm_store_close(s);
4791+
cbm_pipeline_free(p);
4792+
th_rmtree(tmp);
4793+
PASS();
4794+
}
4795+
4796+
TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
4797+
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
4798+
* second, independent resolver in pass_parallel.c (resolve_file_usages /
4799+
* resolve_file_rw) that must consult the same guard — the field census
4800+
* that motivated this caught the sequential-only fix leaving 344 Go→C
4801+
* WRITES alive on a ~1150-file repo. >= 50 files forces the parallel
4802+
* pipeline. */
4803+
char tmp[256];
4804+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rwp_XXXXXX");
4805+
if (!cbm_mkdtemp(tmp)) {
4806+
FAIL("tmpdir");
4807+
}
4808+
write_go_c_ref_guard_fixture(tmp, 52);
4809+
4810+
char db_path[512];
4811+
snprintf(db_path, sizeof(db_path), "%s/go_rwp.db", tmp);
4812+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
4813+
ASSERT_NOT_NULL(p);
4814+
ASSERT_EQ(cbm_pipeline_run(p), 0);
4815+
const char *project = cbm_pipeline_project_name(p);
4816+
4817+
cbm_store_t *s = cbm_store_open_path(db_path);
4818+
ASSERT_NOT_NULL(s);
4819+
4820+
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES"));
4821+
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS"));
4822+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES"));
4823+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS"));
4824+
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE"));
4825+
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES"));
4826+
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE"));
4827+
ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES"));
4828+
4829+
cbm_store_close(s);
4830+
cbm_pipeline_free(p);
4831+
th_rmtree(tmp);
4832+
PASS();
4833+
}
4834+
46914835
/* Count nodes with the given exact name in the project (e.g. a Route path). */
46924836
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
46934837
cbm_node_t *ns = NULL;
@@ -12805,6 +12949,8 @@ SUITE(pipeline) {
1280512949
#endif
1280612950
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
1280712951
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
12952+
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
12953+
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
1280812954
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
1280912955
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
1281012956
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);

‎tests/test_registry.c‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,34 @@ TEST(cross_language_suffix_match_drops_py_vs_js) {
811811
PASS();
812812
}
813813

814+
TEST(cross_language_ref_drops_go_vs_c) {
815+
/* #1928: the USAGE/WRITES/READS analog of #725. Reference edges carry no
816+
* import-closure evidence, so EVERY registry strategy is a bare-name
817+
* guess and no strategy-level carve-out applies — the predicate takes no
818+
* strategy at all. */
819+
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "bpf/probe.c"));
820+
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "driver/mock.hpp"));
821+
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_C, "pkg/events/event.go"));
822+
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_PYTHON, "web/src/pages/Editor.js"));
823+
/* Same language → keep. */
824+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "pkg/state/state.go"));
825+
/* C and C++ are one family: .h maps to CBM_LANG_CPP, and a .c file
826+
* referencing its own header's declarations is not a boundary. */
827+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_C, "bpf/probe.h"));
828+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_CPP, "driver/compat.c"));
829+
/* …but Go into a header is still a boundary. */
830+
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "bpf/probe.h"));
831+
/* JS/TS/TSX/ArkTS are one family. */
832+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_JAVASCRIPT, "lib/util.ts"));
833+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_TYPESCRIPT, "ui/Panel.tsx"));
834+
/* Unknown caller/target language or no path → nothing to judge → keep. */
835+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_COUNT, "store.py"));
836+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, NULL));
837+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, ""));
838+
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "Makefile.inc.unknownext"));
839+
PASS();
840+
}
841+
814842
TEST(dynamic_suppress_drops_weak_method_matches) {
815843
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
816844
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
@@ -945,6 +973,7 @@ SUITE(registry) {
945973
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
946974
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
947975
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
976+
RUN_TEST(cross_language_ref_drops_go_vs_c);
948977
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
949978
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
950979
}

0 commit comments

Comments
 (0)