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()