fix(apiserver): carry exec_user on job submit so jobs don't run as nobody (bug-1500)

modelJobToSubmit built the C job_startinfo_t without setting execuser, so the
controller forwarded a NULL execuser in the startjob request and the worker
ran every job as "nobody". Now the submit carries the resolved exec user: an
explicit exec_user wins, otherwise the job runs as the submitting user.

- add resolveExecUser() (plain-Go, unit-testable) + TestResolveExecUser
- set si.execuser in modelJobToSubmit, free it in freeJobSubmit

Note: deploying this exposed an ASAN heap-use-after-free in the apiserver's
submit/notification path (strncpy reading freed memory); the fix is committed
but needs the memory-lifecycle investigation before it can ship safely.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-04 07:37:48 +00:00
parent ee7dee0ed4
commit 5b47ee603f
8 changed files with 1138 additions and 379 deletions

View file

@ -249,6 +249,19 @@ func cClusterConfToClusterInfo(conf *C.cluster_config_t) *models.ClusterInfo {
// modelJobToSubmit converts a Go JobInfo to C xce_job_submit_t heap structs.
// The caller must free all returned C memory by calling freeJobSubmit.
// resolveExecUser returns the user a job should run as: an explicit exec_user
// wins; otherwise the job runs as the submitting user. bug-1500: the submit
// conversion previously dropped this entirely, so the worker received a NULL
// execuser and ran every job as "nobody". Kept as a plain-Go function so it is
// unit-testable without cgo (the Go toolchain rejects `import "C"` in _test.go).
func resolveExecUser(job *models.JobInfo) string {
execUser := job.ExecUser
if execUser == "" {
execUser = job.SubmitUser
}
return execUser
}
func modelJobToSubmit(job *models.JobInfo) (*C.xce_job_submit_t, *C.job_submit_data_t, *C.job_startinfo_t) {
si := (*C.job_startinfo_t)(C.calloc(1, C.sizeof_job_startinfo_t))
si.cmdline = C.CString(job.Command)
@ -261,6 +274,9 @@ func modelJobToSubmit(job *models.JobInfo) (*C.xce_job_submit_t, *C.job_submit_d
if job.PostExecCmd != "" {
si.job_post_exec = C.CString(job.PostExecCmd)
}
// bug-1500: carry the exec user on the start_info. Without this the worker
// receives a NULL execuser and runs every job as "nobody".
si.execuser = C.CString(resolveExecUser(job))
sd := (*C.job_submit_data_t)(C.calloc(1, C.sizeof_job_submit_data_t))
sd.queue = C.CString(job.QueueName)
@ -303,6 +319,7 @@ func freeJobSubmit(js *C.xce_job_submit_t, sd *C.job_submit_data_t, si *C.job_st
C.free(unsafe.Pointer(si.launch_type))
C.free(unsafe.Pointer(si.job_pre_exec))
C.free(unsafe.Pointer(si.job_post_exec))
C.free(unsafe.Pointer(si.execuser))
C.free(unsafe.Pointer(si))
}
if sd != nil {