// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(); }