mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-09-10 01:27:54 +00:00
Move handling the stat header into `pkg/copy`. All copy-related should ideally be located in this package to increase locality and reduce scattering where possible. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
588 lines
19 KiB
Go
588 lines
19 KiB
Go
package copy
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
buildahCopiah "github.com/containers/buildah/copier"
|
|
"github.com/containers/buildah/pkg/chrootuser"
|
|
"github.com/containers/buildah/util"
|
|
"github.com/containers/podman/v2/libpod"
|
|
"github.com/containers/podman/v2/libpod/define"
|
|
"github.com/containers/podman/v2/pkg/cgroups"
|
|
"github.com/containers/podman/v2/pkg/rootless"
|
|
"github.com/containers/storage"
|
|
"github.com/containers/storage/pkg/archive"
|
|
"github.com/containers/storage/pkg/idtools"
|
|
securejoin "github.com/cyphar/filepath-securejoin"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ********************************* NOTE *************************************
|
|
//
|
|
// Most security bugs are caused by attackers playing around with symlinks
|
|
// trying to escape from the container onto the host and/or trick into data
|
|
// corruption on the host. Hence, file operations on containers (including
|
|
// *stat) should always be handled by `github.com/containers/buildah/copier`
|
|
// which makes sure to evaluate files in a chroot'ed environment.
|
|
//
|
|
// Please make sure to add verbose comments when changing code to make the
|
|
// lives of future readers easier.
|
|
//
|
|
// ****************************************************************************
|
|
|
|
var (
|
|
_stdin = os.Stdin.Name()
|
|
_stdout = os.Stdout.Name()
|
|
)
|
|
|
|
// CopyItem is the source or destination of a copy operation. Use the
|
|
// CopyItemFrom* functions to create one for the specific source/destination
|
|
// item.
|
|
type CopyItem struct {
|
|
// The original path provided by the caller. Useful in error messages.
|
|
original string
|
|
// The resolved path on the host or container. Maybe altered at
|
|
// multiple stages when copying.
|
|
resolved string
|
|
// The root for copying data in a chroot'ed environment.
|
|
root string
|
|
|
|
// IDPair of the resolved path.
|
|
idPair *idtools.IDPair
|
|
// Storage ID mappings.
|
|
idMappings *storage.IDMappingOptions
|
|
|
|
// Internal FileInfo. We really don't want users to mess with a
|
|
// CopyItem but only plug and play with it.
|
|
info FileInfo
|
|
// Error when creating the upper FileInfo. Some errors are non-fatal,
|
|
// for instance, when a destination *base* path does not exist.
|
|
statError error
|
|
|
|
writer io.Writer
|
|
reader io.Reader
|
|
|
|
// Needed to clean up resources (e.g., unmount a container).
|
|
cleanUpFuncs []deferFunc
|
|
}
|
|
|
|
// deferFunc allows for returning functions that must be deferred at call sites.
|
|
type deferFunc func()
|
|
|
|
// Stat returns the FileInfo.
|
|
func (item *CopyItem) Stat() (*FileInfo, error) {
|
|
return &item.info, item.statError
|
|
}
|
|
|
|
// CleanUp releases resources such as the container mounts. It *must* be
|
|
// called even in case of errors.
|
|
func (item *CopyItem) CleanUp() {
|
|
for _, f := range item.cleanUpFuncs {
|
|
f()
|
|
}
|
|
}
|
|
|
|
// CopyItemForWriter returns a CopyItem for the specified io.WriteCloser. Note
|
|
// that the returned item can only act as a copy destination.
|
|
func CopyItemForWriter(writer io.Writer) (item CopyItem, _ error) {
|
|
item.writer = writer
|
|
item.info.IsStream = true
|
|
return item, nil
|
|
}
|
|
|
|
// CopyItemForReader returns a CopyItem for the specified io.ReaderCloser. Note
|
|
// that the returned item can only act as a copy source.
|
|
//
|
|
// Note that the specified reader will be auto-decompressed if needed.
|
|
func CopyItemForReader(reader io.Reader) (item CopyItem, _ error) {
|
|
item.info.IsStream = true
|
|
decompressed, err := archive.DecompressStream(reader)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
item.reader = decompressed
|
|
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
|
|
if err := decompressed.Close(); err != nil {
|
|
logrus.Errorf("Error closing decompressed reader of copy item: %v", err)
|
|
}
|
|
})
|
|
return item, nil
|
|
}
|
|
|
|
// CopyItemForHost creates a CopyItem for the specified host path. It's a
|
|
// destination by default. Use isSource to set it as a destination.
|
|
//
|
|
// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
|
|
func CopyItemForHost(hostPath string, isSource bool) (item CopyItem, _ error) {
|
|
if hostPath == "-" {
|
|
if isSource {
|
|
hostPath = _stdin
|
|
} else {
|
|
hostPath = _stdout
|
|
}
|
|
}
|
|
|
|
if hostPath == _stdin {
|
|
return CopyItemForReader(os.Stdin)
|
|
}
|
|
|
|
if hostPath == _stdout {
|
|
return CopyItemForWriter(os.Stdout)
|
|
}
|
|
|
|
// Now do the dance for the host data.
|
|
resolvedHostPath, err := filepath.Abs(hostPath)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
|
|
resolvedHostPath = preserveBasePath(hostPath, resolvedHostPath)
|
|
item.original = hostPath
|
|
item.resolved = resolvedHostPath
|
|
item.root = "/"
|
|
|
|
statInfo, statError := os.Stat(resolvedHostPath)
|
|
item.statError = statError
|
|
|
|
// It exists, we're done.
|
|
if statError == nil {
|
|
item.info.Name = statInfo.Name()
|
|
item.info.Size = statInfo.Size()
|
|
item.info.Mode = statInfo.Mode()
|
|
item.info.ModTime = statInfo.ModTime()
|
|
item.info.IsDir = statInfo.IsDir()
|
|
item.info.LinkTarget = resolvedHostPath
|
|
return item, nil
|
|
}
|
|
|
|
// The source must exist, but let's try to give some human-friendly
|
|
// errors.
|
|
if isSource {
|
|
if os.IsNotExist(item.statError) {
|
|
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", hostPath)
|
|
}
|
|
return item, item.statError // could be a permission error
|
|
}
|
|
|
|
// If we're a destination, we need to make sure that the parent
|
|
// directory exists.
|
|
parent := filepath.Dir(resolvedHostPath)
|
|
if _, err := os.Stat(parent); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on the host", parent)
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
// CopyItemForContainer creates a CopyItem for the specified path on the
|
|
// container. It's a destination by default. Use isSource to set it as a
|
|
// destination. Note that the container path may resolve to a path outside of
|
|
// the container's mount point if the path hits a volume or mount on the
|
|
// container.
|
|
//
|
|
// Note that callers *must* call (CopyItem).CleanUp(), even in case of errors.
|
|
func CopyItemForContainer(container *libpod.Container, containerPath string, pause bool, isSource bool) (item CopyItem, _ error) {
|
|
// Mount and pause the container.
|
|
containerMountPoint, err := item.mountAndPauseContainer(container, pause)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
|
|
// Make sure that "/" copies the *contents* of the mount point and not
|
|
// the directory.
|
|
if containerPath == "/" {
|
|
containerPath += "/."
|
|
}
|
|
|
|
// Now resolve the container's path. It may hit a volume, it may hit a
|
|
// bind mount, it may be relative.
|
|
resolvedRoot, resolvedContainerPath, err := resolveContainerPaths(container, containerMountPoint, containerPath)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
resolvedContainerPath = preserveBasePath(containerPath, resolvedContainerPath)
|
|
|
|
idMappings, idPair, err := getIDMappingsAndPair(container, containerMountPoint)
|
|
if err != nil {
|
|
return item, err
|
|
}
|
|
|
|
item.original = containerPath
|
|
item.resolved = resolvedContainerPath
|
|
item.root = resolvedRoot
|
|
item.idMappings = idMappings
|
|
item.idPair = idPair
|
|
|
|
statInfo, statError := secureStat(resolvedRoot, resolvedContainerPath)
|
|
item.statError = statError
|
|
|
|
// It exists, we're done.
|
|
if statError == nil {
|
|
item.info.IsDir = statInfo.IsDir
|
|
item.info.Name = filepath.Base(statInfo.Name)
|
|
item.info.Size = statInfo.Size
|
|
item.info.Mode = statInfo.Mode
|
|
item.info.ModTime = statInfo.ModTime
|
|
item.info.IsDir = statInfo.IsDir
|
|
item.info.LinkTarget = resolvedContainerPath
|
|
return item, nil
|
|
}
|
|
|
|
// The source must exist, but let's try to give some human-friendly
|
|
// errors.
|
|
if isSource {
|
|
if os.IsNotExist(statError) {
|
|
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
|
|
}
|
|
return item, item.statError // could be a permission error
|
|
}
|
|
|
|
// If we're a destination, we need to make sure that the parent
|
|
// directory exists.
|
|
parent := filepath.Dir(resolvedContainerPath)
|
|
if _, err := secureStat(resolvedRoot, parent); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return item, errors.Wrapf(os.ErrNotExist, "%q could not be found on container %s (resolved to %q)", containerPath, container.ID(), resolvedContainerPath)
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
return item, nil
|
|
}
|
|
|
|
// putOptions returns PUT options for buildah's copier package.
|
|
func (item *CopyItem) putOptions() buildahCopiah.PutOptions {
|
|
options := buildahCopiah.PutOptions{}
|
|
if item.idMappings != nil {
|
|
options.UIDMap = item.idMappings.UIDMap
|
|
options.GIDMap = item.idMappings.GIDMap
|
|
}
|
|
if item.idPair != nil {
|
|
options.ChownDirs = item.idPair
|
|
options.ChownFiles = item.idPair
|
|
}
|
|
return options
|
|
}
|
|
|
|
// getOptions returns GET options for buildah's copier package.
|
|
func (item *CopyItem) getOptions() buildahCopiah.GetOptions {
|
|
options := buildahCopiah.GetOptions{}
|
|
if item.idMappings != nil {
|
|
options.UIDMap = item.idMappings.UIDMap
|
|
options.GIDMap = item.idMappings.GIDMap
|
|
}
|
|
if item.idPair != nil {
|
|
options.ChownDirs = item.idPair
|
|
options.ChownFiles = item.idPair
|
|
}
|
|
return options
|
|
|
|
}
|
|
|
|
// mount and pause the container. Also set the item's cleanUpFuncs. Those
|
|
// *must* be invoked by callers, even in case of errors.
|
|
func (item *CopyItem) mountAndPauseContainer(container *libpod.Container, pause bool) (string, error) {
|
|
// Make sure to pause and unpause the container. We cannot pause on
|
|
// cgroupsv1 as rootless user, in which case we turn off pausing.
|
|
if pause && rootless.IsRootless() {
|
|
cgroupv2, _ := cgroups.IsCgroup2UnifiedMode()
|
|
if !cgroupv2 {
|
|
logrus.Debugf("Cannot pause container for copying as a rootless user on cgroupsv1: default to not pause")
|
|
pause = false
|
|
}
|
|
}
|
|
|
|
// Mount and unmount the container.
|
|
mountPoint, err := container.Mount()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
|
|
if err := container.Unmount(false); err != nil {
|
|
logrus.Errorf("Error unmounting container after copy operation: %v", err)
|
|
}
|
|
})
|
|
|
|
// Pause and unpause the container.
|
|
if pause {
|
|
if err := container.Pause(); err != nil {
|
|
// Ignore errors when the container isn't running. No
|
|
// need to pause.
|
|
if errors.Cause(err) != define.ErrCtrStateInvalid {
|
|
return "", err
|
|
}
|
|
} else {
|
|
item.cleanUpFuncs = append(item.cleanUpFuncs, func() {
|
|
if err := container.Unpause(); err != nil {
|
|
logrus.Errorf("Error unpausing container after copy operation: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
return mountPoint, nil
|
|
}
|
|
|
|
// buildahGlobs returns the root, dir and glob used in buildah's copier
|
|
// package.
|
|
//
|
|
// Note that dir is always empty.
|
|
func (item *CopyItem) buildahGlobs() (root string, glob string, err error) {
|
|
root = item.root
|
|
|
|
// If the root and the resolved path are equal, then dir must be empty
|
|
// and the glob must be ".".
|
|
if filepath.Clean(root) == filepath.Clean(item.resolved) {
|
|
glob = "."
|
|
return
|
|
}
|
|
|
|
glob, err = filepath.Rel(root, item.resolved)
|
|
return
|
|
}
|
|
|
|
// preserveBasePath makes sure that the original base path (e.g., "/" or "./")
|
|
// is preserved. The filepath API among tends to clean up a bit too much but
|
|
// we *must* preserve this data by all means.
|
|
func preserveBasePath(original, resolved string) string {
|
|
// Handle "/"
|
|
if strings.HasSuffix(original, "/") {
|
|
if !strings.HasSuffix(resolved, "/") {
|
|
resolved += "/"
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
// Handle "/."
|
|
if strings.HasSuffix(original, "/.") {
|
|
if strings.HasSuffix(resolved, "/") { // could be root!
|
|
resolved += "."
|
|
} else if !strings.HasSuffix(resolved, "/.") {
|
|
resolved += "/."
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
return resolved
|
|
}
|
|
|
|
// secureStat extracts file info for path in a chroot'ed environment in root.
|
|
func secureStat(root string, path string) (*buildahCopiah.StatForItem, error) {
|
|
var glob string
|
|
var err error
|
|
|
|
// If root and path are equal, then dir must be empty and the glob must
|
|
// be ".".
|
|
if filepath.Clean(root) == filepath.Clean(path) {
|
|
glob = "."
|
|
} else {
|
|
glob, err = filepath.Rel(root, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
globStats, err := buildahCopiah.Stat(root, "", buildahCopiah.StatOptions{}, []string{glob})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(globStats) != 1 {
|
|
return nil, errors.Errorf("internal libpod error: secureStat: expected 1 item but got %d", len(globStats))
|
|
}
|
|
|
|
stat, exists := globStats[0].Results[glob] // only one glob passed, so that's okay
|
|
if !exists {
|
|
return stat, os.ErrNotExist
|
|
}
|
|
|
|
var statErr error
|
|
if stat.Error != "" {
|
|
statErr = errors.New(stat.Error)
|
|
}
|
|
return stat, statErr
|
|
}
|
|
|
|
// resolveContainerPaths resolves the container's mount point and the container
|
|
// path as specified by the user. Both may resolve to paths outside of the
|
|
// container's mount point when the container path hits a volume or bind mount.
|
|
//
|
|
// NOTE: We must take volumes and bind mounts into account as, regrettably, we
|
|
// can copy to/from stopped containers. In that case, the volumes and bind
|
|
// mounts are not present. For running containers, the runtime (e.g., runc or
|
|
// crun) takes care of these mounts. For stopped ones, we need to do quite
|
|
// some dance, as done below.
|
|
func resolveContainerPaths(container *libpod.Container, mountPoint string, containerPath string) (string, string, error) {
|
|
// Let's first make sure we have a path relative to the mount point.
|
|
pathRelativeToContainerMountPoint := containerPath
|
|
if !filepath.IsAbs(containerPath) {
|
|
// If the containerPath is not absolute, it's relative to the
|
|
// container's working dir. To be extra careful, let's first
|
|
// join the working dir with "/", and the add the containerPath
|
|
// to it.
|
|
pathRelativeToContainerMountPoint = filepath.Join(filepath.Join("/", container.WorkingDir()), containerPath)
|
|
}
|
|
// NOTE: the secure join makes sure that we follow symlinks. This way,
|
|
// we catch scenarios where the container path symlinks to a volume or
|
|
// bind mount.
|
|
resolvedPathOnTheContainerMountPoint, err := securejoin.SecureJoin(mountPoint, pathRelativeToContainerMountPoint)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
pathRelativeToContainerMountPoint = strings.TrimPrefix(pathRelativeToContainerMountPoint, mountPoint)
|
|
pathRelativeToContainerMountPoint = filepath.Join("/", pathRelativeToContainerMountPoint)
|
|
|
|
// Now we have an "absolute container Path" but not yet resolved on the
|
|
// host (e.g., "/foo/bar/file.txt"). As mentioned above, we need to
|
|
// check if "/foo/bar/file.txt" is on a volume or bind mount. To do
|
|
// that, we need to walk *down* the paths to the root. Assuming
|
|
// volume-1 is mounted to "/foo" and volume-2 is mounted to "/foo/bar",
|
|
// we must select "/foo/bar". Once selected, we need to rebase the
|
|
// remainder (i.e, "/file.txt") on the volume's mount point on the
|
|
// host. Same applies to bind mounts.
|
|
|
|
searchPath := pathRelativeToContainerMountPoint
|
|
for {
|
|
volume, err := findVolume(container, searchPath)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if volume != nil {
|
|
logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)
|
|
// We found a matching volume for searchPath. We now
|
|
// need to first find the relative path of our input
|
|
// path to the searchPath, and then join it with the
|
|
// volume's mount point.
|
|
pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
|
|
absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(volume.MountPoint(), pathRelativeToVolume)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return volume.MountPoint(), absolutePathOnTheVolumeMount, nil
|
|
}
|
|
|
|
if mount := findBindMount(container, searchPath); mount != nil {
|
|
logrus.Debugf("Container path %q resolved to bind mount %q:%q on path %q", containerPath, mount.Source, mount.Destination, searchPath)
|
|
// We found a matching bind mount for searchPath. We
|
|
// now need to first find the relative path of our
|
|
// input path to the searchPath, and then join it with
|
|
// the source of the bind mount.
|
|
pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
|
|
absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return mount.Source, absolutePathOnTheBindMount, nil
|
|
|
|
}
|
|
|
|
if searchPath == "/" {
|
|
// Cannot go beyond "/", so we're done.
|
|
break
|
|
}
|
|
|
|
// Walk *down* the path (e.g., "/foo/bar/x" -> "/foo/bar").
|
|
searchPath = filepath.Dir(searchPath)
|
|
}
|
|
|
|
// No volume, no bind mount but just a normal path on the container.
|
|
return mountPoint, resolvedPathOnTheContainerMountPoint, nil
|
|
}
|
|
|
|
// findVolume checks if the specified container path matches a volume inside
|
|
// the container. It returns a matching volume or nil.
|
|
func findVolume(c *libpod.Container, containerPath string) (*libpod.Volume, error) {
|
|
runtime := c.Runtime()
|
|
cleanedContainerPath := filepath.Clean(containerPath)
|
|
for _, vol := range c.Config().NamedVolumes {
|
|
if cleanedContainerPath == filepath.Clean(vol.Dest) {
|
|
return runtime.GetVolume(vol.Name)
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// findBindMount checks if the specified container path matches a bind mount
|
|
// inside the container. It returns a matching mount or nil.
|
|
func findBindMount(c *libpod.Container, containerPath string) *specs.Mount {
|
|
cleanedPath := filepath.Clean(containerPath)
|
|
for _, m := range c.Config().Spec.Mounts {
|
|
if m.Type != "bind" {
|
|
continue
|
|
}
|
|
if cleanedPath == filepath.Clean(m.Destination) {
|
|
mount := m
|
|
return &mount
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getIDMappingsAndPair returns the ID mappings for the container and the host
|
|
// ID pair.
|
|
func getIDMappingsAndPair(container *libpod.Container, containerMount string) (*storage.IDMappingOptions, *idtools.IDPair, error) {
|
|
user, err := getContainerUser(container, containerMount)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
idMappingOpts, err := container.IDMappings()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
hostUID, hostGID, err := util.GetHostIDs(idtoolsToRuntimeSpec(idMappingOpts.UIDMap), idtoolsToRuntimeSpec(idMappingOpts.GIDMap), user.UID, user.GID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
idPair := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)}
|
|
return &idMappingOpts, &idPair, nil
|
|
}
|
|
|
|
// getContainerUser returns the specs.User of the container.
|
|
func getContainerUser(container *libpod.Container, mountPoint string) (specs.User, error) {
|
|
userspec := container.Config().User
|
|
|
|
uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec)
|
|
u := specs.User{
|
|
UID: uid,
|
|
GID: gid,
|
|
Username: userspec,
|
|
}
|
|
|
|
if !strings.Contains(userspec, ":") {
|
|
groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID))
|
|
if err2 != nil {
|
|
if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil {
|
|
err = err2
|
|
}
|
|
} else {
|
|
u.AdditionalGids = groups
|
|
}
|
|
}
|
|
|
|
return u, err
|
|
}
|
|
|
|
// idtoolsToRuntimeSpec converts idtools ID mapping to the one of the runtime spec.
|
|
func idtoolsToRuntimeSpec(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) {
|
|
for _, idmap := range idMaps {
|
|
tempIDMap := specs.LinuxIDMapping{
|
|
ContainerID: uint32(idmap.ContainerID),
|
|
HostID: uint32(idmap.HostID),
|
|
Size: uint32(idmap.Size),
|
|
}
|
|
convertedIDMap = append(convertedIDMap, tempIDMap)
|
|
}
|
|
return convertedIDMap
|
|
}
|