libpod: support idmap for --rootfs

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>
This commit is contained in:
Giuseppe Scrivano
2023-01-27 16:58:30 +01:00
parent cbb45a6d42
commit 2bb4c7cdde
10 changed files with 140 additions and 30 deletions

View File

@ -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 #.

View File

@ -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

View File

@ -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 {

View File

@ -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,18 +101,34 @@ func parseIDMapMountOption(idMappings stypes.IDMappingOptions, option string) ([
uidMappings := make([]spec.LinuxIDMapping, len(uidMap))
gidMappings := make([]spec.LinuxIDMapping, len(gidMap))
for i, uidmap := range uidMap {
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 {
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
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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" {
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

View File

@ -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)
}
}
}

View File

@ -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