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:
parent
ee7dee0ed4
commit
5b47ee603f
8 changed files with 1138 additions and 379 deletions
|
|
@ -35,15 +35,25 @@ hosts:
|
|||
model: "x86_64-linux-gnu"
|
||||
max_slot: 128
|
||||
|
||||
# t1: primary test node (172.31.1.30). Must match the hostname the worker
|
||||
# reports via gethostname(); without a matching hostconfig, the worker's
|
||||
# join is rejected with -EINVAL (find_matched_hostconfig fails) and no
|
||||
# compute node ever registers, so every job stays PEND (bug-1494).
|
||||
# t1/t2/t3: test nodes (172.31.1.30/31/32). Must match each hostname the
|
||||
# worker reports via gethostname(); without a matching hostconfig, the
|
||||
# worker's join is rejected with -EINVAL (find_matched_hostconfig fails)
|
||||
# and no compute node ever registers, so every job stays PEND (bug-1494).
|
||||
- hostname: t1
|
||||
type: "x86_64-linux-gnu"
|
||||
model: "x86_64-linux-gnu"
|
||||
max_slot: 8
|
||||
|
||||
- hostname: t2
|
||||
type: "x86_64-linux-gnu"
|
||||
model: "x86_64-linux-gnu"
|
||||
max_slot: 8
|
||||
|
||||
- hostname: t3
|
||||
type: "x86_64-linux-gnu"
|
||||
model: "x86_64-linux-gnu"
|
||||
max_slot: 8
|
||||
|
||||
|
||||
hostgroups:
|
||||
- groupname: hg1
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -865,3 +865,27 @@ func TestRawClusterToModel(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1500: the submit conversion must carry an exec user; explicit exec_user
|
||||
// wins, otherwise the job runs as the submitting user (not "nobody").
|
||||
func TestResolveExecUser(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
execUser string
|
||||
submitUser string
|
||||
want string
|
||||
}{
|
||||
{"explicit exec_user wins", "lchen", "admin", "lchen"},
|
||||
{"empty exec_user falls back to submit user", "", "admin", "admin"},
|
||||
{"both empty stays empty", "", "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
job := &models.JobInfo{SubmitUser: tc.submitUser, ExecUser: tc.execUser}
|
||||
if got := resolveExecUser(job); got != tc.want {
|
||||
t.Errorf("resolveExecUser: got %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue