Skip to content
LinuxDeep Dive Published Updated 6 min readViews unavailable

Linux's File-Descriptor Mount API: fsopen, fsmount, and move_mount

How Linux builds, configures, clones, changes, and attaches mounts through file descriptors, with clearer lifetimes and fewer path-based races.

The traditional mount(2) interface compresses source, target, filesystem type, flags, and a filesystem-specific option string into one operation. It works, but it makes configuration hard to inspect and ties the final action to pathname lookup at the same moment the kernel creates the mount.

Linux’s newer mount API separates that lifecycle. A process can create a filesystem context with fsopen(), configure it through fsconfig(), turn it into a detached mount with fsmount(), and attach that mount using move_mount(). Related calls can open or clone an existing mount tree and change mount attributes through descriptors. The design makes object lifetime explicit, but it does not grant privileges or make unsafe mount choices harmless.

fsopen creates a configuration context

fsopen() selects a filesystem type and returns a descriptor for an fs_context. The descriptor represents an unfinished configuration, not a mounted filesystem. fsconfig() sends typed commands to it: set strings, binary values, paths represented by descriptors, flags, or the source device, then issue the create command.

Conceptually, a new mount follows this state machine:

fsopen("tmpfs")
  -> fsconfig(options...)
  -> fsconfig(CMD_CREATE)
  -> fsmount()
  -> detached mount FD
  -> move_mount() into a namespace path

Every transition can fail independently. Close the context descriptor on error, and do not claim a mount exists until attachment succeeds. Filesystem-specific validation happens through the selected filesystem, so option names and combinations still require its documentation.

Typed configuration replaces one ambiguous string

The old data argument often contains comma-separated text interpreted differently by each filesystem. fsconfig() identifies whether an option is a string, flag, binary blob, path, or file descriptor. This avoids some quoting and lifetime ambiguity and lets the kernel hold references to objects rather than forcing every input back through a pathname.

Typed does not mean universally standardized. A key valid for ext4 may be meaningless for tmpfs, and a driver can evolve its accepted options. Treat EINVAL, unsupported keys, and permission errors as specific configuration failures. Log the filesystem type and option key, but redact secrets and never dump arbitrary binary values.

Once the context is created, changing a userspace copy of an option does nothing. The kernel owns the configured state associated with the descriptor.

fsmount returns a detached tree

fsmount() turns a successfully created context into a mount object referenced by a file descriptor. It is detached: no process reaches it through the namespace until it is attached. Setup code can prepare the mount and apply supported attributes before making it visible to workloads.

The mount descriptor is an O_PATH-style handle, not a general file descriptor for reading the filesystem root. Use the mount API operations designed for it. Closing the last reference to an unattached mount lets the kernel clean it up, which gives error paths a natural rollback mechanism.

This staged visibility is useful for container setup. A runtime can create and configure a mount away from the final tree, enter the intended mount namespace, then attach it at a prepared target. The runtime must still coordinate target creation and namespace membership with other threads.

move_mount performs the attachment

move_mount() moves a mount from a source described by directory descriptor and path into a target described the same way. With an empty-path flag, the source can be the detached mount descriptor from fsmount() or open_tree().

The call makes the mount visible at the target. Validate that target through a trusted directory descriptor and use the narrowest supported flags. A hostile process able to replace directories or control the mount namespace can still alter the surrounding topology unless the runtime isolates those operations.

Attachment and application-level readiness are different events. A mounted filesystem may still require ownership changes, policy setup, or a service health check before clients should use it. Publish readiness only after the entire contract is satisfied.

open_tree captures or clones an existing mount

open_tree() returns a descriptor referring to an existing mount tree. It can capture the tree itself or clone it into a detached mount that can later be moved elsewhere. Recursive cloning can include submounts, which is powerful and easy to over-include.

A descriptor is safer than remembering a path that may later point somewhere else. It also makes mount manipulation composable across helper processes through controlled descriptor passing. The recipient must authenticate the sender and validate what the descriptor represents; descriptor passing transfers authority, not semantic intent.

Cloning does not copy filesystem data. It constructs another mount-tree view with the kernel’s propagation and reference semantics. Changes to underlying files remain changes to the same filesystem unless a separate copy-on-write layer provides isolation.

mount_setattr changes recursive policy explicitly

mount_setattr() can change properties such as read-only state, nosuid, nodev, noexec, atime behavior, propagation, and ID-mapped mounting according to kernel support. It can operate recursively, making one call affect a complete subtree.

Recursive changes need a topology review. A submount inserted unexpectedly before the call could inherit restrictions or, more dangerously, remain outside them if the runtime assumed a different tree. Build in an isolated namespace, prevent concurrent mount manipulation, inspect /proc/self/mountinfo, and fail closed when a required attribute is unsupported.

An ID-mapped mount changes how ownership IDs are interpreted for that mount. It is not the same as recursively changing file owners. Its user namespace and filesystem support requirements deserve separate compatibility tests.

Privilege and namespace rules still apply

These syscalls do not bypass CAP_SYS_ADMIN, user-namespace rules, filesystem restrictions, Linux Security Module policy, or lockdown decisions. A container runtime that holds mount authority remains a high-value component even if its code avoids path races.

Apply seccomp policy carefully. Allowing fsopen() while blocking a required later call can leave confusing partial state, while broadly allowing all mount-related operations expands attack surface. Separate a short-lived privileged setup helper from the workload where practical, pass only the finished descriptors or namespace, and drop authority before executing untrusted code.

Kernel and libc support also vary. Feature-detect every required syscall and flag. A fallback to traditional mount(2) should be a conscious compatibility path with its own race and option review, not an automatic retry after any error.

Treat descriptor lifetimes as a transaction

Track each object by state: context created, filesystem created, detached mount created, attributes applied, and target attached. Use close-on-exec on every intermediate descriptor. On failure before attachment, close the detached tree. After attachment, verify the actual mount ID, type, source, and options from mount information rather than trusting only requested inputs.

Test interruption at every stage and make cleanup idempotent. Also test namespace exit, helper crash, unsupported filesystem options, target replacement, recursive submounts, and a kernel without one of the APIs. The goal is not merely to reach a mounted state, but to know exactly which mount became visible and under which policy.

The file-descriptor mount API turns mounting from one overloaded call into an inspectable lifecycle. That gives runtimes better tools for race resistance and rollback. Its safety comes from combining those handles with namespace isolation, privilege minimization, topology validation, and strict error handling.

Related:

Sources:

Comments