The New Mount API

Since Linux 5.2 (2019) the kernel has offered a second way to mount a filesystem, built around a kernel object called the filesystem context (struct fs_context) and a family of system calls — fsopen, fsconfig, fsmount, move_mount, plus fspick and open_tree. Where the venerable mount(2) crams everything — device, target, filesystem type, and a comma-separated string of options — into a single syscall that returns nothing but an errno, the new API breaks mounting into discrete, individually-reportable steps: create a context, feed it parameters one at a time, create the superblock, get a detached mount, graft it onto the tree. The detached-mount idea also gives userspace, for the first time, a fully-configured mount it can inspect before attaching, move atomically, or hand to another namespace. This note covers the syscall family, the fs_context lifecycle, why the old interface was replaced, and the companion query calls statmount/listmount added in 6.8. It builds on Filesystem Registration and Mounting Internals (which owns superblock creation and file_system_type) and The Mount Tree and vfsmount (which owns the vfsmount/mount-tree structure); this note owns the userspace syscall surface and the context state machine.

Why the Old mount(2) Had To Go

The classic mount(2) has the prototype mount(source, target, fstype, mountflags, data). It is, as David Howells (the author of the new API) and Jonathan Corbet put it, a heavily overloaded multiplexer: the same call creates a new mount, remounts (reconfigures) an existing one, makes a bind mount, moves a mount, and changes mount propagation, all selected by bits in mountflags. Four concrete problems drove the redesign (Corbet/Howells, LWN 759499):

  1. The 4096-byte option page. Filesystem-specific options are passed through the opaque data pointer, which the kernel copies as a single page. As LWN notes, “the options for a mount operation must all fit within a single 4096-byte page — the fact that this is a problem for some users is illustrative in its own right.” A filesystem like NFS or a heavily-tuned overlay can run out of room.

  2. No structured error reporting. When a mount fails — a bad option, an unsupported feature, a corrupt superblock — mount(2) can only return a single errno such as EINVAL. Which option was wrong, or why the superblock was rejected, is lost. The kernel “cannot communicate the details of a problem to user space” beyond that one integer (per LWN 759499). Anyone who has stared at mount: wrong fs type, bad option, bad superblock... knows this pain.

  3. All-or-nothing parsing. Because every option arrives at once in the data page, the filesystem must parse the whole blob in one shot. There is no way to validate options incrementally, and the legacy parse_monolithic path treats the page as a comma-separated key[=val] list with no per-item feedback.

  4. No pre-configured, detached superblock. With mount(2) you cannot create and configure a superblock without immediately attaching it to a mount point in the current namespace. You cannot build a mount in one place and atomically install it elsewhere, nor easily share one superblock across several mounts, nor construct a mount inside a container’s future namespace before the namespace exists.

The new API solves all four: parameters arrive one at a time (no page limit, incremental validation), failures leave human-readable strings on a file descriptor you can read(2), and the result of configuration is a detached mount — a fully-formed mount object tied to a file descriptor but not yet visible anywhere — which move_mount then grafts onto the tree.

Mental Model — A Context With a Lifecycle

The pivot of the whole design is struct fs_context. Think of it as a mutable build-order for a mount: you open one, you set fields on it incrementally, you ask the kernel to realize a superblock from it, and finally you turn that into a mount. Internally the kernel tracks a phase field on the context (FS_CONTEXT_CREATE_PARAMSFS_CONTEXT_CREATINGFS_CONTEXT_AWAITING_MOUNT …) and refuses out-of-order operations with -EBUSY (visible in vfs_cmd_create() and fsmount() in fs/fsopen.c and fs/namespace.c, v6.12).

flowchart TD
  A["fsopen for ext4<br/>→ context fd"] --> B["fsconfig SET_STRING<br/>source = /dev/sda1"]
  B --> C["fsconfig SET_STRING<br/>errors = remount-ro"]
  C --> D["fsconfig CMD_CREATE<br/>→ build superblock via get_tree"]
  D -->|error| E["read fd →<br/>e: bad option foo"]
  D -->|ok| F["fsmount fd<br/>→ DETACHED mount fd"]
  F --> G["move_mount mountfd to /mnt<br/>with F_EMPTY_PATH<br/>→ attached to tree"]
  H["open_tree dfd, path,<br/>OPEN_TREE_CLONE"] -.->|alt source of<br/>detached mount| G

