$RodHat_
Console Tips

mount_setattr(2): change mount flags on a live tree

Published by · Updated

mount_setattr(2): change mount flags on a live tree
Photo: AI-generated — no human photographer / RodHat AI Cover

mount(2) with MS_REMOUNT has been broken for decades in ways the documentation never volunteers. Flip MS_RDONLY on a bind mount, get back 0, watch the mount stay writable. There is also no way to express “apply MS_NOSUID to this subtree and everything underneath it”; mount(2) acts on one mount record and ignores everything else. Run findmnt --submounts /path sometime and count the records you’re not touching.

Linux 5.12 added mount_setattr(2).

The interface

#include <linux/mount.h>
#include <sys/syscall.h>
#include <unistd.h>

struct mount_attr {
    __u64 attr_set;    /* attributes to set */
    __u64 attr_clr;    /* attributes to clear */
    __u64 propagation; /* MS_SHARED, MS_PRIVATE, MS_SLAVE, MS_UNBINDABLE */
    __u64 userns_fd;   /* fd of a user namespace for UID/GID shifting */
};

long syscall(SYS_mount_setattr, int dirfd, const char *pathname,
             unsigned int flags, struct mount_attr *attr, size_t size);

No glibc wrapper in any released version. syscall(2) it. Pass sizeof(struct mount_attr) as size; non-zero unknown fields return E2BIG, same versioning trick as openat2(2).

attr_set and attr_clr

The old MS_REMOUNT blasts a new flag set onto the mount. mount_setattr uses a set/clear pair: put what you want enabled in attr_set, what you want disabled in attr_clr, and anything not mentioned in either is left alone. You can enforce MOUNT_ATTR_NOSUID on a mount without knowing or caring whether it is already MOUNT_ATTR_RDONLY.

Available flags:

MOUNT_ATTR_RDONLY       read-only
MOUNT_ATTR_NOSUID       no set-uid / set-gid execution
MOUNT_ATTR_NODEV        no device files
MOUNT_ATTR_NOEXEC       no execution
MOUNT_ATTR_NOSYMFOLLOW  no symlink following (Linux 5.18)

MOUNT_ATTR__ATIME       mask for selecting one of:
  MOUNT_ATTR_RELATIME   relatime
  MOUNT_ATTR_NOATIME    no atime updates
  MOUNT_ATTR_STRICTATIME force atime on every read

Atime constants are mutually exclusive. To change the atime mode, put the new constant in attr_set and put MOUNT_ATTR__ATIME (the whole mask) in attr_clr. That clears the old mode bits before the new ones land.

AT_RECURSIVE

The flags argument takes AT_RECURSIVE. With it, the kernel walks every mount in the subtree rooted at dirfd/pathname and applies the change to each one atomically. This is the thing mount(2) cannot do.

struct mount_attr attr = {
    .attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID | MOUNT_ATTR_NOEXEC,
};

if (syscall(SYS_mount_setattr, AT_FDCWD, "/container/rootfs",
            AT_RECURSIVE, &attr, sizeof(attr)) < 0)
    err(1, "mount_setattr");

That one call covers every bind mount, every tmpfs, every overlay inside /container/rootfs. The old approach was a recursive findmnt driving a loop of mount --bind --remount calls, which is not atomic and fails silently on shared propagation mounts depending on how the namespace is set up.

AT_EMPTY_PATH with open_tree

If you have a mount fd from open_tree(2) (part of the same new-mount-API generation: see the fsopen/fsmount tip), pass the fd as dirfd, an empty string as pathname, and add AT_EMPTY_PATH to flags. The call operates on the fd directly without path resolution. Useful in container setup code where you’re working with detached mount trees before attaching them to the namespace.

/* clone the subtree into a detached mount fd */
int mfd = syscall(SYS_open_tree, AT_FDCWD, "/container/rootfs",
                  OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
if (mfd < 0) err(1, "open_tree");

/* make the clone read-only before attaching it */
struct mount_attr attr = { .attr_set = MOUNT_ATTR_RDONLY };
if (syscall(SYS_mount_setattr, mfd, "", AT_EMPTY_PATH | AT_RECURSIVE,
            &attr, sizeof(attr)) < 0)
    err(1, "mount_setattr");

/* mfd is now a cloned, read-only subtree; not yet in any namespace */

userns_fd

Set userns_fd to an fd pointing at a user namespace and the kernel rewrites UID/GID ownership on the mount to map into that namespace. This is how rootless container runtimes attach a cloned subtree and shift uid=0 inside the clone to an unprivileged host UID. Requires CAP_SYS_ADMIN in the user namespace.

FreeBSD

FreeBSD’s mount(2) can reconfigure an existing mount via MNT_UPDATE and it works correctly on the mount record you named. Propagating flags to a jail’s entire mount tree goes through jail_set(2) parameters (allow.noexec, etc.) rather than by walking mount records. Different model, same result for the common case. Fine-grained per-mount control like AT_RECURSIVE is manual on FreeBSD too.

POSIX never touched any of this. Everyone built their own.

What mount_setattr does not do

MS_REMOUNT is still the only way to change filesystem-level options (the options string: errors=remount-ro, journal mode, barrier behavior). mount_setattr handles VFS-layer flags only: the generic ones that apply regardless of filesystem type. If you need to change an ext4-specific option on a live mount, you are still calling mount(2).

Requires CAP_SYS_ADMIN in the user namespace owning the mount. On kernels before 5.12, ENOSYS is your fallback signal; build a mount --bind --remount loop and accept that it is not atomic.

Sources

  1. mount_setattr(2) man page (man7.org)
  2. Improving the mount API (Linux 5.12 mount changes) (LWN.net)