From 58489b82b41e585fae87f70ddd559511bc280a85 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 31 Aug 2026 10:13:00 +0000 Subject: [PATCH] libpod: fix :O overlay volumes in a userns When the user owning the storage is not mapped into the container user namespace (e.g. root with --userns=auto), the runtime cannot mount an overlay volume from inside the user namespace. Mount the overlay in podman instead and pass the runtime a bind mount. Closes: https://github.com/podman-container-tools/podman/issues/28758 Signed-off-by: Giuseppe Scrivano --- libpod/container_internal_common.go | 64 ++++++++++++++++++++++++----- test/system/170-run-userns.bats | 25 +++++++++++ 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index b447945059..062286ed18 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -298,6 +298,10 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc return nil, nil, err } + // If the storage owner is not mapped into the user namespace, the runtime + // cannot mount overlay volumes from inside it. + forceOverlayMount := !hasCurrentUserMapped(c) + // Add named volumes for _, namedVol := range c.config.NamedVolumes { volume, err := c.runtime.GetVolume(namedVol.Name) @@ -340,12 +344,27 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc return nil, nil, err } + if forceOverlayMount { + if upperDir == "" { + upperDir = filepath.Join(contentDir, "upper") + } + if workDir == "" { + workDir = filepath.Join(contentDir, "work") + } + } + overlayOpts = &overlay.Options{ RootUID: c.RootUID(), RootGID: c.RootGID(), UpperDirOptionFragment: upperDir, WorkDirOptionFragment: workDir, GraphOpts: c.runtime.store.GraphOptions(), + ForceMount: forceOverlayMount, + } + if forceOverlayMount { + // podman mounts the overlay itself, so it must apply the + // container mount label; otherwise the runtime would. + overlayOpts.MountLabel = c.MountLabel() } overlayMount, err = overlay.MountWithOptions(contentDir, mountPoint, namedVol.Dest, overlayOpts) @@ -394,7 +413,7 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc m.Source = safeMount.mountPoint continue } - if o == "idmap" || strings.HasPrefix(o, "idmap=") { + if isIdmapOption(o) { var err error m.UIDMappings, m.GIDMappings, err = parseIDMapMountOption(c.config.IDMappings, o) if err != nil { @@ -474,12 +493,22 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc if err != nil { return nil, nil, err } + if forceOverlayMount && upperDir == "" && workDir == "" { + upperDir = filepath.Join(contentDir, "upper") + workDir = filepath.Join(contentDir, "work") + } overlayOpts := &overlay.Options{ RootUID: c.RootUID(), RootGID: c.RootGID(), UpperDirOptionFragment: upperDir, WorkDirOptionFragment: workDir, GraphOpts: c.runtime.store.GraphOptions(), + ForceMount: forceOverlayMount, + } + if forceOverlayMount { + // podman mounts the overlay itself, so it must apply the + // container mount label; otherwise the runtime would. + overlayOpts.MountLabel = c.MountLabel() } overlayMount, err := overlay.MountWithOptions(contentDir, overlayVol.Source, overlayVol.Dest, overlayOpts) @@ -533,11 +562,23 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc } var overlayMount spec.Mount - if volume.ReadWrite { - overlayMount, err = overlay.Mount(contentDir, imagePath, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) - } else { - overlayMount, err = overlay.MountReadOnly(contentDir, imagePath, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) + overlayOpts := &overlay.Options{ + RootUID: c.RootUID(), + RootGID: c.RootGID(), + GraphOpts: c.runtime.store.GraphOptions(), + ReadOnly: !volume.ReadWrite, + ForceMount: forceOverlayMount, } + if forceOverlayMount { + // podman mounts the overlay itself, so it must apply the + // container mount label; otherwise the runtime would. + overlayOpts.MountLabel = c.MountLabel() + } + if forceOverlayMount && volume.ReadWrite { + overlayOpts.UpperDirOptionFragment = filepath.Join(contentDir, "upper") + overlayOpts.WorkDirOptionFragment = filepath.Join(contentDir, "work") + } + overlayMount, err = overlay.MountWithOptions(contentDir, imagePath, volume.Dest, overlayOpts) if err != nil { return nil, nil, fmt.Errorf("creating overlay mount for image %q failed: %w", volume.Source, err) } @@ -3004,13 +3045,14 @@ func (c *Container) createSecretMountDir(runPath string) error { return err } +// isIdmapOption reports whether a single volume option requests idmapping +// (the "idmap" or "idmap=..." option). +func isIdmapOption(o string) bool { + return o == "idmap" || strings.HasPrefix(o, "idmap=") +} + func hasIdmapOption(options []string) bool { - for _, o := range options { - if o == "idmap" || strings.HasPrefix(o, "idmap=") { - return true - } - } - return false + return slices.ContainsFunc(options, isIdmapOption) } // Fix ownership and permissions of the specified volume if necessary. diff --git a/test/system/170-run-userns.bats b/test/system/170-run-userns.bats index 267358fd14..d8822c021c 100644 --- a/test/system/170-run-userns.bats +++ b/test/system/170-run-userns.bats @@ -204,3 +204,28 @@ EOF run_podman rm -f $cname } + +# CANNOT BE PARALLELIZED: userns=auto, rootless, => not enough unused IDs in user namespace +@test "podman :O overlay volume in a userns that does not map the storage owner" { + # An overlay volume (:O) must be read-write inside a userns that does not + # map the storage owner. See containers/crun#2212. + skip_if_rootless "storage is not owned by an unmapped user when rootless" + grep -E -q "^containers:" /etc/subuid || skip "no IDs allocated for user 'containers'" + + local volname="myvol_$(random_string)" + run_podman volume create $volname + + run_podman run --rm -v $volname:/vol $IMAGE sh -c "echo lower > /vol/lower.txt" + + # The lower content must be readable and writes (to the upper dir) must work. + run_podman run --rm --userns=auto -v $volname:/mnt:O $IMAGE sh -c \ + "cat /mnt/lower.txt; echo upper > /mnt/new.txt && cat /mnt/new.txt" + assert "$output" =~ "lower" "overlay lower is readable under userns" + assert "$output" =~ "upper" "overlay upper is writable under userns" + + # Writes are ephemeral: the named volume itself is untouched. + run_podman run --rm -v $volname:/vol $IMAGE ls /vol + assert "$output" = "lower.txt" "writes to a :O overlay do not leak into the volume" + + run_podman volume rm $volname +}