diff --git a/run.go b/run.go index e346e1369..1282d070c 100644 --- a/run.go +++ b/run.go @@ -82,14 +82,6 @@ func (b *Builder) Run(command []string, options RunOptions) error { if err != nil { return err } - if options.User != "" { - user, err = getUser(options.User) - } else { - user, err = getUser(image.Config.User) - } - if err != nil { - return err - } g := generate.New() if image.OS != "" { @@ -98,8 +90,6 @@ func (b *Builder) Run(command []string, options RunOptions) error { if image.Architecture != "" { g.SetPlatformArch(image.Architecture) } - g.SetProcessUID(user.UID) - g.SetProcessGID(user.GID) for _, envSpec := range append(image.Config.Env, options.Env...) { env := strings.SplitN(envSpec, "=", 2) if len(env) > 1 { @@ -149,6 +139,16 @@ func (b *Builder) Run(command []string, options RunOptions) error { if !options.NetworkDisabled { g.RemoveLinuxNamespace("network") } + if options.User != "" { + user, err = getUser(mountPoint, options.User) + } else { + user, err = getUser(mountPoint, image.Config.User) + } + if err != nil { + return err + } + g.SetProcessUID(user.UID) + g.SetProcessGID(user.GID) spec := g.Spec() if spec.Process.Cwd == "" { spec.Process.Cwd = DefaultWorkingDir diff --git a/tests/run.bats b/tests/run.bats index c73db3c49..35fbf5764 100644 --- a/tests/run.bats +++ b/tests/run.bats @@ -22,3 +22,76 @@ load helpers buildah unmount $cid buildah rm $cid } + +@test "run-user" { + if ! which runc ; then + skip + fi + eval $(go env) + echo CGO_ENABLED=${CGO_ENABLED} + if test "$CGO_ENABLED" -ne 1; then + skip + fi + cid=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json alpine) + root=$(buildah mount $cid) + + testuser=jimbo + testgroup=jimbogroup + testuid=$RANDOM + testgid=$RANDOM + testgroupid=$RANDOM + echo "$testuser:x:$testuid:$testgid:Jimbo Jenkins:/home/$testuser:/bin/sh" >> $root/etc/passwd + echo "$testgroup:x:$testgroupid:" >> $root/etc/group + + buildah config $cid -u "" + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = 0 ] + run buildah --debug=false run -- $cid id -g + [ "$output" = 0 ] + + buildah config $cid -u ${testuser} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgid ] + + buildah config $cid -u ${testuid} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgid ] + + buildah config $cid -u ${testuser}:${testgroup} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgroupid ] + + buildah config $cid -u ${testuid}:${testgroup} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgroupid ] + + buildah config $cid -u ${testuser}:${testgroupid} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgroupid ] + + buildah config $cid -u ${testuid}:${testgroupid} + buildah run -- $cid id + run buildah --debug=false run -- $cid id -u + [ "$output" = $testuid ] + run buildah --debug=false run -- $cid id -g + [ "$output" = $testgroupid ] + + buildah unmount $cid + buildah rm $cid +} diff --git a/user.go b/user.go index f6de349e9..455622737 100644 --- a/user.go +++ b/user.go @@ -1,33 +1,78 @@ package buildah import ( + "fmt" "os/user" "strconv" + "strings" "github.com/opencontainers/runtime-spec/specs-go" ) -// TODO: we should doing these lookups using data that's actually in the container. -func getUser(username string) (specs.User, error) { - if username == "" { +func getUser(rootdir, userspec string) (specs.User, error) { + var gid64 uint64 + var gerr error = user.UnknownGroupError("error looking up group") + + spec := strings.SplitN(userspec, ":", 2) + userspec = spec[0] + groupspec := "" + if userspec == "" { return specs.User{}, nil } - runuser, err := user.Lookup(username) - if err != nil { - return specs.User{}, err + if len(spec) > 1 { + groupspec = spec[1] } - uid, err := strconv.ParseUint(runuser.Uid, 10, 32) - if err != nil { - return specs.User{}, nil + + uid64, uerr := strconv.ParseUint(userspec, 10, 32) + if uerr == nil && groupspec == "" { + // We parsed the user name as a number, and there's no group + // component, so we need to look up the user's primary GID. + name := "" + name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64) + if gerr == nil { + userspec = name + } else { + if userrec, err := user.LookupId(userspec); err == nil { + gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32) + userspec = userrec.Name + } + } } - gid, err := strconv.ParseUint(runuser.Gid, 10, 32) - if err != nil { - return specs.User{}, nil + if uerr != nil { + uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec) + gerr = uerr } - u := specs.User{ - UID: uint32(uid), - GID: uint32(gid), - Username: username, + if uerr != nil { + if userrec, err := user.Lookup(userspec); err == nil { + uid64, uerr = strconv.ParseUint(userrec.Uid, 10, 32) + gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32) + } } - return u, nil + + if groupspec != "" { + gid64, gerr = strconv.ParseUint(groupspec, 10, 32) + if gerr != nil { + gid64, gerr = lookupGroupInContainer(rootdir, groupspec) + } + if gerr != nil { + if group, err := user.LookupGroup(groupspec); err == nil { + gid64, gerr = strconv.ParseUint(group.Gid, 10, 32) + } + } + } + + if uerr == nil && gerr == nil { + u := specs.User{ + UID: uint32(uid64), + GID: uint32(gid64), + Username: userspec, + } + return u, nil + } + + err := fmt.Errorf("%v determining run uid", uerr) + if uerr == nil { + err = fmt.Errorf("%v determining run gid", gerr) + } + return specs.User{}, err } diff --git a/user_basic.go b/user_basic.go new file mode 100644 index 000000000..fcf793264 --- /dev/null +++ b/user_basic.go @@ -0,0 +1,19 @@ +// +build !cgo !linux + +package buildah + +import ( + "fmt" +) + +func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) { + return 0, 0, fmt.Errorf("user lookup not supported") +} + +func lookupGroupInContainer(rootdir, groupname string) (uint64, error) { + return 0, fmt.Errorf("group lookup not supported") +} + +func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) { + return "", 0, fmt.Errorf("primary group lookup by uid not supported") +} diff --git a/user_unix_cgo.go b/user_unix_cgo.go new file mode 100644 index 000000000..4ac1561d3 --- /dev/null +++ b/user_unix_cgo.go @@ -0,0 +1,122 @@ +// +build cgo +// +build linux + +package buildah + +// #include +// #include +// #include +// #include +// #include +// #include +// typedef FILE * pFILE; +import "C" + +import ( + "fmt" + "os/user" + "path/filepath" + "sync" + "syscall" + "unsafe" +) + +func fopenContainerFile(rootdir, filename string) (C.pFILE, error) { + var st, lst syscall.Stat_t + + ctrfile := filepath.Join(rootdir, filename) + cctrfile := C.CString(ctrfile) + defer C.free(unsafe.Pointer(cctrfile)) + mode := C.CString("r") + defer C.free(unsafe.Pointer(mode)) + f, err := C.fopen(cctrfile, mode) + if f == nil || err != nil { + return nil, fmt.Errorf("error opening %q: %v", ctrfile, err) + } + if err = syscall.Fstat(int(C.fileno(f)), &st); err != nil { + return nil, fmt.Errorf("fstat(%q): %v", ctrfile, err) + } + if err = syscall.Lstat(ctrfile, &lst); err != nil { + return nil, fmt.Errorf("lstat(%q): %v", ctrfile, err) + } + if st.Dev != lst.Dev || st.Ino != lst.Ino { + return nil, fmt.Errorf("%q is not a regular file") + } + return f, nil +} + +var ( + lookupUser, lookupGroup sync.Mutex +) + +func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) { + name := C.CString(username) + defer C.free(unsafe.Pointer(name)) + + f, err := fopenContainerFile(rootdir, "/etc/passwd") + if err != nil { + return 0, 0, err + } + defer C.fclose(f) + + lookupUser.Lock() + defer lookupUser.Unlock() + + pwd := C.fgetpwent(f) + for pwd != nil { + if C.strcmp(pwd.pw_name, name) != 0 { + pwd = C.fgetpwent(f) + continue + } + return uint64(pwd.pw_uid), uint64(pwd.pw_gid), nil + } + + return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username)) +} + +func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) { + f, err := fopenContainerFile(rootdir, "/etc/passwd") + if err != nil { + return "", 0, err + } + defer C.fclose(f) + + lookupUser.Lock() + defer lookupUser.Unlock() + + pwd := C.fgetpwent(f) + for pwd != nil { + if uint64(pwd.pw_uid) != userid { + pwd = C.fgetpwent(f) + continue + } + return C.GoString(pwd.pw_name), uint64(pwd.pw_gid), nil + } + + return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid)) +} + +func lookupGroupInContainer(rootdir, groupname string) (uint64, error) { + name := C.CString(groupname) + defer C.free(unsafe.Pointer(name)) + + f, err := fopenContainerFile(rootdir, "/etc/group") + if err != nil { + return 0, err + } + defer C.fclose(f) + + lookupGroup.Lock() + defer lookupGroup.Unlock() + + grp := C.fgetgrent(f) + for grp != nil { + if C.strcmp(grp.gr_name, name) != 0 { + grp = C.fgetgrent(f) + continue + } + return uint64(grp.gr_gid), nil + } + + return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname)) +}