diff --git a/libcontainer/configs/mount.go b/libcontainer/configs/mount.go index 3f315f7186c..784c6182051 100644 --- a/libcontainer/configs/mount.go +++ b/libcontainer/configs/mount.go @@ -30,6 +30,9 @@ type Mount struct { // Relabel source if set, "z" indicates shared, "Z" indicates unshared. Relabel string `json:"relabel"` + // RecAttr represents mount properties to be applied recursively (AT_RECURSIVE), see mount_setattr(2). + RecAttr *unix.MountAttr `json:"rec_attr"` + // Extensions are additional flags that are specific to runc. Extensions int `json:"extensions"` diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 19103d89237..51660f5efb9 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -493,6 +493,9 @@ func mountToRootfs(m *configs.Mount, c *mountConfig) error { } return mountPropagate(m, rootfs, mountLabel, mountFd) } + if err := setRecAttr(m, rootfs); err != nil { + return err + } return nil } @@ -1123,3 +1126,12 @@ func mountPropagate(m *configs.Mount, rootfs string, mountLabel string, mountFd } return nil } + +func setRecAttr(m *configs.Mount, rootfs string) error { + if m.RecAttr == nil { + return nil + } + return utils.WithProcfd(rootfs, m.Destination, func(procfd string) error { + return unix.MountSetattr(-1, procfd, unix.AT_RECURSIVE, m.RecAttr) + }) +} diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 2cbc679911a..c7ca4c8af1f 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -26,9 +26,13 @@ import ( ) var ( - initMapsOnce sync.Once - namespaceMapping map[specs.LinuxNamespaceType]configs.NamespaceType - mountPropagationMapping map[string]int + initMapsOnce sync.Once + namespaceMapping map[specs.LinuxNamespaceType]configs.NamespaceType + mountPropagationMapping map[string]int + recAttrFlags map[string]struct { + clear bool + flag uint64 + } mountFlags, extensionFlags map[string]struct { clear bool flag int @@ -99,6 +103,32 @@ func initMaps() { "sync": {false, unix.MS_SYNCHRONOUS}, "symfollow": {true, unix.MS_NOSYMFOLLOW}, // since kernel 5.10 } + + recAttrFlags = map[string]struct { + clear bool + flag uint64 + }{ + "rro": {false, unix.MOUNT_ATTR_RDONLY}, + "rrw": {true, unix.MOUNT_ATTR_RDONLY}, + "rnosuid": {false, unix.MOUNT_ATTR_NOSUID}, + "rsuid": {true, unix.MOUNT_ATTR_NOSUID}, + "rnodev": {false, unix.MOUNT_ATTR_NODEV}, + "rdev": {true, unix.MOUNT_ATTR_NODEV}, + "rnoexec": {false, unix.MOUNT_ATTR_NOEXEC}, + "rexec": {true, unix.MOUNT_ATTR_NOEXEC}, + "rnodiratime": {false, unix.MOUNT_ATTR_NODIRATIME}, + "rdiratime": {true, unix.MOUNT_ATTR_NODIRATIME}, + "rrelatime": {false, unix.MOUNT_ATTR_RELATIME}, + "rnorelatime": {true, unix.MOUNT_ATTR_RELATIME}, + "rnoatime": {false, unix.MOUNT_ATTR_NOATIME}, + "ratime": {true, unix.MOUNT_ATTR_NOATIME}, + "rstrictatime": {false, unix.MOUNT_ATTR_STRICTATIME}, + "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 { clear bool flag int @@ -133,6 +163,9 @@ func KnownMountOptions() []string { res = append(res, k) } } + for k := range recAttrFlags { + res = append(res, k) + } for k := range extensionFlags { res = append(res, k) } @@ -924,8 +957,9 @@ func setupUserNamespace(spec *specs.Spec, config *configs.Config) error { // structure with fields that depends on options set accordingly. func parseMountOptions(options []string) *configs.Mount { var ( - data []string - m configs.Mount + data []string + m configs.Mount + recAttrSet, recAttrClr uint64 ) initMaps() for _, o := range options { @@ -940,6 +974,17 @@ func parseMountOptions(options []string) *configs.Mount { } } else if f, exists := mountPropagationMapping[o]; exists && f != 0 { m.PropagationFlags = append(m.PropagationFlags, f) + } else if f, exists := recAttrFlags[o]; exists { + if f.clear { + recAttrClr |= f.flag + } else { + recAttrSet |= f.flag + if f.flag&unix.MOUNT_ATTR__ATIME == f.flag { + // https://man7.org/linux/man-pages/man2/mount_setattr.2.html + // "cannot simply specify the access-time setting in attr_set, but must also include MOUNT_ATTR__ATIME in the attr_clr field." + recAttrClr |= unix.MOUNT_ATTR__ATIME + } + } } else if f, exists := extensionFlags[o]; exists && f.flag != 0 { if f.clear { m.Extensions &= ^f.flag @@ -951,6 +996,12 @@ func parseMountOptions(options []string) *configs.Mount { } } m.Data = strings.Join(data, ",") + if recAttrSet != 0 || recAttrClr != 0 { + m.RecAttr = &unix.MountAttr{ + Attr_set: recAttrSet, + Attr_clr: recAttrClr, + } + } return &m } diff --git a/tests/integration/mounts_recursive.bats b/tests/integration/mounts_recursive.bats new file mode 100644 index 00000000000..b3ce579fc02 --- /dev/null +++ b/tests/integration/mounts_recursive.bats @@ -0,0 +1,78 @@ +#!/usr/bin/env bats + +load helpers + +TESTVOLUME="${BATS_RUN_TMPDIR}/mounts_recursive" + +function setup_volume() { + # requires root (in the current user namespace) to mount tmpfs outside runc + requires root + + mkdir -p "${TESTVOLUME}" + mount -t tmpfs none "${TESTVOLUME}" + echo "foo" >"${TESTVOLUME}/foo" + + mkdir "${TESTVOLUME}/subvol" + mount -t tmpfs none "${TESTVOLUME}/subvol" + echo "bar" >"${TESTVOLUME}/subvol/bar" +} + +function teardown_volume() { + umount -R "${TESTVOLUME}" +} + +function setup() { + setup_volume + setup_busybox +} + +function teardown() { + teardown_volume + teardown_bundle +} + +@test "runc run [rbind,ro mount is read-only but not recursively]" { + update_config ".mounts += [{source: \"${TESTVOLUME}\" , destination: \"/mnt\", options: [\"rbind\",\"ro\"]}]" + + runc run -d --console-socket "$CONSOLE_SOCKET" test_rbind_ro + [ "$status" -eq 0 ] + + runc exec test_rbind_ro touch /mnt/foo + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] + + runc exec test_rbind_ro touch /mnt/subvol/bar + [ "$status" -eq 0 ] +} + +@test "runc run [rbind,rro mount is recursively read-only]" { + requires_kernel 5.12 + update_config ".mounts += [{source: \"${TESTVOLUME}\" , destination: \"/mnt\", options: [\"rbind\",\"rro\"]}]" + + runc run -d --console-socket "$CONSOLE_SOCKET" test_rbind_rro + [ "$status" -eq 0 ] + + runc exec test_rbind_rro touch /mnt/foo + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] + + runc exec test_rbind_rro touch /mnt/subvol/bar + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] +} + +@test "runc run [rbind,ro,rro mount is recursively read-only too]" { + requires_kernel 5.12 + update_config ".mounts += [{source: \"${TESTVOLUME}\" , destination: \"/mnt\", options: [\"rbind\",\"ro\",\"rro\"]}]" + + runc run -d --console-socket "$CONSOLE_SOCKET" test_rbind_ro_rro + [ "$status" -eq 0 ] + + runc exec test_rbind_ro_rro touch /mnt/foo + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] + + runc exec test_rbind_ro_rro touch /mnt/subvol/bar + [ "$status" -eq 1 ] + [[ "${output}" == *"Read-only file system"* ]] +}