// +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 user string } 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, user: fields[3], } } 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, ErrNoSuchUser } func lookupAdditionalGroupsForUIDInContainer(rootdir string, userid uint64) (gid []uint32, err error) { // Get the username associated with userid username, _, err := lookupGroupForUIDInContainer(rootdir, userid) if err != nil { return nil, err } cmd, f, err := openChrootedFile(rootdir, "/etc/group") if err != nil { return nil, err } defer func() { _ = cmd.Wait() }() rc := bufio.NewReader(f) defer f.Close() lookupGroup.Lock() defer lookupGroup.Unlock() grp := parseNextGroup(rc) for grp != nil { if strings.Contains(grp.user, username) { gid = append(gid, uint32(grp.gid)) } grp = parseNextGroup(rc) } return gid, nil } 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)) }