diff --git a/src/watcher/svn_state.c b/src/watcher/svn_state.c index cd3eef391..a1da84f5e 100644 --- a/src/watcher/svn_state.c +++ b/src/watcher/svn_state.c @@ -1173,8 +1173,12 @@ static int walk_candidates(svn_walk_t *walk, candidate_list_t *candidates) { } static int fingerprint_candidates(parser_t *parser, cbm_svn_observation_t *observation) { - qsort(parser->candidates.paths, parser->candidates.count, sizeof(*parser->candidates.paths), - candidate_compare); + /* With no candidates the array is NULL, and glibc declares qsort's base + * nonnull even for a zero count. */ + if (parser->candidates.count > 1) { + qsort(parser->candidates.paths, parser->candidates.count, sizeof(*parser->candidates.paths), + candidate_compare); + } svn_walk_t walk = {0}; walk.root = parser->root; int result = 0; diff --git a/tests/test_svn_state.c b/tests/test_svn_state.c index eca0d9d36..ff6a9f17c 100644 --- a/tests/test_svn_state.c +++ b/tests/test_svn_state.c @@ -198,6 +198,10 @@ TEST(parse_rejects_declarations_and_truncated_documents) { PASS(); } +/* No entry here needs its content read, so the candidate list stays empty: + * this also guards against sorting a NULL array. That guard binds only on the + * trapping UBSan leg (-fno-sanitize-recover=undefined); macOS and recovering + * UBSan pass without it. */ TEST(parse_clean_props_and_externals_are_not_local_changes) { wc_fixture_t fixture; ASSERT_TRUE(wc_fixture_init(&fixture, "cbm_svn_clean"));