summaryrefslogtreecommitdiff
path: root/pkg/copy/item.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/copy/item.go')
-rw-r--r--pkg/copy/item.go588
1 files changed, 0 insertions, 588 deletions
diff --git a/pkg/copy/item.go b/pkg/copy/item.go
deleted file mode 100644
index df8bf30b9..000000000
--- a/pkg/copy/item.go
+++ /dev/null
@@ -1,588 +0,0 @@
-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
-}