go: os/user: LookupUser() doesn't find users on macOS when compiled with CGO_ENABLED=0

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

go version go1.9.2 darwin/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

GOARCH=“amd64” GOBIN=“” GOCACHE=“/home/jeff_walter/.cache/go-build” GOEXE=“” GOHOSTARCH=“amd64” GOHOSTOS=“freebsd” GOOS=“darwin” GOPATH=“/home/jeff_walter/src/agent” GORACE=“” GOROOT=“/usr/local/go” GOTMPDIR=“” GOTOOLDIR=“/usr/local/go/pkg/tool/freebsd_amd64” GCCGO=“gccgo” CC=“clang” CXX=“clang++” CGO_ENABLED=“0” CGO_CFLAGS=“-g -O2” CGO_CPPFLAGS=“” CGO_CXXFLAGS=“-g -O2” CGO_FFLAGS=“-g -O2” CGO_LDFLAGS=“-g -O2” PKG_CONFIG=“pkg-config” GOGCCFLAGS=“-fPIC -m64 -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build053729832=/tmp/go-build -gno-record-gcc-switches -fno-common”

What did you do?

If I run the following program with sudo I get a different result than I get when I run it as ‘someuser’ (where ‘someuser’ is my current logged in user).

package main

import (
    "fmt"
    "os/user"
)

func main() {
    u, err := user.Lookup("someuser")
    if err != nil {
        fmt.Printf("%s", err)
        return
    }

    fmt.Printf("%V", u)
}

What did you expect to see?

$ ./user &{%!V(string=502) %!V(string=20) %!V(string=someuser) %!V(string=) %!V(string=/Users/someuser)}

What did you see instead?

$ sudo ./user user: unknown user someuser

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Comments: 24 (11 by maintainers)

Commits related to this issue

Most upvoted comments

So, to recap. Compiling with GCO_ENABLED=0 and calling user.Lookup() on a user other than yourself or root fails.

The following commands were run as jeff_walter:

$ ./user jeff_walter
Current user: &{502 20 jeff_walter  /Users/jeff_walter}
Looking up username: jeff_walter
&{502 20 jeff_walter  /Users/jeff_walter}

$ sudo ./user jeff_walter
Current user: &{0 0 root  /Users/jeff_walter}
Looking up username: jeff_walter
user: unknown user jeff_walter

$ ./user user
Current user: &{502 20 jeff_walter  /Users/jeff_walter}
Looking up username: user
user: unknown user user

$ sudo ./user user
Current user: &{0 0 root  /Users/jeff_walter}
Looking up username: user
user: unknown user user

$ ./user root
Current user: &{502 20 jeff_walter  /Users/jeff_walter}
Looking up username: root
&{0 0 root System Administrator /var/root}

$ sudo ./user root
Current user: &{0 0 root  /Users/jeff_walter}
Looking up username: root
&{0 0 root  /Users/jeff_walter} <--- THIS ALSO LOOKS ODD, BUT I GUESS SORT OF MAKES SENSE. STILL SEEMS UNEXPECTED.

The following commands were run as root:

$ ./user root
Current user: &{0 0 root  /var/root}
Looking up username: root
&{0 0 root  /var/root}

$ ./user jeff_walter
Current user: &{0 0 root  /var/root}
Looking up username: jeff_walter
user: unknown user jeff_walter

I came across the problem and had a workaround by shell out to macOS’s directory service tool dscacheutil. The credit goes to @tweekmonster https://github.com/tweekmonster/luser. The snippet is attached at the end of the comment.

The root cause is macOS queries directory service instead of relying on /etc/passwd, you can’t even find your own name (cat /etc/passwd | grep $USER). So when cgo is disabled, os/user.Lookup simply checks /etc/passwd and finds nothing.

This didn’t cause a big problem because we take another path when looking for current user and then fallback to lookupUser.

However, I think we should address this problem because:

  • It has valid use cases, e.g. a process running as root spawn a new process as another user based on config, it wants to check the target user does exists, but will always fail when cgo is disabled.
  • It might even be the case for BSD distros as well (based on man page, I don’t have BSD systems at hand)

Some possible solutions:

  • Document it in os/user.Lookup and stating when not using cgo or cross compile it will have problem on macOS, and user should shell out or use a third party library.
  • Create a new os/user/lookup_darwin.go to use dscacheutil (see snippet) or put it under a supplementary package, x/sys seems to be the closest one but shells out is not low level syscall…

References

Snippet

To build and run it on a mac

echo "Without CGO"
CGO_ENABLED=0 go build -o macuserlookup-gostd main.go
./macuserlookup-gostd --user $USER
sudo ./macuserlookup-gostd --user $USER

echo "With CGO"
CGO_ENABLED=1 go build -o macuserlookup-cgo main.go
./macuserlookup-cgo --user $USER
sudo ./macuserlookup-cgo --user $USER

echo "With dscacheutil"
go build -o macuserlookup-ds main.go
./macuserlookup-ds --user $USER --method ds
sudo ./macuserlookup-ds --user $USER --method ds

The output should be like NOTE: only using std without cgo failed to lookup myself from root

