diff --git a/docs/source/markdown/options/rootfs.md b/docs/source/markdown/options/rootfs.md index 1a773de8b1..6e4fba3388 100644 --- a/docs/source/markdown/options/rootfs.md +++ b/docs/source/markdown/options/rootfs.md @@ -21,3 +21,12 @@ finishes executing, similar to a tmpfs mount point being unmounted. Note: On **SELinux** systems, the rootfs needs the correct label, which is by default **unconfined_u:object_r:container_file_t:s0**. + + The `idmap` option if specified, creates an idmapped mount to the target user +namespace in the container. +The idmap option supports a custom mapping that can be different than the user +namespace used by the container. The mapping can be specified after the idmap +option like: `idmap=uids=0-1-10#10-11-10;gids=0-100-10`. For each triplet, the +first value is the start of the backing file system IDs that are mapped to the +second value on the host. The length of this mapping is given in the third value. +Multiple ranges are separated with #. diff --git a/libpod/container_config.go b/libpod/container_config.go index 3720e110bf..84e1d3dcf3 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -116,6 +116,8 @@ type ContainerRootFSConfig struct { Rootfs string `json:"rootfs,omitempty"` // RootfsOverlay tells if rootfs has to be mounted as an overlay RootfsOverlay bool `json:"rootfs_overlay,omitempty"` + // RootfsMapping specifies if there are mappings to apply to the rootfs. + RootfsMapping *string `json:"rootfs_mapping,omitempty"` // ShmDir is the path to be mounted on /dev/shm in container. // If not set manually at creation time, Libpod will create a tmpfs // with the size specified in ShmSize and populate this with the path of diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 5dab3122e3..607319c753 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -35,6 +35,7 @@ import ( "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idmap" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/mount" @@ -370,9 +371,6 @@ func (c *Container) syncContainer() error { } func (c *Container) setupStorageMapping(dest, from *storage.IDMappingOptions) { - if c.config.Rootfs != "" { - return - } *dest = *from // If we are creating a container inside a pod, we always want to inherit the // userns settings from the infra container. So clear the auto userns settings @@ -1525,6 +1523,31 @@ func (c *Container) mountStorage() (_ string, deferredErr error) { // We need to mount the container before volumes - to ensure the copyup // works properly. mountPoint := c.config.Rootfs + + if c.config.RootfsMapping != nil { + uidMappings, gidMappings, err := parseIDMapMountOption(c.config.IDMappings, *c.config.RootfsMapping, false) + if err != nil { + return "", err + } + + pid, cleanupFunc, err := idmap.CreateUsernsProcess(util.RuntimeSpecToIDtools(uidMappings), util.RuntimeSpecToIDtools(gidMappings)) + if err != nil { + return "", err + } + defer cleanupFunc() + + if err := idmap.CreateIDMappedMount(c.config.Rootfs, c.config.Rootfs, pid); err != nil { + return "", fmt.Errorf("failed to create idmapped mount: %w", err) + } + defer func() { + if deferredErr != nil { + if err := unix.Unmount(c.config.Rootfs, 0); err != nil { + logrus.Errorf("Unmounting idmapped rootfs for container %s after mount error: %v", c.ID(), err) + } + } + }() + } + // Check if overlay has to be created on top of Rootfs if c.config.RootfsOverlay { overlayDest := c.runtime.GraphRoot() @@ -1795,6 +1818,11 @@ func (c *Container) cleanupStorage() error { cleanupErr = err } } + if c.config.RootfsMapping != nil { + if err := unix.Unmount(c.config.Rootfs, 0); err != nil { + logrus.Errorf("Unmounting idmapped rootfs for container %s after mount error: %v", c.ID(), err) + } + } for _, containerMount := range c.config.Mounts { if err := c.unmountSHM(containerMount); err != nil { diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index f603c51dcf..a5d1fc047a 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -74,7 +74,7 @@ func parseOptionIDs(option string) ([]idtools.IDMap, error) { return ret, nil } -func parseIDMapMountOption(idMappings stypes.IDMappingOptions, option string) ([]spec.LinuxIDMapping, []spec.LinuxIDMapping, error) { +func parseIDMapMountOption(idMappings stypes.IDMappingOptions, option string, invert bool) ([]spec.LinuxIDMapping, []spec.LinuxIDMapping, error) { uidMap := idMappings.UIDMap gidMap := idMappings.GIDMap if strings.HasPrefix(option, "idmap=") { @@ -101,17 +101,33 @@ func parseIDMapMountOption(idMappings stypes.IDMappingOptions, option string) ([ uidMappings := make([]spec.LinuxIDMapping, len(uidMap)) gidMappings := make([]spec.LinuxIDMapping, len(gidMap)) for i, uidmap := range uidMap { - uidMappings[i] = spec.LinuxIDMapping{ - HostID: uint32(uidmap.ContainerID), - ContainerID: uint32(uidmap.HostID), - Size: uint32(uidmap.Size), + if invert { + uidMappings[i] = spec.LinuxIDMapping{ + HostID: uint32(uidmap.ContainerID), + ContainerID: uint32(uidmap.HostID), + Size: uint32(uidmap.Size), + } + } else { + uidMappings[i] = spec.LinuxIDMapping{ + HostID: uint32(uidmap.HostID), + ContainerID: uint32(uidmap.ContainerID), + Size: uint32(uidmap.Size), + } } } for i, gidmap := range gidMap { - gidMappings[i] = spec.LinuxIDMapping{ - HostID: uint32(gidmap.ContainerID), - ContainerID: uint32(gidmap.HostID), - Size: uint32(gidmap.Size), + if invert { + gidMappings[i] = spec.LinuxIDMapping{ + HostID: uint32(gidmap.ContainerID), + ContainerID: uint32(gidmap.HostID), + Size: uint32(gidmap.Size), + } + } else { + gidMappings[i] = spec.LinuxIDMapping{ + HostID: uint32(gidmap.HostID), + ContainerID: uint32(gidmap.ContainerID), + Size: uint32(gidmap.Size), + } } } return uidMappings, gidMappings, nil @@ -288,7 +304,7 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { for _, o := range m.Options { if o == "idmap" || strings.HasPrefix(o, "idmap=") { var err error - m.UIDMappings, m.GIDMappings, err = parseIDMapMountOption(c.config.IDMappings, o) + m.UIDMappings, m.GIDMappings, err = parseIDMapMountOption(c.config.IDMappings, o, true) if err != nil { return nil, err } diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go index 167ffabe64..50103d944c 100644 --- a/libpod/container_internal_test.go +++ b/libpod/container_internal_test.go @@ -65,7 +65,7 @@ func TestParseIDMapMountOption(t *testing.T) { UIDMap: uidMap, GIDMap: gidMap, } - uids, gids, err := parseIDMapMountOption(options, "idmap") + uids, gids, err := parseIDMapMountOption(options, "idmap", true) assert.Nil(t, err) assert.Equal(t, len(uids), 1) assert.Equal(t, len(gids), 1) @@ -78,7 +78,7 @@ func TestParseIDMapMountOption(t *testing.T) { assert.Equal(t, gids[0].HostID, uint32(0)) assert.Equal(t, gids[0].Size, uint32(10000)) - uids, gids, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10") + uids, gids, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10", true) assert.Nil(t, err) assert.Equal(t, len(uids), 2) assert.Equal(t, len(gids), 1) @@ -95,19 +95,19 @@ func TestParseIDMapMountOption(t *testing.T) { assert.Equal(t, gids[0].HostID, uint32(0)) assert.Equal(t, gids[0].Size, uint32(10)) - _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10;foobar=bar") + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10;foobar=bar", true) assert.NotNil(t, err) - _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12") + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12", true) assert.NotNil(t, err) - _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12--12") + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12--12", true) assert.NotNil(t, err) - _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#-1-12-12") + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#-1-12-12", true) assert.NotNil(t, err) - _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0--12-0") + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0--12-0", true) assert.NotNil(t, err) } diff --git a/libpod/options.go b/libpod/options.go index d9a6f99765..3ce81ed6d3 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1334,7 +1334,7 @@ func WithCommand(command []string) CtrCreateOption { // WithRootFS sets the rootfs for the container. // This creates a container from a directory on disk and not an image. -func WithRootFS(rootfs string, overlay bool) CtrCreateOption { +func WithRootFS(rootfs string, overlay bool, mapping *string) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized @@ -1344,6 +1344,7 @@ func WithRootFS(rootfs string, overlay bool) CtrCreateOption { } ctr.config.Rootfs = rootfs ctr.config.RootfsOverlay = overlay + ctr.config.RootfsMapping = mapping return nil } } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 635a0820c8..2e78267ef8 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -118,7 +118,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener } if s.Rootfs != "" { - options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay)) + options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay, s.RootfsMapping)) } newImage, resolvedImageName, imageData, err := getImageFromSpec(ctx, rt, s) @@ -513,7 +513,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l options = append(options, libpod.WithShmSize(*s.ShmSize)) } if s.Rootfs != "" { - options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay)) + options = append(options, libpod.WithRootFS(s.Rootfs, s.RootfsOverlay, s.RootfsMapping)) } // Default used if not overridden on command line diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 2e7078115e..8905000248 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -238,6 +238,8 @@ type ContainerStorageConfig struct { Rootfs string `json:"rootfs,omitempty"` // RootfsOverlay tells if rootfs is actually an overlay on top of base path RootfsOverlay bool `json:"rootfs_overlay,omitempty"` + // RootfsMapping specifies if there are mappings to apply to the rootfs. + RootfsMapping *string `json:"rootfs_mapping,omitempty"` // ImageVolumeMode indicates how image volumes will be created. // Supported modes are "ignore" (do not create), "tmpfs" (create as // tmpfs), and "anonymous" (create as anonymous volumes). @@ -600,9 +602,15 @@ func NewSpecGenerator(arg string, rootfs bool) *SpecGenerator { csc.Rootfs = arg // check if rootfs should use overlay lastColonIndex := strings.LastIndex(csc.Rootfs, ":") - if lastColonIndex != -1 && lastColonIndex+1 < len(csc.Rootfs) && csc.Rootfs[lastColonIndex+1:] == "O" { - csc.RootfsOverlay = true - csc.Rootfs = csc.Rootfs[:lastColonIndex] + if lastColonIndex != -1 { + lastPart := csc.Rootfs[lastColonIndex+1:] + if lastPart == "O" { + csc.RootfsOverlay = true + csc.Rootfs = csc.Rootfs[:lastColonIndex] + } else if lastPart == "idmap" || strings.HasPrefix(lastPart, "idmap=") { + csc.RootfsMapping = &lastPart + csc.Rootfs = csc.Rootfs[:lastColonIndex] + } } } else { csc.Image = arg diff --git a/pkg/specgen/specgen_test.go b/pkg/specgen/specgen_test.go index b838d9d30b..b11bd30748 100644 --- a/pkg/specgen/specgen_test.go +++ b/pkg/specgen/specgen_test.go @@ -7,19 +7,31 @@ import ( ) func TestNewSpecGeneratorWithRootfs(t *testing.T) { + idmap := "idmap" + idmapMappings := "idmap=uids=1-1-2000" tests := []struct { rootfs string expectedRootfsOverlay bool expectedRootfs string + expectedMapping *string }{ - {"/root/a:b:O", true, "/root/a:b"}, - {"/root/a:b/c:O", true, "/root/a:b/c"}, - {"/root/a:b/c:", false, "/root/a:b/c:"}, - {"/root/a/b", false, "/root/a/b"}, + {"/root/a:b:O", true, "/root/a:b", nil}, + {"/root/a:b/c:O", true, "/root/a:b/c", nil}, + {"/root/a:b/c:", false, "/root/a:b/c:", nil}, + {"/root/a/b", false, "/root/a/b", nil}, + {"/root/a:b/c:idmap", false, "/root/a:b/c", &idmap}, + {"/root/a:b/c:idmap=uids=1-1-2000", false, "/root/a:b/c", &idmapMappings}, } for _, args := range tests { val := NewSpecGenerator(args.rootfs, true) + assert.Equal(t, val.RootfsOverlay, args.expectedRootfsOverlay) assert.Equal(t, val.Rootfs, args.expectedRootfs) + if args.expectedMapping == nil { + assert.Nil(t, val.RootfsMapping) + } else { + assert.NotNil(t, val.RootfsMapping) + assert.Equal(t, *val.RootfsMapping, *args.expectedMapping) + } } } diff --git a/test/system/030-run.bats b/test/system/030-run.bats index e6ad64bb0a..91e6a37276 100644 --- a/test/system/030-run.bats +++ b/test/system/030-run.bats @@ -1021,4 +1021,38 @@ EOF run_podman run --net=host --cgroupns=host --rm $IMAGE sh -c "grep ' / /sys/fs/cgroup ' /proc/self/mountinfo | tail -n 1 | grep '/sys/fs/cgroup ro'" } +@test "podman run - rootfs with idmapped mounts" { + skip_if_rootless "idmapped mounts work only with root for now" + + skip_if_remote "userns=auto is set on the server" + + egrep -q "^containers:" /etc/subuid || skip "no IDs allocated for user 'containers'" + + # check if the underlying file system supports idmapped mounts + check_dir=$PODMAN_TMPDIR/idmap-check + mkdir $check_dir + run_podman '?' run --rm --uidmap=0:1000:10000 --rootfs $check_dir:idmap true + if [[ "$output" == *"failed to create idmapped mount: invalid argument"* ]]; then + skip "idmapped mounts not supported" + fi + + run_podman image mount $IMAGE + src="$output" + + # we cannot use idmap on top of overlay, so we need a copy + romount=$PODMAN_TMPDIR/rootfs + cp -ar "$src" "$romount" + + run_podman image unmount $IMAGE + + run_podman run --rm --uidmap=0:1000:10000 --rootfs $romount:idmap stat -c %u:%g /bin + is "$output" "0:0" + + run_podman run --uidmap=0:1000:10000 --rm --rootfs "$romount:idmap=uids=0-1001-10000;gids=0-1002-10000" stat -c %u:%g /bin + is "$output" "1:2" + + rm -rf $romount +} + + # vim: filetype=sh