From d3adafd8537da1fdd5c6169a3c5df22e0ea25e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Wed, 6 May 2026 11:28:10 +0200 Subject: [PATCH] volume: Fix file subpath mount over existing image file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a volume subpath pointing to a single file was mounted onto a target path that already existed as a file in the container image, the container creation failed with "open /var/lib/docker/tmp/safe-mountXXX: not a directory". Skip the volume population step when the volume mount path is a file, since directory content copying is not applicable to single-file mounts. Signed-off-by: Paweł Gronowski --- daemon/create_unix.go | 10 +++++++++- integration/volume/mount_test.go | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/daemon/create_unix.go b/daemon/create_unix.go index 0a51f12960..5dba30f458 100644 --- a/daemon/create_unix.go +++ b/daemon/create_unix.go @@ -103,12 +103,20 @@ func (daemon *Daemon) populateVolume(ctx context.Context, c *container.Container return err } - if _, err := os.Stat(ctrDestPath); err != nil { + stat, err := os.Stat(ctrDestPath) + if err != nil { if os.IsNotExist(err) { return nil } return err } + + // Volume population copies directory contents from the container image + // into the volume. This only applies when the source is a directory. + if !stat.IsDir() { + return nil + } + uid, gid := daemon.idMapping.RootPair() volumePath, cleanup, err := mnt.Setup(ctx, c.MountLabel, idtools.Identity{UID: uid, GID: gid}, nil) if err != nil { diff --git a/integration/volume/mount_test.go b/integration/volume/mount_test.go index ccd840265e..e9db5e6b5f 100644 --- a/integration/volume/mount_test.go +++ b/integration/volume/mount_test.go @@ -46,6 +46,7 @@ func TestRunMountVolumeSubdir(t *testing.T) { {name: "subdir link", opts: mount.VolumeOptions{Subpath: "hack/good"}, cmd: []string{"ls", "/volume"}, expected: "hello.txt"}, {name: "subdir with copy data", opts: mount.VolumeOptions{Subpath: "bin"}, volumeTarget: "/bin", cmd: []string{"ls", "/bin/busybox"}, expected: "/bin/busybox", skipPlatform: "windows:copy not supported on Windows"}, {name: "file", opts: mount.VolumeOptions{Subpath: "bar.txt"}, cmd: []string{"cat", "/volume"}, expected: "foo", skipPlatform: "windows:file bind mounts not supported on Windows"}, + {name: "file over existing target", opts: mount.VolumeOptions{Subpath: "file"}, volumeTarget: "/etc/localtime", cmd: []string{"cat", "/etc/localtime"}, expected: "from-volume", skipPlatform: "windows:file bind mounts not supported on Windows"}, {name: "relative with backtracks", opts: mount.VolumeOptions{Subpath: "../../../../../../etc/passwd"}, cmd: []string{"cat", "/volume"}, createErr: "subpath must be a relative path within the volume"}, {name: "not existing", opts: mount.VolumeOptions{Subpath: "not-existing-path"}, cmd: []string{"cat", "/volume"}, startErr: (&safepath.ErrNotAccessible{}).Error()}, @@ -131,6 +132,7 @@ func TestRunMountImage(t *testing.T) { name string opts mount.ImageOptions cmd []string + target string createErr string startErr string expected string @@ -143,6 +145,7 @@ func TestRunMountImage(t *testing.T) { {name: "subdir link", opts: mount.ImageOptions{Subpath: "hack/good"}, cmd: []string{"ls", "/image"}, expected: "hello"}, {name: "subdir link outside context", opts: mount.ImageOptions{Subpath: "hack/bad"}, cmd: []string{"ls", "/image"}, startErr: (&safepath.ErrEscapesBase{}).Error()}, {name: "file", opts: mount.ImageOptions{Subpath: "subdir/hello"}, cmd: []string{"cat", "/image"}, expected: "world"}, + {name: "file_over_existing_target", opts: mount.ImageOptions{Subpath: "subdir/hello"}, cmd: []string{"cat", "/etc/localtime"}, target: "/etc/localtime", expected: "world"}, {name: "relative with backtracks", opts: mount.ImageOptions{Subpath: "../../../../../../etc/passwd"}, cmd: []string{"cat", "/image"}, createErr: "subpath must be a relative path within the volume"}, {name: "not existing", opts: mount.ImageOptions{Subpath: "not-existing-path"}, cmd: []string{"cat", "/image"}, startErr: (&safepath.ErrNotAccessible{}).Error()}, @@ -163,12 +166,17 @@ func TestRunMountImage(t *testing.T) { Cmd: tc.cmd, } + target := tc.target + if target == "" { + target = "/image" + } + hostCfg := containertypes.HostConfig{ Mounts: []mount.Mount{ { Type: mount.TypeImage, Source: testImage, - Target: "/image", + Target: target, ImageOptions: &tc.opts, }, }, @@ -239,6 +247,7 @@ func TestRunMountImage(t *testing.T) { // . // |-- bar.txt (file with "foo") // |-- bin (directory) +// |-- file (file with "from-volume") // |-- subdir (directory) // | |-- hello.txt (file with "world") // |-- hack (directory) @@ -267,6 +276,7 @@ func setupTestVolume(t *testing.T, apiClient client.APIClient) string { } initCmd := "echo foo > /volume/bar.txt && " + + "echo from-volume > /volume/file && " + "mkdir /volume/bin && " + "mkdir /volume/subdir && " + "echo world > /volume/subdir/hello.txt && " +