Skip to content

Commit

Permalink
podman cp: support copying on tmpfs mounts
Browse files Browse the repository at this point in the history
Traditionally, the path resolution for containers has been resolved on
the *host*; relative to the container's mount point or relative to
specified bind mounts or volumes.

While this works nicely for non-running containers, it poses a problem
for running ones.  In that case, certain kinds of mounts (e.g., tmpfs)
will not resolve correctly.  A tmpfs is held in memory and hence cannot
be resolved relatively to the container's mount point.  A copy operation
will succeed but the data will not show up inside the container.

To support these kinds of mounts, we need to join the *running*
container's mount namespace (and PID namespace) when copying.

Note that this change implies moving the copy and stat logic into
`libpod` since we need to keep the container locked to avoid race
conditions.  The immediate benefit is that all logic is now inside
`libpod`; the code isn't scattered anymore.

Further note that Docker does not support copying to tmpfs mounts.

Tests have been extended to cover *both* path resolutions for running
and created containers.  New tests have been added to exercise the
tmpfs-mount case.

For the record: Some tests could be improved by using `start -a` instead
of a start-exec sequence.  Unfortunately, `start -a` is flaky in the CI
which forced me to use the more expensive start-exec option.

Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg committed Mar 10, 2021
1 parent 08eba6f commit 1671dac
Show file tree
Hide file tree
Showing 16 changed files with 713 additions and 382 deletions.
5 changes: 3 additions & 2 deletions cmd/podman/containers/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ func copyFromContainer(container string, containerPath string, hostPath string)
}

putOptions := buildahCopiah.PutOptions{
ChownDirs: &idPair,
ChownFiles: &idPair,
ChownDirs: &idPair,
ChownFiles: &idPair,
IgnoreDevices: true,
}
if !containerInfo.IsDir && (!hostInfo.IsDir || hostInfoErr != nil) {
// If we're having a file-to-file copy, make sure to
Expand Down
6 changes: 6 additions & 0 deletions libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,12 @@ func (c *Container) NamespacePath(linuxNS LinuxNS) (string, error) { //nolint:in
}
}

return c.namespacePath(linuxNS)
}

// namespacePath returns the path of one of the container's namespaces
// If the container is not running, an error will be returned
func (c *Container) namespacePath(linuxNS LinuxNS) (string, error) { //nolint:interfacer
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return "", errors.Wrapf(define.ErrCtrStopped, "cannot get namespace path unless container %s is running", c.ID())
}
Expand Down
64 changes: 44 additions & 20 deletions libpod/container_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package libpod

import (
"context"
"io"
"io/ioutil"
"net/http"
"os"
Expand Down Expand Up @@ -350,10 +351,6 @@ func (c *Container) Mount() (string, error) {
}
}

if c.state.State == define.ContainerStateRemoving {
return "", errors.Wrapf(define.ErrCtrStateInvalid, "cannot mount container %s as it is being removed", c.ID())
}

defer c.newContainerEvent(events.Mount)
return c.mount()
}
Expand All @@ -368,7 +365,6 @@ func (c *Container) Unmount(force bool) error {
return err
}
}

if c.state.Mounted {
mounted, err := c.runtime.storageService.MountedContainerImage(c.ID())
if err != nil {
Expand Down Expand Up @@ -848,31 +844,59 @@ func (c *Container) ShouldRestart(ctx context.Context) bool {
return c.shouldRestart()
}

// ResolvePath resolves the specified path on the root for the container. The
// root must either be the mounted image of the container or the already
// mounted container storage.
//
// It returns the resolved root and the resolved path. Note that the path may
// resolve to the container's mount point or to a volume or bind mount.
func (c *Container) ResolvePath(ctx context.Context, root string, path string) (string, string, error) {
logrus.Debugf("Resolving path %q (root %q) on container %s", path, root, c.ID())
// CopyFromArchive copies the contents from the specified tarStream to path
// *inside* the container.
func (c *Container) CopyFromArchive(ctx context.Context, containerPath string, tarStream io.Reader) (func() error, error) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

// Minimal sanity checks.
if len(root)*len(path) == 0 {
return "", "", errors.Wrapf(define.ErrInternal, "ResolvePath: root (%q) and path (%q) must be non empty", root, path)
if err := c.syncContainer(); err != nil {
return nil, err
}
}
if _, err := os.Stat(root); err != nil {
return "", "", errors.Wrapf(err, "cannot locate root to resolve path on container %s", c.ID())

return c.copyFromArchive(ctx, containerPath, tarStream)
}

// CopyToArchive copies the contents from the specified path *inside* the
// container to the tarStream.
func (c *Container) CopyToArchive(ctx context.Context, containerPath string, tarStream io.Writer) (func() error, error) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

if err := c.syncContainer(); err != nil {
return nil, err
}
}

return c.copyToArchive(ctx, containerPath, tarStream)
}

// Stat the specified path *inside* the container and return a file info.
func (c *Container) Stat(ctx context.Context, containerPath string) (*define.FileInfo, error) {
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()

if err := c.syncContainer(); err != nil {
return "", "", err
return nil, err
}
}

var mountPoint string
var err error
if c.state.Mounted {
mountPoint = c.state.Mountpoint
} else {
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
defer c.unmount(false)
}

return c.resolvePath(root, path)
info, _, _, err := c.stat(ctx, mountPoint, containerPath)
return info, err
}
266 changes: 266 additions & 0 deletions libpod/container_copy_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// +build linux

