Skip to content

Commit

Permalink
libpod: use OCI idmappings for mounts
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
giuseppe committed Dec 14, 2022
1 parent 2a48296 commit fdcc225
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 1 deletion.
5 changes: 5 additions & 0 deletions docs/source/markdown/options/mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
72 changes: 71 additions & 1 deletion libpod/container_internal_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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" {
Expand Down
96 changes: 96 additions & 0 deletions libpod/container_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,109 @@ 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"
)

// 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()
Expand Down

0 comments on commit fdcc225

Please sign in to comment.