Skip to content
LinuxDeep Dive Published Updated 5 min readViews unavailable

Linux mount_setattr: Change Mount-Tree Attributes Without Conflating Propagation

Use mount_setattr to reason about recursive read-only mounts, per-mount attributes, propagation, and failure handling in Linux mount namespaces.

Making a bind mount read-only is easy to describe and easy to overstate. A mount tree can contain child mounts whose attributes differ from the parent, and the word “recursive” has a specific meaning in the newer Linux mount API. mount_setattr(2) changes attributes on a mount or, with AT_RECURSIVE, a mount tree. It also accepts a propagation field, but propagation and read-only status are different properties. Treat them as separate decisions.

Identify the target tree first

Inventory the relevant namespace before changing it:

findmnt -R -o TARGET,SOURCE,FSTYPE,OPTIONS,PROPAGATION /srv/sandbox
cat /proc/self/mountinfo

These commands describe the calling process’s mount namespace. If the service lives in another namespace, inspect from the correct process context; the host’s view is not automatically the service’s view. Keep a copy of the initial mount table and use a disposable mount namespace for testing. Avoid experimenting on the host’s root mount tree.

The syscall takes a directory FD, path, flags, a zero-initialized struct mount_attr, and its size. attr_set adds MOUNT_ATTR_* attributes, while attr_clr removes them. For a read-only request, set MOUNT_ATTR_RDONLY in attr_set. AT_RECURSIVE applies the requested attribute change to the entire mount tree rather than just one mount. AT_EMPTY_PATH lets an empty path refer to the mount held by dirfd. Check the exact call contract in the target kernel and libc headers; mount_setattr dates from Linux 5.12, while libc wrapper availability may differ.

The other commonly used per-mount attributes are MOUNT_ATTR_NOSUID, MOUNT_ATTR_NODEV, and MOUNT_ATTR_NOEXEC. They express different restrictions. NOEXEC is not a general ban on interpreting file contents; a program can still read a script and pass it to an interpreter from elsewhere. NODEV concerns device special files, not all access to physical devices through other interfaces. The right policy is therefore a combination of mount attributes, namespace construction, permissions, and any higher-level confinement mechanism the service actually uses.

The structure’s size parameter is part of the extensibility contract. The manual explains that newer kernels may accept a larger structure while older kernels know only the initial version; zero-fill the structure so unknown future fields are not accidentally treated as requests. Do not build code that assumes a header from a modern build machine means the deployed kernel implements every requested flag. Detect an unsupported syscall or attribute at setup time and fail the sandbox initialization explicitly.

Do not confuse read-only with propagation

The propagation field can request MS_PRIVATE, MS_SHARED, MS_SLAVE, or MS_UNBINDABLE; zero leaves propagation unchanged. A read-only mount can still participate in a shared propagation relationship. Conversely, making a tree private does not itself make it read-only. The mount_namespaces(7) manual explains how shared subtrees propagate mount and unmount events; use it to decide whether another namespace may observe topology changes.

This distinction is important for a sandbox assembled from bind mounts. A policy that needs a stable, read-only view must verify both the attribute state of descendants and the desired propagation relationship. A read-only remount of only the top-level mount is not equivalent to a recursive policy when writable child mounts exist.

Consider a service root at /srv/sandbox containing a separate writable mount at /srv/sandbox/cache. Making only the top-level mount read-only leaves the cache mount’s own attributes untouched. A recursive read-only operation includes it, which may be the intended security boundary - or may break a service that genuinely needs the cache. Map the tree first, decide which descendants are exceptions, and arrange those exceptions deliberately in a private test namespace. “Recursive” is not automatically the safer choice when the target contains unrelated mounts.

Propagation is also a topology question. A shared mount can pass mount and unmount events to peers, whereas private propagation prevents that sharing. Changing propagation on a subtree can alter how later mounts appear in containers or services. Verify the PROPAGATION column from the same namespace both before and after setup; a read-only flag alone will not answer that question.

Handle failures as incomplete setup

On success the syscall returns zero; on failure it returns -1 with errno. The manual documents EBUSY when attempting a read-only change while the mount still has files open for writing, and EINVAL for several invalid mount or attribute conditions. Do not swallow these errors and then start an application as though the sandbox were established. Fail closed, report the target and error, and inspect the resulting tree before retrying.

For a builder that uses open_tree, mount_setattr, and move_mount, configure a detached clone before attaching it to the destination, following the manual’s example. That makes the staged tree easier to inspect and avoids silently changing an unrelated live mount. The exact capability and namespace requirements depend on the caller’s environment; a successful build on a privileged development host does not prove an unprivileged deployment will work.

The man page gives several important error distinctions beyond a generic failure. An invalid target that is not a mount point can return EINVAL; a bad descriptor can return EBADF; a busy mount with write-open files can return EBUSY. Log the syscall arguments at a safe level of detail and the actual errno. Do not automatically retry with weaker flags or silently omit AT_RECURSIVE: that would turn a deployment failure into a different security policy. If an older runtime cannot use this API, select an explicitly reviewed alternate setup path rather than improvising a sequence of remount commands.

For tests, use a fresh namespace and a small tree with at least one nested mount. Check both a file on the top-level mount and one on the child. Repeat with an open writer to exercise the busy path, and verify that the application is never launched after the setup failure. The negative test is as important as the successful mount table: it proves the launcher does not mistake a partial setup for isolation.

Finally, re-read /proc/self/mountinfo or findmnt from the service namespace and exercise an attempted write against a disposable file. A syscall success code verifies that the requested operation completed, not that every path the application can reach has the desired policy.

Related:

Sources:

Comments