Преглед изворни кода

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 <github@gone.nl>
Sebastiaan van Stijn пре 1 година
родитељ
комит
df3a321164

+ 3 - 3
builder/dockerfile/internals_linux.go

@@ -8,7 +8,7 @@ import (
 
 
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/docker/docker/pkg/idtools"
 	"github.com/moby/sys/symlink"
 	"github.com/moby/sys/symlink"
-	lcUser "github.com/opencontainers/runc/libcontainer/user"
+	"github.com/moby/sys/user"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
 )
 )
 
 
@@ -57,7 +57,7 @@ func lookupUser(userStr, filepath string) (int, error) {
 	if err == nil {
 	if err == nil {
 		return uid, nil
 		return uid, nil
 	}
 	}
-	users, err := lcUser.ParsePasswdFileFilter(filepath, func(u lcUser.User) bool {
+	users, err := user.ParsePasswdFileFilter(filepath, func(u user.User) bool {
 		return u.Name == userStr
 		return u.Name == userStr
 	})
 	})
 	if err != nil {
 	if err != nil {
@@ -76,7 +76,7 @@ func lookupGroup(groupStr, filepath string) (int, error) {
 	if err == nil {
 	if err == nil {
 		return gid, nil
 		return gid, nil
 	}
 	}
-	groups, err := lcUser.ParseGroupFileFilter(filepath, func(g lcUser.Group) bool {
+	groups, err := user.ParseGroupFileFilter(filepath, func(g user.Group) bool {
 		return g.Name == groupStr
 		return g.Name == groupStr
 	})
 	})
 	if err != nil {
 	if err != nil {

+ 1 - 1
daemon/oci_linux.go

@@ -27,8 +27,8 @@ import (
 	volumemounts "github.com/docker/docker/volume/mounts"
 	volumemounts "github.com/docker/docker/volume/mounts"
 	"github.com/moby/sys/mount"
 	"github.com/moby/sys/mount"
 	"github.com/moby/sys/mountinfo"
 	"github.com/moby/sys/mountinfo"
+	"github.com/moby/sys/user"
 	"github.com/opencontainers/runc/libcontainer/cgroups"
 	"github.com/opencontainers/runc/libcontainer/cgroups"
-	"github.com/opencontainers/runc/libcontainer/user"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
 	specs "github.com/opencontainers/runtime-spec/specs-go"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
 	"golang.org/x/sys/unix"
 	"golang.org/x/sys/unix"

+ 1 - 1
pkg/idtools/idtools_unix.go

@@ -12,7 +12,7 @@ import (
 	"strconv"
 	"strconv"
 	"syscall"
 	"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 {
 func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error {

+ 1 - 0
vendor.mod

@@ -73,6 +73,7 @@ require (
 	github.com/moby/sys/sequential v0.5.0
 	github.com/moby/sys/sequential v0.5.0
 	github.com/moby/sys/signal v0.7.0
 	github.com/moby/sys/signal v0.7.0
 	github.com/moby/sys/symlink v0.2.0
 	github.com/moby/sys/symlink v0.2.0
+	github.com/moby/sys/user v0.1.0
 	github.com/moby/term v0.5.0
 	github.com/moby/term v0.5.0
 	github.com/morikuni/aec v1.0.0
 	github.com/morikuni/aec v1.0.0
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/go-digest v1.0.0

+ 2 - 0
vendor.sum

@@ -936,6 +936,8 @@ github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI
 github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
 github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
 github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
 github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
 github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
 github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
+github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
+github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
 github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
 github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
 github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=

+ 202 - 0
vendor/github.com/moby/sys/user/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 157 - 0
vendor/github.com/moby/sys/user/lookup_unix.go

@@ -0,0 +1,157 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package user
+
+import (
+	"io"
+	"os"
+	"strconv"
+
+	"golang.org/x/sys/unix"
+)
+
+// Unix-specific path to the passwd and group formatted files.
+const (
+	unixPasswdPath = "/etc/passwd"
+	unixGroupPath  = "/etc/group"
+)
+
+// LookupUser looks up a user by their username in /etc/passwd. If the user
+// cannot be found (or there is no /etc/passwd file on the filesystem), then
+// LookupUser returns an error.
+func LookupUser(username string) (User, error) {
+	return lookupUserFunc(func(u User) bool {
+		return u.Name == username
+	})
+}
+
+// LookupUid looks up a user by their user id in /etc/passwd. If the user cannot
+// be found (or there is no /etc/passwd file on the filesystem), then LookupId
+// returns an error.
+func LookupUid(uid int) (User, error) {
+	return lookupUserFunc(func(u User) bool {
+		return u.Uid == uid
+	})
+}
+
+func lookupUserFunc(filter func(u User) bool) (User, error) {
+	// Get operating system-specific passwd reader-closer.
+	passwd, err := GetPasswd()
+	if err != nil {
+		return User{}, err
+	}
+	defer passwd.Close()
+
+	// Get the users.
+	users, err := ParsePasswdFilter(passwd, filter)
+	if err != nil {
+		return User{}, err
+	}
+
+	// No user entries found.
+	if len(users) == 0 {
+		return User{}, ErrNoPasswdEntries
+	}
+
+	// Assume the first entry is the "correct" one.
+	return users[0], nil
+}
+
+// LookupGroup looks up a group by its name in /etc/group. If the group cannot
+// be found (or there is no /etc/group file on the filesystem), then LookupGroup
+// returns an error.
+func LookupGroup(groupname string) (Group, error) {
+	return lookupGroupFunc(func(g Group) bool {
+		return g.Name == groupname
+	})
+}
+
+// LookupGid looks up a group by its group id in /etc/group. If the group cannot
+// be found (or there is no /etc/group file on the filesystem), then LookupGid
+// returns an error.
+func LookupGid(gid int) (Group, error) {
+	return lookupGroupFunc(func(g Group) bool {
+		return g.Gid == gid
+	})
+}
+
+func lookupGroupFunc(filter func(g Group) bool) (Group, error) {
+	// Get operating system-specific group reader-closer.
+	group, err := GetGroup()
+	if err != nil {
+		return Group{}, err
+	}
+	defer group.Close()
+
+	// Get the users.
+	groups, err := ParseGroupFilter(group, filter)
+	if err != nil {
+		return Group{}, err
+	}
+
+	// No user entries found.
+	if len(groups) == 0 {
+		return Group{}, ErrNoGroupEntries
+	}
+
+	// Assume the first entry is the "correct" one.
+	return groups[0], nil
+}
+
+func GetPasswdPath() (string, error) {
+	return unixPasswdPath, nil
+}
+
+func GetPasswd() (io.ReadCloser, error) {
+	return os.Open(unixPasswdPath)
+}
+
+func GetGroupPath() (string, error) {
+	return unixGroupPath, nil
+}
+
+func GetGroup() (io.ReadCloser, error) {
+	return os.Open(unixGroupPath)
+}
+
+// CurrentUser looks up the current user by their user id in /etc/passwd. If the
+// user cannot be found (or there is no /etc/passwd file on the filesystem),
+// then CurrentUser returns an error.
+func CurrentUser() (User, error) {
+	return LookupUid(unix.Getuid())
+}
+
+// CurrentGroup looks up the current user's group by their primary group id's
+// entry in /etc/passwd. If the group cannot be found (or there is no
+// /etc/group file on the filesystem), then CurrentGroup returns an error.
+func CurrentGroup() (Group, error) {
+	return LookupGid(unix.Getgid())
+}
+
+func currentUserSubIDs(fileName string) ([]SubID, error) {
+	u, err := CurrentUser()
+	if err != nil {
+		return nil, err
+	}
+	filter := func(entry SubID) bool {
+		return entry.Name == u.Name || entry.Name == strconv.Itoa(u.Uid)
+	}
+	return ParseSubIDFileFilter(fileName, filter)
+}
+
+func CurrentUserSubUIDs() ([]SubID, error) {
+	return currentUserSubIDs("/etc/subuid")
+}
+
+func CurrentUserSubGIDs() ([]SubID, error) {
+	return currentUserSubIDs("/etc/subgid")
+}
+
+func CurrentProcessUIDMap() ([]IDMap, error) {
+	return ParseIDMapFile("/proc/self/uid_map")
+}
+
+func CurrentProcessGIDMap() ([]IDMap, error) {
+	return ParseIDMapFile("/proc/self/gid_map")
+}

+ 605 - 0
vendor/github.com/moby/sys/user/user.go

@@ -0,0 +1,605 @@
+package user
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+)
+
+const (
+	minID = 0
+	maxID = 1<<31 - 1 // for 32-bit systems compatibility
+)
+
+var (
+	// ErrNoPasswdEntries is returned if no matching entries were found in /etc/group.
+	ErrNoPasswdEntries = errors.New("no matching entries in passwd file")
+	// ErrNoGroupEntries is returned if no matching entries were found in /etc/passwd.
+	ErrNoGroupEntries = errors.New("no matching entries in group file")
+	// ErrRange is returned if a UID or GID is outside of the valid range.
+	ErrRange = fmt.Errorf("uids and gids must be in range %d-%d", minID, maxID)
+)
+
+type User struct {
+	Name  string
+	Pass  string
+	Uid   int
+	Gid   int
+	Gecos string
+	Home  string
+	Shell string
+}
+
+type Group struct {
+	Name string
+	Pass string
+	Gid  int
+	List []string
+}
+
+// SubID represents an entry in /etc/sub{u,g}id
+type SubID struct {
+	Name  string
+	SubID int64
+	Count int64
+}
+
+// IDMap represents an entry in /proc/PID/{u,g}id_map
+type IDMap struct {
+	ID       int64
+	ParentID int64
+	Count    int64
+}
+
+func parseLine(line []byte, v ...interface{}) {
+	parseParts(bytes.Split(line, []byte(":")), v...)
+}
+
+func parseParts(parts [][]byte, v ...interface{}) {
+	if len(parts) == 0 {
+		return
+	}
+
+	for i, p := range parts {
+		// Ignore cases where we don't have enough fields to populate the arguments.
+		// Some configuration files like to misbehave.
+		if len(v) <= i {
+			break
+		}
+
+		// Use the type of the argument to figure out how to parse it, scanf() style.
+		// This is legit.
+		switch e := v[i].(type) {
+		case *string:
+			*e = string(p)
+		case *int:
+			// "numbers", with conversion errors ignored because of some misbehaving configuration files.
+			*e, _ = strconv.Atoi(string(p))
+		case *int64:
+			*e, _ = strconv.ParseInt(string(p), 10, 64)
+		case *[]string:
+			// Comma-separated lists.
+			if len(p) != 0 {
+				*e = strings.Split(string(p), ",")
+			} else {
+				*e = []string{}
+			}
+		default:
+			// Someone goof'd when writing code using this function. Scream so they can hear us.
+			panic(fmt.Sprintf("parseLine only accepts {*string, *int, *int64, *[]string} as arguments! %#v is not a pointer!", e))
+		}
+	}
+}
+
+func ParsePasswdFile(path string) ([]User, error) {
+	passwd, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer passwd.Close()
+	return ParsePasswd(passwd)
+}
+
+func ParsePasswd(passwd io.Reader) ([]User, error) {
+	return ParsePasswdFilter(passwd, nil)
+}
+
+func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) {
+	passwd, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer passwd.Close()
+	return ParsePasswdFilter(passwd, filter)
+}
+
+func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) {
+	if r == nil {
+		return nil, errors.New("nil source for passwd-formatted data")
+	}
+
+	var (
+		s   = bufio.NewScanner(r)
+		out = []User{}
+	)
+
+	for s.Scan() {
+		line := bytes.TrimSpace(s.Bytes())
+		if len(line) == 0 {
+			continue
+		}
+
+		// see: man 5 passwd
+		//  name:password:UID:GID:GECOS:directory:shell
+		// Name:Pass:Uid:Gid:Gecos:Home:Shell
+		//  root:x:0:0:root:/root:/bin/bash
+		//  adm:x:3:4:adm:/var/adm:/bin/false
+		p := User{}
+		parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return out, nil
+}
+
+func ParseGroupFile(path string) ([]Group, error) {
+	group, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+
+	defer group.Close()
+	return ParseGroup(group)
+}
+
+func ParseGroup(group io.Reader) ([]Group, error) {
+	return ParseGroupFilter(group, nil)
+}
+
+func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) {
+	group, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer group.Close()
+	return ParseGroupFilter(group, filter)
+}
+
+func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) {
+	if r == nil {
+		return nil, errors.New("nil source for group-formatted data")
+	}
+	rd := bufio.NewReader(r)
+	out := []Group{}
+
+	// Read the file line-by-line.
+	for {
+		var (
+			isPrefix  bool
+			wholeLine []byte
+			err       error
+		)
+
+		// Read the next line. We do so in chunks (as much as reader's
+		// buffer is able to keep), check if we read enough columns
+		// already on each step and store final result in wholeLine.
+		for {
+			var line []byte
+			line, isPrefix, err = rd.ReadLine()
+
+			if err != nil {
+				// We should return no error if EOF is reached
+				// without a match.
+				if err == io.EOF {
+					err = nil
+				}
+				return out, err
+			}
+
+			// Simple common case: line is short enough to fit in a
+			// single reader's buffer.
+			if !isPrefix && len(wholeLine) == 0 {
+				wholeLine = line
+				break
+			}
+
+			wholeLine = append(wholeLine, line...)
+
+			// Check if we read the whole line already.
+			if !isPrefix {
+				break
+			}
+		}
+
+		// There's no spec for /etc/passwd or /etc/group, but we try to follow
+		// the same rules as the glibc parser, which allows comments and blank
+		// space at the beginning of a line.
+		wholeLine = bytes.TrimSpace(wholeLine)
+		if len(wholeLine) == 0 || wholeLine[0] == '#' {
+			continue
+		}
+
+		// see: man 5 group
+		//  group_name:password:GID:user_list
+		// Name:Pass:Gid:List
+		//  root:x:0:root
+		//  adm:x:4:root,adm,daemon
+		p := Group{}
+		parseLine(wholeLine, &p.Name, &p.Pass, &p.Gid, &p.List)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+}
+
+type ExecUser struct {
+	Uid   int
+	Gid   int
+	Sgids []int
+	Home  string
+}
+
+// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the
+// given file paths and uses that data as the arguments to GetExecUser. If the
+// files cannot be opened for any reason, the error is ignored and a nil
+// io.Reader is passed instead.
+func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) {
+	var passwd, group io.Reader
+
+	if passwdFile, err := os.Open(passwdPath); err == nil {
+		passwd = passwdFile
+		defer passwdFile.Close()
+	}
+
+	if groupFile, err := os.Open(groupPath); err == nil {
+		group = groupFile
+		defer groupFile.Close()
+	}
+
+	return GetExecUser(userSpec, defaults, passwd, group)
+}
+
+// GetExecUser parses a user specification string (using the passwd and group
+// readers as sources for /etc/passwd and /etc/group data, respectively). In
+// the case of blank fields or missing data from the sources, the values in
+// defaults is used.
+//
+// GetExecUser will return an error if a user or group literal could not be
+// found in any entry in passwd and group respectively.
+//
+// Examples of valid user specifications are:
+//   - ""
+//   - "user"
+//   - "uid"
+//   - "user:group"
+//   - "uid:gid
+//   - "user:gid"
+//   - "uid:group"
+//
+// It should be noted that if you specify a numeric user or group id, they will
+// not be evaluated as usernames (only the metadata will be filled). So attempting
+// to parse a user with user.Name = "1337" will produce the user with a UID of
+// 1337.
+func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) {
+	if defaults == nil {
+		defaults = new(ExecUser)
+	}
+
+	// Copy over defaults.
+	user := &ExecUser{
+		Uid:   defaults.Uid,
+		Gid:   defaults.Gid,
+		Sgids: defaults.Sgids,
+		Home:  defaults.Home,
+	}
+
+	// Sgids slice *cannot* be nil.
+	if user.Sgids == nil {
+		user.Sgids = []int{}
+	}
+
+	// Allow for userArg to have either "user" syntax, or optionally "user:group" syntax
+	var userArg, groupArg string
+	parseLine([]byte(userSpec), &userArg, &groupArg)
+
+	// Convert userArg and groupArg to be numeric, so we don't have to execute
+	// Atoi *twice* for each iteration over lines.
+	uidArg, uidErr := strconv.Atoi(userArg)
+	gidArg, gidErr := strconv.Atoi(groupArg)
+
+	// Find the matching user.
+	users, err := ParsePasswdFilter(passwd, func(u User) bool {
+		if userArg == "" {
+			// Default to current state of the user.
+			return u.Uid == user.Uid
+		}
+
+		if uidErr == nil {
+			// If the userArg is numeric, always treat it as a UID.
+			return uidArg == u.Uid
+		}
+
+		return u.Name == userArg
+	})
+
+	// If we can't find the user, we have to bail.
+	if err != nil && passwd != nil {
+		if userArg == "" {
+			userArg = strconv.Itoa(user.Uid)
+		}
+		return nil, fmt.Errorf("unable to find user %s: %w", userArg, err)
+	}
+
+	var matchedUserName string
+	if len(users) > 0 {
+		// First match wins, even if there's more than one matching entry.
+		matchedUserName = users[0].Name
+		user.Uid = users[0].Uid
+		user.Gid = users[0].Gid
+		user.Home = users[0].Home
+	} else if userArg != "" {
+		// If we can't find a user with the given username, the only other valid
+		// option is if it's a numeric username with no associated entry in passwd.
+
+		if uidErr != nil {
+			// Not numeric.
+			return nil, fmt.Errorf("unable to find user %s: %w", userArg, ErrNoPasswdEntries)
+		}
+		user.Uid = uidArg
+
+		// Must be inside valid uid range.
+		if user.Uid < minID || user.Uid > maxID {
+			return nil, ErrRange
+		}
+
+		// Okay, so it's numeric. We can just roll with this.
+	}
+
+	// On to the groups. If we matched a username, we need to do this because of
+	// the supplementary group IDs.
+	if groupArg != "" || matchedUserName != "" {
+		groups, err := ParseGroupFilter(group, func(g Group) bool {
+			// If the group argument isn't explicit, we'll just search for it.
+			if groupArg == "" {
+				// Check if user is a member of this group.
+				for _, u := range g.List {
+					if u == matchedUserName {
+						return true
+					}
+				}
+				return false
+			}
+
+			if gidErr == nil {
+				// If the groupArg is numeric, always treat it as a GID.
+				return gidArg == g.Gid
+			}
+
+			return g.Name == groupArg
+		})
+		if err != nil && group != nil {
+			return nil, fmt.Errorf("unable to find groups for spec %v: %w", matchedUserName, err)
+		}
+
+		// Only start modifying user.Gid if it is in explicit form.
+		if groupArg != "" {
+			if len(groups) > 0 {
+				// First match wins, even if there's more than one matching entry.
+				user.Gid = groups[0].Gid
+			} else {
+				// If we can't find a group with the given name, the only other valid
+				// option is if it's a numeric group name with no associated entry in group.
+
+				if gidErr != nil {
+					// Not numeric.
+					return nil, fmt.Errorf("unable to find group %s: %w", groupArg, ErrNoGroupEntries)
+				}
+				user.Gid = gidArg
+
+				// Must be inside valid gid range.
+				if user.Gid < minID || user.Gid > maxID {
+					return nil, ErrRange
+				}
+
+				// Okay, so it's numeric. We can just roll with this.
+			}
+		} else if len(groups) > 0 {
+			// Supplementary group ids only make sense if in the implicit form.
+			user.Sgids = make([]int, len(groups))
+			for i, group := range groups {
+				user.Sgids[i] = group.Gid
+			}
+		}
+	}
+
+	return user, nil
+}
+
+// GetAdditionalGroups looks up a list of groups by name or group id
+// against the given /etc/group formatted data. If a group name cannot
+// be found, an error will be returned. If a group id cannot be found,
+// or the given group data is nil, the id will be returned as-is
+// provided it is in the legal range.
+func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) {
+	groups := []Group{}
+	if group != nil {
+		var err error
+		groups, err = ParseGroupFilter(group, func(g Group) bool {
+			for _, ag := range additionalGroups {
+				if g.Name == ag || strconv.Itoa(g.Gid) == ag {
+					return true
+				}
+			}
+			return false
+		})
+		if err != nil {
+			return nil, fmt.Errorf("Unable to find additional groups %v: %w", additionalGroups, err)
+		}
+	}
+
+	gidMap := make(map[int]struct{})
+	for _, ag := range additionalGroups {
+		var found bool
+		for _, g := range groups {
+			// if we found a matched group either by name or gid, take the
+			// first matched as correct
+			if g.Name == ag || strconv.Itoa(g.Gid) == ag {
+				if _, ok := gidMap[g.Gid]; !ok {
+					gidMap[g.Gid] = struct{}{}
+					found = true
+					break
+				}
+			}
+		}
+		// we asked for a group but didn't find it. let's check to see
+		// if we wanted a numeric group
+		if !found {
+			gid, err := strconv.ParseInt(ag, 10, 64)
+			if err != nil {
+				// Not a numeric ID either.
+				return nil, fmt.Errorf("Unable to find group %s: %w", ag, ErrNoGroupEntries)
+			}
+			// Ensure gid is inside gid range.
+			if gid < minID || gid > maxID {
+				return nil, ErrRange
+			}
+			gidMap[int(gid)] = struct{}{}
+		}
+	}
+	gids := []int{}
+	for gid := range gidMap {
+		gids = append(gids, gid)
+	}
+	return gids, nil
+}
+
+// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups
+// that opens the groupPath given and gives it as an argument to
+// GetAdditionalGroups.
+func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) {
+	var group io.Reader
+
+	if groupFile, err := os.Open(groupPath); err == nil {
+		group = groupFile
+		defer groupFile.Close()
+	}
+	return GetAdditionalGroups(additionalGroups, group)
+}
+
+func ParseSubIDFile(path string) ([]SubID, error) {
+	subid, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer subid.Close()
+	return ParseSubID(subid)
+}
+
+func ParseSubID(subid io.Reader) ([]SubID, error) {
+	return ParseSubIDFilter(subid, nil)
+}
+
+func ParseSubIDFileFilter(path string, filter func(SubID) bool) ([]SubID, error) {
+	subid, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer subid.Close()
+	return ParseSubIDFilter(subid, filter)
+}
+
+func ParseSubIDFilter(r io.Reader, filter func(SubID) bool) ([]SubID, error) {
+	if r == nil {
+		return nil, errors.New("nil source for subid-formatted data")
+	}
+
+	var (
+		s   = bufio.NewScanner(r)
+		out = []SubID{}
+	)
+
+	for s.Scan() {
+		line := bytes.TrimSpace(s.Bytes())
+		if len(line) == 0 {
+			continue
+		}
+
+		// see: man 5 subuid
+		p := SubID{}
+		parseLine(line, &p.Name, &p.SubID, &p.Count)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return out, nil
+}
+
+func ParseIDMapFile(path string) ([]IDMap, error) {
+	r, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+	return ParseIDMap(r)
+}
+
+func ParseIDMap(r io.Reader) ([]IDMap, error) {
+	return ParseIDMapFilter(r, nil)
+}
+
+func ParseIDMapFileFilter(path string, filter func(IDMap) bool) ([]IDMap, error) {
+	r, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+	return ParseIDMapFilter(r, filter)
+}
+
+func ParseIDMapFilter(r io.Reader, filter func(IDMap) bool) ([]IDMap, error) {
+	if r == nil {
+		return nil, errors.New("nil source for idmap-formatted data")
+	}
+
+	var (
+		s   = bufio.NewScanner(r)
+		out = []IDMap{}
+	)
+
+	for s.Scan() {
+		line := bytes.TrimSpace(s.Bytes())
+		if len(line) == 0 {
+			continue
+		}
+
+		// see: man 7 user_namespaces
+		p := IDMap{}
+		parseParts(bytes.Fields(line), &p.ID, &p.ParentID, &p.Count)
+
+		if filter == nil || filter(p) {
+			out = append(out, p)
+		}
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	return out, nil
+}

+ 43 - 0
vendor/github.com/moby/sys/user/user_fuzzer.go

@@ -0,0 +1,43 @@
+//go:build gofuzz
+// +build gofuzz
+
+package user
+
+import (
+	"io"
+	"strings"
+)
+
+func IsDivisbleBy(n int, divisibleby int) bool {
+	return (n % divisibleby) == 0
+}
+
+func FuzzUser(data []byte) int {
+	if len(data) == 0 {
+		return -1
+	}
+	if !IsDivisbleBy(len(data), 5) {
+		return -1
+	}
+
+	var divided [][]byte
+
+	chunkSize := len(data) / 5
+
+	for i := 0; i < len(data); i += chunkSize {
+		end := i + chunkSize
+
+		divided = append(divided, data[i:end])
+	}
+
+	_, _ = ParsePasswdFilter(strings.NewReader(string(divided[0])), nil)
+
+	var passwd, group io.Reader
+
+	group = strings.NewReader(string(divided[1]))
+	_, _ = GetAdditionalGroups([]string{string(divided[2])}, group)
+
+	passwd = strings.NewReader(string(divided[3]))
+	_, _ = GetExecUser(string(divided[4]), nil, passwd, group)
+	return 1
+}

+ 3 - 0
vendor/modules.txt

@@ -916,6 +916,9 @@ github.com/moby/sys/signal
 # github.com/moby/sys/symlink v0.2.0
 # github.com/moby/sys/symlink v0.2.0
 ## explicit; go 1.16
 ## explicit; go 1.16
 github.com/moby/sys/symlink
 github.com/moby/sys/symlink
+# github.com/moby/sys/user v0.1.0
+## explicit; go 1.17
+github.com/moby/sys/user
 # github.com/moby/term v0.5.0
 # github.com/moby/term v0.5.0
 ## explicit; go 1.18
 ## explicit; go 1.18
 github.com/moby/term
 github.com/moby/term