The fs_context lifecycle and the two ways to produce a detached mount. What it shows: the linear pipeline — create context, set params one at a time, create superblock, materialize a detached mount, then attach — and that read(fd) is the channel for the structured error strings the old mount(2) never had. The insight: a “detached mount” (from either fsmount or open_tree) is the new currency; move_mount is the single operation that makes it visible in the mount tree.

The Syscall Family, Step by Step

All these calls live at fixed numbers in the syscall table (verified in syscall_64.tbl, v6.12): open_tree=428, move_mount=429, fsopen=430, fsconfig=431, fsmount=432, fspick=433. Note there is no fsinfo syscallfsinfo() was Howells’ proposed query call but it was never merged; the query mechanism that did land is statmount/listmount (numbers 457/458, see below).

Uncertain

The in-kernel doc mount_api.rst (still shipping in v6.12) states the parameter description “will allow the description to be queried from userspace using the fsinfo() syscall.” This is stale documentationfsinfo() was never merged into mainline. The merged query interface is statmount/listmount. Verified against syscall_64.tbl (no fsinfo row) and the statmount/listmount LWN article. uncertain

fsopen — create the context

int fsopen(const char *fsname, unsigned int flags);

fsopen("ext4", FSOPEN_CLOEXEC) looks up the registered file_system_type named "ext4" (returning -ENODEV if no such filesystem is registered or loadable), allocates an fs_context for the mount purpose (fs_context_for_mount), allocates a log buffer for messages, and returns a file descriptor backed by an anonymous inode whose f_op is fscontext_fops. The only flag is FSOPEN_CLOEXEC (value 0x1), which sets close-on-exec; anything else gives -EINVAL. The caller must hold mount capability (may_mount(), i.e. CAP_SYS_ADMIN in the relevant user namespace) or the call returns -EPERM. The context fd is the handle threaded through every subsequent step (all from fs/fsopen.c, v6.12).

fsconfig — set parameters, one at a time

int fsconfig(int fd, unsigned int cmd, const char *key,
             const void *value, int aux);

This is the workhorse. cmd selects what kind of value you are supplying (from enum fsconfig_command in uapi/linux/mount.h, v6.12):

cmdMeaningvalue/aux
FSCONFIG_SET_FLAG (0)A valueless flag, e.g. ro; may be prefixed no to negatevalue NULL, aux 0
FSCONFIG_SET_STRING (1)A string valuevalue = NUL string, aux 0
FSCONFIG_SET_BINARY (2)A binary blob (≤ 1 MiB)value = blob, aux = size
FSCONFIG_SET_PATH (3)An object referenced by pathvalue = path, aux = dirfd or AT_FDCWD
FSCONFIG_SET_PATH_EMPTY (4)As SET_PATH but with AT_EMPTY_PATH semantics
FSCONFIG_SET_FD (5)An object referenced by an open fdaux = fd
FSCONFIG_CMD_CREATE (6)Action: build/reuse a superblockall NULL/0
FSCONFIG_CMD_RECONFIGURE (7)Action: remount (reconfigure)all NULL/0
FSCONFIG_CMD_CREATE_EXCL (8)Action: create a new superblock, fail if one would be reusedall NULL/0

The crucial point is that the source (block device, NFS host:/path, etc.) is itself just a parameter named "source" — you set it with fsconfig(fd, FSCONFIG_SET_STRING, "source", "/dev/sda1", 0). There is no privileged “device” argument as in mount(2). Each call validates that one parameter and can return a fine-grained error; the kernel may also append a message to the context’s log (see error reporting below). FSCONFIG_CMD_CREATE_EXCL (added so callers can detect an unwanted superblock-sharing reuse) is one of several commands that legacy contexts reject with -EOPNOTSUPP (fs/fsopen.c, v6.12).

fsmount — materialize a detached mount

int fsmount(int fs_fd, unsigned int flags, unsigned int attr_flags);

