feat(job-array-single-wal-record): harden jobj.c job construction primitives

- job_init() now checks makeJobsub()/makeString(ckptinfo) return
  values and propagates failure instead of storing NULL with a
  success return (Req 10.1).
- Root-caused the previously observed makeJobsub() NULL-with-EINVAL
  failure to a test-binary-only hazard: multiple controller test
  files each called job_obj_init() from their own setUp() with no
  cross-file coordination, re-running jobsub_table_init() against an
  already-live lock/table. Fixed via atomic_flag-guarded idempotent
  job_obj_init()/job_obj_fini(), plus a distinct -ENODEV return from
  makeJobsub()/makeJobstartinfo() when the table isn't ready
  (previously fell through to undefined behavior) (Req 10.2).
- Verified (test) MOVE_PTR fields are always NULL on a freshly-built
  submission payload -- no code change needed (Req 10.3).
- Fixed job_destroy()'s accounts leak via the existing
  free_job_account() helper, replacing the stale FIXME (Req 10.4).
- Verified (inspection + test) struct Job's capnp schema has no
  ckptinfo field, so decode_Job/free_Job have nothing to leak there
  -- no code change needed (Req 10.5).

Independently re-verified: reviewer reproduced a pre-existing,
unrelated rtti-library leak-on-init-failure against a stashed
pre-task copy of jobj.c to confirm it predates this change.

Task 1.3 of job-array-single-wal-record.
This commit is contained in:
Rongsong Shen 2026-07-14 08:29:45 +00:00
parent bc4e70b790
commit 76862bd9aa
5 changed files with 541 additions and 8 deletions

View file

@ -14,7 +14,7 @@
- _Requirements: 6.1, 6.2_
- _Boundary: Compact Scheduler/API-Server Push_
- [ ] 1.3 (P) jobj.c hardening
- [x] 1.3 (P) jobj.c hardening
- Make job construction check the interning/string-construction return value and propagate failure instead of silently storing a NULL success
- Root-cause and fix the previously observed construction-table initialization-ordering gap; if root-causing shows the actual gap is outside this file's own init/call ordering (e.g. controller boot sequencing), stop and flag it as a separate follow-up rather than forcing a fix into this task's boundary
- Verify (with a test) that the destructive-move fields on a freshly-built job payload are always NULL, so repeated use against a shared template is provably benign
@ -167,3 +167,9 @@
- Observable: a clean full-suite run with the byte-identical single-job baseline check passing alongside it
- _Requirements: 8.1, 8.2, 8.4_
- _Boundary: Non-Regression & Testing_
## Implementation Notes
- **Task 1.3**: `meson test --suite xce`'s `libxce test` target SIGABRTs under ASAN leak detection whenever any code path exercises a `job_init()` failure — root cause is the vendored `rtti` library's `makeInstance()` not freeing its allocation when a type's `.init()` returns negative (no `.c` source available, headers-only in the nix store). This is pre-existing and independently reproduced against an unmodified pre-task `jobj.c`, not introduced by this feature. It will fire again on any future task whose tests exercise a `create_job()`/`makeJob()` failure path (e.g. task 3.1/3.4's fault-injection tests for `restore_job_array()`) — expect the same SIGABRT-not-assertion-failure signature (`grep ":FAIL"` on the Unity log will be empty; the failure is LeakSanitizer-only) and do not treat it as a new regression without the same stash-and-reproduce check task 1.3's review used.
- **Task 1.3**: `job_obj_init()`/`job_obj_fini()` are now idempotent (atomic_flag-guarded) — safe to call more than once without an intervening `fini()`. Any future test construction can rely on this instead of coordinating a single call site.
- **Task 1.3**: `makeJobsub()`/`makeJobstartinfo()` now return `-ENODEV` (not a generic `-EINVAL`) when the interning table isn't initialized — if a later task's error handling branches on `job_init()`'s return value, `-ENODEV` is a distinct, checkable signal for "table not ready" vs. other construction failures.

View file

