fix: never qsort an empty k8s vector; split the spawner deletion test

Two reds of dry run 38321896354 on main 56700bdb, both reproduced locally:

- pass_k8s.c sorted empty vectors with qsort(NULL, 0, ...). glibc declares
  qsort's base nonnull, so GCC's UBSan reports it on Linux (pass_k8s.c:832,
  :1009); macOS does not flag it. Every qsort in the file now goes through
  k8s_qsort, which skips arrays of fewer than two elements.

- incremental_deleted_spawner_drops_its_process (cb497a82) deleted a shell
  spawner and a Python spawner in one step and expected closure repair.
  The Python namespace admission (#2531, merged after it) declines closure
  repair for any deleted Python module -- its names leave its importers'
  namespaces -- and pipeline_python_namespace_admission_deleted_and_empty
  pins that. The spawner test now deletes in two steps: the shell script
  through closure repair (its Process goes, 2 stay), then the Python file
  through the full route (1 stays). Both admission rules stay covered.

pipeline_semantic_manifest_repro 49 passed; pipeline, infrascan, spawns,
incremental and the pipeline_* suites 607 passed (macOS).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-13 03:00:47 +02:00
parent 56700bdb71
commit 2317c2e774
2 changed files with 42 additions and 19 deletions
+18 -9
View File
@@ -760,6 +760,15 @@ static void k8s_kusts_free(k8s_kusts_t *ks) {
*ks = (k8s_kusts_t){0};
}
/* qsort for this file's vectors. An empty vector's items is NULL, and glibc
* declares qsort's base nonnull, so sorting it is undefined even with n == 0
* (UBSan nonnull-attribute on Linux). Fewer than two elements need no sort. */
static void k8s_qsort(void *base, size_t n, size_t size, int (*cmp)(const void *, const void *)) {
if (base && n > 1) {
qsort(base, n, size, cmp);
}
}
static int k8s_cmp_str(const void *a, const void *b) {
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
@@ -827,9 +836,9 @@ static void k8s_kusts_wire(cbm_pipeline_ctx_t *ctx, k8s_kusts_t *ks, CBMHashTabl
}
}
for (int i = 0; i < ks->count; i++) {
qsort(ks->items[i].files, (size_t)ks->items[i].file_n, sizeof(*ks->items[i].files),
k8s_cmp_str);
qsort(ks->items[i].kids, (size_t)ks->items[i].kid_n, sizeof(int), k8s_cmp_int);
k8s_qsort(ks->items[i].files, (size_t)ks->items[i].file_n, sizeof(*ks->items[i].files),
k8s_cmp_str);
k8s_qsort(ks->items[i].kids, (size_t)ks->items[i].kid_n, sizeof(int), k8s_cmp_int);
}
}
@@ -1006,7 +1015,7 @@ static void k8s_bundles_build(cbm_pipeline_ctx_t *ctx, k8s_kusts_t *ks, k8s_bund
if (!b->names || !b->members || !b->charts) {
return;
}
qsort(ks->items, (size_t)ks->count, sizeof(*ks->items), k8s_cmp_kust);
k8s_qsort(ks->items, (size_t)ks->count, sizeof(*ks->items), k8s_cmp_kust);
CBMHashTable *by_path = cbm_ht_create(0);
if (!by_path) {
return;
@@ -1345,7 +1354,7 @@ static void k8s_paths_sort_unique(k8s_paths_t *v) {
if (v->count == 0) {
return;
}
qsort((void *)v->items, (size_t)v->count, sizeof(*v->items), k8s_cmp_str);
k8s_qsort((void *)v->items, (size_t)v->count, sizeof(*v->items), k8s_cmp_str);
int w = SKIP_ONE;
for (int i = SKIP_ONE; i < v->count; i++) {
if (strcmp(v->items[w - SKIP_ONE], v->items[i]) != 0) {
@@ -1522,7 +1531,7 @@ static void k8s_deploys_build(cbm_pipeline_ctx_t *ctx, k8s_kusts_t *ks, k8s_bund
k8s_dir_members(ks, by_path, &manifests, f->deploys[d].dir, seen, d + SKIP_ONE,
&members[d], NULL);
}
qsort(keys, un, sizeof(*keys), k8s_cmp_deploy_key);
k8s_qsort(keys, un, sizeof(*keys), k8s_cmp_deploy_key);
/* deploy d reaches deploy e when e sits in a manifest d applies */
for (int d = 0; d < n; d++) {
for (int i = 0; i < members[d].count; i++) {
@@ -1626,7 +1635,7 @@ static void k8s_ids_sort_unique(k8s_ids_t *v) {
if (v->count < PAIR_LEN) {
return;
}
qsort(v->items, (size_t)v->count, sizeof(*v->items), k8s_cmp_id);
k8s_qsort(v->items, (size_t)v->count, sizeof(*v->items), k8s_cmp_id);
int w = SKIP_ONE;
for (int i = SKIP_ONE; i < v->count; i++) {
if (v->items[i] != v->items[w - SKIP_ONE]) {
@@ -1974,7 +1983,7 @@ static void k8s_link_crds(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, const k8s_f
snprintf(order[i].key, sizeof(order[i].key), "%s/%s", f->crds[i].group, f->crds[i].kind);
order[i].crd = i;
}
qsort(order, (size_t)f->crd_n, sizeof(*order), k8s_cmp_crd_key);
k8s_qsort(order, (size_t)f->crd_n, sizeof(*order), k8s_cmp_crd_key);
for (int i = f->crd_n - SKIP_ONE; i >= 0; i--) {
cbm_ht_set(by_type, order[i].key, (void *)(intptr_t)(i + SKIP_ONE)); /* first of run */
}
@@ -2128,7 +2137,7 @@ static int k8s_cmp_match(const void *a, const void *b) {
* there is one, is the one it selects. */
static int k8s_draw_matches(cbm_pipeline_ctx_t *ctx, const k8s_record_array_t *recs,
const k8s_record_t *svc, k8s_match_t *m, int n) {
qsort(m, (size_t)n, sizeof(*m), k8s_cmp_match);
k8s_qsort(m, (size_t)n, sizeof(*m), k8s_cmp_match);
int edges = 0;
for (int g = 0; g < n;) {
int end = g;
+24 -10
View File
@@ -3524,11 +3524,18 @@ TEST(incremental_shell_deploy_edit_keeps_its_edges) {
/* Deleting the only file that starts a program leaves no Process node
* behind -- a full index never makes one without a SPAWNS edge. One script
* (pass_shell.c) and one Python call (pass_spawns.c) go; the program a kept
* script starts stays. `legacy`: through the full-load route, else the
* closure-repair route. Returns the Process count after the deletion, or -1;
* *route: the route taken. */
static int incremental_spawner_delete(bool legacy, cbm_incremental_route_t *route) {
* (pass_shell.c) and one Python call (pass_spawns.c) start programs; the
* program a kept script starts stays.
* - `legacy`: both go at once, through the full-load route.
* - otherwise in two steps: the script alone goes through closure repair,
* then the Python file, whose deletion the Python namespace admission
* declines (a deleted module's names leave its importers' namespaces), so
* it takes the full route.
* Returns the Process count after the last deletion, or -1. *route: the route
* of the first (or only) step; *after_script and *python_route: the count and
* the route after the script-only step and the Python step (two-step mode). */
static int incremental_spawner_delete(bool legacy, cbm_incremental_route_t *route,
int *after_script, cbm_incremental_route_t *python_route) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_spawngc_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
@@ -3545,28 +3552,35 @@ static int incremental_spawner_delete(bool legacy, cbm_incremental_route_t *rout
char path[512];
snprintf(path, sizeof(path), "%s/hack/a.sh", tmp);
(void)remove(path);
if (!legacy) {
*after_script =
k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1;
*route = cbm_pipeline_incremental_test_last_route();
}
snprintf(path, sizeof(path), "%s/app.py", tmp);
(void)remove(path);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (legacy) {
cbm_pipeline_incremental_test_force_legacy_partial_once();
}
#else
(void)legacy;
#endif
int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1;
*route = cbm_pipeline_incremental_test_last_route();
*(legacy ? route : python_route) = cbm_pipeline_incremental_test_last_route();
th_rmtree(tmp);
return full == 3 ? after : -1;
}
TEST(incremental_deleted_spawner_drops_its_process) {
cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE;
int left = incremental_spawner_delete(false, &route);
cbm_incremental_route_t python_route = CBM_INCREMENTAL_ROUTE_NONE;
int after_script = -1;
int left = incremental_spawner_delete(false, &route, &after_script, &python_route);
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR);
ASSERT_EQ(after_script, 2); /* tool-a gone; tool-b and tool-c stay */
ASSERT_EQ(python_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL);
ASSERT_EQ(left, 1);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
left = incremental_spawner_delete(true, &route);
left = incremental_spawner_delete(true, &route, &after_script, &python_route);
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL);
ASSERT_EQ(left, 1);
#endif