Once FSCONFIG_CMD_CREATE has produced a superblock (the context phase becomes FS_CONTEXT_AWAITING_MOUNT), fsmount turns that superblock’s root into a vfsmount via vfs_create_mount() — but crucially it does not attach it anywhere. Reading fs/namespace.c (v6.12), fsmount allocates a fresh anonymous mount namespace, hangs the new mount in it as the sole root, opens an O_PATH file on it, and marks the file FMODE_NEED_UNMOUNT. That last flag means: if you simply close this fd without ever attaching the mount, the kernel dissolves and unmounts it for you. The attr_flags argument carries MOUNT_ATTR_* bits — MOUNT_ATTR_RDONLY, MOUNT_ATTR_NOSUID, MOUNT_ATTR_NODEV, MOUNT_ATTR_NOEXEC, the atime triplet (RELATIME/NOATIME/STRICTATIME), MOUNT_ATTR_NODIRATIME, MOUNT_ATTR_NOSYMFOLLOW, and MOUNT_ATTR_IDMAP for idmapped mounts — so per-mount semantics are set here, separately from the superblock-wide SB_* flags. The returned fd behaves like an O_PATH descriptor: you can already use it as a dirfd for *at() syscalls before the mount is visible in the tree (per fsopen(2) man page).

move_mount — attach the detached mount to the tree

int move_mount(int from_dfd, const char *from_path,
               int to_dfd, const char *to_path, unsigned int flags);

move_mount is the single operation that makes a detached mount visible, or that relocates an existing one (it is also the modern equivalent of mount --move). To attach an fsmount result you call it with the mount fd as from_dfd, an empty from_path, and MOVE_MOUNT_F_EMPTY_PATH, naming the target with to_dfd/to_path:

move_mount(mountfd, "", AT_FDCWD, "/mnt/data", MOVE_MOUNT_F_EMPTY_PATH);

The flags split into source-side (MOVE_MOUNT_F_*) and target-side (MOVE_MOUNT_T_*) controls for following symlinks (*_SYMLINKS), triggering automounts (*_AUTOMOUNTS), and permitting an empty path (*_EMPTY_PATH). Two special flags matter for current behavior (from uapi/linux/mount.h, v6.12, and move_mount(2)): MOVE_MOUNT_SET_GROUP (since Linux 5.15) sets the propagation sharing group instead of moving, and MOVE_MOUNT_BENEATH (since Linux 6.5) inserts the new mount beneath the current top mount on the mount stack rather than on top of it — useful for swapping a filesystem under a running mount without unmounting. The mask MOVE_MOUNT__MASK is 0x377.

open_tree and fspick — the other entry points

open_tree(int dfd, const char *filename, unsigned int flags) has two modes. Plain, it returns an O_PATH fd referring to an existing mount (like open(O_PATH) but mount-aware). With OPEN_TREE_CLONE it creates a detached copy of the subtree — “equivalent to a bind-mount that would be created by mount(2) with MS_BIND, except that it is tied to a file descriptor and is not mounted onto the filesystem” (open_tree(2)). Add AT_RECURSIVE (only valid with OPEN_TREE_CLONE) for a recursive clone, like mount --rbind. So open_tree is the second producer of detached mounts — it clones from the existing tree, whereas fsmount builds from a fresh superblock; both feed move_mount.

fspick(int dfd, const char *path, unsigned int flags) is the reconfiguration counterpart of fsopen: instead of creating a brand-new context for a new filesystem, it grabs the existing superblock at a path into a FS_CONTEXT_FOR_RECONFIGURE context. You then fsconfig new options and issue FSCONFIG_CMD_RECONFIGURE — the new-API way to do a remount, replacing mount(MS_REMOUNT). It requires the path to be a mount root (-EINVAL otherwise) and supports FSPICK_CLOEXEC, FSPICK_SYMLINK_NOFOLLOW, FSPICK_NO_AUTOMOUNT, FSPICK_EMPTY_PATH (fs/fsopen.c, v6.12).

Structured Error Reporting — the readable fd

