1package idtools // import "github.com/docker/docker/pkg/idtools" 2 3import ( 4 "bufio" 5 "fmt" 6 "os" 7 "sort" 8 "strconv" 9 "strings" 10) 11 12// IDMap contains a single entry for user namespace range remapping. An array 13// of IDMap entries represents the structure that will be provided to the Linux 14// kernel for creating a user namespace. 15type IDMap struct { 16 ContainerID int `json:"container_id"` 17 HostID int `json:"host_id"` 18 Size int `json:"size"` 19} 20 21type subIDRange struct { 22 Start int 23 Length int 24} 25 26type ranges []subIDRange 27 28func (e ranges) Len() int { return len(e) } 29func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } 30func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start } 31 32const ( 33 subuidFileName = "/etc/subuid" 34 subgidFileName = "/etc/subgid" 35) 36 37// MkdirAllAndChown creates a directory (include any along the path) and then modifies 38// ownership to the requested uid/gid. If the directory already exists, this 39// function will still change ownership to the requested uid/gid pair. 40func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error { 41 return mkdirAs(path, mode, owner, true, true) 42} 43 44// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. 45// If the directory already exists, this function still changes ownership. 46// Note that unlike os.Mkdir(), this function does not return IsExist error 47// in case path already exists. 48func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { 49 return mkdirAs(path, mode, owner, false, true) 50} 51 52// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies 53// ownership ONLY of newly created directories to the requested uid/gid. If the 54// directories along the path exist, no change of ownership will be performed 55func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error { 56 return mkdirAs(path, mode, owner, true, false) 57} 58 59// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. 60// If the maps are empty, then the root uid/gid will default to "real" 0/0 61func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { 62 uid, err := toHost(0, uidMap) 63 if err != nil { 64 return -1, -1, err 65 } 66 gid, err := toHost(0, gidMap) 67 if err != nil { 68 return -1, -1, err 69 } 70 return uid, gid, nil 71} 72 73// toContainer takes an id mapping, and uses it to translate a 74// host ID to the remapped ID. If no map is provided, then the translation 75// assumes a 1-to-1 mapping and returns the passed in id 76func toContainer(hostID int, idMap []IDMap) (int, error) { 77 if idMap == nil { 78 return hostID, nil 79 } 80 for _, m := range idMap { 81 if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) { 82 contID := m.ContainerID + (hostID - m.HostID) 83 return contID, nil 84 } 85 } 86 return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) 87} 88 89// toHost takes an id mapping and a remapped ID, and translates the 90// ID to the mapped host ID. If no map is provided, then the translation 91// assumes a 1-to-1 mapping and returns the passed in id # 92func toHost(contID int, idMap []IDMap) (int, error) { 93 if idMap == nil { 94 return contID, nil 95 } 96 for _, m := range idMap { 97 if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) { 98 hostID := m.HostID + (contID - m.ContainerID) 99 return hostID, nil 100 } 101 } 102 return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) 103} 104 105// Identity is either a UID and GID pair or a SID (but not both) 106type Identity struct { 107 UID int 108 GID int 109 SID string 110} 111 112// IdentityMapping contains a mappings of UIDs and GIDs 113type IdentityMapping struct { 114 uids []IDMap 115 gids []IDMap 116} 117 118// NewIdentityMapping takes a requested user and group name and 119// using the data from /etc/sub{uid,gid} ranges, creates the 120// proper uid and gid remapping ranges for that user/group pair 121func NewIdentityMapping(username, groupname string) (*IdentityMapping, error) { 122 subuidRanges, err := parseSubuid(username) 123 if err != nil { 124 return nil, err 125 } 126 subgidRanges, err := parseSubgid(groupname) 127 if err != nil { 128 return nil, err 129 } 130 if len(subuidRanges) == 0 { 131 return nil, fmt.Errorf("No subuid ranges found for user %q", username) 132 } 133 if len(subgidRanges) == 0 { 134 return nil, fmt.Errorf("No subgid ranges found for group %q", groupname) 135 } 136 137 return &IdentityMapping{ 138 uids: createIDMap(subuidRanges), 139 gids: createIDMap(subgidRanges), 140 }, nil 141} 142 143// NewIDMappingsFromMaps creates a new mapping from two slices 144// Deprecated: this is a temporary shim while transitioning to IDMapping 145func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IdentityMapping { 146 return &IdentityMapping{uids: uids, gids: gids} 147} 148 149// RootPair returns a uid and gid pair for the root user. The error is ignored 150// because a root user always exists, and the defaults are correct when the uid 151// and gid maps are empty. 152func (i *IdentityMapping) RootPair() Identity { 153 uid, gid, _ := GetRootUIDGID(i.uids, i.gids) 154 return Identity{UID: uid, GID: gid} 155} 156 157// ToHost returns the host UID and GID for the container uid, gid. 158// Remapping is only performed if the ids aren't already the remapped root ids 159func (i *IdentityMapping) ToHost(pair Identity) (Identity, error) { 160 var err error 161 target := i.RootPair() 162 163 if pair.UID != target.UID { 164 target.UID, err = toHost(pair.UID, i.uids) 165 if err != nil { 166 return target, err 167 } 168 } 169 170 if pair.GID != target.GID { 171 target.GID, err = toHost(pair.GID, i.gids) 172 } 173 return target, err 174} 175 176// ToContainer returns the container UID and GID for the host uid and gid 177func (i *IdentityMapping) ToContainer(pair Identity) (int, int, error) { 178 uid, err := toContainer(pair.UID, i.uids) 179 if err != nil { 180 return -1, -1, err 181 } 182 gid, err := toContainer(pair.GID, i.gids) 183 return uid, gid, err 184} 185 186// Empty returns true if there are no id mappings 187func (i *IdentityMapping) Empty() bool { 188 return len(i.uids) == 0 && len(i.gids) == 0 189} 190 191// UIDs return the UID mapping 192// TODO: remove this once everything has been refactored to use pairs 193func (i *IdentityMapping) UIDs() []IDMap { 194 return i.uids 195} 196 197// GIDs return the UID mapping 198// TODO: remove this once everything has been refactored to use pairs 199func (i *IdentityMapping) GIDs() []IDMap { 200 return i.gids 201} 202 203func createIDMap(subidRanges ranges) []IDMap { 204 idMap := []IDMap{} 205 206 // sort the ranges by lowest ID first 207 sort.Sort(subidRanges) 208 containerID := 0 209 for _, idrange := range subidRanges { 210 idMap = append(idMap, IDMap{ 211 ContainerID: containerID, 212 HostID: idrange.Start, 213 Size: idrange.Length, 214 }) 215 containerID = containerID + idrange.Length 216 } 217 return idMap 218} 219 220func parseSubuid(username string) (ranges, error) { 221 return parseSubidFile(subuidFileName, username) 222} 223 224func parseSubgid(username string) (ranges, error) { 225 return parseSubidFile(subgidFileName, username) 226} 227 228// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid) 229// and return all found ranges for a specified username. If the special value 230// "ALL" is supplied for username, then all ranges in the file will be returned 231func parseSubidFile(path, username string) (ranges, error) { 232 var rangeList ranges 233 234 subidFile, err := os.Open(path) 235 if err != nil { 236 return rangeList, err 237 } 238 defer subidFile.Close() 239 240 s := bufio.NewScanner(subidFile) 241 for s.Scan() { 242 if err := s.Err(); err != nil { 243 return rangeList, err 244 } 245 246 text := strings.TrimSpace(s.Text()) 247 if text == "" || strings.HasPrefix(text, "#") { 248 continue 249 } 250 parts := strings.Split(text, ":") 251 if len(parts) != 3 { 252 return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path) 253 } 254 if parts[0] == username || username == "ALL" { 255 startid, err := strconv.Atoi(parts[1]) 256 if err != nil { 257 return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) 258 } 259 length, err := strconv.Atoi(parts[2]) 260 if err != nil { 261 return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err) 262 } 263 rangeList = append(rangeList, subIDRange{startid, length}) 264 } 265 } 266 return rangeList, nil 267} 268