Skip to content

Commit

Permalink
use chroot to run useradd/usermod
Browse files Browse the repository at this point in the history
  • Loading branch information
n0rad committed Jan 15, 2024
1 parent d47fe07 commit b032a6c
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 13 deletions.
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.UnknownGroupIdError(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)
}
22 changes: 11 additions & 11 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 @@ -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

0 comments on commit b032a6c

Please sign in to comment.