Without CGO
./macuserlookup-gostd 2020/11/10 19:18:32 Current user is at15 Look up user at15 using std
./macuserlookup-gostd 2020/11/10 19:18:32 Found user at15 using std: &{501 20 at15  /Users/at15}
Password:
./macuserlookup-gostd 2020/11/10 19:18:40 Current user is root Look up user at15 using std
./macuserlookup-gostd 2020/11/10 19:18:40 Look up user at15 using std failed: user: unknown user at15
With CGO
./macuserlookup-cgo 2020/11/10 19:18:40 Current user is at15 Look up user at15 using std
./macuserlookup-cgo 2020/11/10 19:18:40 Found user at15 using std: &{501 20 at15 Pinglei Guo /Users/at15}
./macuserlookup-cgo 2020/11/10 19:18:40 Current user is root Look up user at15 using std
./macuserlookup-cgo 2020/11/10 19:18:40 Found user at15 using std: &{501 20 at15 Pinglei Guo /Users/at15}
With dscacheutil
./macuserlookup-ds 2020/11/10 19:18:41 Current user is at15 Look up user at15 using ds
./macuserlookup-ds 2020/11/10 19:18:41 Found user at15 using ds: &{501 20 at15 Pinglei Guo /Users/at15}
./macuserlookup-ds 2020/11/10 19:18:41 Current user is root Look up user at15 using ds
./macuserlookup-ds 2020/11/10 19:18:41 Found user at15 using ds: &{501 20 at15 Pinglei Guo /Users/at15}
package main

/* macuserlookup shows how current os/user.Lookup does not work when not using cgo.
It also provides a workaround using dscacheutil based on https://github.com/tweekmonster/luser
The original issue is found in https://github.com/golang/go/issues/24383#issuecomment-372908869

echo "Without CGO"
CGO_ENABLED=0 go build -o macuserlookup-gostd main.go
./macuserlookup-gostd --user $USER
sudo ./macuserlookup-gostd --user $USER

echo "With CGO"
CGO_ENABLED=1 go build -o macuserlookup-cgo main.go
./macuserlookup-cgo --user $USER
sudo ./macuserlookup-cgo --user $USER

echo "With dscacheutil"
go build -o macuserlookup-ds main.go
./macuserlookup-ds --user $USER --method ds
sudo ./macuserlookup-ds --user $USER --method ds

References

- https://superuser.com/questions/191330/users-dont-appear-in-etc-passwd-on-mac-os-x
- man page for getpwnam_r `These functions obtain information from opendirectoryd(8)`
  - iOS https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/getpwnam_r.3.html
  - FreeBSD https://www.freebsd.org/cgi/man.cgi?getpwnam_r
  - Linux https://linux.die.net/man/3/getpwnam_r
*/

import (
	"flag"
	"fmt"
	"log"
	"os"
	"os/exec"
	"os/user"
	"strings"
)

var (
	fUser   string
	fMethod string
)

func init() {
	// Set binary name to prefix because we use different name in the example command
	log.SetPrefix(os.Args[0] + " ")
}

func main() {
	flag.StringVar(&fUser, "user", "at15", "User to lookup")
	flag.StringVar(&fMethod, "method", "std", "std (use cgo), dsc (shell out to macOS's directory service)")
	flag.Parse()
	lookUp(fMethod, fUser)
}

func lookUp(method string, username string) {
	var (
		u   *user.User
		err error
	)
	log.Printf("Current user is %s Look up user %s using %s", os.Getenv("USER"), username, method)
	switch method {
	case "std":
		u, err = user.Lookup(username)
	case "ds":
		u, err = dsLookup(username)
	default:
		log.Fatalf("Unsupported look up method %s", method)
	}
	if err != nil {
		log.Fatalf("Look up user %s using %s failed: %s", username, method, err)
	} else {
		log.Printf("Found user %s using %s: %s", username, method, u)
	}
}

const (
	// dsbin is a cli for querying macOS directory service.
	dsbin = "dscacheutil"
)

// dsLookup shells out to dscacheutil to get uid, gid from username.
func dsLookup(username string) (*user.User, error) {
	// dscacheutil -q user -a name at15
	// name: at15
	// password: ********
	// uid: 123456
	// gid: 123456
	// dir: /Users/at15
	// shell: /bin/zsh
	// gecos: At15
	//
	m, err := runDS("-q", "user", "-a", "name", username)
	if err != nil {
		return nil, err
	}
	u := &user.User{
		Uid:      m["uid"],
		Gid:      m["gid"],
		Username: m["name"],
		Name:     m["gecos"],
		HomeDir:  m["dir"],
	}
	if u.Username == "" || u.Username != username {
		return nil, user.UnknownUserError(username)
	}
	return u, nil
}

// runDS shells out query to dscacheutil and parse the output to key value pair.
func runDS(args ...string) (map[string]string, error) {
	b, err := exec.Command(dsbin, args...).CombinedOutput()
	if err != nil {
		cmd := strings.Join(append([]string{dsbin}, args...), " ")
		return nil, fmt.Errorf("error query directory service using %s: %w output %s", cmd, err, b)
	}
	return parseDSOutput(string(b))
}

// parseDSOutput splits dscacheutil output into key value pair.
// It returns error if no pair is found.
func parseDSOutput(s string) (map[string]string, error) {
	const sep = ": "
	lines := strings.Split(s, "\n")
	m := make(map[string]string)
	for _, line := range lines {
		keyEnd := strings.Index(line, sep)
		if keyEnd <= 0 { // the name must be longer than 1, i.e. `: value` does not exist
			continue
		}
		m[line[:keyEnd]] = line[keyEnd+len(sep):]
	}
	if len(m) == 0 {
		return m, fmt.Errorf("error parse %s output %s", dsbin, s)
	}
	return m, nil
}

Apparently, macOS doesn’t really use the /etc/passwd file anymore. Apparently, you can get the user info by parsing some plist files.

See https://apple.stackexchange.com/a/186899

$ sudo defaults read /var/db/dslocal/nodes/Default/users/user.plist uid
(
    503
)

$ sudo defaults read /var/db/dslocal/nodes/Default/users/user.plist gid
(
    20
)

$ sudo defaults read /var/db/dslocal/nodes/Default/users/user.plist passwd
(
    "********"
)

$ sudo defaults read /var/db/dslocal/nodes/Default/users/user.plist name
(
    user
)