package libpod

import (
"context"
"io"
"os"
"path/filepath"
"runtime"
"strings"

buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/buildah/pkg/chrootuser"
"github.com/containers/buildah/util"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/storage"
"github.com/containers/storage/pkg/idtools"
"github.com/docker/docker/pkg/archive"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)

func (c *Container) copyFromArchive(ctx context.Context, path string, reader io.Reader) (func() error, error) {
var (
mountPoint string
resolvedRoot string
resolvedPath string
unmount func()
err error
)

// Make sure that "/" copies the *contents* of the mount point and not
// the directory.
if path == "/" {
path = "/."
}

// Optimization: only mount if the container is not already.
if c.state.Mounted {
mountPoint = c.state.Mountpoint
unmount = func() {}
} else {
// NOTE: make sure to unmount in error paths.
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
unmount = func() { c.unmount(false) }
}

if c.state.State == define.ContainerStateRunning {
resolvedRoot = "/"
resolvedPath = c.pathAbs(path)
} else {
resolvedRoot, resolvedPath, err = c.resolvePath(mountPoint, path)
if err != nil {
unmount()
return nil, err
}
}

decompressed, err := archive.DecompressStream(reader)
if err != nil {
unmount()
return nil, err
}

idMappings, idPair, err := getIDMappingsAndPair(c, mountPoint)
if err != nil {
decompressed.Close()
unmount()
return nil, err
}

logrus.Debugf("Container copy *to* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID())

return func() error {
defer unmount()
defer decompressed.Close()
putOptions := buildahCopiah.PutOptions{
UIDMap: idMappings.UIDMap,
GIDMap: idMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
}

return c.joinMountAndExec(ctx,
func() error {
return buildahCopiah.Put(resolvedRoot, resolvedPath, putOptions, decompressed)
},
)
}, nil
}

func (c *Container) copyToArchive(ctx context.Context, path string, writer io.Writer) (func() error, error) {
var (
mountPoint string
unmount func()
err error
)

// Optimization: only mount if the container is not already.
if c.state.Mounted {
mountPoint = c.state.Mountpoint
unmount = func() {}
} else {
// NOTE: make sure to unmount in error paths.
mountPoint, err = c.mount()
if err != nil {
return nil, err
}
unmount = func() { c.unmount(false) }
}

statInfo, resolvedRoot, resolvedPath, err := c.stat(ctx, mountPoint, path)
if err != nil {
unmount()
return nil, err
}

idMappings, idPair, err := getIDMappingsAndPair(c, mountPoint)
if err != nil {
unmount()
return nil, err
}

logrus.Debugf("Container copy *from* %q (resolved: %q) on container %q (ID: %s)", path, resolvedPath, c.Name(), c.ID())

return func() error {
defer unmount()
getOptions := buildahCopiah.GetOptions{
// Unless the specified points to ".", we want to copy the base directory.
KeepDirectoryNames: statInfo.IsDir && filepath.Base(path) != ".",
UIDMap: idMappings.UIDMap,
GIDMap: idMappings.GIDMap,
ChownDirs: idPair,
ChownFiles: idPair,
Excludes: []string{"dev", "proc", "sys"},
}
return c.joinMountAndExec(ctx,
func() error {
return buildahCopiah.Get(resolvedRoot, "", getOptions, []string{resolvedPath}, writer)
},
)
}, nil
}

// getIDMappingsAndPair returns the ID mappings for the container and the host
// ID pair.
func getIDMappingsAndPair(container *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 *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
}

// joinMountAndExec executes the specified function `f` inside the container's
// mount and PID namespace. That allows for having the exact view on the
// container's file system.
//
// Note, if the container is not running `f()` will be executed as is.
func (c *Container) joinMountAndExec(ctx context.Context, f func() error) error {
if c.state.State != define.ContainerStateRunning {
return f()
}

// Container's running, so we need to execute `f()` inside its mount NS.
errChan := make(chan error)
go func() {
runtime.LockOSThread()

// Join the mount and PID NS of the container.
getFD := func(ns LinuxNS) (*os.File, error) {
nsPath, err := c.namespacePath(ns)
if err != nil {
return nil, err
}
return os.Open(nsPath)
}

mountFD, err := getFD(MountNS)
if err != nil {
errChan <- err
return
}
defer mountFD.Close()

pidFD, err := getFD(PIDNS)
if err != nil {
errChan <- err
return
}
defer pidFD.Close()
if err := unix.Unshare(unix.CLONE_NEWNS); err != nil {
errChan <- err
return
}
if err := unix.Setns(int(pidFD.Fd()), unix.CLONE_NEWPID); err != nil {
errChan <- err
return
}

if err := unix.Setns(int(mountFD.Fd()), unix.CLONE_NEWNS); err != nil {
errChan <- err
return
}

// Last but not least, execute the workload.
errChan <- f()
}()
return <-errChan
}
Loading

0 comments on commit 1671dac

Please sign in to comment.