fusermount: fix sync-init TOCTOU by mounting on a pinned mountpoint fd

The --sync-init code path validates the mountpoint in
mount_fuse_prepare() (check_perm() chdir()s into the directory and
rewrites the path to ".", or opens a regular-file mountpoint and
rewrites it to /proc/self/fd/N), but that pinned reference is a local
variable that is discarded. mount_fuse_finish_fsmount() later mounts
using the original path string, which move_mount() re-resolves through
whatever the symlinks now say.

An unprivileged user controls the sync-init socket, so
wait_for_signal() gives an unbounded check-to-use window: validate an
attacker-owned directory, swap a parent component to a symlink into
/etc, then signal -- the mount lands on a root-owned directory,
defeating check_nonroot_dir_access(). With user_allow_other this yields
local root via a fake sudoers drop-in.

Pin the validated inode as a single fd (mount_context.mnt_fd) obtained
from the already-chdir'd CWD or the open mountpoint fd, mount onto it
via move_mount() with MOVE_MOUNT_T_EMPTY_PATH (no path re-resolution),
and assert at the mount site that fstat(mnt_fd) still matches the stat
captured during validation. The legacy mount() path and the library
direct-mount path are unchanged (mnt_fd == -1 keeps path resolution).

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
This commit is contained in:
Bernd Schubert
2026-06-11 11:29:27 +02:00
parent b6cb345591
commit bad8b22c91
4 changed files with 101 additions and 28 deletions
+2 -1
View File
@@ -633,7 +633,8 @@ int fuse_kern_fsmount_mo(const char *mnt, const struct mount_opts *mo,
/* codeql[cpp/path-injection] verification is in the function */
const char *devname = fuse_mnt_get_devname();
return fuse_kern_fsmount(mnt, mo->flags, mo->blkdev, mo->fsname,
/* in-process direct mount: no suid boundary, resolve by path (mnt_fd -1) */
return fuse_kern_fsmount(mnt, -1, mo->flags, mo->blkdev, mo->fsname,
mo->subtype, devname, mo->kernel_opts,
mtab_opts);
}
+17 -13
View File
@@ -294,16 +294,15 @@ int apply_fsconfig_mount_opts(int fsfd, const char *opts)
return 0;
}
int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
const char *fsname, const char *subtype,
int fuse_kern_fsmount(const char *mnt, int dest_mnt_fd, unsigned long flags,
int blkdev, const char *fsname, const char *subtype,
const char *source_dev, const char *kernel_opts,
const char *mtab_opts)
{
char *type = NULL;
char *source = NULL;
int fsfd = -1;
int mntfd = -1;
int mountfd = -1;
int err, res;
unsigned int mount_attrs;
@@ -382,8 +381,8 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
}
/* Create mount object with mount attributes */
mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs);
if (mntfd == -1) {
mountfd = fsmount(fsfd, FSMOUNT_CLOEXEC, mount_attrs);
if (mountfd == -1) {
err = -errno;
log_fsconfig_kmsg(fsfd);
fprintf(stderr, "fuse: fsmount failed: %s\n",
@@ -394,9 +393,14 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
close(fsfd);
fsfd = -1;
/* Attach to mount point */
if (move_mount(mntfd, "", AT_FDCWD, mnt, MOVE_MOUNT_F_EMPTY_PATH) ==
-1) {
if (dest_mnt_fd >= 0)
res = move_mount(mountfd, "", dest_mnt_fd, "",
MOVE_MOUNT_F_EMPTY_PATH |
MOVE_MOUNT_T_EMPTY_PATH);
else
res = move_mount(mountfd, "", AT_FDCWD, mnt,
MOVE_MOUNT_F_EMPTY_PATH);
if (res == -1) {
err = -errno;
fprintf(stderr, "fuse: move_mount failed: %s\n",
strerror(errno));
@@ -407,7 +411,7 @@ int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
if (err == -1)
goto out_umount;
close(mntfd);
close(mountfd);
free(source);
free(type);
return 0;
@@ -417,7 +421,7 @@ out_umount:
/* race free umount */
char fd_path[64];
snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mntfd);
snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mountfd);
if (umount2(fd_path, MNT_DETACH) == -1 && errno != EINVAL) {
fprintf(stderr,
"fuse: cleanup umount failed: %s\n",
@@ -425,8 +429,8 @@ out_umount:
}
}
out_close_mntfd:
if (mntfd != -1)
close(mntfd);
if (mountfd != -1)
close(mountfd);
out_free:
free(source);
free(type);
+7 -3
View File
@@ -37,7 +37,11 @@ int fuse_kern_mount_get_base_mtab_opts(const struct mount_opts *mo,
/**
* Mount using the new Linux mount API (fsopen/fsconfig/fsmount/move_mount)
* @mnt: mountpoint
* @mnt: mountpoint, used for the /etc/mtab record (and as the move_mount
* target when @mnt_fd is -1)
* @dest_mnt_fd: pre-resolved mountpoint fd to mount
* or -1 to resolve @mnt by path. A pinned fd closes the suid
* fusermount sync-init TOCTOU; in-process direct-mount callers pass -1.
* @flags: mount flags (MS_NOSUID, MS_NODEV, etc.)
* @blkdev: 1 for fuseblk, 0 for fuse
* @fsname: filesystem name (or NULL)
@@ -52,8 +56,8 @@ int fuse_kern_mount_get_base_mtab_opts(const struct mount_opts *mo,
*
* Returns: 0 on success, -1 on failure with errno set
*/
int fuse_kern_fsmount(const char *mnt, unsigned long flags, int blkdev,
const char *fsname, const char *subtype,
int fuse_kern_fsmount(const char *mnt, int mnt_fd, unsigned long flags,
int blkdev, const char *fsname, const char *subtype,
const char *source_dev, const char *kernel_opts,
const char *mtab_opts);
+73 -9
View File
@@ -19,7 +19,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>
@@ -931,6 +930,40 @@ static int do_mount(const char *mnt, const char **typep, mode_t rootmode,
return 0;
}
/*
* Pin @open_path (the validated mountpoint -- "." after chdir, or the path
* itself for root) as an O_PATH fd and fstat() it into @stbuf. Verify the
* pinned inode still has @stbuf's pre-pin owner: done on the held fd this is
* immune to a symlink swap of @open_path between validation and the mount, so
* a path redirected to a differently-owned inode is rejected. @name is the
* user-facing path for diagnostics. Returns the fd, or -1 on failure.
*/
static int pin_mountpoint(const char *open_path, const char *name,
uid_t want_uid, struct stat *stbuf)
{
int fd = open(open_path, O_PATH | O_CLOEXEC);
if (fd == -1) {
fprintf(stderr, "%s: failed to pin mountpoint %s: %s\n",
progname, name, strerror(errno));
return -1;
}
if (fstat(fd, stbuf) == -1) {
fprintf(stderr, "%s: failed to access mountpoint %s: %s\n",
progname, name, strerror(errno));
close(fd);
return -1;
}
if (want_uid != (uid_t)-1 && stbuf->st_uid != want_uid) {
fprintf(stderr,
"%s: mountpoint %s changed owner between check and mount\n",
progname, name);
close(fd);
return -1;
}
return fd;
}
static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd)
{
int res;
@@ -945,9 +978,18 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd)
return -1;
}
/* No permission checking is done for root */
if (getuid() == 0)
/*
* Root skips the permission checks, but still pin the mountpoint inode:
* external tools may invoke this suid helper as root, and the pinned fd
* makes move_mount() target exactly the validated inode regardless of a
* later symlink swap.
*/
if (getuid() == 0) {
*mountpoint_fd = pin_mountpoint(mnt, mnt, (uid_t)-1, stbuf);
if (*mountpoint_fd == -1)
return -1;
return 0;
}
if (S_ISDIR(stbuf->st_mode)) {
res = chdir(mnt);
@@ -969,6 +1011,13 @@ static int check_perm(const char **mntp, struct stat *stbuf, int *mountpoint_fd)
res = check_nonroot_dir_access(progname, origmnt, mnt, stbuf);
if (res)
return res;
/* Reached only for non-root; root returned above. CWD is the
* just-validated directory after chdir.
*/
*mountpoint_fd = pin_mountpoint(".", origmnt, getuid(), stbuf);
if (*mountpoint_fd == -1)
return -1;
} else if (S_ISREG(stbuf->st_mode)) {
static char procfile[256];
*mountpoint_fd = open(mnt, O_WRONLY);
@@ -1041,6 +1090,10 @@ struct mount_context {
char *mtab_opts; /* mtab/utab record string for add_mount() */
char *x_opts;
char *kern_mnt_opts; /* user-provided -o opts with x-* removed */
/* Pinned mountpoint inode, resolved once during check_perm(); the
* move_mount() target. Immune to symlink swaps. -1 if unset.
*/
int mnt_fd;
};
/*
@@ -1055,6 +1108,7 @@ static int mount_fuse_prepare(const char *mnt, const char *opts,
const char *real_mnt = mnt;
memset(ctx, 0, sizeof(*ctx));
ctx->mnt_fd = -1;
ctx->dev = fuse_mnt_get_devname();
@@ -1083,11 +1137,19 @@ static int mount_fuse_prepare(const char *mnt, const char *opts,
res = check_perm(&real_mnt, &ctx->stbuf, &mountpoint_fd);
restore_privs();
if (res == -1) {
if (mountpoint_fd != -1)
close(mountpoint_fd);
if (res == -1)
goto fail_close_fd;
}
/*
* check_perm() pinned the validated inode (directory or regular file,
* root or not) as mountpoint_fd, so the mount targets exactly that inode
* regardless of later symlink swaps of the path. The fd stays open until
* after the mount.
*/
ctx->mnt_fd = mountpoint_fd;
return ctx->fd;
@@ -1141,10 +1203,10 @@ static int mount_fuse_finish_fsmount(const char *mnt,
final_mtab_opts = x_mtab_opts;
}
/* Use new mount API */
res = fuse_kern_fsmount(mnt, mp.flags, mp.blkdev,
mp.fsname, mp.subtype, ctx->dev,
mp.optbuf, final_mtab_opts);
/* Use new mount API; mount onto the pinned fd, not the path string */
res = fuse_kern_fsmount(mnt, ctx->mnt_fd, mp.flags, mp.blkdev,
mp.fsname, mp.subtype, ctx->dev, mp.optbuf,
final_mtab_opts);
if (res == -1)
goto fail_free_merged;
@@ -1332,6 +1394,8 @@ static int mount_fuse_sync_init(const char *mnt, const char *opts,
out:
close(fd);
if (ctx.mnt_fd != -1)
close(ctx.mnt_fd);
free(ctx.source);
free(ctx.mtab_opts);
free(ctx.x_opts);