mirror of
https://github.com/containers/skopeo.git
synced 2025-04-27 19:05:32 +00:00
342 lines
9.8 KiB
Go
342 lines
9.8 KiB
Go
//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)
|
|
}
|