Skip to content

Commit

Permalink
Merge pull request #24655 from mheon/fix_volume_perms_cp
Browse files Browse the repository at this point in the history
Mount volumes before copying into a container
  • Loading branch information
openshift-merge-bot[bot] authored Jan 8, 2025
2 parents 164a47e + e66b788 commit 0798f54
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 22 deletions.
137 changes: 136 additions & 1 deletion libpod/container_copy_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ package libpod

import (
"errors"
"fmt"
"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/v5/libpod/define"
"github.com/containers/podman/v5/libpod/shutdown"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/stringid"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)
Expand All @@ -25,7 +29,9 @@ func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir boo
resolvedRoot string
resolvedPath string
unmount func()
cleanupFuncs []func()
err error
locked bool = true
)

// Make sure that "/" copies the *contents* of the mount point and not
Expand All @@ -44,19 +50,146 @@ func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir boo
if err != nil {
return nil, err
}
c.state.Mountpoint = mountPoint
if err := c.save(); err != nil {
return nil, err
}

unmount = func() {
if !locked {
c.lock.Lock()
defer c.lock.Unlock()
}

if err := c.syncContainer(); err != nil {
logrus.Errorf("Unable to sync container %s state: %v", c.ID(), err)
return
}

// These have to be first, some of them rely on container rootfs still being mounted.
for _, cleanupFunc := range cleanupFuncs {
cleanupFunc()
}
if err := c.unmount(false); err != nil {
logrus.Errorf("Failed to unmount container: %v", err)
}

if c.ensureState(define.ContainerStateConfigured, define.ContainerStateExited) {
c.state.Mountpoint = ""
if err := c.save(); err != nil {
logrus.Errorf("Writing container %s state: %v", c.ID(), err)
}
}
}

// Before we proceed, mount all named volumes associated with the
// container.
// This solves two issues:
// Firstly, it ensures that if the volume actually requires a mount, we
// will mount it for safe use.
// (For example, image volumes, volume plugins).
// Secondly, it copies up into the volume if necessary.
// This ensures that permissions are correct for copies into volumes on
// containers that have never started.
if len(c.config.NamedVolumes) > 0 {
for _, v := range c.config.NamedVolumes {
vol, err := c.mountNamedVolume(v, mountPoint)
if err != nil {
unmount()
return nil, err
}

volUnmountName := fmt.Sprintf("volume unmount %s %s", vol.Name(), stringid.GenerateNonCryptoID()[0:12])

// The unmount function can be called in two places:
// First, from unmount(), our generic cleanup function that gets
// called on success or on failure by error.
// Second, from the shutdown handler on receipt of a SIGTERM
// or similar.
volUnmountFunc := func() error {
vol.lock.Lock()
defer vol.lock.Unlock()

if err := vol.unmount(false); err != nil {
return err
}

return nil
}

cleanupFuncs = append(cleanupFuncs, func() {
_ = shutdown.Unregister(volUnmountName)

if err := volUnmountFunc(); err != nil {
logrus.Errorf("Unmounting container %s volume %s: %v", c.ID(), vol.Name(), err)
}
})

if err := shutdown.Register(volUnmountName, func(_ os.Signal) error {
return volUnmountFunc()
}); err != nil && !errors.Is(err, shutdown.ErrHandlerExists) {
return nil, fmt.Errorf("adding shutdown handler for volume %s unmount: %w", vol.Name(), err)
}
}
}
}

resolvedRoot, resolvedPath, err = c.resolveCopyTarget(mountPoint, path)
resolvedRoot, resolvedPath, volume, err := c.resolveCopyTarget(mountPoint, path)
if err != nil {
unmount()
return nil, err
}

if volume != nil {
// This must be the first cleanup function so it fires before volume unmounts happen.
cleanupFuncs = append([]func(){func() {
// This is a gross hack to ensure correct permissions
// on a volume that was copied into that needed, but did
// not receive, a copy-up.
// Why do we need this?
// Basically: fixVolumePermissions is needed to ensure
// the volume has the right permissions.
// However, fixVolumePermissions only fires on a volume
// that is not empty iff a copy-up occurred.
// In this case, the volume is not empty as we just
// copied into it, so in order to get
// fixVolumePermissions to actually run, we must
// convince it that a copy-up occurred - even if it did
// not.
// At the same time, clear NeedsCopyUp as we just
// populated the volume and that will block a future
// copy-up.
volume.lock.Lock()

if err := volume.update(); err != nil {
logrus.Errorf("Unable to update volume %s status: %v", volume.Name(), err)
volume.lock.Unlock()
return
}

if volume.state.NeedsCopyUp && volume.state.NeedsChown {
volume.state.NeedsCopyUp = false
volume.state.CopiedUp = true
if err := volume.save(); err != nil {
logrus.Errorf("Unable to save volume %s state: %v", volume.Name(), err)
volume.lock.Unlock()
return
}

volume.lock.Unlock()

for _, namedVol := range c.config.NamedVolumes {
if namedVol.Name == volume.Name() {
if err := c.fixVolumePermissions(namedVol); err != nil {
logrus.Errorf("Unable to fix volume %s permissions: %v", volume.Name(), err)
}
return
}
}
}
}}, cleanupFuncs...)
}

