mirror of
https://github.com/libfuse/libfuse.git
synced 2026-07-07 14:47:41 +08:00
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:
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user