From fdcc2257df0fb0cb72d3fbe1b5aa8625955e1219 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 14 Dec 2022 16:41:58 +0100 Subject: [PATCH] libpod: use OCI idmappings for mounts Now that the OCI runtime specs have support for idmapped mounts, let's use them instead of relying on the custom annotation in crun. Also add the mechanism to specify the mapping to use. Pick the same format used by crun so it won't be a breaking change for users that are already using it. Signed-off-by: Giuseppe Scrivano --- docs/source/markdown/options/mount.md | 5 ++ libpod/container_internal_common.go | 72 +++++++++++++++++++- libpod/container_internal_test.go | 96 +++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/docs/source/markdown/options/mount.md b/docs/source/markdown/options/mount.md index 5f3969abbc..ffdee8abc5 100644 --- a/docs/source/markdown/options/mount.md +++ b/docs/source/markdown/options/mount.md @@ -37,6 +37,11 @@ Current supported mount TYPEs are **bind**, **volume**, **image**, **tmpfs** and . U, chown: true or false (default). Change recursively the owner and group of the source volume based on the UID and GID of the container. ยท idmap: true or false (default). If specified, create 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 #. Options specific to image: diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index b4a5e494d4..8b765888f2 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -47,6 +47,7 @@ import ( "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/lockfile" + stypes "github.com/containers/storage/types" securejoin "github.com/cyphar/filepath-securejoin" runcuser "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -56,6 +57,66 @@ import ( "github.com/sirupsen/logrus" ) +func parseOptionIDs(option string) ([]idtools.IDMap, error) { + ranges := strings.Split(option, "#") + ret := make([]idtools.IDMap, len(ranges)) + for i, m := range ranges { + var v idtools.IDMap + _, err := fmt.Sscanf(m, "%d-%d-%d", &v.ContainerID, &v.HostID, &v.Size) + if err != nil { + return nil, err + } + if v.ContainerID < 0 || v.HostID < 0 || v.Size < 1 { + return nil, fmt.Errorf("invalid value for %q", option) + } + ret[i] = v + } + return ret, nil +} + +func parseIDMapMountOption(idMappings stypes.IDMappingOptions, option string) ([]spec.LinuxIDMapping, []spec.LinuxIDMapping, error) { + uidMap := idMappings.UIDMap + gidMap := idMappings.GIDMap + if strings.HasPrefix(option, "idmap=") { + var err error + options := strings.Split(strings.SplitN(option, "=", 2)[1], ";") + for _, i := range options { + switch { + case strings.HasPrefix(i, "uids="): + uidMap, err = parseOptionIDs(strings.Replace(i, "uids=", "", 1)) + if err != nil { + return nil, nil, err + } + case strings.HasPrefix(i, "gids="): + gidMap, err = parseOptionIDs(strings.Replace(i, "gids=", "", 1)) + if err != nil { + return nil, nil, err + } + default: + return nil, nil, fmt.Errorf("unknown option %q", i) + } + } + } + + 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), + } + } + for i, gidmap := range gidMap { + gidMappings[i] = spec.LinuxIDMapping{ + HostID: uint32(gidmap.ContainerID), + ContainerID: uint32(gidmap.HostID), + Size: uint32(gidmap.Size), + } + } + return uidMappings, gidMappings, nil +} + // Internal only function which returns upper and work dir from // overlay options. func getOverlayUpperAndWorkDir(options []string) (string, string, error) { @@ -217,13 +278,22 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { } } - // Check if the spec file mounts contain the options z, Z or U. + // Check if the spec file mounts contain the options z, Z, U or idmap. // If they have z or Z, relabel the source directory and then remove the option. // If they have U, chown the source directory and them remove the option. + // If they have idmap, then calculate the mappings to use in the OCI config file. for i := range g.Config.Mounts { m := &g.Config.Mounts[i] var options []string 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) + if err != nil { + return nil, err + } + continue + } switch o { case "U": if m.Type == "tmpfs" { diff --git a/libpod/container_internal_test.go b/libpod/container_internal_test.go index 46a2da5443..af3a5886c0 100644 --- a/libpod/container_internal_test.go +++ b/libpod/container_internal_test.go @@ -8,6 +8,8 @@ import ( "runtime" "testing" + "github.com/containers/storage/pkg/idtools" + stypes "github.com/containers/storage/types" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" ) @@ -15,6 +17,100 @@ import ( // hookPath is the path to an example hook executable. var hookPath string +func TestParseOptionIDs(t *testing.T) { + _, err := parseOptionIDs("uids=100-200-2") + assert.NotNil(t, err) + + mappings, err := parseOptionIDs("100-200-2") + assert.Nil(t, err) + assert.NotNil(t, mappings) + + assert.Equal(t, len(mappings), 1) + + assert.Equal(t, mappings[0].ContainerID, 100) + assert.Equal(t, mappings[0].HostID, 200) + assert.Equal(t, mappings[0].Size, 2) + + mappings, err = parseOptionIDs("100-200-2#300-400-5") + assert.Nil(t, err) + assert.NotNil(t, mappings) + + assert.Equal(t, len(mappings), 2) + + assert.Equal(t, mappings[0].ContainerID, 100) + assert.Equal(t, mappings[0].HostID, 200) + assert.Equal(t, mappings[0].Size, 2) + + assert.Equal(t, mappings[1].ContainerID, 300) + assert.Equal(t, mappings[1].HostID, 400) + assert.Equal(t, mappings[1].Size, 5) +} + +func TestParseIDMapMountOption(t *testing.T) { + uidMap := []idtools.IDMap{ + { + ContainerID: 0, + HostID: 1000, + Size: 10000, + }, + } + gidMap := []idtools.IDMap{ + { + ContainerID: 0, + HostID: 2000, + Size: 10000, + }, + } + options := stypes.IDMappingOptions{ + UIDMap: uidMap, + GIDMap: gidMap, + } + uids, gids, err := parseIDMapMountOption(options, "idmap") + assert.Nil(t, err) + assert.Equal(t, len(uids), 1) + assert.Equal(t, len(gids), 1) + + assert.Equal(t, uids[0].ContainerID, uint32(1000)) + assert.Equal(t, uids[0].HostID, uint32(0)) + assert.Equal(t, uids[0].Size, uint32(10000)) + + assert.Equal(t, gids[0].ContainerID, uint32(2000)) + 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") + assert.Nil(t, err) + assert.Equal(t, len(uids), 2) + assert.Equal(t, len(gids), 1) + + assert.Equal(t, uids[0].ContainerID, uint32(1)) + assert.Equal(t, uids[0].HostID, uint32(0)) + assert.Equal(t, uids[0].Size, uint32(10)) + + assert.Equal(t, uids[1].ContainerID, uint32(11)) + assert.Equal(t, uids[1].HostID, uint32(10)) + assert.Equal(t, uids[1].Size, uint32(10)) + + assert.Equal(t, gids[0].ContainerID, uint32(3)) + 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") + assert.NotNil(t, err) + + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12") + assert.NotNil(t, err) + + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0-12--12") + assert.NotNil(t, err) + + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#-1-12-12") + assert.NotNil(t, err) + + _, _, err = parseIDMapMountOption(options, "idmap=uids=0-1-10#10-11-10;gids=0-3-10#0--12-0") + assert.NotNil(t, err) +} + func TestPostDeleteHooks(t *testing.T) { ctx := context.Background() dir := t.TempDir()