@@ -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). */
46924836static 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);
0 commit comments