Files
libfuse/util/mount.fuse.c
T
Bernd Schubert 3fedbac9b6 mount.fuse: clear error and test skip when binary unreachable after privilege drop
With drop_privileges, mount.fuse3 execs the file system binary only
after dropping all capabilities (CAP_DAC_OVERRIDE included) while
keeping uid 0. The kernel then applies plain DAC checks to the exec, so
a binary under a directory the capability-less process cannot traverse
(e.g. a user-owned mode-0700 home) fails with EACCES and /bin/sh prints
a bare 'Permission denied', surfacing in the tests as 'file system
process terminated prematurely'.

Resolving and opening the binary before the drop was rejected: it would
perform a privileged path resolution and open() on invoker-influenced
input (confused-deputy / privilege-escalation risk). The drop-first,
resolve-after order is preserved.

Instead, pre-flight the upcoming PATH lookup after the drop and fail
with a diagnostic that names the binary and points at mount.fuse3(8).
The check runs with the final credentials, so it is deny-only. The test
detects the same situation up front (uid/gid-aware traversal check) and
skips with a precise reason; root-owned CI checkouts keep running.

Signed-off-by: Bernd Schubert <bernd@bsbernd.com>
2026-06-12 12:08:57 +02:00

648 lines
15 KiB
C

/*
FUSE: Filesystem in Userspace
Copyright (C) 2001-2007 Miklos Szeredi <miklos@szeredi.hu>
This program can be distributed under the terms of the GNU GPLv2.
See the file GPL2.txt.
*/
/* For environ */
#define _GNU_SOURCE
#include "fuse_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/wait.h>
#ifdef HAVE_SERVICEMOUNT
#include <spawn.h>
#endif
#ifdef linux
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/capability.h>
#include <linux/securebits.h>
/* for 2.6 kernels */
#if !defined(SECBIT_KEEP_CAPS) && defined(SECURE_KEEP_CAPS)
#define SECBIT_KEEP_CAPS (issecure_mask(SECURE_KEEP_CAPS))
#endif
#if !defined(SECBIT_KEEP_CAPS_LOCKED) && defined(SECURE_KEEP_CAPS_LOCKED)
#define SECBIT_KEEP_CAPS_LOCKED (issecure_mask(SECURE_KEEP_CAPS_LOCKED))
#endif
#if !defined(SECBIT_NO_SETUID_FIXUP) && defined(SECURE_NO_SETUID_FIXUP)
#define SECBIT_NO_SETUID_FIXUP (issecure_mask(SECURE_NO_SETUID_FIXUP))
#endif
#if !defined(SECBIT_NO_SETUID_FIXUP_LOCKED) && defined(SECURE_NO_SETUID_FIXUP_LOCKED)
#define SECBIT_NO_SETUID_FIXUP_LOCKED (issecure_mask(SECURE_NO_SETUID_FIXUP_LOCKED))
#endif
#if !defined(SECBIT_NOROOT) && defined(SECURE_NOROOT)
#define SECBIT_NOROOT (issecure_mask(SECURE_NOROOT))
#endif
#if !defined(SECBIT_NOROOT_LOCKED) && defined(SECURE_NOROOT_LOCKED)
#define SECBIT_NOROOT_LOCKED (issecure_mask(SECURE_NOROOT_LOCKED))
#endif
#endif
/* linux < 3.5 */
#ifndef PR_SET_NO_NEW_PRIVS
#define PR_SET_NO_NEW_PRIVS 38
#endif
#include "fuse.h"
#ifdef HAVE_SERVICEMOUNT
# include "mount_service.h"
#endif
static char *progname;
static char *xstrdup(const char *s)
{
char *t = strdup(s);
if (!t) {
fprintf(stderr, "%s: failed to allocate memory\n", progname);
exit(1);
}
return t;
}
static void *xrealloc(void *oldptr, size_t size)
{
void *ptr = realloc(oldptr, size);
if (!ptr) {
fprintf(stderr, "%s: failed to allocate memory\n", progname);
exit(1);
}
return ptr;
}
static void add_arg(char **cmdp, const char *opt)
{
size_t optlen = strlen(opt);
size_t cmdlen = *cmdp ? strlen(*cmdp) : 0;
if (optlen >= (SIZE_MAX - cmdlen - 4)/4) {
fprintf(stderr, "%s: argument too long\n", progname);
exit(1);
}
char *cmd = xrealloc(*cmdp, cmdlen + optlen * 4 + 4);
char *s;
s = cmd + cmdlen;
if (*cmdp)
*s++ = ' ';
*s++ = '\'';
for (; *opt; opt++) {
if (*opt == '\'') {
*s++ = '\'';
*s++ = '\\';
*s++ = '\'';
*s++ = '\'';
} else
*s++ = *opt;
}
*s++ = '\'';
*s = '\0';
*cmdp = cmd;
}
static char *add_option(const char *opt, char *options)
{
int oldlen = options ? strlen(options) : 0;
size_t newsize = oldlen + 1 + strlen(opt) + 1;
options = xrealloc(options, newsize);
if (!oldlen) {
strncpy(options, opt, newsize - 1);
options[newsize - 1] = '\0';
} else {
strncat(options, ",", newsize - strlen(options) - 1);
strncat(options, opt, newsize - strlen(options) - 1);
}
return options;
}
static int prepare_fuse_fd(const char *mountpoint, const char* subtype,
const char *options)
{
int fuse_fd = -1;
int flags = -1;
int subtype_len = strlen(subtype) + 9;
char* options_copy = xrealloc(NULL, subtype_len);
snprintf(options_copy, subtype_len, "subtype=%s", subtype);
options_copy = add_option(options, options_copy);
fuse_fd = fuse_open_channel(mountpoint, options_copy);
if (fuse_fd == -1) {
exit(1);
}
flags = fcntl(fuse_fd, F_GETFD);
if (flags == -1 || fcntl(fuse_fd, F_SETFD, flags & ~FD_CLOEXEC) == -1) {
fprintf(stderr, "%s: Failed to clear CLOEXEC: %s\n",
progname, strerror(errno));
exit(1);
}
return fuse_fd;
}
#ifdef linux
static uint64_t get_capabilities(void)
{
/*
* This invokes the capset syscall directly to avoid the libcap
* dependency, which isn't really justified just for this.
*/
struct __user_cap_header_struct header = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = 0,
};
struct __user_cap_data_struct data[2];
memset(data, 0, sizeof(data));
if (syscall(SYS_capget, &header, data) == -1) {
fprintf(stderr, "%s: Failed to get capabilities: %s\n",
progname, strerror(errno));
exit(1);
}
return data[0].effective | ((uint64_t) data[1].effective << 32);
}
static void set_capabilities(uint64_t caps)
{
/*
* This invokes the capset syscall directly to avoid the libcap
* dependency, which isn't really justified just for this.
*/
struct __user_cap_header_struct header = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = 0,
};
struct __user_cap_data_struct data[2];
memset(data, 0, sizeof(data));
data[0].effective = data[0].permitted = caps;
data[1].effective = data[1].permitted = caps >> 32;
if (syscall(SYS_capset, &header, data) == -1) {
fprintf(stderr, "%s: Failed to set capabilities: %s\n",
progname, strerror(errno));
exit(1);
}
}
static void drop_and_lock_capabilities(void)
{
/* Set and lock securebits. */
if (prctl(PR_SET_SECUREBITS,
SECBIT_KEEP_CAPS_LOCKED |
SECBIT_NO_SETUID_FIXUP |
SECBIT_NO_SETUID_FIXUP_LOCKED |
SECBIT_NOROOT |
SECBIT_NOROOT_LOCKED) == -1) {
fprintf(stderr, "%s: Failed to set securebits %s\n",
progname, strerror(errno));
exit(1);
}
/* Clear the capability bounding set. */
int cap;
for (cap = 0; ; cap++) {
int cap_status = prctl(PR_CAPBSET_READ, cap);
if (cap_status == 0) {
continue;
}
if (cap_status == -1 && errno == EINVAL) {
break;
}
if (cap_status != 1) {
fprintf(stderr,
"%s: Failed to get capability %u: %s\n",
progname, cap, strerror(errno));
exit(1);
}
if (prctl(PR_CAPBSET_DROP, cap) == -1) {
fprintf(stderr,
"%s: Failed to drop capability %u: %s\n",
progname, cap, strerror(errno));
}
}
/* Drop capabilities. */
set_capabilities(0);
/* Prevent re-acquisition of privileges. */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
fprintf(stderr, "%s: Failed to set no_new_privs: %s\n",
progname, strerror(errno));
exit(1);
}
}
/*
* Mirror the PATH lookup /bin/sh is about to do. Returns 0 if the binary is
* reachable and executable, otherwise a negative errno explaining why
* (-EACCES = permission denied, -ENOENT = not found). Must run after
* drop_and_lock_capabilities(): with no capabilities left and
* SECBIT_NO_SETUID_FIXUP set, access(X_OK) is a plain DAC check matching
* the upcoming execve, so this is deny-only -- it cannot grant access the
* exec would be refused, only report it earlier with a better message.
*/
/* access(X_OK), with errno mapped to a negative errno (-ENOENT = "not found"). */
static int access_executable(const char *path)
{
if (access(path, X_OK) == 0)
return 0;
return errno == EACCES ? -EACCES : -ENOENT;
}
static int check_binary_access(const char *name)
{
const char *path_env, *seg;
char test_path[PATH_MAX];
int err = -ENOENT;
/* A name containing '/' is run verbatim by the shell, no PATH search. */
if (strchr(name, '/'))
return access_executable(name);
path_env = getenv("PATH");
if (!path_env)
return -ENOENT;
/* Walk PATH segment by segment without copying it (no strtok). */
for (seg = path_env; *seg; ) {
size_t dir_len = strcspn(seg, ":");
/* skip empty and oversized entries */
if (dir_len &&
snprintf(test_path, sizeof(test_path), "%.*s/%s",
(int)dir_len, seg, name) < (int)sizeof(test_path)) {
int ret = access_executable(test_path);
if (ret == 0)
return 0;
if (ret == -EACCES)
err = -EACCES;
}
seg += dir_len;
if (*seg == ':')
seg++;
}
return err;
}
#endif
#ifdef HAVE_SERVICEMOUNT
#define FUSERVICEMOUNT_PROG "fuservicemount3"
static int mount_service_child(char **argv)
{
const char *full_path = FUSERVICEMOUNT_DIR "/" FUSERVICEMOUNT_PROG;
pid_t child_pid;
int child_status;
int ret;
/*
* First try the install path, then a system install, just like we do
* for fusermount. See man 7 environ for the global environ pointer.
*/
ret = posix_spawn(&child_pid, full_path, NULL, NULL,
(char *const *)argv, environ);
if (ret)
ret = posix_spawnp(&child_pid, FUSERVICEMOUNT_PROG, NULL, NULL,
(char * const *)argv, environ);
if (ret) {
fprintf(stderr, "%s: could not start %s helper: %s\n",
argv[0], FUSERVICEMOUNT_PROG, strerror(ret));
return MOUNT_SERVICE_FALLBACK_NEEDED;
}
do {
ret = waitpid(child_pid, &child_status, 0);
} while (ret < 0 && errno == EINTR);
if (ret < 0) {
fprintf(stderr, "%s: could not wait for %s helper: %s\n",
argv[0], FUSERVICEMOUNT_PROG, strerror(errno));
return MOUNT_SERVICE_FALLBACK_NEEDED;
}
if (WIFEXITED(child_status))
return WEXITSTATUS(child_status);
/* terminated due to signal or coredump */
return EXIT_FAILURE;
}
static int try_service_main(const char *argv0, const char *fstype,
const char *source, const char *mountpoint,
const char *options)
{
struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
int ret;
if (!mount_service_present(fstype))
return MOUNT_SERVICE_FALLBACK_NEEDED;
/* This can be an empty string if "mount.fuse3 null# /tmp/a" */
if (source && source[0] == 0)
source = NULL;
ret = fuse_opt_add_arg(&args, argv0);
if (ret)
goto out;
if (source) {
ret = fuse_opt_add_arg(&args, source);
if (ret)
goto out;
}
ret = fuse_opt_add_arg(&args, mountpoint);
if (ret)
goto out;
ret = fuse_opt_add_arg(&args, "-t");
if (ret)
goto out;
ret = fuse_opt_add_arg(&args, fstype);
if (ret)
goto out;
if (options) {
ret = fuse_opt_add_arg(&args, "-o");
if (ret)
goto out;
ret = fuse_opt_add_arg(&args, options);
if (ret)
goto out;
}
/* If we're root, just do the mount directly. */
if (getuid() != 0)
ret = mount_service_child(args.argv);
else
ret = mount_service_main(args.argc, args.argv);
out:
fuse_opt_free_args(&args);
return ret;
}
#endif
int main(int argc, char *argv[])
{
char *type = NULL;
char *source;
char *dup_source = NULL;
const char *mountpoint;
char *basename;
char *options = NULL;
char *command = NULL;
char *setuid_name = NULL;
int i;
int dev = 1;
int suid = 1;
int pass_fuse_fd = 0;
int fuse_fd = 0;
int drop_privileges = 0;
char *dev_fd_mountpoint = NULL;
progname = argv[0];
basename = strrchr(argv[0], '/');
if (basename)
basename++;
else
basename = argv[0];
if (strncmp(basename, "mount.fuse.", 11) == 0)
type = basename + 11;
if (strncmp(basename, "mount.fuseblk.", 14) == 0)
type = basename + 14;
if (type && !type[0])
type = NULL;
if (argc < 3) {
fprintf(stderr,
"usage: %s %s destination [-t type] [-o opt[,opts...]]\n",
progname, type ? "source" : "type#[source]");
exit(1);
}
source = argv[1];
if (source && !source[0])
source = NULL;
mountpoint = argv[2];
for (i = 3; i < argc; i++) {
if (strcmp(argv[i], "-t") == 0) {
i++;
if (i == argc) {
fprintf(stderr,
"%s: missing argument to option '-t'\n",
progname);
exit(1);
}
type = argv[i];
if (strncmp(type, "fuse.", 5) == 0)
type += 5;
else if (strncmp(type, "fuseblk.", 8) == 0)
type += 8;
if (!type[0]) {
fprintf(stderr,
"%s: empty type given as argument to option '-t'\n",
progname);
exit(1);
}
}
}
if (!type) {
if (source) {
dup_source = xstrdup(source);
type = dup_source;
source = strchr(type, '#');
if (source)
*source++ = '\0';
if (!type[0]) {
fprintf(stderr, "%s: empty filesystem type\n",
progname);
exit(1);
}
} else {
fprintf(stderr, "%s: empty source\n", progname);
exit(1);
}
}
for (i = 3; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
continue;
} else if (strcmp(argv[i], "-o") == 0) {
char *opts;
const char *opt;
i++;
if (i == argc)
break;
opts = xstrdup(argv[i]);
opt = strtok(opts, ",");
while (opt) {
int j;
int ignore = 0;
const char *ignore_opts[] = { "",
"user",
"nofail",
"nouser",
"users",
"auto",
"noauto",
"_netdev",
NULL};
if (strncmp(opt, "setuid=", 7) == 0) {
setuid_name = xstrdup(opt + 7);
ignore = 1;
} else if (strcmp(opt,
"drop_privileges") == 0) {
pass_fuse_fd = 1;
drop_privileges = 1;
ignore = 1;
}
for (j = 0; ignore_opts[j]; j++)
if (strcmp(opt, ignore_opts[j]) == 0)
ignore = 1;
if (!ignore) {
if (strcmp(opt, "nodev") == 0)
dev = 0;
else if (strcmp(opt, "nosuid") == 0)
suid = 0;
options = add_option(opt, options);
}
opt = strtok(NULL, ",");
}
free(opts);
}
}
if (drop_privileges) {
uint64_t required_caps = CAP_TO_MASK(CAP_SETPCAP) |
CAP_TO_MASK(CAP_SYS_ADMIN);
if ((get_capabilities() & required_caps) != required_caps) {
fprintf(stderr, "%s: drop_privileges was requested, which launches the FUSE file system fully unprivileged. In order to do so %s must be run with privileges, please invoke with CAP_SYS_ADMIN and CAP_SETPCAP (e.g. as root).\n",
progname, progname);
exit(1);
}
}
if (dev)
options = add_option("dev", options);
if (suid)
options = add_option("suid", options);
if (setuid_name && setuid_name[0]) {
#ifdef linux
if (drop_privileges) {
/*
* Make securebits more permissive before calling
* setuid(). Specifically, if SECBIT_KEEP_CAPS and
* SECBIT_NO_SETUID_FIXUP weren't set, setuid() would
* have the side effect of dropping all capabilities,
* and we need to retain CAP_SETPCAP in order to drop
* all privileges before exec().
*/
if (prctl(PR_SET_SECUREBITS,
SECBIT_KEEP_CAPS |
SECBIT_NO_SETUID_FIXUP) == -1) {
fprintf(stderr,
"%s: Failed to set securebits %s\n",
progname, strerror(errno));
exit(1);
}
}
#endif
struct passwd *pwd = getpwnam(setuid_name);
if (!pwd || setgid(pwd->pw_gid) == -1 || setuid(pwd->pw_uid) == -1) {
fprintf(stderr, "%s: Failed to setuid to %s: %s\n",
progname, setuid_name, strerror(errno));
exit(1);
}
} else if (!getenv("HOME")) {
/* Hack to make filesystems work in the boot environment */
setenv("HOME", "/root", 0);
}
if (pass_fuse_fd) {
fuse_fd = prepare_fuse_fd(mountpoint, type, options);
dev_fd_mountpoint = xrealloc(NULL, 20);
snprintf(dev_fd_mountpoint, 20, "/dev/fd/%d", fuse_fd);
mountpoint = dev_fd_mountpoint;
}
#ifdef linux
if (drop_privileges) {
int err;
drop_and_lock_capabilities();
err = check_binary_access(type);
if (err) {
fprintf(stderr, "%s: cannot execute '%s' after dropping privileges (%s)\n",
progname, type,
err == -EACCES ? "permission denied" : "not found");
exit(1);
}
}
#endif
#ifdef HAVE_SERVICEMOUNT
/*
* Now that we know the desired filesystem type, see if we can find
* a socket service implementing that, if we haven't selected any weird
* options that would prevent that.
*/
if (!pass_fuse_fd && !(setuid_name && setuid_name[0])) {
int ret = try_service_main(argv[0], type, source, mountpoint,
options);
if (ret != MOUNT_SERVICE_FALLBACK_NEEDED)
return ret;
}
#endif
add_arg(&command, type);
if (source)
add_arg(&command, source);
add_arg(&command, mountpoint);
if (options) {
add_arg(&command, "-o");
add_arg(&command, options);
}
free(options);
free(dev_fd_mountpoint);
free(dup_source);
free(setuid_name);
execl("/bin/sh", "/bin/sh", "-c", command, NULL);
fprintf(stderr, "%s: failed to execute /bin/sh: %s\n", progname,
strerror(errno));
if (pass_fuse_fd)
close(fuse_fd);
free(command);
return 1;
}