Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,12 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
continue;
}
tgt = cbm_gbuf_find_by_qn(rc->main_gbuf, res.qualified_name);
/* #1928: the registry fallback is a bare-name guess — never let it
* bind a reference across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
Expand Down Expand Up @@ -2713,7 +2719,7 @@ static void resolve_file_throws(resolve_ctx_t *rc, resolve_worker_state_t *ws,
/* Resolve reads/writes for one file. */
static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFileResult *result,
const char *rel, const char *module_qn, const char **imp_keys,
const char **imp_vals, int imp_count) {
const char **imp_vals, int imp_count, CBMLanguage lang) {
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
if (!rw->var_name) {
Expand All @@ -2733,6 +2739,12 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
if (!tgt || src->id == tgt->id) {
continue;
}
/* #1928: every resolution here is a bare-name registry guess — never
* let it bind a read/write across a language boundary. Mirrors the
* sequential twin (pass_usages.c). */
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
const char *etype = rw->is_write ? "WRITES" : "READS";
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
}
Expand Down Expand Up @@ -3179,7 +3191,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) {

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

/* ── INHERITS + DECORATES + IMPLEMENTS ──────────────────── */
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pass_usages.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
continue;
}
tgt = cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
/* #1928: the registry fallback is a bare-name guess — never let it
* bind a reference across a language boundary (the LSP-backed
* semantic branch above is not affected). */
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
cbm_pipeline_node_is_callable_target(tgt))) {
continue;
Expand Down Expand Up @@ -270,7 +276,7 @@ static int resolve_throw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
/* Resolve READS/WRITES edges for one file's extracted read/write accesses. */
static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result, const char *rel,
const char *module_qn, const char **imp_keys, const char **imp_vals,
int imp_count) {
int imp_count, CBMLanguage lang) {
int resolved = 0;
for (int r = 0; r < result->rw.count; r++) {
CBMReadWrite *rw = &result->rw.items[r];
Expand All @@ -293,6 +299,11 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
if (!tgt || src->id == tgt->id) {
continue;
}
/* #1928: every resolution here is a bare-name registry guess — never
* let it bind a read/write across a language boundary. */
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}

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

free(module_qn);
free_import_map(imp_keys, imp_vals, imp_count);
Expand Down
8 changes: 8 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
const char *strategy);

/* #1928: USAGE/WRITES/READS analog of the CALLS guard above. Reference edges
* resolved by the short-name registry carry no import-closure evidence, so a
* cross-language binding is a bare-name collision for EVERY strategy — drop
* it whenever the caller's language and the target file's language disagree
* (JS/TS family members and the C/C++ header family excepted). Pure;
* unit-tested in test_registry.c. */
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);

/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);

Expand Down
38 changes: 38 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,13 @@ static bool js_ts_family(CBMLanguage lang) {
lang == CBM_LANG_ARKTS;
}

/* C and C++ are one family for cross-language checks: .h maps to CBM_LANG_CPP
* in the extension table, so a .c file referencing a symbol declared in its
* own header would otherwise read as a language boundary. */
static bool c_cpp_family(CBMLanguage lang) {
return lang == CBM_LANG_C || lang == CBM_LANG_CPP;
}

static const char *path_basename(const char *path) {
if (!path || !path[0]) {
return path;
Expand Down Expand Up @@ -508,6 +515,37 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
return true;
}

bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path) {
/* #1928: USAGE / WRITES / READS analog of the CALLS guard above. A
* variable or field reference resolved by the short-name registry must
* not cross a language boundary: unlike CALLS, a reference edge carries
* no import-closure evidence at all — a Go test's local `event` and an
* eBPF C probe's automatic `event` share nothing but the spelling, so
* EVERY registry strategy is a bare-name guess here and none is exempt.
* LSP-backed semantic references resolve before the registry fallback
* and never reach this predicate, which is where a genuine cross-language
* binding (a future cgo resolver) would live. The JS/TS family keeps its
* exemption (.js/.ts/.d.ts pairs legitimately share symbols), and C/C++
* count as one family (.h maps to CBM_LANG_CPP). */
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
return false;
}
CBMLanguage target_lang = cbm_language_for_filename(path_basename(target_file_path));
if (target_lang == CBM_LANG_COUNT) {
return false;
}
if (caller_lang == target_lang) {
return false;
}
if (js_ts_family(caller_lang) && js_ts_family(target_lang)) {
return false;
}
if (c_cpp_family(caller_lang) && c_cpp_family(target_lang)) {
return false;
}
return true;
}

/* ── Lifecycle ──────────────────────────────────────────────────── */

