Skip to content

Commit c175ca1

Browse files
fix(pipeline): a bare Go reference never binds a struct Field
The READS/WRITES resolvers and the USAGE registry fallback hand bare reference text to the short-name registry, which contains Field nodes - so once Go struct fields exist (DeusData#1935), every Go local err := ... binds whichever struct field is named err, project-wide: 21308 USAGE and 5191 WRITES onto Go fields on the measured repo, top target a test struct's field T collecting 3013 edges. In Go that binding is impossible by construction: a field is only reachable through a selector expression (x.f), and selector references resolve on the LSP path - every Field-targeted reference edge in the census carried dot-less text. Add cbm_go_suppress_bare_field_ref() next to the DeusData#1928 predicate and consult it at the same four sites (both READS/WRITES resolvers, both USAGE registry fallbacks): drop the bind when the file is Go, the target label is Field, and the reference text has no '.'. Go-gated because C#/Java/C++/Python method bodies legitimately reference their own members bare (cp_reads_writes_cs_static_field pins that shape). Field-validated on the DeusData#1940 stack: USAGE onto Go fields 21308 -> 0, WRITES 5191 -> 0; the only remaining field-targeted edges are 2466 CALLS, which are DeusData#1906/DeusData#1907's selector-guard territory. Reproduce- first pipeline probes (sequential + parallel twins) were RED on the stack without this commit. Fixes DeusData#1942 Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
1 parent e07a12b commit c175ca1

6 files changed

Lines changed: 169 additions & 0 deletions

File tree

src/pipeline/pass_parallel.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,6 +2660,11 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
26602660
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
26612661
continue;
26622662
}
2663+
/* #1942: a bare Go reference can never denote a struct field. */
2664+
if (tgt &&
2665+
cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) {
2666+
continue;
2667+
}
26632668
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
26642669
cbm_pipeline_node_is_callable_target(tgt))) {
26652670
continue;
@@ -2745,6 +2750,10 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
27452750
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
27462751
continue;
27472752
}
2753+
/* #1942: a bare Go reference can never denote a struct field. */
2754+
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) {
2755+
continue;
2756+
}
27482757
const char *etype = rw->is_write ? "WRITES" : "READS";
27492758
cbm_gbuf_insert_edge(ws->local_edge_buf, src->id, tgt->id, etype, "{}");
27502759
}

src/pipeline/pass_usages.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
216216
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
217217
continue;
218218
}
219+
/* #1942: a bare Go reference can never denote a struct field. */
220+
if (tgt &&
221+
cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->ref_name, tgt->label)) {
222+
continue;
223+
}
219224
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
220225
cbm_pipeline_node_is_callable_target(tgt))) {
221226
continue;
@@ -304,6 +309,10 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
304309
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
305310
continue;
306311
}
312+
/* #1942: a bare Go reference can never denote a struct field. */
313+
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->var_name, tgt->label)) {
314+
continue;
315+
}
307316

308317
const char *edge_type = rw->is_write ? "WRITES" : "READS";
309318
cbm_gbuf_insert_edge(ctx->gbuf, src->id, tgt->id, edge_type, "{}");

src/pipeline/pipeline.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
296296
* unit-tested in test_registry.c. */
297297
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);
298298

299+
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
300+
* field access is always a selector expression, and selector references
301+
* resolve on the LSP path. Drops a READS/WRITES/USAGE bind whose target is a
302+
* Field when the reference text carries no '.'. Go only: other OO languages
303+
* legitimately reference their own members bare inside method bodies. Pure;
304+
* unit-tested in test_registry.c. */
305+
bool cbm_go_suppress_bare_field_ref(bool is_go, const char *ref_name, const char *target_label);
306+
299307
/* Get the label of a qualified name, or NULL if not found. */
300308
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
301309

src/pipeline/registry.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,24 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
546546
return true;
547547
}
548548

549+
bool cbm_go_suppress_bare_field_ref(bool is_go, const char *ref_name, const char *target_label) {
550+
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
551+
* field access is always a selector expression (x.f), and selector
552+
* references resolve through the LSP join, never through the bare-name
553+
* registry fallback. Every Field-targeted reference edge in the field
554+
* census carried dot-less text, so dropping the bind loses nothing real.
555+
* Go-gated: a C#/Java/C++/Python method body legitimately references its
556+
* own members bare (cp_reads_writes_cs_static_field pins that shape as
557+
* required), so a global veto would break those languages. */
558+
if (!is_go || !ref_name || !ref_name[0] || !target_label) {
559+
return false;
560+
}
561+
if (strcmp(target_label, "Field") != 0) {
562+
return false;
563+
}
564+
return strchr(ref_name, '.') == NULL;
565+
}
566+
549567
/* ── Lifecycle ──────────────────────────────────────────────────── */
550568

551569
cbm_registry_t *cbm_registry_new(void) {

tests/test_pipeline.c

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4832,6 +4832,108 @@ TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) {
48324832
PASS();
48334833
}
48344834

