Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 18 additions & 26 deletions runner/internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,17 @@ func (ex *RunExecutor) Run(ctx context.Context) (err error) {
ctx = log.WithLogger(ctx, log.NewEntry(logger, int(log.DefaultEntry.Logger.Level))) // todo loglevel
log.Info(ctx, "Run job", "log_level", log.GetLogger(ctx).Logger.Level.String())

if ex.jobSpec.User != nil {
if err := fillUser(ex.jobSpec.User); err != nil {
ex.SetJobStateWithTerminationReason(
ctx,
types.JobStateFailed,
types.TerminationReasonExecutorError,
fmt.Sprintf("Failed to fill in the job user fields (%s)", err),
)
return gerrors.Wrap(err)
}
if ex.jobSpec.User == nil {
ex.jobSpec.User = &schemas.User{Uid: &ex.currentUid}
}
if err := fillUser(ex.jobSpec.User); err != nil {
ex.SetJobStateWithTerminationReason(
ctx,
types.JobStateFailed,
types.TerminationReasonExecutorError,
fmt.Sprintf("Failed to fill in the job user fields (%s)", err),
)
return gerrors.Wrap(err)
}

if err := ex.setupFiles(ctx); err != nil {
Expand Down Expand Up @@ -331,37 +332,28 @@ func (ex *RunExecutor) execJob(ctx context.Context, jobLogFile io.Writer) error
cmd.Dir = workingDir
}

// User must be already set
user := ex.jobSpec.User
if user != nil {
// Strictly speaking, we need CAP_SETUID and CAP_GUID (for Cmd.Start()->
// Cmd.SysProcAttr.Credential) and CAP_CHOWN (for startCommand()->os.Chown()),
// but for the sake of simplicity we instead check if we are root or not
if ex.currentUid == 0 {
log.Trace(
ctx, "Using credentials",
"uid", *user.Uid, "gid", *user.Gid, "groups", user.GroupIds,
"username", user.GetUsername(), "groupname", user.GetGroupname(),
"home", user.HomeDir,
)
log.Trace(ctx, "Current user", "uid", ex.currentUid)

// 1. Ideally, We should check uid, gid, and supplementary groups mismatches,
// but, for the sake of simplicity, we only check uid. Unprivileged runner
// should not receive job requests where user credentials do not match the
// current user's ones in the first place (it should be handled by the server)
// 2. Strictly speaking, we need CAP_SETUID and CAP_GUID (for Cmd.Start()->
// Cmd.SysProcAttr.Credential) and CAP_CHOWN (for startCommand()->os.Chown()),
// but for the sake of simplicity we instead check if we are root or not
if *user.Uid != ex.currentUid && ex.currentUid != 0 {
return gerrors.Newf("cannot start job as %d, current uid is %d", *user.Uid, ex.currentUid)
}

if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
// It's safe to setuid(2)/setgid(2)/setgroups(2) as unprivileged user if we use
// user's own credentials (basically, it's noop)
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: *user.Uid,
Gid: *user.Gid,
Groups: user.GroupIds,
}
} else {
log.Info(ctx, "Current user is not root, cannot set process credentials", "uid", ex.currentUid)
}

envMap := NewEnvMap(ParseEnvList(os.Environ()), jobEnvs, ex.secrets)
Expand Down
109 changes: 57 additions & 52 deletions runner/internal/executor/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,67 +39,72 @@ func (ex *RunExecutor) setupFiles(ctx context.Context) error {
homeDir := ex.workingDir
uid := -1
gid := -1
if ex.jobSpec.User != nil {
if ex.jobSpec.User.HomeDir != "" {
homeDir = ex.jobSpec.User.HomeDir
}
if ex.jobSpec.User.Uid != nil {
uid = int(*ex.jobSpec.User.Uid)
}
if ex.jobSpec.User.Gid != nil {
gid = int(*ex.jobSpec.User.Gid)
}
// User must be already set
if ex.jobSpec.User.HomeDir != "" {
homeDir = ex.jobSpec.User.HomeDir
}
if ex.jobSpec.User.Uid != nil {
uid = int(*ex.jobSpec.User.Uid)
}
if ex.jobSpec.User.Gid != nil {
gid = int(*ex.jobSpec.User.Gid)
}

for _, fa := range ex.jobSpec.FileArchives {
log.Trace(ctx, "Extracting file archive", "id", fa.Id, "path", fa.Path)

p := path.Clean(fa.Path)
// `~username[/path/to]` is not supported
if p == "~" {
p = homeDir
} else if rest, found := strings.CutPrefix(p, "~/"); found {
p = path.Join(homeDir, rest)
} else if !path.IsAbs(p) {
p = path.Join(ex.workingDir, p)
}
dir, root := path.Split(p)
if err := mkdirAll(ctx, dir, uid, gid); err != nil {
archivePath := path.Join(ex.archiveDir, fa.Id)
if err := extractFileArchive(ctx, archivePath, fa.Path, ex.workingDir, uid, gid, homeDir); err != nil {
return gerrors.Wrap(err)
}
}

if err := os.RemoveAll(p); err != nil {
log.Warning(ctx, "Failed to remove", "path", p, "err", err)
}
if err := os.RemoveAll(ex.archiveDir); err != nil {
log.Warning(ctx, "Failed to remove file archives dir", "path", ex.archiveDir, "err", err)
}

archivePath := path.Join(ex.archiveDir, fa.Id)
archive, err := os.Open(archivePath)
if err != nil {
return gerrors.Wrap(err)
}
defer func() {
_ = archive.Close()
if err := os.Remove(archivePath); err != nil {
log.Warning(ctx, "Failed to remove archive", "path", archivePath, "err", err)
}
}()
return nil
}

var paths []string
repl := fmt.Sprintf("%s$2", root)
renameAndRemember := func(s string) string {
s = renameRegex.ReplaceAllString(s, repl)
paths = append(paths, s)
return s
}
if err := extract.Tar(ctx, archive, dir, renameAndRemember); err != nil {
return gerrors.Wrap(err)
}
func extractFileArchive(ctx context.Context, archivePath string, targetPath string, targetRoot string, uid int, gid int, homeDir string) error {
log.Trace(ctx, "Extracting file archive", "archive", archivePath, "target", targetPath)

if uid != -1 || gid != -1 {
for _, p := range paths {
if err := os.Chown(path.Join(dir, p), uid, gid); err != nil {
log.Warning(ctx, "Failed to chown", "path", p, "err", err)
}
targetPath = path.Clean(targetPath)
// `~username[/path/to]` is not supported
if targetPath == "~" {
targetPath = homeDir
} else if rest, found := strings.CutPrefix(targetPath, "~/"); found {
targetPath = path.Join(homeDir, rest)
} else if !path.IsAbs(targetPath) {
targetPath = path.Join(targetRoot, targetPath)
}
dir, root := path.Split(targetPath)
if err := mkdirAll(ctx, dir, uid, gid); err != nil {
return gerrors.Wrap(err)
}
if err := os.RemoveAll(targetPath); err != nil {
log.Warning(ctx, "Failed to remove", "path", targetPath, "err", err)
}

archive, err := os.Open(archivePath)
if err != nil {
return gerrors.Wrap(err)
}
defer archive.Close()

var paths []string
repl := fmt.Sprintf("%s$2", root)
renameAndRemember := func(s string) string {
s = renameRegex.ReplaceAllString(s, repl)
paths = append(paths, s)
return s
}
if err := extract.Tar(ctx, archive, dir, renameAndRemember); err != nil {
return gerrors.Wrap(err)
}

if uid != -1 || gid != -1 {
for _, p := range paths {
if err := os.Chown(path.Join(dir, p), uid, gid); err != nil {
log.Warning(ctx, "Failed to chown", "path", p, "err", err)
}
}
}
Expand Down
Loading