package engine import ( "bufio" "fmt" "os" "path/filepath" "strconv" "strings" ) // User is one identity inside the sandbox, resolved from a docker-style // user spec against the image's /etc/passwd and /etc/group. type User struct { UID, GID int Home string } // LookupUser resolves `user[:group]` — names or numbers, as docker and // podman take them — inside rootfs. A name must exist in the image's // passwd/group; a number need not, then the group defaults to the uid // and the home to "" (the engine's /root). A named user without an // explicit group gets the group of its passwd entry. func LookupUser(rootfs, spec string) (User, error) { userPart, groupPart, hasGroup := strings.Cut(spec, ":") if userPart == "" { return User{}, fmt.Errorf("invalid user %q", spec) } u := User{} entry, found, err := passwdEntry(rootfs, userPart) if err != nil { return User{}, err } switch { case found: u.UID, u.GID, u.Home = entry.uid, entry.gid, entry.home default: n, err := strconv.Atoi(userPart) if err != nil || n < 0 { return User{}, fmt.Errorf("user %q not found in the image's /etc/passwd", userPart) } u.UID, u.GID = n, n } if hasGroup { gid, found, err := groupEntry(rootfs, groupPart) if err != nil { return User{}, err } if !found { n, err := strconv.Atoi(groupPart) if err != nil || n < 0 { return User{}, fmt.Errorf("group %q not found in the image's /etc/group", groupPart) } gid = n } u.GID = gid } return u, nil } type passwdLine struct { uid, gid int home string } // passwdEntry finds key (a name or a numeric uid) in rootfs/etc/passwd. func passwdEntry(rootfs, key string) (passwdLine, bool, error) { var result passwdLine found := false err := eachColonLine(filepath.Join(rootfs, "etc", "passwd"), func(fields []string) bool { if len(fields) < 7 || (fields[0] != key && fields[2] != key) { return false } uid, err1 := strconv.Atoi(fields[2]) gid, err2 := strconv.Atoi(fields[3]) if err1 != nil || err2 != nil { return false } result, found = passwdLine{uid: uid, gid: gid, home: fields[5]}, true return true }) return result, found, err } // groupEntry finds key (a name or a numeric gid) in rootfs/etc/group. func groupEntry(rootfs, key string) (int, bool, error) { gid, found := 0, false err := eachColonLine(filepath.Join(rootfs, "etc", "group"), func(fields []string) bool { if len(fields) < 3 || (fields[0] != key && fields[2] != key) { return false } n, err := strconv.Atoi(fields[2]) if err != nil { return false } gid, found = n, true return true }) return gid, found, err } // eachColonLine calls fn per line of a colon-separated file until fn // returns true; a missing file is not an error (busybox images may // lack /etc/group). func eachColonLine(path string, fn func(fields []string) bool) error { f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil } return err } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := sc.Text() if line == "" || strings.HasPrefix(line, "#") { continue } if fn(strings.Split(line, ":")) { return nil } } return sc.Err() }