fix(scheduler): a batch replay exits 0 under LeakSanitizer (bug-1884, part 6)

- kill_sched_resval() (every resource-value array's item killer) frees
  the value, not only a string's buffer: each replaced or torn-down value
  leaked its struct, including every WORKERS_PUSH value update.
- put_resvals() shares the value it creates with later entries for the
  same index in one call; it created a second one that replaced (and now
  freed) the first while still writing through it.
- JOB_UPDATE and the duplicate-JOBS_PUSH path kill_resreq() the old parsed
  resreq, and free the rejected job's schedobj (xce_core_free_schedobj(),
  exported from core.c), instead of freeing only the containers.
- The previous attempt's (or ALTER alternative's) pend reason is freed when
  a new one replaces it.
- kill_resreq_ctx() frees the parser context's error message, and the
  primary-resreq PEG action frees an earlier primary's message first.
- write_snapshot_entry() frees the snapshot it builds (config and resource
  objects detached, they are the core's); the save side strdup()s its
  strings so the generated free_SnapshotData() applies.

Test: test_cli_batch_replay_exits_clean_under_leak_detection
(test_sim_discrete_clock.c) runs the real binary on a two-session fixture
with detect_leaks=1 and requires exit 0: RED before (exit 1), GREEN now.
All nine HA-soak recordings replay with exit 0 and no LeakSanitizer report.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-29 09:44:43 +00:00
parent f6cab0ab87
commit 90b99d91e9
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
12 changed files with 107 additions and 14 deletions

View file

@ -505,6 +505,9 @@ void kill_resreq_ctx(resreq_context_t* ctx)
if (ctx == NULL) { if (ctx == NULL) {
return; return;
} }
/* bug-1884: a primary's parse error lands here (xce_resreq.peg) */
FREEUP(ctx->errmsg);
} }
int parse_resreq(xce_resreq_t* resreq, char* data, var resmgr, char** errmsg) int parse_resreq(xce_resreq_t* resreq, char* data, var resmgr, char** errmsg)

View file

@ -106,6 +106,8 @@ primary_resreq <- < (! ('{' / '}' ) .)* > {
xce_primary_resreq_t *primary = (xce_primary_resreq_t *)calloc(1, sizeof(*primary)); xce_primary_resreq_t *primary = (xce_primary_resreq_t *)calloc(1, sizeof(*primary));
if (primary != NULL) { if (primary != NULL) {
auxil->primary = primary; auxil->primary = primary;
free(auxil->errmsg); /* bug-1884: an earlier primary's message */
auxil->errmsg = NULL;
parse_resreq_primary(primary, (char *)$0, auxil->resmgr, &(auxil->errmsg)); parse_resreq_primary(primary, (char *)$0, auxil->resmgr, &(auxil->errmsg));
} }
} }

View file

@ -450,6 +450,12 @@ extern void xce_core_build_schedobj(xce_sched_obj_t*, const char* user,
const char* queue, const char* project, const char* queue, const char* project,
xce_resreq_t*); xce_resreq_t*);
/* xce_core_free_schedobj - bug-1884: frees a schedobj built by
* xce_core_build_schedobj() (names, limit lists, per-primary wrappers and
* their pend reasons, last allocation and reason). Its resreqs belong to
* the job and are not freed. NULL-safe. */
extern void xce_core_free_schedobj(xce_sched_obj_t* obj);
/* /*
* sched_check_host_satisfy()/sched_check_shared_satisfy() - relocated here * sched_check_host_satisfy()/sched_check_shared_satisfy() - relocated here
* from scheduler.h (scheduler-driver-equivalence fix, bug-1836), following * from scheduler.h (scheduler-driver-equivalence fix, bug-1836), following

View file

@ -209,7 +209,7 @@ int xce_sched_core_init(xce_sched_core_t* core)
* used only by xce_sched_core_fini() after the table (and its shallow * used only by xce_sched_core_fini() after the table (and its shallow
* datakiller) is gone -- see core_destroy_and_free_(). Here rather than in * datakiller) is gone -- see core_destroy_and_free_(). Here rather than in
* schedobj.c so the core's teardown links on its own. */ * schedobj.c so the core's teardown links on its own. */
static void core_schedobj_free_(xce_sched_obj_t* obj) void xce_core_free_schedobj(xce_sched_obj_t* obj)
{ {
int i; int i;
@ -268,7 +268,7 @@ static void core_schedobj_free_(xce_sched_obj_t* obj)
static void core_job_free_(xce_sched_job_t* job) static void core_job_free_(xce_sched_job_t* job)
{ {
free_JobRusages_ptr(&(job->rusages)); free_JobRusages_ptr(&(job->rusages));
core_schedobj_free_(job->schedobj); xce_core_free_schedobj(job->schedobj);
if (job->resreq != NULL) { if (job->resreq != NULL) {
kill_resreq(job->resreq); kill_resreq(job->resreq);
free(job->resreq); free(job->resreq);

View file

@ -7,6 +7,7 @@
#include "core.h" #include "core.h"
#include "misc.h" #include "misc.h"
#include "resobj.h" #include "resobj.h"
#include "resreq.h"
#include "sched/types.h" #include "sched/types.h"
#include "utils.h" #include "utils.h"
#include "xcealloc.h" #include "xcealloc.h"
@ -334,6 +335,7 @@ static xce_sched_job_t* core_build_job(xce_sched_core_t* core, xce_job_t* j)
job->schedobj = (xce_sched_obj_t*)calloc(1, sizeof(*(job->schedobj))); job->schedobj = (xce_sched_obj_t*)calloc(1, sizeof(*(job->schedobj)));
if (job->schedobj == NULL) { if (job->schedobj == NULL) {
kill_resreq(job->resreq);
FREEUP(job->resreq); FREEUP(job->resreq);
FREEUP(job); FREEUP(job);
return NULL; return NULL;
@ -1047,8 +1049,11 @@ int xce_core_push_events(xce_sched_core_t* core, xce_core_event_t* events,
* FREEUP(job) below only frees the job shell itself, not * FREEUP(job) below only frees the job shell itself, not
* these two fields, matching this function's existing * these two fields, matching this function's existing
* "job->allocs/accounts are out of scope" convention. */ * "job->allocs/accounts are out of scope" convention. */
/* bug-1884: free what the job owns, not only the
* containers */
xce_core_free_schedobj(job->schedobj);
kill_resreq(job->resreq);
FREEUP(job->resreq); FREEUP(job->resreq);
FREEUP(job->schedobj);
FREEUP(job); FREEUP(job);
continue; continue;
} }
@ -1374,6 +1379,8 @@ int xce_core_push_events(xce_sched_core_t* core, xce_core_event_t* events,
core_rebind_schedobj_resreq(job->schedobj, newreq); core_rebind_schedobj_resreq(job->schedobj, newreq);
} }
/* bug-1884: the schedobj now points at newreq */
kill_resreq(job->resreq);
FREEUP(job->resreq); FREEUP(job->resreq);
job->resreq = newreq; job->resreq = newreq;
} }

View file

@ -354,6 +354,27 @@ static int write_replay_header(FILE* f, xce_scheduler_t* sched,
* save_snapshot() result unfreed for the same reason, detect_leaks=0 * save_snapshot() result unfreed for the same reason, detect_leaks=0
* across this whole suite) -- writing one is out of this task's boundary. * across this whole suite) -- writing one is out of this task's boundary.
*/ */
/* sched_snapshot_data_free - bug-1884: frees what
* xce_sched_core_save_snapshot() allocated in `snap` (not `snap` itself).
* The config and the resource objects are the core's (borrowed, see
* snapshot.c's field notes), so they are detached first. */
static void sched_snapshot_data_free(sched_snapshot_data_t* snap)
{
int i;
if (snap == NULL) {
return;
}
snap->config = NULL;
if (snap->resources != NULL) {
for (i = 0; i < snap->resources->count; i++) {
snap->resources->resources[i] = NULL;
}
}
free_SnapshotData(snap);
}
int write_snapshot_entry(FILE* f, xce_sched_core_t* core, uint64_t ts) int write_snapshot_entry(FILE* f, xce_sched_core_t* core, uint64_t ts)
{ {
struct capn c; struct capn c;
@ -382,6 +403,8 @@ int write_snapshot_entry(FILE* f, xce_sched_core_t* core, uint64_t ts)
rc = write_capnp_message(f, &c); rc = write_capnp_message(f, &c);
capn_free(&c); capn_free(&c);
sched_snapshot_data_free(&snap);
return rc; return rc;
} }

View file

@ -98,6 +98,19 @@ int put_resvals(zarray_t* array, int nres, int* idxs, float* vals)
val->u.fvalue = 0.0; val->u.fvalue = 0.0;
zarray_put(array, idxs[i], val); zarray_put(array, idxs[i], val);
xvals[i] = val; xvals[i] = val;
/* bug-1884: a later entry for the same index shares this value;
* creating a second one replaced (and, with the array's killer,
* freed) this one while xvals[i] still pointed at it */
{
int k;
for (k = i + 1; k < nres; k++) {
if ((idxs[k] == idxs[i]) && (xvals[k] == NULL)) {
xvals[k] = val;
}
}
}
} }
} }