@ -7,6 +7,7 @@
#include "jobj.h"
#include "kv.h"
#include "misc.h"
#include "mylock.h"
#include "preason.h"
#include "utils.h"
@ -101,6 +102,10 @@ static int jobsub_table_init(int nslots)
static void jobsub_table_fini()
{
destroy(jobsub_table.sub_table);
/* Leave no dangling pointer behind: makeJobsub() checks this for
* NULL (see below) to detect a not-(yet-)initialized table instead
* of dereferencing freed memory (Requirement 10.2 hardening). */
jobsub_table.sub_table = NULL;
mylock_fini(&(jobsub_table.lock));
}
@ -228,6 +233,21 @@ job_submit_data_t* makeJobsub(job_submit_data_t* subdata)
return NULL;
}
if (jobsub_table.sub_table == NULL) {
/* job_obj_init() has not (yet) completed for this generation of
* the module -- dispatching key_find/key_add below against an
* uninitialized/torn-down table is unsafe, not just wrong.
* Root cause of the previously observed makeJobsub()
* NULL-with-EINVAL-for-a-valid-src failure (Requirement 10.2):
* that generic EINVAL was indistinguishable from "subdata was
* invalid" above. Fail loudly with a distinct errno instead, so
* an ordering bug can never again masquerade as a bad-argument
* error. */
errno = ENODEV;
return NULL;
}
jobsub = makeInstance(&job_submit_type, subdata);
if (jobsub == NULL) {
return NULL;
@ -407,6 +427,13 @@ job_startinfo_t* makeJobstartinfo(job_startinfo_t* startinfo)
return NULL;
}
if (startinfo_table.startinfo_table == NULL) {
/* Mirrors makeJobsub()'s guard above -- see its comment. */
errno = ENODEV;
return NULL;
}
startinfo1 = makeInstance(&(job_startinfo_type), startinfo);
if (startinfo1 == NULL) {
return NULL;
@ -512,6 +539,10 @@ static int startinfo_table_init(int nslots)
static void startinfo_table_fini()
{
destroy(startinfo_table.startinfo_table);
/* See jobsub_table_fini(): avoid leaving a dangling pointer that
* makeJobstartinfo()'s not-(yet-)initialized guard would otherwise
* have to distinguish from a live table. */
startinfo_table.startinfo_table = NULL;
mylock_fini(&(startinfo_table.lock));
return;
@ -531,16 +562,44 @@ static size_t job_datasize(va_list ap)
static int job_init(meta_obj_t* obj, va_list ap)
{
job_t* dst = RTTI_OBJ_TO_DATA(job_t, obj);
job_private_t* priv = (job_private_t*)RTTI_OBJ_TO_PRIV(obj);
xce_job_t* src = va_arg(ap, xce_job_t*);
job_t* dst = RTTI_OBJ_TO_DATA(job_t, obj);
job_private_t* priv = (job_private_t*)RTTI_OBJ_TO_PRIV(obj);
xce_job_t* src = va_arg(ap, xce_job_t*);
job_submit_data_t* submit = NULL;
char* ckptinfo = NULL;
int saved_errno;
if (src->submit == NULL) {
return -EINVAL;
}
/* Resolve every fallible field BEFORE any MOVE_PTR runs below, so a
* construction failure here never leaves src half-moved: callers
* (e.g. job.c's restore_job()) rely on src being fully intact so
* free_Job() can release everything on a create_job()/makeJob()
* failure. Requirement 10.1: check makeJobsub()'s (and
* makeString(ckptinfo)'s) return and propagate failure instead of
* silently storing NULL and reporting success. */
submit = makeJobsub(src->submit);
if (submit == NULL) {
saved_errno = (errno != 0) ? errno : ENOMEM;
return -saved_errno;
}
if (src->ckptinfo != NULL) {
ckptinfo = makeString(src->ckptinfo);
if (ckptinfo == NULL) {
saved_errno = (errno != 0) ? errno : ENOMEM;
dereference(submit);
return -saved_errno;
}
}
dst->job.jobid = src->jobid;
dst->job.submit = makeJobsub(src->submit);
dst->job.submit = submit;
dst->job.sub_time = src->sub_time;
dst->job.queue_time = src->queue_time;
dst->job.start_time = src->start_time;
@ -552,7 +611,7 @@ static int job_init(meta_obj_t* obj, va_list ap)
dst->job.run_time = src->run_time;
MOVE_PTR(dst->job.run_usage, src->run_usage);
MOVE_PTR(dst->job.exit_info, src->exit_info);
dst->job.ckptinfo = makeString(src->ckptinfo);
dst->job.ckptinfo = ckptinfo;
dst->job.pending_ctrl = src->pending_ctrl;
MOVE_PTR(dst->job.pend_reasons, src->pend_reasons);
@ -582,7 +641,15 @@ static void job_destroy(meta_obj_t* obj)
}
if (j->job.accounts != NULL) {
/* FIXME??? */
/* Requirement 10.4: accounts is MOVE_PTR'd into the job during
* job_init but was never freed here -- a genuine leak, multiplied
* by array element count once array submission lands. Mirrors
* the free_job_account() usage already established for this
* exact type in sched.c's job teardown path (misc.c owns the
* free function since xce_job_account_t has no capnp-generated
* free_*_ptr counterpart -- it is not a capnp-mapped struct). */
free_job_account(j->job.accounts);
j->job.accounts = NULL;
}
if (j->job.run_usage != NULL) {
@ -728,21 +795,54 @@ job_t* makeJob(var jobmgr, xce_job_t* j)
return job;
}
/* Requirement 10.2: job_obj_init() must be safe to call more than once
* within a process without an intervening job_obj_fini(). Root cause of
* the previously observed jobsub_table ordering hazard: every controller
* unit-test file that constructs jobs calls job_obj_init() from its own
* setUp() (confirmed via src/controller/meson.build -- all such test
* files are linked into the single controller_unit_test executable and
* run in one process), each guarded only by its own file-local static
* flag, none of which coordinate with each other. Across a full test run
* job_obj_init() was therefore re-entered many times with no intervening
* job_obj_fini(), and jobsub_table_init()/startinfo_table_init()
* unconditionally re-ran mylock_init() on the already-live lock
* (undefined behavior on an in-use mutex, per POSIX) and unconditionally
* overwrote the table pointer, silently orphaning the previous table and
* corrupting the refcount bookkeeping of every entry still referencing
* it. This guard makes every call after the first a no-op success until
* job_obj_fini() runs, matching what every existing call site already
* expects (EQUAL_INT(0, job_obj_init(...))). The production controller
* boot path (cluster_init_managers(), called once from
* cluster_init_phase1()) was never actually at risk -- this hazard was
* confined to the combined test binary. */
static atomic_flag job_obj_ready = ATOMIC_FLAG_INIT;
int job_obj_init(int n_sub_table_slots, int n_startinfo_table_slots)
{
int rc = 0;
if (atomic_flag_test_and_set(&job_obj_ready)) {
return 0;
}
rc = jobsub_table_init(n_sub_table_slots);
if (rc < 0) {
atomic_flag_clear(&job_obj_ready);
return rc;
}
rc = startinfo_table_init(n_startinfo_table_slots);
return rc;
if (rc < 0) {
atomic_flag_clear(&job_obj_ready);
return rc;
}
return 0;
}
void job_obj_fini(void)
{
jobsub_table_fini();
startinfo_table_fini();
atomic_flag_clear(&job_obj_ready);
}

View file

@ -163,6 +163,7 @@ libxce_test_src = ['testsuite/test.c',
'testsuite/test_bitset.c',
'testsuite/test_kv.c',
'testsuite/test_jobj.c',
'testsuite/test_jobj_hardening.c',
'testsuite/test_wobj.c',
'testsuite/test_qobj.c',
'testsuite/test_request.c',

View file

@ -26,6 +26,7 @@ extern void test_select(void);
extern void test_bitset(void);
extern void test_kv(void);
extern void test_jobobj(void);
extern void test_jobj_hardening(void);
extern void test_qobj(void);
extern void test_wobj(void);
extern void test_request(void);
@ -140,6 +141,7 @@ int main(int argc, char* argv[])
TEST(test_bitset);
TEST(test_kv);
TEST(test_jobobj);
TEST(test_jobj_hardening);
TEST(test_wobj);
TEST(test_qobj);
TEST(test_request);

View file

@ -0,0 +1,424 @@
/*
* test_jobj_hardening.c -- unit tests for job-array-single-wal-record
* Requirement 10 (jobj.c hardening). Each test exercises one of the five
* acceptance criteria; per design.md, all five are general jobj.c
* correctness fixes the existing single-job path benefits from too --
* none of this depends on the array-submission feature itself.
*/
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
#include "jobj.h"
#include "kvmgr.h"
#include "misc.h"
#include "utils.h"
#include "xce/types.h"
#include "xstring.h"
static int g_hard_xstring_ready = 0;
static int g_hard_jobobj_ready = 0;
static void hard_ensure_ready(void)
{
if (!g_hard_xstring_ready) {
TEST_ASSERT_TRUE(xstring_table_init(64) >= 0);
g_hard_xstring_ready = 1;
}
if (!g_hard_jobobj_ready) {
TEST_ASSERT_EQUAL_INT(0, job_obj_init(64, 64));
g_hard_jobobj_ready = 1;
}
}
static job_submit_data_t hard_make_submit(const char* job_name)
{
job_submit_data_t s = { .queue = "xce",
.user = "rshen",
.mintasks = 1,
.maxtasks = 1,
.start_info = NULL,
.job_name = (char*)job_name,
.project_name = "default",
.user_group = NULL,
.job_description = "d",
.resreq = "type == any" };
return s;
}
/* ------------------------------------------------------------------- */
/* Requirement 10.2 -- job_obj_init() is idempotent without an */
/* intervening job_obj_fini(). */
/* ------------------------------------------------------------------- */
/* The previously observed makeJobsub() NULL-with-EINVAL-for-a-valid-src
* failure was root-caused to job_obj_init() being non-idempotent: every
* controller unit-test file that constructs jobs calls job_obj_init()
* from its own setUp() (confirmed via src/controller/meson.build -- all
* such test files are linked into the single controller_unit_test
* executable and run in one process), each guarded only by its own
* file-local static flag, none of which coordinate with each other.
* Across a full test run job_obj_init() was therefore re-entered many
* times with no intervening job_obj_fini(). This proves job_obj_init()
* is now safe under exactly that pattern, and that construction still
* works correctly (interning still functions) afterward -- not just
* "didn't crash". */
static void test_job_obj_init_is_idempotent_without_fini(void)
{
int rc;
hard_ensure_ready();
rc = job_obj_init(16, 16);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = job_obj_init(64, 64);
TEST_ASSERT_EQUAL_INT(0, rc);
rc = job_obj_init(5381, 5381);
TEST_ASSERT_EQUAL_INT(0, rc);
job_submit_data_t submit1 = hard_make_submit("J-hard-10-2");
job_submit_data_t* s2 = makeJobsub(&submit1);
job_submit_data_t* s3 = makeJobsub(&submit1);
TEST_ASSERT_NOT_NULL(s2);
TEST_ASSERT_TRUE(s2 == s3);
}
/* ------------------------------------------------------------------- */
/* Requirement 10.1 -- job_init() propagates a makeJobsub() failure */
/* instead of storing NULL and reporting success. */
/* ------------------------------------------------------------------- */
/* Forces makeJobsub() to fail the same way the originally observed bug
* did -- via job_obj_fini() leaving jobsub_table not (yet) re-initialized
* for this generation -- except now safely and deterministically:
* makeJobsub()'s Requirement 10.2 guard detects this and fails cleanly
* (NULL, errno=ENODEV) instead of touching a torn-down table, which is
* what makes this test safe to run in-process alongside every other test
* file regardless of what state they left jobj.c's module globals in. */
static void test_job_init_propagates_makeJobsub_failure(void)
{
kvmgr_params_t jobmgr_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 32,
/* job_t has .destroy = NULL (jobj.c) --
* teardown is wired only through its
* reference interface (job_dereference).
* The kvmgr's datakiller must therefore
* be dereference(), not the generic
* destroy(), or job_destroy() never
* runs and every raw-heap MOVE_PTR
* field (e.g. accounts) silently leaks
* on manager teardown. */
.datakiller = dereference };
var jobmgr;
job_submit_data_t submit1 = hard_make_submit("J-hard-10-1");
xce_job_t job1 = { .submit = &submit1, .jobid = 90001 };
job_t* j2 = NULL;
hard_ensure_ready();
jobmgr = makeInstance(&xkvmgr_type, &jobmgr_params);
TEST_ASSERT_TRUE(jobmgr != NULL);
/* Force the "not (yet) initialized" state this test needs. */
job_obj_fini();
g_hard_jobobj_ready = 0;
j2 = makeJob(jobmgr, &job1);
/* The bug: the original job_init() stored makeJobsub()'s NULL return
* into dst->job.submit and still returned success (1), so makeJob()
* would incorrectly hand back a non-NULL job_t with a NULL
* job.submit. The fix must fail construction outright instead. */
TEST_ASSERT_NULL(j2);
destroy(jobmgr);
/* Restore module state for every test that runs after this one. */
hard_ensure_ready();
/* Known, out-of-boundary residual: under ASAN leak detection this
* exercise reports a ~240-byte direct leak attributed to
* makeInstance() (via makeJob_/makeJob, jobj.c) for THIS call.
* Root-caused by inspection: makeInstance() allocates the object's
* meta_obj_t+privdata+data block BEFORE invoking .init(), and when
* .init() (job_init) returns a negative rc, makeInstance() returns
* NULL to the caller without freeing that block -- every RTTI
* .init() failure path in this codebase (including the pre-existing
* "src->submit == NULL" branch a few lines above ours, and
* jobsub_init's own ENOMEM branches) shares this exact
* characteristic; it is a property of the vendored rtti library's
* makeInstance(), not of job_init()'s logic, and not something
* src/lib/jobj.c can fix (no source access, no in-boundary seam).
* job_init() itself is correct here: Requirement 10.1 only asks it
* to check makeJobsub()'s return and propagate failure instead of
* storing NULL-with-success, which it now does -- this residual
* leak would occur identically even if job_init() never allocated
* anything at all before failing. Flagged to the parent task as a
* separate, pre-existing, out-of-scope follow-up candidate. */
}
/* ------------------------------------------------------------------- */
/* Requirement 10.3 -- MOVE_PTR fields are always NULL on a freshly */
/* built (calloc'd) submission payload, so reuse against a shared */
/* template across array elements is provably benign. */
/* ------------------------------------------------------------------- */
/* Mirrors build_job_submit_payloads() (src/controller/job.c) exactly:
* calloc a fresh xce_job_t and set only submit/jobid/sub_time/
* queue_time/state -- every MOVE_PTR field (pidinfo/allocs/accounts/
* run_usage/exit_info/pend_reasons) is left at its calloc-zeroed NULL.
* This is also the pattern the array-expansion path (restore_job_array,
* a later task) will use per element. */
static void test_freshly_submitted_job_move_ptr_fields_are_null(void)
{
kvmgr_params_t jobmgr_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 32,
/* job_t has .destroy = NULL (jobj.c) --
* teardown is wired only through its
* reference interface (job_dereference).
* The kvmgr's datakiller must therefore
* be dereference(), not the generic
* destroy(), or job_destroy() never
* runs and every raw-heap MOVE_PTR
* field (e.g. accounts) silently leaks
* on manager teardown. */
.datakiller = dereference };
var jobmgr;
job_submit_data_t submit1 = hard_make_submit("J-hard-10-3");
time_t now = time(NULL);
xce_job_t* job_a = calloc(1, sizeof(*job_a));
xce_job_t* job_b = calloc(1, sizeof(*job_b));
job_t* j2 = NULL;
job_t* j3 = NULL;
TEST_ASSERT_NOT_NULL(job_a);
TEST_ASSERT_NOT_NULL(job_b);
hard_ensure_ready();
job_a->submit = &submit1;
job_a->jobid = 90003;
job_a->sub_time = now;
job_a->queue_time = now;
job_a->state = JOB_STAT_PEND;
/* A second, independent element built from a different calloc'd
* source but sharing the SAME submit template -- the array
* construction pattern this feature relies on. */
job_b->submit = &submit1;
job_b->jobid = 90004;
job_b->sub_time = now;
job_b->queue_time = now;
job_b->state = JOB_STAT_PEND;
jobmgr = makeInstance(&xkvmgr_type, &jobmgr_params);
TEST_ASSERT_TRUE(jobmgr != NULL);
j2 = makeJob(jobmgr, job_a);
TEST_ASSERT_NOT_NULL(j2);
TEST_ASSERT_NULL(j2->job.pidinfo);
TEST_ASSERT_NULL(j2->job.allocs);
TEST_ASSERT_NULL(j2->job.accounts);
TEST_ASSERT_NULL(j2->job.run_usage);
TEST_ASSERT_NULL(j2->job.exit_info);
TEST_ASSERT_NULL(j2->job.pend_reasons);
j3 = makeJob(jobmgr, job_b);
TEST_ASSERT_NOT_NULL(j3);
TEST_ASSERT_NULL(j3->job.pidinfo);
TEST_ASSERT_NULL(j3->job.allocs);
TEST_ASSERT_NULL(j3->job.accounts);
TEST_ASSERT_NULL(j3->job.run_usage);
TEST_ASSERT_NULL(j3->job.exit_info);
TEST_ASSERT_NULL(j3->job.pend_reasons);
/* makeJobsub()'s interning collapses the shared template to one
* real allocation -- confirms the "right foundation for array
* scale" design.md calls out. */
TEST_ASSERT_TRUE(j2->job.submit == j3->job.submit);
destroy(jobmgr);
free(job_a);
free(job_b);
}
/* ------------------------------------------------------------------- */
/* Requirement 10.4 -- job_destroy() no longer leaks accounts. */
/* ------------------------------------------------------------------- */
static xce_job_account_t* hard_build_accounts(uint64_t jobid)
{
xce_job_account_t* acct = calloc(1, sizeof(*acct));
TEST_ASSERT_NOT_NULL(acct);
acct->jobid = jobid;
acct->nhosts = 1;
acct->accounts = calloc(1, sizeof(*acct->accounts));
TEST_ASSERT_NOT_NULL(acct->accounts);
xce_host_account_t* host = calloc(1, sizeof(*host));
TEST_ASSERT_NOT_NULL(host);
/* host->host intentionally left NULL: free_host_account() (misc.c,
* pre-existing, out of this task's src/lib/jobj.c boundary) does not
* free that field -- a separate, unrelated gap in misc.c, not
* Requirement 10.4's accounts leak this test targets. Leaving it
* NULL keeps this test isolated to proving job_destroy() drives
* free_job_account() correctly. */
host->naccount = 1;
host->accounts = calloc(1, sizeof(*host->accounts));
TEST_ASSERT_NOT_NULL(host->accounts);
xce_res_account_t* res = calloc(1, sizeof(*res));
TEST_ASSERT_NOT_NULL(res);
res->resname = strdup("cpu");
res->used = 1.0;
host->accounts[0] = res;
acct->accounts[0] = host;
acct->nres_shared = 1;
acct->accounts_shared = calloc(1, sizeof(*acct->accounts_shared));
TEST_ASSERT_NOT_NULL(acct->accounts_shared);
xce_res_account_t* shared = calloc(1, sizeof(*shared));
TEST_ASSERT_NOT_NULL(shared);
shared->resname = strdup("mem");
acct->accounts_shared[0] = shared;
return acct;
}
/* Run this test binary under ASAN with real leak detection
* (ASAN_OPTIONS=detect_leaks=1, the xcelib_test default -- unlike the
* controller suite, which sets detect_leaks=0 for pre-existing, unrelated
* leaks) to prove job_destroy() no longer leaks j->job.accounts. */
static void test_job_destroy_does_not_leak_accounts(void)
{
kvmgr_params_t jobmgr_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 32,
/* job_t has .destroy = NULL (jobj.c) --
* teardown is wired only through its
* reference interface (job_dereference).
* The kvmgr's datakiller must therefore
* be dereference(), not the generic
* destroy(), or job_destroy() never
* runs and every raw-heap MOVE_PTR
* field (e.g. accounts) silently leaks
* on manager teardown. */
.datakiller = dereference };
var jobmgr;
job_submit_data_t submit1 = hard_make_submit("J-hard-10-4");
time_t now = time(NULL);
xce_job_t* job1 = calloc(1, sizeof(*job1));
job_t* j2 = NULL;
TEST_ASSERT_NOT_NULL(job1);
hard_ensure_ready();
job1->submit = &submit1;
job1->jobid = 90005;
job1->sub_time = now;
job1->queue_time = now;
job1->state = JOB_STAT_PEND;
job1->accounts = hard_build_accounts(90005);
jobmgr = makeInstance(&xkvmgr_type, &jobmgr_params);
TEST_ASSERT_TRUE(jobmgr != NULL);
j2 = makeJob(jobmgr, job1);
TEST_ASSERT_NOT_NULL(j2);
TEST_ASSERT_NOT_NULL(j2->job.accounts); /* MOVE_PTR'd in */
TEST_ASSERT_NULL(job1->accounts); /* moved out of src */
/* destroy(jobmgr) tears down every stored job_t via its datakiller
* (dereference), which drives job_dereference -> job_destroy. */
destroy(jobmgr);
free(job1);
}
/* ------------------------------------------------------------------- */
/* Requirement 10.5 -- decode_Job/free_Job have no ckptinfo wire field */
/* to leak in the first place. */
/* ------------------------------------------------------------------- */
/* Verified by inspection, not exercisable from src/lib: decode_Job() and
* free_Job() are capnp-generated, controller-side symbols consuming
* job.c, which src/lib does not link. struct Job's capnp schema
* (src/schema/controller.capnp) declares jobid/submitData/subtime/
* queuetime/starttime/finishtime/state/allocs/runtime/pidinfo/
* pendreason/transientSince -- there is no ckptinfo field and no
* "JobAccount"-style struct for accounts either. decode_Job() is
* mechanically generated from that schema, so it structurally cannot
* populate xce_job_t.ckptinfo; free_Job() correctly never frees it
* either -- confirmed non-issue, not a gap.
*
* This test instead verifies the one piece of this invariant jobj.c
* itself owns: job_init()'s ckptinfo handling for the realistic
* freshly-submitted-job case (src->ckptinfo == NULL, exactly what
* build_job_submit_payloads()/array-element construction produce)
* leaves dst->job.ckptinfo NULL with no leak and no crash on destroy. */
static void test_ckptinfo_null_for_freshly_submitted_job_no_leak(void)
{
kvmgr_params_t jobmgr_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 32,
/* job_t has .destroy = NULL (jobj.c) --
* teardown is wired only through its
* reference interface (job_dereference).
* The kvmgr's datakiller must therefore
* be dereference(), not the generic
* destroy(), or job_destroy() never
* runs and every raw-heap MOVE_PTR
* field (e.g. accounts) silently leaks
* on manager teardown. */
.datakiller = dereference };
var jobmgr;
job_submit_data_t submit1 = hard_make_submit("J-hard-10-5");
time_t now = time(NULL);
/* ckptinfo intentionally left NULL by the initializer below --
* matches a freshly-submitted job. */
xce_job_t job1 = { .submit = &submit1,
.jobid = 90006,
.sub_time = now,
.queue_time = now,
.state = JOB_STAT_PEND };
job_t* j2 = NULL;
hard_ensure_ready();
jobmgr = makeInstance(&xkvmgr_type, &jobmgr_params);
TEST_ASSERT_TRUE(jobmgr != NULL);
j2 = makeJob(jobmgr, &job1);
TEST_ASSERT_NOT_NULL(j2);
TEST_ASSERT_NULL(j2->job.ckptinfo);
destroy(jobmgr);
}
void test_jobj_hardening(void)
{
printf("\n>> jobj hardening test (job-array-single-wal-record Requirement 10)\n\n");
RUN_TEST(test_job_obj_init_is_idempotent_without_fini);
RUN_TEST(test_job_init_propagates_makeJobsub_failure);
RUN_TEST(test_freshly_submitted_job_move_ptr_fields_are_null);
RUN_TEST(test_job_destroy_does_not_leak_accounts);
RUN_TEST(test_ckptinfo_null_for_freshly_submitted_job_no_leak);
job_obj_fini();
xstring_table_fini();
g_hard_jobobj_ready = 0;
g_hard_xstring_ready = 0;
}