fix(pipeline): guard unsafe header overlay pairs

Keep the single-header overlay fast path for safe header-only additions, but fall back when a C-family header delta contains type-like nodes and a same-stem implementation file exists.

This prevents active overlay from selecting header-owned type rows where a fresh full rebuild selects richer source-owned type rows, as seen in the store.h/store.c batch diagnostics.

Adds a pipeline canary for header type/implementation pair fallback while preserving the single-header overlay and multi-file header safety canaries.

Validation: focused header overlay tests passed; pipeline suite passed 326; source safety OK; diff check clean; production build passed; one_source_file benchmark stayed incremental_overlay at 29.677x. Full ASan target was also run and was not clean, with obsolete in-run and environment-sensitive failures recorded in the notes.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-04 11:42:28 -04:00
parent b6cc825941
commit 320d63a7d2
3 changed files with 123 additions and 0 deletions
+54
View File
@@ -85,6 +85,54 @@ static bool incr_changed_contains_c_family_header(const cbm_file_info_t *changed
return false;
}
static bool incr_file_delta_has_type_like_node(const cbm_pipeline_file_delta_t *delta) {
if (!delta || delta->change_kind == CBM_PIPELINE_DELTA_CHANGE_DELETE) {
return false;
}
for (int i = 0; i < delta->delta.node_count; i++) {
const cbm_node_t *node = &delta->delta.nodes[i];
if (cbm_label_is_type_like(node->label)) {
return true;
}
}
return false;
}
static bool incr_same_stem_impl_exists(const char *path) {
static const char *const impl_exts[] = {".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".cu"};
if (!path || !path[0]) {
return false;
}
const char *dot = strrchr(path, '.');
if (!dot || dot == path) {
return false;
}
size_t stem_len = (size_t)(dot - path);
if (stem_len >= CBM_PATH_MAX || stem_len > (size_t)INT_MAX) {
return false;
}
for (size_t i = 0; i < sizeof(impl_exts) / sizeof(impl_exts[0]); i++) {
char candidate[CBM_PATH_MAX];
int n = snprintf(candidate, sizeof(candidate), "%.*s%s", (int)stem_len, path,
impl_exts[i]);
if (n < 0 || (size_t)n >= sizeof(candidate)) {
continue;
}
if (cbm_file_exists(candidate)) {
return true;
}
}
return false;
}
static bool incr_header_overlay_has_type_impl_pair(const cbm_file_info_t *file,
const cbm_pipeline_file_delta_t *delta) {
if (!file || !incr_is_c_family_header(file->language, file->rel_path)) {
return false;
}
return incr_file_delta_has_type_like_node(delta) && incr_same_stem_impl_exists(file->path);
}
/* ── File classification ─────────────────────────────────────────── */
/* Classify discovered files against stored metadata.
@@ -1563,6 +1611,12 @@ static int incr_try_overlay_upsert_route(cbm_pipeline_t *p, cbm_store_t *store,
itoa_buf_incr(rc));
goto cleanup;
}
if (incr_header_overlay_has_type_impl_pair(&changed_files[i], &deltas[i])) {
cbm_pipeline_set_publish_reason(p, CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR);
cbm_log_info("incremental.overlay.fallback", "reason",
CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR);
goto cleanup;
}
int preserved = 0;
rc = cbm_pipeline_file_delta_add_preserved_inbound_edges(store, &deltas[i], &preserved);
if (rc != CBM_STORE_OK) {
+1
View File
@@ -349,6 +349,7 @@ typedef struct {
#define CBM_PIPELINE_DELTA_REASON_INBOUND_EDGES_REQUIRE_FULL "inbound_edges_require_full"
#define CBM_PIPELINE_DELTA_REASON_PREFLIGHT_ERROR "preflight_error"
#define CBM_PIPELINE_DELTA_REASON_CROSS_FILE_NODE_QN_COLLISION "cross_file_node_qn_collision"
#define CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR "header_type_impl_pair"
/* Conservative default exact-delta caps. Larger affected sets fall back to the
* containment path unless config opts into a benchmarked frontier size. */
+68
View File
@@ -10212,6 +10212,73 @@ TEST(incremental_overlay_publish_single_c_header_uses_active_overlay) {
PASS();
}
TEST(incremental_overlay_single_c_header_type_impl_pair_falls_back) {
if (setup_incremental_repo() != 0) {
FAIL("setup failed");
}
char header_path[CBM_PATH_MAX];
char source_path[CBM_PATH_MAX];
int n = snprintf(header_path, sizeof(header_path), "%s/paired.h", g_incr_tmpdir);
ASSERT(n >= 0 && (size_t)n < sizeof(header_path));
n = snprintf(source_path, sizeof(source_path), "%s/paired.c", g_incr_tmpdir);
ASSERT(n >= 0 && (size_t)n < sizeof(source_path));
ASSERT_EQ(th_write_file(header_path,
"#ifndef PAIRED_H\n"
"#define PAIRED_H\n"
"typedef struct Paired Paired;\n"
"int paired_value(Paired *p);\n"
"#endif\n"),
0);
ASSERT_EQ(th_write_file(source_path,
"#include \"paired.h\"\n\n"
"struct Paired {\n"
" int value;\n"
"};\n\n"
"int paired_value(Paired *p) {\n"
" return p ? p->value : 0;\n"
"}\n"),
0);
cbm_config_t *cfg = incremental_test_config(g_incr_tmpdir);
ASSERT_NOT_NULL(cfg);
ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_OVERLAY_PUBLISH,
CBM_CONFIG_OVERLAY_PUBLISH_SMALL_DELTAS),
0);
cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
cbm_pipeline_free(p);
ASSERT_EQ(th_write_file(header_path,
"#ifndef PAIRED_H\n"
"#define PAIRED_H\n"
"typedef struct Paired Paired;\n"
"int paired_value(Paired *p);\n"
"static int paired_extra(void) {\n"
" return 7;\n"
"}\n"
"#endif\n"),
0);
p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
pipeline_capture_logs_start();
int run_rc = cbm_pipeline_run(p);
const char *logs = pipeline_capture_logs_end();
ASSERT_EQ(run_rc, 0);
ASSERT(strstr(logs, "msg=incremental.overlay.fallback reason="
CBM_PIPELINE_DELTA_REASON_HEADER_TYPE_IMPL_PAIR) != NULL);
ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT);
cbm_pipeline_free(p);
cbm_config_close(cfg);
cleanup_incremental_repo();
PASS();
}
TEST(incremental_c_header_uses_exact_not_overlay_until_active_edges_are_exact) {
enum {
PIPELINE_C_HEADER_OVERLAY_MAX_AFFECTED = CBM_SZ_16,
@@ -13761,6 +13828,7 @@ SUITE(pipeline) {
RUN_TEST(incremental_fast_falls_back_for_oversized_inbound_frontier_and_matches_full);
RUN_TEST(incremental_fast_c_header_frontier_too_large_uses_full_rebuild);
RUN_TEST(incremental_overlay_publish_single_c_header_uses_active_overlay);
RUN_TEST(incremental_overlay_single_c_header_type_impl_pair_falls_back);
RUN_TEST(incremental_c_header_uses_exact_not_overlay_until_active_edges_are_exact);
RUN_TEST(incremental_fast_configured_frontier_cap_allows_bounded_exact);
RUN_TEST(incremental_fast_mixed_unowned_edge_frontier_falls_back_before_exact_build);