From 1dd7f13dfbc1dd377eabace0239b1c05cd60b144 Mon Sep 17 00:00:00 2001
From: baude <bbaude@redhat.com>
Date: Thu, 25 Oct 2018 13:39:25 -0500
Subject: [PATCH] get user and group information using securejoin and runc's
 user library

for the purposes of performance and security, we use securejoin to contstruct
the root fs's path so that symlinks are what they appear to be and no pointing
to something naughty.

then instead of chrooting to parse /etc/passwd|/etc/group, we now use the runc user/group
methods which saves us quite a bit of performance.

Signed-off-by: baude <bbaude@redhat.com>
---
 libpod/container_api.go            |   8 +-
 libpod/container_internal.go       |  56 ++++++-----
 libpod/container_internal_linux.go |  44 ++------
 pkg/lookup/lookup.go               | 156 +++++++++++++++++++++++++++++
 4 files changed, 201 insertions(+), 63 deletions(-)
 create mode 100644 pkg/lookup/lookup.go

diff --git a/libpod/container_api.go b/libpod/container_api.go
index 41a131ea212..83f93cf9eb6 100644
--- a/libpod/container_api.go
+++ b/libpod/container_api.go
@@ -10,8 +10,8 @@ import (
 	"time"
 
 	"github.com/containers/libpod/libpod/driver"
-	"github.com/containers/libpod/pkg/chrootuser"
 	"github.com/containers/libpod/pkg/inspect"
+	"github.com/containers/libpod/pkg/lookup"
 	"github.com/containers/storage/pkg/stringid"
 	"github.com/docker/docker/daemon/caps"
 	"github.com/pkg/errors"
@@ -292,13 +292,13 @@ func (c *Container) Exec(tty, privileged bool, env, cmd []string, user string) e
 	// the host
 	hostUser := ""
 	if user != "" {
-		uid, gid, err := chrootuser.GetUser(c.state.Mountpoint, user)
+		execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, nil)
 		if err != nil {
-			return errors.Wrapf(err, "error getting user to launch exec session as")
+			return err
 		}
 
 		// runc expects user formatted as uid:gid
-		hostUser = fmt.Sprintf("%d:%d", uid, gid)
+		hostUser = fmt.Sprintf("%d:%d", execUser.Uid, execUser.Gid)
 	}
 
 	// Generate exec session ID
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index 2af216358b4..d928c4aedd5 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -12,9 +13,9 @@ import (
 	"strings"
 	"syscall"
 
-	"github.com/containers/libpod/pkg/chrootuser"
 	"github.com/containers/libpod/pkg/hooks"
 	"github.com/containers/libpod/pkg/hooks/exec"
+	"github.com/containers/libpod/pkg/lookup"
 	"github.com/containers/libpod/pkg/resolvconf"
 	"github.com/containers/libpod/pkg/rootless"
 	"github.com/containers/libpod/pkg/secrets"
@@ -1094,6 +1095,7 @@ func (c *Container) generateHosts() (str
 }
 
 func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator) error {
+	var uid, gid int
 	mountPoint := c.state.Mountpoint
 	if !c.state.Mounted {
 		return errors.Wrapf(ErrInternal, "container is not mounted")
@@ -1117,6 +1119,18 @@ func (c *Container) addLocalVolumes(ctx
 		}
 	}
 
+	if c.config.User != "" {
+		if !c.state.Mounted {
+			return errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
+		}
+		execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, c.config.User, nil)
+		if err != nil {
+			return err
+		}
+		uid = execUser.Uid
+		gid = execUser.Gid
+	}
+
 	for k := range imageData.ContainerConfig.Volumes {
 		mount := spec.Mount{
 			Destination: k,
@@ -1129,27 +1143,13 @@ func (c *Container) addLocalVolumes(ctx
 		volumePath := filepath.Join(c.config.StaticDir, "volumes", k)
 		srcPath := filepath.Join(mountPoint, k)
 
-		var (
-			uid uint32
-			gid uint32
-		)
-		if c.config.User != "" {
-			if !c.state.Mounted {
-				return errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
-			}
-			uid, gid, err = chrootuser.GetUser(c.state.Mountpoint, c.config.User)
-			if err != nil {
-				return err
-			}
-		}
-
 		if _, err := os.Stat(srcPath); os.IsNotExist(err) {
 			logrus.Infof("Volume image mount point %s does not exist in root FS, need to create it", k)
 			if err = os.MkdirAll(srcPath, 0755); err != nil {
 				return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID)
 			}
 
-			if err = os.Chown(srcPath, int(uid), int(gid)); err != nil {
+			if err = os.Chown(srcPath, uid, gid); err != nil {
 				return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", srcPath, k, c.ID)
 			}
 		}
@@ -1159,7 +1159,7 @@ func (c *Container) addLocalVolumes(ctx
 				return errors.Wrapf(err, "error creating directory %q for volume %q in container %q", volumePath, k, c.ID)
 			}
 
-			if err = os.Chown(volumePath, int(uid), int(gid)); err != nil {
+			if err = os.Chown(volumePath, uid, gid); err != nil {
 				return errors.Wrapf(err, "error chowning directory %q for volume %q in container %q", volumePath, k, c.ID)
 			}
 
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index 0a1784ba740..7bf2c71cac6 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -17,11 +17,9 @@ import (
 
 	cnitypes "github.com/containernetworking/cni/pkg/types/current"
 	crioAnnotations "github.com/containers/libpod/pkg/annotations"
-	"github.com/containers/libpod/pkg/chrootuser"
+	"github.com/containers/libpod/pkg/lookup"
 	"github.com/containers/libpod/pkg/rootless"
 	"github.com/containers/storage/pkg/idtools"
-	"github.com/cyphar/filepath-securejoin"
-	"github.com/opencontainers/runc/libcontainer/user"
 	spec "github.com/opencontainers/runtime-spec/specs-go"
 	"github.com/opencontainers/runtime-tools/generate"
 	"github.com/opencontainers/selinux/go-selinux/label"
@@ -135,6 +133,10 @@ func (c *Container) cleanupNetwork() error {
 // Generate spec for a container
 // Accepts a map of the container's dependencies
 func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
+	execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, c.config.User, nil)
+	if err != nil {
+		return nil, err
+	}
 	g := generate.NewFromSpec(c.config.Spec)
 
 	// If network namespace was requested, add it now
@@ -188,7 +190,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
 		}
 	}
 
-	var err error
 	if !rootless.IsRootless() {
 		if c.state.ExtensionStageHooks, err = c.setupOCIHooks(ctx, g.Config); err != nil {
 			return nil, errors.Wrapf(err, "error setting up OCI Hooks")
@@ -206,13 +207,9 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
 		if !c.state.Mounted {
 			return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate User field", c.ID())
 		}
-		uid, gid, err := chrootuser.GetUser(c.state.Mountpoint, c.config.User)
-		if err != nil {
-			return nil, err
-		}
 		// User and Group must go together
-		g.SetProcessUID(uid)
-		g.SetProcessGID(gid)
+		g.SetProcessUID(uint32(execUser.Uid))
+		g.SetProcessGID(uint32(execUser.Gid))
 	}
 
 	// Add addition groups if c.config.GroupAdd is not empty
@@ -220,11 +217,8 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
 		if !c.state.Mounted {
 			return nil, errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to add additional groups", c.ID())
 		}
-		for _, group := range c.config.Groups {
-			gid, err := chrootuser.GetGroup(c.state.Mountpoint, group)
-			if err != nil {
-				return nil, err
-			}
+		gids, _ := lookup.GetContainerGroups(c.config.Groups, c.state.Mountpoint, nil)
+		for _, gid := range gids {
 			g.AddProcessAdditionalGid(gid)
 		}
 	}
@@ -237,26 +231,6 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) {
 
 	// Look up and add groups the user belongs to, if a group wasn't directly specified
 	if !rootless.IsRootless() && !strings.Contains(c.config.User, ":") {
-		var groupDest, passwdDest string
-		defaultExecUser := user.ExecUser{
-			Uid:  0,
-			Gid:  0,
-			Home: "/",
-		}
-
-		// Make sure the /etc/group  and /etc/passwd destinations are not a symlink to something naughty
-		if groupDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/group"); err != nil {
-			logrus.Debug(err)
-			return nil, err
-		}
-		if passwdDest, err = securejoin.SecureJoin(c.state.Mountpoint, "/etc/passwd"); err != nil {
-			logrus.Debug(err)
-			return nil, err
-		}
-		execUser, err := user.GetExecUserPath(c.config.User, &defaultExecUser, passwdDest, groupDest)
-		if err != nil {
-			return nil, err
-		}
 		for _, gid := range execUser.Sgids {
 			g.AddProcessAdditionalGid(uint32(gid))
 		}
diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go
new file mode 100644
index 00000000000..b27e2a724bc
--- /dev/null
+++ b/pkg/lookup/lookup.go
@@ -0,0 +1,156 @@
+package lookup
+
+import (
+	"github.com/cyphar/filepath-securejoin"
+	"github.com/opencontainers/runc/libcontainer/user"
+	"github.com/sirupsen/logrus"
+	"strconv"
+)
+
+const (
+	etcpasswd = "/etc/passwd"
+	etcgroup  = "/etc/group"
+)
+
+// Overrides allows you to override defaults in GetUserGroupInfo
+type Overrides struct {
+	DefaultUser            *user.ExecUser
+	ContainerEtcPasswdPath string
+	ContainerEtcGroupPath  string
+}
+
+// GetUserGroupInfo takes string forms of the the container's mount path and the container user and
+// returns a ExecUser with uid, gid, sgids, and home.  And override can be provided for defaults.
+func GetUserGroupInfo(containerMount, containerUser string, override *Overrides) (*user.ExecUser, error) {
+	var (
+		passwdDest, groupDest string
+		defaultExecUser       *user.ExecUser
+		err                   error
+	)
+	passwdPath := etcpasswd
+	groupPath := etcgroup
+
+	if override != nil {
+		// Check for an override /etc/passwd path
+		if override.ContainerEtcPasswdPath != "" {
+			passwdPath = override.ContainerEtcPasswdPath
+		}
+		// Check for an override for /etc/group path
+		if override.ContainerEtcGroupPath != "" {
+			groupPath = override.ContainerEtcGroupPath
+		}
+	}
+
+	// Check for an override default user
+	if override != nil && override.DefaultUser != nil {
+		defaultExecUser = override.DefaultUser
+	} else {
+		// Define a default container user
+		//defaultExecUser = &user.ExecUser{
+		//	Uid:  0,
+		//	Gid:  0,
+		//	Home: "/",
+		defaultExecUser = nil
+
+	}
+
+	// Make sure the /etc/group  and /etc/passwd destinations are not a symlink to something naughty
+	if passwdDest, err = securejoin.SecureJoin(containerMount, passwdPath); err != nil {
+		logrus.Debug(err)
+		return nil, err
+	}
+	if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil {
+		logrus.Debug(err)
+		return nil, err
+	}
+	return user.GetExecUserPath(containerUser, defaultExecUser, passwdDest, groupDest)
+}
+
+// GetContainerGroups uses securejoin to get a list of numerical groupids from a container. Per the runc
+// function it calls: If a group name cannot be found, an error will be returned. If a group id cannot be found,
+// or the given group data is nil, the id will be returned as-is  provided it is in the legal range.
+func GetContainerGroups(groups []string, containerMount string, override *Overrides) ([]uint32, error) {
+	var (
+		groupDest string
+		err       error
+		uintgids  []uint32
+	)
+
+	groupPath := etcgroup
+	if override != nil && override.ContainerEtcGroupPath != "" {
+		groupPath = override.ContainerEtcGroupPath
+	}
+
+	if groupDest, err = securejoin.SecureJoin(containerMount, groupPath); err != nil {
+		logrus.Debug(err)
+		return nil, err
+	}
+
+	gids, err := user.GetAdditionalGroupsPath(groups, groupDest)
+	if err != nil {
+		return nil, err
+	}
+	// For libpod, we want []uint32s
+	for _, gid := range gids {
+		uintgids = append(uintgids, uint32(gid))
+	}
+	return uintgids, nil
+}
+
+// GetUser takes a containermount path and user name or id and returns
+// a matching User structure from /etc/passwd.  If it cannot locate a user
+// with the provided information, an ErrNoPasswdEntries is returned.
+func GetUser(containerMount, userIDorName string) (*user.User, error) {
+	var inputIsName bool
+	uid, err := strconv.Atoi(userIDorName)
+	if err != nil {
+		inputIsName = true
+	}
+	passwdDest, err := securejoin.SecureJoin(containerMount, etcpasswd)
+	if err != nil {
+		return nil, err
+	}
+	users, err := user.ParsePasswdFileFilter(passwdDest, func(u user.User) bool {
+		if inputIsName {
+			return u.Name == userIDorName
+		}
+		return u.Uid == uid
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(users) > 0 {
+		return &users[0], nil
+	}
+	return nil, user.ErrNoPasswdEntries
+}
+
+// GetGroup takes ac ontainermount path and a group name or id and returns
+// a match Group struct from /etc/group.  if it cannot locate a group,
+// an ErrNoGroupEntries error is returned.
+func GetGroup(containerMount, groupIDorName string) (*user.Group, error) {
+	var inputIsName bool
+	gid, err := strconv.Atoi(groupIDorName)
+	if err != nil {
+		inputIsName = true
+	}
+
+	groupDest, err := securejoin.SecureJoin(containerMount, etcgroup)
+	if err != nil {
+		return nil, err
+	}
+
+	groups, err := user.ParseGroupFileFilter(groupDest, func(g user.Group) bool {
+		if inputIsName {
+			return g.Name == groupIDorName
+		}
+		return g.Gid == gid
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(groups) > 0 {
+		return &groups[0], nil
+	}
+	return nil, user.ErrNoGroupEntries
+}
