//go:build linux package storage import ( "fmt" "os" "os/user" "strconv" drivers "github.com/containers/storage/drivers" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/unshare" "github.com/containers/storage/types" securejoin "github.com/cyphar/filepath-securejoin" libcontainerUser "github.com/moby/sys/user" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) // getAdditionalSubIDs looks up the additional IDs configured for // the specified user. // The argument USERNAME is ignored for rootless users, as it is not // possible to use an arbitrary entry in /etc/sub*id. // Differently, if the username is not specified for root users, a // default name is used. func getAdditionalSubIDs(username string) (*idSet, *idSet, error) { var uids, gids *idSet if unshare.IsRootless() { username = os.Getenv("USER") if username == "" { var id string if os.Geteuid() == 0 { id = strconv.Itoa(unshare.GetRootlessUID()) } else { id = strconv.Itoa(os.Geteuid()) } userID, err := user.LookupId(id) if err == nil { username = userID.Username } } } else if username == "" { username = RootAutoUserNsUser } mappings, err := idtools.NewIDMappings(username, username) if err != nil { logrus.Errorf("Cannot find mappings for user %q: %v", username, err) } else { uids = getHostIDs(mappings.UIDs()) gids = getHostIDs(mappings.GIDs()) } return uids, gids, nil } // getAvailableIDs returns the list of ranges that are usable by the current user. // When running as root, it looks up the additional IDs assigned to the specified user. // When running as rootless, the mappings assigned to the unprivileged user are converted // to the IDs inside of the initial rootless user namespace. func (s *store) getAvailableIDs() (*idSet, *idSet, error) { if s.additionalUIDs == nil { uids, gids, err := getAdditionalSubIDs(s.autoUsernsUser) if err != nil { return nil, nil, err } // Store the result so we don't need to look it up again next time s.additionalUIDs, s.additionalGIDs = uids, gids } if !unshare.IsRootless() { // No mapping to inner namespace needed return s.additionalUIDs, s.additionalGIDs, nil } // We are already inside of the rootless user namespace. // We need to remap the configured mappings to what is available // inside of the rootless userns. u := newIDSet([]interval{{start: 1, end: s.additionalUIDs.size() + 1}}) g := newIDSet([]interval{{start: 1, end: s.additionalGIDs.size() + 1}}) return u, g, nil } // nobodyUser returns the UID and GID of the "nobody" user. Hardcode its value // for simplicity. const nobodyUser = 65534 // parseMountedFiles returns the maximum UID and GID found in the /etc/passwd and // /etc/group files. func parseMountedFiles(containerMount, passwdFile, groupFile string) uint32 { var ( passwd *os.File group *os.File size int err error ) if passwdFile == "" { passwd, err = secureOpen(containerMount, "/etc/passwd") } else { // User-specified override from a volume. Will not be in // container root. passwd, err = os.Open(passwdFile) } if err == nil { defer passwd.Close() users, err := libcontainerUser.ParsePasswd(passwd) if err == nil { for _, u := range users { // Skip the "nobody" user otherwise we end up with 65536 // ids with most images if u.Name == "nobody" || u.Name == "nogroup" { continue } if u.Uid > size && u.Uid != nobodyUser { size = u.Uid + 1 } if u.Gid > size && u.Gid != nobodyUser { size = u.Gid + 1 } } } } if groupFile == "" { group, err = secureOpen(containerMount, "/etc/group") } else { // User-specified override from a volume. Will not be in // container root. group, err = os.Open(groupFile) } if err == nil { defer group.Close() groups, err := libcontainerUser.ParseGroup(group) if err == nil { for _, g := range groups { if g.Name == "nobody" || g.Name == "nogroup" { continue } if g.Gid > size && g.Gid != nobodyUser { size = g.Gid + 1 } } } } return uint32(size) } // getMaxSizeFromImage returns the maximum ID used by the specified image. // On entry, rlstore must be locked for writing, and lstores must be locked for reading. func (s *store) getMaxSizeFromImage(image *Image, rlstore rwLayerStore, lstores []roLayerStore, passwdFile, groupFile string) (_ uint32, retErr error) { layerStores := append([]roLayerStore{rlstore}, lstores...) size := uint32(0) var topLayer *Layer layerName := image.TopLayer outer: for { for _, ls := range layerStores { layer, err := ls.Get(layerName) if err != nil { continue } if image.TopLayer == layerName { topLayer = layer } for _, uid := range layer.UIDs { if uid >= size { size = uid + 1 } } for _, gid := range layer.GIDs { if gid >= size { size = gid + 1 } } layerName = layer.Parent if layerName == "" { break outer } continue outer } return 0, fmt.Errorf("cannot find layer %q", layerName) } layerOptions := &LayerOptions{ IDMappingOptions: types.IDMappingOptions{ HostUIDMapping: true, HostGIDMapping: true, UIDMap: nil, GIDMap: nil, }, } // We need to create a temporary layer so we can mount it and lookup the // maximum IDs used. clayer, _, err := rlstore.create("", topLayer, nil, "", nil, layerOptions, false, nil, nil) if err != nil { return 0, err } defer func() { if err2 := rlstore.Delete(clayer.ID); err2 != nil { if retErr == nil { retErr = fmt.Errorf("deleting temporary layer %#v: %w", clayer.ID, err2) } else { logrus.Errorf("Error deleting temporary layer %#v: %v", clayer.ID, err2) } } }() mountOptions := drivers.MountOpts{ MountLabel: "", UidMaps: nil, GidMaps: nil, Options: nil, } mountpoint, err := rlstore.Mount(clayer.ID, mountOptions) if err != nil { return 0, err } defer func() { if _, err2 := rlstore.unmount(clayer.ID, true, false); err2 != nil { if retErr == nil { retErr = fmt.Errorf("unmounting temporary layer %#v: %w", clayer.ID, err2) } else { logrus.Errorf("Error unmounting temporary layer %#v: %v", clayer.ID, err2) } } }() userFilesSize := parseMountedFiles(mountpoint, passwdFile, groupFile) if userFilesSize > size { size = userFilesSize } return size, nil } // getAutoUserNS creates an automatic user namespace // If image != nil, On entry, rlstore must be locked for writing, and lstores must be locked for reading. func (s *store) getAutoUserNS(options *types.AutoUserNsOptions, image *Image, rlstore rwLayerStore, lstores []roLayerStore) ([]idtools.IDMap, []idtools.IDMap, error) { requestedSize := uint32(0) initialSize := uint32(1) if options.Size > 0 { requestedSize = options.Size } if options.InitialSize > 0 { initialSize = options.InitialSize } availableUIDs, availableGIDs, err := s.getAvailableIDs() if err != nil { return nil, nil, fmt.Errorf("cannot read mappings: %w", err) } // Look at every container that is using a user namespace and store // the intervals that are already used. containers, err := s.Containers() if err != nil { return nil, nil, err } var usedUIDs, usedGIDs []idtools.IDMap for _, c := range containers { usedUIDs = append(usedUIDs, c.UIDMap...) usedGIDs = append(usedGIDs, c.GIDMap...) } size := requestedSize // If there is no requestedSize, lookup the maximum used IDs in the layers // metadata. Make sure the size is at least s.autoNsMinSize and it is not // bigger than s.autoNsMaxSize. // This is a best effort heuristic. if requestedSize == 0 { size = max(s.autoNsMinSize, initialSize) if image != nil { sizeFromImage, err := s.getMaxSizeFromImage(image, rlstore, lstores, options.PasswdFile, options.GroupFile) if err != nil { return nil, nil, err } if sizeFromImage > size { size = sizeFromImage } } if s.autoNsMaxSize > 0 && size > s.autoNsMaxSize { return nil, nil, fmt.Errorf("the container needs a user namespace with size %v that is bigger than the maximum value allowed with userns=auto %v", size, s.autoNsMaxSize) } } return getAutoUserNSIDMappings( int(size), availableUIDs, availableGIDs, usedUIDs, usedGIDs, options.AdditionalUIDMappings, options.AdditionalGIDMappings, ) } // getAutoUserNSIDMappings computes the user/group id mappings for the automatic user namespace. func getAutoUserNSIDMappings( size int, availableUIDs, availableGIDs *idSet, usedUIDMappings, usedGIDMappings, additionalUIDMappings, additionalGIDMappings []idtools.IDMap, ) ([]idtools.IDMap, []idtools.IDMap, error) { usedUIDs := getHostIDs(append(usedUIDMappings, additionalUIDMappings...)) usedGIDs := getHostIDs(append(usedGIDMappings, additionalGIDMappings...)) // Exclude additional uids and gids from requested range. targetIDs := newIDSet([]interval{{start: 0, end: size}}) requestedContainerUIDs := targetIDs.subtract(getContainerIDs(additionalUIDMappings)) requestedContainerGIDs := targetIDs.subtract(getContainerIDs(additionalGIDMappings)) // Make sure the specified additional IDs are not used as part of the automatic // mapping availableUIDs, err := availableUIDs.subtract(usedUIDs).findAvailable(requestedContainerUIDs.size()) if err != nil { return nil, nil, err } availableGIDs, err = availableGIDs.subtract(usedGIDs).findAvailable(requestedContainerGIDs.size()) if err != nil { return nil, nil, err } uidMap := append(availableUIDs.zip(requestedContainerUIDs), additionalUIDMappings...) gidMap := append(availableGIDs.zip(requestedContainerGIDs), additionalGIDMappings...) return uidMap, gidMap, nil } // Securely open (read-only) a file in a container mount. func secureOpen(containerMount, file string) (*os.File, error) { tmpFile, err := securejoin.OpenInRoot(containerMount, file) if err != nil { return nil, err } defer tmpFile.Close() return securejoin.Reopen(tmpFile, unix.O_RDONLY) }