From b8432dce46b289a7a883444a004375a203393353 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Thu, 8 Oct 2015 11:46:10 -0400 Subject: [PATCH 01/75] Add utility/support package for user namespace support The `pkg/idtools` package supports the creation of user(s) for retrieving /etc/sub{u,g}id ranges and creation of the UID/GID mappings provided to clone() to add support for user namespaces in Docker. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- user/idtools.go | 207 +++++++++++++++++++++++++++++++ user/usergroupadd_linux.go | 155 +++++++++++++++++++++++ user/usergroupadd_unsupported.go | 12 ++ 3 files changed, 374 insertions(+) create mode 100644 user/idtools.go create mode 100644 user/usergroupadd_linux.go create mode 100644 user/usergroupadd_unsupported.go diff --git a/user/idtools.go b/user/idtools.go new file mode 100644 index 00000000..6a2b6060 --- /dev/null +++ b/user/idtools.go @@ -0,0 +1,207 @@ +package idtools + +import ( + "bufio" + "fmt" + "os" + "sort" + "strconv" + "strings" + + "github.com/docker/docker/pkg/system" +) + +// IDMap contains a single entry for user namespace range remapping. An array +// of IDMap entries represents the structure that will be provided to the Linux +// kernel for creating a user namespace. +type IDMap struct { + ContainerID int `json:"container_id"` + HostID int `json:"host_id"` + Size int `json:"size"` +} + +type subIDRange struct { + Start int + Length int +} + +type ranges []subIDRange + +func (e ranges) Len() int { return len(e) } +func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } +func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start } + +const ( + subuidFileName string = "/etc/subuid" + subgidFileName string = "/etc/subgid" +) + +// MkdirAllAs creates a directory (include any along the path) and then modifies +// ownership to the requested uid/gid. If the directory already exists, this +// function will still change ownership to the requested uid/gid pair. +func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, true) +} + +// MkdirAs creates a directory and then modifies ownership to the requested uid/gid. +// If the directory already exists, this function still changes ownership +func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, false) +} + +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { + if mkAll { + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + } else { + if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { + return err + } + } + // even if it existed, we will chown to change ownership as requested + if err := os.Chown(path, ownerUID, ownerGID); err != nil { + return err + } + return nil +} + +// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. +// If the maps are empty, then the root uid/gid will default to "real" 0/0 +func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { + var uid, gid int + + if uidMap != nil { + xUID, err := ToHost(0, uidMap) + if err != nil { + return -1, -1, err + } + uid = xUID + } + if gidMap != nil { + xGID, err := ToHost(0, gidMap) + if err != nil { + return -1, -1, err + } + gid = xGID + } + return uid, gid, nil +} + +// ToContainer takes an id mapping, and uses it to translate a +// host ID to the remapped ID. If no map is provided, then the translation +// assumes a 1-to-1 mapping and returns the passed in id +func ToContainer(hostID int, idMap []IDMap) (int, error) { + if idMap == nil { + return hostID, nil + } + for _, m := range idMap { + if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) { + contID := m.ContainerID + (hostID - m.HostID) + return contID, nil + } + } + return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) +} + +// ToHost takes an id mapping and a remapped ID, and translates the +// ID to the mapped host ID. If no map is provided, then the translation +// assumes a 1-to-1 mapping and returns the passed in id # +func ToHost(contID int, idMap []IDMap) (int, error) { + if idMap == nil { + return contID, nil + } + for _, m := range idMap { + if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) { + hostID := m.HostID + (contID - m.ContainerID) + return hostID, nil + } + } + return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) +} + +// CreateIDMappings takes a requested user and group name and +// using the data from /etc/sub{uid,gid} ranges, creates the +// proper uid and gid remapping ranges for that user/group pair +func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) { + subuidRanges, err := parseSubuid(username) + if err != nil { + return nil, nil, err + } + subgidRanges, err := parseSubgid(groupname) + if err != nil { + return nil, nil, err + } + if len(subuidRanges) == 0 { + return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username) + } + if len(subgidRanges) == 0 { + return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname) + } + + return createIDMap(subuidRanges), createIDMap(subgidRanges), nil +} + +func createIDMap(subidRanges ranges) []IDMap { + idMap := []IDMap{} + + // sort the ranges by lowest ID first + sort.Sort(subidRanges) + containerID := 0 + for _, idrange := range subidRanges { + idMap = append(idMap, IDMap{ + ContainerID: containerID, + HostID: idrange.Start, + Size: idrange.Length, + }) + containerID = containerID + idrange.Length + } + return idMap +} + +func parseSubuid(username string) (ranges, error) { + return parseSubidFile(subuidFileName, username) +} + +func parseSubgid(username string) (ranges, error) { + return parseSubidFile(subgidFileName, username) +} + +func parseSubidFile(path, username string) (ranges, error) { + var rangeList ranges + + subidFile, err := os.Open(path) + if err != nil { + return rangeList, err + } + defer subidFile.Close() + + s := bufio.NewScanner(subidFile) + for s.Scan() { + if err := s.Err(); err != nil { + return rangeList, err + } + + text := strings.TrimSpace(s.Text()) + if text == "" { + continue + } + parts := strings.Split(text, ":") + if len(parts) != 3 { + return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) + } + if parts[0] == username { + // return the first entry for a user; ignores potential for multiple ranges per user + startid, err := strconv.Atoi(parts[1]) + if err != nil { + return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) + } + length, err := strconv.Atoi(parts[2]) + if err != nil { + return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) + } + rangeList = append(rangeList, subIDRange{startid, length}) + } + } + return rangeList, nil +} diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go new file mode 100644 index 00000000..c1eedff1 --- /dev/null +++ b/user/usergroupadd_linux.go @@ -0,0 +1,155 @@ +package idtools + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +// add a user and/or group to Linux /etc/passwd, /etc/group using standard +// Linux distribution commands: +// adduser --uid --shell /bin/login --no-create-home --disabled-login --ingroup +// useradd -M -u -s /bin/nologin -N -g +// addgroup --gid +// groupadd -g + +const baseUID int = 10000 +const baseGID int = 10000 +const idMAX int = 65534 + +var ( + userCommand string + groupCommand string + + cmdTemplates = map[string]string{ + "adduser": "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s", + "useradd": "-M -u %d -s /bin/false -N -g %s %s", + "addgroup": "--gid %d %s", + "groupadd": "-g %d %s", + } +) + +func init() { + // set up which commands are used for adding users/groups dependent on distro + if _, err := resolveBinary("adduser"); err == nil { + userCommand = "adduser" + } else if _, err := resolveBinary("useradd"); err == nil { + userCommand = "useradd" + } + if _, err := resolveBinary("addgroup"); err == nil { + groupCommand = "addgroup" + } else if _, err := resolveBinary("groupadd"); err == nil { + groupCommand = "groupadd" + } +} + +func resolveBinary(binname string) (string, error) { + binaryPath, err := exec.LookPath(binname) + if err != nil { + return "", err + } + resolvedPath, err := filepath.EvalSymlinks(binaryPath) + if err != nil { + return "", err + } + //only return no error if the final resolved binary basename + //matches what was searched for + if filepath.Base(resolvedPath) == binname { + return resolvedPath, nil + } + return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) +} + +// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair +// and calls the appropriate helper function to add the group and then +// the user to the group in /etc/group and /etc/passwd respectively. +// This new user's /etc/sub{uid,gid} ranges will be used for user namespace +// mapping ranges in containers. +func AddNamespaceRangesUser(name string) (int, int, error) { + // Find unused uid, gid pair + uid, err := findUnusedUID(baseUID) + if err != nil { + return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err) + } + gid, err := findUnusedGID(baseGID) + if err != nil { + return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err) + } + + // First add the group that we will use + if err := addGroup(name, gid); err != nil { + return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err) + } + // Add the user as a member of the group + if err := addUser(name, uid, name); err != nil { + return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) + } + return uid, gid, nil +} + +func addUser(userName string, uid int, groupName string) error { + + if userCommand == "" { + return fmt.Errorf("Cannot add user; no useradd/adduser binary found") + } + args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName) + return execAddCmd(userCommand, args) +} + +func addGroup(groupName string, gid int) error { + + if groupCommand == "" { + return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found") + } + args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName) + // only error out if the error isn't that the group already exists + // if the group exists then our needs are already met + if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") { + return err + } + return nil +} + +func execAddCmd(cmd, args string) error { + execCmd := exec.Command(cmd, strings.Split(args, " ")...) + out, err := execCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out)) + } + return nil +} + +func findUnusedUID(startUID int) (int, error) { + return findUnused("passwd", startUID) +} + +func findUnusedGID(startGID int) (int, error) { + return findUnused("group", startGID) +} + +func findUnused(file string, id int) (int, error) { + for { + cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id) + cmd := exec.Command("sh", "-c", cmdStr) + if err := cmd.Run(); err != nil { + // if a non-zero return code occurs, then we know the ID was not found + // and is usable + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + if status.ExitStatus() == 1 { + //no match, we can use this ID + return id, nil + } + } + } + return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err) + } + id++ + if id > idMAX { + return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file) + } + } +} diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go new file mode 100644 index 00000000..d98b354c --- /dev/null +++ b/user/usergroupadd_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux + +package idtools + +import "fmt" + +// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair +// and calls the appropriate helper function to add the group and then +// the user to the group in /etc/group and /etc/passwd respectively. +func AddNamespaceRangesUser(name string) (int, int, error) { + return -1, -1, fmt.Errorf("No support for adding users or groups on this OS") +} From b39b044a3bc4683e86b1a68e09d642bb90f7e2cc Mon Sep 17 00:00:00 2001 From: John Howard Date: Mon, 12 Oct 2015 08:42:07 -0700 Subject: [PATCH 02/75] Windows: Daemon broken on master Signed-off-by: John Howard --- user/idtools.go | 19 ------------------- user/usergroupadd_linux.go | 20 ++++++++++++++++++++ user/usergroupadd_unsupported.go | 16 +++++++++++++++- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 6a2b6060..f6671403 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -7,8 +7,6 @@ import ( "sort" "strconv" "strings" - - "github.com/docker/docker/pkg/system" ) // IDMap contains a single entry for user namespace range remapping. An array @@ -49,23 +47,6 @@ func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, false) } -func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { - if mkAll { - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { - return err - } - } else { - if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { - return err - } - } - // even if it existed, we will chown to change ownership as requested - if err := os.Chown(path, ownerUID, ownerGID); err != nil { - return err - } - return nil -} - // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. // If the maps are empty, then the root uid/gid will default to "real" 0/0 func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index c1eedff1..c65fc336 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -2,10 +2,13 @@ package idtools import ( "fmt" + "os" "os/exec" "path/filepath" "strings" "syscall" + + "github.com/docker/docker/pkg/system" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard @@ -153,3 +156,20 @@ func findUnused(file string, id int) (int, error) { } } } + +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { + if mkAll { + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + } else { + if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { + return err + } + } + // even if it existed, we will chown to change ownership as requested + if err := os.Chown(path, ownerUID, ownerGID); err != nil { + return err + } + return nil +} diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go index d98b354c..2ec21fd6 100644 --- a/user/usergroupadd_unsupported.go +++ b/user/usergroupadd_unsupported.go @@ -2,7 +2,12 @@ package idtools -import "fmt" +import ( + "fmt" + "os" + + "github.com/docker/docker/pkg/system" +) // AddNamespaceRangesUser takes a name and finds an unused uid, gid pair // and calls the appropriate helper function to add the group and then @@ -10,3 +15,12 @@ import "fmt" func AddNamespaceRangesUser(name string) (int, int, error) { return -1, -1, fmt.Errorf("No support for adding users or groups on this OS") } + +// Platforms such as Windows do not support the UID/GID concept. So make this +// just a wrapper around system.MkdirAll. +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + return nil +} From 9b842bbd522ececb72bf0bbee32c829a5fa38f21 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 14 Oct 2015 14:35:48 -0400 Subject: [PATCH 03/75] Correct build-time directory creation with user namespaced daemon This fixes errors in ownership on directory creation during build that can cause inaccessible files depending on the paths in the Dockerfile and non-existing directories in the starting image. Add tests for the mkdir variants in pkg/idtools Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- user/idtools.go | 11 +- user/idtools_unix.go | 60 ++++++++ user/idtools_unix_test.go | 243 +++++++++++++++++++++++++++++++ user/idtools_windows.go | 18 +++ user/usergroupadd_linux.go | 20 --- user/usergroupadd_unsupported.go | 16 +- 6 files changed, 331 insertions(+), 37 deletions(-) create mode 100644 user/idtools_unix.go create mode 100644 user/idtools_unix_test.go create mode 100644 user/idtools_windows.go diff --git a/user/idtools.go b/user/idtools.go index f6671403..a1301ee9 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -38,13 +38,20 @@ const ( // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, true) + return mkdirAs(path, mode, ownerUID, ownerGID, true, true) +} + +// MkdirAllNewAs creates a directory (include any along the path) and then modifies +// ownership ONLY of newly created directories to the requested uid/gid. If the +// directories along the path exist, no change of ownership will be performed +func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { + return mkdirAs(path, mode, ownerUID, ownerGID, true, false) } // MkdirAs creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, false) + return mkdirAs(path, mode, ownerUID, ownerGID, false, true) } // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. diff --git a/user/idtools_unix.go b/user/idtools_unix.go new file mode 100644 index 00000000..b57d6ef1 --- /dev/null +++ b/user/idtools_unix.go @@ -0,0 +1,60 @@ +// +build !windows + +package idtools + +import ( + "os" + "path/filepath" + + "github.com/docker/docker/pkg/system" +) + +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { + // make an array containing the original path asked for, plus (for mkAll == true) + // all path components leading up to the complete path that don't exist before we MkdirAll + // so that we can chown all of them properly at the end. If chownExisting is false, we won't + // chown the full directory path if it exists + var paths []string + if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + paths = []string{path} + } else if err == nil && chownExisting { + if err := os.Chown(path, ownerUID, ownerGID); err != nil { + return err + } + // short-circuit--we were called with an existing directory and chown was requested + return nil + } else if err == nil { + // nothing to do; directory path fully exists already and chown was NOT requested + return nil + } + + if mkAll { + // walk back to "/" looking for directories which do not exist + // and add them to the paths array for chown after creation + dirPath := path + for { + dirPath = filepath.Dir(dirPath) + if dirPath == "/" { + break + } + if _, err := os.Stat(dirPath); err != nil && os.IsNotExist(err) { + paths = append(paths, dirPath) + } + } + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + } else { + if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { + return err + } + } + // even if it existed, we will chown the requested path + any subpaths that + // didn't exist when we called MkdirAll + for _, pathComponent := range paths { + if err := os.Chown(pathComponent, ownerUID, ownerGID); err != nil { + return err + } + } + return nil +} diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go new file mode 100644 index 00000000..55b338c9 --- /dev/null +++ b/user/idtools_unix_test.go @@ -0,0 +1,243 @@ +// +build !windows + +package idtools + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" +) + +type node struct { + uid int + gid int +} + +func TestMkdirAllAs(t *testing.T) { + dirName, err := ioutil.TempDir("", "mkdirall") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + "usr/bin": {0, 0}, + "lib": {33, 33}, + "lib/x86_64": {45, 45}, + "lib/x86_64/share": {1, 1}, + } + + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid + if err := MkdirAllAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr/share"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test 2-deep new directories--both should be owned by the uid/gid pair + if err := MkdirAllAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { + t.Fatal(err) + } + testTree["lib/some"] = node{101, 101} + testTree["lib/some/other"] = node{101, 101} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should be chowned, but nothing else + if err := MkdirAllAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + testTree["usr"] = node{102, 102} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func TestMkdirAllNewAs(t *testing.T) { + + dirName, err := ioutil.TempDir("", "mkdirnew") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + "usr/bin": {0, 0}, + "lib": {33, 33}, + "lib/x86_64": {45, 45}, + "lib/x86_64/share": {1, 1}, + } + + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid + if err := MkdirAllNewAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr/share"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test 2-deep new directories--both should be owned by the uid/gid pair + if err := MkdirAllNewAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { + t.Fatal(err) + } + testTree["lib/some"] = node{101, 101} + testTree["lib/some/other"] = node{101, 101} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should NOT be chowned + if err := MkdirAllNewAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func TestMkdirAs(t *testing.T) { + + dirName, err := ioutil.TempDir("", "mkdir") + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.RemoveAll(dirName) + + testTree := map[string]node{ + "usr": {0, 0}, + } + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } + + // test a directory that already exists; should just chown to the requested uid/gid + if err := MkdirAs(filepath.Join(dirName, "usr"), 0755, 99, 99); err != nil { + t.Fatal(err) + } + testTree["usr"] = node{99, 99} + verifyTree, err := readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } + + // create a subdir under a dir which doesn't exist--should fail + if err := MkdirAs(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, 102, 102); err == nil { + t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") + } + + // create a subdir under an existing dir; should only change the ownership of the new subdir + if err := MkdirAs(filepath.Join(dirName, "usr", "bin"), 0755, 102, 102); err != nil { + t.Fatal(err) + } + testTree["usr/bin"] = node{102, 102} + verifyTree, err = readTree(dirName, "") + if err != nil { + t.Fatal(err) + } + if err := compareTrees(testTree, verifyTree); err != nil { + t.Fatal(err) + } +} + +func buildTree(base string, tree map[string]node) error { + for path, node := range tree { + fullPath := filepath.Join(base, path) + if err := os.MkdirAll(fullPath, 0755); err != nil { + return fmt.Errorf("Couldn't create path: %s; error: %v", fullPath, err) + } + if err := os.Chown(fullPath, node.uid, node.gid); err != nil { + return fmt.Errorf("Couldn't chown path: %s; error: %v", fullPath, err) + } + } + return nil +} + +func readTree(base, root string) (map[string]node, error) { + tree := make(map[string]node) + + dirInfos, err := ioutil.ReadDir(base) + if err != nil { + return nil, fmt.Errorf("Couldn't read directory entries for %q: %v", base, err) + } + + for _, info := range dirInfos { + s := &syscall.Stat_t{} + if err := syscall.Stat(filepath.Join(base, info.Name()), s); err != nil { + return nil, fmt.Errorf("Can't stat file %q: %v", filepath.Join(base, info.Name()), err) + } + tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} + if info.IsDir() { + // read the subdirectory + subtree, err := readTree(filepath.Join(base, info.Name()), filepath.Join(root, info.Name())) + if err != nil { + return nil, err + } + for path, nodeinfo := range subtree { + tree[path] = nodeinfo + } + } + } + return tree, nil +} + +func compareTrees(left, right map[string]node) error { + if len(left) != len(right) { + return fmt.Errorf("Trees aren't the same size") + } + for path, nodeLeft := range left { + if nodeRight, ok := right[path]; ok { + if nodeRight.uid != nodeLeft.uid || nodeRight.gid != nodeLeft.gid { + // mismatch + return fmt.Errorf("mismatched ownership for %q: expected: %d:%d, got: %d:%d", path, + nodeLeft.uid, nodeLeft.gid, nodeRight.uid, nodeRight.gid) + } + continue + } + return fmt.Errorf("right tree didn't contain path %q", path) + } + return nil +} diff --git a/user/idtools_windows.go b/user/idtools_windows.go new file mode 100644 index 00000000..c9e3c937 --- /dev/null +++ b/user/idtools_windows.go @@ -0,0 +1,18 @@ +// +build windows + +package idtools + +import ( + "os" + + "github.com/docker/docker/pkg/system" +) + +// Platforms such as Windows do not support the UID/GID concept. So make this +// just a wrapper around system.MkdirAll. +func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { + if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + return err + } + return nil +} diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index c65fc336..c1eedff1 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -2,13 +2,10 @@ package idtools import ( "fmt" - "os" "os/exec" "path/filepath" "strings" "syscall" - - "github.com/docker/docker/pkg/system" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard @@ -156,20 +153,3 @@ func findUnused(file string, id int) (int, error) { } } } - -func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { - if mkAll { - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { - return err - } - } else { - if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { - return err - } - } - // even if it existed, we will chown to change ownership as requested - if err := os.Chown(path, ownerUID, ownerGID); err != nil { - return err - } - return nil -} diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go index 2ec21fd6..d98b354c 100644 --- a/user/usergroupadd_unsupported.go +++ b/user/usergroupadd_unsupported.go @@ -2,12 +2,7 @@ package idtools -import ( - "fmt" - "os" - - "github.com/docker/docker/pkg/system" -) +import "fmt" // AddNamespaceRangesUser takes a name and finds an unused uid, gid pair // and calls the appropriate helper function to add the group and then @@ -15,12 +10,3 @@ import ( func AddNamespaceRangesUser(name string) (int, int, error) { return -1, -1, fmt.Errorf("No support for adding users or groups on this OS") } - -// Platforms such as Windows do not support the UID/GID concept. So make this -// just a wrapper around system.MkdirAll. -func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll bool) error { - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { - return err - } - return nil -} From 2eab9477204002d673b583a0fa599c76a5fa8c4f Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 26 Feb 2016 14:49:43 +0100 Subject: [PATCH 04/75] pkg: idtools: fix subid files parsing Since Docker is already skipping newlines in /etc/sub{uid,gid}, this patch skips commented out lines - otherwise Docker fails to start. Add unit test also. Signed-off-by: Antonio Murdaca --- user/idtools.go | 2 +- user/idtools_unix_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/user/idtools.go b/user/idtools.go index a1301ee9..73416400 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -171,7 +171,7 @@ func parseSubidFile(path, username string) (ranges, error) { } text := strings.TrimSpace(s.Text()) - if text == "" { + if text == "" || strings.HasPrefix(text, "#") { continue } parts := strings.Split(text, ":") diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 55b338c9..540d3079 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -241,3 +241,31 @@ func compareTrees(left, right map[string]node) error { } return nil } + +func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "parsesubid") + if err != nil { + t.Fatal(err) + } + fnamePath := filepath.Join(tmpDir, "testsubuid") + fcontent := `tss:100000:65536 +# empty default subuid/subgid file + +dockremap:231072:65536` + if err := ioutil.WriteFile(fnamePath, []byte(fcontent), 0644); err != nil { + t.Fatal(err) + } + ranges, err := parseSubidFile(fnamePath, "dockremap") + if err != nil { + t.Fatal(err) + } + if len(ranges) != 1 { + t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges)) + } + if ranges[0].Start != 231072 { + t.Fatalf("wanted 231072, got %d instead", ranges[0].Start) + } + if ranges[0].Length != 65536 { + t.Fatalf("wanted 65536, got %d instead", ranges[0].Length) + } +} From 340c794aee2fd54aacc439cc2e98c9bd83b5e001 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 16 Mar 2016 18:44:10 -0400 Subject: [PATCH 05/75] Change subordinate range-owning user to be a system user Change user/group creation to use flags to adduser/useradd to enforce it being a system user. Use system user defaults that auto-create a matching group. These changes allow us to remove all group creation code, and in doing so we also removed the code that finds available uid, gid integers and use post-creation query to gather the system-generated uid and gid. The only added complexity is that today distros don't auto-create subordinate ID ranges for a new ID if it is a system ID, so we now need to handle finding a free range and then calling the `usermod` tool to add the ranges for that ID. Note that this requires the distro supports the `-v` and `-w` flags on `usermod` for subordinate ID range additions. Docker-DCO-1.1-Signed-off-by: Phil Estes (github: estesp) --- user/idtools.go | 6 +- user/usergroupadd_linux.go | 189 ++++++++++++++++++++++--------------- 2 files changed, 115 insertions(+), 80 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 73416400..6bca4662 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -155,6 +155,9 @@ func parseSubgid(username string) (ranges, error) { return parseSubidFile(subgidFileName, username) } +// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) +// and return all found ranges for a specified username. If the special value +// "ALL" is supplied for username, then all ranges in the file will be returned func parseSubidFile(path, username string) (ranges, error) { var rangeList ranges @@ -178,8 +181,7 @@ func parseSubidFile(path, username string) (ranges, error) { if len(parts) != 3 { return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) } - if parts[0] == username { - // return the first entry for a user; ignores potential for multiple ranges per user + if parts[0] == username || username == "ALL" { startid, err := strconv.Atoi(parts[1]) if err != nil { return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index c1eedff1..86d9e21e 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -4,31 +4,31 @@ import ( "fmt" "os/exec" "path/filepath" + "regexp" + "sort" + "strconv" "strings" - "syscall" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard // Linux distribution commands: -// adduser --uid --shell /bin/login --no-create-home --disabled-login --ingroup -// useradd -M -u -s /bin/nologin -N -g -// addgroup --gid -// groupadd -g - -const baseUID int = 10000 -const baseGID int = 10000 -const idMAX int = 65534 +// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group +// useradd -r -s /bin/false var ( - userCommand string - groupCommand string + userCommand string cmdTemplates = map[string]string{ - "adduser": "--uid %d --shell /bin/false --no-create-home --disabled-login --ingroup %s %s", - "useradd": "-M -u %d -s /bin/false -N -g %s %s", - "addgroup": "--gid %d %s", - "groupadd": "-g %d %s", + "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s", + "useradd": "-r -s /bin/false %s", + "usermod": "-%s %d-%d %s", } + + idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) + // default length for a UID/GID subordinate range + defaultRangeLen = 65536 + defaultRangeStart = 100000 + userMod = "usermod" ) func init() { @@ -38,11 +38,6 @@ func init() { } else if _, err := resolveBinary("useradd"); err == nil { userCommand = "useradd" } - if _, err := resolveBinary("addgroup"); err == nil { - groupCommand = "addgroup" - } else if _, err := resolveBinary("groupadd"); err == nil { - groupCommand = "groupadd" - } } func resolveBinary(binname string) (string, error) { @@ -62,94 +57,132 @@ func resolveBinary(binname string) (string, error) { return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) } -// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair -// and calls the appropriate helper function to add the group and then -// the user to the group in /etc/group and /etc/passwd respectively. -// This new user's /etc/sub{uid,gid} ranges will be used for user namespace +// AddNamespaceRangesUser takes a username and uses the standard system +// utility to create a system user/group pair used to hold the +// /etc/sub{uid,gid} ranges which will be used for user namespace // mapping ranges in containers. func AddNamespaceRangesUser(name string) (int, int, error) { - // Find unused uid, gid pair - uid, err := findUnusedUID(baseUID) + if err := addUser(name); err != nil { + return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) + } + + // Query the system for the created uid and gid pair + out, err := execCmd("id", name) if err != nil { - return -1, -1, fmt.Errorf("Unable to find unused UID: %v", err) + return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err) + } + matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) + if len(matches) != 3 { + return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out)) } - gid, err := findUnusedGID(baseGID) + uid, err := strconv.Atoi(matches[1]) if err != nil { - return -1, -1, fmt.Errorf("Unable to find unused GID: %v", err) + return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err) } - - // First add the group that we will use - if err := addGroup(name, gid); err != nil { - return -1, -1, fmt.Errorf("Error adding group %q: %v", name, err) + gid, err := strconv.Atoi(matches[2]) + if err != nil { + return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err) } - // Add the user as a member of the group - if err := addUser(name, uid, name); err != nil { - return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) + + // Now we need to create the subuid/subgid ranges for our new user/group (system users + // do not get auto-created ranges in subuid/subgid) + + if err := createSubordinateRanges(name); err != nil { + return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err) } return uid, gid, nil } -func addUser(userName string, uid int, groupName string) error { +func addUser(userName string) error { if userCommand == "" { return fmt.Errorf("Cannot add user; no useradd/adduser binary found") } - args := fmt.Sprintf(cmdTemplates[userCommand], uid, groupName, userName) - return execAddCmd(userCommand, args) + args := fmt.Sprintf(cmdTemplates[userCommand], userName) + out, err := execCmd(userCommand, args) + if err != nil { + return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out)) + } + return nil } -func addGroup(groupName string, gid int) error { +func createSubordinateRanges(name string) error { - if groupCommand == "" { - return fmt.Errorf("Cannot add group; no groupadd/addgroup binary found") + // first, we should verify that ranges weren't automatically created + // by the distro tooling + ranges, err := parseSubuid(name) + if err != nil { + return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err) } - args := fmt.Sprintf(cmdTemplates[groupCommand], gid, groupName) - // only error out if the error isn't that the group already exists - // if the group exists then our needs are already met - if err := execAddCmd(groupCommand, args); err != nil && !strings.Contains(err.Error(), "already exists") { - return err + if len(ranges) == 0 { + // no UID ranges; let's create one + startID, err := findNextUIDRange() + if err != nil { + return fmt.Errorf("Can't find available subuid range: %v", err) + } + out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name)) + if err != nil { + return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) + } } - return nil -} -func execAddCmd(cmd, args string) error { - execCmd := exec.Command(cmd, strings.Split(args, " ")...) - out, err := execCmd.CombinedOutput() + ranges, err = parseSubgid(name) if err != nil { - return fmt.Errorf("Failed to add user/group with error: %v; output: %q", err, string(out)) + return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err) + } + if len(ranges) == 0 { + // no GID ranges; let's create one + startID, err := findNextGIDRange() + if err != nil { + return fmt.Errorf("Can't find available subgid range: %v", err) + } + out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name)) + if err != nil { + return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) + } } return nil } -func findUnusedUID(startUID int) (int, error) { - return findUnused("passwd", startUID) +func findNextUIDRange() (int, error) { + ranges, err := parseSubuid("ALL") + if err != nil { + return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err) + } + sort.Sort(ranges) + return findNextRangeStart(ranges) } -func findUnusedGID(startGID int) (int, error) { - return findUnused("group", startGID) +func findNextGIDRange() (int, error) { + ranges, err := parseSubgid("ALL") + if err != nil { + return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err) + } + sort.Sort(ranges) + return findNextRangeStart(ranges) } -func findUnused(file string, id int) (int, error) { - for { - cmdStr := fmt.Sprintf("cat /etc/%s | cut -d: -f3 | grep '^%d$'", file, id) - cmd := exec.Command("sh", "-c", cmdStr) - if err := cmd.Run(); err != nil { - // if a non-zero return code occurs, then we know the ID was not found - // and is usable - if exiterr, ok := err.(*exec.ExitError); ok { - // The program has exited with an exit code != 0 - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - if status.ExitStatus() == 1 { - //no match, we can use this ID - return id, nil - } - } - } - return -1, fmt.Errorf("Error looking in /etc/%s for unused ID: %v", file, err) - } - id++ - if id > idMAX { - return -1, fmt.Errorf("Maximum id in %q reached with finding unused numeric ID", file) +func findNextRangeStart(rangeList ranges) (int, error) { + startID := defaultRangeStart + for _, arange := range rangeList { + if wouldOverlap(arange, startID) { + startID = arange.Start + arange.Length } } + return startID, nil +} + +func wouldOverlap(arange subIDRange, ID int) bool { + low := ID + high := ID + defaultRangeLen + if (low >= arange.Start && low <= arange.Start+arange.Length) || + (high <= arange.Start+arange.Length && high >= arange.Start) { + return true + } + return false +} + +func execCmd(cmd, args string) ([]byte, error) { + execCmd := exec.Command(cmd, strings.Split(args, " ")...) + return execCmd.CombinedOutput() } From f675a090fcc0ea4fad93e6e1837d2168b7b7cdb7 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Wed, 6 Apr 2016 17:24:17 -0400 Subject: [PATCH 06/75] Lazy init useradd and remove init() This should not have been in init() as it causes these lookups to happen in all reexecs of the Docker binary. The only time it needs to be resolved is when a user is added, which is extremely rare. Docker-DCO-1.1-Signed-off-by: Phil Estes --- user/usergroupadd_linux.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 86d9e21e..4a4aaed0 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "sync" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard @@ -16,6 +17,7 @@ import ( // useradd -r -s /bin/false var ( + once sync.Once userCommand string cmdTemplates = map[string]string{ @@ -31,15 +33,6 @@ var ( userMod = "usermod" ) -func init() { - // set up which commands are used for adding users/groups dependent on distro - if _, err := resolveBinary("adduser"); err == nil { - userCommand = "adduser" - } else if _, err := resolveBinary("useradd"); err == nil { - userCommand = "useradd" - } -} - func resolveBinary(binname string) (string, error) { binaryPath, err := exec.LookPath(binname) if err != nil { @@ -94,7 +87,14 @@ func AddNamespaceRangesUser(name string) (int, int, error) { } func addUser(userName string) error { - + once.Do(func() { + // set up which commands are used for adding users/groups dependent on distro + if _, err := resolveBinary("adduser"); err == nil { + userCommand = "adduser" + } else if _, err := resolveBinary("useradd"); err == nil { + userCommand = "useradd" + } + }) if userCommand == "" { return fmt.Errorf("Cannot add user; no useradd/adduser binary found") } From f67ec215d1671122b4138b05db589314afd14023 Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Tue, 23 Aug 2016 12:49:13 -0400 Subject: [PATCH 07/75] Don't start daemon in userns mode if graphdir inaccessible Warn the user and fail daemon start if the graphdir path has any elements which will deny access to the remapped root uid/gid. Docker-DCO-1.1-Signed-off-by: Phil Estes --- user/idtools_unix.go | 26 ++++++++++++++++++++++++++ user/idtools_windows.go | 7 +++++++ 2 files changed, 33 insertions(+) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index b57d6ef1..dcbb305f 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -58,3 +58,29 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown } return nil } + +// CanAccess takes a valid (existing) directory and a uid, gid pair and determines +// if that uid, gid pair has access (execute bit) to the directory +func CanAccess(path string, uid, gid int) bool { + statInfo, err := system.Stat(path) + if err != nil { + return false + } + fileMode := os.FileMode(statInfo.Mode()) + permBits := fileMode.Perm() + return accessible(statInfo.UID() == uint32(uid), + statInfo.GID() == uint32(gid), permBits) +} + +func accessible(isOwner, isGroup bool, perms os.FileMode) bool { + if isOwner && (perms&0100 == 0100) { + return true + } + if isGroup && (perms&0010 == 0010) { + return true + } + if perms&0001 == 0001 { + return true + } + return false +} diff --git a/user/idtools_windows.go b/user/idtools_windows.go index c9e3c937..49f67e78 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -16,3 +16,10 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown } return nil } + +// CanAccess takes a valid (existing) directory and a uid, gid pair and determines +// if that uid, gid pair has access (execute bit) to the directory +// Windows does not require/support this function, so always return true +func CanAccess(path string, uid, gid int) bool { + return true +} From 3afc0fa3ec1d28090432f77b28620593822e312c Mon Sep 17 00:00:00 2001 From: Phil Estes Date: Thu, 20 Oct 2016 14:43:42 -0500 Subject: [PATCH 08/75] Add support for looking up user/groups via `getent` When processing the --userns-remap flag, add the capability to call out to `getent` if the user and group information is not found via local file parsing code already in libcontainer/user. Signed-off-by: Phil Estes --- user/idtools_unix.go | 122 +++++++++++++++++++++++++++++++++++++ user/usergroupadd_linux.go | 24 -------- user/utils_unix.go | 32 ++++++++++ 3 files changed, 154 insertions(+), 24 deletions(-) create mode 100644 user/utils_unix.go diff --git a/user/idtools_unix.go b/user/idtools_unix.go index dcbb305f..a911163b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -3,10 +3,22 @@ package idtools import ( + "bytes" + "fmt" + "io" "os" "path/filepath" + "strings" + "sync" + "github.com/docker/docker/pkg/integration/cmd" "github.com/docker/docker/pkg/system" + "github.com/opencontainers/runc/libcontainer/user" +) + +var ( + entOnce sync.Once + getentCmd string ) func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { @@ -84,3 +96,113 @@ func accessible(isOwner, isGroup bool, perms os.FileMode) bool { } return false } + +// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username, +// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +func LookupUser(username string) (user.User, error) { + // first try a local system files lookup using existing capabilities + usr, err := user.LookupUser(username) + if err == nil { + return usr, nil + } + // local files lookup failed; attempt to call `getent` to query configured passwd dbs + usr, err = getentUser(fmt.Sprintf("%s %s", "passwd", username)) + if err != nil { + return user.User{}, err + } + return usr, nil +} + +// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid, +// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +func LookupUID(uid int) (user.User, error) { + // first try a local system files lookup using existing capabilities + usr, err := user.LookupUid(uid) + if err == nil { + return usr, nil + } + // local files lookup failed; attempt to call `getent` to query configured passwd dbs + return getentUser(fmt.Sprintf("%s %d", "passwd", uid)) +} + +func getentUser(args string) (user.User, error) { + reader, err := callGetent(args) + if err != nil { + return user.User{}, err + } + users, err := user.ParsePasswd(reader) + if err != nil { + return user.User{}, err + } + if len(users) == 0 { + return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", strings.Split(args, " ")[1]) + } + return users[0], nil +} + +// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name, +// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +func LookupGroup(groupname string) (user.Group, error) { + // first try a local system files lookup using existing capabilities + group, err := user.LookupGroup(groupname) + if err == nil { + return group, nil + } + // local files lookup failed; attempt to call `getent` to query configured group dbs + return getentGroup(fmt.Sprintf("%s %s", "group", groupname)) +} + +// LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID, +// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +func LookupGID(gid int) (user.Group, error) { + // first try a local system files lookup using existing capabilities + group, err := user.LookupGid(gid) + if err == nil { + return group, nil + } + // local files lookup failed; attempt to call `getent` to query configured group dbs + return getentGroup(fmt.Sprintf("%s %d", "group", gid)) +} + +func getentGroup(args string) (user.Group, error) { + reader, err := callGetent(args) + if err != nil { + return user.Group{}, err + } + groups, err := user.ParseGroup(reader) + if err != nil { + return user.Group{}, err + } + if len(groups) == 0 { + return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", strings.Split(args, " ")[1]) + } + return groups[0], nil +} + +func callGetent(args string) (io.Reader, error) { + entOnce.Do(func() { getentCmd, _ = resolveBinary("getent") }) + // if no `getent` command on host, can't do anything else + if getentCmd == "" { + return nil, fmt.Errorf("") + } + out, err := execCmd(getentCmd, args) + if err != nil { + exitCode, errC := cmd.GetExitCode(err) + if errC != nil { + return nil, err + } + switch exitCode { + case 1: + return nil, fmt.Errorf("getent reported invalid parameters/database unknown") + case 2: + terms := strings.Split(args, " ") + return nil, fmt.Errorf("getent unable to find entry %q in %s database", terms[1], terms[0]) + case 3: + return nil, fmt.Errorf("getent database doesn't support enumeration") + default: + return nil, err + } + + } + return bytes.NewReader(out), nil +} diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 4a4aaed0..9da7975e 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -2,8 +2,6 @@ package idtools import ( "fmt" - "os/exec" - "path/filepath" "regexp" "sort" "strconv" @@ -33,23 +31,6 @@ var ( userMod = "usermod" ) -func resolveBinary(binname string) (string, error) { - binaryPath, err := exec.LookPath(binname) - if err != nil { - return "", err - } - resolvedPath, err := filepath.EvalSymlinks(binaryPath) - if err != nil { - return "", err - } - //only return no error if the final resolved binary basename - //matches what was searched for - if filepath.Base(resolvedPath) == binname { - return resolvedPath, nil - } - return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) -} - // AddNamespaceRangesUser takes a username and uses the standard system // utility to create a system user/group pair used to hold the // /etc/sub{uid,gid} ranges which will be used for user namespace @@ -181,8 +162,3 @@ func wouldOverlap(arange subIDRange, ID int) bool { } return false } - -func execCmd(cmd, args string) ([]byte, error) { - execCmd := exec.Command(cmd, strings.Split(args, " ")...) - return execCmd.CombinedOutput() -} diff --git a/user/utils_unix.go b/user/utils_unix.go new file mode 100644 index 00000000..9703ecbd --- /dev/null +++ b/user/utils_unix.go @@ -0,0 +1,32 @@ +// +build !windows + +package idtools + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" +) + +func resolveBinary(binname string) (string, error) { + binaryPath, err := exec.LookPath(binname) + if err != nil { + return "", err + } + resolvedPath, err := filepath.EvalSymlinks(binaryPath) + if err != nil { + return "", err + } + //only return no error if the final resolved binary basename + //matches what was searched for + if filepath.Base(resolvedPath) == binname { + return resolvedPath, nil + } + return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) +} + +func execCmd(cmd, args string) ([]byte, error) { + execCmd := exec.Command(cmd, strings.Split(args, " ")...) + return execCmd.CombinedOutput() +} From 896159a58ca70c96b7b179ecb069b444d5068484 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 8 Nov 2016 17:21:02 +0100 Subject: [PATCH 09/75] Remove use of pkg/integration in pkg/idtools This remove a dependency on `go-check` (and more) when using `pkg/idtools`. `pkg/integration` should never be called from any other package then `integration`. Signed-off-by: Vincent Demeester --- user/idtools_unix.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index a911163b..f9eb31c3 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -11,7 +11,6 @@ import ( "strings" "sync" - "github.com/docker/docker/pkg/integration/cmd" "github.com/docker/docker/pkg/system" "github.com/opencontainers/runc/libcontainer/user" ) @@ -187,7 +186,7 @@ func callGetent(args string) (io.Reader, error) { } out, err := execCmd(getentCmd, args) if err != nil { - exitCode, errC := cmd.GetExitCode(err) + exitCode, errC := system.GetExitCode(err) if errC != nil { return nil, err } From f389d734cfbb0aa3c27e2aa41357b84fbb30188b Mon Sep 17 00:00:00 2001 From: unclejack Date: Tue, 13 Dec 2016 22:10:11 +0200 Subject: [PATCH 10/75] pkg: return directly without ifs where possible Signed-off-by: Cristian Staretu --- user/idtools_unix.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index f9eb31c3..7c7e82ae 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -29,11 +29,8 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { paths = []string{path} } else if err == nil && chownExisting { - if err := os.Chown(path, ownerUID, ownerGID); err != nil { - return err - } // short-circuit--we were called with an existing directory and chown was requested - return nil + return os.Chown(path, ownerUID, ownerGID) } else if err == nil { // nothing to do; directory path fully exists already and chown was NOT requested return nil From 8a9ffb363e91682ebb2187e4eeaa0b6aff35bd3d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 May 2017 18:06:46 -0400 Subject: [PATCH 11/75] Partial refactor of UID/GID usage to use a unified struct. Signed-off-by: Daniel Nephin --- user/idtools.go | 70 ++++++++++++++++++++++++++++++++++++----- user/idtools_unix.go | 6 ++-- user/idtools_windows.go | 2 +- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 6bca4662..d7a3a453 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -37,6 +37,7 @@ const ( // MkdirAllAs creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. +// Deprecated: Use MkdirAllAndChown func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, true, true) } @@ -44,16 +45,38 @@ func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { // MkdirAllNewAs creates a directory (include any along the path) and then modifies // ownership ONLY of newly created directories to the requested uid/gid. If the // directories along the path exist, no change of ownership will be performed +// Deprecated: Use MkdirAllAndChownNew func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, true, false) } // MkdirAs creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership +// Deprecated: Use MkdirAndChown with a IDPair func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, false, true) } +// MkdirAllAndChown creates a directory (include any along the path) and then modifies +// ownership to the requested uid/gid. If the directory already exists, this +// function will still change ownership to the requested uid/gid pair. +func MkdirAllAndChown(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, true, true) +} + +// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. +// If the directory already exists, this function still changes ownership +func MkdirAndChown(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, false, true) +} + +// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies +// ownership ONLY of newly created directories to the requested uid/gid. If the +// directories along the path exist, no change of ownership will be performed +func MkdirAllAndChownNew(path string, mode os.FileMode, ids IDPair) error { + return mkdirAs(path, mode, ids.UID, ids.GID, true, false) +} + // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. // If the maps are empty, then the root uid/gid will default to "real" 0/0 func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { @@ -108,26 +131,59 @@ func ToHost(contID int, idMap []IDMap) (int, error) { return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) } -// CreateIDMappings takes a requested user and group name and +// IDPair is a UID and GID pair +type IDPair struct { + UID int + GID int +} + +// IDMappings contains a mappings of UIDs and GIDs +type IDMappings struct { + uids []IDMap + gids []IDMap +} + +// NewIDMappings takes a requested user and group name and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair -func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) { +func NewIDMappings(username, groupname string) (*IDMappings, error) { subuidRanges, err := parseSubuid(username) if err != nil { - return nil, nil, err + return nil, err } subgidRanges, err := parseSubgid(groupname) if err != nil { - return nil, nil, err + return nil, err } if len(subuidRanges) == 0 { - return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username) + return nil, fmt.Errorf("No subuid ranges found for user %q", username) } if len(subgidRanges) == 0 { - return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname) + return nil, fmt.Errorf("No subgid ranges found for group %q", groupname) } - return createIDMap(subuidRanges), createIDMap(subgidRanges), nil + return &IDMappings{ + uids: createIDMap(subuidRanges), + gids: createIDMap(subgidRanges), + }, nil +} + +// RootPair returns a uid and gid pair for the root user +func (i *IDMappings) RootPair() (IDPair, error) { + uid, gid, err := GetRootUIDGID(i.uids, i.gids) + return IDPair{UID: uid, GID: gid}, err +} + +// UIDs return the UID mapping +// TODO: remove this once everything has been refactored to use pairs +func (i *IDMappings) UIDs() []IDMap { + return i.uids +} + +// GIDs return the UID mapping +// TODO: remove this once everything has been refactored to use pairs +func (i *IDMappings) GIDs() []IDMap { + return i.gids } func createIDMap(subidRanges ranges) []IDMap { diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 7c7e82ae..0b28249f 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -69,15 +69,15 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory -func CanAccess(path string, uid, gid int) bool { +func CanAccess(path string, pair IDPair) bool { statInfo, err := system.Stat(path) if err != nil { return false } fileMode := os.FileMode(statInfo.Mode()) permBits := fileMode.Perm() - return accessible(statInfo.UID() == uint32(uid), - statInfo.GID() == uint32(gid), permBits) + return accessible(statInfo.UID() == uint32(pair.UID), + statInfo.GID() == uint32(pair.GID), permBits) } func accessible(isOwner, isGroup bool, perms os.FileMode) bool { diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 49f67e78..8ed83530 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -20,6 +20,6 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory // Windows does not require/support this function, so always return true -func CanAccess(path string, uid, gid int) bool { +func CanAccess(path string, pair IDPair) bool { return true } From ef675ed1f1ca0cfef85529103e62151043a809a1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 24 May 2017 11:53:41 -0400 Subject: [PATCH 12/75] Remove unused functions from archive. Signed-off-by: Daniel Nephin --- user/idtools.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/user/idtools.go b/user/idtools.go index d7a3a453..39c9805d 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -118,6 +118,7 @@ func ToContainer(hostID int, idMap []IDMap) (int, error) { // ToHost takes an id mapping and a remapped ID, and translates the // ID to the mapped host ID. If no map is provided, then the translation // assumes a 1-to-1 mapping and returns the passed in id # +// Depercated: use IDMappings.UIDToHost and IDMappings.GIDToHost func ToHost(contID int, idMap []IDMap) (int, error) { if idMap == nil { return contID, nil @@ -174,6 +175,16 @@ func (i *IDMappings) RootPair() (IDPair, error) { return IDPair{UID: uid, GID: gid}, err } +// UIDToHost returns the host UID for the container uid +func (i *IDMappings) UIDToHost(uid int) (int, error) { + return ToHost(uid, i.uids) +} + +// GIDToHost returns the host GID for the container gid +func (i *IDMappings) GIDToHost(gid int) (int, error) { + return ToHost(gid, i.gids) +} + // UIDs return the UID mapping // TODO: remove this once everything has been refactored to use pairs func (i *IDMappings) UIDs() []IDMap { From a0486094a1efde0c6e22f8931478afa7f8ee1126 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 24 May 2017 14:10:15 -0400 Subject: [PATCH 13/75] Convert tarAppender to the newIDMappings. Signed-off-by: Daniel Nephin --- user/idtools.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 39c9805d..5488a8be 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -99,10 +99,10 @@ func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { return uid, gid, nil } -// ToContainer takes an id mapping, and uses it to translate a +// toContainer takes an id mapping, and uses it to translate a // host ID to the remapped ID. If no map is provided, then the translation // assumes a 1-to-1 mapping and returns the passed in id -func ToContainer(hostID int, idMap []IDMap) (int, error) { +func toContainer(hostID int, idMap []IDMap) (int, error) { if idMap == nil { return hostID, nil } @@ -169,6 +169,12 @@ func NewIDMappings(username, groupname string) (*IDMappings, error) { }, nil } +// NewIDMappingsFromMaps creates a new mapping from two slices +// Deprecated: this is a temporary shim while transitioning to IDMapping +func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IDMappings { + return &IDMappings{uids: uids, gids: gids} +} + // RootPair returns a uid and gid pair for the root user func (i *IDMappings) RootPair() (IDPair, error) { uid, gid, err := GetRootUIDGID(i.uids, i.gids) @@ -185,6 +191,21 @@ func (i *IDMappings) GIDToHost(gid int) (int, error) { return ToHost(gid, i.gids) } +// UIDToContainer returns the container UID for the host uid +func (i *IDMappings) UIDToContainer(uid int) (int, error) { + return toContainer(uid, i.uids) +} + +// GIDToContainer returns the container GID for the host gid +func (i *IDMappings) GIDToContainer(gid int) (int, error) { + return toContainer(gid, i.gids) +} + +// Empty returns true if there are no id mappings +func (i *IDMappings) Empty() bool { + return len(i.uids) == 0 && len(i.gids) == 0 +} + // UIDs return the UID mapping // TODO: remove this once everything has been refactored to use pairs func (i *IDMappings) UIDs() []IDMap { From da0b07ef8289098421f667e8c401a132f01257ba Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 31 May 2017 17:18:04 -0400 Subject: [PATCH 14/75] Remove ToHost and replace it with IDMappings.ToHost Signed-off-by: Daniel Nephin --- user/idtools.go | 64 +++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 5488a8be..cd5ca51d 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -80,21 +80,13 @@ func MkdirAllAndChownNew(path string, mode os.FileMode, ids IDPair) error { // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. // If the maps are empty, then the root uid/gid will default to "real" 0/0 func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { - var uid, gid int - - if uidMap != nil { - xUID, err := ToHost(0, uidMap) - if err != nil { - return -1, -1, err - } - uid = xUID + uid, err := toHost(0, uidMap) + if err != nil { + return -1, -1, err } - if gidMap != nil { - xGID, err := ToHost(0, gidMap) - if err != nil { - return -1, -1, err - } - gid = xGID + gid, err := toHost(0, gidMap) + if err != nil { + return -1, -1, err } return uid, gid, nil } @@ -115,11 +107,10 @@ func toContainer(hostID int, idMap []IDMap) (int, error) { return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) } -// ToHost takes an id mapping and a remapped ID, and translates the +// toHost takes an id mapping and a remapped ID, and translates the // ID to the mapped host ID. If no map is provided, then the translation // assumes a 1-to-1 mapping and returns the passed in id # -// Depercated: use IDMappings.UIDToHost and IDMappings.GIDToHost -func ToHost(contID int, idMap []IDMap) (int, error) { +func toHost(contID int, idMap []IDMap) (int, error) { if idMap == nil { return contID, nil } @@ -181,24 +172,35 @@ func (i *IDMappings) RootPair() (IDPair, error) { return IDPair{UID: uid, GID: gid}, err } -// UIDToHost returns the host UID for the container uid -func (i *IDMappings) UIDToHost(uid int) (int, error) { - return ToHost(uid, i.uids) -} +// ToHost returns the host UID and GID for the container uid, gid. +// Remapping is only performed if the ids aren't already the remapped root ids +func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) { + target, err := i.RootPair() + if err != nil { + return IDPair{}, err + } -// GIDToHost returns the host GID for the container gid -func (i *IDMappings) GIDToHost(gid int) (int, error) { - return ToHost(gid, i.gids) -} + if pair.UID != target.UID { + target.UID, err = toHost(pair.UID, i.uids) + if err != nil { + return target, err + } + } -// UIDToContainer returns the container UID for the host uid -func (i *IDMappings) UIDToContainer(uid int) (int, error) { - return toContainer(uid, i.uids) + if pair.GID != target.GID { + target.GID, err = toHost(pair.GID, i.gids) + } + return target, err } -// GIDToContainer returns the container GID for the host gid -func (i *IDMappings) GIDToContainer(gid int) (int, error) { - return toContainer(gid, i.gids) +// ToContainer returns the container UID and GID for the host uid and gid +func (i *IDMappings) ToContainer(pair IDPair) (int, int, error) { + uid, err := toContainer(pair.UID, i.uids) + if err != nil { + return -1, -1, err + } + gid, err := toContainer(pair.GID, i.gids) + return uid, gid, err } // Empty returns true if there are no id mappings From 9124b4566e4bfef5a3b59974ee31870f70fbd22e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 31 May 2017 17:36:48 -0400 Subject: [PATCH 15/75] Remove MkdirAllNewAs and update tests. Signed-off-by: Daniel Nephin --- user/idtools.go | 8 ------ user/idtools_unix_test.go | 54 +++++++++++++-------------------------- 2 files changed, 18 insertions(+), 44 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index cd5ca51d..36cd688c 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -42,14 +42,6 @@ func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { return mkdirAs(path, mode, ownerUID, ownerGID, true, true) } -// MkdirAllNewAs creates a directory (include any along the path) and then modifies -// ownership ONLY of newly created directories to the requested uid/gid. If the -// directories along the path exist, no change of ownership will be performed -// Deprecated: Use MkdirAllAndChownNew -func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, true, false) -} - // MkdirAs creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership // Deprecated: Use MkdirAndChown with a IDPair diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 540d3079..31522a54 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -9,6 +9,8 @@ import ( "path/filepath" "syscall" "testing" + + "github.com/stretchr/testify/require" ) type node struct { @@ -76,12 +78,9 @@ func TestMkdirAllAs(t *testing.T) { } } -func TestMkdirAllNewAs(t *testing.T) { - +func TestMkdirAllAndChownNew(t *testing.T) { dirName, err := ioutil.TempDir("", "mkdirnew") - if err != nil { - t.Fatalf("Couldn't create temp dir: %v", err) - } + require.NoError(t, err) defer os.RemoveAll(dirName) testTree := map[string]node{ @@ -91,49 +90,32 @@ func TestMkdirAllNewAs(t *testing.T) { "lib/x86_64": {45, 45}, "lib/x86_64/share": {1, 1}, } - - if err := buildTree(dirName, testTree); err != nil { - t.Fatal(err) - } + require.NoError(t, buildTree(dirName, testTree)) // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - if err := MkdirAllNewAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { - t.Fatal(err) - } + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, IDPair{99, 99}) + require.NoError(t, err) + testTree["usr/share"] = node{99, 99} verifyTree, err := readTree(dirName, "") - if err != nil { - t.Fatal(err) - } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.NoError(t, compareTrees(testTree, verifyTree)) // test 2-deep new directories--both should be owned by the uid/gid pair - if err := MkdirAllNewAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { - t.Fatal(err) - } + err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{101, 101}) + require.NoError(t, err) testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} verifyTree, err = readTree(dirName, "") - if err != nil { - t.Fatal(err) - } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.NoError(t, compareTrees(testTree, verifyTree)) // test a directory that already exists; should NOT be chowned - if err := MkdirAllNewAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { - t.Fatal(err) - } + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, IDPair{102, 102}) + require.NoError(t, err) verifyTree, err = readTree(dirName, "") - if err != nil { - t.Fatal(err) - } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.NoError(t, compareTrees(testTree, verifyTree)) } func TestMkdirAs(t *testing.T) { From 013fefa8706c4f0c0ba5af06551f92e23f381ffb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 31 May 2017 17:56:23 -0400 Subject: [PATCH 16/75] Remove error return from RootPair There is no case which would resolve in this error. The root user always exists, and if the id maps are empty, the default value of 0 is correct. Signed-off-by: Daniel Nephin --- user/idtools.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 36cd688c..68a072db 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -158,19 +158,19 @@ func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IDMappings { return &IDMappings{uids: uids, gids: gids} } -// RootPair returns a uid and gid pair for the root user -func (i *IDMappings) RootPair() (IDPair, error) { - uid, gid, err := GetRootUIDGID(i.uids, i.gids) - return IDPair{UID: uid, GID: gid}, err +// RootPair returns a uid and gid pair for the root user. The error is ignored +// because a root user always exists, and the defaults are correct when the uid +// and gid maps are empty. +func (i *IDMappings) RootPair() IDPair { + uid, gid, _ := GetRootUIDGID(i.uids, i.gids) + return IDPair{UID: uid, GID: gid} } // ToHost returns the host UID and GID for the container uid, gid. // Remapping is only performed if the ids aren't already the remapped root ids func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) { - target, err := i.RootPair() - if err != nil { - return IDPair{}, err - } + var err error + target := i.RootPair() if pair.UID != target.UID { target.UID, err = toHost(pair.UID, i.uids) From 8a07e96f3fbcea354c1b4eae06d3d789d480d883 Mon Sep 17 00:00:00 2001 From: John Howard Date: Thu, 1 Jun 2017 18:59:11 -0700 Subject: [PATCH 17/75] LCOW: Create layer folders with correct ACL Signed-off-by: John Howard --- user/idtools_unix.go | 2 +- user/idtools_windows.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 0b28249f..8701bb7f 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -49,7 +49,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown paths = append(paths, dirPath) } } - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { return err } } else { diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 8ed83530..45d2878e 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -11,7 +11,7 @@ import ( // Platforms such as Windows do not support the UID/GID concept. So make this // just a wrapper around system.MkdirAll. func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { return err } return nil From d1b011c6d8a3f6e160dc783298eba4e82302ccf8 Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 27 Jul 2017 09:51:23 +0200 Subject: [PATCH 18/75] Switch Stat syscalls to x/sys/unix Switch some more usage of the Stat function and the Stat_t type from the syscall package to golang.org/x/sys. Those were missing in PR #33399. Signed-off-by: Tobias Klauser --- user/idtools_unix_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 31522a54..2463342a 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -7,10 +7,10 @@ import ( "io/ioutil" "os" "path/filepath" - "syscall" "testing" "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" ) type node struct { @@ -187,8 +187,8 @@ func readTree(base, root string) (map[string]node, error) { } for _, info := range dirInfos { - s := &syscall.Stat_t{} - if err := syscall.Stat(filepath.Join(base, info.Name()), s); err != nil { + s := &unix.Stat_t{} + if err := unix.Stat(filepath.Join(base, info.Name()), s); err != nil { return nil, fmt.Errorf("Can't stat file %q: %v", filepath.Join(base, info.Name()), err) } tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} From df681c487b30d4cf6469124698e6d5a6701e71e5 Mon Sep 17 00:00:00 2001 From: Danyal Khaliq Date: Tue, 26 Sep 2017 12:01:08 +0500 Subject: [PATCH 19/75] Increase Coverage of pkg/idtools Signed-off-by: Danyal Khaliq --- user/idtools_unix_test.go | 132 +++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 2463342a..6d3b591e 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -6,19 +6,27 @@ import ( "fmt" "io/ioutil" "os" + "os/user" "path/filepath" "testing" + "github.com/gotestyourself/gotestyourself/skip" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) +const ( + tempUser = "tempuser" +) + type node struct { uid int gid int } func TestMkdirAllAs(t *testing.T) { + RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdirall") if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) @@ -79,6 +87,7 @@ func TestMkdirAllAs(t *testing.T) { } func TestMkdirAllAndChownNew(t *testing.T) { + RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdirnew") require.NoError(t, err) defer os.RemoveAll(dirName) @@ -119,7 +128,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { } func TestMkdirAs(t *testing.T) { - + RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdir") if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) @@ -224,6 +233,11 @@ func compareTrees(left, right map[string]node) error { return nil } +func delUser(t *testing.T, name string) { + _, err := execCmd("userdel", name) + assert.NoError(t, err) +} + func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { tmpDir, err := ioutil.TempDir("", "parsesubid") if err != nil { @@ -251,3 +265,119 @@ dockremap:231072:65536` t.Fatalf("wanted 65536, got %d instead", ranges[0].Length) } } + +func TestGetRootUIDGID(t *testing.T) { + uidMap := []IDMap{ + { + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }, + } + gidMap := []IDMap{ + { + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }, + } + + uid, gid, err := GetRootUIDGID(uidMap, gidMap) + assert.NoError(t, err) + assert.Equal(t, os.Getegid(), uid) + assert.Equal(t, os.Getegid(), gid) + + uidMapError := []IDMap{ + { + ContainerID: 1, + HostID: os.Getuid(), + Size: 1, + }, + } + _, _, err = GetRootUIDGID(uidMapError, gidMap) + assert.EqualError(t, err, "Container ID 0 cannot be mapped to a host ID") +} + +func TestToContainer(t *testing.T) { + uidMap := []IDMap{ + { + ContainerID: 2, + HostID: 2, + Size: 1, + }, + } + + containerID, err := toContainer(2, uidMap) + assert.NoError(t, err) + assert.Equal(t, uidMap[0].ContainerID, containerID) +} + +func TestNewIDMappings(t *testing.T) { + RequiresRoot(t) + _, _, err := AddNamespaceRangesUser(tempUser) + assert.NoError(t, err) + defer delUser(t, tempUser) + + tempUser, err := user.Lookup(tempUser) + assert.NoError(t, err) + + gids, err := tempUser.GroupIds() + assert.NoError(t, err) + group, err := user.LookupGroupId(string(gids[0])) + assert.NoError(t, err) + + idMappings, err := NewIDMappings(tempUser.Username, group.Name) + assert.NoError(t, err) + + rootUID, rootGID, err := GetRootUIDGID(idMappings.UIDs(), idMappings.GIDs()) + assert.NoError(t, err) + + dirName, err := ioutil.TempDir("", "mkdirall") + assert.NoError(t, err, "Couldn't create temp directory") + defer os.RemoveAll(dirName) + + err = MkdirAllAs(dirName, 0700, rootUID, rootGID) + assert.NoError(t, err, "Couldn't change ownership of file path. Got error") + assert.True(t, CanAccess(dirName, idMappings.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) +} + +func TestLookupUserAndGroup(t *testing.T) { + RequiresRoot(t) + uid, gid, err := AddNamespaceRangesUser(tempUser) + assert.NoError(t, err) + defer delUser(t, tempUser) + + fetchedUser, err := LookupUser(tempUser) + assert.NoError(t, err) + + fetchedUserByID, err := LookupUID(uid) + assert.NoError(t, err) + assert.Equal(t, fetchedUserByID, fetchedUser) + + fetchedGroup, err := LookupGroup(tempUser) + assert.NoError(t, err) + + fetchedGroupByID, err := LookupGID(gid) + assert.NoError(t, err) + assert.Equal(t, fetchedGroupByID, fetchedGroup) +} + +func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { + fakeUser := "fakeuser" + _, err := LookupUser(fakeUser) + assert.EqualError(t, err, "getent unable to find entry \""+fakeUser+"\" in passwd database") + + _, err = LookupUID(-1) + assert.Error(t, err) + + fakeGroup := "fakegroup" + _, err = LookupGroup(fakeGroup) + assert.EqualError(t, err, "getent unable to find entry \""+fakeGroup+"\" in group database") + + _, err = LookupGID(-1) + assert.Error(t, err) +} + +func RequiresRoot(t *testing.T) { + skip.IfCondition(t, os.Getuid() != 0, "skipping test that requires root") +} From 96887eead31ca6534a1b766003272a311f6d4435 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 2 Oct 2017 09:47:09 -0400 Subject: [PATCH 20/75] idtools don't chown if not needed In some cases (e.g. NFS), a chown may technically be a no-op but still return `EPERM`, so only call `chown` when neccessary. This is particularly problematic for docker users bind-mounting an NFS share into a container. Signed-off-by: Brian Goff --- user/idtools_unix.go | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 8701bb7f..ff7968f8 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -26,14 +26,19 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // so that we can chown all of them properly at the end. If chownExisting is false, we won't // chown the full directory path if it exists var paths []string - if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { - paths = []string{path} - } else if err == nil && chownExisting { + + stat, err := system.Stat(path) + if err == nil { + if !chownExisting { + return nil + } + // short-circuit--we were called with an existing directory and chown was requested - return os.Chown(path, ownerUID, ownerGID) - } else if err == nil { - // nothing to do; directory path fully exists already and chown was NOT requested - return nil + return lazyChown(path, ownerUID, ownerGID, stat) + } + + if os.IsNotExist(err) { + paths = []string{path} } if mkAll { @@ -60,7 +65,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll for _, pathComponent := range paths { - if err := os.Chown(pathComponent, ownerUID, ownerGID); err != nil { + if err := lazyChown(pathComponent, ownerUID, ownerGID, nil); err != nil { return err } } @@ -202,3 +207,20 @@ func callGetent(args string) (io.Reader, error) { } return bytes.NewReader(out), nil } + +// lazyChown performs a chown only if the uid/gid don't match what's requested +// Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the +// dir is on an NFS share, so don't call chown unless we absolutely must. +func lazyChown(p string, uid, gid int, stat *system.StatT) error { + if stat == nil { + var err error + stat, err = system.Stat(p) + if err != nil { + return err + } + } + if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) { + return nil + } + return os.Chown(p, uid, gid) +} From b7d71bff0244a38cccafd7c975c0ead603ec1929 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 19 Nov 2017 00:41:09 +0100 Subject: [PATCH 21/75] Update idtools tests to test non-deprecated functions Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix_test.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 6d3b591e..afefdb34 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -25,7 +25,7 @@ type node struct { gid int } -func TestMkdirAllAs(t *testing.T) { +func TestMkdirAllAndChown(t *testing.T) { RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdirall") if err != nil { @@ -46,7 +46,7 @@ func TestMkdirAllAs(t *testing.T) { } // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - if err := MkdirAllAs(filepath.Join(dirName, "usr", "share"), 0755, 99, 99); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0755, IDPair{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr/share"] = node{99, 99} @@ -59,7 +59,7 @@ func TestMkdirAllAs(t *testing.T) { } // test 2-deep new directories--both should be owned by the uid/gid pair - if err := MkdirAllAs(filepath.Join(dirName, "lib", "some", "other"), 0755, 101, 101); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{UID: 101, GID: 101}); err != nil { t.Fatal(err) } testTree["lib/some"] = node{101, 101} @@ -73,7 +73,7 @@ func TestMkdirAllAs(t *testing.T) { } // test a directory that already exists; should be chowned, but nothing else - if err := MkdirAllAs(filepath.Join(dirName, "usr"), 0755, 102, 102); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr"] = node{102, 102} @@ -102,7 +102,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { require.NoError(t, buildTree(dirName, testTree)) // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, IDPair{99, 99}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, IDPair{UID: 99, GID: 99}) require.NoError(t, err) testTree["usr/share"] = node{99, 99} @@ -111,7 +111,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { require.NoError(t, compareTrees(testTree, verifyTree)) // test 2-deep new directories--both should be owned by the uid/gid pair - err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{101, 101}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{UID: 101, GID: 101}) require.NoError(t, err) testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} @@ -120,14 +120,14 @@ func TestMkdirAllAndChownNew(t *testing.T) { require.NoError(t, compareTrees(testTree, verifyTree)) // test a directory that already exists; should NOT be chowned - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, IDPair{102, 102}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 102, GID: 102}) require.NoError(t, err) verifyTree, err = readTree(dirName, "") require.NoError(t, err) require.NoError(t, compareTrees(testTree, verifyTree)) } -func TestMkdirAs(t *testing.T) { +func TestMkdirAndChown(t *testing.T) { RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdir") if err != nil { @@ -143,7 +143,7 @@ func TestMkdirAs(t *testing.T) { } // test a directory that already exists; should just chown to the requested uid/gid - if err := MkdirAs(filepath.Join(dirName, "usr"), 0755, 99, 99); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr"] = node{99, 99} @@ -156,12 +156,12 @@ func TestMkdirAs(t *testing.T) { } // create a subdir under a dir which doesn't exist--should fail - if err := MkdirAs(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, 102, 102); err == nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, IDPair{UID: 102, GID: 102}); err == nil { t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") } // create a subdir under an existing dir; should only change the ownership of the new subdir - if err := MkdirAs(filepath.Join(dirName, "usr", "bin"), 0755, 102, 102); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0755, IDPair{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr/bin"] = node{102, 102} @@ -336,7 +336,7 @@ func TestNewIDMappings(t *testing.T) { assert.NoError(t, err, "Couldn't create temp directory") defer os.RemoveAll(dirName) - err = MkdirAllAs(dirName, 0700, rootUID, rootGID) + err = MkdirAllAndChown(dirName, 0700, IDPair{UID: rootUID, GID: rootGID}) assert.NoError(t, err, "Couldn't change ownership of file path. Got error") assert.True(t, CanAccess(dirName, idMappings.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) } From 5ac3c73076cf782469253d891a0099956f2a03af Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 19 Nov 2017 00:49:17 +0100 Subject: [PATCH 22/75] Minor refactor in idtools Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 68a072db..c905a647 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -52,21 +52,21 @@ func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { // MkdirAllAndChown creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. -func MkdirAllAndChown(path string, mode os.FileMode, ids IDPair) error { - return mkdirAs(path, mode, ids.UID, ids.GID, true, true) +func MkdirAllAndChown(path string, mode os.FileMode, owner IDPair) error { + return mkdirAs(path, mode, owner.UID, owner.GID, true, true) } // MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership -func MkdirAndChown(path string, mode os.FileMode, ids IDPair) error { - return mkdirAs(path, mode, ids.UID, ids.GID, false, true) +func MkdirAndChown(path string, mode os.FileMode, owner IDPair) error { + return mkdirAs(path, mode, owner.UID, owner.GID, false, true) } // MkdirAllAndChownNew creates a directory (include any along the path) and then modifies // ownership ONLY of newly created directories to the requested uid/gid. If the // directories along the path exist, no change of ownership will be performed -func MkdirAllAndChownNew(path string, mode os.FileMode, ids IDPair) error { - return mkdirAs(path, mode, ids.UID, ids.GID, true, false) +func MkdirAllAndChownNew(path string, mode os.FileMode, owner IDPair) error { + return mkdirAs(path, mode, owner.UID, owner.GID, true, false) } // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. From 111ed2cfcd779884b9cc3ad6e5aff5df292e599a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 19 Nov 2017 00:52:47 +0100 Subject: [PATCH 23/75] Remove deprecated MkdirAllAs(), MkdirAs() Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index c905a647..49cc97c3 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -34,21 +34,6 @@ const ( subgidFileName string = "/etc/subgid" ) -// MkdirAllAs creates a directory (include any along the path) and then modifies -// ownership to the requested uid/gid. If the directory already exists, this -// function will still change ownership to the requested uid/gid pair. -// Deprecated: Use MkdirAllAndChown -func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, true, true) -} - -// MkdirAs creates a directory and then modifies ownership to the requested uid/gid. -// If the directory already exists, this function still changes ownership -// Deprecated: Use MkdirAndChown with a IDPair -func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error { - return mkdirAs(path, mode, ownerUID, ownerGID, false, true) -} - // MkdirAllAndChown creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. From db7d60ce9d0a6a59e6ec457d153e254df8c3fb45 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 27 Nov 2017 13:11:11 -0800 Subject: [PATCH 24/75] idtools.MkdirAs*: error out if dir exists as file Standard golang's `os.MkdirAll()` function returns "not a directory" error in case a directory to be created already exists but is not a directory (e.g. a file). Our own `idtools.MkdirAs*()` functions do not replicate the behavior. This is a bug since all `Mkdir()`-like functions are expected to ensure the required directory exists and is indeed a directory, and return an error otherwise. As the code is using our in-house `system.Stat()` call returning a type which is incompatible with that of golang's `os.Stat()`, I had to amend the `system` package with `IsDir()`. A test case is also provided. Signed-off-by: Kir Kolyshkin --- user/idtools_unix.go | 4 ++++ user/idtools_unix_test.go | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index ff7968f8..b5e9c840 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "sync" + "syscall" "github.com/docker/docker/pkg/system" "github.com/opencontainers/runc/libcontainer/user" @@ -29,6 +30,9 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown stat, err := system.Stat(path) if err == nil { + if !stat.IsDir() { + return &os.PathError{"mkdir", path, syscall.ENOTDIR} + } if !chownExisting { return nil } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index afefdb34..396ff202 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -378,6 +378,20 @@ func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { assert.Error(t, err) } +// TestMkdirIsNotDir checks that mkdirAs() function (used by MkdirAll...) +// returns a correct error in case a directory which it is about to create +// already exists but is a file (rather than a directory). +func TestMkdirIsNotDir(t *testing.T) { + file, err := ioutil.TempFile("", t.Name()) + if err != nil { + t.Fatalf("Couldn't create temp dir: %v", err) + } + defer os.Remove(file.Name()) + + err = mkdirAs(file.Name(), 0755, 0, 0, false, false) + assert.EqualError(t, err, "mkdir "+file.Name()+": not a directory") +} + func RequiresRoot(t *testing.T) { skip.IfCondition(t, os.Getuid() != 0, "skipping test that requires root") } From fd60e086b3536a4d8bc58ee829f1b4800299f21a Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Mon, 25 Sep 2017 12:39:36 -0700 Subject: [PATCH 25/75] Simplify/fix MkdirAll usage This subtle bug keeps lurking in because error checking for `Mkdir()` and `MkdirAll()` is slightly different wrt to `EEXIST`/`IsExist`: - for `Mkdir()`, `IsExist` error should (usually) be ignored (unless you want to make sure directory was not there before) as it means "the destination directory was already there" - for `MkdirAll()`, `IsExist` error should NEVER be ignored. Mostly, this commit just removes ignoring the IsExist error, as it should not be ignored. Also, there are a couple of cases then IsExist is handled as "directory already exist" which is wrong. As a result, some code that never worked as intended is now removed. NOTE that `idtools.MkdirAndChown()` behaves like `os.MkdirAll()` rather than `os.Mkdir()` -- so its description is amended accordingly, and its usage is handled as such (i.e. IsExist error is not ignored). For more details, a quote from my runc commit 6f82d4b (July 2015): TL;DR: check for IsExist(err) after a failed MkdirAll() is both redundant and wrong -- so two reasons to remove it. Quoting MkdirAll documentation: > MkdirAll creates a directory named path, along with any necessary > parents, and returns nil, or else returns an error. If path > is already a directory, MkdirAll does nothing and returns nil. This means two things: 1. If a directory to be created already exists, no error is returned. 2. If the error returned is IsExist (EEXIST), it means there exists a non-directory with the same name as MkdirAll need to use for directory. Example: we want to MkdirAll("a/b"), but file "a" (or "a/b") already exists, so MkdirAll fails. The above is a theory, based on quoted documentation and my UNIX knowledge. 3. In practice, though, current MkdirAll implementation [1] returns ENOTDIR in most of cases described in #2, with the exception when there is a race between MkdirAll and someone else creating the last component of MkdirAll argument as a file. In this very case MkdirAll() will indeed return EEXIST. Because of #1, IsExist check after MkdirAll is not needed. Because of #2 and #3, ignoring IsExist error is just plain wrong, as directory we require is not created. It's cleaner to report the error now. Note this error is all over the tree, I guess due to copy-paste, or trying to follow the same usage pattern as for Mkdir(), or some not quite correct examples on the Internet. [1] https://github.com/golang/go/blob/f9ed2f75/src/os/path.go Signed-off-by: Kir Kolyshkin --- user/idtools.go | 4 +++- user/idtools_unix.go | 4 ++-- user/idtools_windows.go | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 49cc97c3..6108ae3f 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -42,7 +42,9 @@ func MkdirAllAndChown(path string, mode os.FileMode, owner IDPair) error { } // MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. -// If the directory already exists, this function still changes ownership +// If the directory already exists, this function still changes ownership. +// Note that unlike os.Mkdir(), this function does not return IsExist error +// in case path already exists. func MkdirAndChown(path string, mode os.FileMode, owner IDPair) error { return mkdirAs(path, mode, owner.UID, owner.GID, false, true) } diff --git a/user/idtools_unix.go b/user/idtools_unix.go index b5e9c840..aedf8ad3 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -31,7 +31,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown stat, err := system.Stat(path) if err == nil { if !stat.IsDir() { - return &os.PathError{"mkdir", path, syscall.ENOTDIR} + return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} } if !chownExisting { return nil @@ -58,7 +58,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown paths = append(paths, dirPath) } } - if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil { return err } } else { diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 45d2878e..94ca33af 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -11,7 +11,7 @@ import ( // Platforms such as Windows do not support the UID/GID concept. So make this // just a wrapper around system.MkdirAll. func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { - if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil { return err } return nil From 400e461c4fdafe3c429d29cb75474687e8727ed9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 18 Dec 2017 17:41:53 +0100 Subject: [PATCH 26/75] Remove redundant build-tags Files that are suffixed with `_linux.go` or `_windows.go` are already only built on Linux / Windows, so these build-tags were redundant. Signed-off-by: Sebastiaan van Stijn --- user/idtools_windows.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 94ca33af..ec491777 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -1,5 +1,3 @@ -// +build windows - package idtools import ( From 20e76a8fe6d8929251d0e58e13d58837b71c253d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 5 Feb 2018 16:05:59 -0500 Subject: [PATCH 27/75] Add canonical import comment Signed-off-by: Daniel Nephin --- user/idtools.go | 2 +- user/idtools_unix.go | 2 +- user/idtools_unix_test.go | 2 +- user/idtools_windows.go | 2 +- user/usergroupadd_linux.go | 2 +- user/usergroupadd_unsupported.go | 2 +- user/utils_unix.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 6108ae3f..e2b49315 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -1,4 +1,4 @@ -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "bufio" diff --git a/user/idtools_unix.go b/user/idtools_unix.go index aedf8ad3..1d87ea3b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -1,6 +1,6 @@ // +build !windows -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "bytes" diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 396ff202..931e332b 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -1,6 +1,6 @@ // +build !windows -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" diff --git a/user/idtools_windows.go b/user/idtools_windows.go index ec491777..d72cc289 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -1,4 +1,4 @@ -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "os" diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 9da7975e..6272c5a4 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -1,4 +1,4 @@ -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go index d98b354c..e7c4d631 100644 --- a/user/usergroupadd_unsupported.go +++ b/user/usergroupadd_unsupported.go @@ -1,6 +1,6 @@ // +build !linux -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import "fmt" diff --git a/user/utils_unix.go b/user/utils_unix.go index 9703ecbd..903ac450 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -1,6 +1,6 @@ // +build !windows -package idtools +package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" From b286fb1656d0c4f575340c17cff7639ebcdabde4 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 13 Mar 2018 15:28:34 -0400 Subject: [PATCH 28/75] Automated migration using gty-migrate-from-testify --ignore-build-tags Signed-off-by: Daniel Nephin --- user/idtools_unix_test.go | 82 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 931e332b..e493b9e8 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -10,9 +10,9 @@ import ( "path/filepath" "testing" + "github.com/gotestyourself/gotestyourself/assert" + is "github.com/gotestyourself/gotestyourself/assert/cmp" "github.com/gotestyourself/gotestyourself/skip" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "golang.org/x/sys/unix" ) @@ -89,7 +89,7 @@ func TestMkdirAllAndChown(t *testing.T) { func TestMkdirAllAndChownNew(t *testing.T) { RequiresRoot(t) dirName, err := ioutil.TempDir("", "mkdirnew") - require.NoError(t, err) + assert.NilError(t, err) defer os.RemoveAll(dirName) testTree := map[string]node{ @@ -99,32 +99,32 @@ func TestMkdirAllAndChownNew(t *testing.T) { "lib/x86_64": {45, 45}, "lib/x86_64/share": {1, 1}, } - require.NoError(t, buildTree(dirName, testTree)) + assert.NilError(t, buildTree(dirName, testTree)) // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, IDPair{UID: 99, GID: 99}) - require.NoError(t, err) + assert.NilError(t, err) testTree["usr/share"] = node{99, 99} verifyTree, err := readTree(dirName, "") - require.NoError(t, err) - require.NoError(t, compareTrees(testTree, verifyTree)) + assert.NilError(t, err) + assert.NilError(t, compareTrees(testTree, verifyTree)) // test 2-deep new directories--both should be owned by the uid/gid pair err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{UID: 101, GID: 101}) - require.NoError(t, err) + assert.NilError(t, err) testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} verifyTree, err = readTree(dirName, "") - require.NoError(t, err) - require.NoError(t, compareTrees(testTree, verifyTree)) + assert.NilError(t, err) + assert.NilError(t, compareTrees(testTree, verifyTree)) // test a directory that already exists; should NOT be chowned err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 102, GID: 102}) - require.NoError(t, err) + assert.NilError(t, err) verifyTree, err = readTree(dirName, "") - require.NoError(t, err) - require.NoError(t, compareTrees(testTree, verifyTree)) + assert.NilError(t, err) + assert.NilError(t, compareTrees(testTree, verifyTree)) } func TestMkdirAndChown(t *testing.T) { @@ -235,7 +235,7 @@ func compareTrees(left, right map[string]node) error { func delUser(t *testing.T, name string) { _, err := execCmd("userdel", name) - assert.NoError(t, err) + assert.Check(t, err) } func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { @@ -283,9 +283,9 @@ func TestGetRootUIDGID(t *testing.T) { } uid, gid, err := GetRootUIDGID(uidMap, gidMap) - assert.NoError(t, err) - assert.Equal(t, os.Getegid(), uid) - assert.Equal(t, os.Getegid(), gid) + assert.Check(t, err) + assert.Check(t, is.Equal(os.Getegid(), uid)) + assert.Check(t, is.Equal(os.Getegid(), gid)) uidMapError := []IDMap{ { @@ -295,7 +295,7 @@ func TestGetRootUIDGID(t *testing.T) { }, } _, _, err = GetRootUIDGID(uidMapError, gidMap) - assert.EqualError(t, err, "Container ID 0 cannot be mapped to a host ID") + assert.Check(t, is.Error(err, "Container ID 0 cannot be mapped to a host ID")) } func TestToContainer(t *testing.T) { @@ -308,74 +308,74 @@ func TestToContainer(t *testing.T) { } containerID, err := toContainer(2, uidMap) - assert.NoError(t, err) - assert.Equal(t, uidMap[0].ContainerID, containerID) + assert.Check(t, err) + assert.Check(t, is.Equal(uidMap[0].ContainerID, containerID)) } func TestNewIDMappings(t *testing.T) { RequiresRoot(t) _, _, err := AddNamespaceRangesUser(tempUser) - assert.NoError(t, err) + assert.Check(t, err) defer delUser(t, tempUser) tempUser, err := user.Lookup(tempUser) - assert.NoError(t, err) + assert.Check(t, err) gids, err := tempUser.GroupIds() - assert.NoError(t, err) + assert.Check(t, err) group, err := user.LookupGroupId(string(gids[0])) - assert.NoError(t, err) + assert.Check(t, err) idMappings, err := NewIDMappings(tempUser.Username, group.Name) - assert.NoError(t, err) + assert.Check(t, err) rootUID, rootGID, err := GetRootUIDGID(idMappings.UIDs(), idMappings.GIDs()) - assert.NoError(t, err) + assert.Check(t, err) dirName, err := ioutil.TempDir("", "mkdirall") - assert.NoError(t, err, "Couldn't create temp directory") + assert.Check(t, err, "Couldn't create temp directory") defer os.RemoveAll(dirName) err = MkdirAllAndChown(dirName, 0700, IDPair{UID: rootUID, GID: rootGID}) - assert.NoError(t, err, "Couldn't change ownership of file path. Got error") - assert.True(t, CanAccess(dirName, idMappings.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) + assert.Check(t, err, "Couldn't change ownership of file path. Got error") + assert.Check(t, CanAccess(dirName, idMappings.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) } func TestLookupUserAndGroup(t *testing.T) { RequiresRoot(t) uid, gid, err := AddNamespaceRangesUser(tempUser) - assert.NoError(t, err) + assert.Check(t, err) defer delUser(t, tempUser) fetchedUser, err := LookupUser(tempUser) - assert.NoError(t, err) + assert.Check(t, err) fetchedUserByID, err := LookupUID(uid) - assert.NoError(t, err) - assert.Equal(t, fetchedUserByID, fetchedUser) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(fetchedUserByID, fetchedUser)) fetchedGroup, err := LookupGroup(tempUser) - assert.NoError(t, err) + assert.Check(t, err) fetchedGroupByID, err := LookupGID(gid) - assert.NoError(t, err) - assert.Equal(t, fetchedGroupByID, fetchedGroup) + assert.Check(t, err) + assert.Check(t, is.DeepEqual(fetchedGroupByID, fetchedGroup)) } func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { fakeUser := "fakeuser" _, err := LookupUser(fakeUser) - assert.EqualError(t, err, "getent unable to find entry \""+fakeUser+"\" in passwd database") + assert.Check(t, is.Error(err, "getent unable to find entry \""+fakeUser+"\" in passwd database")) _, err = LookupUID(-1) - assert.Error(t, err) + assert.Check(t, is.ErrorContains(err, "")) fakeGroup := "fakegroup" _, err = LookupGroup(fakeGroup) - assert.EqualError(t, err, "getent unable to find entry \""+fakeGroup+"\" in group database") + assert.Check(t, is.Error(err, "getent unable to find entry \""+fakeGroup+"\" in group database")) _, err = LookupGID(-1) - assert.Error(t, err) + assert.Check(t, is.ErrorContains(err, "")) } // TestMkdirIsNotDir checks that mkdirAs() function (used by MkdirAll...) @@ -389,7 +389,7 @@ func TestMkdirIsNotDir(t *testing.T) { defer os.Remove(file.Name()) err = mkdirAs(file.Name(), 0755, 0, 0, false, false) - assert.EqualError(t, err, "mkdir "+file.Name()+": not a directory") + assert.Check(t, is.Error(err, "mkdir "+file.Name()+": not a directory")) } func RequiresRoot(t *testing.T) { From 33772b292dfe0599f379c473c37c2b21de2c1309 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 20 Apr 2018 11:59:18 +0200 Subject: [PATCH 29/75] Fix typo in idtools tests It should check `os.Geteuid` with `uid` instead of `os.Getegid`. On the container (where the tests run), the uid and gid seems to be the same, thus this doesn't fail. Signed-off-by: Vincent Demeester --- user/idtools_unix_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index e493b9e8..8c449013 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -284,7 +284,7 @@ func TestGetRootUIDGID(t *testing.T) { uid, gid, err := GetRootUIDGID(uidMap, gidMap) assert.Check(t, err) - assert.Check(t, is.Equal(os.Getegid(), uid)) + assert.Check(t, is.Equal(os.Geteuid(), uid)) assert.Check(t, is.Equal(os.Getegid(), gid)) uidMapError := []IDMap{ From e8f28926fc303c432da85ba1b9f4ea3b4abac5cd Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 20 Apr 2018 11:59:08 +0200 Subject: [PATCH 30/75] Skip some tests requires root uid when run as user Signed-off-by: Vincent Demeester --- user/idtools_unix_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 8c449013..7d8c9715 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -393,5 +393,5 @@ func TestMkdirIsNotDir(t *testing.T) { } func RequiresRoot(t *testing.T) { - skip.IfCondition(t, os.Getuid() != 0, "skipping test that requires root") + skip.If(t, os.Getuid() != 0, "skipping test that requires root") } From 3aa6a47551bca31453c17b8bce1e0feb3fa0154f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 19 May 2018 13:38:54 +0200 Subject: [PATCH 31/75] Various code-cleanup remove unnescessary import aliases, brackets, and so on. Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index e2b49315..d1f173a3 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -30,8 +30,8 @@ func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start } const ( - subuidFileName string = "/etc/subuid" - subgidFileName string = "/etc/subgid" + subuidFileName = "/etc/subuid" + subgidFileName = "/etc/subgid" ) // MkdirAllAndChown creates a directory (include any along the path) and then modifies From 2c770cc3cd36ed52f3e33cec2aff7fb6ebd581da Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 11 Jun 2018 15:32:11 +0200 Subject: [PATCH 32/75] =?UTF-8?q?Update=20tests=20to=20use=20gotest.tools?= =?UTF-8?q?=20=F0=9F=91=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Vincent Demeester --- user/idtools_unix_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 7d8c9715..608000a6 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -10,10 +10,10 @@ import ( "path/filepath" "testing" - "github.com/gotestyourself/gotestyourself/assert" - is "github.com/gotestyourself/gotestyourself/assert/cmp" - "github.com/gotestyourself/gotestyourself/skip" "golang.org/x/sys/unix" + "gotest.tools/assert" + is "gotest.tools/assert/cmp" + "gotest.tools/skip" ) const ( From 9494191387b16fafe4242da922e0bd2ceca3d601 Mon Sep 17 00:00:00 2001 From: Salahuddin Khan Date: Wed, 15 Nov 2017 22:20:33 -0800 Subject: [PATCH 33/75] Add ADD/COPY --chown flag support to Windows This implements chown support on Windows. Built-in accounts as well as accounts included in the SAM database of the container are supported. NOTE: IDPair is now named Identity and IDMappings is now named IdentityMapping. The following are valid examples: ADD --chown=Guest . COPY --chown=Administrator . COPY --chown=Guests . COPY --chown=ContainerUser . On Windows an owner is only granted the permission to read the security descriptor and read/write the discretionary access control list. This fix also grants read/write and execute permissions to the owner. Signed-off-by: Salahuddin Khan --- user/idtools.go | 45 ++++++++++++++++++++------------------- user/idtools_unix.go | 9 ++++---- user/idtools_unix_test.go | 28 ++++++++++++------------ user/idtools_windows.go | 10 +++++---- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index d1f173a3..230422ea 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -37,23 +37,23 @@ const ( // MkdirAllAndChown creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership to the requested uid/gid pair. -func MkdirAllAndChown(path string, mode os.FileMode, owner IDPair) error { - return mkdirAs(path, mode, owner.UID, owner.GID, true, true) +func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error { + return mkdirAs(path, mode, owner, true, true) } // MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. // If the directory already exists, this function still changes ownership. // Note that unlike os.Mkdir(), this function does not return IsExist error // in case path already exists. -func MkdirAndChown(path string, mode os.FileMode, owner IDPair) error { - return mkdirAs(path, mode, owner.UID, owner.GID, false, true) +func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { + return mkdirAs(path, mode, owner, false, true) } // MkdirAllAndChownNew creates a directory (include any along the path) and then modifies // ownership ONLY of newly created directories to the requested uid/gid. If the // directories along the path exist, no change of ownership will be performed -func MkdirAllAndChownNew(path string, mode os.FileMode, owner IDPair) error { - return mkdirAs(path, mode, owner.UID, owner.GID, true, false) +func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error { + return mkdirAs(path, mode, owner, true, false) } // GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. @@ -102,22 +102,23 @@ func toHost(contID int, idMap []IDMap) (int, error) { return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) } -// IDPair is a UID and GID pair -type IDPair struct { +// Identity is either a UID and GID pair or a SID (but not both) +type Identity struct { UID int GID int + SID string } -// IDMappings contains a mappings of UIDs and GIDs -type IDMappings struct { +// IdentityMapping contains a mappings of UIDs and GIDs +type IdentityMapping struct { uids []IDMap gids []IDMap } -// NewIDMappings takes a requested user and group name and +// NewIdentityMapping takes a requested user and group name and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair -func NewIDMappings(username, groupname string) (*IDMappings, error) { +func NewIdentityMapping(username, groupname string) (*IdentityMapping, error) { subuidRanges, err := parseSubuid(username) if err != nil { return nil, err @@ -133,7 +134,7 @@ func NewIDMappings(username, groupname string) (*IDMappings, error) { return nil, fmt.Errorf("No subgid ranges found for group %q", groupname) } - return &IDMappings{ + return &IdentityMapping{ uids: createIDMap(subuidRanges), gids: createIDMap(subgidRanges), }, nil @@ -141,21 +142,21 @@ func NewIDMappings(username, groupname string) (*IDMappings, error) { // NewIDMappingsFromMaps creates a new mapping from two slices // Deprecated: this is a temporary shim while transitioning to IDMapping -func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IDMappings { - return &IDMappings{uids: uids, gids: gids} +func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IdentityMapping { + return &IdentityMapping{uids: uids, gids: gids} } // RootPair returns a uid and gid pair for the root user. The error is ignored // because a root user always exists, and the defaults are correct when the uid // and gid maps are empty. -func (i *IDMappings) RootPair() IDPair { +func (i *IdentityMapping) RootPair() Identity { uid, gid, _ := GetRootUIDGID(i.uids, i.gids) - return IDPair{UID: uid, GID: gid} + return Identity{UID: uid, GID: gid} } // ToHost returns the host UID and GID for the container uid, gid. // Remapping is only performed if the ids aren't already the remapped root ids -func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) { +func (i *IdentityMapping) ToHost(pair Identity) (Identity, error) { var err error target := i.RootPair() @@ -173,7 +174,7 @@ func (i *IDMappings) ToHost(pair IDPair) (IDPair, error) { } // ToContainer returns the container UID and GID for the host uid and gid -func (i *IDMappings) ToContainer(pair IDPair) (int, int, error) { +func (i *IdentityMapping) ToContainer(pair Identity) (int, int, error) { uid, err := toContainer(pair.UID, i.uids) if err != nil { return -1, -1, err @@ -183,19 +184,19 @@ func (i *IDMappings) ToContainer(pair IDPair) (int, int, error) { } // Empty returns true if there are no id mappings -func (i *IDMappings) Empty() bool { +func (i *IdentityMapping) Empty() bool { return len(i.uids) == 0 && len(i.gids) == 0 } // UIDs return the UID mapping // TODO: remove this once everything has been refactored to use pairs -func (i *IDMappings) UIDs() []IDMap { +func (i *IdentityMapping) UIDs() []IDMap { return i.uids } // GIDs return the UID mapping // TODO: remove this once everything has been refactored to use pairs -func (i *IDMappings) GIDs() []IDMap { +func (i *IdentityMapping) GIDs() []IDMap { return i.gids } diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 1d87ea3b..fb239743 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -21,11 +21,12 @@ var ( getentCmd string ) -func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { +func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { // make an array containing the original path asked for, plus (for mkAll == true) // all path components leading up to the complete path that don't exist before we MkdirAll // so that we can chown all of them properly at the end. If chownExisting is false, we won't // chown the full directory path if it exists + var paths []string stat, err := system.Stat(path) @@ -38,7 +39,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown } // short-circuit--we were called with an existing directory and chown was requested - return lazyChown(path, ownerUID, ownerGID, stat) + return lazyChown(path, owner.UID, owner.GID, stat) } if os.IsNotExist(err) { @@ -69,7 +70,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll for _, pathComponent := range paths { - if err := lazyChown(pathComponent, ownerUID, ownerGID, nil); err != nil { + if err := lazyChown(pathComponent, owner.UID, owner.GID, nil); err != nil { return err } } @@ -78,7 +79,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory -func CanAccess(path string, pair IDPair) bool { +func CanAccess(path string, pair Identity) bool { statInfo, err := system.Stat(path) if err != nil { return false diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 608000a6..be5d6026 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -46,7 +46,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0755, IDPair{UID: 99, GID: 99}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0755, Identity{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr/share"] = node{99, 99} @@ -59,7 +59,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test 2-deep new directories--both should be owned by the uid/gid pair - if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{UID: 101, GID: 101}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0755, Identity{UID: 101, GID: 101}); err != nil { t.Fatal(err) } testTree["lib/some"] = node{101, 101} @@ -73,7 +73,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test a directory that already exists; should be chowned, but nothing else - if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 102, GID: 102}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0755, Identity{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr"] = node{102, 102} @@ -102,7 +102,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, buildTree(dirName, testTree)) // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, IDPair{UID: 99, GID: 99}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, Identity{UID: 99, GID: 99}) assert.NilError(t, err) testTree["usr/share"] = node{99, 99} @@ -111,7 +111,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, compareTrees(testTree, verifyTree)) // test 2-deep new directories--both should be owned by the uid/gid pair - err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, IDPair{UID: 101, GID: 101}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, Identity{UID: 101, GID: 101}) assert.NilError(t, err) testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} @@ -120,7 +120,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, compareTrees(testTree, verifyTree)) // test a directory that already exists; should NOT be chowned - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 102, GID: 102}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, Identity{UID: 102, GID: 102}) assert.NilError(t, err) verifyTree, err = readTree(dirName, "") assert.NilError(t, err) @@ -143,7 +143,7 @@ func TestMkdirAndChown(t *testing.T) { } // test a directory that already exists; should just chown to the requested uid/gid - if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0755, IDPair{UID: 99, GID: 99}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0755, Identity{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr"] = node{99, 99} @@ -156,12 +156,12 @@ func TestMkdirAndChown(t *testing.T) { } // create a subdir under a dir which doesn't exist--should fail - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, IDPair{UID: 102, GID: 102}); err == nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, Identity{UID: 102, GID: 102}); err == nil { t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") } // create a subdir under an existing dir; should only change the ownership of the new subdir - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0755, IDPair{UID: 102, GID: 102}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0755, Identity{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr/bin"] = node{102, 102} @@ -326,19 +326,19 @@ func TestNewIDMappings(t *testing.T) { group, err := user.LookupGroupId(string(gids[0])) assert.Check(t, err) - idMappings, err := NewIDMappings(tempUser.Username, group.Name) + idMapping, err := NewIdentityMapping(tempUser.Username, group.Name) assert.Check(t, err) - rootUID, rootGID, err := GetRootUIDGID(idMappings.UIDs(), idMappings.GIDs()) + rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDs(), idMapping.GIDs()) assert.Check(t, err) dirName, err := ioutil.TempDir("", "mkdirall") assert.Check(t, err, "Couldn't create temp directory") defer os.RemoveAll(dirName) - err = MkdirAllAndChown(dirName, 0700, IDPair{UID: rootUID, GID: rootGID}) + err = MkdirAllAndChown(dirName, 0700, Identity{UID: rootUID, GID: rootGID}) assert.Check(t, err, "Couldn't change ownership of file path. Got error") - assert.Check(t, CanAccess(dirName, idMappings.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) + assert.Check(t, CanAccess(dirName, idMapping.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) } func TestLookupUserAndGroup(t *testing.T) { @@ -388,7 +388,7 @@ func TestMkdirIsNotDir(t *testing.T) { } defer os.Remove(file.Name()) - err = mkdirAs(file.Name(), 0755, 0, 0, false, false) + err = mkdirAs(file.Name(), 0755, Identity{UID: 0, GID: 0}, false, false) assert.Check(t, is.Error(err, "mkdir "+file.Name()+": not a directory")) } diff --git a/user/idtools_windows.go b/user/idtools_windows.go index d72cc289..4ae38a1b 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -6,9 +6,11 @@ import ( "github.com/docker/docker/pkg/system" ) -// Platforms such as Windows do not support the UID/GID concept. So make this -// just a wrapper around system.MkdirAll. -func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { +// This is currently a wrapper around MkdirAll, however, since currently +// permissions aren't set through this path, the identity isn't utilized. +// Ownership is handled elsewhere, but in the future could be support here +// too. +func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { if err := system.MkdirAll(path, mode, ""); err != nil { return err } @@ -18,6 +20,6 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory // Windows does not require/support this function, so always return true -func CanAccess(path string, pair IDPair) bool { +func CanAccess(path string, identity Identity) bool { return true } From 3d94a7905fe068d1821ba79faa7056b850fc2540 Mon Sep 17 00:00:00 2001 From: Jonas Dohse Date: Thu, 30 May 2019 16:11:22 +0200 Subject: [PATCH 34/75] Stop sorting uid and gid ranges in id maps Moby currently sorts uid and gid ranges in id maps. This causes subuid and subgid files to be interpreted wrongly. The subuid file ``` > cat /etc/subuid jonas:100000:1000 jonas:1000:1 ``` configures that the container uids 0-999 are mapped to the host uids 100000-100999 and uid 1000 in the container is mapped to uid 1000 on the host. The expected uid_map is: ``` > docker run ubuntu cat /proc/self/uid_map 0 100000 1000 1000 1000 1 ``` Moby currently sorts the ranges by the first id in the range. Therefore with the subuid file above the uid 0 in the container is mapped to uid 100000 on host and the uids 1-1000 in container are mapped to the uids 1-1000 on the host. The resulting uid_map is: ``` > docker run ubuntu cat /proc/self/uid_map 0 1000 1 1 100000 1000 ``` The ordering was implemented to work around a limitation in Linux 3.8. This is fixed since Linux 3.9 as stated on the user namespaces manpage [1]: > In the initial implementation (Linux 3.8), this requirement was > satisfied by a simplistic implementation that imposed the further > requirement that the values in both field 1 and field 2 of successive > lines must be in ascending numerical order, which prevented some > otherwise valid maps from being created. Linux 3.9 and later fix this > limitation, allowing any valid set of nonoverlapping maps. This fix changes the interpretation of subuid and subgid files which do not have the ids of in the numerical order for each individual user. This breaks users that rely on the current behaviour. The desired mapping above - map low user ids in the container to high user ids on the host and some higher user ids in the container to lower user on host - can unfortunately not archived with the current behaviour. [1] http://man7.org/linux/man-pages/man7/user_namespaces.7.html Signed-off-by: Jonas Dohse --- user/idtools.go | 3 --- user/idtools_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 user/idtools_test.go diff --git a/user/idtools.go b/user/idtools.go index 230422ea..b3af7a42 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -4,7 +4,6 @@ import ( "bufio" "fmt" "os" - "sort" "strconv" "strings" ) @@ -203,8 +202,6 @@ func (i *IdentityMapping) GIDs() []IDMap { func createIDMap(subidRanges ranges) []IDMap { idMap := []IDMap{} - // sort the ranges by lowest ID first - sort.Sort(subidRanges) containerID := 0 for _, idrange := range subidRanges { idMap = append(idMap, IDMap{ diff --git a/user/idtools_test.go b/user/idtools_test.go new file mode 100644 index 00000000..7627d199 --- /dev/null +++ b/user/idtools_test.go @@ -0,0 +1,28 @@ +package idtools // import "github.com/docker/docker/pkg/idtools" + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestCreateIDMapOrder(t *testing.T) { + subidRanges := ranges{ + {100000, 1000}, + {1000, 1}, + } + + idMap := createIDMap(subidRanges) + assert.DeepEqual(t, idMap, []IDMap{ + { + ContainerID: 0, + HostID: 100000, + Size: 1000, + }, + { + ContainerID: 1000, + HostID: 1000, + Size: 1, + }, + }) +} From 1790be404b1b434598b479018a8cba4875d63349 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 8 Aug 2019 11:51:00 +0200 Subject: [PATCH 35/75] Allow system.MkDirAll() to be used as drop-in for os.MkDirAll() also renamed the non-windows variant of this file to be consistent with other files in this package Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 2 +- user/idtools_windows.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index fb239743..3981ff64 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -59,7 +59,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting paths = append(paths, dirPath) } } - if err := system.MkdirAll(path, mode, ""); err != nil { + if err := system.MkdirAll(path, mode); err != nil { return err } } else { diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 4ae38a1b..35ede0ff 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -11,7 +11,7 @@ import ( // Ownership is handled elsewhere, but in the future could be support here // too. func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { - if err := system.MkdirAll(path, mode, ""); err != nil { + if err := system.MkdirAll(path, mode); err != nil { return err } return nil From 50f89a48353dd4402783ae3023e0b7e327a7aef1 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 7 Aug 2019 02:38:51 +0200 Subject: [PATCH 36/75] unconvert: remove unnescessary conversions Signed-off-by: Kir Kolyshkin Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index be5d6026..11092c46 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -323,7 +323,7 @@ func TestNewIDMappings(t *testing.T) { gids, err := tempUser.GroupIds() assert.Check(t, err) - group, err := user.LookupGroupId(string(gids[0])) + group, err := user.LookupGroupId(gids[0]) assert.Check(t, err) idMapping, err := NewIdentityMapping(tempUser.Username, group.Name) From 27cb94cb92fffbd0bc8f811eb7f6bc13b8e6d488 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 27 Nov 2019 15:40:23 +0100 Subject: [PATCH 37/75] pkg/idtools: normalize comment formatting Signed-off-by: Sebastiaan van Stijn --- user/utils_unix.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/utils_unix.go b/user/utils_unix.go index 903ac450..bcf6a4ff 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -18,8 +18,8 @@ func resolveBinary(binname string) (string, error) { if err != nil { return "", err } - //only return no error if the final resolved binary basename - //matches what was searched for + // only return no error if the final resolved binary basename + // matches what was searched for if filepath.Base(resolvedPath) == binname { return resolvedPath, nil } From 6c35f197bbe6426aceb92b4134accacb71988492 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 7 Feb 2020 14:39:24 +0100 Subject: [PATCH 38/75] bump gotest.tools v3.0.1 for compatibility with Go 1.14 full diff: https://github.com/gotestyourself/gotest.tools/compare/v2.3.0...v3.0.1 Signed-off-by: Sebastiaan van Stijn --- user/idtools_test.go | 2 +- user/idtools_unix_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/user/idtools_test.go b/user/idtools_test.go index 7627d199..34b57b6b 100644 --- a/user/idtools_test.go +++ b/user/idtools_test.go @@ -3,7 +3,7 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "testing" - "gotest.tools/assert" + "gotest.tools/v3/assert" ) func TestCreateIDMapOrder(t *testing.T) { diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 11092c46..fd1edd73 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -11,9 +11,9 @@ import ( "testing" "golang.org/x/sys/unix" - "gotest.tools/assert" - is "gotest.tools/assert/cmp" - "gotest.tools/skip" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" ) const ( From c471a1c2e905c62ab8e17e463529291b610d2ceb Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Wed, 11 Mar 2020 19:16:29 -0700 Subject: [PATCH 39/75] pkg/idtools: fix use of bufio.Scanner.Err The Err() method should be called after the Scan() loop, not inside it. Fixes: b8432dce46b Signed-off-by: Kir Kolyshkin --- user/idtools.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index b3af7a42..db1fd1a9 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -236,10 +236,6 @@ func parseSubidFile(path, username string) (ranges, error) { s := bufio.NewScanner(subidFile) for s.Scan() { - if err := s.Err(); err != nil { - return rangeList, err - } - text := strings.TrimSpace(s.Text()) if text == "" || strings.HasPrefix(text, "#") { continue @@ -260,5 +256,6 @@ func parseSubidFile(path, username string) (ranges, error) { rangeList = append(rangeList, subIDRange{startid, length}) } } - return rangeList, nil + + return rangeList, s.Err() } From 8f173ebbc8d0f5396b03c0d5531b1fc364d56b3b Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Sun, 24 May 2020 18:59:06 +0530 Subject: [PATCH 40/75] remove group name from identity mapping NewIdentityMapping took group name as an argument, and used the group name also to parse the /etc/sub{uid,gui}. But as per linux man pages, the sub{uid,gid} file maps username or uid, not a group name. Therefore, all occurrences where mapping is used need to consider only username and uid. Code trying to map using gid and group name in the daemon is also removed. Signed-off-by: Akhil Mohan --- user/idtools.go | 25 --------------------- user/idtools_unix.go | 47 +++++++++++++++++++++++++++++++++++++++ user/idtools_unix_test.go | 7 +----- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index db1fd1a9..7569ac15 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -114,31 +114,6 @@ type IdentityMapping struct { gids []IDMap } -// NewIdentityMapping takes a requested user and group name and -// using the data from /etc/sub{uid,gid} ranges, creates the -// proper uid and gid remapping ranges for that user/group pair -func NewIdentityMapping(username, groupname string) (*IdentityMapping, error) { - subuidRanges, err := parseSubuid(username) - if err != nil { - return nil, err - } - subgidRanges, err := parseSubgid(groupname) - if err != nil { - return nil, err - } - if len(subuidRanges) == 0 { - return nil, fmt.Errorf("No subuid ranges found for user %q", username) - } - if len(subgidRanges) == 0 { - return nil, fmt.Errorf("No subgid ranges found for group %q", groupname) - } - - return &IdentityMapping{ - uids: createIDMap(subuidRanges), - gids: createIDMap(subgidRanges), - }, nil -} - // NewIDMappingsFromMaps creates a new mapping from two slices // Deprecated: this is a temporary shim while transitioning to IDMapping func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IdentityMapping { diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 3981ff64..3d64c1b4 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -8,12 +8,14 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "sync" "syscall" "github.com/docker/docker/pkg/system" "github.com/opencontainers/runc/libcontainer/user" + "github.com/pkg/errors" ) var ( @@ -229,3 +231,48 @@ func lazyChown(p string, uid, gid int, stat *system.StatT) error { } return os.Chown(p, uid, gid) } + +// NewIdentityMapping takes a requested username and +// using the data from /etc/sub{uid,gid} ranges, creates the +// proper uid and gid remapping ranges for that user/group pair +func NewIdentityMapping(username string) (*IdentityMapping, error) { + usr, err := LookupUser(username) + if err != nil { + return nil, fmt.Errorf("Could not get user for username %s: %v", username, err) + } + + uid := strconv.Itoa(usr.Uid) + + subuidRangesWithUserName, err := parseSubuid(username) + if err != nil { + return nil, err + } + subgidRangesWithUserName, err := parseSubgid(username) + if err != nil { + return nil, err + } + + subuidRangesWithUID, err := parseSubuid(uid) + if err != nil { + return nil, err + } + subgidRangesWithUID, err := parseSubgid(uid) + if err != nil { + return nil, err + } + + subuidRanges := append(subuidRangesWithUserName, subuidRangesWithUID...) + subgidRanges := append(subgidRangesWithUserName, subgidRangesWithUID...) + + if len(subuidRanges) == 0 { + return nil, errors.Errorf("no subuid ranges found for user %q", username) + } + if len(subgidRanges) == 0 { + return nil, errors.Errorf("no subgid ranges found for user %q", username) + } + + return &IdentityMapping{ + uids: createIDMap(subuidRanges), + gids: createIDMap(subgidRanges), + }, nil +} diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index fd1edd73..849d6237 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -321,12 +321,7 @@ func TestNewIDMappings(t *testing.T) { tempUser, err := user.Lookup(tempUser) assert.Check(t, err) - gids, err := tempUser.GroupIds() - assert.Check(t, err) - group, err := user.LookupGroupId(gids[0]) - assert.Check(t, err) - - idMapping, err := NewIdentityMapping(tempUser.Username, group.Name) + idMapping, err := NewIdentityMapping(tempUser.Username) assert.Check(t, err) rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDs(), idMapping.GIDs()) From d237acd5548259a648196993f746916c9d79ac67 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 20 Aug 2020 10:40:06 +0200 Subject: [PATCH 41/75] pkg/idtools: refactor to avoid string-splitting The package used a lot of string-formatting, followed by string-splitting. This looked to originate from attempts to use templating to allow future extensibility (b8432dce46b289a7a883444a004375a203393353). Looking at the history of the package, only a single update was made to these templates, 5 years go, which makes it unlikely that more templating will be needed. This patch simplifies the handling of arguments to use `[]string` instead of a single `string` (and splitting to a `[]string`). This both simplifies the code somewhat, and prevents user/group-names containing spaces to be splitted (causing, e.g. `getent` to fail). Note that user/group-names containing spaces are invalid (or at least discouraged), there are situations where such names may be used, so we should avoid breaking on such names. Before this change, a user/group name with a space in its name would fail; dockerd --userns-remap="user:domain users" INFO[2020-08-19T10:26:59.288868661+02:00] Starting up Error during groupname lookup for "domain users": getent unable to find entry "domain" in group database With this change: # Add some possibly problematic usernames for testing # need to do this manually, as `adduser` / `useradd` won't accept these names echo 'user name:x:1002:1002::/home/one:/bin/false' >> /etc/passwd; \ echo 'user name:x:1002:' >> /etc/group; \ echo 'user name:1266401166:65536' >> /etc/subuid; \ echo 'user name:1266401153:65536' >> /etc/subgid; \ echo 'user$HOME:x:1003:1003::/home/one:/bin/false' >> /etc/passwd; \ echo 'user$HOME:x:1003:' >> /etc/group; \ echo 'user$HOME:1266401166:65536' >> /etc/subuid; \ echo 'user$HOME:1266401153:65536' >> /etc/subgid; \ echo 'user'"'"'name:x:1004:1004::/home/one:/bin/false' >> /etc/passwd; \ echo 'user'"'"'name:x:1004:' >> /etc/group; \ echo 'user'"'"'name:1266401166:65536' >> /etc/subuid; \ echo 'user'"'"'name:1266401153:65536' >> /etc/subgid; \ echo 'user"name:x:1005:1005::/home/one:/bin/false' >> /etc/passwd; \ echo 'user"name:x:1005:' >> /etc/group; \ echo 'user"name:1266401166:65536' >> /etc/subuid; \ echo 'user"name:1266401153:65536' >> /etc/subgid; # Start the daemon using those users dockerd --userns-remap="user name:user name" dockerd --userns-remap='user$HOME:user$HOME' dockerd --userns-remap="user'name":"user'name" dockerd --userns-remap='user"name':'user"name' Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 52 ++++++++++++++++++-------------------- user/usergroupadd_linux.go | 34 ++++++++++++------------- user/utils_unix.go | 5 ++-- 3 files changed, 44 insertions(+), 47 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 3d64c1b4..5defe645 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -9,7 +9,6 @@ import ( "os" "path/filepath" "strconv" - "strings" "sync" "syscall" @@ -107,14 +106,14 @@ func accessible(isOwner, isGroup bool, perms os.FileMode) bool { // LookupUser uses traditional local system files lookup (from libcontainer/user) on a username, // followed by a call to `getent` for supporting host configured non-files passwd and group dbs -func LookupUser(username string) (user.User, error) { +func LookupUser(name string) (user.User, error) { // first try a local system files lookup using existing capabilities - usr, err := user.LookupUser(username) + usr, err := user.LookupUser(name) if err == nil { return usr, nil } // local files lookup failed; attempt to call `getent` to query configured passwd dbs - usr, err = getentUser(fmt.Sprintf("%s %s", "passwd", username)) + usr, err = getentUser(name) if err != nil { return user.User{}, err } @@ -130,11 +129,11 @@ func LookupUID(uid int) (user.User, error) { return usr, nil } // local files lookup failed; attempt to call `getent` to query configured passwd dbs - return getentUser(fmt.Sprintf("%s %d", "passwd", uid)) + return getentUser(strconv.Itoa(uid)) } -func getentUser(args string) (user.User, error) { - reader, err := callGetent(args) +func getentUser(name string) (user.User, error) { + reader, err := callGetent("passwd", name) if err != nil { return user.User{}, err } @@ -143,21 +142,21 @@ func getentUser(args string) (user.User, error) { return user.User{}, err } if len(users) == 0 { - return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", strings.Split(args, " ")[1]) + return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name) } return users[0], nil } // LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name, // followed by a call to `getent` for supporting host configured non-files passwd and group dbs -func LookupGroup(groupname string) (user.Group, error) { +func LookupGroup(name string) (user.Group, error) { // first try a local system files lookup using existing capabilities - group, err := user.LookupGroup(groupname) + group, err := user.LookupGroup(name) if err == nil { return group, nil } // local files lookup failed; attempt to call `getent` to query configured group dbs - return getentGroup(fmt.Sprintf("%s %s", "group", groupname)) + return getentGroup(name) } // LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID, @@ -169,11 +168,11 @@ func LookupGID(gid int) (user.Group, error) { return group, nil } // local files lookup failed; attempt to call `getent` to query configured group dbs - return getentGroup(fmt.Sprintf("%s %d", "group", gid)) + return getentGroup(strconv.Itoa(gid)) } -func getentGroup(args string) (user.Group, error) { - reader, err := callGetent(args) +func getentGroup(name string) (user.Group, error) { + reader, err := callGetent("group", name) if err != nil { return user.Group{}, err } @@ -182,18 +181,18 @@ func getentGroup(args string) (user.Group, error) { return user.Group{}, err } if len(groups) == 0 { - return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", strings.Split(args, " ")[1]) + return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name) } return groups[0], nil } -func callGetent(args string) (io.Reader, error) { +func callGetent(database, key string) (io.Reader, error) { entOnce.Do(func() { getentCmd, _ = resolveBinary("getent") }) // if no `getent` command on host, can't do anything else if getentCmd == "" { - return nil, fmt.Errorf("") + return nil, fmt.Errorf("unable to find getent command") } - out, err := execCmd(getentCmd, args) + out, err := execCmd(getentCmd, database, key) if err != nil { exitCode, errC := system.GetExitCode(err) if errC != nil { @@ -203,8 +202,7 @@ func callGetent(args string) (io.Reader, error) { case 1: return nil, fmt.Errorf("getent reported invalid parameters/database unknown") case 2: - terms := strings.Split(args, " ") - return nil, fmt.Errorf("getent unable to find entry %q in %s database", terms[1], terms[0]) + return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database) case 3: return nil, fmt.Errorf("getent database doesn't support enumeration") default: @@ -235,19 +233,19 @@ func lazyChown(p string, uid, gid int, stat *system.StatT) error { // NewIdentityMapping takes a requested username and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair -func NewIdentityMapping(username string) (*IdentityMapping, error) { - usr, err := LookupUser(username) +func NewIdentityMapping(name string) (*IdentityMapping, error) { + usr, err := LookupUser(name) if err != nil { - return nil, fmt.Errorf("Could not get user for username %s: %v", username, err) + return nil, fmt.Errorf("Could not get user for username %s: %v", name, err) } uid := strconv.Itoa(usr.Uid) - subuidRangesWithUserName, err := parseSubuid(username) + subuidRangesWithUserName, err := parseSubuid(name) if err != nil { return nil, err } - subgidRangesWithUserName, err := parseSubgid(username) + subgidRangesWithUserName, err := parseSubgid(name) if err != nil { return nil, err } @@ -265,10 +263,10 @@ func NewIdentityMapping(username string) (*IdentityMapping, error) { subgidRanges := append(subgidRangesWithUserName, subgidRangesWithUID...) if len(subuidRanges) == 0 { - return nil, errors.Errorf("no subuid ranges found for user %q", username) + return nil, errors.Errorf("no subuid ranges found for user %q", name) } if len(subgidRanges) == 0 { - return nil, errors.Errorf("no subgid ranges found for user %q", username) + return nil, errors.Errorf("no subgid ranges found for user %q", name) } return &IdentityMapping{ diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 6272c5a4..bf7ae056 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -17,18 +17,13 @@ import ( var ( once sync.Once userCommand string - - cmdTemplates = map[string]string{ - "adduser": "--system --shell /bin/false --no-create-home --disabled-login --disabled-password --group %s", - "useradd": "-r -s /bin/false %s", - "usermod": "-%s %d-%d %s", - } - idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) +) + +const ( // default length for a UID/GID subordinate range defaultRangeLen = 65536 defaultRangeStart = 100000 - userMod = "usermod" ) // AddNamespaceRangesUser takes a username and uses the standard system @@ -67,7 +62,7 @@ func AddNamespaceRangesUser(name string) (int, int, error) { return uid, gid, nil } -func addUser(userName string) error { +func addUser(name string) error { once.Do(func() { // set up which commands are used for adding users/groups dependent on distro if _, err := resolveBinary("adduser"); err == nil { @@ -76,13 +71,18 @@ func addUser(userName string) error { userCommand = "useradd" } }) - if userCommand == "" { - return fmt.Errorf("Cannot add user; no useradd/adduser binary found") + var args []string + switch userCommand { + case "adduser": + args = []string{"--system", "--shell", "/bin/false", "--no-create-home", "--disabled-login", "--disabled-password", "--group", name} + case "useradd": + args = []string{"-r", "-s", "/bin/false", name} + default: + return fmt.Errorf("cannot add user; no useradd/adduser binary found") } - args := fmt.Sprintf(cmdTemplates[userCommand], userName) - out, err := execCmd(userCommand, args) - if err != nil { - return fmt.Errorf("Failed to add user with error: %v; output: %q", err, string(out)) + + if out, err := execCmd(userCommand, args...); err != nil { + return fmt.Errorf("failed to add user with error: %v; output: %q", err, string(out)) } return nil } @@ -101,7 +101,7 @@ func createSubordinateRanges(name string) error { if err != nil { return fmt.Errorf("Can't find available subuid range: %v", err) } - out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "v", startID, startID+defaultRangeLen-1, name)) + out, err := execCmd("usermod", "-v", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) if err != nil { return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) } @@ -117,7 +117,7 @@ func createSubordinateRanges(name string) error { if err != nil { return fmt.Errorf("Can't find available subgid range: %v", err) } - out, err := execCmd(userMod, fmt.Sprintf(cmdTemplates[userMod], "w", startID, startID+defaultRangeLen-1, name)) + out, err := execCmd("usermod", "-w", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) if err != nil { return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) } diff --git a/user/utils_unix.go b/user/utils_unix.go index bcf6a4ff..1e2d4a7a 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -6,7 +6,6 @@ import ( "fmt" "os/exec" "path/filepath" - "strings" ) func resolveBinary(binname string) (string, error) { @@ -26,7 +25,7 @@ func resolveBinary(binname string) (string, error) { return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) } -func execCmd(cmd, args string) ([]byte, error) { - execCmd := exec.Command(cmd, strings.Split(args, " ")...) +func execCmd(cmd string, arg ...string) ([]byte, error) { + execCmd := exec.Command(cmd, arg...) return execCmd.CombinedOutput() } From 8ec694d678ccd569a93413ac66a607f2cf26119d Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 6 Oct 2020 19:30:07 +0000 Subject: [PATCH 42/75] Ensure MkdirAllAndChown also sets perms Generally if we ever need to change perms of a dir, between versions, this ensures the permissions actually change when we think it should change without having to handle special cases if it already existed. Signed-off-by: Brian Goff (cherry picked from commit edb62a3ace8c4303822a391b38231e577f8c2ee8) Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 11 ++++++++--- user/idtools_unix.go | 14 ++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 7569ac15..25a57b23 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -35,13 +35,13 @@ const ( // MkdirAllAndChown creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this -// function will still change ownership to the requested uid/gid pair. +// function will still change ownership and permissions. func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error { return mkdirAs(path, mode, owner, true, true) } // MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. -// If the directory already exists, this function still changes ownership. +// If the directory already exists, this function still changes ownership and permissions. // Note that unlike os.Mkdir(), this function does not return IsExist error // in case path already exists. func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { @@ -50,7 +50,7 @@ func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { // MkdirAllAndChownNew creates a directory (include any along the path) and then modifies // ownership ONLY of newly created directories to the requested uid/gid. If the -// directories along the path exist, no change of ownership will be performed +// directories along the path exist, no change of ownership or permissions will be performed func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error { return mkdirAs(path, mode, owner, true, false) } @@ -234,3 +234,8 @@ func parseSubidFile(path, username string) (ranges, error) { return rangeList, s.Err() } + +// CurrentIdentity returns the identity of the current process +func CurrentIdentity() Identity { + return Identity{UID: os.Getuid(), GID: os.Getegid()} +} diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 5defe645..a03af120 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -40,7 +40,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting } // short-circuit--we were called with an existing directory and chown was requested - return lazyChown(path, owner.UID, owner.GID, stat) + return setPermissions(path, mode, owner.UID, owner.GID, stat) } if os.IsNotExist(err) { @@ -71,7 +71,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll for _, pathComponent := range paths { - if err := lazyChown(pathComponent, owner.UID, owner.GID, nil); err != nil { + if err := setPermissions(pathComponent, mode, owner.UID, owner.GID, nil); err != nil { return err } } @@ -213,10 +213,11 @@ func callGetent(database, key string) (io.Reader, error) { return bytes.NewReader(out), nil } -// lazyChown performs a chown only if the uid/gid don't match what's requested +// setPermissions performs a chown/chmod only if the uid/gid don't match what's requested // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the // dir is on an NFS share, so don't call chown unless we absolutely must. -func lazyChown(p string, uid, gid int, stat *system.StatT) error { +// Likewise for setting permissions. +func setPermissions(p string, mode os.FileMode, uid, gid int, stat *system.StatT) error { if stat == nil { var err error stat, err = system.Stat(p) @@ -224,6 +225,11 @@ func lazyChown(p string, uid, gid int, stat *system.StatT) error { return err } } + if os.FileMode(stat.Mode()).Perm() != mode.Perm() { + if err := os.Chmod(p, mode.Perm()); err != nil { + return err + } + } if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) { return nil } From e1535c4b6b2f9cc74d0bd850375feac31997598d Mon Sep 17 00:00:00 2001 From: Grant Millar Date: Tue, 9 Feb 2021 16:13:35 +0000 Subject: [PATCH 43/75] Fix userns-remap option when username & UID match Signed-off-by: Grant Millar --- user/idtools_unix.go | 51 +++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index a03af120..e7d25ee4 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -245,38 +245,51 @@ func NewIdentityMapping(name string) (*IdentityMapping, error) { return nil, fmt.Errorf("Could not get user for username %s: %v", name, err) } - uid := strconv.Itoa(usr.Uid) - - subuidRangesWithUserName, err := parseSubuid(name) + subuidRanges, err := lookupSubUIDRanges(usr) if err != nil { return nil, err } - subgidRangesWithUserName, err := parseSubgid(name) + subgidRanges, err := lookupSubGIDRanges(usr) if err != nil { return nil, err } - subuidRangesWithUID, err := parseSubuid(uid) + return &IdentityMapping{ + uids: subuidRanges, + gids: subgidRanges, + }, nil +} + +func lookupSubUIDRanges(usr user.User) ([]IDMap, error) { + rangeList, err := parseSubuid(strconv.Itoa(usr.Uid)) if err != nil { return nil, err } - subgidRangesWithUID, err := parseSubgid(uid) + if len(rangeList) == 0 { + rangeList, err = parseSubuid(usr.Name) + if err != nil { + return nil, err + } + } + if len(rangeList) == 0 { + return nil, errors.Errorf("no subuid ranges found for user %q", usr.Name) + } + return createIDMap(rangeList), nil +} + +func lookupSubGIDRanges(usr user.User) ([]IDMap, error) { + rangeList, err := parseSubgid(strconv.Itoa(usr.Uid)) if err != nil { return nil, err } - - subuidRanges := append(subuidRangesWithUserName, subuidRangesWithUID...) - subgidRanges := append(subgidRangesWithUserName, subgidRangesWithUID...) - - if len(subuidRanges) == 0 { - return nil, errors.Errorf("no subuid ranges found for user %q", name) + if len(rangeList) == 0 { + rangeList, err = parseSubgid(usr.Name) + if err != nil { + return nil, err + } } - if len(subgidRanges) == 0 { - return nil, errors.Errorf("no subgid ranges found for user %q", name) + if len(rangeList) == 0 { + return nil, errors.Errorf("no subgid ranges found for user %q", usr.Name) } - - return &IdentityMapping{ - uids: createIDMap(subuidRanges), - gids: createIDMap(subgidRanges), - }, nil + return createIDMap(rangeList), nil } From 6b4f2b87bf00d4d13f28f0e2d73913645ad13d23 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 18 Jun 2021 10:37:04 +0200 Subject: [PATCH 44/75] pkg/system: deprecate some consts and move them to pkg/idtools These consts were used in combination with idtools utilities, which makes it a more logical location for these consts to live. Signed-off-by: Sebastiaan van Stijn --- user/idtools_windows.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 35ede0ff..0f5aadd4 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -6,6 +6,15 @@ import ( "github.com/docker/docker/pkg/system" ) +const ( + SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" +) + +const ( + ContainerAdministratorSidString = "S-1-5-93-2-1" + ContainerUserSidString = "S-1-5-93-2-2" +) + // This is currently a wrapper around MkdirAll, however, since currently // permissions aren't set through this path, the identity isn't utilized. // Ownership is handled elsewhere, but in the future could be support here From 487b6be36a03508703130329e1366067adec6b45 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 23 Aug 2021 15:14:53 +0200 Subject: [PATCH 45/75] Update to Go 1.17.0, and gofmt with Go 1.17 Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 1 + user/idtools_unix_test.go | 1 + user/usergroupadd_unsupported.go | 1 + user/utils_unix.go | 1 + 4 files changed, 4 insertions(+) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index e7d25ee4..ceec0339 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 849d6237..f1670a6f 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go index e7c4d631..5e24577e 100644 --- a/user/usergroupadd_unsupported.go +++ b/user/usergroupadd_unsupported.go @@ -1,3 +1,4 @@ +//go:build !linux // +build !linux package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/utils_unix.go b/user/utils_unix.go index 1e2d4a7a..540672af 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" From 3c2d209aa0c373c9c955dbc2c45968559f9add7f Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Tue, 24 Aug 2021 18:10:50 +0800 Subject: [PATCH 46/75] refactor: move from io/ioutil to io and os package The io/ioutil package has been deprecated in Go 1.16. This commit replaces the existing io/ioutil functions with their new definitions in io and os packages. Signed-off-by: Eng Zer Jun --- user/idtools_unix_test.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index f1670a6f..c43ed6fc 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -5,7 +5,6 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" - "io/ioutil" "os" "os/user" "path/filepath" @@ -28,7 +27,7 @@ type node struct { func TestMkdirAllAndChown(t *testing.T) { RequiresRoot(t) - dirName, err := ioutil.TempDir("", "mkdirall") + dirName, err := os.MkdirTemp("", "mkdirall") if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) } @@ -89,7 +88,7 @@ func TestMkdirAllAndChown(t *testing.T) { func TestMkdirAllAndChownNew(t *testing.T) { RequiresRoot(t) - dirName, err := ioutil.TempDir("", "mkdirnew") + dirName, err := os.MkdirTemp("", "mkdirnew") assert.NilError(t, err) defer os.RemoveAll(dirName) @@ -130,7 +129,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { func TestMkdirAndChown(t *testing.T) { RequiresRoot(t) - dirName, err := ioutil.TempDir("", "mkdir") + dirName, err := os.MkdirTemp("", "mkdir") if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) } @@ -191,7 +190,7 @@ func buildTree(base string, tree map[string]node) error { func readTree(base, root string) (map[string]node, error) { tree := make(map[string]node) - dirInfos, err := ioutil.ReadDir(base) + dirInfos, err := os.ReadDir(base) if err != nil { return nil, fmt.Errorf("Couldn't read directory entries for %q: %v", base, err) } @@ -240,7 +239,7 @@ func delUser(t *testing.T, name string) { } func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "parsesubid") + tmpDir, err := os.MkdirTemp("", "parsesubid") if err != nil { t.Fatal(err) } @@ -249,7 +248,7 @@ func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { # empty default subuid/subgid file dockremap:231072:65536` - if err := ioutil.WriteFile(fnamePath, []byte(fcontent), 0644); err != nil { + if err := os.WriteFile(fnamePath, []byte(fcontent), 0644); err != nil { t.Fatal(err) } ranges, err := parseSubidFile(fnamePath, "dockremap") @@ -328,7 +327,7 @@ func TestNewIDMappings(t *testing.T) { rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDs(), idMapping.GIDs()) assert.Check(t, err) - dirName, err := ioutil.TempDir("", "mkdirall") + dirName, err := os.MkdirTemp("", "mkdirall") assert.Check(t, err, "Couldn't create temp directory") defer os.RemoveAll(dirName) @@ -378,7 +377,7 @@ func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { // returns a correct error in case a directory which it is about to create // already exists but is a file (rather than a directory). func TestMkdirIsNotDir(t *testing.T) { - file, err := ioutil.TempFile("", t.Name()) + file, err := os.CreateTemp("", t.Name()) if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) } From 0ff8db30c921d1f4359f5730c733a9b85123dc3f Mon Sep 17 00:00:00 2001 From: Cory Snider Date: Mon, 14 Mar 2022 15:24:29 -0400 Subject: [PATCH 47/75] Finish refactor of UID/GID usage to a new struct Finish the refactor which was partially completed with commit 34536c498d56, passing around IdentityMapping structs instead of pairs of []IDMap slices. Existing code which uses []IDMap relies on zero-valued fields to be valid, empty mappings. So in order to successfully finish the refactoring without introducing bugs, their replacement therefore also needs to have a useful zero value which represents an empty mapping. Change IdentityMapping to be a pass-by-value type so that there are no nil pointers to worry about. The functionality provided by the deprecated NewIDMappingsFromMaps function is required by unit tests to to construct arbitrary IdentityMapping values. And the daemon will always need to access the mappings to pass them to the Linux kernel. Accommodate these use cases by exporting the struct fields instead. BuildKit currently depends on the UIDs and GIDs methods so we cannot get rid of them yet. Signed-off-by: Cory Snider --- user/idtools.go | 54 ++++++++++++++++++++------------------- user/idtools_unix.go | 25 +++++++++++++----- user/idtools_unix_test.go | 4 +-- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 25a57b23..1e0a8900 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -108,70 +108,72 @@ type Identity struct { SID string } -// IdentityMapping contains a mappings of UIDs and GIDs -type IdentityMapping struct { - uids []IDMap - gids []IDMap +// Chown changes the numeric uid and gid of the named file to id.UID and id.GID. +func (id Identity) Chown(name string) error { + return os.Chown(name, id.UID, id.GID) } -// NewIDMappingsFromMaps creates a new mapping from two slices -// Deprecated: this is a temporary shim while transitioning to IDMapping -func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IdentityMapping { - return &IdentityMapping{uids: uids, gids: gids} +// IdentityMapping contains a mappings of UIDs and GIDs. +// The zero value represents an empty mapping. +type IdentityMapping struct { + UIDMaps []IDMap `json:"UIDMaps"` + GIDMaps []IDMap `json:"GIDMaps"` } // RootPair returns a uid and gid pair for the root user. The error is ignored // because a root user always exists, and the defaults are correct when the uid // and gid maps are empty. -func (i *IdentityMapping) RootPair() Identity { - uid, gid, _ := GetRootUIDGID(i.uids, i.gids) +func (i IdentityMapping) RootPair() Identity { + uid, gid, _ := GetRootUIDGID(i.UIDMaps, i.GIDMaps) return Identity{UID: uid, GID: gid} } // ToHost returns the host UID and GID for the container uid, gid. // Remapping is only performed if the ids aren't already the remapped root ids -func (i *IdentityMapping) ToHost(pair Identity) (Identity, error) { +func (i IdentityMapping) ToHost(pair Identity) (Identity, error) { var err error target := i.RootPair() if pair.UID != target.UID { - target.UID, err = toHost(pair.UID, i.uids) + target.UID, err = toHost(pair.UID, i.UIDMaps) if err != nil { return target, err } } if pair.GID != target.GID { - target.GID, err = toHost(pair.GID, i.gids) + target.GID, err = toHost(pair.GID, i.GIDMaps) } return target, err } // ToContainer returns the container UID and GID for the host uid and gid -func (i *IdentityMapping) ToContainer(pair Identity) (int, int, error) { - uid, err := toContainer(pair.UID, i.uids) +func (i IdentityMapping) ToContainer(pair Identity) (int, int, error) { + uid, err := toContainer(pair.UID, i.UIDMaps) if err != nil { return -1, -1, err } - gid, err := toContainer(pair.GID, i.gids) + gid, err := toContainer(pair.GID, i.GIDMaps) return uid, gid, err } // Empty returns true if there are no id mappings -func (i *IdentityMapping) Empty() bool { - return len(i.uids) == 0 && len(i.gids) == 0 +func (i IdentityMapping) Empty() bool { + return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } -// UIDs return the UID mapping -// TODO: remove this once everything has been refactored to use pairs -func (i *IdentityMapping) UIDs() []IDMap { - return i.uids +// UIDs returns the mapping for UID. +// +// Deprecated: reference the UIDMaps field directly. +func (i IdentityMapping) UIDs() []IDMap { + return i.UIDMaps } -// GIDs return the UID mapping -// TODO: remove this once everything has been refactored to use pairs -func (i *IdentityMapping) GIDs() []IDMap { - return i.gids +// GIDs returns the mapping for GID. +// +// Deprecated: reference the GIDMaps field directly. +func (i IdentityMapping) GIDs() []IDMap { + return i.GIDMaps } func createIDMap(subidRanges ranges) []IDMap { diff --git a/user/idtools_unix.go b/user/idtools_unix.go index ceec0339..7a7ccc3e 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -240,24 +240,37 @@ func setPermissions(p string, mode os.FileMode, uid, gid int, stat *system.StatT // NewIdentityMapping takes a requested username and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair +// +// Deprecated: Use LoadIdentityMapping. func NewIdentityMapping(name string) (*IdentityMapping, error) { + m, err := LoadIdentityMapping(name) + if err != nil { + return nil, err + } + return &m, err +} + +// LoadIdentityMapping takes a requested username and +// using the data from /etc/sub{uid,gid} ranges, creates the +// proper uid and gid remapping ranges for that user/group pair +func LoadIdentityMapping(name string) (IdentityMapping, error) { usr, err := LookupUser(name) if err != nil { - return nil, fmt.Errorf("Could not get user for username %s: %v", name, err) + return IdentityMapping{}, fmt.Errorf("Could not get user for username %s: %v", name, err) } subuidRanges, err := lookupSubUIDRanges(usr) if err != nil { - return nil, err + return IdentityMapping{}, err } subgidRanges, err := lookupSubGIDRanges(usr) if err != nil { - return nil, err + return IdentityMapping{}, err } - return &IdentityMapping{ - uids: subuidRanges, - gids: subgidRanges, + return IdentityMapping{ + UIDMaps: subuidRanges, + GIDMaps: subgidRanges, }, nil } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index c43ed6fc..162d0257 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -321,10 +321,10 @@ func TestNewIDMappings(t *testing.T) { tempUser, err := user.Lookup(tempUser) assert.Check(t, err) - idMapping, err := NewIdentityMapping(tempUser.Username) + idMapping, err := LoadIdentityMapping(tempUser.Username) assert.Check(t, err) - rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDs(), idMapping.GIDs()) + rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDMaps, idMapping.GIDMaps) assert.Check(t, err) dirName, err := os.MkdirTemp("", "mkdirall") From 08c3d783201475e982970908bfb1c6ac377c4b35 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 9 Aug 2022 18:01:28 +0200 Subject: [PATCH 48/75] pkg/idtools: remove deprecated NewIdentityMapping, UIDS() and GIDS() These were deprecated in 0ff8db30c921d1f4359f5730c733a9b85123dc3f, which is in the 22.06 branch, and no longer in use since e05f614267213c93bd941d8a8980a0265a2e4634 so we can remove them from the master branch. Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 14 -------------- user/idtools_unix.go | 13 ------------- 2 files changed, 27 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 1e0a8900..79d682c6 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -162,20 +162,6 @@ func (i IdentityMapping) Empty() bool { return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } -// UIDs returns the mapping for UID. -// -// Deprecated: reference the UIDMaps field directly. -func (i IdentityMapping) UIDs() []IDMap { - return i.UIDMaps -} - -// GIDs returns the mapping for GID. -// -// Deprecated: reference the GIDMaps field directly. -func (i IdentityMapping) GIDs() []IDMap { - return i.GIDMaps -} - func createIDMap(subidRanges ranges) []IDMap { idMap := []IDMap{} diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 7a7ccc3e..2f7cac8c 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -237,19 +237,6 @@ func setPermissions(p string, mode os.FileMode, uid, gid int, stat *system.StatT return os.Chown(p, uid, gid) } -// NewIdentityMapping takes a requested username and -// using the data from /etc/sub{uid,gid} ranges, creates the -// proper uid and gid remapping ranges for that user/group pair -// -// Deprecated: Use LoadIdentityMapping. -func NewIdentityMapping(name string) (*IdentityMapping, error) { - m, err := LoadIdentityMapping(name) - if err != nil { - return nil, err - } - return &m, err -} - // LoadIdentityMapping takes a requested username and // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair From 9a4bd8a1096a8b48a7572f9831268e73811de155 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 27 Sep 2022 17:08:08 +0200 Subject: [PATCH 49/75] pkg/idtools: mkdirAs(): fix infinite loops and repeated "chown" This fixes an inifinite loop in mkdirAs(), used by `MkdirAllAndChown`, `MkdirAndChown`, and `MkdirAllAndChownNew`, as well as directories being chown'd multiple times when relative paths are used. The for loop in this function was incorrectly assuming that; 1. `filepath.Dir()` would always return the parent directory of any given path 2. traversing any given path to ultimately result in "/" While this is correct for absolute and "cleaned" paths, both assumptions are incorrect in some variations of "path"; 1. for paths with a trailing path-separator ("some/path/"), or dot ("."), `filepath.Dir()` considers the (implicit) "." to be a location _within_ the directory, and returns "some/path" as ("parent") directory. This resulted in the path itself to be included _twice_ in the list of paths to chown. 2. for relative paths ("./some-path", "../some-path"), "traversing" the path would never end in "/", causing the for loop to run indefinitely: ```go // walk back to "/" looking for directories which do not exist // and add them to the paths array for chown after creation dirPath := path for { dirPath = filepath.Dir(dirPath) if dirPath == "/" { break } if _, err := os.Stat(dirPath); err != nil && os.IsNotExist(err) { paths = append(paths, dirPath) } } ``` A _partial_ mitigation for this would be to use `filepath.Clean()` before using the path (while `filepath.Dir()` _does_ call `filepath.Clean()`, it only does so _after_ some processing, so only cleans the result). Doing so would prevent the double chown from happening, but would not prevent the "final" path to be "." or ".." (in the relative path case), still causing an infinite loop, or additional checks for "." / ".." to be needed. | path | filepath.Dir(path) | filepath.Dir(filepath.Clean(path)) | |----------------|--------------------|------------------------------------| | some-path | . | . | | ./some-path | . | . | | ../some-path | .. | .. | | some/path/ | some/path | some | | ./some/path/ | some/path | some | | ../some/path/ | ../some/path | ../some | | some/path/. | some/path | some | | ./some/path/. | some/path | some | | ../some/path/. | ../some/path | ../some | | /some/path/ | /some/path | /some | | /some/path/. | /some/path | /some | Instead, this patch adds a `filepath.Abs()` to the function, so make sure that paths are both cleaned, and not resulting in an infinite loop. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 4 ++ user/idtools_unix_test.go | 92 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 2f7cac8c..98330d2b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -30,6 +30,10 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting // chown the full directory path if it exists var paths []string + path, err := filepath.Abs(path) + if err != nil { + return err + } stat, err := system.Stat(path) if err == nil { diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 162d0257..32803477 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -127,6 +127,98 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, compareTrees(testTree, verifyTree)) } +func TestMkdirAllAndChownNewRelative(t *testing.T) { + RequiresRoot(t) + + tests := []struct { + in string + out []string + }{ + { + in: "dir1", + out: []string{"dir1"}, + }, + { + in: "dir2/subdir2", + out: []string{"dir2", "dir2/subdir2"}, + }, + { + in: "dir3/subdir3/", + out: []string{"dir3", "dir3/subdir3"}, + }, + { + in: "dir4/subdir4/.", + out: []string{"dir4", "dir4/subdir4"}, + }, + { + in: "dir5/././subdir5/", + out: []string{"dir5", "dir5/subdir5"}, + }, + { + in: "./dir6", + out: []string{"dir6"}, + }, + { + in: "./dir7/subdir7", + out: []string{"dir7", "dir7/subdir7"}, + }, + { + in: "./dir8/subdir8/", + out: []string{"dir8", "dir8/subdir8"}, + }, + { + in: "./dir9/subdir9/.", + out: []string{"dir9", "dir9/subdir9"}, + }, + { + in: "./dir10/././subdir10/", + out: []string{"dir10", "dir10/subdir10"}, + }, + } + + // Set the current working directory to the temp-dir, as we're + // testing relative paths. + tmpDir := t.TempDir() + setWorkingDirectory(t, tmpDir) + + const expectedUIDGID = 101 + + for _, tc := range tests { + tc := tc + t.Run(tc.in, func(t *testing.T) { + for _, p := range tc.out { + _, err := os.Stat(p) + assert.ErrorIs(t, err, os.ErrNotExist) + } + + err := MkdirAllAndChownNew(tc.in, 0755, Identity{UID: expectedUIDGID, GID: expectedUIDGID}) + assert.Check(t, err) + + for _, p := range tc.out { + s := &unix.Stat_t{} + err = unix.Stat(p, s) + if assert.Check(t, err) { + assert.Check(t, is.Equal(uint64(s.Uid), uint64(expectedUIDGID))) + assert.Check(t, is.Equal(uint64(s.Gid), uint64(expectedUIDGID))) + } + } + }) + } +} + +// Change the current working directory for the duration of the test. This may +// break if tests are run in parallel. +func setWorkingDirectory(t *testing.T, dir string) { + t.Helper() + cwd, err := os.Getwd() + assert.NilError(t, err) + t.Cleanup(func() { + assert.NilError(t, os.Chdir(cwd)) + }) + err = os.Chdir(dir) + assert.NilError(t, err) +} + func TestMkdirAndChown(t *testing.T) { RequiresRoot(t) dirName, err := os.MkdirTemp("", "mkdir") From b0b487cc129f39754e32fc1470f362dd69b7013a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 23 Sep 2022 20:16:47 +0200 Subject: [PATCH 50/75] pkg/*: fix "empty-lines" (revive) pkg/directory/directory.go:9:49: empty-lines: extra empty line at the start of a block (revive) pkg/pubsub/publisher.go:8:48: empty-lines: extra empty line at the start of a block (revive) pkg/loopback/attach_loopback.go:96:69: empty-lines: extra empty line at the start of a block (revive) pkg/devicemapper/devmapper_wrapper.go:136:48: empty-lines: extra empty line at the start of a block (revive) pkg/devicemapper/devmapper.go:391:35: empty-lines: extra empty line at the end of a block (revive) pkg/devicemapper/devmapper.go:676:35: empty-lines: extra empty line at the end of a block (revive) pkg/archive/changes_posix_test.go:15:38: empty-lines: extra empty line at the end of a block (revive) pkg/devicemapper/devmapper.go:241:51: empty-lines: extra empty line at the start of a block (revive) pkg/fileutils/fileutils_test.go:17:47: empty-lines: extra empty line at the end of a block (revive) pkg/fileutils/fileutils_test.go:34:48: empty-lines: extra empty line at the end of a block (revive) pkg/fileutils/fileutils_test.go:318:32: empty-lines: extra empty line at the end of a block (revive) pkg/tailfile/tailfile.go:171:6: empty-lines: extra empty line at the end of a block (revive) pkg/tarsum/fileinfosums_test.go:16:41: empty-lines: extra empty line at the end of a block (revive) pkg/tarsum/tarsum_test.go:198:42: empty-lines: extra empty line at the start of a block (revive) pkg/tarsum/tarsum_test.go:294:25: empty-lines: extra empty line at the start of a block (revive) pkg/tarsum/tarsum_test.go:407:34: empty-lines: extra empty line at the end of a block (revive) pkg/ioutils/fswriters_test.go:52:45: empty-lines: extra empty line at the end of a block (revive) pkg/ioutils/writers_test.go:24:39: empty-lines: extra empty line at the end of a block (revive) pkg/ioutils/bytespipe_test.go:78:26: empty-lines: extra empty line at the end of a block (revive) pkg/sysinfo/sysinfo_linux_test.go:13:37: empty-lines: extra empty line at the end of a block (revive) pkg/archive/archive_linux_test.go:57:64: empty-lines: extra empty line at the end of a block (revive) pkg/archive/changes.go:248:72: empty-lines: extra empty line at the start of a block (revive) pkg/archive/changes_posix_test.go:15:38: empty-lines: extra empty line at the end of a block (revive) pkg/archive/copy.go:248:124: empty-lines: extra empty line at the end of a block (revive) pkg/archive/diff_test.go:198:44: empty-lines: extra empty line at the end of a block (revive) pkg/archive/archive.go:304:12: empty-lines: extra empty line at the end of a block (revive) pkg/archive/archive.go:749:37: empty-lines: extra empty line at the end of a block (revive) pkg/archive/archive.go:812:81: empty-lines: extra empty line at the start of a block (revive) pkg/archive/copy_unix_test.go:347:34: empty-lines: extra empty line at the end of a block (revive) pkg/system/path.go:11:39: empty-lines: extra empty line at the end of a block (revive) pkg/system/meminfo_linux.go:29:21: empty-lines: extra empty line at the end of a block (revive) pkg/plugins/plugins.go:135:32: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/response.go:71:48: empty-lines: extra empty line at the start of a block (revive) pkg/authorization/api_test.go:18:51: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/middleware_test.go:23:44: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/middleware_unix_test.go:17:46: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/api_test.go:57:45: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/response.go:83:50: empty-lines: extra empty line at the start of a block (revive) pkg/authorization/api_test.go:66:47: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/middleware_unix_test.go:45:48: empty-lines: extra empty line at the end of a block (revive) pkg/authorization/response.go:145:75: empty-lines: extra empty line at the start of a block (revive) pkg/authorization/middleware_unix_test.go:56:51: empty-lines: extra empty line at the end of a block (revive) Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 1 - user/usergroupadd_linux.go | 1 - 2 files changed, 2 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 98330d2b..2758726c 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -213,7 +213,6 @@ func callGetent(database, key string) (io.Reader, error) { default: return nil, err } - } return bytes.NewReader(out), nil } diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index bf7ae056..3ad9255d 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -88,7 +88,6 @@ func addUser(name string) error { } func createSubordinateRanges(name string) error { - // first, we should verify that ranges weren't automatically created // by the distro tooling ranges, err := parseSubuid(name) From 966753b21f8b6b4d65f49e9b60c1388792a998dc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 16:20:33 +0200 Subject: [PATCH 51/75] pkg/system: move GetExitCode() to pkg/idtools, and un-export This utility was only used in a single place, and had no external consumers. Move it to where it's used. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 2758726c..0dc6119a 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "strconv" "sync" @@ -199,7 +200,7 @@ func callGetent(database, key string) (io.Reader, error) { } out, err := execCmd(getentCmd, database, key) if err != nil { - exitCode, errC := system.GetExitCode(err) + exitCode, errC := getExitCode(err) if errC != nil { return nil, err } @@ -217,6 +218,18 @@ func callGetent(database, key string) (io.Reader, error) { return bytes.NewReader(out), nil } +// getExitCode returns the ExitStatus of the specified error if its type is +// exec.ExitError, returns 0 and an error otherwise. +func getExitCode(err error) (int, error) { + exitCode := 0 + if exiterr, ok := err.(*exec.ExitError); ok { + if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { + return procExit.ExitStatus(), nil + } + } + return exitCode, fmt.Errorf("failed to get exit code") +} + // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the // dir is on an NFS share, so don't call chown unless we absolutely must. From bb49a85f584f61b3897fefe6b0148f7d45e2c4db Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 15:15:16 +0200 Subject: [PATCH 52/75] pkg/idtools: mkdirAs() be more explicit about ignored args on Windows Signed-off-by: Sebastiaan van Stijn --- user/idtools_windows.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 0f5aadd4..4f5b3bba 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -19,11 +19,8 @@ const ( // permissions aren't set through this path, the identity isn't utilized. // Ownership is handled elsewhere, but in the future could be support here // too. -func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { - if err := system.MkdirAll(path, mode); err != nil { - return err - } - return nil +func mkdirAs(path string, _ os.FileMode, _ Identity, _, _ bool) error { + return system.MkdirAll(path, 0) } // CanAccess takes a valid (existing) directory and a uid, gid pair and determines From 14acd6ddd923a8a5f3addf8c758ceebfcab97e87 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 8 Oct 2022 18:45:33 +0200 Subject: [PATCH 53/75] pkg/idtools: mkdirAs(): move var and comment to where it's used Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 0dc6119a..2a2fb73b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -25,12 +25,6 @@ var ( ) func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { - // make an array containing the original path asked for, plus (for mkAll == true) - // all path components leading up to the complete path that don't exist before we MkdirAll - // so that we can chown all of them properly at the end. If chownExisting is false, we won't - // chown the full directory path if it exists - - var paths []string path, err := filepath.Abs(path) if err != nil { return err @@ -49,6 +43,11 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting return setPermissions(path, mode, owner.UID, owner.GID, stat) } + // make an array containing the original path asked for, plus (for mkAll == true) + // all path components leading up to the complete path that don't exist before we MkdirAll + // so that we can chown all of them properly at the end. If chownExisting is false, we won't + // chown the full directory path if it exists + var paths []string if os.IsNotExist(err) { paths = []string{path} } From ac19915105f88e247245d67d053e238fb595325f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 15:19:01 +0200 Subject: [PATCH 54/75] pkg/idtools: remove unused CanAccess() stub for Windows Signed-off-by: Sebastiaan van Stijn --- user/idtools_windows.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 4f5b3bba..32953f45 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -22,10 +22,3 @@ const ( func mkdirAs(path string, _ os.FileMode, _ Identity, _, _ bool) error { return system.MkdirAll(path, 0) } - -// CanAccess takes a valid (existing) directory and a uid, gid pair and determines -// if that uid, gid pair has access (execute bit) to the directory -// Windows does not require/support this function, so always return true -func CanAccess(path string, identity Identity) bool { - return true -} From b8ca2ace66332f1b8a793436f00c69ef61270657 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 15:24:51 +0200 Subject: [PATCH 55/75] pkg/idtools: CanAccess(): reorder checks to allow early return Merge the accessible() function into CanAccess, and check world- readable permissions first, before checking owner and group. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 2a2fb73b..5ecd2619 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -90,20 +90,17 @@ func CanAccess(path string, pair Identity) bool { if err != nil { return false } - fileMode := os.FileMode(statInfo.Mode()) - permBits := fileMode.Perm() - return accessible(statInfo.UID() == uint32(pair.UID), - statInfo.GID() == uint32(pair.GID), permBits) -} - -func accessible(isOwner, isGroup bool, perms os.FileMode) bool { - if isOwner && (perms&0100 == 0100) { + perms := os.FileMode(statInfo.Mode()).Perm() + if perms&0o001 == 0o001 { + // world access return true } - if isGroup && (perms&0010 == 0010) { + if statInfo.UID() == uint32(pair.UID) && (perms&0o100 == 0o100) { + // owner access. return true } - if perms&0001 == 0001 { + if statInfo.GID() == uint32(pair.GID) && (perms&0o010 == 0o010) { + // group access. return true } return false From 623c876d7bc8be013440c381ef971073fbade7ef Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 8 Oct 2022 21:55:04 +0200 Subject: [PATCH 56/75] pkg/idtools: format code with gofumpt Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix_test.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 32803477..37355e0d 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -46,7 +46,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0755, Identity{UID: 99, GID: 99}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr/share"] = node{99, 99} @@ -59,7 +59,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test 2-deep new directories--both should be owned by the uid/gid pair - if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0755, Identity{UID: 101, GID: 101}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}); err != nil { t.Fatal(err) } testTree["lib/some"] = node{101, 101} @@ -73,7 +73,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test a directory that already exists; should be chowned, but nothing else - if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0755, Identity{UID: 102, GID: 102}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr"] = node{102, 102} @@ -102,7 +102,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, buildTree(dirName, testTree)) // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0755, Identity{UID: 99, GID: 99}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}) assert.NilError(t, err) testTree["usr/share"] = node{99, 99} @@ -111,7 +111,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, compareTrees(testTree, verifyTree)) // test 2-deep new directories--both should be owned by the uid/gid pair - err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0755, Identity{UID: 101, GID: 101}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}) assert.NilError(t, err) testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} @@ -120,7 +120,7 @@ func TestMkdirAllAndChownNew(t *testing.T) { assert.NilError(t, compareTrees(testTree, verifyTree)) // test a directory that already exists; should NOT be chowned - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0755, Identity{UID: 102, GID: 102}) + err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}) assert.NilError(t, err) verifyTree, err = readTree(dirName, "") assert.NilError(t, err) @@ -191,7 +191,7 @@ func TestMkdirAllAndChownNewRelative(t *testing.T) { assert.ErrorIs(t, err, os.ErrNotExist) } - err := MkdirAllAndChownNew(tc.in, 0755, Identity{UID: expectedUIDGID, GID: expectedUIDGID}) + err := MkdirAllAndChownNew(tc.in, 0o755, Identity{UID: expectedUIDGID, GID: expectedUIDGID}) assert.Check(t, err) for _, p := range tc.out { @@ -235,7 +235,7 @@ func TestMkdirAndChown(t *testing.T) { } // test a directory that already exists; should just chown to the requested uid/gid - if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0755, Identity{UID: 99, GID: 99}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 99, GID: 99}); err != nil { t.Fatal(err) } testTree["usr"] = node{99, 99} @@ -248,12 +248,12 @@ func TestMkdirAndChown(t *testing.T) { } // create a subdir under a dir which doesn't exist--should fail - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0755, Identity{UID: 102, GID: 102}); err == nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0o755, Identity{UID: 102, GID: 102}); err == nil { t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") } // create a subdir under an existing dir; should only change the ownership of the new subdir - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0755, Identity{UID: 102, GID: 102}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0o755, Identity{UID: 102, GID: 102}); err != nil { t.Fatal(err) } testTree["usr/bin"] = node{102, 102} @@ -269,7 +269,7 @@ func TestMkdirAndChown(t *testing.T) { func buildTree(base string, tree map[string]node) error { for path, node := range tree { fullPath := filepath.Join(base, path) - if err := os.MkdirAll(fullPath, 0755); err != nil { + if err := os.MkdirAll(fullPath, 0o755); err != nil { return fmt.Errorf("Couldn't create path: %s; error: %v", fullPath, err) } if err := os.Chown(fullPath, node.uid, node.gid); err != nil { @@ -340,7 +340,7 @@ func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { # empty default subuid/subgid file dockremap:231072:65536` - if err := os.WriteFile(fnamePath, []byte(fcontent), 0644); err != nil { + if err := os.WriteFile(fnamePath, []byte(fcontent), 0o644); err != nil { t.Fatal(err) } ranges, err := parseSubidFile(fnamePath, "dockremap") @@ -423,7 +423,7 @@ func TestNewIDMappings(t *testing.T) { assert.Check(t, err, "Couldn't create temp directory") defer os.RemoveAll(dirName) - err = MkdirAllAndChown(dirName, 0700, Identity{UID: rootUID, GID: rootGID}) + err = MkdirAllAndChown(dirName, 0o700, Identity{UID: rootUID, GID: rootGID}) assert.Check(t, err, "Couldn't change ownership of file path. Got error") assert.Check(t, CanAccess(dirName, idMapping.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) } @@ -475,7 +475,7 @@ func TestMkdirIsNotDir(t *testing.T) { } defer os.Remove(file.Name()) - err = mkdirAs(file.Name(), 0755, Identity{UID: 0, GID: 0}, false, false) + err = mkdirAs(file.Name(), 0o755, Identity{UID: 0, GID: 0}, false, false) assert.Check(t, is.Error(err, "mkdir "+file.Name()+": not a directory")) } From 4cbf553071ece8afdcef583611ef8d476700a392 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 15:27:20 +0200 Subject: [PATCH 57/75] pkg/idtools: don't use system.MkdirAll() where not needed On unix, it's an alias for os.MkdirAll, so remove its use to be more transparent what's being used. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 5ecd2619..e780d160 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -65,7 +65,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting paths = append(paths, dirPath) } } - if err := system.MkdirAll(path, mode); err != nil { + if err := os.MkdirAll(path, mode); err != nil { return err } } else { From d8a81d575120251e5b5265767eb42f25ced533f9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Oct 2022 15:48:46 +0200 Subject: [PATCH 58/75] pkg/idtools: cleanup errors Most of the package was using stdlib's errors package, so replacing two calls to pkg/errors with stdlib. Also fixing capitalization of error strings. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 7 +++---- user/idtools_unix_test.go | 12 ++++++------ user/usergroupadd_linux.go | 26 +++++++++++++------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index e780d160..e2c46464 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -16,7 +16,6 @@ import ( "github.com/docker/docker/pkg/system" "github.com/opencontainers/runc/libcontainer/user" - "github.com/pkg/errors" ) var ( @@ -255,7 +254,7 @@ func setPermissions(p string, mode os.FileMode, uid, gid int, stat *system.StatT func LoadIdentityMapping(name string) (IdentityMapping, error) { usr, err := LookupUser(name) if err != nil { - return IdentityMapping{}, fmt.Errorf("Could not get user for username %s: %v", name, err) + return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) } subuidRanges, err := lookupSubUIDRanges(usr) @@ -285,7 +284,7 @@ func lookupSubUIDRanges(usr user.User) ([]IDMap, error) { } } if len(rangeList) == 0 { - return nil, errors.Errorf("no subuid ranges found for user %q", usr.Name) + return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name) } return createIDMap(rangeList), nil } @@ -302,7 +301,7 @@ func lookupSubGIDRanges(usr user.User) ([]IDMap, error) { } } if len(rangeList) == 0 { - return nil, errors.Errorf("no subgid ranges found for user %q", usr.Name) + return nil, fmt.Errorf("no subgid ranges found for user %q", usr.Name) } return createIDMap(rangeList), nil } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 37355e0d..0979daf9 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -270,10 +270,10 @@ func buildTree(base string, tree map[string]node) error { for path, node := range tree { fullPath := filepath.Join(base, path) if err := os.MkdirAll(fullPath, 0o755); err != nil { - return fmt.Errorf("Couldn't create path: %s; error: %v", fullPath, err) + return fmt.Errorf("couldn't create path: %s; error: %v", fullPath, err) } if err := os.Chown(fullPath, node.uid, node.gid); err != nil { - return fmt.Errorf("Couldn't chown path: %s; error: %v", fullPath, err) + return fmt.Errorf("couldn't chown path: %s; error: %v", fullPath, err) } } return nil @@ -284,13 +284,13 @@ func readTree(base, root string) (map[string]node, error) { dirInfos, err := os.ReadDir(base) if err != nil { - return nil, fmt.Errorf("Couldn't read directory entries for %q: %v", base, err) + return nil, fmt.Errorf("couldn't read directory entries for %q: %v", base, err) } for _, info := range dirInfos { s := &unix.Stat_t{} if err := unix.Stat(filepath.Join(base, info.Name()), s); err != nil { - return nil, fmt.Errorf("Can't stat file %q: %v", filepath.Join(base, info.Name()), err) + return nil, fmt.Errorf("can't stat file %q: %v", filepath.Join(base, info.Name()), err) } tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} if info.IsDir() { @@ -309,7 +309,7 @@ func readTree(base, root string) (map[string]node, error) { func compareTrees(left, right map[string]node) error { if len(left) != len(right) { - return fmt.Errorf("Trees aren't the same size") + return fmt.Errorf("trees aren't the same size") } for path, nodeLeft := range left { if nodeRight, ok := right[path]; ok { @@ -425,7 +425,7 @@ func TestNewIDMappings(t *testing.T) { err = MkdirAllAndChown(dirName, 0o700, Identity{UID: rootUID, GID: rootGID}) assert.Check(t, err, "Couldn't change ownership of file path. Got error") - assert.Check(t, CanAccess(dirName, idMapping.RootPair()), fmt.Sprintf("Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID)) + assert.Check(t, CanAccess(dirName, idMapping.RootPair()), "Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID) } func TestLookupUserAndGroup(t *testing.T) { diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 3ad9255d..24bb86c1 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -32,21 +32,21 @@ const ( // mapping ranges in containers. func AddNamespaceRangesUser(name string) (int, int, error) { if err := addUser(name); err != nil { - return -1, -1, fmt.Errorf("Error adding user %q: %v", name, err) + return -1, -1, fmt.Errorf("error adding user %q: %v", name, err) } // Query the system for the created uid and gid pair out, err := execCmd("id", name) if err != nil { - return -1, -1, fmt.Errorf("Error trying to find uid/gid for new user %q: %v", name, err) + return -1, -1, fmt.Errorf("error trying to find uid/gid for new user %q: %v", name, err) } matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) if len(matches) != 3 { - return -1, -1, fmt.Errorf("Can't find uid, gid from `id` output: %q", string(out)) + return -1, -1, fmt.Errorf("can't find uid, gid from `id` output: %q", string(out)) } uid, err := strconv.Atoi(matches[1]) if err != nil { - return -1, -1, fmt.Errorf("Can't convert found uid (%s) to int: %v", matches[1], err) + return -1, -1, fmt.Errorf("can't convert found uid (%s) to int: %v", matches[1], err) } gid, err := strconv.Atoi(matches[2]) if err != nil { @@ -57,7 +57,7 @@ func AddNamespaceRangesUser(name string) (int, int, error) { // do not get auto-created ranges in subuid/subgid) if err := createSubordinateRanges(name); err != nil { - return -1, -1, fmt.Errorf("Couldn't create subordinate ID ranges: %v", err) + return -1, -1, fmt.Errorf("couldn't create subordinate ID ranges: %v", err) } return uid, gid, nil } @@ -92,33 +92,33 @@ func createSubordinateRanges(name string) error { // by the distro tooling ranges, err := parseSubuid(name) if err != nil { - return fmt.Errorf("Error while looking for subuid ranges for user %q: %v", name, err) + return fmt.Errorf("error while looking for subuid ranges for user %q: %v", name, err) } if len(ranges) == 0 { // no UID ranges; let's create one startID, err := findNextUIDRange() if err != nil { - return fmt.Errorf("Can't find available subuid range: %v", err) + return fmt.Errorf("can't find available subuid range: %v", err) } out, err := execCmd("usermod", "-v", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) if err != nil { - return fmt.Errorf("Unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) + return fmt.Errorf("unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) } } ranges, err = parseSubgid(name) if err != nil { - return fmt.Errorf("Error while looking for subgid ranges for user %q: %v", name, err) + return fmt.Errorf("error while looking for subgid ranges for user %q: %v", name, err) } if len(ranges) == 0 { // no GID ranges; let's create one startID, err := findNextGIDRange() if err != nil { - return fmt.Errorf("Can't find available subgid range: %v", err) + return fmt.Errorf("can't find available subgid range: %v", err) } out, err := execCmd("usermod", "-w", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) if err != nil { - return fmt.Errorf("Unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) + return fmt.Errorf("unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) } } return nil @@ -127,7 +127,7 @@ func createSubordinateRanges(name string) error { func findNextUIDRange() (int, error) { ranges, err := parseSubuid("ALL") if err != nil { - return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subuid file: %v", err) + return -1, fmt.Errorf("couldn't parse all ranges in /etc/subuid file: %v", err) } sort.Sort(ranges) return findNextRangeStart(ranges) @@ -136,7 +136,7 @@ func findNextUIDRange() (int, error) { func findNextGIDRange() (int, error) { ranges, err := parseSubgid("ALL") if err != nil { - return -1, fmt.Errorf("Couldn't parse all ranges in /etc/subgid file: %v", err) + return -1, fmt.Errorf("couldn't parse all ranges in /etc/subgid file: %v", err) } sort.Sort(ranges) return findNextRangeStart(ranges) From 45b8f6ce3ff215b119bcabbd4947c55ba3f57a95 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 8 Oct 2022 18:51:42 +0200 Subject: [PATCH 59/75] pkg/idtools: don't use system.Stat() on unix Looks like we don't need the abstraction, so we can reduce the dependency on pkg/system. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index e2c46464..dd39bc9b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -14,7 +14,6 @@ import ( "sync" "syscall" - "github.com/docker/docker/pkg/system" "github.com/opencontainers/runc/libcontainer/user" ) @@ -29,7 +28,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting return err } - stat, err := system.Stat(path) + stat, err := os.Stat(path) if err == nil { if !stat.IsDir() { return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} @@ -85,20 +84,21 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting // CanAccess takes a valid (existing) directory and a uid, gid pair and determines // if that uid, gid pair has access (execute bit) to the directory func CanAccess(path string, pair Identity) bool { - statInfo, err := system.Stat(path) + statInfo, err := os.Stat(path) if err != nil { return false } - perms := os.FileMode(statInfo.Mode()).Perm() + perms := statInfo.Mode().Perm() if perms&0o001 == 0o001 { // world access return true } - if statInfo.UID() == uint32(pair.UID) && (perms&0o100 == 0o100) { + ssi := statInfo.Sys().(*syscall.Stat_t) + if ssi.Uid == uint32(pair.UID) && (perms&0o100 == 0o100) { // owner access. return true } - if statInfo.GID() == uint32(pair.GID) && (perms&0o010 == 0o010) { + if ssi.Gid == uint32(pair.GID) && (perms&0o010 == 0o010) { // group access. return true } @@ -229,20 +229,21 @@ func getExitCode(err error) (int, error) { // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the // dir is on an NFS share, so don't call chown unless we absolutely must. // Likewise for setting permissions. -func setPermissions(p string, mode os.FileMode, uid, gid int, stat *system.StatT) error { +func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) error { if stat == nil { var err error - stat, err = system.Stat(p) + stat, err = os.Stat(p) if err != nil { return err } } - if os.FileMode(stat.Mode()).Perm() != mode.Perm() { + if stat.Mode().Perm() != mode.Perm() { if err := os.Chmod(p, mode.Perm()); err != nil { return err } } - if stat.UID() == uint32(uid) && stat.GID() == uint32(gid) { + ssi := stat.Sys().(*syscall.Stat_t) + if ssi.Uid == uint32(uid) && ssi.Gid == uint32(gid) { return nil } return os.Chown(p, uid, gid) From 54804cfdad683a9f96d70231743e6475a9b27eb1 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 9 Oct 2022 14:44:25 +0200 Subject: [PATCH 60/75] pkg/idtools: simplify if-statement Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index dd39bc9b..1965a3fa 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -59,17 +59,15 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting if dirPath == "/" { break } - if _, err := os.Stat(dirPath); err != nil && os.IsNotExist(err) { + if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) { paths = append(paths, dirPath) } } - if err := os.MkdirAll(path, mode); err != nil { - return err - } - } else { - if err := os.Mkdir(path, mode); err != nil && !os.IsExist(err) { + if err = os.MkdirAll(path, mode); err != nil { return err } + } else if err = os.Mkdir(path, mode); err != nil { + return err } // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll From 66de297599b12eb18dadd20644958d5bfb30ec4c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 9 Oct 2022 14:47:04 +0200 Subject: [PATCH 61/75] pkg/idtools: setPermissions() accept Identity as argument Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 1965a3fa..2bd8140b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -37,8 +37,8 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting return nil } - // short-circuit--we were called with an existing directory and chown was requested - return setPermissions(path, mode, owner.UID, owner.GID, stat) + // short-circuit -- we were called with an existing directory and chown was requested + return setPermissions(path, mode, owner, stat) } // make an array containing the original path asked for, plus (for mkAll == true) @@ -72,7 +72,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll for _, pathComponent := range paths { - if err := setPermissions(pathComponent, mode, owner.UID, owner.GID, nil); err != nil { + if err = setPermissions(pathComponent, mode, owner, nil); err != nil { return err } } @@ -227,7 +227,7 @@ func getExitCode(err error) (int, error) { // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the // dir is on an NFS share, so don't call chown unless we absolutely must. // Likewise for setting permissions. -func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) error { +func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error { if stat == nil { var err error stat, err = os.Stat(p) @@ -241,10 +241,10 @@ func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) } } ssi := stat.Sys().(*syscall.Stat_t) - if ssi.Uid == uint32(uid) && ssi.Gid == uint32(gid) { + if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) { return nil } - return os.Chown(p, uid, gid) + return os.Chown(p, owner.UID, owner.GID) } // LoadIdentityMapping takes a requested username and From 68d084cdde7e7835e2b63fb0771cd7219c3f39c5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 8 Oct 2022 22:23:41 +0200 Subject: [PATCH 62/75] pkg/idtools: remove CanAccess(), and move to daemon The implementation of CanAccess() is very rudimentary, and should not be used for anything other than a basic check (and maybe not even for that). It's only used in a single location in the daemon, so move it there, and un-export it to not encourage others to use it out of context. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 24 ------------------------ user/idtools_unix_test.go | 9 ++++++++- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 2bd8140b..7d31c69d 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -79,30 +79,6 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting return nil } -// CanAccess takes a valid (existing) directory and a uid, gid pair and determines -// if that uid, gid pair has access (execute bit) to the directory -func CanAccess(path string, pair Identity) bool { - statInfo, err := os.Stat(path) - if err != nil { - return false - } - perms := statInfo.Mode().Perm() - if perms&0o001 == 0o001 { - // world access - return true - } - ssi := statInfo.Sys().(*syscall.Stat_t) - if ssi.Uid == uint32(pair.UID) && (perms&0o100 == 0o100) { - // owner access. - return true - } - if ssi.Gid == uint32(pair.GID) && (perms&0o010 == 0o010) { - // group access. - return true - } - return false -} - // LookupUser uses traditional local system files lookup (from libcontainer/user) on a username, // followed by a call to `getent` for supporting host configured non-files passwd and group dbs func LookupUser(name string) (user.User, error) { diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 0979daf9..1e65e2af 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -6,8 +6,10 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" "os" + "os/exec" "os/user" "path/filepath" + "syscall" "testing" "golang.org/x/sys/unix" @@ -425,7 +427,12 @@ func TestNewIDMappings(t *testing.T) { err = MkdirAllAndChown(dirName, 0o700, Identity{UID: rootUID, GID: rootGID}) assert.Check(t, err, "Couldn't change ownership of file path. Got error") - assert.Check(t, CanAccess(dirName, idMapping.RootPair()), "Unable to access %s directory with user UID:%d and GID:%d", dirName, rootUID, rootGID) + cmd := exec.Command("ls", "-la", dirName) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: uint32(rootUID), Gid: uint32(rootGID)}, + } + out, err := cmd.CombinedOutput() + assert.Check(t, err, "Unable to access %s directory with user UID:%d and GID:%d:\n%s", dirName, rootUID, rootGID, string(out)) } func TestLookupUserAndGroup(t *testing.T) { From 5fb47b5e99df612ed2f7d991ae819b44a393aacf Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 9 Oct 2022 18:59:12 +0200 Subject: [PATCH 63/75] pkg/idtools: remove execCmd() utility The `execCmd()` utility was a basic wrapper around `exec.Command()`. Inlining it makes the code more transparent. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 2 +- user/idtools_unix_test.go | 4 ++-- user/usergroupadd_linux.go | 11 +++++++---- user/utils_unix.go | 5 ----- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 7d31c69d..72e9c08a 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -167,7 +167,7 @@ func callGetent(database, key string) (io.Reader, error) { if getentCmd == "" { return nil, fmt.Errorf("unable to find getent command") } - out, err := execCmd(getentCmd, database, key) + out, err := exec.Command(getentCmd, database, key).CombinedOutput() if err != nil { exitCode, errC := getExitCode(err) if errC != nil { diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 1e65e2af..5a4acc25 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -328,8 +328,8 @@ func compareTrees(left, right map[string]node) error { } func delUser(t *testing.T, name string) { - _, err := execCmd("userdel", name) - assert.Check(t, err) + out, err := exec.Command("userdel", name).CombinedOutput() + assert.Check(t, err, out) } func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 24bb86c1..f0c075e2 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -2,6 +2,7 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" + "os/exec" "regexp" "sort" "strconv" @@ -36,7 +37,7 @@ func AddNamespaceRangesUser(name string) (int, int, error) { } // Query the system for the created uid and gid pair - out, err := execCmd("id", name) + out, err := exec.Command("id", name).CombinedOutput() if err != nil { return -1, -1, fmt.Errorf("error trying to find uid/gid for new user %q: %v", name, err) } @@ -81,7 +82,7 @@ func addUser(name string) error { return fmt.Errorf("cannot add user; no useradd/adduser binary found") } - if out, err := execCmd(userCommand, args...); err != nil { + if out, err := exec.Command(userCommand, args...).CombinedOutput(); err != nil { return fmt.Errorf("failed to add user with error: %v; output: %q", err, string(out)) } return nil @@ -100,7 +101,8 @@ func createSubordinateRanges(name string) error { if err != nil { return fmt.Errorf("can't find available subuid range: %v", err) } - out, err := execCmd("usermod", "-v", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) + idRange := fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1) + out, err := exec.Command("usermod", "-v", idRange, name).CombinedOutput() if err != nil { return fmt.Errorf("unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) } @@ -116,7 +118,8 @@ func createSubordinateRanges(name string) error { if err != nil { return fmt.Errorf("can't find available subgid range: %v", err) } - out, err := execCmd("usermod", "-w", fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1), name) + idRange := fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1) + out, err := exec.Command("usermod", "-w", idRange, name).CombinedOutput() if err != nil { return fmt.Errorf("unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) } diff --git a/user/utils_unix.go b/user/utils_unix.go index 540672af..05cc6963 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -25,8 +25,3 @@ func resolveBinary(binname string) (string, error) { } return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) } - -func execCmd(cmd string, arg ...string) ([]byte, error) { - execCmd := exec.Command(cmd, arg...) - return execCmd.CombinedOutput() -} From 18279d1b54c4e110423d88cb50c6f45843dc995a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 5 May 2023 17:59:39 +0200 Subject: [PATCH 64/75] remove pre-go1.17 build-tags Removed pre-go1.17 build-tags with go fix; go mod init go fix -mod=readonly ./... rm go.mod Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 1 - user/idtools_unix_test.go | 1 - user/usergroupadd_unsupported.go | 1 - user/utils_unix.go | 1 - 4 files changed, 4 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 72e9c08a..2df4ae8f 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 5a4acc25..c9a2f64f 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go index 5e24577e..6a9311c4 100644 --- a/user/usergroupadd_unsupported.go +++ b/user/usergroupadd_unsupported.go @@ -1,5 +1,4 @@ //go:build !linux -// +build !linux package idtools // import "github.com/docker/docker/pkg/idtools" diff --git a/user/utils_unix.go b/user/utils_unix.go index 05cc6963..517a2f52 100644 --- a/user/utils_unix.go +++ b/user/utils_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows package idtools // import "github.com/docker/docker/pkg/idtools" From 3aca3436b9fb5b54a2619cae0a9d2874608f4d79 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Fri, 9 Jun 2023 15:21:44 +0200 Subject: [PATCH 65/75] run `getent` with a noop stdin Signed-off-by: Nicolas De Loof --- user/idtools_unix.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 2df4ae8f..a3a13600 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -166,7 +166,10 @@ func callGetent(database, key string) (io.Reader, error) { if getentCmd == "" { return nil, fmt.Errorf("unable to find getent command") } - out, err := exec.Command(getentCmd, database, key).CombinedOutput() + command := exec.Command(getentCmd, database, key) + // we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin + command.Stdin = io.NopCloser(bytes.NewReader(nil)) + out, err := command.CombinedOutput() if err != nil { exitCode, errC := getExitCode(err) if errC != nil { From a8e5f1c387df24dfcb295534f1c53655b2f75277 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 5 Jul 2023 12:24:24 +0200 Subject: [PATCH 66/75] pkg/idtools: use string-literals for easier grep'ing Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index c9a2f64f..24225d62 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -458,14 +458,14 @@ func TestLookupUserAndGroup(t *testing.T) { func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { fakeUser := "fakeuser" _, err := LookupUser(fakeUser) - assert.Check(t, is.Error(err, "getent unable to find entry \""+fakeUser+"\" in passwd database")) + assert.Check(t, is.Error(err, `getent unable to find entry "fakeuser" in passwd database`)) _, err = LookupUID(-1) assert.Check(t, is.ErrorContains(err, "")) fakeGroup := "fakegroup" _, err = LookupGroup(fakeGroup) - assert.Check(t, is.Error(err, "getent unable to find entry \""+fakeGroup+"\" in group database")) + assert.Check(t, is.Error(err, `getent unable to find entry "fakegroup" in group database`)) _, err = LookupGID(-1) assert.Check(t, is.ErrorContains(err, "")) From 8686e267271919d43dfb35916864d71d3c4fd1ea Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 6 Sep 2023 17:29:59 +0200 Subject: [PATCH 67/75] pkg/idtools: remove sync.Once, and include lookup error When running a `docker cp` to copy files to/from a container, the lookup of the `getent` executable happens within the container's filesystem, so we cannot re-use the results. Unfortunately, that also means we can't preserve the results for any other uses of these functions, but probably the lookup should not be "too" costly. Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index a3a13600..01f4c40b 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -10,17 +10,11 @@ import ( "os/exec" "path/filepath" "strconv" - "sync" "syscall" "github.com/opencontainers/runc/libcontainer/user" ) -var ( - entOnce sync.Once - getentCmd string -) - func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { path, err := filepath.Abs(path) if err != nil { @@ -161,10 +155,10 @@ func getentGroup(name string) (user.Group, error) { } func callGetent(database, key string) (io.Reader, error) { - entOnce.Do(func() { getentCmd, _ = resolveBinary("getent") }) - // if no `getent` command on host, can't do anything else - if getentCmd == "" { - return nil, fmt.Errorf("unable to find getent command") + getentCmd, err := resolveBinary("getent") + // if no `getent` command within the execution environment, can't do anything else + if err != nil { + return nil, fmt.Errorf("unable to find getent command: %w", err) } command := exec.Command(getentCmd, database, key) // we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin From 94b31f9f9f937ae45f9357ea153ecf42991569cf Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 24 Oct 2023 15:45:02 +0200 Subject: [PATCH 68/75] migrate to github.com/moby/sys/user The github.com/opencontainers/runc/libcontainer/user package was moved to a separate module. While there's still uses of the old module in our code-base, runc itself is migrating to the new module, and deprecated the old package (for runc 1.2). Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 01f4c40b..cd621bdc 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -12,7 +12,7 @@ import ( "strconv" "syscall" - "github.com/opencontainers/runc/libcontainer/user" + "github.com/moby/sys/user" ) func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { From be0024876a4226bfc88f7ecda60cc561fcbd658a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 5 Nov 2024 19:21:45 +0100 Subject: [PATCH 69/75] pkg/idtools: fix shadowed variable (govet) pkg/idtools/usergroupadd_linux.go:94:2: shadow: declaration of "ranges" shadows declaration at line 25 (govet) ranges, err := parseSubuid(name) ^ pkg/idtools/usergroupadd_linux.go:131:2: shadow: declaration of "ranges" shadows declaration at line 25 (govet) ranges, err := parseSubuid("ALL") ^ pkg/idtools/usergroupadd_linux.go:140:2: shadow: declaration of "ranges" shadows declaration at line 25 (govet) ranges, err := parseSubgid("ALL") ^ Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 22 +++++++++++----------- user/idtools_test.go | 2 +- user/usergroupadd_linux.go | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index 79d682c6..82b325a2 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -22,11 +22,11 @@ type subIDRange struct { Length int } -type ranges []subIDRange +type subIDRanges []subIDRange -func (e ranges) Len() int { return len(e) } -func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } -func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start } +func (e subIDRanges) Len() int { return len(e) } +func (e subIDRanges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } +func (e subIDRanges) Less(i, j int) bool { return e[i].Start < e[j].Start } const ( subuidFileName = "/etc/subuid" @@ -162,7 +162,7 @@ func (i IdentityMapping) Empty() bool { return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } -func createIDMap(subidRanges ranges) []IDMap { +func createIDMap(subidRanges subIDRanges) []IDMap { idMap := []IDMap{} containerID := 0 @@ -177,19 +177,19 @@ func createIDMap(subidRanges ranges) []IDMap { return idMap } -func parseSubuid(username string) (ranges, error) { +func parseSubuid(username string) (subIDRanges, error) { return parseSubidFile(subuidFileName, username) } -func parseSubgid(username string) (ranges, error) { +func parseSubgid(username string) (subIDRanges, error) { return parseSubidFile(subgidFileName, username) } // parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) -// and return all found ranges for a specified username. If the special value -// "ALL" is supplied for username, then all ranges in the file will be returned -func parseSubidFile(path, username string) (ranges, error) { - var rangeList ranges +// and return all found subIDRanges for a specified username. If the special value +// "ALL" is supplied for username, then all subIDRanges in the file will be returned +func parseSubidFile(path, username string) (subIDRanges, error) { + var rangeList subIDRanges subidFile, err := os.Open(path) if err != nil { diff --git a/user/idtools_test.go b/user/idtools_test.go index 34b57b6b..72ff779b 100644 --- a/user/idtools_test.go +++ b/user/idtools_test.go @@ -7,7 +7,7 @@ import ( ) func TestCreateIDMapOrder(t *testing.T) { - subidRanges := ranges{ + subidRanges := subIDRanges{ {100000, 1000}, {1000, 1}, } diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index f0c075e2..7fd6c413 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -145,7 +145,7 @@ func findNextGIDRange() (int, error) { return findNextRangeStart(ranges) } -func findNextRangeStart(rangeList ranges) (int, error) { +func findNextRangeStart(rangeList subIDRanges) (int, error) { startID := defaultRangeStart for _, arange := range rangeList { if wouldOverlap(arange, startID) { From 758bc527bb56655a60331d2bcec75492e02c78a3 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 12 Nov 2024 14:00:16 +0100 Subject: [PATCH 70/75] pkg/idtools: remove redundant capturing of loop vars (copyloopvar) pkg/idtools/idtools_unix_test.go:188:3: The copy of the 'for' variable "tc" can be deleted (Go 1.22+) (copyloopvar) tc := tc ^ Signed-off-by: Sebastiaan van Stijn --- user/idtools_unix_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 24225d62..689ef525 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -185,7 +185,6 @@ func TestMkdirAllAndChownNewRelative(t *testing.T) { const expectedUIDGID = 101 for _, tc := range tests { - tc := tc t.Run(tc.in, func(t *testing.T) { for _, p := range tc.out { _, err := os.Stat(p) From 3d387597c5756811fe10b46f4eed6ef569cf4a57 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 21 Dec 2024 14:25:11 +0100 Subject: [PATCH 71/75] pkg/idtools: remove uses of deprecated system.MkdirAll Signed-off-by: Sebastiaan van Stijn --- user/idtools_windows.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 32953f45..5b7c2ad7 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -2,8 +2,6 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "os" - - "github.com/docker/docker/pkg/system" ) const ( @@ -15,10 +13,10 @@ const ( ContainerUserSidString = "S-1-5-93-2-2" ) -// This is currently a wrapper around MkdirAll, however, since currently +// This is currently a wrapper around [os.MkdirAll] since currently // permissions aren't set through this path, the identity isn't utilized. // Ownership is handled elsewhere, but in the future could be support here // too. func mkdirAs(path string, _ os.FileMode, _ Identity, _, _ bool) error { - return system.MkdirAll(path, 0) + return os.MkdirAll(path, 0) } From 5805d709fcfa73d5723bfabf52b0c159b187deb6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 15 Jul 2024 16:28:14 +0200 Subject: [PATCH 72/75] pkg/idtools: use lazyregexp to compile regexes on first use Signed-off-by: Sebastiaan van Stijn --- user/usergroupadd_linux.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index 7fd6c413..b1b90d4b 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -3,11 +3,12 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( "fmt" "os/exec" - "regexp" "sort" "strconv" "strings" "sync" + + "github.com/docker/docker/internal/lazyregexp" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard @@ -18,7 +19,7 @@ import ( var ( once sync.Once userCommand string - idOutRegexp = regexp.MustCompile(`uid=([0-9]+).*gid=([0-9]+)`) + idOutRegexp = lazyregexp.New(`uid=([0-9]+).*gid=([0-9]+)`) ) const ( From 9ecbe7b59e079da529588d6abc89a28b1d961ca4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 7 Jan 2025 12:27:49 +0100 Subject: [PATCH 73/75] pkg/idtools: rewrite to use moby/sys/user Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 81 +++++--------------------------------- user/idtools_test.go | 28 ------------- user/idtools_unix.go | 44 +++++++++------------ user/idtools_unix_test.go | 18 +++++---- user/usergroupadd_linux.go | 33 ++++++++++------ 5 files changed, 59 insertions(+), 145 deletions(-) delete mode 100644 user/idtools_test.go diff --git a/user/idtools.go b/user/idtools.go index 82b325a2..0bd36460 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -1,11 +1,10 @@ package idtools // import "github.com/docker/docker/pkg/idtools" import ( - "bufio" "fmt" "os" - "strconv" - "strings" + + "github.com/moby/sys/user" ) // IDMap contains a single entry for user namespace range remapping. An array @@ -17,17 +16,6 @@ type IDMap struct { Size int `json:"size"` } -type subIDRange struct { - Start int - Length int -} - -type subIDRanges []subIDRange - -func (e subIDRanges) Len() int { return len(e) } -func (e subIDRanges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } -func (e subIDRanges) Less(i, j int) bool { return e[i].Start < e[j].Start } - const ( subuidFileName = "/etc/subuid" subgidFileName = "/etc/subgid" @@ -162,65 +150,16 @@ func (i IdentityMapping) Empty() bool { return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } -func createIDMap(subidRanges subIDRanges) []IDMap { - idMap := []IDMap{} - - containerID := 0 - for _, idrange := range subidRanges { - idMap = append(idMap, IDMap{ - ContainerID: containerID, - HostID: idrange.Start, - Size: idrange.Length, - }) - containerID = containerID + idrange.Length - } - return idMap -} - -func parseSubuid(username string) (subIDRanges, error) { - return parseSubidFile(subuidFileName, username) -} - -func parseSubgid(username string) (subIDRanges, error) { - return parseSubidFile(subgidFileName, username) +func parseSubuid(username string) ([]user.SubID, error) { + return user.ParseSubIDFileFilter(subuidFileName, func(sid user.SubID) bool { + return sid.Name == username + }) } -// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) -// and return all found subIDRanges for a specified username. If the special value -// "ALL" is supplied for username, then all subIDRanges in the file will be returned -func parseSubidFile(path, username string) (subIDRanges, error) { - var rangeList subIDRanges - - subidFile, err := os.Open(path) - if err != nil { - return rangeList, err - } - defer subidFile.Close() - - s := bufio.NewScanner(subidFile) - for s.Scan() { - text := strings.TrimSpace(s.Text()) - if text == "" || strings.HasPrefix(text, "#") { - continue - } - parts := strings.Split(text, ":") - if len(parts) != 3 { - return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) - } - if parts[0] == username || username == "ALL" { - startid, err := strconv.Atoi(parts[1]) - if err != nil { - return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) - } - length, err := strconv.Atoi(parts[2]) - if err != nil { - return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) - } - rangeList = append(rangeList, subIDRange{startid, length}) - } - } - - return rangeList, s.Err() +func parseSubgid(username string) ([]user.SubID, error) { + return user.ParseSubIDFileFilter(subgidFileName, func(sid user.SubID) bool { + return sid.Name == username + }) } // CurrentIdentity returns the identity of the current process diff --git a/user/idtools_test.go b/user/idtools_test.go deleted file mode 100644 index 72ff779b..00000000 --- a/user/idtools_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package idtools // import "github.com/docker/docker/pkg/idtools" - -import ( - "testing" - - "gotest.tools/v3/assert" -) - -func TestCreateIDMapOrder(t *testing.T) { - subidRanges := subIDRanges{ - {100000, 1000}, - {1000, 1}, - } - - idMap := createIDMap(subidRanges) - assert.DeepEqual(t, idMap, []IDMap{ - { - ContainerID: 0, - HostID: 100000, - Size: 1000, - }, - { - ContainerID: 1000, - HostID: 1000, - Size: 1, - }, - }) -} diff --git a/user/idtools_unix.go b/user/idtools_unix.go index cd621bdc..e0453383 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -228,11 +228,11 @@ func LoadIdentityMapping(name string) (IdentityMapping, error) { return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) } - subuidRanges, err := lookupSubUIDRanges(usr) + subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr) if err != nil { return IdentityMapping{}, err } - subgidRanges, err := lookupSubGIDRanges(usr) + subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr) if err != nil { return IdentityMapping{}, err } @@ -243,36 +243,28 @@ func LoadIdentityMapping(name string) (IdentityMapping, error) { }, nil } -func lookupSubUIDRanges(usr user.User) ([]IDMap, error) { - rangeList, err := parseSubuid(strconv.Itoa(usr.Uid)) +func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) { + uidstr := strconv.Itoa(usr.Uid) + rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool { + return sid.Name == usr.Name || sid.Name == uidstr + }) if err != nil { return nil, err } - if len(rangeList) == 0 { - rangeList, err = parseSubuid(usr.Name) - if err != nil { - return nil, err - } - } if len(rangeList) == 0 { return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name) } - return createIDMap(rangeList), nil -} -func lookupSubGIDRanges(usr user.User) ([]IDMap, error) { - rangeList, err := parseSubgid(strconv.Itoa(usr.Uid)) - if err != nil { - return nil, err - } - if len(rangeList) == 0 { - rangeList, err = parseSubgid(usr.Name) - if err != nil { - return nil, err - } - } - if len(rangeList) == 0 { - return nil, fmt.Errorf("no subgid ranges found for user %q", usr.Name) + idMap := []IDMap{} + + containerID := 0 + for _, idrange := range rangeList { + idMap = append(idMap, IDMap{ + ContainerID: containerID, + HostID: int(idrange.SubID), + Size: int(idrange.Count), + }) + containerID = containerID + int(idrange.Count) } - return createIDMap(rangeList), nil + return idMap, nil } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 689ef525..7ec219f6 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -6,7 +6,7 @@ import ( "fmt" "os" "os/exec" - "os/user" + stduser "os/user" "path/filepath" "syscall" "testing" @@ -15,6 +15,8 @@ import ( "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" + + "github.com/moby/sys/user" ) const ( @@ -343,18 +345,20 @@ dockremap:231072:65536` if err := os.WriteFile(fnamePath, []byte(fcontent), 0o644); err != nil { t.Fatal(err) } - ranges, err := parseSubidFile(fnamePath, "dockremap") + ranges, err := user.ParseSubIDFileFilter(fnamePath, func(sid user.SubID) bool { + return sid.Name == "dockremap" + }) if err != nil { t.Fatal(err) } if len(ranges) != 1 { t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges)) } - if ranges[0].Start != 231072 { - t.Fatalf("wanted 231072, got %d instead", ranges[0].Start) + if ranges[0].SubID != 231072 { + t.Fatalf("wanted 231072, got %d instead", ranges[0].SubID) } - if ranges[0].Length != 65536 { - t.Fatalf("wanted 65536, got %d instead", ranges[0].Length) + if ranges[0].Count != 65536 { + t.Fatalf("wanted 65536, got %d instead", ranges[0].Count) } } @@ -410,7 +414,7 @@ func TestNewIDMappings(t *testing.T) { assert.Check(t, err) defer delUser(t, tempUser) - tempUser, err := user.Lookup(tempUser) + tempUser, err := stduser.Lookup(tempUser) assert.Check(t, err) idMapping, err := LoadIdentityMapping(tempUser.Username) diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go index b1b90d4b..8a9cb65c 100644 --- a/user/usergroupadd_linux.go +++ b/user/usergroupadd_linux.go @@ -9,6 +9,7 @@ import ( "sync" "github.com/docker/docker/internal/lazyregexp" + "github.com/moby/sys/user" ) // add a user and/or group to Linux /etc/passwd, /etc/group using standard @@ -129,38 +130,44 @@ func createSubordinateRanges(name string) error { } func findNextUIDRange() (int, error) { - ranges, err := parseSubuid("ALL") + ranges, err := user.CurrentUserSubUIDs() if err != nil { return -1, fmt.Errorf("couldn't parse all ranges in /etc/subuid file: %v", err) } - sort.Sort(ranges) + sortRanges(ranges) return findNextRangeStart(ranges) } func findNextGIDRange() (int, error) { - ranges, err := parseSubgid("ALL") + ranges, err := user.CurrentUserSubGIDs() if err != nil { return -1, fmt.Errorf("couldn't parse all ranges in /etc/subgid file: %v", err) } - sort.Sort(ranges) + sortRanges(ranges) return findNextRangeStart(ranges) } -func findNextRangeStart(rangeList subIDRanges) (int, error) { - startID := defaultRangeStart +func sortRanges(ranges []user.SubID) { + sort.Slice(ranges, func(i, j int) bool { + return ranges[i].SubID < ranges[j].SubID + }) +} + +func findNextRangeStart(rangeList []user.SubID) (int, error) { + var startID int64 = defaultRangeStart for _, arange := range rangeList { if wouldOverlap(arange, startID) { - startID = arange.Start + arange.Length + startID = arange.SubID + arange.Count } } - return startID, nil + return int(startID), nil } -func wouldOverlap(arange subIDRange, ID int) bool { - low := ID - high := ID + defaultRangeLen - if (low >= arange.Start && low <= arange.Start+arange.Length) || - (high <= arange.Start+arange.Length && high >= arange.Start) { +func wouldOverlap(arange user.SubID, ID int64) bool { + var low int64 = ID + var high int64 = ID + defaultRangeLen + if (low >= arange.SubID && low <= arange.SubID+arange.Count) || + (high <= arange.SubID+arange.Count && high >= arange.SubID) { return true } return false From 6134528784119fc5a8bc155f77ebaefb9df9a615 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 12 Dec 2024 20:46:11 -0800 Subject: [PATCH 74/75] Split internal idtools functionality Separare idtools functionality that is used internally from the functionlality used by importers. The `pkg/idtools` package is now much smaller and more generic. Signed-off-by: Derek McGowan Signed-off-by: Sebastiaan van Stijn --- user/idtools.go | 21 +--- user/idtools_unix.go | 132 +++-------------------- user/idtools_unix_test.go | 112 +------------------- user/idtools_windows.go | 8 +- user/usergroupadd_linux.go | 174 ------------------------------- user/usergroupadd_unsupported.go | 12 --- user/utils_unix.go | 26 ----- 7 files changed, 22 insertions(+), 463 deletions(-) delete mode 100644 user/usergroupadd_linux.go delete mode 100644 user/usergroupadd_unsupported.go delete mode 100644 user/utils_unix.go diff --git a/user/idtools.go b/user/idtools.go index 0bd36460..d2fbd943 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -1,10 +1,8 @@ -package idtools // import "github.com/docker/docker/pkg/idtools" +package idtools import ( "fmt" "os" - - "github.com/moby/sys/user" ) // IDMap contains a single entry for user namespace range remapping. An array @@ -16,11 +14,6 @@ type IDMap struct { Size int `json:"size"` } -const ( - subuidFileName = "/etc/subuid" - subgidFileName = "/etc/subgid" -) - // MkdirAllAndChown creates a directory (include any along the path) and then modifies // ownership to the requested uid/gid. If the directory already exists, this // function will still change ownership and permissions. @@ -150,18 +143,6 @@ func (i IdentityMapping) Empty() bool { return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } -func parseSubuid(username string) ([]user.SubID, error) { - return user.ParseSubIDFileFilter(subuidFileName, func(sid user.SubID) bool { - return sid.Name == username - }) -} - -func parseSubgid(username string) ([]user.SubID, error) { - return user.ParseSubIDFileFilter(subgidFileName, func(sid user.SubID) bool { - return sid.Name == username - }) -} - // CurrentIdentity returns the identity of the current process func CurrentIdentity() Identity { return Identity{UID: os.Getuid(), GID: os.Getegid()} diff --git a/user/idtools_unix.go b/user/idtools_unix.go index e0453383..1f11fe47 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -1,13 +1,10 @@ //go:build !windows -package idtools // import "github.com/docker/docker/pkg/idtools" +package idtools import ( - "bytes" "fmt" - "io" "os" - "os/exec" "path/filepath" "strconv" "syscall" @@ -72,127 +69,25 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting return nil } -// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username, -// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username +// +// Deprecated: use [user.LookupUser] instead func LookupUser(name string) (user.User, error) { - // first try a local system files lookup using existing capabilities - usr, err := user.LookupUser(name) - if err == nil { - return usr, nil - } - // local files lookup failed; attempt to call `getent` to query configured passwd dbs - usr, err = getentUser(name) - if err != nil { - return user.User{}, err - } - return usr, nil + return user.LookupUser(name) } -// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid, -// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid +// +// Deprecated: use [user.LookupUid] instead func LookupUID(uid int) (user.User, error) { - // first try a local system files lookup using existing capabilities - usr, err := user.LookupUid(uid) - if err == nil { - return usr, nil - } - // local files lookup failed; attempt to call `getent` to query configured passwd dbs - return getentUser(strconv.Itoa(uid)) -} - -func getentUser(name string) (user.User, error) { - reader, err := callGetent("passwd", name) - if err != nil { - return user.User{}, err - } - users, err := user.ParsePasswd(reader) - if err != nil { - return user.User{}, err - } - if len(users) == 0 { - return user.User{}, fmt.Errorf("getent failed to find passwd entry for %q", name) - } - return users[0], nil + return user.LookupUid(uid) } // LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name, -// followed by a call to `getent` for supporting host configured non-files passwd and group dbs +// +// Deprecated: use [user.LookupGroup] instead func LookupGroup(name string) (user.Group, error) { - // first try a local system files lookup using existing capabilities - group, err := user.LookupGroup(name) - if err == nil { - return group, nil - } - // local files lookup failed; attempt to call `getent` to query configured group dbs - return getentGroup(name) -} - -// LookupGID uses traditional local system files lookup (from libcontainer/user) on a group ID, -// followed by a call to `getent` for supporting host configured non-files passwd and group dbs -func LookupGID(gid int) (user.Group, error) { - // first try a local system files lookup using existing capabilities - group, err := user.LookupGid(gid) - if err == nil { - return group, nil - } - // local files lookup failed; attempt to call `getent` to query configured group dbs - return getentGroup(strconv.Itoa(gid)) -} - -func getentGroup(name string) (user.Group, error) { - reader, err := callGetent("group", name) - if err != nil { - return user.Group{}, err - } - groups, err := user.ParseGroup(reader) - if err != nil { - return user.Group{}, err - } - if len(groups) == 0 { - return user.Group{}, fmt.Errorf("getent failed to find groups entry for %q", name) - } - return groups[0], nil -} - -func callGetent(database, key string) (io.Reader, error) { - getentCmd, err := resolveBinary("getent") - // if no `getent` command within the execution environment, can't do anything else - if err != nil { - return nil, fmt.Errorf("unable to find getent command: %w", err) - } - command := exec.Command(getentCmd, database, key) - // we run getent within container filesystem, but without /dev so /dev/null is not available for exec to mock stdin - command.Stdin = io.NopCloser(bytes.NewReader(nil)) - out, err := command.CombinedOutput() - if err != nil { - exitCode, errC := getExitCode(err) - if errC != nil { - return nil, err - } - switch exitCode { - case 1: - return nil, fmt.Errorf("getent reported invalid parameters/database unknown") - case 2: - return nil, fmt.Errorf("getent unable to find entry %q in %s database", key, database) - case 3: - return nil, fmt.Errorf("getent database doesn't support enumeration") - default: - return nil, err - } - } - return bytes.NewReader(out), nil -} - -// getExitCode returns the ExitStatus of the specified error if its type is -// exec.ExitError, returns 0 and an error otherwise. -func getExitCode(err error) (int, error) { - exitCode := 0 - if exiterr, ok := err.(*exec.ExitError); ok { - if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { - return procExit.ExitStatus(), nil - } - } - return exitCode, fmt.Errorf("failed to get exit code") + return user.LookupGroup(name) } // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested @@ -223,7 +118,8 @@ func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo // using the data from /etc/sub{uid,gid} ranges, creates the // proper uid and gid remapping ranges for that user/group pair func LoadIdentityMapping(name string) (IdentityMapping, error) { - usr, err := LookupUser(name) + // TODO: Consider adding support for calling out to "getent" + usr, err := user.LookupUser(name) if err != nil { return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 7ec219f6..381a1d7a 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -1,26 +1,17 @@ //go:build !windows -package idtools // import "github.com/docker/docker/pkg/idtools" +package idtools import ( "fmt" "os" - "os/exec" - stduser "os/user" "path/filepath" - "syscall" "testing" "golang.org/x/sys/unix" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" - - "github.com/moby/sys/user" -) - -const ( - tempUser = "tempuser" ) type node struct { @@ -327,41 +318,6 @@ func compareTrees(left, right map[string]node) error { return nil } -func delUser(t *testing.T, name string) { - out, err := exec.Command("userdel", name).CombinedOutput() - assert.Check(t, err, out) -} - -func TestParseSubidFileWithNewlinesAndComments(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "parsesubid") - if err != nil { - t.Fatal(err) - } - fnamePath := filepath.Join(tmpDir, "testsubuid") - fcontent := `tss:100000:65536 -# empty default subuid/subgid file - -dockremap:231072:65536` - if err := os.WriteFile(fnamePath, []byte(fcontent), 0o644); err != nil { - t.Fatal(err) - } - ranges, err := user.ParseSubIDFileFilter(fnamePath, func(sid user.SubID) bool { - return sid.Name == "dockremap" - }) - if err != nil { - t.Fatal(err) - } - if len(ranges) != 1 { - t.Fatalf("wanted 1 element in ranges, got %d instead", len(ranges)) - } - if ranges[0].SubID != 231072 { - t.Fatalf("wanted 231072, got %d instead", ranges[0].SubID) - } - if ranges[0].Count != 65536 { - t.Fatalf("wanted 65536, got %d instead", ranges[0].Count) - } -} - func TestGetRootUIDGID(t *testing.T) { uidMap := []IDMap{ { @@ -408,72 +364,6 @@ func TestToContainer(t *testing.T) { assert.Check(t, is.Equal(uidMap[0].ContainerID, containerID)) } -func TestNewIDMappings(t *testing.T) { - RequiresRoot(t) - _, _, err := AddNamespaceRangesUser(tempUser) - assert.Check(t, err) - defer delUser(t, tempUser) - - tempUser, err := stduser.Lookup(tempUser) - assert.Check(t, err) - - idMapping, err := LoadIdentityMapping(tempUser.Username) - assert.Check(t, err) - - rootUID, rootGID, err := GetRootUIDGID(idMapping.UIDMaps, idMapping.GIDMaps) - assert.Check(t, err) - - dirName, err := os.MkdirTemp("", "mkdirall") - assert.Check(t, err, "Couldn't create temp directory") - defer os.RemoveAll(dirName) - - err = MkdirAllAndChown(dirName, 0o700, Identity{UID: rootUID, GID: rootGID}) - assert.Check(t, err, "Couldn't change ownership of file path. Got error") - cmd := exec.Command("ls", "-la", dirName) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{Uid: uint32(rootUID), Gid: uint32(rootGID)}, - } - out, err := cmd.CombinedOutput() - assert.Check(t, err, "Unable to access %s directory with user UID:%d and GID:%d:\n%s", dirName, rootUID, rootGID, string(out)) -} - -func TestLookupUserAndGroup(t *testing.T) { - RequiresRoot(t) - uid, gid, err := AddNamespaceRangesUser(tempUser) - assert.Check(t, err) - defer delUser(t, tempUser) - - fetchedUser, err := LookupUser(tempUser) - assert.Check(t, err) - - fetchedUserByID, err := LookupUID(uid) - assert.Check(t, err) - assert.Check(t, is.DeepEqual(fetchedUserByID, fetchedUser)) - - fetchedGroup, err := LookupGroup(tempUser) - assert.Check(t, err) - - fetchedGroupByID, err := LookupGID(gid) - assert.Check(t, err) - assert.Check(t, is.DeepEqual(fetchedGroupByID, fetchedGroup)) -} - -func TestLookupUserAndGroupThatDoesNotExist(t *testing.T) { - fakeUser := "fakeuser" - _, err := LookupUser(fakeUser) - assert.Check(t, is.Error(err, `getent unable to find entry "fakeuser" in passwd database`)) - - _, err = LookupUID(-1) - assert.Check(t, is.ErrorContains(err, "")) - - fakeGroup := "fakegroup" - _, err = LookupGroup(fakeGroup) - assert.Check(t, is.Error(err, `getent unable to find entry "fakegroup" in group database`)) - - _, err = LookupGID(-1) - assert.Check(t, is.ErrorContains(err, "")) -} - // TestMkdirIsNotDir checks that mkdirAs() function (used by MkdirAll...) // returns a correct error in case a directory which it is about to create // already exists but is a file (rather than a directory). diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 5b7c2ad7..43702f7f 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -1,16 +1,20 @@ -package idtools // import "github.com/docker/docker/pkg/idtools" +package idtools import ( "os" ) const ( + // Deprecated: copy value locally SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" ) const ( + // Deprecated: copy value locally ContainerAdministratorSidString = "S-1-5-93-2-1" - ContainerUserSidString = "S-1-5-93-2-2" + + // Deprecated: copy value locally + ContainerUserSidString = "S-1-5-93-2-2" ) // This is currently a wrapper around [os.MkdirAll] since currently diff --git a/user/usergroupadd_linux.go b/user/usergroupadd_linux.go deleted file mode 100644 index 8a9cb65c..00000000 --- a/user/usergroupadd_linux.go +++ /dev/null @@ -1,174 +0,0 @@ -package idtools // import "github.com/docker/docker/pkg/idtools" - -import ( - "fmt" - "os/exec" - "sort" - "strconv" - "strings" - "sync" - - "github.com/docker/docker/internal/lazyregexp" - "github.com/moby/sys/user" -) - -// add a user and/or group to Linux /etc/passwd, /etc/group using standard -// Linux distribution commands: -// adduser --system --shell /bin/false --disabled-login --disabled-password --no-create-home --group -// useradd -r -s /bin/false - -var ( - once sync.Once - userCommand string - idOutRegexp = lazyregexp.New(`uid=([0-9]+).*gid=([0-9]+)`) -) - -const ( - // default length for a UID/GID subordinate range - defaultRangeLen = 65536 - defaultRangeStart = 100000 -) - -// AddNamespaceRangesUser takes a username and uses the standard system -// utility to create a system user/group pair used to hold the -// /etc/sub{uid,gid} ranges which will be used for user namespace -// mapping ranges in containers. -func AddNamespaceRangesUser(name string) (int, int, error) { - if err := addUser(name); err != nil { - return -1, -1, fmt.Errorf("error adding user %q: %v", name, err) - } - - // Query the system for the created uid and gid pair - out, err := exec.Command("id", name).CombinedOutput() - if err != nil { - return -1, -1, fmt.Errorf("error trying to find uid/gid for new user %q: %v", name, err) - } - matches := idOutRegexp.FindStringSubmatch(strings.TrimSpace(string(out))) - if len(matches) != 3 { - return -1, -1, fmt.Errorf("can't find uid, gid from `id` output: %q", string(out)) - } - uid, err := strconv.Atoi(matches[1]) - if err != nil { - return -1, -1, fmt.Errorf("can't convert found uid (%s) to int: %v", matches[1], err) - } - gid, err := strconv.Atoi(matches[2]) - if err != nil { - return -1, -1, fmt.Errorf("Can't convert found gid (%s) to int: %v", matches[2], err) - } - - // Now we need to create the subuid/subgid ranges for our new user/group (system users - // do not get auto-created ranges in subuid/subgid) - - if err := createSubordinateRanges(name); err != nil { - return -1, -1, fmt.Errorf("couldn't create subordinate ID ranges: %v", err) - } - return uid, gid, nil -} - -func addUser(name string) error { - once.Do(func() { - // set up which commands are used for adding users/groups dependent on distro - if _, err := resolveBinary("adduser"); err == nil { - userCommand = "adduser" - } else if _, err := resolveBinary("useradd"); err == nil { - userCommand = "useradd" - } - }) - var args []string - switch userCommand { - case "adduser": - args = []string{"--system", "--shell", "/bin/false", "--no-create-home", "--disabled-login", "--disabled-password", "--group", name} - case "useradd": - args = []string{"-r", "-s", "/bin/false", name} - default: - return fmt.Errorf("cannot add user; no useradd/adduser binary found") - } - - if out, err := exec.Command(userCommand, args...).CombinedOutput(); err != nil { - return fmt.Errorf("failed to add user with error: %v; output: %q", err, string(out)) - } - return nil -} - -func createSubordinateRanges(name string) error { - // first, we should verify that ranges weren't automatically created - // by the distro tooling - ranges, err := parseSubuid(name) - if err != nil { - return fmt.Errorf("error while looking for subuid ranges for user %q: %v", name, err) - } - if len(ranges) == 0 { - // no UID ranges; let's create one - startID, err := findNextUIDRange() - if err != nil { - return fmt.Errorf("can't find available subuid range: %v", err) - } - idRange := fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1) - out, err := exec.Command("usermod", "-v", idRange, name).CombinedOutput() - if err != nil { - return fmt.Errorf("unable to add subuid range to user: %q; output: %s, err: %v", name, out, err) - } - } - - ranges, err = parseSubgid(name) - if err != nil { - return fmt.Errorf("error while looking for subgid ranges for user %q: %v", name, err) - } - if len(ranges) == 0 { - // no GID ranges; let's create one - startID, err := findNextGIDRange() - if err != nil { - return fmt.Errorf("can't find available subgid range: %v", err) - } - idRange := fmt.Sprintf("%d-%d", startID, startID+defaultRangeLen-1) - out, err := exec.Command("usermod", "-w", idRange, name).CombinedOutput() - if err != nil { - return fmt.Errorf("unable to add subgid range to user: %q; output: %s, err: %v", name, out, err) - } - } - return nil -} - -func findNextUIDRange() (int, error) { - ranges, err := user.CurrentUserSubUIDs() - if err != nil { - return -1, fmt.Errorf("couldn't parse all ranges in /etc/subuid file: %v", err) - } - sortRanges(ranges) - return findNextRangeStart(ranges) -} - -func findNextGIDRange() (int, error) { - ranges, err := user.CurrentUserSubGIDs() - if err != nil { - return -1, fmt.Errorf("couldn't parse all ranges in /etc/subgid file: %v", err) - } - sortRanges(ranges) - return findNextRangeStart(ranges) -} - -func sortRanges(ranges []user.SubID) { - sort.Slice(ranges, func(i, j int) bool { - return ranges[i].SubID < ranges[j].SubID - }) -} - -func findNextRangeStart(rangeList []user.SubID) (int, error) { - var startID int64 = defaultRangeStart - for _, arange := range rangeList { - if wouldOverlap(arange, startID) { - startID = arange.SubID + arange.Count - } - } - return int(startID), nil -} - -func wouldOverlap(arange user.SubID, ID int64) bool { - var low int64 = ID - var high int64 = ID + defaultRangeLen - if (low >= arange.SubID && low <= arange.SubID+arange.Count) || - (high <= arange.SubID+arange.Count && high >= arange.SubID) { - return true - } - return false -} diff --git a/user/usergroupadd_unsupported.go b/user/usergroupadd_unsupported.go deleted file mode 100644 index 6a9311c4..00000000 --- a/user/usergroupadd_unsupported.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !linux - -package idtools // import "github.com/docker/docker/pkg/idtools" - -import "fmt" - -// AddNamespaceRangesUser takes a name and finds an unused uid, gid pair -// and calls the appropriate helper function to add the group and then -// the user to the group in /etc/group and /etc/passwd respectively. -func AddNamespaceRangesUser(name string) (int, int, error) { - return -1, -1, fmt.Errorf("No support for adding users or groups on this OS") -} diff --git a/user/utils_unix.go b/user/utils_unix.go deleted file mode 100644 index 517a2f52..00000000 --- a/user/utils_unix.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build !windows - -package idtools // import "github.com/docker/docker/pkg/idtools" - -import ( - "fmt" - "os/exec" - "path/filepath" -) - -func resolveBinary(binname string) (string, error) { - binaryPath, err := exec.LookPath(binname) - if err != nil { - return "", err - } - resolvedPath, err := filepath.EvalSymlinks(binaryPath) - if err != nil { - return "", err - } - // only return no error if the final resolved binary basename - // matches what was searched for - if filepath.Base(resolvedPath) == binname { - return resolvedPath, nil - } - return "", fmt.Errorf("Binary %q does not resolve to a binary of that name in $PATH (%q)", binname, resolvedPath) -} From db55716d7f03d3b169b94d5f099405d857b5560b Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 10 Jan 2025 12:45:36 -0800 Subject: [PATCH 75/75] Update interface to fit into user package Removed duplicated structs Removed deprecated functionality Simplified calls which previously relied on unused functionality Signed-off-by: Derek McGowan --- user/idtools.go | 116 +++++++++----------- user/idtools_unix.go | 63 ++++------- user/idtools_unix_test.go | 223 ++++++++++++++++++++------------------ user/idtools_windows.go | 17 +-- 4 files changed, 195 insertions(+), 224 deletions(-) diff --git a/user/idtools.go b/user/idtools.go index d2fbd943..595b7a92 100644 --- a/user/idtools.go +++ b/user/idtools.go @@ -1,44 +1,53 @@ -package idtools +package user import ( "fmt" "os" ) -// IDMap contains a single entry for user namespace range remapping. An array -// of IDMap entries represents the structure that will be provided to the Linux -// kernel for creating a user namespace. -type IDMap struct { - ContainerID int `json:"container_id"` - HostID int `json:"host_id"` - Size int `json:"size"` +// MkdirOpt is a type for options to pass to Mkdir calls +type MkdirOpt func(*mkdirOptions) + +type mkdirOptions struct { + onlyNew bool +} + +// WithOnlyNew is an option for MkdirAllAndChown that will only change ownership and permissions +// on newly created directories. If the directory already exists, it will not be modified +func WithOnlyNew(o *mkdirOptions) { + o.onlyNew = true } // MkdirAllAndChown creates a directory (include any along the path) and then modifies -// ownership to the requested uid/gid. If the directory already exists, this -// function will still change ownership and permissions. -func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error { - return mkdirAs(path, mode, owner, true, true) +// ownership to the requested uid/gid. By default, if the directory already exists, this +// function will still change ownership and permissions. If WithOnlyNew is passed as an +// option, then only the newly created directories will have ownership and permissions changed. +func MkdirAllAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error { + var options mkdirOptions + for _, opt := range opts { + opt(&options) + } + + return mkdirAs(path, mode, uid, gid, true, options.onlyNew) } // MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. -// If the directory already exists, this function still changes ownership and permissions. +// By default, if the directory already exists, this function still changes ownership and permissions. +// If WithOnlyNew is passed as an option, then only the newly created directory will have ownership +// and permissions changed. // Note that unlike os.Mkdir(), this function does not return IsExist error // in case path already exists. -func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { - return mkdirAs(path, mode, owner, false, true) -} - -// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies -// ownership ONLY of newly created directories to the requested uid/gid. If the -// directories along the path exist, no change of ownership or permissions will be performed -func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error { - return mkdirAs(path, mode, owner, true, false) +func MkdirAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error { + var options mkdirOptions + for _, opt := range opts { + opt(&options) + } + return mkdirAs(path, mode, uid, gid, false, options.onlyNew) } -// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. +// getRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. // If the maps are empty, then the root uid/gid will default to "real" 0/0 -func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { +func getRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { uid, err := toHost(0, uidMap) if err != nil { return -1, -1, err @@ -58,12 +67,12 @@ func toContainer(hostID int, idMap []IDMap) (int, error) { return hostID, nil } for _, m := range idMap { - if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) { - contID := m.ContainerID + (hostID - m.HostID) + if (int64(hostID) >= m.ParentID) && (int64(hostID) <= (m.ParentID + m.Count - 1)) { + contID := int(m.ID + (int64(hostID) - m.ParentID)) return contID, nil } } - return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) + return -1, fmt.Errorf("host ID %d cannot be mapped to a container ID", hostID) } // toHost takes an id mapping and a remapped ID, and translates the @@ -74,24 +83,12 @@ func toHost(contID int, idMap []IDMap) (int, error) { return contID, nil } for _, m := range idMap { - if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) { - hostID := m.HostID + (contID - m.ContainerID) + if (int64(contID) >= m.ID) && (int64(contID) <= (m.ID + m.Count - 1)) { + hostID := int(m.ParentID + (int64(contID) - m.ID)) return hostID, nil } } - return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) -} - -// Identity is either a UID and GID pair or a SID (but not both) -type Identity struct { - UID int - GID int - SID string -} - -// Chown changes the numeric uid and gid of the named file to id.UID and id.GID. -func (id Identity) Chown(name string) error { - return os.Chown(name, id.UID, id.GID) + return -1, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID) } // IdentityMapping contains a mappings of UIDs and GIDs. @@ -104,46 +101,41 @@ type IdentityMapping struct { // RootPair returns a uid and gid pair for the root user. The error is ignored // because a root user always exists, and the defaults are correct when the uid // and gid maps are empty. -func (i IdentityMapping) RootPair() Identity { - uid, gid, _ := GetRootUIDGID(i.UIDMaps, i.GIDMaps) - return Identity{UID: uid, GID: gid} +func (i IdentityMapping) RootPair() (int, int) { + uid, gid, _ := getRootUIDGID(i.UIDMaps, i.GIDMaps) + return uid, gid } // ToHost returns the host UID and GID for the container uid, gid. // Remapping is only performed if the ids aren't already the remapped root ids -func (i IdentityMapping) ToHost(pair Identity) (Identity, error) { +func (i IdentityMapping) ToHost(uid, gid int) (int, int, error) { var err error - target := i.RootPair() + ruid, rgid := i.RootPair() - if pair.UID != target.UID { - target.UID, err = toHost(pair.UID, i.UIDMaps) + if uid != ruid { + ruid, err = toHost(uid, i.UIDMaps) if err != nil { - return target, err + return ruid, rgid, err } } - if pair.GID != target.GID { - target.GID, err = toHost(pair.GID, i.GIDMaps) + if gid != rgid { + rgid, err = toHost(gid, i.GIDMaps) } - return target, err + return ruid, rgid, err } // ToContainer returns the container UID and GID for the host uid and gid -func (i IdentityMapping) ToContainer(pair Identity) (int, int, error) { - uid, err := toContainer(pair.UID, i.UIDMaps) +func (i IdentityMapping) ToContainer(uid, gid int) (int, int, error) { + ruid, err := toContainer(uid, i.UIDMaps) if err != nil { return -1, -1, err } - gid, err := toContainer(pair.GID, i.GIDMaps) - return uid, gid, err + rgid, err := toContainer(gid, i.GIDMaps) + return ruid, rgid, err } // Empty returns true if there are no id mappings func (i IdentityMapping) Empty() bool { return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 } - -// CurrentIdentity returns the identity of the current process -func CurrentIdentity() Identity { - return Identity{UID: os.Getuid(), GID: os.Getegid()} -} diff --git a/user/idtools_unix.go b/user/idtools_unix.go index 1f11fe47..4e39d244 100644 --- a/user/idtools_unix.go +++ b/user/idtools_unix.go @@ -1,6 +1,6 @@ //go:build !windows -package idtools +package user import ( "fmt" @@ -8,11 +8,9 @@ import ( "path/filepath" "strconv" "syscall" - - "github.com/moby/sys/user" ) -func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { +func mkdirAs(path string, mode os.FileMode, uid, gid int, mkAll, onlyNew bool) error { path, err := filepath.Abs(path) if err != nil { return err @@ -23,21 +21,21 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting if !stat.IsDir() { return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} } - if !chownExisting { + if onlyNew { return nil } // short-circuit -- we were called with an existing directory and chown was requested - return setPermissions(path, mode, owner, stat) + return setPermissions(path, mode, uid, gid, stat) } // make an array containing the original path asked for, plus (for mkAll == true) // all path components leading up to the complete path that don't exist before we MkdirAll - // so that we can chown all of them properly at the end. If chownExisting is false, we won't + // so that we can chown all of them properly at the end. If onlyNew is true, we won't // chown the full directory path if it exists var paths []string if os.IsNotExist(err) { - paths = []string{path} + paths = append(paths, path) } if mkAll { @@ -49,7 +47,7 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting if dirPath == "/" { break } - if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) { + if _, err = os.Stat(dirPath); os.IsNotExist(err) { paths = append(paths, dirPath) } } @@ -62,39 +60,18 @@ func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting // even if it existed, we will chown the requested path + any subpaths that // didn't exist when we called MkdirAll for _, pathComponent := range paths { - if err = setPermissions(pathComponent, mode, owner, nil); err != nil { + if err = setPermissions(pathComponent, mode, uid, gid, nil); err != nil { return err } } return nil } -// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username -// -// Deprecated: use [user.LookupUser] instead -func LookupUser(name string) (user.User, error) { - return user.LookupUser(name) -} - -// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid -// -// Deprecated: use [user.LookupUid] instead -func LookupUID(uid int) (user.User, error) { - return user.LookupUid(uid) -} - -// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name, -// -// Deprecated: use [user.LookupGroup] instead -func LookupGroup(name string) (user.Group, error) { - return user.LookupGroup(name) -} - // setPermissions performs a chown/chmod only if the uid/gid don't match what's requested // Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the // dir is on an NFS share, so don't call chown unless we absolutely must. // Likewise for setting permissions. -func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error { +func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) error { if stat == nil { var err error stat, err = os.Stat(p) @@ -108,10 +85,10 @@ func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo } } ssi := stat.Sys().(*syscall.Stat_t) - if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) { + if ssi.Uid == uint32(uid) && ssi.Gid == uint32(gid) { return nil } - return os.Chown(p, owner.UID, owner.GID) + return os.Chown(p, uid, gid) } // LoadIdentityMapping takes a requested username and @@ -119,9 +96,9 @@ func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo // proper uid and gid remapping ranges for that user/group pair func LoadIdentityMapping(name string) (IdentityMapping, error) { // TODO: Consider adding support for calling out to "getent" - usr, err := user.LookupUser(name) + usr, err := LookupUser(name) if err != nil { - return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) + return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %w", name, err) } subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr) @@ -139,9 +116,9 @@ func LoadIdentityMapping(name string) (IdentityMapping, error) { }, nil } -func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) { +func lookupSubRangesFile(path string, usr User) ([]IDMap, error) { uidstr := strconv.Itoa(usr.Uid) - rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool { + rangeList, err := ParseSubIDFileFilter(path, func(sid SubID) bool { return sid.Name == usr.Name || sid.Name == uidstr }) if err != nil { @@ -153,14 +130,14 @@ func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) { idMap := []IDMap{} - containerID := 0 + var containerID int64 for _, idrange := range rangeList { idMap = append(idMap, IDMap{ - ContainerID: containerID, - HostID: int(idrange.SubID), - Size: int(idrange.Count), + ID: containerID, + ParentID: idrange.SubID, + Count: idrange.Count, }) - containerID = containerID + int(idrange.Count) + containerID = containerID + idrange.Count } return idMap, nil } diff --git a/user/idtools_unix_test.go b/user/idtools_unix_test.go index 381a1d7a..db7fc42e 100644 --- a/user/idtools_unix_test.go +++ b/user/idtools_unix_test.go @@ -1,17 +1,15 @@ //go:build !windows -package idtools +package user import ( + "errors" "fmt" "os" "path/filepath" "testing" "golang.org/x/sys/unix" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/skip" ) type node struct { @@ -20,12 +18,8 @@ type node struct { } func TestMkdirAllAndChown(t *testing.T) { - RequiresRoot(t) - dirName, err := os.MkdirTemp("", "mkdirall") - if err != nil { - t.Fatalf("Couldn't create temp dir: %v", err) - } - defer os.RemoveAll(dirName) + requiresRoot(t) + dirName := t.TempDir() testTree := map[string]node{ "usr": {0, 0}, @@ -40,7 +34,7 @@ func TestMkdirAllAndChown(t *testing.T) { } // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0o755, 99, 99); err != nil { t.Fatal(err) } testTree["usr/share"] = node{99, 99} @@ -48,12 +42,10 @@ func TestMkdirAllAndChown(t *testing.T) { if err != nil { t.Fatal(err) } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + compareTrees(t, testTree, verifyTree) // test 2-deep new directories--both should be owned by the uid/gid pair - if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0o755, 101, 101); err != nil { t.Fatal(err) } testTree["lib/some"] = node{101, 101} @@ -62,12 +54,10 @@ func TestMkdirAllAndChown(t *testing.T) { if err != nil { t.Fatal(err) } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + compareTrees(t, testTree, verifyTree) // test a directory that already exists; should be chowned, but nothing else - if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}); err != nil { + if err := MkdirAllAndChown(filepath.Join(dirName, "usr"), 0o755, 102, 102); err != nil { t.Fatal(err) } testTree["usr"] = node{102, 102} @@ -75,16 +65,12 @@ func TestMkdirAllAndChown(t *testing.T) { if err != nil { t.Fatal(err) } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + compareTrees(t, testTree, verifyTree) } func TestMkdirAllAndChownNew(t *testing.T) { - RequiresRoot(t) - dirName, err := os.MkdirTemp("", "mkdirnew") - assert.NilError(t, err) - defer os.RemoveAll(dirName) + requiresRoot(t) + dirName := t.TempDir() testTree := map[string]node{ "usr": {0, 0}, @@ -93,36 +79,45 @@ func TestMkdirAllAndChownNew(t *testing.T) { "lib/x86_64": {45, 45}, "lib/x86_64/share": {1, 1}, } - assert.NilError(t, buildTree(dirName, testTree)) + if err := buildTree(dirName, testTree); err != nil { + t.Fatal(err) + } // test adding a directory to a pre-existing dir; only the new dir is owned by the uid/gid - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr", "share"), 0o755, Identity{UID: 99, GID: 99}) - assert.NilError(t, err) + if err := MkdirAllAndChown(filepath.Join(dirName, "usr", "share"), 0o755, 99, 99, WithOnlyNew); err != nil { + t.Fatal(err) + } testTree["usr/share"] = node{99, 99} verifyTree, err := readTree(dirName, "") - assert.NilError(t, err) - assert.NilError(t, compareTrees(testTree, verifyTree)) + if err != nil { + t.Fatal(err) + } + compareTrees(t, testTree, verifyTree) // test 2-deep new directories--both should be owned by the uid/gid pair - err = MkdirAllAndChownNew(filepath.Join(dirName, "lib", "some", "other"), 0o755, Identity{UID: 101, GID: 101}) - assert.NilError(t, err) + if err = MkdirAllAndChown(filepath.Join(dirName, "lib", "some", "other"), 0o755, 101, 101, WithOnlyNew); err != nil { + t.Fatal(err) + } testTree["lib/some"] = node{101, 101} testTree["lib/some/other"] = node{101, 101} - verifyTree, err = readTree(dirName, "") - assert.NilError(t, err) - assert.NilError(t, compareTrees(testTree, verifyTree)) + if verifyTree, err = readTree(dirName, ""); err != nil { + t.Fatal(err) + } + compareTrees(t, testTree, verifyTree) // test a directory that already exists; should NOT be chowned - err = MkdirAllAndChownNew(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 102, GID: 102}) - assert.NilError(t, err) - verifyTree, err = readTree(dirName, "") - assert.NilError(t, err) - assert.NilError(t, compareTrees(testTree, verifyTree)) + if err = MkdirAllAndChown(filepath.Join(dirName, "usr"), 0o755, 102, 102, WithOnlyNew); err != nil { + t.Fatal(err) + } + if verifyTree, err = readTree(dirName, ""); err != nil { + t.Fatal(err) + } + compareTrees(t, testTree, verifyTree) } func TestMkdirAllAndChownNewRelative(t *testing.T) { - RequiresRoot(t) + requiresRoot(t) tests := []struct { in string @@ -181,18 +176,26 @@ func TestMkdirAllAndChownNewRelative(t *testing.T) { t.Run(tc.in, func(t *testing.T) { for _, p := range tc.out { _, err := os.Stat(p) - assert.ErrorIs(t, err, os.ErrNotExist) + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected file not exists for %v, got %v", p, err) + } } - err := MkdirAllAndChownNew(tc.in, 0o755, Identity{UID: expectedUIDGID, GID: expectedUIDGID}) - assert.Check(t, err) + if err := MkdirAllAndChown(tc.in, 0o755, expectedUIDGID, expectedUIDGID, WithOnlyNew); err != nil { + t.Fatal(err) + } for _, p := range tc.out { s := &unix.Stat_t{} - err = unix.Stat(p, s) - if assert.Check(t, err) { - assert.Check(t, is.Equal(uint64(s.Uid), uint64(expectedUIDGID))) - assert.Check(t, is.Equal(uint64(s.Gid), uint64(expectedUIDGID))) + if err := unix.Stat(p, s); err != nil { + t.Errorf("stat %v: %v", p, err) + continue + } + if s.Uid != expectedUIDGID { + t.Errorf("expected UID: %d, got: %d", expectedUIDGID, s.Uid) + } + if s.Gid != expectedUIDGID { + t.Errorf("expected GID: %d, got: %d", expectedUIDGID, s.Gid) } } }) @@ -204,21 +207,22 @@ func TestMkdirAllAndChownNewRelative(t *testing.T) { func setWorkingDirectory(t *testing.T, dir string) { t.Helper() cwd, err := os.Getwd() - assert.NilError(t, err) + if err != nil { + t.Fatal(err) + } t.Cleanup(func() { - assert.NilError(t, os.Chdir(cwd)) + if err := os.Chdir(cwd); err != nil { + t.Error(err) + } }) - err = os.Chdir(dir) - assert.NilError(t, err) + if err = os.Chdir(dir); err != nil { + t.Fatal(err) + } } func TestMkdirAndChown(t *testing.T) { - RequiresRoot(t) - dirName, err := os.MkdirTemp("", "mkdir") - if err != nil { - t.Fatalf("Couldn't create temp dir: %v", err) - } - defer os.RemoveAll(dirName) + requiresRoot(t) + dirName := t.TempDir() testTree := map[string]node{ "usr": {0, 0}, @@ -228,7 +232,7 @@ func TestMkdirAndChown(t *testing.T) { } // test a directory that already exists; should just chown to the requested uid/gid - if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0o755, Identity{UID: 99, GID: 99}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr"), 0o755, 99, 99); err != nil { t.Fatal(err) } testTree["usr"] = node{99, 99} @@ -236,17 +240,15 @@ func TestMkdirAndChown(t *testing.T) { if err != nil { t.Fatal(err) } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + compareTrees(t, testTree, verifyTree) // create a subdir under a dir which doesn't exist--should fail - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0o755, Identity{UID: 102, GID: 102}); err == nil { - t.Fatalf("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin", "subdir"), 0o755, 102, 102); err == nil { + t.Fatal("Trying to create a directory with Mkdir where the parent doesn't exist should have failed") } // create a subdir under an existing dir; should only change the ownership of the new subdir - if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0o755, Identity{UID: 102, GID: 102}); err != nil { + if err := MkdirAndChown(filepath.Join(dirName, "usr", "bin"), 0o755, 102, 102); err != nil { t.Fatal(err) } testTree["usr/bin"] = node{102, 102} @@ -254,19 +256,17 @@ func TestMkdirAndChown(t *testing.T) { if err != nil { t.Fatal(err) } - if err := compareTrees(testTree, verifyTree); err != nil { - t.Fatal(err) - } + compareTrees(t, testTree, verifyTree) } func buildTree(base string, tree map[string]node) error { for path, node := range tree { fullPath := filepath.Join(base, path) if err := os.MkdirAll(fullPath, 0o755); err != nil { - return fmt.Errorf("couldn't create path: %s; error: %v", fullPath, err) + return err } if err := os.Chown(fullPath, node.uid, node.gid); err != nil { - return fmt.Errorf("couldn't chown path: %s; error: %v", fullPath, err) + return err } } return nil @@ -277,13 +277,13 @@ func readTree(base, root string) (map[string]node, error) { dirInfos, err := os.ReadDir(base) if err != nil { - return nil, fmt.Errorf("couldn't read directory entries for %q: %v", base, err) + return nil, err } for _, info := range dirInfos { s := &unix.Stat_t{} if err := unix.Stat(filepath.Join(base, info.Name()), s); err != nil { - return nil, fmt.Errorf("can't stat file %q: %v", filepath.Join(base, info.Name()), err) + return nil, fmt.Errorf("can't stat file %q: %w", filepath.Join(base, info.Name()), err) } tree[filepath.Join(root, info.Name())] = node{int(s.Uid), int(s.Gid)} if info.IsDir() { @@ -300,84 +300,99 @@ func readTree(base, root string) (map[string]node, error) { return tree, nil } -func compareTrees(left, right map[string]node) error { +func compareTrees(t testing.TB, left, right map[string]node) { + t.Helper() if len(left) != len(right) { - return fmt.Errorf("trees aren't the same size") + t.Fatal("trees aren't the same size") } for path, nodeLeft := range left { if nodeRight, ok := right[path]; ok { if nodeRight.uid != nodeLeft.uid || nodeRight.gid != nodeLeft.gid { // mismatch - return fmt.Errorf("mismatched ownership for %q: expected: %d:%d, got: %d:%d", path, + t.Fatalf("mismatched ownership for %q: expected: %d:%d, got: %d:%d", path, nodeLeft.uid, nodeLeft.gid, nodeRight.uid, nodeRight.gid) } continue } - return fmt.Errorf("right tree didn't contain path %q", path) + t.Fatalf("right tree didn't contain path %q", path) } - return nil } func TestGetRootUIDGID(t *testing.T) { uidMap := []IDMap{ { - ContainerID: 0, - HostID: os.Getuid(), - Size: 1, + ID: 0, + ParentID: int64(os.Getuid()), + Count: 1, }, } gidMap := []IDMap{ { - ContainerID: 0, - HostID: os.Getgid(), - Size: 1, + ID: 0, + ParentID: int64(os.Getgid()), + Count: 1, }, } - uid, gid, err := GetRootUIDGID(uidMap, gidMap) - assert.Check(t, err) - assert.Check(t, is.Equal(os.Geteuid(), uid)) - assert.Check(t, is.Equal(os.Getegid(), gid)) + uid, gid, err := getRootUIDGID(uidMap, gidMap) + if err != nil { + t.Fatal(err) + } + if uid != os.Getuid() { + t.Fatalf("expected %d, got %d", os.Getuid(), uid) + } + if gid != os.Getgid() { + t.Fatalf("expected %d, got %d", os.Getgid(), gid) + } uidMapError := []IDMap{ { - ContainerID: 1, - HostID: os.Getuid(), - Size: 1, + ID: 1, + ParentID: int64(os.Getuid()), + Count: 1, }, } - _, _, err = GetRootUIDGID(uidMapError, gidMap) - assert.Check(t, is.Error(err, "Container ID 0 cannot be mapped to a host ID")) + _, _, err = getRootUIDGID(uidMapError, gidMap) + if expected := "container ID 0 cannot be mapped to a host ID"; err.Error() != expected { + t.Fatalf("expected error: %v, got: %v", expected, err) + } } func TestToContainer(t *testing.T) { uidMap := []IDMap{ { - ContainerID: 2, - HostID: 2, - Size: 1, + ID: 2, + ParentID: 2, + Count: 1, }, } containerID, err := toContainer(2, uidMap) - assert.Check(t, err) - assert.Check(t, is.Equal(uidMap[0].ContainerID, containerID)) + if err != nil { + t.Fatal(err) + } + if uidMap[0].ID != int64(containerID) { + t.Fatalf("expected %d, got %d", uidMap[0].ID, containerID) + } } // TestMkdirIsNotDir checks that mkdirAs() function (used by MkdirAll...) // returns a correct error in case a directory which it is about to create // already exists but is a file (rather than a directory). func TestMkdirIsNotDir(t *testing.T) { - file, err := os.CreateTemp("", t.Name()) + file, err := os.CreateTemp(t.TempDir(), t.Name()) if err != nil { t.Fatalf("Couldn't create temp dir: %v", err) } - defer os.Remove(file.Name()) - err = mkdirAs(file.Name(), 0o755, Identity{UID: 0, GID: 0}, false, false) - assert.Check(t, is.Error(err, "mkdir "+file.Name()+": not a directory")) + err = mkdirAs(file.Name(), 0o755, 0, 0, false, false) + if expected := "mkdir " + file.Name() + ": not a directory"; err.Error() != expected { + t.Fatalf("expected error: %v, got: %v", expected, err) + } } -func RequiresRoot(t *testing.T) { - skip.If(t, os.Getuid() != 0, "skipping test that requires root") +func requiresRoot(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root") + } } diff --git a/user/idtools_windows.go b/user/idtools_windows.go index 43702f7f..9de730ca 100644 --- a/user/idtools_windows.go +++ b/user/idtools_windows.go @@ -1,26 +1,13 @@ -package idtools +package user import ( "os" ) -const ( - // Deprecated: copy value locally - SeTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" -) - -const ( - // Deprecated: copy value locally - ContainerAdministratorSidString = "S-1-5-93-2-1" - - // Deprecated: copy value locally - ContainerUserSidString = "S-1-5-93-2-2" -) - // This is currently a wrapper around [os.MkdirAll] since currently // permissions aren't set through this path, the identity isn't utilized. // Ownership is handled elsewhere, but in the future could be support here // too. -func mkdirAs(path string, _ os.FileMode, _ Identity, _, _ bool) error { +func mkdirAs(path string, _ os.FileMode, _, _ int, _, _ bool) error { return os.MkdirAll(path, 0) }