Skip to content

Commit 5fd6545

Browse files
authored
[runner] Check capabilities(7) (#3587)
1 parent a46600f commit 5fd6545

File tree

5 files changed

+103
-11
lines changed

5 files changed

+103
-11
lines changed

runner/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/urfave/cli/v3 v3.6.1
2424
golang.org/x/crypto v0.22.0
2525
golang.org/x/sys v0.26.0
26+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77
2627
)
2728

2829
require (
@@ -84,4 +85,5 @@ require (
8485
gopkg.in/warnings.v0 v0.1.2 // indirect
8586
gopkg.in/yaml.v3 v3.0.1 // indirect
8687
gotest.tools/v3 v3.5.1 // indirect
88+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
8789
)

runner/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
321321
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
322322
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
323323
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
324+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77 h1:iQtQTjFUOcTT19fI8sTCzYXsjeVs56et3D8AbKS2Uks=
325+
kernel.org/pub/linux/libs/security/libcap/cap v1.2.77/go.mod h1:oV+IO8kGh0B7TxErbydDe2+BRmi9g/W0CkpVV+QBTJU=
326+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
327+
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=

runner/internal/executor/executor.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/dstackai/dstack/runner/consts"
2727
"github.com/dstackai/dstack/runner/internal/common"
2828
"github.com/dstackai/dstack/runner/internal/connections"
29+
cap "github.com/dstackai/dstack/runner/internal/linux/capabilities"
2930
linuxuser "github.com/dstackai/dstack/runner/internal/linux/user"
3031
"github.com/dstackai/dstack/runner/internal/log"
3132
"github.com/dstackai/dstack/runner/internal/schemas"
@@ -467,10 +468,19 @@ func (ex *RunExecutor) execJob(ctx context.Context, jobLogFile io.Writer) error
467468
}
468469
cmd.Dir = ex.jobWorkingDir
469470

470-
// Strictly speaking, we need CAP_SETUID and CAP_GUID (for Cmd.Start()->
471-
// Cmd.SysProcAttr.Credential) and CAP_CHOWN (for startCommand()->os.Chown()),
472-
// but for the sake of simplicity we instead check if we are root or not
473-
if ex.currentUser.IsRoot() {
471+
// CAP_SET{UID,GID} for startCommand() -> Cmd.Start() -> set{uid,gid,groups} syscalls during fork-exec
472+
// CAP_CHOWN for startCommand() -> os.Chown(pts.Name())
473+
if missing, err := cap.Check(cap.SETUID, cap.SETGID, cap.CHOWN); err != nil {
474+
log.Error(
475+
ctx, "Failed to check capabilities, won't try to set process credentials",
476+
"err", err, "user", ex.currentUser,
477+
)
478+
} else if len(missing) > 0 {
479+
log.Info(
480+
ctx, "Required capabilities are missing, cannot set process credentials",
481+
"missing", missing, "user", ex.currentUser,
482+
)
483+
} else {
474484
log.Trace(ctx, "Using credentials", "user", ex.jobUser)
475485
if cmd.SysProcAttr == nil {
476486
cmd.SysProcAttr = &syscall.SysProcAttr{}
@@ -480,8 +490,6 @@ func (ex *RunExecutor) execJob(ctx context.Context, jobLogFile io.Writer) error
480490
return fmt.Errorf("prepare process credentials: %w", err)
481491
}
482492
cmd.SysProcAttr.Credential = creds
483-
} else {
484-
log.Info(ctx, "Current user is not root, cannot set process credentials", "user", ex.currentUser)
485493
}
486494

487495
envMap := NewEnvMap(ParseEnvList(os.Environ()), jobEnvs, ex.secrets)
@@ -509,11 +517,15 @@ func (ex *RunExecutor) execJob(ctx context.Context, jobLogFile io.Writer) error
509517
// Note: we already set RLIMIT_MEMLOCK to unlimited in the shim if we've detected IB devices
510518
// (see configureHpcNetworkingIfAvailable() function), but, as it's on the shim side, it only works
511519
// with VM-based backends.
512-
rlimitMemlock := unix.Rlimit{Cur: unix.RLIM_INFINITY, Max: unix.RLIM_INFINITY}
513-
// TODO: Check if we have CAP_SYS_RESOURCE. In container environments, even root usually doesn't have
514-
// this capability.
515-
if err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &rlimitMemlock); err != nil {
516-
log.Error(ctx, "Failed to set resource limits", "err", err)
520+
if ok, err := cap.Has(cap.SYS_RESOURCE); err != nil {
521+
log.Error(ctx, "Failed to check capabilities, won't try to set resource limits", "err", err)
522+
} else if !ok {
523+
log.Info(ctx, "Required capability is missing, cannot set resource limits", "missing", cap.SYS_RESOURCE)
524+
} else {
525+
rlimitMemlock := unix.Rlimit{Cur: unix.RLIM_INFINITY, Max: unix.RLIM_INFINITY}
526+
if err := unix.Setrlimit(unix.RLIMIT_MEMLOCK, &rlimitMemlock); err != nil {
527+
log.Error(ctx, "Failed to set resource limits", "err", err)
528+
}
517529
}
518530

519531
// HOME must be added after writeDstackProfile to avoid overriding the correct per-user value set by sshd
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//go:build darwin
2+
3+
package capabilities
4+
5+
import "errors"
6+
7+
type Capability string
8+
9+
const (
10+
SETUID = Capability("SETUID")
11+
SETGID = Capability("SETGID")
12+
CHOWN = Capability("CHOWN")
13+
SYS_RESOURCE = Capability("SYS_RESOURCE")
14+
)
15+
16+
func Has(c Capability) (bool, error) {
17+
return false, errors.New("not supported")
18+
}
19+
20+
func Check(cs ...Capability) (missing []Capability, err error) {
21+
return nil, errors.New("not supported")
22+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//go:build linux
2+
3+
package capabilities
4+
5+
import (
6+
"strings"
7+
8+
"kernel.org/pub/linux/libs/security/libcap/cap"
9+
)
10+
11+
type Capability cap.Value
12+
13+
const (
14+
SETUID = Capability(cap.SETUID)
15+
SETGID = Capability(cap.SETGID)
16+
CHOWN = Capability(cap.CHOWN)
17+
SYS_RESOURCE = Capability(cap.SYS_RESOURCE)
18+
)
19+
20+
// String returns a text representation of the capability in the form used by container folks:
21+
// UPPER_CASE, no CAP_ prefix: cap_sys_admin -> SYS_ADMIN
22+
func (c Capability) String() string {
23+
return strings.ToUpper(cap.Value(c).String()[4:])
24+
}
25+
26+
// Has returns true if the current process has the specified capability in its effective set
27+
func Has(c Capability) (bool, error) {
28+
set, err := cap.GetPID(0)
29+
if err != nil {
30+
return false, err
31+
}
32+
return set.GetFlag(cap.Effective, cap.Value(c))
33+
}
34+
35+
// Check checks and returns those capabilities that are _missing_ from the effective set
36+
// of the current process
37+
func Check(cs ...Capability) (missing []Capability, err error) {
38+
set, err := cap.GetPID(0)
39+
if err != nil {
40+
return nil, err
41+
}
42+
for _, c := range cs {
43+
ok, err := set.GetFlag(cap.Effective, cap.Value(c))
44+
if err != nil {
45+
return nil, err
46+
}
47+
if !ok {
48+
missing = append(missing, c)
49+
}
50+
}
51+
return missing, nil
52+
}

0 commit comments

Comments
 (0)