Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use chroot when manipulating users and use go to check users #1681

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions internal/distro/distro.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (
setfilesCmd = "setfiles"
wipefsCmd = "wipefs"
systemctlCmd = "systemctl"
chrootCmd = "chroot"

// Filesystem tools
btrfsMkfsCmd = "mkfs.btrfs"
Expand Down Expand Up @@ -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/"
Expand All @@ -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 }
Expand All @@ -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))
}
Expand Down
246 changes: 246 additions & 0 deletions internal/exec/util/lookup_unix.go
Original file line number Diff line number Diff line change
@@ -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)
}
26 changes: 13 additions & 13 deletions internal/exec/util/passwd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions internal/exec/util/user_group_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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),
Expand Down
Loading