From adcb3a7a609ba756f8b9de17521f0e3dce3b778e Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Fri, 11 Dec 2020 11:00:25 +0100 Subject: remote copy Implement `podman-remote cp` and break out the logic from the previously added `pkg/copy` into it's basic building blocks and move them up into the `ContainerEngine` interface and `cmd/podman`. The `--pause` and `--extract` flags are now deprecated and turned into nops. Note that this commit is vendoring a non-release version of Buildah to pull in updates to the copier package. Signed-off-by: Valentin Rothberg --- pkg/copy/item.go | 588 ------------------------------------------------------- 1 file changed, 588 deletions(-) delete mode 100644 pkg/copy/item.go (limited to 'pkg/copy/item.go') 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 -} -- cgit v1.2.3-54-g00ecf