cbm_registry_t *cbm_registry_new(void) {
Expand Down
152 changes: 149 additions & 3 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -824,8 +824,8 @@ TEST(pipeline_calls_resolution) {

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

static bool cross_file_call_exists(cbm_store_t *s, const char *project, const char *src_name,
const char *tgt_name) {
return cross_file_edge_exists(s, project, src_name, tgt_name, "CALLS");
}

/* True iff the exact named CALLS edge exists and its serialized strategy
* contains `strategy_fragment`. Parallel synthetic-carrier regressions use
* this on a separate ordinary-call control: it proves the cross-file LSP ran
Expand Down Expand Up @@ -4688,6 +4693,145 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
PASS();
}

/* Fixture for the #1928 cross-language reference-guard probes (sequential and
* parallel twins). pad_files > 0 adds filler files to push the index over the
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
* path and both must consult the guard. */
static void write_go_c_ref_guard_fixture(const char *tmp, int pad_files) {
write_temp_file(tmp, "go.mod", "module example.com/fxguard\n\ngo 1.22\n");
/* The C probe: a local named `event` and a file-scope function `handle`,
* the two shapes Go identifiers collide with. */
write_temp_file(tmp, "probe/probe.c",
"static int total_events = 0;\n"
"\n"
"static int handle(void) {\n"
" int event = 0;\n"
" total_events += event;\n"
" return event;\n"
"}\n");
/* Go: a local write named like the C local, and a value use named like
* the C function. Neither can touch anything in a C translation unit. */
write_temp_file(tmp, "app/app.go",
"package app\n"
"\n"
"func TrackEvent() int {\n"
"\tevent := 1\n"
"\treturn event\n"
"}\n"
"\n"
"func UsesHandle() int {\n"
"\th := handle\n"
"\t_ = h\n"
"\treturn 4\n"
"}\n"
"\n"
"func WriteTotal() {\n"
"\ttotal_events := 5\n"
"\t_ = total_events\n"
"}\n");
/* Same-language control: a Go package-level var written from another
* file must keep its WRITES edge. */
write_temp_file(tmp, "state/vars.go",
"package state\n"
"\n"
"var Counter int\n");
write_temp_file(tmp, "state/state.go",
"package state\n"
"\n"
"func Bump() {\n"
"\tCounter = 2\n"
"}\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "pad/filler%d.go", i);
snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i);
write_temp_file(tmp, name, body);
}
}

TEST(pipeline_go_rw_usage_never_cross_into_c) {
/* #1928: USAGE and WRITES/READS resolve through the same short-name
* registry as CALLS but never consulted the #725 cross-language guard.
* On a Go tree with eBPF C probes every Go identifier spelled like a C
* one produced an edge into the C file — 31.5% of all WRITES on the
* measured repo crossed the Go/C boundary. Go code cannot write a C
* automatic variable, so every edge in that class is false. This is the
* SEQUENTIAL-path twin; the parallel twin follows. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rw_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_go_c_ref_guard_fixture(tmp, 0);

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_rw.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);

cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);

/* Reproduce-first: RED before the fix — the Go local write binds the C
* probe's global, and the Go value use binds the C `handle`. */
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE"));
/* Same-language reference edges survive the guard. */
ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES"));

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
/* Parallel twin of the test above: USAGE and WRITES/READS each have a
* second, independent resolver in pass_parallel.c (resolve_file_usages /
* resolve_file_rw) that must consult the same guard — the field census
* that motivated this caught the sequential-only fix leaving 344 Go→C
* WRITES alive on a ~1150-file repo. >= 50 files forces the parallel
* pipeline. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rwp_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_go_c_ref_guard_fixture(tmp, 52);

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_rwp.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);

cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);

ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES"));
ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE"));
ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES"));

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

/* Count nodes with the given exact name in the project (e.g. a Route path). */
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
cbm_node_t *ns = NULL;
Expand Down Expand Up @@ -12805,6 +12949,8 @@ SUITE(pipeline) {
#endif
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);
Expand Down
29 changes: 29 additions & 0 deletions tests/test_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,34 @@ TEST(cross_language_suffix_match_drops_py_vs_js) {
PASS();
}

TEST(cross_language_ref_drops_go_vs_c) {
/* #1928: the USAGE/WRITES/READS analog of #725. Reference edges carry no
* import-closure evidence, so EVERY registry strategy is a bare-name
* guess and no strategy-level carve-out applies — the predicate takes no
* strategy at all. */
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "bpf/probe.c"));
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "driver/mock.hpp"));
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_C, "pkg/events/event.go"));
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_PYTHON, "web/src/pages/Editor.js"));
/* Same language → keep. */
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "pkg/state/state.go"));
/* C and C++ are one family: .h maps to CBM_LANG_CPP, and a .c file
* referencing its own header's declarations is not a boundary. */
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_C, "bpf/probe.h"));
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_CPP, "driver/compat.c"));
/* …but Go into a header is still a boundary. */
ASSERT_TRUE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "bpf/probe.h"));
/* JS/TS/TSX/ArkTS are one family. */
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_JAVASCRIPT, "lib/util.ts"));
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_TYPESCRIPT, "ui/Panel.tsx"));
/* Unknown caller/target language or no path → nothing to judge → keep. */
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_COUNT, "store.py"));
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, NULL));
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, ""));
ASSERT_FALSE(cbm_suppress_cross_language_ref(CBM_LANG_GO, "Makefile.inc.unknownext"));
PASS();
}

TEST(dynamic_suppress_drops_weak_method_matches) {
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
Expand Down Expand Up @@ -945,6 +973,7 @@ SUITE(registry) {
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
RUN_TEST(cross_language_ref_drops_go_vs_c);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
}
Loading