podman/libpod/container_stat_linux.go
Valentin Rothberg 1f2f7e7459 podman cp: evaluate symlink correctly when copying from container
When copying from a container, make sure to evaluate the symlinks
correctly.  Add tests copying a symlinked directory from a running and
a non-running container to execute both path-resolution paths.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
2021-03-09 10:45:15 +01:00

182 lines
5.9 KiB
Go

// +build linux
package libpod
import (
"context"
"os"
"path/filepath"
"strings"
"github.com/containers/buildah/copier"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/copy"
"github.com/pkg/errors"
)
// statInsideMount stats the specified path *inside* the container's mount and PID
// namespace. It returns the file info along with the resolved root ("/") and
// the resolved path (relative to the root).
func (c *Container) statInsideMount(ctx context.Context, containerPath string) (*copier.StatForItem, string, string, error) {
resolvedRoot := "/"
resolvedPath := c.pathAbs(containerPath)
var statInfo *copier.StatForItem
err := c.joinMountAndExec(ctx,
func() error {
var statErr error
statInfo, statErr = secureStat(resolvedRoot, resolvedPath)
return statErr
},
)
return statInfo, resolvedRoot, resolvedPath, err
}
// statOnHost stats the specified path *on the host*. It returns the file info
// along with the resolved root and the resolved path. Both paths are absolute
// to the host's root. Note that the paths may resolved outside the
// container's mount point (e.g., to a volume or bind mount).
func (c *Container) statOnHost(ctx context.Context, mountPoint string, containerPath string) (*copier.StatForItem, string, string, error) {
// Now resolve the container's path. It may hit a volume, it may hit a
// bind mount, it may be relative.
resolvedRoot, resolvedPath, err := c.resolvePath(mountPoint, containerPath)
if err != nil {
return nil, "", "", err
}
statInfo, err := secureStat(resolvedRoot, resolvedPath)
return statInfo, resolvedRoot, resolvedPath, err
}
func (c *Container) stat(ctx context.Context, containerMountPoint string, containerPath string) (*define.FileInfo, string, string, error) {
var (
resolvedRoot string
resolvedPath string
absContainerPath string
statInfo *copier.StatForItem
statErr error
)
// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if containerPath == "/" {
containerPath = "/."
}
// Wildcards are not allowed.
// TODO: it's now technically possible wildcards.
// We may consider enabling support in the future.
if strings.Contains(containerPath, "*") {
return nil, "", "", copy.ErrENOENT
}
if c.state.State == define.ContainerStateRunning {
// If the container is running, we need to join it's mount namespace
// and stat there.
statInfo, resolvedRoot, resolvedPath, statErr = c.statInsideMount(ctx, containerPath)
} else {
// If the container is NOT running, we need to resolve the path
// on the host.
statInfo, resolvedRoot, resolvedPath, statErr = c.statOnHost(ctx, containerMountPoint, containerPath)
}
if statErr != nil {
if statInfo == nil {
return nil, "", "", statErr
}
// Not all errors from secureStat map to ErrNotExist, so we
// have to look into the error string. Turning it into an
// ENOENT let's the API handlers return the correct status code
// which is crucial for the remote client.
if os.IsNotExist(statErr) || strings.Contains(statErr.Error(), "o such file or directory") {
statErr = copy.ErrENOENT
}
}
if statInfo.IsSymlink {
// Symlinks are already evaluated and always relative to the
// container's mount point.
absContainerPath = statInfo.ImmediateTarget
} else if strings.HasPrefix(resolvedPath, containerMountPoint) {
// If the path is on the container's mount point, strip it off.
absContainerPath = strings.TrimPrefix(resolvedPath, containerMountPoint)
absContainerPath = filepath.Join("/", absContainerPath)
} else {
// No symlink and not on the container's mount point, so let's
// move it back to the original input. It must have evaluated
// to a volume or bind mount but we cannot return host paths.
absContainerPath = containerPath
}
// Preserve the base path as specified by the user. The `filepath`
// packages likes to remove trailing slashes and dots that are crucial
// to the copy logic.
absContainerPath = copy.PreserveBasePath(containerPath, absContainerPath)
resolvedPath = copy.PreserveBasePath(containerPath, resolvedPath)
info := &define.FileInfo{
IsDir: statInfo.IsDir,
Name: filepath.Base(absContainerPath),
Size: statInfo.Size,
Mode: statInfo.Mode,
ModTime: statInfo.ModTime,
LinkTarget: absContainerPath,
}
return info, resolvedRoot, resolvedPath, statErr
}
// secureStat extracts file info for path in a chroot'ed environment in root.
func secureStat(root string, path string) (*copier.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 := copier.Stat(root, "", copier.StatOptions{}, []string{glob})
if err != nil {
return nil, err
}
if len(globStats) != 1 {
return nil, errors.Errorf("internal error: secureStat: expected 1 item but got %d", len(globStats))
}
if len(globStats) != 1 {
return nil, errors.Errorf("internal error: secureStat: expected 1 result but got %d", len(globStats[0].Results))
}
// NOTE: the key in the map differ from `glob` when hitting symlink.
// Hence, we just take the first (and only) key/value pair.
for _, stat := range globStats[0].Results {
var statErr error
if stat.Error != "" {
statErr = errors.New(stat.Error)
}
// If necessary evaluate the symlink
if stat.IsSymlink {
target, err := copier.Eval(root, path, copier.EvalOptions{})
if err != nil {
return nil, errors.Wrap(err, "error evaluating symlink in container")
}
// Need to make sure the symlink is relative to the root!
target = strings.TrimPrefix(target, root)
target = filepath.Join("/", target)
stat.ImmediateTarget = target
}
return stat, statErr
}
// Nothing found!
return nil, copy.ErrENOENT
}