diff --git a/server/cmd/api/api/process.go b/server/cmd/api/api/process.go index 8367637f..a59d8e81 100644 --- a/server/cmd/api/api/process.go +++ b/server/cmd/api/api/process.go @@ -64,6 +64,16 @@ func (h *processHandle) setExited(code int) { h.mu.Unlock() } +func isUserCmdError(err error) bool { + return errors.Is(err, exec.ErrNotFound) || + errors.Is(err, exec.ErrDot) || + errors.Is(err, os.ErrNotExist) || + errors.Is(err, os.ErrPermission) || + errors.Is(err, syscall.EISDIR) || + errors.Is(err, syscall.ENOEXEC) || + errors.Is(err, syscall.ENOTDIR) +} + func buildCmd(body *oapi.ProcessExecRequest) (*exec.Cmd, error) { if body == nil || body.Command == "" { return nil, errors.New("command required") @@ -177,6 +187,9 @@ func (s *ApiService) ProcessExec(ctx context.Context, request oapi.ProcessExecRe defer cancel() } if err := cmd.Start(); err != nil { + if isUserCmdError(err) { + return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } log.Error("failed to start process", "err", err) return oapi.ProcessExec500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil } @@ -263,6 +276,9 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn var errStart error ptyFile, errStart = pty.Start(cmd) if errStart != nil { + if isUserCmdError(errStart) { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: errStart.Error()}}, nil + } log.Error("failed to start PTY process", "err", errStart) return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil } @@ -294,6 +310,9 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdin"}}, nil } if err := cmd.Start(); err != nil { + if isUserCmdError(err) { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } log.Error("failed to start process", "err", err) return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil } diff --git a/server/cmd/api/api/process_test.go b/server/cmd/api/api/process_test.go index 844a8aff..ca1f9bdd 100644 --- a/server/cmd/api/api/process_test.go +++ b/server/cmd/api/api/process_test.go @@ -203,6 +203,30 @@ func TestProcessNotFoundRoutes(t *testing.T) { } } +func TestProcessExec_CommandNotFound(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle)} + + body := &oapi.ProcessExecRequest{Command: "nonexistent_binary_that_does_not_exist"} + resp, err := svc.ProcessExec(ctx, oapi.ProcessExecRequestObject{Body: body}) + require.NoError(t, err) + _, ok := resp.(oapi.ProcessExec400JSONResponse) + require.True(t, ok, "expected 400 for nonexistent command, got %T", resp) +} + +func TestProcessSpawn_CommandNotFound(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := &ApiService{procs: make(map[string]*processHandle), stz: scaletozero.NewNoopController()} + + body := &oapi.ProcessSpawnRequest{Command: "nonexistent_binary_that_does_not_exist"} + resp, err := svc.ProcessSpawn(ctx, oapi.ProcessSpawnRequestObject{Body: body}) + require.NoError(t, err) + _, ok := resp.(oapi.ProcessSpawn400JSONResponse) + require.True(t, ok, "expected 400 for nonexistent command, got %T", resp) +} + func TestBuildCmd_AsRootSetsCredential(t *testing.T) { t.Parallel() asRoot := true