workdir presence checks

A container's workdir can be specified via the CLI via `--workdir` and
via an image config with the CLI having precedence.

Since images have a tendency to specify workdirs without necessarily
shipping the paths with the root FS, make sure that Podman creates the
workdir.  When specified via the CLI, do not create the path, but check
for its existence and return a human-friendly error.

NOTE: `crun` is performing a similar check that would yield exit code
127.  With this change, however, Podman performs the check and yields
exit code 126.  Since this is specific to `crun`, I do not consider it
to be a breaking change of Podman.

Fixes: #9040
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2021-01-21 15:41:14 +01:00
parent 7b186dcb9e
commit 0f668aa085
7 changed files with 127 additions and 40 deletions

View File

@ -174,25 +174,60 @@ func (c *Container) prepare() error {
return err return err
} }
// Ensure container entrypoint is created (if required) // Make sure the workdir exists
if c.config.CreateWorkingDir { if err := c.resolveWorkDir(); err != nil {
workdir, err := securejoin.SecureJoin(c.state.Mountpoint, c.WorkingDir()) return err
if err != nil { }
return errors.Wrapf(err, "error creating path to container %s working dir", c.ID())
}
rootUID := c.RootUID()
rootGID := c.RootGID()
if err := os.MkdirAll(workdir, 0755); err != nil { return nil
if os.IsExist(err) { }
return nil
// resolveWorkDir resolves the container's workdir and, depending on the
// configuration, will create it, or error out if it does not exist.
// Note that the container must be mounted before.
func (c *Container) resolveWorkDir() error {
workdir := c.WorkingDir()
// If the specified workdir is a subdir of a volume or mount,
// we don't need to do anything. The runtime is taking care of
// that.
if isPathOnVolume(c, workdir) || isPathOnBindMount(c, workdir) {
logrus.Debugf("Workdir %q resolved to a volume or mount", workdir)
return nil
}
_, resolvedWorkdir, err := c.resolvePath(c.state.Mountpoint, workdir)
if err != nil {
return err
}
logrus.Debugf("Workdir %q resolved to host path %q", workdir, resolvedWorkdir)
// No need to create it (e.g., `--workdir=/foo`), so let's make sure
// the path exists on the container.
if !c.config.CreateWorkingDir {
if _, err := os.Stat(resolvedWorkdir); err != nil {
if os.IsNotExist(err) {
return errors.Errorf("workdir %q does not exist on container %s", workdir, c.ID())
} }
return errors.Wrapf(err, "error creating container %s working dir", c.ID()) // This might be a serious error (e.g., permission), so
// we need to return the full error.
return errors.Wrapf(err, "error detecting workdir %q on container %s", workdir, c.ID())
} }
}
if err := os.Chown(workdir, rootUID, rootGID); err != nil { // Ensure container entrypoint is created (if required).
return errors.Wrapf(err, "error chowning container %s working directory to container root", c.ID()) rootUID := c.RootUID()
rootGID := c.RootGID()
if err := os.MkdirAll(resolvedWorkdir, 0755); err != nil {
if os.IsExist(err) {
return nil
} }
return errors.Wrapf(err, "error creating container %s workdir", c.ID())
}
if err := os.Chown(resolvedWorkdir, rootUID, rootGID); err != nil {
return errors.Wrapf(err, "error chowning container %s workdir to container root", c.ID())
} }
return nil return nil

View File

