fix(apiserver): reject submit options the job never gets; add exec_home (bug-1936)

Cause: CreateJob accepted in_file, out_file, err_file, out_dir,
asked_hosts, begin_time, term_time and transfer_files and returned
success, but neither the controller's submit data/start info nor the
worker carries any of them (the worker never stages transferred files),
so they were silently dropped. Exec home had no submit field at all, and
a relative cwd would have been resolved against the worker unit's
directory.

Fix: new preCheckSubmitOptions() runs right after binding, before file
uploads and ID allocation, and answers 400 naming every unsupported
option that is set; cwd and the new exec_home field must be absolute and
fit the worker's job_assignment_t buffers (1023 bytes). exec_home goes
to JobInfo.ExecHome and from there (submitStartInfo) to the start info's
exechome. num_files already reaches exdata as nofile_limit; a handler
test now pins it. The array %J/%I submit-time substitution test (9.4)
checks cwd instead of out_file/err_file, which are now refused. Swagger
regenerated with `swag init -g cmd/main.go -o docs`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 11:48:45 +00:00
parent 725b1d23fd
commit edad8c7385
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
6 changed files with 200 additions and 12 deletions

View file

@ -1085,7 +1085,8 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
// CreateJob godoc
// @Summary Create a new job
// @Description Submit a new job with specified requirements and parameters
// @Description Submit a new job with specified requirements and parameters.
// @Description in_file, out_file, err_file, out_dir, asked_hosts, begin_time, term_time and transfer_files are not supported yet and are rejected with 400; cwd and exec_home must be absolute paths.
// @Tags jobs
// @Accept json
// @Produce json
@ -1114,6 +1115,14 @@ func (j JobController) CreateJob(ctx *gin.Context) {
return
}
// bug-1936: refuse an option the job would never get, before anything
// (file uploads, ID allocation) happens for it.
if err := preCheckSubmitOptions(&req); err != nil {
logger.Error("Job submission rejected", zap.Error(err))
ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil)
return
}
// precheck the request parameter before generating the job ID
arraySize, isArray, err := preCheckJobRequest(&req)
if err != nil {
@ -1220,6 +1229,7 @@ func (j JobController) CreateJob(ctx *gin.Context) {
UserPriority: req.JobPriority,
DependCond: req.DependCond,
CWD: req.CWD,
ExecHome: req.ExecHome, // bug-1936: exechome of the start info
FromHost: req.FromHost, // bug-1676: client-asserted hostname, passed through unchanged
PreExecCmd: req.PreExecCmd,
PostExecCmd: req.PostExecCmd,
@ -1281,6 +1291,62 @@ func (j JobController) CreateJob(ctx *gin.Context) {
ReturnSuccess(ctx, common.CREATED, common.SUCCESS_MSG, response)
}
// maxWorkerPathLen is the longest cwd or exec home the worker takes: xcecd
// hands both to xcecd_worker in job_assignment_t's cwd/home[PATH_LENGTH]
// (1024 bytes, src/worker/include/uv_ipc.h), NUL included.
const maxWorkerPathLen = 1024 - 1
// preCheckSubmitOptions rejects what a submission asks for but the job
// would not get (bug-1936). The controller's submit data and start info
// have no field for stdin/stdout/stderr paths, an output directory, asked
// hosts or a begin/termination time, and the worker never stages
// transferred files in or out, so each of those is refused instead of
// being accepted and dropped. cwd and exec_home do reach the worker, which
// runs the job there with HOME set; both must be absolute and fit its
// buffers.
func preCheckSubmitOptions(req *models.JobSubmitRequest) error {
unsupported := []struct {
field string
set bool
}{
{"in_file", req.InFile != ""},
{"out_file", req.OutFile != ""},
{"err_file", req.ErrFile != ""},
{"out_dir", req.OutDir != ""},
{"asked_hosts", strings.TrimSpace(req.AskedHosts) != ""},
{"begin_time", !req.BeginTime.Time.IsZero()},
{"term_time", !req.TermTime.Time.IsZero()},
{"transfer_files", len(req.TransferFiles) > 0},
}
var names []string
for _, u := range unsupported {
if u.set {
names = append(names, u.field)
}
}
if len(names) > 0 {
return fmt.Errorf("job submission option(s) %s not supported: the job would not get them",
strings.Join(names, ", "))
}
for _, p := range []struct{ field, value string }{
{"cwd", req.CWD},
{"exec_home", req.ExecHome},
} {
if p.value == "" {
continue
}
if !strings.HasPrefix(p.value, "/") {
return fmt.Errorf("%s %q must be an absolute path", p.field, p.value)
}
if len(p.value) > maxWorkerPathLen {
return fmt.Errorf("%s is too long: %d bytes, more than the limit of %d",
p.field, len(p.value), maxWorkerPathLen)
}
}
return nil
}
func preCheckJobRestartRequest(req *models.JobRestartRequest) error {
logger.Debug("Enter preCheckJobRestartRequest")
if err := validateAskedHosts(req.AskedHosts); err != nil {