fix(scheduler): ALTER allocation type confusion (bug-1769); NULL queue in legacy fcfs hooks (bug-1771)

bug-1769: every builder stores xce_compound_schedobj_t* (24 bytes) in
obj->u.alters, but sched_make_alter_allocation() read them as
xce_sched_obj_t* (176 bytes) and ran the compound allocation on them -- a
heap-buffer-overflow read of ->u.compound and write of ->alloc for any real
`{..} || {..}` resreq. The compound allocation now takes its resreq
explicitly (sched_make_compound_alloc_), and each ALTER alternative is
tried on behalf of the ALTER obj itself; a failed alternative's partial
allocation is dropped before the next (rollback itself is bug-1766).
sched_ctx_init() rejected ALTER objs ("bad sched obj type"), so ALTER jobs
could never allocate at all -- it now accepts them like COMPOUND (all
downstream checks only distinguish PRIMARY).

bug-1771: plugin_commit_decision() fans every DISPATCH out to the legacy
"xce_sched_fcfs" plugin even when it never tracked the job; fcfs_del_job()
(and fcfs's job-add hook) dereferenced job->queue->name with no NULL
check, SEGV'ing the allocator for a job whose queue was never registered.

Tests (test_parjob_e2e_multislot.c, real ingest + live allocator thread):
test_alter_resreq_job_dispatches_via_real_allocator (ASAN
heap-buffer-overflow before, DISPATCH after) and
test_dispatch_of_job_with_unregistered_queue_does_not_crash (SEGV at
fcfs.c:318 before). Scheduler suite 87/87.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-25 12:59:31 +00:00
parent 4ba0f32766
commit 1ecd348f03
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 162 additions and 15 deletions

View file

@ -585,10 +585,128 @@ test_multihost_parallel_job_allocation_spans_two_hosts_via_real_dispatch(void)
"maxTasks");
}
/*
* bug-1771: plugin_commit_decision() fans every DISPATCH out to every
* registered plugin's commit_hook, including the legacy "xce_sched_fcfs"
* plugin that never tracked the job. Its fcfs_del_job() dereferenced
* job->queue->name unconditionally, so a job whose queue was never
* registered (job->queue == NULL) SEGV'd the allocator thread. Same real
* dispatch chain as above, minus the QUEUE_UPDATE.
*/
static void test_dispatch_of_job_with_unregistered_queue_does_not_crash(void)
{
xce_scheduler_t* sched = start_sched();
xce_worker_t* worker_ptrs[2];
xce_core_event_t worker_event;
xce_job_t* job_ptrs[1];
xce_core_event_t job_event;
xce_sched_job_t* sjob;
xce_decision_t* decision;
worker_ptrs[0] = make_wire_worker(WORKER_A_NAME, WORKER_SLOTS);
worker_ptrs[1] = make_wire_worker(WORKER_B_NAME, WORKER_SLOTS);
memset(&worker_event, 0, sizeof(worker_event));
worker_event.type = XCE_CORE_EVENT_WORKERS_PUSH;
worker_event.u.workers_push.nworkers = 2;
worker_event.u.workers_push.workers = worker_ptrs;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &worker_event, 1)
>= 0);
job_ptrs[0] = make_push_job(JOB_ID + 1, RESREQ_STR, "e2e_user",
"never_registered_queue", "e2e_proj", 1,
JOB_MAXTASKS);
memset(&job_event, 0, sizeof(job_event));
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
job_event.u.jobs_push.njobs = 1;
job_event.u.jobs_push.jobs = job_ptrs;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &job_event, 1)
>= 0);
sjob = sched_get_job(&(sched->core), JOB_ID + 1);
TEST_ASSERT_NOT_NULL(sjob);
TEST_ASSERT_NULL_MESSAGE(sjob->queue,
"fixture: the job's queue must be unregistered (job->queue NULL)");
TEST_ASSERT_TRUE(xce_core_begin_session(&(sched->core)) >= 0);
decision = dispatch_and_wait(sched, JOB_ID + 1);
TEST_ASSERT_NOT_NULL_MESSAGE(decision,
"the allocator must still produce a decision (it used to SEGV in "
"the legacy fcfs plugin's commit hook)");
TEST_ASSERT_EQUAL_INT(XCE_DECISION_DISPATCH, decision->op);
}
/*
* bug-1769: core_build_alter_schedobj() stores xce_compound_schedobj_t*
* (24 bytes) in obj->u.alters, but sched_make_alter_allocation() read them
* back as xce_sched_obj_t* (176 bytes) and ran the compound allocation on
* that -- a heap-buffer-overflow read of ->u.compound and write of ->alloc.
* No test drove a real ALTER job before: a real `{..} || {..}` resreq,
* ingested and dispatched through the live allocator thread.
*/
static void test_alter_resreq_job_dispatches_via_real_allocator(void)
{
xce_scheduler_t* sched = start_sched();
xce_queue_t* queue;
xce_core_event_t queue_event;
xce_worker_t* worker_ptrs[2];
xce_core_event_t worker_event;
xce_job_t* job_ptrs[1];
xce_core_event_t job_event;
xce_sched_job_t* sjob;
xce_decision_t* decision;
const uint64_t jobid = JOB_ID + 2;
queue = make_wire_queue("alter_queue", 1);
memset(&queue_event, 0, sizeof(queue_event));
queue_event.type = XCE_CORE_EVENT_QUEUE_UPDATE;
queue_event.u.queue_update.queue = queue;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &queue_event, 1)
>= 0);
worker_ptrs[0] = make_wire_worker(WORKER_A_NAME, WORKER_SLOTS);
worker_ptrs[1] = make_wire_worker(WORKER_B_NAME, WORKER_SLOTS);
memset(&worker_event, 0, sizeof(worker_event));
worker_event.type = XCE_CORE_EVENT_WORKERS_PUSH;
worker_event.u.workers_push.nworkers = 2;
worker_event.u.workers_push.workers = worker_ptrs;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &worker_event, 1)
>= 0);
job_ptrs[0] = make_push_job(
jobid, (char*)"{1*{rusage[mem=1]}} || {1*{rusage[mem=2]}}",
"e2e_user", "alter_queue", "e2e_proj", 1, 1);
memset(&job_event, 0, sizeof(job_event));
job_event.type = XCE_CORE_EVENT_JOBS_PUSH;
job_event.u.jobs_push.njobs = 1;
job_event.u.jobs_push.jobs = job_ptrs;
TEST_ASSERT_TRUE(xce_core_push_events(&(sched->core), &job_event, 1)
>= 0);
sjob = sched_get_job(&(sched->core), jobid);
TEST_ASSERT_NOT_NULL(sjob);
TEST_ASSERT_NOT_NULL(sjob->schedobj);
TEST_ASSERT_EQUAL_INT_MESSAGE(XCE_RESREQ_ALTER, sjob->schedobj->tag,
"fixture: the resreq must parse as ALTER");
TEST_ASSERT_EQUAL_INT(2, sjob->schedobj->u.alters.count);
TEST_ASSERT_TRUE(xce_core_begin_session(&(sched->core)) >= 0);
decision = dispatch_and_wait(sched, jobid);
TEST_ASSERT_NOT_NULL_MESSAGE(decision,
"the allocator must produce a decision for an ALTER job");
TEST_ASSERT_EQUAL_INT_MESSAGE(XCE_DECISION_DISPATCH, decision->op,
"the first alternative fits on either 2-slot worker");
TEST_ASSERT_NOT_NULL(decision->u.alloc);
TEST_ASSERT_EQUAL_UINT64(jobid, decision->u.alloc->jobid);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(
test_multihost_parallel_job_allocation_spans_two_hosts_via_real_dispatch);
RUN_TEST(test_dispatch_of_job_with_unregistered_queue_does_not_crash);
RUN_TEST(test_alter_resreq_job_dispatches_via_real_allocator);
return UNITY_END();
}