diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/chrootuser/user.go | 71 | ||||
-rw-r--r-- | pkg/chrootuser/user_basic.go | 19 | ||||
-rw-r--r-- | pkg/chrootuser/user_linux.go | 235 |
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)) +} |