diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 25caeb3fd4..c14be24ebd 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -48,6 +48,7 @@ import ( "go.podman.io/podman/v6/pkg/systemd/notifyproxy" "go.podman.io/podman/v6/pkg/util" "go.podman.io/storage" + "go.podman.io/storage/pkg/archive" "go.podman.io/storage/pkg/chrootarchive" "go.podman.io/storage/pkg/directory" "go.podman.io/storage/pkg/fileutils" @@ -791,7 +792,10 @@ func (c *Container) export(out io.Writer) error { }() } - input, err := chrootarchive.Tar(mountPoint, nil, mountPoint) + input, err := chrootarchive.Tar(mountPoint, &archive.TarOptions{ + UIDMaps: c.config.IDMappings.UIDMap, + GIDMaps: c.config.IDMappings.GIDMap, + }, mountPoint) if err != nil { return fmt.Errorf("reading container directory %q: %w", c.ID(), err) } diff --git a/test/e2e/export_test.go b/test/e2e/export_test.go index 92b6a86336..2a6831ec6d 100644 --- a/test/e2e/export_test.go +++ b/test/e2e/export_test.go @@ -3,8 +3,12 @@ package integration import ( + "archive/tar" + "fmt" + "io" "os" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -69,4 +73,41 @@ var _ = Describe("Podman export", func() { Expect(events[0]).To(ContainSubstring("export")) Expect(events[0]).To(ContainSubstring(cid)) }) + + It("podman export preserves ownership with keep-id user namespaces", func() { + SkipIfNotRootless("rootless --userns=keep-id stores host-mapped IDs in the container filesystem") + + // Mirrors the reproducer of #29856: an image with a dedicated + // user (UID/GID 1000), a keep-id container that is created but + // never started, and an export whose tar headers must show + // container ownership (user 1000/1000, root-owned parents). + imageName := "test-export-keepid-user-image" + containerfile := fmt.Sprintf("FROM %s\nRUN adduser -D -u 1000 user && touch /home/user/newfile && chown -R 1000:1000 /home/user\n", ALPINE) + podmanTest.BuildImage(containerfile, imageName, "false", "--network=none") + + create := podmanTest.PodmanExitCleanly("create", "--userns=keep-id", imageName) + cid := strings.TrimSpace(create.OutputToString()) + + outfile := filepath.Join(podmanTest.TempDir, "keep-id-export.tar") + podmanTest.PodmanExitCleanly("export", "-o", outfile, cid) + + file, err := os.Open(outfile) + Expect(err).ToNot(HaveOccurred()) + defer file.Close() + + reader := tar.NewReader(file) + owners := map[string]string{} + for { + header, err := reader.Next() + if err == io.EOF { + break + } + Expect(err).ToNot(HaveOccurred()) + owners[strings.TrimSuffix(header.Name, "/")] = fmt.Sprintf("%d/%d", header.Uid, header.Gid) + } + + Expect(owners["home"]).To(Equal("0/0"), "home/ should be owned by root in the exported tar") + Expect(owners["home/user"]).To(Equal("1000/1000"), "home/user should keep container ownership in the exported tar") + Expect(owners["home/user/newfile"]).To(Equal("1000/1000"), "files under home/user should keep container ownership in the exported tar") + }) })