mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-06 08:35:21 +08:00
fix(pipeline): skip the function sort when the graph has no functions
phase1_scan_functions() in pass_semantic_edges.c sorts the collected Function/Method node pointers so the func order is deterministic. When the graph has no Function or Method node at all -- a struct-only or config-only project -- node_ptrs is never allocated and the call becomes qsort(NULL, 0, ...). The func_count <= 0 early-out lives in phase 1b, which runs only after phase 1 has returned, so it never guards this call. A NULL qsort base is undefined behaviour. glibc declares the parameter nonnull, so UBSan on the Linux leg reports "null pointer passed as argument 1, which is declared to never be null" at the call. The macOS SDK carries no such attribute, so neither the default sanitizer build nor the Homebrew LLVM diag lane can flag it there. The existing pipeline_docstring_go_class test already reaches the call on Linux; the harness never gated on recoverable UBSan reports, so it went unnoticed. Guard the sort on func_count > 0. The new test pipeline_semantic_edges_no_functions indexes a Go struct-only fixture through the full pipeline and pins the fixture to what it claims (Struct >= 1, Function = Method = 0). On the Linux leg with UBSAN_OPTIONS=halt_on_error=1 the pipeline suite aborts at the qsort call before the fix and completes with it. Distilled from #1810 with co-author credit. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> Co-authored-by: Yusuf S. Demirci <ysf-samet@outlook.com>
This commit is contained in:
co-authored by
Yusuf S. Demirci
parent
aa44c28ea5
commit
77a0c7d941
@@ -992,8 +992,12 @@ static int phase1_scan_functions(cbm_gbuf_t *gbuf, cbm_sem_func_t **out_funcs,
|
||||
* sequence — so an unstable order changes WHICH semantic edges are
|
||||
* emitted. Sort the cheap pointer array by qualified name (unique) and
|
||||
* re-derive the three fields set so far; the heavy per-func payloads are
|
||||
* filled in later phases, so no 12.7 KB structs are moved. */
|
||||
qsort(node_ptrs, (size_t)func_count, sizeof(node_ptrs[0]), cmp_node_ptr_by_qn);
|
||||
* filled in later phases, so no 12.7 KB structs are moved.
|
||||
* A graph with no Function/Method nodes never allocates node_ptrs, and
|
||||
* qsort's base is declared nonnull (glibc), so skip the sort outright. */
|
||||
if (func_count > 0) {
|
||||
qsort(node_ptrs, (size_t)func_count, sizeof(node_ptrs[0]), cmp_node_ptr_by_qn);
|
||||
}
|
||||
for (int k = 0; k < func_count; k++) {
|
||||
funcs[k].node_id = node_ptrs[k]->id;
|
||||
funcs[k].file_path = node_ptrs[k]->file_path;
|
||||
|
||||
@@ -13409,6 +13409,54 @@ TEST(pipeline_markdown_and_config_prose_reaches_fts_body) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A graph with no Function or Method nodes at all -- a struct-only or
|
||||
* config-only project -- must run pass_semantic_edges cleanly. Its phase-1
|
||||
* scan used to hand qsort() a NULL base with count 0, before the func_count
|
||||
* early-out in phase 1b could run. glibc declares qsort's base nonnull, so
|
||||
* UBSan on the Linux leg reports that call; the macOS SDK carries no such
|
||||
* attribute, so the sanitizer is silent there. The label counts pin the
|
||||
* fixture to what it claims: at least one Struct and zero Function/Method,
|
||||
* i.e. the scan genuinely finds nothing to sort. */
|
||||
TEST(pipeline_semantic_edges_no_functions) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_nofunc_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
|
||||
char path[512];
|
||||
char dbpath[512];
|
||||
snprintf(dbpath, sizeof(dbpath), "%s/nofunc.db", tmp);
|
||||
|
||||
snprintf(path, sizeof(path), "%s/main.go", tmp);
|
||||
ASSERT_EQ(th_write_file(path, "package main\n\ntype Widget struct{}\n"), 0);
|
||||
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, dbpath, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
cbm_pipeline_free(p);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(dbpath);
|
||||
ASSERT_NOT_NULL(s);
|
||||
sqlite3_stmt *st = NULL;
|
||||
ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT COUNT(*) FROM nodes WHERE label = ?1",
|
||||
-1, &st, NULL),
|
||||
SQLITE_OK);
|
||||
const char *labels[] = {"Struct", "Function", "Method"};
|
||||
int counts[3] = {0, 0, 0};
|
||||
for (size_t i = 0; i < sizeof(labels) / sizeof(labels[0]); i++) {
|
||||
sqlite3_reset(st);
|
||||
sqlite3_bind_text(st, 1, labels[i], -1, SQLITE_TRANSIENT);
|
||||
ASSERT_EQ(sqlite3_step(st), SQLITE_ROW);
|
||||
counts[i] = sqlite3_column_int(st, 0);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
cbm_store_close(s);
|
||||
ASSERT_GT(counts[0], 0);
|
||||
ASSERT_EQ(counts[1], 0);
|
||||
ASSERT_EQ(counts[2], 0);
|
||||
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
|
||||
/* Join two Studio Export range strings and hand back the result. The caller
|
||||
* owns nothing: the string lives in the aggregate's arena, so copy it out
|
||||
@@ -13827,6 +13875,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(pipeline_ensemble_routing_unterminated_item_is_safe);
|
||||
RUN_TEST(pipeline_delta_patch_indexes_docstring_into_fts_body);
|
||||
RUN_TEST(pipeline_markdown_and_config_prose_reaches_fts_body);
|
||||
RUN_TEST(pipeline_semantic_edges_no_functions);
|
||||
}
|
||||
|
||||
/* Focused semantic-manifest and publication contracts. Kept separate from the
|
||||
|
||||
Reference in New Issue
Block a user