Support recursive mount attrs ("rro", "rnosuid", "rnodev", ...)

The new mount option "rro" makes the mount point recursively read-only,
by calling `mount_setattr(2)` with `MOUNT_ATTR_RDONLY` and `AT_RECURSIVE`.
https://man7.org/linux/man-pages/man2/mount_setattr.2.html

Requires kernel >= 5.12.

The "rro" option string conforms to the proposal in util-linux/util-linux Issue 1501.

Fix issue 2823

Similary, this commit also adds the following mount options:
- rrw
- r[no]{suid,dev,exec,relatime,atime,strictatime,diratime,symfollow}

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
This commit is contained in:
Akihiro Suda
2021-11-12 18:41:11 +09:00
parent ba935a51a6
commit 382eba4354
4 changed files with 149 additions and 5 deletions

View File

@@ -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"`

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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"* ]]
}