summaryrefslogtreecommitdiff
path: root/pkg/chrootuser
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/chrootuser')
-rw-r--r--pkg/chrootuser/user.go71
-rw-r--r--pkg/chrootuser/user_basic.go19
-rw-r--r--pkg/chrootuser/user_linux.go235
3 files changed, 325 insertions, 0 deletions
diff --git a/pkg/chrootuser/user.go b/pkg/chrootuser/user.go
new file mode 100644
index 000000000..a024877a5
--- /dev/null
+++ b/pkg/chrootuser/user.go
@@ -0,0 +1,71 @@
+package chrootuser
+
+import (
+ "os/user"
+ "strconv"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+// GetUser will return the uid, gid of the user specified in the userspec
+// it will use the /etc/password and /etc/shadow files inside of the rootdir
+// to return this information.
+// userspace format [user | user:group | uid | uid:gid | user:gid | uid:group ]
+func GetUser(rootdir, userspec string) (uint32, uint32, error) {
+ var gid64 uint64
+ var gerr error = user.UnknownGroupError("error looking up group")
+
+ spec := strings.SplitN(userspec, ":", 2)
+ userspec = spec[0]
+ groupspec := ""
+ if userspec == "" {
+ return 0, 0, nil
+ }
+ if len(spec) > 1 {
+ groupspec = spec[1]
+ }
+
+ uid64, uerr := strconv.ParseUint(userspec, 10, 32)
+ if uerr == nil && groupspec == "" {
+ // We parsed the user name as a number, and there's no group
+ // component, so try to look up the primary GID of the user who
+ // has this UID.
+ var name string
+ name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64)
+ if gerr == nil {
+ userspec = name
+ } else {
+ // Leave userspec alone, but swallow the error and just
+ // use GID 0.
+ gid64 = 0
+ gerr = nil
+ }
+ }
+ if uerr != nil {
+ // The user ID couldn't be parsed as a number, so try to look
+ // up the user's UID and primary GID.
+ uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec)
+ gerr = uerr
+ }
+
+ if groupspec != "" {
+ // We have a group name or number, so parse it.
+ gid64, gerr = strconv.ParseUint(groupspec, 10, 32)
+ if gerr != nil {
+ // The group couldn't be parsed as a number, so look up
+ // the group's GID.
+ gid64, gerr = lookupGroupInContainer(rootdir, groupspec)
+ }
+ }
+
+ if uerr == nil && gerr == nil {
+ return uint32(uid64), uint32(gid64), nil
+ }
+
+ err := errors.Wrapf(uerr, "error determining run uid")
+ if uerr == nil {
+ err = errors.Wrapf(gerr, "error determining run gid")
+ }
+ return 0, 0, err
+}
diff --git a/pkg/chrootuser/user_basic.go b/pkg/chrootuser/user_basic.go
new file mode 100644
index 000000000..4f89af557
--- /dev/null
+++ b/pkg/chrootuser/user_basic.go
@@ -0,0 +1,19 @@
+// +build !linux
+
+package chrootuser
+
+import (
+ "github.com/pkg/errors"
+)
+
+func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) {
+ return 0, 0, errors.New("user lookup not supported")
+}
+
+func lookupGroupInContainer(rootdir, groupname string) (uint64, error) {
+ return 0, errors.New("group lookup not supported")
+}
+
+func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) {
+ return "", 0, errors.New("primary group lookup by uid not supported")
+}
diff --git a/pkg/chrootuser/user_linux.go b/pkg/chrootuser/user_linux.go
new file mode 100644
index 000000000..2baf9ea33
--- /dev/null
+++ b/pkg/chrootuser/user_linux.go
@@ -0,0 +1,235 @@
+// +build linux
+
+package chrootuser
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "os/user"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/containers/storage/pkg/reexec"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/sys/unix"
+)
+
+const (
+ openChrootedCommand = "chrootuser-open"
+)
+
+func init() {
+ reexec.Register(openChrootedCommand, openChrootedFileMain)
+}
+
+func openChrootedFileMain() {
+ status := 0
+ flag.Parse()
+ if len(flag.Args()) < 1 {
+ os.Exit(1)
+ }
+ // Our first parameter is the directory to chroot into.
+ if err := unix.Chdir(flag.Arg(0)); err != nil {
+ fmt.Fprintf(os.Stderr, "chdir(): %v", err)
+ os.Exit(1)
+ }
+ if err := unix.Chroot(flag.Arg(0)); err != nil {
+ fmt.Fprintf(os.Stderr, "chroot(): %v", err)
+ os.Exit(1)
+ }
+ // Anything else is a file we want to dump out.
+ for _, filename := range flag.Args()[1:] {
+ f, err := os.Open(filename)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "open(%q): %v", filename, err)
+ status = 1
+ continue
+ }
+ _, err = io.Copy(os.Stdout, f)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "read(%q): %v", filename, err)
+ }
+ f.Close()
+ }
+ os.Exit(status)
+}
+
+func openChrootedFile(rootdir, filename string) (*exec.Cmd, io.ReadCloser, error) {
+ // The child process expects a chroot and one or more filenames that
+ // will be consulted relative to the chroot directory and concatenated
+ // to its stdout. Start it up.
+ cmd := reexec.Command(openChrootedCommand, rootdir, filename)
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, nil, err
+ }
+ err = cmd.Start()
+ if err != nil {
+ return nil, nil, err
+ }
+ // Hand back the child's stdout for reading, and the child to reap.
+ return cmd, stdout, nil
+}
+
+var (
+ lookupUser, lookupGroup sync.Mutex
+)
+
+type lookupPasswdEntry struct {
+ name string
+ uid uint64
+ gid uint64
+}
+type lookupGroupEntry struct {
+ name string
+ gid uint64
+}
+
+func readWholeLine(rc *bufio.Reader) ([]byte, error) {
+ line, isPrefix, err := rc.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ for isPrefix {
+ // We didn't get a whole line. Keep reading chunks until we find an end of line, and discard them.
+ for isPrefix {
+ logrus.Debugf("discarding partial line %q", string(line))
+ _, isPrefix, err = rc.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ }
+ // That last read was the end of a line, so now we try to read the (beginning of?) the next line.
+ line, isPrefix, err = rc.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ }
+ return line, nil
+}
+
+func parseNextPasswd(rc *bufio.Reader) *lookupPasswdEntry {
+ line, err := readWholeLine(rc)
+ if err != nil {
+ return nil
+ }
+ fields := strings.Split(string(line), ":")
+ if len(fields) < 7 {
+ return nil
+ }
+ uid, err := strconv.ParseUint(fields[2], 10, 32)
+ if err != nil {
+ return nil
+ }
+ gid, err := strconv.ParseUint(fields[3], 10, 32)
+ if err != nil {
+ return nil
+ }
+ return &lookupPasswdEntry{
+ name: fields[0],
+ uid: uid,
+ gid: gid,
+ }
+}
+
+func parseNextGroup(rc *bufio.Reader) *lookupGroupEntry {
+ line, err := readWholeLine(rc)
+ if err != nil {
+ return nil
+ }
+ fields := strings.Split(string(line), ":")
+ if len(fields) < 4 {
+ return nil
+ }
+ gid, err := strconv.ParseUint(fields[2], 10, 32)
+ if err != nil {
+ return nil
+ }
+ return &lookupGroupEntry{
+ name: fields[0],
+ gid: gid,
+ }
+}
+
+func lookupUserInContainer(rootdir, username string) (uid uint64, gid uint64, err error) {
+ cmd, f, err := openChrootedFile(rootdir, "/etc/passwd")
+ if err != nil {
+ return 0, 0, err
+ }
+ defer func() {
+ _ = cmd.Wait()
+ }()
+ rc := bufio.NewReader(f)
+ defer f.Close()
+
+ lookupUser.Lock()
+ defer lookupUser.Unlock()
+
+ pwd := parseNextPasswd(rc)
+ for pwd != nil {
+ if pwd.name != username {
+ pwd = parseNextPasswd(rc)
+ continue
+ }
+ return pwd.uid, pwd.gid, nil
+ }
+
+ return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username))
+}
+
+func lookupGroupForUIDInContainer(rootdir string, userid uint64) (username string, gid uint64, err error) {
+ cmd, f, err := openChrootedFile(rootdir, "/etc/passwd")
+ if err != nil {
+ return "", 0, err
+ }
+ defer func() {
+ _ = cmd.Wait()
+ }()
+ rc := bufio.NewReader(f)
+ defer f.Close()
+
+ lookupUser.Lock()
+ defer lookupUser.Unlock()
+
+ pwd := parseNextPasswd(rc)
+ for pwd != nil {
+ if pwd.uid != userid {
+ pwd = parseNextPasswd(rc)
+ continue
+ }
+ return pwd.name, pwd.gid, nil
+ }
+
+ return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid))
+}
+
+func lookupGroupInContainer(rootdir, groupname string) (gid uint64, err error) {
+ cmd, f, err := openChrootedFile(rootdir, "/etc/group")
+ if err != nil {
+ return 0, err
+ }
+ defer func() {
+ _ = cmd.Wait()
+ }()
+ rc := bufio.NewReader(f)
+ defer f.Close()
+
+ lookupGroup.Lock()
+ defer lookupGroup.Unlock()
+
+ grp := parseNextGroup(rc)
+ for grp != nil {
+ if grp.name != groupname {
+ grp = parseNextGroup(rc)
+ continue
+ }
+ return grp.gid, nil
+ }
+
+ return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname))
+}