2018-02-05 16:05:59 -05:00
|
|
|
package idtools // import "github.com/docker/docker/pkg/idtools"
|
2015-10-08 11:46:10 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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 (
|
2018-05-19 07:38:54 -04:00
|
|
|
subuidFileName = "/etc/subuid"
|
|
|
|
subgidFileName = "/etc/subgid"
|
2015-10-08 11:46:10 -04:00
|
|
|
)
|
|
|
|
|
2017-05-19 18:06:46 -04:00
|
|
|
// MkdirAllAndChown creates a directory (include any along the path) and then modifies
|
|
|
|
// ownership to the requested uid/gid. If the directory already exists, this
|
2020-10-06 15:30:07 -04:00
|
|
|
// function will still change ownership and permissions.
|
2017-11-16 01:20:33 -05:00
|
|
|
func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error {
|
|
|
|
return mkdirAs(path, mode, owner, true, true)
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
|
2020-10-06 15:30:07 -04:00
|
|
|
// If the directory already exists, this function still changes ownership and permissions.
|
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 <kolyshkin@gmail.com>
2017-09-25 15:39:36 -04:00
|
|
|
// Note that unlike os.Mkdir(), this function does not return IsExist error
|
|
|
|
// in case path already exists.
|
2017-11-16 01:20:33 -05:00
|
|
|
func MkdirAndChown(path string, mode os.FileMode, owner Identity) error {
|
|
|
|
return mkdirAs(path, mode, owner, false, true)
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2020-10-06 15:30:07 -04:00
|
|
|
// directories along the path exist, no change of ownership or permissions will be performed
|
2017-11-16 01:20:33 -05:00
|
|
|
func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error {
|
|
|
|
return mkdirAs(path, mode, owner, true, false)
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
2015-10-08 11:46:10 -04:00
|
|
|
// 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) {
|
2017-05-31 17:18:04 -04:00
|
|
|
uid, err := toHost(0, uidMap)
|
|
|
|
if err != nil {
|
|
|
|
return -1, -1, err
|
2015-10-08 11:46:10 -04:00
|
|
|
}
|
2017-05-31 17:18:04 -04:00
|
|
|
gid, err := toHost(0, gidMap)
|
|
|
|
if err != nil {
|
|
|
|
return -1, -1, err
|
2015-10-08 11:46:10 -04:00
|
|
|
}
|
|
|
|
return uid, gid, nil
|
|
|
|
}
|
|
|
|
|
2017-05-24 14:10:15 -04:00
|
|
|
// toContainer takes an id mapping, and uses it to translate a
|
2015-10-08 11:46:10 -04:00
|
|
|
// 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
|
2017-05-24 14:10:15 -04:00
|
|
|
func toContainer(hostID int, idMap []IDMap) (int, error) {
|
2015-10-08 11:46:10 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2017-05-31 17:18:04 -04:00
|
|
|
// toHost takes an id mapping and a remapped ID, and translates the
|
2015-10-08 11:46:10 -04:00
|
|
|
// 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 #
|
2017-05-31 17:18:04 -04:00
|
|
|
func toHost(contID int, idMap []IDMap) (int, error) {
|
2015-10-08 11:46:10 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2017-11-16 01:20:33 -05:00
|
|
|
// Identity is either a UID and GID pair or a SID (but not both)
|
|
|
|
type Identity struct {
|
2017-05-19 18:06:46 -04:00
|
|
|
UID int
|
|
|
|
GID int
|
2017-11-16 01:20:33 -05:00
|
|
|
SID string
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
2022-03-14 15:24:29 -04:00
|
|
|
// 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)
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
2022-03-14 15:24:29 -04:00
|
|
|
// 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"`
|
2017-05-24 14:10:15 -04:00
|
|
|
}
|
|
|
|
|
2017-05-31 17:56:23 -04:00
|
|
|
// 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.
|
2022-03-14 15:24:29 -04:00
|
|
|
func (i IdentityMapping) RootPair() Identity {
|
|
|
|
uid, gid, _ := GetRootUIDGID(i.UIDMaps, i.GIDMaps)
|
2017-11-16 01:20:33 -05:00
|
|
|
return Identity{UID: uid, GID: gid}
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
2017-05-31 17:18:04 -04:00
|
|
|
// 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
|
2022-03-14 15:24:29 -04:00
|
|
|
func (i IdentityMapping) ToHost(pair Identity) (Identity, error) {
|
2017-05-31 17:56:23 -04:00
|
|
|
var err error
|
|
|
|
target := i.RootPair()
|
2017-05-24 11:53:41 -04:00
|
|
|
|
2017-05-31 17:18:04 -04:00
|
|
|
if pair.UID != target.UID {
|
2022-03-14 15:24:29 -04:00
|
|
|
target.UID, err = toHost(pair.UID, i.UIDMaps)
|
2017-05-31 17:18:04 -04:00
|
|
|
if err != nil {
|
|
|
|
return target, err
|
|
|
|
}
|
|
|
|
}
|
2017-05-24 11:53:41 -04:00
|
|
|
|
2017-05-31 17:18:04 -04:00
|
|
|
if pair.GID != target.GID {
|
2022-03-14 15:24:29 -04:00
|
|
|
target.GID, err = toHost(pair.GID, i.GIDMaps)
|
2017-05-31 17:18:04 -04:00
|
|
|
}
|
|
|
|
return target, err
|
2017-05-24 14:10:15 -04:00
|
|
|
}
|
|
|
|
|
2017-05-31 17:18:04 -04:00
|
|
|
// ToContainer returns the container UID and GID for the host uid and gid
|
2022-03-14 15:24:29 -04:00
|
|
|
func (i IdentityMapping) ToContainer(pair Identity) (int, int, error) {
|
|
|
|
uid, err := toContainer(pair.UID, i.UIDMaps)
|
2017-05-31 17:18:04 -04:00
|
|
|
if err != nil {
|
|
|
|
return -1, -1, err
|
|
|
|
}
|
2022-03-14 15:24:29 -04:00
|
|
|
gid, err := toContainer(pair.GID, i.GIDMaps)
|
2017-05-31 17:18:04 -04:00
|
|
|
return uid, gid, err
|
2017-05-24 14:10:15 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Empty returns true if there are no id mappings
|
2022-03-14 15:24:29 -04:00
|
|
|
func (i IdentityMapping) Empty() bool {
|
|
|
|
return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0
|
2017-05-24 14:10:15 -04:00
|
|
|
}
|
|
|
|
|
2022-03-14 15:24:29 -04:00
|
|
|
// UIDs returns the mapping for UID.
|
|
|
|
//
|
|
|
|
// Deprecated: reference the UIDMaps field directly.
|
|
|
|
func (i IdentityMapping) UIDs() []IDMap {
|
|
|
|
return i.UIDMaps
|
2017-05-19 18:06:46 -04:00
|
|
|
}
|
|
|
|
|
2022-03-14 15:24:29 -04:00
|
|
|
// GIDs returns the mapping for GID.
|
|
|
|
//
|
|
|
|
// Deprecated: reference the GIDMaps field directly.
|
|
|
|
func (i IdentityMapping) GIDs() []IDMap {
|
|
|
|
return i.GIDMaps
|
2015-10-08 11:46:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func createIDMap(subidRanges ranges) []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) (ranges, error) {
|
|
|
|
return parseSubidFile(subuidFileName, username)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseSubgid(username string) (ranges, error) {
|
|
|
|
return parseSubidFile(subgidFileName, username)
|
|
|
|
}
|
|
|
|
|
2016-03-16 18:44:10 -04:00
|
|
|
// 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
|
2015-10-08 11:46:10 -04:00
|
|
|
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() {
|
|
|
|
text := strings.TrimSpace(s.Text())
|
2016-02-26 08:49:43 -05:00
|
|
|
if text == "" || strings.HasPrefix(text, "#") {
|
2015-10-08 11:46:10 -04:00
|
|
|
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)
|
|
|
|
}
|
2016-03-16 18:44:10 -04:00
|
|
|
if parts[0] == username || username == "ALL" {
|
2015-10-08 11:46:10 -04:00
|
|
|
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})
|
|
|
|
}
|
|
|
|
}
|
2020-03-11 22:16:29 -04:00
|
|
|
|
|
|
|
return rangeList, s.Err()
|
2015-10-08 11:46:10 -04:00
|
|
|
}
|
2020-10-06 15:30:07 -04:00
|
|
|
|
|
|
|
// CurrentIdentity returns the identity of the current process
|
|
|
|
func CurrentIdentity() Identity {
|
|
|
|
return Identity{UID: os.Getuid(), GID: os.Getegid()}
|
|
|
|
}
|