4835+
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name);
4836+
4837+
/* Fixture for the #1942 bare-reference-vs-Field probes: a Go struct field
4838+
* named like the commonest local (`err`), and a function whose local of the
4839+
* same name must NOT bind it — in Go a field is only reachable through a
4840+
* selector expression, never a bare identifier. Needs Go Field extraction
4841+
* (#1935) to have anything to falsely bind. */
4842+
static void write_go_bare_field_fixture(const char *tmp, int pad_files) {
4843+
write_temp_file(tmp, "go.mod", "module example.com/fxbare\n\ngo 1.22\n");
4844+
write_temp_file(tmp, "state/state.go",
4845+
"package state\n"
4846+
"\n"
4847+
"type Tracker struct {\n"
4848+
"\terr error\n"
4849+
"\tn int\n"
4850+
"}\n");
4851+
write_temp_file(tmp, "app/app.go",
4852+
"package app\n"
4853+
"\n"
4854+
"import \"errors\"\n"
4855+
"\n"
4856+
"func Run() error {\n"
4857+
"\terr := errors.New(\"x\")\n"
4858+
"\treturn err\n"
4859+
"}\n");
4860+
for (int i = 0; i < pad_files; i++) {
4861+
char name[64];
4862+
char body[128];
4863+
snprintf(name, sizeof(name), "pad/filler%d.go", i);
4864+
snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i);
4865+
write_temp_file(tmp, name, body);
4866+
}
4867+
}
4868+
4869+
TEST(pipeline_go_bare_ref_never_binds_field) {
4870+
/* #1942: the READS/WRITES resolvers and the USAGE registry fallback hand
4871+
* bare reference text to the short-name registry, which contains Field
4872+
* nodes — so every Go local `err := …` bound whichever struct field was
4873+
* named err (21308 USAGE / 5191 WRITES onto Go fields on the measured
4874+
* repo, top target a test struct's field collecting 3013 edges).
4875+
* Sequential-path twin; the parallel twin follows. */
4876+
char tmp[256];
4877+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_bare_XXXXXX");
4878+
if (!cbm_mkdtemp(tmp)) {
4879+
FAIL("tmpdir");
4880+
}
4881+
write_go_bare_field_fixture(tmp, 0);
4882+
4883+
char db_path[512];
4884+
snprintf(db_path, sizeof(db_path), "%s/go_bare.db", tmp);
4885+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
4886+
ASSERT_NOT_NULL(p);
4887+
ASSERT_EQ(cbm_pipeline_run(p), 0);
4888+
const char *project = cbm_pipeline_project_name(p);
4889+
4890+
cbm_store_t *s = cbm_store_open_path(db_path);
4891+
ASSERT_NOT_NULL(s);
4892+
4893+
/* The field must exist for the probe to mean anything (#1935's fix). */
4894+
ASSERT_TRUE(count_nodes_named(s, project, "err") >= 1);
4895+
/* Reproduce-first: RED before the fix — the bare local binds the field. */
4896+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES"));
4897+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS"));
4898+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE"));
4899+
4900+
cbm_store_close(s);
4901+
cbm_pipeline_free(p);
4902+
th_rmtree(tmp);
4903+
PASS();
4904+
}
4905+
4906+
TEST(pipeline_go_bare_ref_never_binds_field_parallel) {
4907+
/* Parallel twin: resolve_file_rw / resolve_file_usages are independent
4908+
* resolvers and must consult the same predicate (#1928's lesson). */
4909+
char tmp[256];
4910+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_barep_XXXXXX");
4911+
if (!cbm_mkdtemp(tmp)) {
4912+
FAIL("tmpdir");
4913+
}
4914+
write_go_bare_field_fixture(tmp, 52);
4915+
4916+
char db_path[512];
4917+
snprintf(db_path, sizeof(db_path), "%s/go_barep.db", tmp);
4918+
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
4919+
ASSERT_NOT_NULL(p);
4920+
ASSERT_EQ(cbm_pipeline_run(p), 0);
4921+
const char *project = cbm_pipeline_project_name(p);
4922+
4923+
cbm_store_t *s = cbm_store_open_path(db_path);
4924+
ASSERT_NOT_NULL(s);
4925+
4926+
ASSERT_TRUE(count_nodes_named(s, project, "err") >= 1);
4927+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES"));
4928+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS"));
4929+
ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE"));
4930+
4931+
cbm_store_close(s);
4932+
cbm_pipeline_free(p);
4933+
th_rmtree(tmp);
4934+
PASS();
4935+
}
4936+
48354937
/* Count nodes with the given exact name in the project (e.g. a Route path). */
48364938
static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) {
48374939
cbm_node_t *ns = NULL;
@@ -12951,6 +13053,8 @@ SUITE(pipeline) {
1295113053
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
1295213054
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
1295313055
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
13056+
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
13057+
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
1295413058
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
1295513059
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
1295613060
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);

tests/test_registry.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,26 @@ TEST(cross_language_ref_drops_go_vs_c) {
839839
PASS();
840840
}
841841

842+
TEST(go_bare_ref_never_binds_field) {
843+
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
844+
* field access is always a selector expression. */
845+
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "err", "Field"));
846+
ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, "config", "Field"));
847+
/* A selector-shaped reference may bind a field. */
848+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "t.err", "Field"));
849+
/* Bare references to non-fields are untouched. */
850+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "Variable"));
851+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", "Function"));
852+
/* Other languages reference their own members bare inside methods —
853+
* never suppressed (cp_reads_writes_cs_static_field pins the C# shape). */
854+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, "_count", "Field"));
855+
/* Degenerate inputs → nothing to judge. */
856+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, NULL, "Field"));
857+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "", "Field"));
858+
ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, "err", NULL));
859+
PASS();
860+
}
861+
842862
TEST(dynamic_suppress_drops_weak_method_matches) {
843863
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
844864
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
@@ -974,6 +994,7 @@ SUITE(registry) {
974994
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
975995
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
976996
RUN_TEST(cross_language_ref_drops_go_vs_c);
997+
RUN_TEST(go_bare_ref_never_binds_field);
977998
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
978999
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
9791000
}

0 commit comments

Comments
 (0)