diff --git a/internal/distro/distro.go b/internal/distro/distro.go index 61ca87aed..7336330a1 100644 --- a/internal/distro/distro.go +++ b/internal/distro/distro.go @@ -45,6 +45,7 @@ var ( setfilesCmd = "setfiles" wipefsCmd = "wipefs" systemctlCmd = "systemctl" + chrootCmd = "chroot" // Filesystem tools btrfsMkfsCmd = "mkfs.btrfs" @@ -73,6 +74,9 @@ var ( // ".ssh/authorized_keys.d/ignition" ("true"), or to // ".ssh/authorized_keys" ("false"). writeAuthorizedKeysFragment = "true" + // lookup for users/groups using go's os/user/lookup parsing /etc/passwd and /etc/groups + // or C's calling getpwnam_r() and getgrnam_r() + userGroupLookupUsingGo = "false" // Special file paths in the real root luksRealRootKeyFilePath = "/etc/luks/" @@ -98,6 +102,7 @@ func UserdelCmd() string { return userdelCmd } func SetfilesCmd() string { return setfilesCmd } func WipefsCmd() string { return wipefsCmd } func SystemctlCmd() string { return systemctlCmd } +func ChrootCmd() string { return chrootCmd } func BtrfsMkfsCmd() string { return btrfsMkfsCmd } func Ext4MkfsCmd() string { return ext4MkfsCmd } @@ -117,8 +122,9 @@ func KargsCmd() string { return kargsCmd } func LuksRealRootKeyFilePath() string { return luksRealRootKeyFilePath } func ResultFilePath() string { return resultFilePath } -func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) && !BlackboxTesting() } -func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } +func UserGroupLookupUsingGo() bool { return bakedStringToBool(userGroupLookupUsingGo) } +func SelinuxRelabel() bool { return bakedStringToBool(selinuxRelabel) && !BlackboxTesting() } +func BlackboxTesting() bool { return bakedStringToBool(blackboxTesting) } func WriteAuthorizedKeysFragment() bool { return bakedStringToBool(fromEnv("WRITE_AUTHORIZED_KEYS_FRAGMENT", writeAuthorizedKeysFragment)) } diff --git a/internal/exec/util/lookup_unix.go b/internal/exec/util/lookup_unix.go new file mode 100644 index 000000000..a8ae04af3 --- /dev/null +++ b/internal/exec/util/lookup_unix.go @@ -0,0 +1,246 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file is the same as go's private `os/user/lookup_unix.go` except that lookup*() functions accept a root path + +////go:build ((unix && !android) || (js && wasm) || wasip1) && ((!cgo && !darwin) || osusergo) +////+build unix,!android js,wasm wasip1 +////+build !cgo,!darwin osusergo + +package util + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "os/user" + "strconv" + "strings" +) + +const ( + userFile = "/etc/passwd" + groupFile = "/etc/group" +) + +var colon = []byte{':'} + +// lineFunc returns a value, an error, or (nil, nil) to skip the row. +type lineFunc func(line []byte) (v any, err error) + +// readColonFile parses r as an /etc/group or /etc/passwd style file, running +// fn for each row. readColonFile returns a value, an error, or (nil, nil) if +// the end of the file is reached without a match. +// +// readCols is the minimum number of colon-separated fields that will be passed +// to fn; in a long line additional fields may be silently discarded. +func readColonFile(r io.Reader, fn lineFunc, readCols int) (v any, err error) { + rd := bufio.NewReader(r) + + // Read the file line-by-line. + for { + var isPrefix bool + var wholeLine []byte + + // Read the next line. We do so in chunks (as much as reader's + // buffer is able to keep), check if we read enough columns + // already on each step and store final result in wholeLine. + for { + var line []byte + line, isPrefix, err = rd.ReadLine() + + if err != nil { + // We should return (nil, nil) if EOF is reached + // without a match. + if err == io.EOF { + err = nil + } + return nil, err + } + + // Simple common case: line is short enough to fit in a + // single reader's buffer. + if !isPrefix && len(wholeLine) == 0 { + wholeLine = line + break + } + + wholeLine = append(wholeLine, line...) + + // Check if we read the whole line (or enough columns) + // already. + if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols { + break + } + } + + // There's no spec for /etc/passwd or /etc/group, but we try to follow + // the same rules as the glibc parser, which allows comments and blank + // space at the beginning of a line. + wholeLine = bytes.TrimSpace(wholeLine) + if len(wholeLine) == 0 || wholeLine[0] == '#' { + continue + } + v, err = fn(wholeLine) + if v != nil || err != nil { + return + } + + // If necessary, skip the rest of the line + for ; isPrefix; _, isPrefix, err = rd.ReadLine() { + if err != nil { + // We should return (nil, nil) if EOF is reached without a match. + if err == io.EOF { + err = nil + } + return nil, err + } + } + } +} + +func matchGroupIndexValue(value string, idx int) lineFunc { + var leadColon string + if idx > 0 { + leadColon = ":" + } + substr := []byte(leadColon + value + ":") + return func(line []byte) (v any, err error) { + if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 { + return + } + // wheel:*:0:root + parts := strings.SplitN(string(line), ":", 4) + if len(parts) < 4 || parts[0] == "" || parts[idx] != value || + // If the file contains +foo and you search for "foo", glibc + // returns an "invalid argument" error. Similarly, if you search + // for a gid for a row where the group name starts with "+" or "-", + // glibc fails to find the record. + parts[0][0] == '+' || parts[0][0] == '-' { + return + } + if _, err := strconv.Atoi(parts[2]); err != nil { + return nil, nil + } + return &user.Group{Name: parts[0], Gid: parts[2]}, nil + } +} + +func findGroupId(id string, r io.Reader) (*user.Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(id, 2), 3); err != nil { + return nil, err + } else if v != nil { + return v.(*user.Group), nil + } + return nil, user.UnknownGroupIdError(id) +} + +func findGroupName(name string, r io.Reader) (*user.Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(name, 0), 3); err != nil { + return nil, err + } else if v != nil { + return v.(*user.Group), nil + } + return nil, user.UnknownGroupError(name) +} + +// returns a *User for a row if that row's has the given value at the +// given index. +func matchUserIndexValue(value string, idx int) lineFunc { + var leadColon string + if idx > 0 { + leadColon = ":" + } + substr := []byte(leadColon + value + ":") + return func(line []byte) (v any, err error) { + if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 { + return + } + // kevin:x:1005:1006::/home/kevin:/usr/bin/zsh + parts := strings.SplitN(string(line), ":", 7) + if len(parts) < 6 || parts[idx] != value || parts[0] == "" || + parts[0][0] == '+' || parts[0][0] == '-' { + return + } + if _, err := strconv.Atoi(parts[2]); err != nil { + return nil, nil + } + if _, err := strconv.Atoi(parts[3]); err != nil { + return nil, nil + } + u := &user.User{ + Username: parts[0], + Uid: parts[2], + Gid: parts[3], + Name: parts[4], + HomeDir: parts[5], + } + // The pw_gecos field isn't quite standardized. Some docs + // say: "It is expected to be a comma separated list of + // personal data where the first item is the full name of the + // user." + u.Name, _, _ = strings.Cut(u.Name, ",") + return u, nil + } +} + +func findUserId(uid string, r io.Reader) (*user.User, error) { + i, e := strconv.Atoi(uid) + if e != nil { + return nil, errors.New("user: invalid userid " + uid) + } + if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil { + return nil, err + } else if v != nil { + return v.(*user.User), nil + } + return nil, user.UnknownUserIdError(i) +} + +func findUsername(name string, r io.Reader) (*user.User, error) { + if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil { + return nil, err + } else if v != nil { + return v.(*user.User), nil + } + return nil, user.UnknownUserError(name) +} + +func lookupGroup(groupname string, root string) (*user.Group, error) { + f, err := os.Open(root + groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupName(groupname, f) +} + +func lookupGroupId(id string, root string) (*user.Group, error) { + f, err := os.Open(root + groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupId(id, f) +} + +func lookupUser(username string, root string) (*user.User, error) { + f, err := os.Open(root + userFile) + if err != nil { + return nil, err + } + defer f.Close() + return findUsername(username, f) +} + +func lookupUserId(uid string, root string) (*user.User, error) { + f, err := os.Open(root + userFile) + if err != nil { + return nil, err + } + defer f.Close() + return findUserId(uid, f) +} diff --git a/internal/exec/util/passwd.go b/internal/exec/util/passwd.go index e6050d049..65eb7ea5b 100644 --- a/internal/exec/util/passwd.go +++ b/internal/exec/util/passwd.go @@ -63,8 +63,8 @@ func (u Util) EnsureUser(c types.PasswdUser) error { } if !shouldExist { if exists { - args := []string{"--remove", "--root", u.DestDir, c.Name} - _, err := u.LogCmd(exec.Command(distro.UserdelCmd(), args...), + args := []string{u.DestDir, distro.UserdelCmd(), "--remove", c.Name} + _, err := u.LogCmd(exec.Command(distro.ChrootCmd(), args...), "deleting user %q", c.Name) if err != nil { return fmt.Errorf("failed to delete user %q: %v", @@ -74,17 +74,16 @@ func (u Util) EnsureUser(c types.PasswdUser) error { return nil } - args := []string{"--root", u.DestDir} + args := []string{u.DestDir} - var cmd string if exists { - cmd = distro.UsermodCmd() + args = append(args, distro.UsermodCmd()) if util.NotEmpty(c.HomeDir) { args = append(args, "--home", *c.HomeDir, "--move-home") } } else { - cmd = distro.UseraddCmd() + args = append(args, distro.UseraddCmd()) args = appendIfStringSet(args, "--home-dir", c.HomeDir) @@ -127,7 +126,7 @@ func (u Util) EnsureUser(c types.PasswdUser) error { args = append(args, c.Name) - _, err = u.LogCmd(exec.Command(cmd, args...), + _, err = u.LogCmd(exec.Command(distro.ChrootCmd(), args...), "creating or modifying user %q", c.Name) return err } @@ -297,13 +296,14 @@ func (u Util) SetPasswordHash(c types.PasswdUser) error { } args := []string{ - "--root", u.DestDir, + u.DestDir, + distro.UsermodCmd(), "--password", pwhash, } args = append(args, c.Name) - _, err := u.LogCmd(exec.Command(distro.UsermodCmd(), args...), + _, err := u.LogCmd(exec.Command(distro.ChrootCmd(), args...), "setting password for %q", c.Name) return err } @@ -319,8 +319,8 @@ func (u Util) EnsureGroup(g types.PasswdGroup) error { } if !shouldExist { if exists { - args := []string{"--root", u.DestDir, g.Name} - _, err := u.LogCmd(exec.Command(distro.GroupdelCmd(), args...), + args := []string{u.DestDir, distro.GroupdelCmd(), "--root", u.DestDir, g.Name} + _, err := u.LogCmd(exec.Command(distro.ChrootCmd(), args...), "deleting group %q", g.Name) if err != nil { return fmt.Errorf("failed to delete group %q: %v", @@ -330,7 +330,7 @@ func (u Util) EnsureGroup(g types.PasswdGroup) error { return nil } - args := []string{"--root", u.DestDir} + args := []string{u.DestDir, distro.GroupaddCmd()} if g.Gid != nil { args = append(args, "--gid", @@ -347,7 +347,7 @@ func (u Util) EnsureGroup(g types.PasswdGroup) error { args = append(args, g.Name) - _, err = u.LogCmd(exec.Command(distro.GroupaddCmd(), args...), + _, err = u.LogCmd(exec.Command(distro.ChrootCmd(), args...), "adding group %q", g.Name) return err } diff --git a/internal/exec/util/user_group_lookup.go b/internal/exec/util/user_group_lookup.go index fa14c9468..330ee8c84 100644 --- a/internal/exec/util/user_group_lookup.go +++ b/internal/exec/util/user_group_lookup.go @@ -25,11 +25,15 @@ import "C" import ( "fmt" + "github.com/coreos/ignition/v2/internal/distro" "os/user" ) // userLookup looks up the user in u.DestDir. func (u Util) userLookup(name string) (*user.User, error) { + if distro.UserGroupLookupUsingGo() { + return lookupUser(name, u.DestDir) + } res := &C.lookup_res_t{} if ret, err := C.user_lookup(C.CString(u.DestDir), @@ -55,6 +59,9 @@ func (u Util) userLookup(name string) (*user.User, error) { // groupLookup looks up the group in u.DestDir. func (u Util) groupLookup(name string) (*user.Group, error) { + if distro.UserGroupLookupUsingGo() { + return lookupGroup(name, u.DestDir) + } res := &C.lookup_res_t{} if ret, err := C.group_lookup(C.CString(u.DestDir),