View file

@ -276,13 +276,18 @@ void kill_sched_worker(void* arg)
sched_worker_release_host_ctx((xce_sched_worker_t*)arg); sched_worker_release_host_ctx((xce_sched_worker_t*)arg);
} }
/* kill_sched_resval - item killer of every resource-value array. bug-1884:
* frees the value itself too -- free_Value() frees only a string value's
* buffer, so every replaced or torn-down value leaked its struct. All
* values stored in these arrays are separately allocated (the wire
* decoder, dup_val(), put_resvals()). */
void kill_sched_resval(xce_val_t* val) void kill_sched_resval(xce_val_t* val)
{ {
if (val == NULL) { if (val == NULL) {
return; return;
} }
free_Value(val); free_Value_ptr(&val);
} }
xce_sched_worker_t* make_sched_worker(void* arg, xce_worker_t* w) xce_sched_worker_t* make_sched_worker(void* arg, xce_worker_t* w)

View file

@ -322,6 +322,9 @@ static int sched_make_primary_allocation(xce_sched_core_t* core,
} }
if (ctx.reason != NULL) { if (ctx.reason != NULL) {
/* bug-1884: the previous attempt's reason (its decision has
* been published) or an earlier alternative's */
free_PendReason_ptr(&(obj->reason));
MOVE_PTR(obj->reason, ctx.reason); MOVE_PTR(obj->reason, ctx.reason);
} }
@ -386,6 +389,9 @@ static int sched_make_compound_alloc_(xce_sched_core_t* core,
} }
if (ctx.reason != NULL) { if (ctx.reason != NULL) {
/* bug-1884: the previous attempt's reason (its decision has
* been published) or an earlier alternative's */
free_PendReason_ptr(&(obj->reason));
MOVE_PTR(obj->reason, ctx.reason); MOVE_PTR(obj->reason, ctx.reason);
} }

View file

