blob: d3ca2613b45c2c3bceec9b15c90cbfe1ab323291 [file] [edit]
// SPDX-License-Identifier: GPL-2.0
/*
* Test that FUSE_SYNCFS is propagated to a userspace server only when the
* server opts in with FUSE_HAS_SYNCFS *and* opened /dev/fuse with
* CAP_SYS_ADMIN in the initial user namespace.
*
* Unlike the libfuse-based selftests, this talks the raw FUSE wire protocol
* over /dev/fuse so it can (a) choose whether to advertise FUSE_HAS_SYNCFS in
* the INIT reply and (b) observe directly whether a FUSE_SYNCFS opcode arrives.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <linux/capability.h>
#include <linux/fuse.h>
#include "../../kselftest.h"
#define FUSE_ROOT_ID 1
/*
* Add or drop CAP_SYS_ADMIN in the effective set via raw capget/capset
* (avoids a libcap dependency). Used to construct a server that opens
* /dev/fuse without the privilege FUSE_HAS_SYNCFS requires.
*/
static int set_sysadmin(int on)
{
struct __user_cap_header_struct hdr = {
.version = _LINUX_CAPABILITY_VERSION_3,
.pid = 0,
};
struct __user_cap_data_struct data[_LINUX_CAPABILITY_U32S_3] = {};
unsigned int idx = CAP_SYS_ADMIN >> 5;
__u32 bit = 1U << (CAP_SYS_ADMIN & 31);
if (syscall(SYS_capget, &hdr, data))
return -1;
if (on)
data[idx].effective |= bit;
else
data[idx].effective &= ~bit;
return syscall(SYS_capset, &hdr, data);
}
/*
* eventfd the server child writes once when it receives FUSE_SYNCFS; the
* parent poll()s it to observe (or rule out) propagation.
*/
static int syncfs_evfd;
static void reply(int fd, uint64_t unique, int error, void *data, size_t len)
{
struct fuse_out_header oh = {
.len = sizeof(oh) + len,
.error = error,
.unique = unique,
};
struct iovec iov[2] = {
{ &oh, sizeof(oh) },
{ data, len },
};
if (writev(fd, iov, data ? 2 : 1) < 0)
ksft_print_msg("server writev failed: %s\n", strerror(errno));
}
static void fill_attr(struct fuse_attr *a, uint64_t ino, uint32_t mode,
uint32_t nlink)
{
memset(a, 0, sizeof(*a));
a->ino = ino;
a->mode = mode;
a->nlink = nlink;
a->blksize = 4096;
}
/*
* Minimal FUSE server. Advertises FUSE_HAS_SYNCFS in its INIT reply iff
* @advertise is set. Signals syncfs_evfd when a FUSE_SYNCFS opcode arrives.
*/
#define SERVER_MAX_WRITE 65536
static void run_server(int fd, int advertise)
{
/*
* The kernel rejects reads (EINVAL) whose buffer is smaller than
* max_write + header, so size generously for the max_write we
* advertise in the INIT reply below.
*/
static char buf[SERVER_MAX_WRITE + 4096];
for (;;) {
ssize_t n = read(fd, buf, sizeof(buf));
struct fuse_in_header *ih = (void *)buf;
if (n < 0) {
if (errno == EINTR || errno == EAGAIN)
continue;
return; /* device closed on unmount/abort */
}
if (n < (ssize_t)sizeof(*ih))
continue;
switch (ih->opcode) {
case FUSE_INIT: {
struct fuse_init_in *in = (void *)(ih + 1);
struct fuse_init_out out = {0};
uint64_t flags = FUSE_INIT_EXT;
out.major = FUSE_KERNEL_VERSION;
out.minor = FUSE_KERNEL_MINOR_VERSION;
out.max_readahead = in->max_readahead;
out.max_write = SERVER_MAX_WRITE;
out.max_background = 16;
out.congestion_threshold = 12;
if (advertise)
flags |= FUSE_HAS_SYNCFS;
out.flags = flags;
out.flags2 = flags >> 32;
reply(fd, ih->unique, 0, &out, sizeof(out));
break;
}
case FUSE_GETATTR: {
struct fuse_attr_out out = {0};
fill_attr(&out.attr, FUSE_ROOT_ID, S_IFDIR | 0755, 2);
reply(fd, ih->unique, 0, &out, sizeof(out));
break;
}
case FUSE_SYNCFS: {
uint64_t one = 1;
if (write(syncfs_evfd, &one, sizeof(one)) < 0)
ksft_print_msg("server eventfd write failed: %s\n",
strerror(errno));
reply(fd, ih->unique, 0, NULL, 0);
break;
}
default:
/*
* Anything else (e.g. OPENDIR from opening the mount
* root) is not needed to drive this test; -ENOSYS lets
* the kernel proceed.
*/
reply(fd, ih->unique, -ENOSYS, NULL, 0);
break;
}
}
}
/*
* Mount a fuse fs backed by a forked server, issue syncfs(), and report
* whether the server observed FUSE_SYNCFS. Returns 0 on success, -1 if the
* environment could not support the test (caller should skip).
*
* If @unpriv_open is set, /dev/fuse is opened with CAP_SYS_ADMIN dropped
* (regained only for the mount() syscall), so the device opener -- the
* server principal FUSE_HAS_SYNCFS is gated on -- lacks the privilege even
* though it remains in the initial user namespace.
*/
static int do_mount_and_syncfs(const char *mnt, int advertise, int unpriv_open,
int *seen)
{
struct pollfd pfd = { .events = POLLIN };
char opts[256];
int fd, mfd = -1, i;
pid_t pid;
syncfs_evfd = eventfd(0, EFD_CLOEXEC);
if (syncfs_evfd < 0)
return -1;
if (unpriv_open && set_sysadmin(0))
goto out_evfd;
fd = open("/dev/fuse", O_RDWR);
if (unpriv_open && set_sysadmin(1)) {
if (fd >= 0)
close(fd);
goto out_evfd;
}
if (fd < 0)
goto out_evfd;
mkdir(mnt, 0755);
snprintf(opts, sizeof(opts),
"fd=%d,rootmode=40000,user_id=%d,group_id=%d",
fd, getuid(), getgid());
if (mount("fuse", mnt, "fuse", 0, opts) < 0)
goto out_fd;
pid = fork();
if (pid < 0)
goto out_umount;
if (pid == 0) {
run_server(fd, advertise);
_exit(0);
}
/*
* The parent does not service the fuse fd; the child does. Close our
* copy so the kernel sees a single server, and so that if the child
* dies the connection aborts instead of hanging us forever.
*/
close(fd);
/*
* mount() returns before the server has answered FUSE_INIT, so the
* first open() can race and fail with ENOTCONN; retry until the
* handshake settles.
*/
for (i = 0; i < 1000; i++) {
mfd = open(mnt, O_RDONLY | O_DIRECTORY);
if (mfd >= 0)
break;
usleep(1000);
}
if (mfd >= 0) {
syncfs(mfd);
close(mfd);
}
/*
* No waiting is needed: the server writes syncfs_evfd before it replies
* to FUSE_SYNCFS, and that reply is what unblocks the synchronous
* syncfs() above. So once syncfs() has returned, the eventfd is already
* signalled if the opcode was propagated, and will never be otherwise.
* poll() with a zero timeout therefore decides both cases immediately.
*/
pfd.fd = syncfs_evfd;
*seen = poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN);
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
umount2(mnt, MNT_DETACH);
close(syncfs_evfd);
return 0;
out_umount:
umount2(mnt, MNT_DETACH);
out_fd:
close(fd);
out_evfd:
close(syncfs_evfd);
return -1;
}
int main(void)
{
char mnt[] = "/tmp/fuse_syncfs_XXXXXX";
int seen, ret;
ksft_print_header();
ksft_set_plan(3);
/* Hard watchdog: never let a stuck syncfs hang the test runner. */
signal(SIGALRM, SIG_DFL);
alarm(60);
if (geteuid() != 0)
ksft_exit_skip("test requires root to mount fuse\n");
if (!mkdtemp(mnt))
ksft_exit_fail_msg("mkdtemp failed\n");
/* T1: host-root mount, server opts in -> syncfs must reach server. */
ret = do_mount_and_syncfs(mnt, 1, 0, &seen);
if (ret < 0)
ksft_test_result_skip("T1: could not mount fuse\n");
else
ksft_test_result(seen,
"T1 host-root + FUSE_HAS_SYNCFS: server receives FUSE_SYNCFS\n");
/* T2: host-root mount, server does NOT opt in -> no FUSE_SYNCFS. */
ret = do_mount_and_syncfs(mnt, 0, 0, &seen);
if (ret < 0)
ksft_test_result_skip("T2: could not mount fuse\n");
else
ksft_test_result(!seen,
"T2 host-root, no opt-in: server does NOT receive FUSE_SYNCFS\n");
/*
* T3: server opts in but opened /dev/fuse without CAP_SYS_ADMIN while
* still in the initial user namespace -> kernel must withhold
* FUSE_SYNCFS. This is the case that distinguishes gating on the
* server's privilege from gating on the mount's user namespace.
*/
ret = do_mount_and_syncfs(mnt, 1, 1, &seen);
if (ret < 0)
ksft_test_result_skip("T3: could not mount fuse unprivileged\n");
else
ksft_test_result(!seen,
"T3 init_userns, opener lacks CAP_SYS_ADMIN: FUSE_SYNCFS withheld\n");
rmdir(mnt);
ksft_finished();
}