From ca00c9edccf6945327f1d7726cc6512ba822a28d Mon Sep 17 00:00:00 2001 From: Yanxin Li <462004436@qq.com> Date: Tue, 6 Oct 2026 19:13:09 +0800 Subject: [PATCH] fix: preserve ownership when exporting keep-id containers Rootless containers created with --userns=keep-id store host-mapped IDs in their mounted root filesystem, so podman export showed host IDs in the tar headers instead of the container ownership: files owned by container UID/GID 1000 were exported as 0/0. Pass the container's UID/GID mappings to the archive tar operation so the tar headers are converted back to container IDs, like podman cp already does. Containers without ID mappings pass an empty mapping, and the archive code skips the conversion in that case. Add an e2e regression test that follows the reproducer of the issue. Fixes: #29856 Signed-off-by: Yanxin Li <462004436@qq.com> --- libpod/container_internal.go | 6 +++++- test/e2e/export_test.go | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) 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") + }) })