@ -446,7 +446,7 @@ int xce_sched_core_save_snapshot(xce_sched_core_t* core,
if (rv == NULL) { if (rv == NULL) {
continue; continue;
} }
rv->name = makeString((char*)rname); rv->name = strdup(rname);
rv->host = NULL; rv->host = NULL;
rv->val = dup_val(val); rv->val = dup_val(val);
@ -548,7 +548,7 @@ int xce_sched_core_save_snapshot(xce_sched_core_t* core,
if (su == NULL) { if (su == NULL) {
continue; continue;
} }
su->name = (u->name != NULL) ? makeString(u->name) : NULL; su->name = (u->name != NULL) ? strdup(u->name) : NULL;
if (arr_append((void***)&users_arr, &users_cap, &users_n, su) < 0) { if (arr_append((void***)&users_arr, &users_cap, &users_n, su) < 0) {
FREEUP(su->name); FREEUP(su->name);
@ -575,7 +575,7 @@ int xce_sched_core_save_snapshot(xce_sched_core_t* core,
if (sp == NULL) { if (sp == NULL) {
continue; continue;
} }
sp->name = (p->name != NULL) ? makeString(p->name) : NULL; sp->name = (p->name != NULL) ? strdup(p->name) : NULL;
sp->priority = p->priority; sp->priority = p->priority;
if (arr_append((void***)&proj_arr, &proj_cap, &proj_n, sp) < 0) { if (arr_append((void***)&proj_arr, &proj_cap, &proj_n, sp) < 0) {

View file

@ -148,10 +148,10 @@ xce_job_t* build_wire_job(xce_sched_job_t* job)
} }
j->submit->queue j->submit->queue
= (job->queue != NULL) ? makeString(job->queue->name) : NULL; = (job->queue != NULL) ? strdup(job->queue->name) : NULL;
j->submit->user = (job->user != NULL) ? makeString(job->user->name) : NULL; j->submit->user = (job->user != NULL) ? strdup(job->user->name) : NULL;
j->submit->project_name j->submit->project_name
= (job->project != NULL) ? makeString(job->project->name) : NULL; = (job->project != NULL) ? strdup(job->project->name) : NULL;
j->submit->mintasks = job->mintasks; j->submit->mintasks = job->mintasks;
j->submit->maxtasks = job->maxtasks; j->submit->maxtasks = job->maxtasks;
@ -206,13 +206,13 @@ xce_queue_t* build_wire_queue(xce_sched_queue_t* q)
return NULL; return NULL;
} }
wq->queue = (q->name != NULL) ? makeString(q->name) : NULL; wq->queue = (q->name != NULL) ? strdup(q->name) : NULL;
wq->attrs = q->attrs; wq->attrs = q->attrs;
wq->status = q->status; wq->status = q->status;
wq->priority = q->priority; wq->priority = q->priority;
wq->accept_interval = q->accept_interval; wq->accept_interval = q->accept_interval;
wq->sched_delay = q->sched_delay; wq->sched_delay = q->sched_delay;
wq->resreq = (q->resreq != NULL) ? makeString(q->resreq) : NULL; wq->resreq = (q->resreq != NULL) ? strdup(q->resreq) : NULL;
/* project_name/job_pick_plugin: no wire slot -- see snapshot.c header /* project_name/job_pick_plugin: no wire slot -- see snapshot.c header
* "Scope decisions". */ * "Scope decisions". */
@ -238,7 +238,7 @@ xce_worker_t* build_wire_worker(xce_sched_worker_t* w,
return NULL; return NULL;
} }
ww->name = (w->name != NULL) ? makeString(w->name) : NULL; ww->name = (w->name != NULL) ? strdup(w->name) : NULL;
ww->status = w->status; ww->status = w->status;
for (i = 0; i < w->resvals.count; i++) { for (i = 0; i < w->resvals.count; i++) {
@ -275,7 +275,7 @@ xce_worker_t* build_wire_worker(xce_sched_worker_t* w,
continue; continue;
} }
nv->name = makeString((char*)rname); nv->name = strdup(rname);
nv->val = dup_val(val); nv->val = dup_val(val);
if (arr_append((void***)&nvs, &cap, &n, nv) < 0) { if (arr_append((void***)&nvs, &cap, &n, nv) < 0) {

View file

@ -1245,6 +1245,33 @@ static void test_cli_help_describes_sim_speed_as_deprecated(void)
free(text); free(text);
} }
/* bug-1884: a batch replay frees what the simulator owns before exit, so an
* ASAN build with LeakSanitizer enabled exits 0. The test binary itself runs
* with detect_leaks=0 (meson.build), so the child re-enables it. Without
* ASAN the option is ignored and the test checks only the exit status. */
static void test_cli_batch_replay_exits_clean_under_leak_detection(void)
{
char fixture[PATH_MAX];
char out[PATH_MAX];
char log[PATH_MAX];
char cmd[4 * PATH_MAX];
write_two_session_fixture(fixture, sizeof(fixture));
make_tmp_file(out, PATH_MAX, "leakout");
make_tmp_file(log, PATH_MAX, "leaklog");
snprintf(cmd, sizeof(cmd),
"ASAN_OPTIONS=detect_leaks=1 " XCE_SCHEDULER_BIN
" --sim %s --sim-verify --sim-output %s --logfile %s "
"> /dev/null 2>&1",
fixture, out, log);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, run_child(cmd),
"a clean batch replay must exit 0 with "
"leak detection enabled");
unlink(out);
unlink(log);
unlink(fixture);
}
int main(void) int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
@ -1265,5 +1292,6 @@ int main(void)
RUN_TEST(test_cli_trailing_sim_speed_without_value_fails); RUN_TEST(test_cli_trailing_sim_speed_without_value_fails);
RUN_TEST(test_cli_interactive_sim_speed_logs_one_warning); RUN_TEST(test_cli_interactive_sim_speed_logs_one_warning);
RUN_TEST(test_cli_help_describes_sim_speed_as_deprecated); RUN_TEST(test_cli_help_describes_sim_speed_as_deprecated);
RUN_TEST(test_cli_batch_replay_exits_clean_under_leak_detection);
return UNITY_END(); return UNITY_END();
} }