Skip to content

Commit 69daa67

Browse files
committed
Correct handling of capabilities
Ensure that capabilities are properly handled for non-root users in privileged containers. We do not want to give full caps, but instead only CapInh and CapEff (others should be all-zeroes). Fixing `podman run` is easy - the same code as the Podman 1.6 fix works there. The `podman exec` command is far more challenging. Exec received a complete rewrite to use Conmon at some point before Podman 1.6, and gained many capabilities in the process. One of those was the ability to actually tweak the capabilities of the exec process - 1.0 did not have that. Since it was needed to resolve this CVE, I was forced to backport a large bit of the 1.0 -> 1.6 exec changes (passing a Process block to the OCI runtime, and using `prepareProcessExec()` to prepare said block). I am honestly uncomfortable with the size and scope of this change but I don't see another way around this. Fixes CVE-2021-20188 Signed-off-by: Matthew Heon <[email protected]>
1 parent 900d033 commit 69daa67

File tree

3 files changed

+132
-48
lines changed

3 files changed

+132
-48
lines changed

libpod/container_api.go

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package libpod
22

33
import (
44
"context"
5-
"fmt"
65
"io/ioutil"
76
"os"
87
"strconv"
@@ -11,9 +10,7 @@ import (
1110

1211
"github.com/containers/libpod/libpod/driver"
1312
"github.com/containers/libpod/pkg/inspect"
14-
"github.com/containers/libpod/pkg/lookup"
1513
"github.com/containers/storage/pkg/stringid"
16-
"github.com/docker/docker/daemon/caps"
1714
"github.com/pkg/errors"
1815
"github.com/sirupsen/logrus"
1916
"k8s.io/apimachinery/pkg/util/wait"
@@ -263,8 +260,6 @@ func (c *Container) Kill(signal uint) error {
263260
// TODO allow specifying streams to attach to
264261
// TODO investigate allowing exec without attaching
265262
func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir string) error {
266-
var capList []string
267-
268263
locked := false
269264
if !c.batched {
270265
locked = true
@@ -287,22 +282,8 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
287282
if conState != ContainerStateRunning {
288283
return errors.Wrapf(ErrCtrStateInvalid, "cannot exec into container that is not running")
289284
}
290-
if privileged || c.config.Privileged {
291-
capList = caps.GetAllCapabilities()
292-
}
293285

294-
// If user was set, look it up in the container to get a UID to use on
295-
// the host
296-
hostUser := ""
297-
if user != "" {
298-
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil)
299-
if err != nil {
300-
return err
301-
}
302-
303-
// runc expects user formatted as uid:gid
304-
hostUser = fmt.Sprintf("%d:%d", execUser.Uid, execUser.Gid)
305-
}
286+
isPrivileged := privileged || c.config.Privileged
306287

307288
// Generate exec session ID
308289
// Ensure we don't conflict with an existing session ID
@@ -324,10 +305,11 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user, workDir
324305

325306
logrus.Debugf("Creating new exec session in container %s with session id %s", c.ID(), sessionID)
326307

327-
execCmd, err := c.runtime.ociRuntime.execContainer(c, cmd, capList, env, tty, workDir, hostUser, sessionID)
308+
execCmd, processFile, err := c.runtime.ociRuntime.execContainer(c, cmd, env, tty, workDir, user, sessionID, isPrivileged)
328309
if err != nil {
329310
return errors.Wrapf(err, "error exec %s", c.ID())
330311
}
312+
defer os.Remove(processFile)
331313
chWait := make(chan error)
332314
go func() {
333315
chWait <- execCmd.Wait()

libpod/oci.go

Lines changed: 121 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import (
1515
"syscall"
1616
"time"
1717

18+
"github.com/containers/libpod/pkg/lookup"
1819
"github.com/containers/libpod/pkg/rootless"
1920
"github.com/containers/libpod/pkg/util"
2021
"github.com/coreos/go-systemd/activation"
2122
"github.com/cri-o/ocicni/pkg/ocicni"
23+
"github.com/docker/docker/daemon/caps"
2224
spec "github.com/opencontainers/runtime-spec/specs-go"
2325
"github.com/opencontainers/selinux/go-selinux"
2426
"github.com/opencontainers/selinux/go-selinux/label"
@@ -735,18 +737,23 @@ func (r *OCIRuntime) unpauseContainer(ctr *Container) error {
735737
// TODO: Add --detach support
736738
// TODO: Convert to use conmon
737739
// TODO: add --pid-file and use that to generate exec session tracking
738-
func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty bool, cwd, user, sessionID string) (*exec.Cmd, error) {
740+
func (r *OCIRuntime) execContainer(c *Container, cmd, env []string, tty bool, cwd, user, sessionID string, privileged bool) (*exec.Cmd, string, error) {
739741
if len(cmd) == 0 {
740-
return nil, errors.Wrapf(ErrInvalidArg, "must provide a command to execute")
742+
return nil, "", errors.Wrapf(ErrInvalidArg, "must provide a command to execute")
741743
}
742744

743745
if sessionID == "" {
744-
return nil, errors.Wrapf(ErrEmptyID, "must provide a session ID for exec")
746+
return nil, "", errors.Wrapf(ErrEmptyID, "must provide a session ID for exec")
745747
}
746748

747749
runtimeDir, err := util.GetRootlessRuntimeDir()
748750
if err != nil {
749-
return nil, err
751+
return nil, "", err
752+
}
753+
754+
processFile, err := prepareProcessExec(c, cmd, env, tty, cwd, user, sessionID, privileged)
755+
if err != nil {
756+
return nil, "", err
750757
}
751758

752759
args := []string{}
@@ -756,34 +763,14 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty
756763

757764
args = append(args, "exec")
758765

759-
if cwd != "" {
760-
args = append(args, "--cwd", cwd)
761-
}
766+
args = append(args, "--process", processFile)
762767

763768
args = append(args, "--pid-file", c.execPidPath(sessionID))
764769

765-
if tty {
766-
args = append(args, "--tty")
767-
} else {
768-
args = append(args, "--tty=false")
769-
}
770-
771-
if user != "" {
772-
args = append(args, "--user", user)
773-
}
774-
775770
if c.config.Spec.Process.NoNewPrivileges {
776771
args = append(args, "--no-new-privs")
777772
}
778773

779-
for _, cap := range capAdd {
780-
args = append(args, "--cap", cap)
781-
}
782-
783-
for _, envVar := range env {
784-
args = append(args, "--env", envVar)
785-
}
786-
787774
// Append container ID and command
788775
args = append(args, c.ID())
789776
args = append(args, cmd...)
@@ -797,10 +784,10 @@ func (r *OCIRuntime) execContainer(c *Container, cmd, capAdd, env []string, tty
797784
execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", runtimeDir))
798785

799786
if err := execCmd.Start(); err != nil {
800-
return nil, errors.Wrapf(err, "cannot start container %s", c.ID())
787+
return nil, "", errors.Wrapf(err, "cannot start container %s", c.ID())
801788
}
802789

803-
return execCmd, nil
790+
return execCmd, processFile, nil
804791
}
805792

806793
// execStopContainer stops all active exec sessions in a container
@@ -892,3 +879,110 @@ func (r *OCIRuntime) checkpointContainer(ctr *Container, options ContainerCheckp
892879
args = append(args, ctr.ID())
893880
return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...)
894881
}
882+
883+
// prepareProcessExec returns the path of the process.json used in runc exec -p.
884+
// Returns path to the created exec process file. This will need to be removed
885+
// by the caller when they're done, best effort.
886+
func prepareProcessExec(c *Container, cmd, env []string, tty bool, cwd, user, sessionID string, privileged bool) (string, error) {
887+
filename := filepath.Join(c.bundlePath(), fmt.Sprintf("exec-process-%s", sessionID))
888+
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0600)
889+
if err != nil {
890+
return "", err
891+
}
892+
defer f.Close()
893+
894+
pspec := c.config.Spec.Process
895+
pspec.SelinuxLabel = c.config.ProcessLabel
896+
pspec.Args = cmd
897+
// We need to default this to false else it will inherit terminal as true
898+
// from the container.
899+
pspec.Terminal = false
900+
if tty {
901+
pspec.Terminal = true
902+
}
903+
if len(env) > 0 {
904+
pspec.Env = append(pspec.Env, env...)
905+
}
906+
907+
if cwd != "" {
908+
pspec.Cwd = cwd
909+
910+
}
911+
912+
var addGroups []string
913+
var sgids []uint32
914+
915+
// if the user is empty, we should inherit the user that the container is currently running with
916+
if user == "" {
917+
user = c.config.User
918+
addGroups = c.config.Groups
919+
}
920+
921+
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil)
922+
if err != nil {
923+
return "", err
924+
}
925+
926+
if len(addGroups) > 0 {
927+
sgids, err = lookup.GetContainerGroups(addGroups, c.state.Mountpoint, nil)
928+
if err != nil {
929+
return "", errors.Wrapf(err, "error looking up supplemental groups for container %s exec session %s", c.ID(), sessionID)
930+
}
931+
}
932+
933+
// If user was set, look it up in the container to get a UID to use on
934+
// the host
935+
if user != "" || len(sgids) > 0 {
936+
if user != "" {
937+
for _, sgid := range execUser.Sgids {
938+
sgids = append(sgids, uint32(sgid))
939+
}
940+
}
941+
processUser := spec.User{
942+
UID: uint32(execUser.Uid),
943+
GID: uint32(execUser.Gid),
944+
AdditionalGids: sgids,
945+
}
946+
947+
pspec.User = processUser
948+
}
949+
950+
allCaps := caps.GetAllCapabilities()
951+
pspec.Capabilities.Effective = []string{}
952+
if privileged {
953+
pspec.Capabilities.Bounding = allCaps
954+
} else {
955+
pspec.Capabilities.Bounding = []string{}
956+
}
957+
pspec.Capabilities.Inheritable = pspec.Capabilities.Bounding
958+
if execUser.Uid == 0 {
959+
pspec.Capabilities.Effective = pspec.Capabilities.Bounding
960+
pspec.Capabilities.Permitted = pspec.Capabilities.Bounding
961+
pspec.Capabilities.Ambient = pspec.Capabilities.Bounding
962+
} else {
963+
pspec.Capabilities.Permitted = pspec.Capabilities.Effective
964+
pspec.Capabilities.Ambient = pspec.Capabilities.Effective
965+
}
966+
967+
hasHomeSet := false
968+
for _, s := range pspec.Env {
969+
if strings.HasPrefix(s, "HOME=") {
970+
hasHomeSet = true
971+
break
972+
}
973+
}
974+
if !hasHomeSet {
975+
pspec.Env = append(pspec.Env, fmt.Sprintf("HOME=%s", execUser.Home))
976+
}
977+
978+
processJSON, err := json.Marshal(pspec)
979+
if err != nil {
980+
return "", err
981+
}
982+
983+
if err := ioutil.WriteFile(filename, processJSON, 0644); err != nil {
984+
return "", err
985+
}
986+
987+
return filename, nil
988+
}

pkg/spec/spec.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,14 @@ func CreateConfigToOCISpec(config *CreateConfig) (*spec.Spec, error) { //nolint
325325
}
326326
} else {
327327
g.SetupPrivileged(true)
328+
if config.User != "" {
329+
user := strings.SplitN(config.User, ":", 2)[0]
330+
if user != "root" && user != "0" {
331+
g.Spec().Process.Capabilities.Effective = []string{}
332+
g.Spec().Process.Capabilities.Permitted = []string{}
333+
g.Spec().Process.Capabilities.Ambient = []string{}
334+
}
335+
}
328336
}
329337

330338
// HANDLE SECCOMP

0 commit comments

Comments
 (0)