From de4333ce06cd02aa86ab3aaf76a1275a16941c73 Mon Sep 17 00:00:00 2001 From: Jiwoo Ahn Date: Fri, 21 Aug 2026 13:41:28 +0900 Subject: [PATCH] libpod: fix permissions for imported volumes Imported volumes now get ownership and permissions matching the container's mount tareget. Previously, permission adjustment was skipped for imported volumes as they were already non-empty when mounted. Fixes: #25442 Signed-off-by: Jiwoo Ahn --- libpod/volume.go | 20 +++++++++++++++++++- test/e2e/volume_create_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/libpod/volume.go b/libpod/volume.go index c383b7ec34..9532c428d3 100644 --- a/libpod/volume.go +++ b/libpod/volume.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "maps" + "os" "time" "github.com/sirupsen/logrus" @@ -104,7 +105,7 @@ type VolumeState struct { // a container, the container will chown the volume to the container process // UID/GID. NeedsChown bool `json:"notYetChowned,omitempty"` - // Indicates that a copy-up event occurred during the current mount of + // CopiedUp indicates that a copy-up event occurred during the current mount of // the volume into a container. // We use this to determine if a chown is appropriate. CopiedUp bool `json:"copiedUp,omitempty"` @@ -346,5 +347,22 @@ func (v *Volume) Import(r io.Reader) error { return fmt.Errorf("extracting into volume %s: %w", v.Name(), err) } + contents, err := os.ReadDir(mountPoint) + if err != nil { + return fmt.Errorf("reading contents of imported volume %s: %w", v.Name(), err) + } + if len(contents) != 0 { + v.lock.Lock() + defer v.lock.Unlock() + if err := v.update(); err != nil { + return err + } + if v.state.NeedsCopyUp && v.state.NeedsChown { + v.state.NeedsCopyUp = false + v.state.CopiedUp = true + return v.save() + } + } + return nil } diff --git a/test/e2e/volume_create_test.go b/test/e2e/volume_create_test.go index 981252b3f2..c61eaa53b1 100644 --- a/test/e2e/volume_create_test.go +++ b/test/e2e/volume_create_test.go @@ -120,6 +120,34 @@ var _ = Describe("Podman volume create", func() { Expect(session.OutputToString()).To(ContainSubstring("hello")) }) + It("podman import volume preserves first mount permission adjustment", func() { + imageName := "volume-copyup-permissions:latest" + containerfile := fmt.Sprintf(`FROM %s +RUN mkdir -p /vol-target && chown 70:71 /vol-target && chmod 750 /vol-target && echo hello > /vol-target/test +`, ALPINE) + podmanTest.BuildImage(containerfile, imageName, "false") + + volName := "my_vol_" + RandomString(10) + podmanTest.PodmanExitCleanly("volume", "create", volName) + + session := podmanTest.PodmanExitCleanly("run", "--volume", volName+":/vol-target", imageName, "stat", "-c", "%u:%g %a", "/vol-target") + Expect(session.OutputToString()).To(Equal("70:71 750")) + + helloTar := filepath.Join(podmanTest.TempDir, "hello.tar") + podmanTest.PodmanExitCleanly("volume", "export", volName, "--output", helloTar) + + importedVolName := "my_vol_" + RandomString(10) + podmanTest.PodmanExitCleanly("volume", "create", importedVolName) + + podmanTest.PodmanExitCleanly("volume", "import", importedVolName, helloTar) + + session = podmanTest.PodmanExitCleanly("run", "--volume", importedVolName+":/vol-target", imageName, "stat", "-c", "%u:%g %a", "/vol-target") + Expect(session.OutputToString()).To(Equal("70:71 750")) + + session = podmanTest.PodmanExitCleanly("run", "--volume", importedVolName+":/vol-target", imageName, "cat", "/vol-target/test") + Expect(session.OutputToString()).To(Equal("hello")) + }) + It("podman import/export volume should fail", func() { // try import on volume or source which does not exist SkipIfRemote("Volume export check does not work with a remote client")