Skip to content

Commit

Permalink
mount: add support for ridmap and idmap
Browse files Browse the repository at this point in the history
ridmap indicates that the id mapping should be applied recursively (only
really relevant for rbind mount entries), and idmap indicates that it
should not be applied recursively (the default). If no mappings are
specified for the mount, we use the userns configuration of the
container. This matches the behaviour in the currently-unreleased
runtime-spec.

This includes a minor change to the state.json serialisation format, but
because there has been no released version of runc with commit
fbf183c ("Add uid and gid mappings to mounts"), we can safely make
this change without affecting running containers. Doing it this way
makes it much easier to handle m.IsIDMapped() and indicating that a
mapping has been specified.

Signed-off-by: Aleksa Sarai <[email protected]>
  • Loading branch information
cyphar committed Dec 14, 2023
1 parent 7795ca4 commit 3b57e45
Show file tree
Hide file tree
Showing 7 changed files with 588 additions and 52 deletions.
2 changes: 1 addition & 1 deletion libcontainer/configs/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package configs
const (
// EXT_COPYUP is a directive to copy up the contents of a directory when
// a tmpfs is mounted over it.
EXT_COPYUP = 1 << iota //nolint:golint // ignore "don't use ALL_CAPS" warning
EXT_COPYUP = 1 << iota //nolint:golint,revive // ignore "don't use ALL_CAPS" warning
)
34 changes: 22 additions & 12 deletions libcontainer/configs/mount_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ package configs

import "golang.org/x/sys/unix"

type MountIDMapping struct {
// Recursive indicates if the mapping needs to be recursive.
Recursive bool `json:"recursive"`

// UserNSPath is a path to a user namespace that indicates the necessary
// id-mappings for MOUNT_ATTR_IDMAP. If set to non-"", UIDMappings and
// GIDMappings must be set to nil.
UserNSPath string `json:"userns_path,omitempty"`

// UIDMappings is the uid mapping set for this mount, to be used with
// MOUNT_ATTR_IDMAP.
UIDMappings []IDMap `json:"uid_mappings,omitempty"`

// GIDMappings is the gid mapping set for this mount, to be used with
// MOUNT_ATTR_IDMAP.
GIDMappings []IDMap `json:"gid_mappings,omitempty"`
}

