From 6a343a7bcefdd92d05b646def79896c467adc9ce Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 18:33:13 +0200 Subject: [PATCH] test(similarity): cover the empty-graph qsort null base (#1367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The production guard for #1367 — collect_fp_entries only sorting when count > 1 — is on main, but nothing exercises it. A fix without a test is a fix that can be silently undone, and this one is unusually easy to undo: removing the guard produces no visible symptom on most lanes. Adds the missing case: a graph with zero Function/Method nodes, where the entry array is never allocated and the null base would reach qsort. The test comment records WHERE the test binds, because that turned out to be the interesting part. Removing the guard again: macOS 25 passed (Apple libc does not mark qsort nonnull, so there is no UB to see) Linux ASan/UBSan, recovering 25 passed (prints "null pointer passed as argument 1", then keeps going) Linux UBSan, trapping exit 1 (process aborts — the RED) So the assertions here are the behavioural half (empty graph → rc 0, no SIMILAR_TO edges), and the UB half is caught by the trap-UBSan leg alone. Both were measured on ubuntu-arm64 in each direction. Spelling that out in the test keeps a future reader from checking it on a recovering lane, seeing green, and concluding the null-base guard is still there when it is not. Signed-off-by: Martin Vogel --- tests/test_simhash.c | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_simhash.c b/tests/test_simhash.c index f22df753d..9f17576bf 100644 --- a/tests/test_simhash.c +++ b/tests/test_simhash.c @@ -803,6 +803,45 @@ TEST(pass_similarity_short_funcs_skipped) { PASS(); } +TEST(pass_similarity_empty_graph_no_entries) { + /* Zero Function/Method nodes → the entry array is never allocated. The + * collect phase must not hand that null base to qsort: glibc declares qsort + * nonnull, so passing NULL is UB even at count 0 (#1367). + * + * WHERE THIS BINDS — read before "verifying" it. Remove the count > 1 guard + * in collect_fp_entries and this test still PASSES on macOS (Apple libc does + * not mark qsort nonnull) and on an ordinary Linux ASan/UBSan build, which + * merely prints "null pointer passed as argument 1" and keeps going. It goes + * RED only where UBSan TRAPS (-fno-sanitize-recover), where the process + * aborts and the runner exits 1. Measured both ways on ubuntu-arm64: + * recovering lane = 25 passed with AND without the guard; trapping lane = + * exit 1 without it, 25 passed with it. + * + * The assertions below are therefore the behavioural half (an empty graph + * yields rc 0 and no SIMILAR_TO edges); the UB half is caught by the + * trap-UBSan leg alone. Do not read a green recovering-lane run as proof + * that the null-base guard is still in place. */ + cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp"); + + atomic_int cancelled = 0; + cbm_pipeline_ctx_t ctx = { + .project_name = "test", + .repo_path = "/tmp", + .gbuf = gb, + .registry = NULL, + .cancelled = &cancelled, + }; + + int rc = cbm_pipeline_pass_similarity(&ctx); + ASSERT_EQ(rc, 0); + + int sim_count = count_similar_to_edges(gb); + ASSERT_EQ(sim_count, 0); + + cbm_gbuf_free(gb); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Suite 4: Full Pipeline Integration (generated test project) * ═══════════════════════════════════════════════════════════════════ */ @@ -1165,6 +1204,7 @@ SUITE(simhash) { RUN_TEST(pass_similarity_edge_properties); RUN_TEST(pass_similarity_max_edges_cap); RUN_TEST(pass_similarity_short_funcs_skipped); + RUN_TEST(pass_similarity_empty_graph_no_entries); /* Suite 4: Full Pipeline Integration */ RUN_TEST(pipeline_minhash_end_to_end);