diff --git a/libcontainer/configs/mount.go b/libcontainer/configs/mount.go index e39440fc08d..bfd356e497f 100644 --- a/libcontainer/configs/mount.go +++ b/libcontainer/configs/mount.go @@ -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 ) diff --git a/libcontainer/configs/mount_linux.go b/libcontainer/configs/mount_linux.go index 3f489295d97..b69e9ab238e 100644 --- a/libcontainer/configs/mount_linux.go +++ b/libcontainer/configs/mount_linux.go @@ -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"` @@ -34,17 +52,9 @@ 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 { @@ -52,5 +62,5 @@ func (m *Mount) IsBind() bool { } func (m *Mount) IsIDMapped() bool { - return len(m.UIDMappings) > 0 || len(m.GIDMappings) > 0 + return m.IDMapping != nil } diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 33f9cc3f5fe..4708e1b773f 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -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 } @@ -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 } diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index 9d3f50c7c70..5c12e08a1db 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, + }, }, }, }, @@ -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, }, }, }, @@ -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 { diff --git a/libcontainer/mount_linux.go b/libcontainer/mount_linux.go index 73cd04c086d..6b5edbb0c14 100644 --- a/libcontainer/mount_linux.go +++ b/libcontainer/mount_linux.go @@ -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 { diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 5dc6ec79f32..6f9f58ad1d0 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -38,6 +38,7 @@ var ( clear bool flag int } + complexFlags map[string]func(*configs.Mount) ) func initMaps() { @@ -126,7 +127,6 @@ func initMaps() { "rnostrictatime": {true, unix.MOUNT_ATTR_STRICTATIME}, "rnosymfollow": {false, unix.MOUNT_ATTR_NOSYMFOLLOW}, // since kernel 5.14 "rsymfollow": {true, unix.MOUNT_ATTR_NOSYMFOLLOW}, // since kernel 5.14 - // No support for MOUNT_ATTR_IDMAP yet (needs UserNS FD) } extensionFlags = map[string]struct { @@ -135,6 +135,17 @@ func initMaps() { }{ "tmpcopyup": {false, configs.EXT_COPYUP}, } + + complexFlags = map[string]func(*configs.Mount){ + "idmap": func(m *configs.Mount) { + m.IDMapping = new(configs.MountIDMapping) + m.IDMapping.Recursive = false // noop + }, + "ridmap": func(m *configs.Mount) { + m.IDMapping = new(configs.MountIDMapping) + m.IDMapping.Recursive = true + }, + } }) } @@ -415,6 +426,19 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { if err := setupUserNamespace(spec, config); err != nil { return nil, err } + // For idmap and ridmap mounts without explicit mappings, use the + // ones from the container's userns. If we are joining another + // userns, stash the path. + for _, m := range config.Mounts { + if m.IDMapping != nil && m.IDMapping.UIDMappings == nil && m.IDMapping.GIDMappings == nil { + if path := config.Namespaces.PathOf(configs.NEWUSER); path != "" { + m.IDMapping.UserNSPath = path + } else { + m.IDMapping.UIDMappings = config.UIDMappings + m.IDMapping.GIDMappings = config.GIDMappings + } + } + } } config.MaskPaths = spec.Linux.MaskedPaths config.ReadonlyPaths = spec.Linux.ReadonlyPaths @@ -447,6 +471,7 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { Domain: domain, } } + } // Set the host UID that should own the container's cgroup. @@ -552,8 +577,14 @@ func createLibcontainerMount(cwd string, m specs.Mount) (*configs.Mount, error) } } - mnt.UIDMappings = toConfigIDMap(m.UIDMappings) - mnt.GIDMappings = toConfigIDMap(m.GIDMappings) + if m.UIDMappings != nil || m.GIDMappings != nil { + if mnt.IDMapping == nil { + // Neither "idmap" nor "ridmap" were specified. + mnt.IDMapping = new(configs.MountIDMapping) + } + mnt.IDMapping.UIDMappings = toConfigIDMap(m.UIDMappings) + mnt.IDMapping.GIDMappings = toConfigIDMap(m.GIDMappings) + } // None of the mount arguments can contain a null byte. Normally such // strings would either cause some other failure or would just be truncated @@ -1046,12 +1077,14 @@ func parseMountOptions(options []string) *configs.Mount { recAttrClr |= unix.MOUNT_ATTR__ATIME } } - } else if f, exists := extensionFlags[o]; exists && f.flag != 0 { + } else if f, exists := extensionFlags[o]; exists { if f.clear { m.Extensions &= ^f.flag } else { m.Extensions |= f.flag } + } else if fn, exists := complexFlags[o]; exists { + fn(&m) } else { data = append(data, o) } diff --git a/tests/integration/idmap.bats b/tests/integration/idmap.bats index 89fe4d82356..a816fe96863 100644 --- a/tests/integration/idmap.bats +++ b/tests/integration/idmap.bats @@ -5,7 +5,6 @@ load helpers function setup() { OVERFLOW_UID="$(cat /proc/sys/kernel/overflowuid)" OVERFLOW_GID="$(cat /proc/sys/kernel/overflowgid)" - requires root requires_kernel 5.12 @@ -37,12 +36,34 @@ function setup() { # Add a symlink-containing source. ln -s source-multi1 source-multi1-symlink + + # Add some top-level files in the mount tree. + mkdir -p mnt-subtree/multi{1,2} + touch mnt-subtree/{foo,bar,baz}.txt + chown 100:211 mnt-subtree/foo.txt + chown 200:222 mnt-subtree/bar.txt + chown 300:233 mnt-subtree/baz.txt + + mounts_file="$PWD/.all-mounts" + echo -n >"$mounts_file" } function teardown() { + if [ -v mounts_file ]; then + xargs -n 1 -a "$mounts_file" -- umount -l + rm -f "$mounts_file" + fi teardown_bundle } +function setup_host_bind_mount() { + src="$1" + dst="$2" + + mount --bind "$src" "$dst" + echo "$dst" >>"$mounts_file" +} + function setup_idmap_userns() { update_config '.linux.namespaces += [{"type": "user"}] | .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}] @@ -341,3 +362,331 @@ function setup_idmap_basic_mount() { [[ "$output" == *"=/tmp/mount-multi1/bar.txt:2000=2202="* ]] [[ "$output" == *"=/tmp/mount-multi1/baz.txt:3000=3303="* ]] } + +@test "idmap mount (non-recursive idmap) [userns]" { + setup_idmap_userns + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] +} + +@test "idmap mount (non-recursive idmap) [no userns]" { + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]] +} + +@test "idmap mount (idmap flag) [userns]" { + setup_idmap_userns + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "idmap"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] +} + +@test "idmap mount (idmap flag) [no userns]" { + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "idmap"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]] +} + +@test "idmap mount (ridmap flag) [userns]" { + setup_idmap_userns + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "ridmap"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:1000=1101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:2000=2202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:3000=3303="* ]] + # The child mounts have the same mapping applied. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:1000=1101="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:1001=2202="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:1002=3303="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:2000=1101="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:2001=2202="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:2002=3303="* ]] +} + +@test "idmap mount (ridmap flag) [no userns]" { + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "ridmap"], + "uidMappings": [ + {"containerID": 100, "hostID": 101000, "size": 3}, + {"containerID": 200, "hostID": 102000, "size": 3}, + {"containerID": 300, "hostID": 103000, "size": 3} + ], + "gidMappings": [ + {"containerID": 210, "hostID": 101100, "size": 10}, + {"containerID": 220, "hostID": 102200, "size": 10}, + {"containerID": 230, "hostID": 103300, "size": 10} + ] + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:101000=101101="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:102000=102202="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:103000=103303="* ]] + # The child mounts have the same mapping applied. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:101000=101101="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101001=102202="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:101002=103303="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:102000=101101="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:102001=102202="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:102002=103303="* ]] +} + +@test "idmap mount (idmap flag, implied mapping) [userns]" { + setup_idmap_userns + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "idmap"], + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] +} + +@test "idmap mount (ridmap flag, implied mapping) [userns]" { + setup_idmap_userns + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "ridmap"], + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]] + # The child mounts have the same mapping applied. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:101=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:102=233="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:200=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:201=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:202=233="* ]] +} + +@test "idmap mount (idmap flag, implied mapping, userns join) [userns]" { + # Create a detached container with the id-mapping we want. + cp config.json config.json.bak + update_config '.linux.namespaces += [{"type": "user"}] + | .linux.uidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}] + | .linux.gidMappings += [{"containerID": 0, "hostID": 100000, "size": 65536}]' + update_config '.process.args = ["sleep", "infinity"]' + + runc run -d --console-socket "$CONSOLE_SOCKET" target_userns + [ "$status" -eq 0 ] + + # Configure our container to attach to the first container's userns. + target_pid="$(__runc state target_userns | jq .pid)" + update_config '.linux.namespaces |= map(if .type == "user" then (.path = "/proc/'"$target_pid"'/ns/" + .type) else . end)' + update_config 'del(.linux.uidMappings) | del(.linux.gidMappings)' + + setup_host_bind_mount "source-multi1/" "mnt-subtree/multi1" + setup_host_bind_mount "source-multi2/" "mnt-subtree/multi2" + + update_config '.mounts += [ + { + "source": "mnt-subtree/", + "destination": "/tmp/mount-tree", + "options": ["rbind", "idmap"], + } + ]' + + update_config '.process.args = ["bash", "-c", "stat -c =%n:%u=%g= /tmp/mount-tree{,/multi1,/multi2}/{foo,bar,baz}.txt"]' + runc run test_debian + [ "$status" -eq 0 ] + [[ "$output" == *"=/tmp/mount-tree/foo.txt:100=211="* ]] + [[ "$output" == *"=/tmp/mount-tree/bar.txt:200=222="* ]] + [[ "$output" == *"=/tmp/mount-tree/baz.txt:300=233="* ]] + # Because we used "idmap", the child mounts were not remapped recursively. + [[ "$output" == *"=/tmp/mount-tree/multi1/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi1/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/foo.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/bar.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] + [[ "$output" == *"=/tmp/mount-tree/multi2/baz.txt:$OVERFLOW_UID=$OVERFLOW_GID="* ]] +}