This is the feature that most directly fixes problem #2. The fs_context fd is readable. Each read(2) returns one queued message, prefixed by a single-character class tag: e for error, w for warning, i for informational (fsopen(2)). The mechanism is fscontext_read() in fs/fsopen.c (v6.12): the context owns a small ring of message buffers (fc->log), and a read pops the oldest entry, copies it out, and returns its length — or -ENODATA when the ring is empty. So a typical failure flow is: fsconfig(..., FSCONFIG_CMD_CREATE, ...) returns -EINVAL, then read(fd, buf, len) yields something like e ext4: Unrecognized mount option "foo" — the exact offending option, which mount(2) could never tell you.

Worked Example — mounting tmpfs with the new API

/* Mount a tmpfs sized 64 MiB at /mnt/scratch, new-API style. */
int fd = fsopen("tmpfs", FSOPEN_CLOEXEC);            /* 1. context for tmpfs   */
fsconfig(fd, FSCONFIG_SET_STRING, "size", "64M", 0);  /* 2. one option at a time */
fsconfig(fd, FSCONFIG_SET_STRING, "mode", "1777", 0); /*    another option        */
if (fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) /* 3. build the superblock  */
        drain_errors(fd);                             /*    read(fd) for messages */
int mnt = fsmount(fd, FSMOUNT_CLOEXEC, 0);            /* 4. detached mount object */
move_mount(mnt, "", AT_FDCWD, "/mnt/scratch",         /* 5. graft onto the tree   */
           MOVE_MOUNT_F_EMPTY_PATH);
close(mnt); close(fd);

Line-by-line: (1) fsopen gives a context fd specific to tmpfs — note no device is named because tmpfs is backed by RAM and swap, not a block device. (2) each fsconfig(FSCONFIG_SET_STRING, …) sets one tmpfs option; if "size" were malformed this single call fails, not the whole mount. (3) FSCONFIG_CMD_CREATE invokes the filesystem’s get_tree op (the superblock-creation machinery owned by Filesystem Registration and Mounting Internals); on failure you drain the message ring. (4) fsmount produces the detached mount fd. (5) move_mount with MOVE_MOUNT_F_EMPTY_PATH and an empty source path attaches it at /mnt/scratch. Closing mnt after a successful move_mount is fine — the mount is now anchored in the tree; closing it before would trigger the FMODE_NEED_UNMOUNT auto-dissolution.

Querying Mounts — statmount and listmount

The old way to ask “what is mounted, and how?” is to parse the text of /proc/self/mountinfo. That scales badly: on a container host the file can list thousands of lines, you cannot query a single mount without reading the whole file, and the textual format is awkward and racy (per Corbet, LWN 950569, which notes that “as this virtual file gets longer, it becomes harder for system-management tools (and humans too) to work with”). Two syscalls merged in Linux 6.8 (March 2024 — confirmed by Phoronix and kernelnewbies; syscall numbers 457/458) fix this:

  • listmount(const struct mnt_id_req *req, u64 *mnt_ids, size_t nr, unsigned flags) returns the 64-bit unique IDs of the child mounts of a given mount. Pass the special parent ID LSMT_ROOT (0xffff…ffff) to start at the namespace root; LISTMOUNT_REVERSE lists later mounts first. The v6.12 implementation walks the namespace’s red-black tree of mounts (do_listmount in fs/namespace.c) and caps the request at one million IDs.
  • statmount(const struct mnt_id_req *req, struct statmount *buf, size_t bufsize, unsigned flags) fills a struct statmount for one mount, in the style of statx: the caller sets a mask of STATMOUNT_* bits to request specific groups, and the kernel reports which it filled. The v6.12 struct statmount carries sb_dev_major/minor, sb_magic, sb_flags, mnt_id/mnt_parent_id (and the old reused IDs), mnt_attr, mnt_propagation, peer-group/master IDs, plus offsets into a trailing string area for fs_type, mnt_root, mnt_point, and mnt_opts. The v6.12 mask set is eight bits: STATMOUNT_SB_BASIC, MNT_BASIC, PROPAGATE_FROM, MNT_ROOT, MNT_POINT, FS_TYPE, MNT_NS_ID, MNT_OPTS (from uapi/linux/mount.h, v6.12).