var idPair *idtools.IDPair
if chown {
// Make sure we chown the files to the container's main user and group ID.
Expand All @@ -74,6 +207,8 @@ func (c *Container) copyFromArchive(path string, chown, noOverwriteDirNonDir boo
return nil, err
}

locked = false

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

return func() error {
Expand Down
2 changes: 1 addition & 1 deletion libpod/container_copy_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ func (c *Container) joinMountAndExec(f func() error) error {

// Similarly, we can just use resolvePath for both running and stopped
// containers.
func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) {
func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, *Volume, error) {
return c.resolvePath(mountPoint, containerPath)
}
4 changes: 2 additions & 2 deletions libpod/container_copy_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ func (c *Container) joinMountAndExec(f func() error) error {
return <-errChan
}

func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, error) {
func (c *Container) resolveCopyTarget(mountPoint string, containerPath string) (string, string, *Volume, error) {
// If the container is running, we will execute the copy
// inside the container's mount namespace so we return a path
// relative to the container's root.
if c.state.State == define.ContainerStateRunning {
return "/", c.pathAbs(containerPath), nil
return "/", c.pathAbs(containerPath), nil, nil
}
return c.resolvePath(mountPoint, containerPath)
}
10 changes: 7 additions & 3 deletions libpod/container_internal_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ func (c *Container) isWorkDirSymlink(resolvedPath string) bool {
break
}
if resolvedSymlink != "" {
_, resolvedSymlinkWorkdir, err := c.resolvePath(c.state.Mountpoint, resolvedSymlink)
_, resolvedSymlinkWorkdir, _, err := c.resolvePath(c.state.Mountpoint, resolvedSymlink)
if isPathOnVolume(c, resolvedSymlinkWorkdir) || isPathOnMount(c, resolvedSymlinkWorkdir) {
// Resolved symlink exists on external volume or mount
return true
Expand Down Expand Up @@ -805,7 +805,7 @@ func (c *Container) resolveWorkDir() error {
return nil
}

_, resolvedWorkdir, err := c.resolvePath(c.state.Mountpoint, workdir)
_, resolvedWorkdir, _, err := c.resolvePath(c.state.Mountpoint, workdir)
if err != nil {
return err
}
Expand Down Expand Up @@ -2988,7 +2988,11 @@ func (c *Container) fixVolumePermissions(v *ContainerNamedVolume) error {
return nil
}

st, err := os.Lstat(filepath.Join(c.state.Mountpoint, v.Dest))
finalPath, err := securejoin.SecureJoin(c.state.Mountpoint, v.Dest)
if err != nil {
return err
}
st, err := os.Lstat(finalPath)
if err == nil {
if stat, ok := st.Sys().(*syscall.Stat_t); ok {
uid, gid := int(stat.Uid), int(stat.Gid)
Expand Down
24 changes: 10 additions & 14 deletions libpod/container_path_resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func (c *Container) pathAbs(path string) string {
// It returns a bool, indicating whether containerPath resolves outside of
// mountPoint (e.g., via a mount or volume), the resolved root (e.g., container
// mount, bind mount or volume) and the resolved path on the root (absolute to
// the host).
func (c *Container) resolvePath(mountPoint string, containerPath string) (string, string, error) {
// the host). If the path is on a named volume, the volume is returned.
func (c *Container) resolvePath(mountPoint string, containerPath string) (string, string, *Volume, error) {
// Let's first make sure we have a path relative to the mount point.
pathRelativeToContainerMountPoint := c.pathAbs(containerPath)
resolvedPathOnTheContainerMountPoint := filepath.Join(mountPoint, pathRelativeToContainerMountPoint)
Expand All @@ -54,21 +54,17 @@ func (c *Container) resolvePath(mountPoint string, containerPath string) (string
for {
volume, err := findVolume(c, searchPath)
if err != nil {
return "", "", err
return "", "", nil, err
}
if volume != nil {
logrus.Debugf("Container path %q resolved to volume %q on path %q", containerPath, volume.Name(), searchPath)

// TODO: We really need to force the volume to mount
// before doing this, but that API is not exposed
// externally right now and doing so is beyond the scope
// of this commit.
mountPoint, err := volume.MountPoint()
if err != nil {
return "", "", err
return "", "", nil, err
}
if mountPoint == "" {
return "", "", fmt.Errorf("volume %s is not mounted, cannot copy into it", volume.Name())
return "", "", nil, fmt.Errorf("volume %s is not mounted, cannot copy into it", volume.Name())
}

// We found a matching volume for searchPath. We now
Expand All @@ -78,9 +74,9 @@ func (c *Container) resolvePath(mountPoint string, containerPath string) (string
pathRelativeToVolume := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheVolumeMount, err := securejoin.SecureJoin(mountPoint, pathRelativeToVolume)
if err != nil {
return "", "", err
return "", "", nil, err
}
return mountPoint, absolutePathOnTheVolumeMount, nil
return mountPoint, absolutePathOnTheVolumeMount, volume, nil
}

if mount := findBindMount(c, searchPath); mount != nil {
Expand All @@ -92,9 +88,9 @@ func (c *Container) resolvePath(mountPoint string, containerPath string) (string
pathRelativeToBindMount := strings.TrimPrefix(pathRelativeToContainerMountPoint, searchPath)
absolutePathOnTheBindMount, err := securejoin.SecureJoin(mount.Source, pathRelativeToBindMount)
if err != nil {
return "", "", err
return "", "", nil, err
}
return mount.Source, absolutePathOnTheBindMount, nil
return mount.Source, absolutePathOnTheBindMount, nil, nil
}

if searchPath == "/" {
Expand All @@ -106,7 +102,7 @@ func (c *Container) resolvePath(mountPoint string, containerPath string) (string
}

// No volume, no bind mount but just a normal path on the container.
return mountPoint, resolvedPathOnTheContainerMountPoint, nil
return mountPoint, resolvedPathOnTheContainerMountPoint, nil, nil
}

// findVolume checks if the specified containerPath matches the destination
Expand Down
2 changes: 1 addition & 1 deletion libpod/container_stat_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
func (c *Container) statOnHost(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)
resolvedRoot, resolvedPath, _, err := c.resolvePath(mountPoint, containerPath)
if err != nil {
return nil, "", "", err
}
Expand Down
26 changes: 26 additions & 0 deletions libpod/shutdown/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,29 @@ func Register(name string, handler func(os.Signal) error) error {

return nil
}

// Unregister un-registers a given shutdown handler.
func Unregister(name string) error {
handlerLock.Lock()
defer handlerLock.Unlock()

if handlers == nil {
return nil
}

if _, ok := handlers[name]; !ok {
return nil
}

delete(handlers, name)

newOrder := []string{}
for _, checkName := range handlerOrder {
if checkName != name {
newOrder = append(newOrder, checkName)
}
}
handlerOrder = newOrder

return nil
}
37 changes: 37 additions & 0 deletions test/e2e/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package integration

import (
"fmt"
"os"
"os/exec"
"os/user"
Expand Down Expand Up @@ -261,4 +262,40 @@ var _ = Describe("Podman cp", func() {
Expect(lsOutput).To(ContainSubstring("bin"))
Expect(lsOutput).To(ContainSubstring("usr"))
})

It("podman cp to volume in container that is not running", func() {
volName := "testvol"
ctrName := "testctr"
imgName := "testimg"
ctrVolPath := "/test/"

dockerfile := fmt.Sprintf(`FROM %s
RUN mkdir %s
RUN chown 9999:9999 %s`, ALPINE, ctrVolPath, ctrVolPath)

podmanTest.BuildImage(dockerfile, imgName, "false")

srcFile, err := os.CreateTemp("", "")
Expect(err).ToNot(HaveOccurred())
defer srcFile.Close()
defer os.Remove(srcFile.Name())

volCreate := podmanTest.Podman([]string{"volume", "create", volName})
volCreate.WaitWithDefaultTimeout()
Expect(volCreate).Should(ExitCleanly())

ctrCreate := podmanTest.Podman([]string{"create", "--name", ctrName, "-v", fmt.Sprintf("%s:%s", volName, ctrVolPath), imgName, "sh"})
ctrCreate.WaitWithDefaultTimeout()
Expect(ctrCreate).To(ExitCleanly())

cp := podmanTest.Podman([]string{"cp", srcFile.Name(), fmt.Sprintf("%s:%s", ctrName, ctrVolPath)})
cp.WaitWithDefaultTimeout()
Expect(cp).To(ExitCleanly())

ls := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:%s", volName, ctrVolPath), ALPINE, "ls", "-al", ctrVolPath})
ls.WaitWithDefaultTimeout()
Expect(ls).To(ExitCleanly())
Expect(ls.OutputToString()).To(ContainSubstring("9999 9999"))
Expect(ls.OutputToString()).To(ContainSubstring(filepath.Base(srcFile.Name())))
})
})

0 comments on commit 0798f54

Please sign in to comment.