@ -13,6 +13,11 @@ import (
// resolveContainerPaths resolves the container's mount point and the container // resolveContainerPaths resolves the container's mount point and the container
// path as specified by the user. Both may resolve to paths outside of the // 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. // container's mount point when the container path hits a volume or bind mount.
//
// 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 (container *Container) resolvePath(mountPoint string, containerPath string) (string, string, error) { func (container *Container) resolvePath(mountPoint string, containerPath string) (string, string, error) {
// Let's first make sure we have a path relative to the mount point. // Let's first make sure we have a path relative to the mount point.
pathRelativeToContainerMountPoint := containerPath pathRelativeToContainerMountPoint := containerPath
@ -97,8 +102,8 @@ func (container *Container) resolvePath(mountPoint string, containerPath string)
return mountPoint, resolvedPathOnTheContainerMountPoint, nil return mountPoint, resolvedPathOnTheContainerMountPoint, nil
} }
// findVolume checks if the specified container path matches a volume inside // findVolume checks if the specified containerPath matches the destination
// the container. It returns a matching volume or nil. // path of a Volume. Returns a matching Volume or nil.
func findVolume(c *Container, containerPath string) (*Volume, error) { func findVolume(c *Container, containerPath string) (*Volume, error) {
runtime := c.Runtime() runtime := c.Runtime()
cleanedContainerPath := filepath.Clean(containerPath) cleanedContainerPath := filepath.Clean(containerPath)
@ -110,8 +115,25 @@ func findVolume(c *Container, containerPath string) (*Volume, error) {
return nil, nil return nil, nil
} }
// findBindMount checks if the specified container path matches a bind mount // isPathOnVolume returns true if the specified containerPath is a subdir of any
// inside the container. It returns a matching mount or nil. // Volume's destination.
func isPathOnVolume(c *Container, containerPath string) bool {
cleanedContainerPath := filepath.Clean(containerPath)
for _, vol := range c.Config().NamedVolumes {
if cleanedContainerPath == filepath.Clean(vol.Dest) {
return true
}
for dest := vol.Dest; dest != "/"; dest = filepath.Dir(dest) {
if cleanedContainerPath == dest {
return true
}
}
}
return false
}
// findBindMounts checks if the specified containerPath matches the destination
// path of a Mount. Returns a matching Mount or nil.
func findBindMount(c *Container, containerPath string) *specs.Mount { func findBindMount(c *Container, containerPath string) *specs.Mount {
cleanedPath := filepath.Clean(containerPath) cleanedPath := filepath.Clean(containerPath)
for _, m := range c.Config().Spec.Mounts { for _, m := range c.Config().Spec.Mounts {
@ -125,3 +147,20 @@ func findBindMount(c *Container, containerPath string) *specs.Mount {
} }
return nil return nil
} }
/// isPathOnBindMount returns true if the specified containerPath is a subdir of any
// Mount's destination.
func isPathOnBindMount(c *Container, containerPath string) bool {
cleanedContainerPath := filepath.Clean(containerPath)
for _, m := range c.Config().Spec.Mounts {
if cleanedContainerPath == filepath.Clean(m.Destination) {
return true
}
for dest := m.Destination; dest != "/"; dest = filepath.Dir(dest) {
if cleanedContainerPath == dest {
return true
}
}
}
return false
}

View File

@ -203,20 +203,6 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat
} }
s.Annotations = annotations s.Annotations = annotations
// workdir
if s.WorkDir == "" {
if newImage != nil {
workingDir, err := newImage.WorkingDir(ctx)
if err != nil {
return nil, err
}
s.WorkDir = workingDir
}
}
if s.WorkDir == "" {
s.WorkDir = "/"
}
if len(s.SeccompProfilePath) < 1 { if len(s.SeccompProfilePath) < 1 {
p, err := libpod.DefaultSeccompPath() p, err := libpod.DefaultSeccompPath()
if err != nil { if err != nil {

View File

@ -272,10 +272,18 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
if s.Entrypoint != nil { if s.Entrypoint != nil {
options = append(options, libpod.WithEntrypoint(s.Entrypoint)) options = append(options, libpod.WithEntrypoint(s.Entrypoint))
} }
// If the user did not set an workdir but the image did, ensure it is // If the user did not specify a workdir on the CLI, let's extract it
// created. // from the image.
if s.WorkDir == "" && img != nil { if s.WorkDir == "" && img != nil {
options = append(options, libpod.WithCreateWorkingDir()) options = append(options, libpod.WithCreateWorkingDir())
wd, err := img.WorkingDir(ctx)
if err != nil {
return nil, err
}
s.WorkDir = wd
}
if s.WorkDir == "" {
s.WorkDir = "/"
} }
if s.StopSignal != nil { if s.StopSignal != nil {
options = append(options, libpod.WithStopSignal(*s.StopSignal)) options = append(options, libpod.WithStopSignal(*s.StopSignal))

View File

@ -2,7 +2,6 @@ package integration
import ( import (
"os" "os"
"strings"
. "github.com/containers/podman/v2/test/utils" . "github.com/containers/podman/v2/test/utils"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -41,12 +40,9 @@ var _ = Describe("Podman run", func() {
}) })
It("podman run a container using non existing --workdir", func() { It("podman run a container using non existing --workdir", func() {
if !strings.Contains(podmanTest.OCIRuntime, "crun") {
Skip("Test only works on crun")
}
session := podmanTest.Podman([]string{"run", "--workdir", "/home/foobar", ALPINE, "pwd"}) session := podmanTest.Podman([]string{"run", "--workdir", "/home/foobar", ALPINE, "pwd"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(127)) Expect(session.ExitCode()).To(Equal(126))
}) })
It("podman run a container on an image with a workdir", func() { It("podman run a container on an image with a workdir", func() {

View File

@ -589,4 +589,25 @@ json-file | f
is "${lines[1]}" "$rand" "Container runs successfully despite warning" is "${lines[1]}" "$rand" "Container runs successfully despite warning"
} }
@test "podman run - check workdir" {
# Workdirs specified via the CLI are not created on the root FS.
run_podman 126 run --rm --workdir /i/do/not/exist $IMAGE pwd
# Note: remote error prepends an attach error.
is "$output" "Error: .*workdir \"/i/do/not/exist\" does not exist on container.*"
testdir=$PODMAN_TMPDIR/volume
mkdir -p $testdir
randomcontent=$(random_string 10)
echo "$randomcontent" > $testdir/content
# Workdir does not exist on the image but is volume mounted.
run_podman run --rm --workdir /IamNotOnTheImage -v $testdir:/IamNotOnTheImage $IMAGE cat content
is "$output" "$randomcontent" "cat random content"
# Workdir does not exist on the image but is created by the runtime as it's
# a subdir of a volume.
run_podman run --rm --workdir /IamNotOntheImage -v $testdir/content:/IamNotOntheImage/foo $IMAGE cat foo
is "$output" "$randomcontent" "cat random content"
}
# vim: filetype=sh # vim: filetype=sh

View File

@ -163,10 +163,12 @@ EOF
https_proxy=https-proxy-in-env-file https_proxy=https-proxy-in-env-file
EOF EOF
# NOTE: it's important to not create the workdir.
# Podman will make sure to create a missing workdir
# if needed. See #9040.
cat >$tmpdir/Containerfile <<EOF cat >$tmpdir/Containerfile <<EOF
FROM $IMAGE FROM $IMAGE
LABEL $label_name=$label_value LABEL $label_name=$label_value
RUN mkdir $workdir
WORKDIR $workdir WORKDIR $workdir
# Test for #7094 - chowning of invalid symlinks # Test for #7094 - chowning of invalid symlinks