type Mount struct {
// Source path for the mount.
Source string `json:"source"`
Expand Down Expand Up @@ -34,23 +52,15 @@ type Mount struct {
// Extensions are additional flags that are specific to runc.
Extensions int `json:"extensions"`

// UIDMappings is used to changing file user owners w/o calling chown.
// Note that, the underlying filesystem should support this feature to be
// used.
// Every mount point could have its own mapping.
UIDMappings []IDMap `json:"uid_mappings,omitempty"`

// GIDMappings is used to changing file group owners w/o calling chown.
// Note that, the underlying filesystem should support this feature to be
// used.
// Every mount point could have its own mapping.
GIDMappings []IDMap `json:"gid_mappings,omitempty"`
// Mapping is the MOUNT_ATTR_IDMAP configuration for the mount. If non-nil,
// the mount is configured to use MOUNT_ATTR_IDMAP-style id mappings.
IDMapping *MountIDMapping `json:"id_mapping,omitempty"`
}

func (m *Mount) IsBind() bool {
return m.Flags&unix.MS_BIND != 0
}

func (m *Mount) IsIDMapped() bool {
return len(m.UIDMappings) > 0 || len(m.GIDMappings) > 0
return m.IDMapping != nil
}
17 changes: 17 additions & 0 deletions libcontainer/configs/validate/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,13 @@ func checkBindOptions(m *configs.Mount) error {
}

func checkIDMapMounts(config *configs.Config, m *configs.Mount) error {
// Make sure MOUNT_ATTR_IDMAP is not set on any of our mounts. This
// attribute is handled differently to all other attributes (through
// m.IDMapping), so make sure we never store it in the actual config. This
// really shouldn't ever happen.
if m.RecAttr != nil && (m.RecAttr.Attr_set|m.RecAttr.Attr_clr)&unix.MOUNT_ATTR_IDMAP != 0 {
return errors.New("mount configuration cannot contain recAttr for MOUNT_ATTR_IDMAP")
}
if !m.IsIDMapped() {
return nil
}
Expand All @@ -318,6 +325,16 @@ func checkIDMapMounts(config *configs.Config, m *configs.Mount) error {
if config.RootlessEUID {
return errors.New("id-mapped mounts are not supported for rootless containers")
}
if m.IDMapping.UserNSPath == "" {
if len(m.IDMapping.UIDMappings) == 0 || len(m.IDMapping.GIDMappings) == 0 {
return errors.New("id-mapped mounts must have both uid and gid mappings specified")
}
} else {
if m.IDMapping.UIDMappings != nil || m.IDMapping.GIDMappings != nil {
// should never happen
return errors.New("[internal error] id-mapped mounts cannot have both userns_path and uid and gid mappings specified")
}
}
return nil
}

Expand Down
165 changes: 138 additions & 27 deletions libcontainer/configs/validate/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,17 +504,82 @@ func TestValidateIDMapMounts(t *testing.T) {
config *configs.Config
}{
{
name: "idmap mount without bind opt specified",
name: "idmap non-bind mount",
isErr: true,
config: &configs.Config{
UIDMappings: mapping,
GIDMappings: mapping,
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap option non-bind mount",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{},
},
},
},
},
{
name: "ridmap option non-bind mount",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/dev/sda1",
Destination: "/abs/path/",
Device: "ext4",
IDMapping: &configs.MountIDMapping{
Recursive: true,
},
},
},
},
},
{
name: "idmap mount no uid mapping",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
GIDMappings: mapping,
},
},
},
},
},
{
name: "idmap mount no gid mapping",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
UIDMappings: mapping,
GIDMappings: mapping,
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
},
},
},
},
Expand All @@ -531,8 +596,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -547,8 +614,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "./rel/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -563,8 +632,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/abs/path/",
Destination: "./rel/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -579,8 +650,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/another-abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -595,8 +668,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/another-abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND | unix.MS_RDONLY,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -609,8 +684,10 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: mapping,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: mapping,
},
},
},
},
Expand All @@ -625,14 +702,16 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
IDMapping: &configs.MountIDMapping{
UIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
},
},
GIDMappings: mapping,
},
GIDMappings: mapping,
},
},
},
Expand All @@ -647,18 +726,50 @@ func TestValidateIDMapMounts(t *testing.T) {
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
UIDMappings: mapping,
GIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
IDMapping: &configs.MountIDMapping{
UIDMappings: mapping,
GIDMappings: []configs.IDMap{
{
ContainerID: 10,
HostID: 10,
Size: 1,
},
},
},
},
},
},
},
{
name: "mount with 'idmap' option but no mappings",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{},
},
},
},
},
{
name: "mount with 'ridmap' option but no mappings",
isErr: true,
config: &configs.Config{
Mounts: []*configs.Mount{
{
Source: "/abs/path/",
Destination: "/abs/path/",
Flags: unix.MS_BIND,
IDMapping: &configs.MountIDMapping{
Recursive: true,
},
},
},
},
},
}

for _, tc := range testCases {
Expand Down
30 changes: 23 additions & 7 deletions libcontainer/mount_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,32 @@ func mountFd(nsHandles *userns.Handles, m *configs.Mount) (*mountSource, error)
sourceType = mountSourceOpenTree

// Configure the id mapping.
usernsFile, err := nsHandles.Get(userns.Mapping{
UIDMappings: m.UIDMappings,
GIDMappings: m.GIDMappings,
})
if err != nil {
return nil, fmt.Errorf("failed to create userns for %s id-mapping: %w", m.Source, err)
var usernsFile *os.File
if m.IDMapping.UserNSPath == "" {
usernsFile, err = nsHandles.Get(userns.Mapping{
UIDMappings: m.IDMapping.UIDMappings,
GIDMappings: m.IDMapping.GIDMappings,
})
if err != nil {
return nil, fmt.Errorf("failed to create userns for %s id-mapping: %w", m.Source, err)
}
} else {
usernsFile, err = os.Open(m.IDMapping.UserNSPath)
if err != nil {
return nil, fmt.Errorf("failed to open existing userns for %s id-mapping: %w", m.Source, err)
}
}
defer usernsFile.Close()

if err := unix.MountSetattr(int(mountFile.Fd()), "", unix.AT_EMPTY_PATH, &unix.MountAttr{
setAttrFlags := uint(unix.AT_EMPTY_PATH)
// If the mount has "ridmap" set, we apply the configuration
// recursively. This allows you to create "rbind" mounts where only
// the top-level mount has an idmapping. I'm not sure why you'd
// want that, but still...
if m.IDMapping.Recursive {
setAttrFlags |= unix.AT_RECURSIVE
}
if err := unix.MountSetattr(int(mountFile.Fd()), "", setAttrFlags, &unix.MountAttr{
Attr_set: unix.MOUNT_ATTR_IDMAP,
Userns_fd: uint64(usernsFile.Fd()),
}); err != nil {
Expand Down
Loading

0 comments on commit 3b57e45

Please sign in to comment.