Both calls can operate across mount namespaces (you supply a mnt_ns_id in mnt_id_req, gated by CAP_SYS_ADMIN over the target namespace). Christian Brauner’s walk-through (Brauner, 2024) shows the full pattern for enumerating every mount in every namespace: the 64-bit mount IDs are never recycled, and three nsfs ioctls added in Linux 6.12 — NS_MNT_GET_INFO, NS_MNT_GET_NEXT, NS_MNT_GET_PREV — let a tool walk from one mount namespace to the next, calling listmount/statmount within each.

Uncertain

The struct statmount field set and the STATMOUNT_* mask grew substantially after 6.12. The current statmount(2) man page (man-pages tarball dated 2025-12-25) documents extra fields — fs_subtype, sb_source, opt_array, opt_sec_array, mnt_uidmap, mnt_gidmap — and ~15 mask bits, introduced across Linux 6.11/6.13/6.15. The eight-bit set and the struct statmount layout above are pinned to v6.12 and will be incomplete on newer kernels. To resolve: re-read uapi/linux/mount.h at the kernel version in use. uncertain

Failure Modes and Common Misunderstandings

  • Out-of-order operations give -EBUSY, not a clear message. The phase state machine rejects, e.g., calling fsmount before FSCONFIG_CMD_CREATE (fc->phase != FS_CONTEXT_AWAITING_MOUNT-EINVAL/-EBUSY in fsmount, v6.12). Beginners forget the explicit FSCONFIG_CMD_CREATE step because with mount(2) superblock creation is implicit.
  • Forgetting to attach leaks nothing but unmounts silently. Because the fsmount fd is FMODE_NEED_UNMOUNT, closing it without move_mount simply tears the mount down — which surprises people expecting a leak or an orphaned mount. This is a feature, not a bug.
  • -ENODEV from fsopen is a filesystem-name problem, not a device problem. fsopen("etx4", …) (typo) fails at get_fs_type, long before any device is involved — the device is just a later "source" parameter.
  • Legacy filesystems silently restrict commands. A filesystem that has not been converted to provide an init_fs_context op uses legacy_fs_context_ops; against such a context, FSCONFIG_SET_BINARY/PATH/FD and FSCONFIG_CMD_CREATE_EXCL return -EOPNOTSUPP (fs/fsopen.c, v6.12). Most in-tree filesystems have been converted, but out-of-tree or older ones may not be.
  • The error ring is small and lossy. Messages live in a fixed-size ring (fc->log); if you do not drain it, new messages can displace old ones. Read after every failing fsconfig/CMD_CREATE.

Alternatives and When To Choose Them

  • Plain mount(2) is still fully supported and is what almost all software uses today; libmount/util-linux and most container runtimes historically called it. Choose the new API when you need structured errors, a pre-configured detached mount to move atomically across namespaces, idmapped mounts (MOUNT_ATTR_IDMAP, which is far cleaner via fsmount/mount_setattr), or to avoid the 4 KiB option page.
  • mount_setattr(2) (syscall 442, verified in syscall_64.tbl, v6.12; added in Linux 5.12 per its man page) is the companion for changing per-mount attributes (read-only, propagation, idmapping) on an already-attached subtree, recursively — it complements, not replaces, this family.
  • For querying, statmount/listmount supersede /proc/self/mountinfo parsing where available (6.8+), but the proc file remains the portable fallback on older kernels.

Production Notes

systemd adopted the new mount API internally and mount.util-linux/libmount gained support for it; container runtimes use open_tree(OPEN_TREE_CLONE) + move_mount to clone and relocate mount trees when constructing a container’s filesystem view, and fsmount + MOUNT_ATTR_IDMAP is the mechanism behind idmapped mounts that let a container map host UIDs/GIDs without chown-ing the backing files. Christian Brauner’s write-up on listing every mount in every namespace (brauner.io, 2024) is the canonical demonstration of listmount/statmount at scale (note: that post dates the syscalls to “v6.9” whereas Phoronix, kernelnewbies, and LWN 950569 place the initial merge in 6.8; the cross-namespace mnt_ns_id support and the nsfs iteration ioctls landed slightly later, through 6.11/6.12, which likely explains the discrepancy). The fspick + FSCONFIG_CMD_RECONFIGURE path is the new-API remount and is gradually replacing MS_REMOUNT in tooling.

See Also