Skip to content

Commit

Permalink
libpod: support idmap for --rootfs
Browse files Browse the repository at this point in the history
add a new option idmap to --rootfs that works in the same way as it
does for volumes.

Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
  • Loading branch information
giuseppe committed Feb 2, 2023
1 parent cbb45a6 commit 2bb4c7c
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 30 deletions.
9 changes: 9 additions & 0 deletions docs/source/markdown/options/rootfs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #.
2 changes: 2 additions & 0 deletions libpod/container_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 31 additions & 3 deletions libpod/container_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 26 additions & 10 deletions libpod/container_internal_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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=") {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 7 additions & 7 deletions libpod/container_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/specgen/generate/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions pkg/specgen/specgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions pkg/specgen/specgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
34 changes: 34 additions & 0 deletions test/system/030-run.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 2bb4c7c

Please sign in to comment.