From: Jimmy Zuber <[email protected]> Check the requests sent when opening and creating new, existing, missing and expired names, against a raw /dev/fuse server with and without FUSE_OPEN_ATOMIC, and that opening an existing read-only file is still permission checked.
Assisted-by: Claude:claude-opus-5-5 [Claude-Code] Signed-off-by: Jimmy Zuber <[email protected]> --- .../selftests/filesystems/fuse/.gitignore | 1 + .../selftests/filesystems/fuse/Makefile | 3 +- .../filesystems/fuse/atomic_open_test.c | 473 ++++++++++++++++++ 3 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore index b5b03db1118c..6e529fee96d4 100644 --- a/tools/testing/selftests/filesystems/fuse/.gitignore +++ b/tools/testing/selftests/filesystems/fuse/.gitignore @@ -1,4 +1,5 @@ # SPDX-License-Identifier: GPL-2.0-only +atomic_open_test fuse_mnt fusectl_test test_syncfs diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile index c2de8d225447..2fe20f0e9143 100644 --- a/tools/testing/selftests/filesystems/fuse/Makefile +++ b/tools/testing/selftests/filesystems/fuse/Makefile @@ -3,7 +3,7 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) TEST_GEN_PROGS := fusectl_test test_syncfs -TEST_GEN_PROGS += write_extend_eof_test +TEST_GEN_PROGS += write_extend_eof_test atomic_open_test TEST_GEN_FILES := fuse_mnt # fuse_acl_cache_test requires libfuse3; add it only when the library is present. @@ -16,6 +16,7 @@ endif include ../../lib.mk $(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread +$(OUTPUT)/atomic_open_test: LDLIBS += -lpthread VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null) ifeq ($(VAR_CFLAGS),) diff --git a/tools/testing/selftests/filesystems/fuse/atomic_open_test.c b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c new file mode 100644 index 000000000000..880367d6cb96 --- /dev/null +++ b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test open() and create with and without FUSE_OPEN_ATOMIC. + * + * Requires root to mount fuse. + */ +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <stdint.h> +#include <string.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <sys/uio.h> +#include <sys/wait.h> +#include <linux/fuse.h> + +#include "../../kselftest_harness.h" + +#define FUSE_ROOT_ID 1 +#define MAX_WRITE (128 * 1024) +#define MAX_ENTRIES 16 +#define NOBODY 65534 + +struct entry { + char name[64]; + uint64_t nodeid; + uint32_t mode; + uint64_t entry_valid; +}; + +/* Server state, shared with the responder thread. */ +struct server { + int fd; + int atomic; + struct entry ent[MAX_ENTRIES]; + int nent; + int nr_lookup, nr_create, nr_open, nr_open_atomic, nr_setattr; +}; + +static void reply(int fd, uint64_t unique, int error, void *data, size_t len) +{ + struct fuse_out_header oh = { + .len = sizeof(oh) + (data ? len : 0), + .error = error, + .unique = unique, + }; + struct iovec iov[2] = { { &oh, sizeof(oh) }, { data, len } }; + + /* Errors here are teardown races (device closed on unmount); ignore. */ + if (writev(fd, iov, data ? 2 : 1) < 0) + return; +} + +static struct entry *find(struct server *s, const char *name) +{ + int i; + + for (i = 0; i < s->nent; i++) + if (!strcmp(s->ent[i].name, name)) + return &s->ent[i]; + return NULL; +} + +static struct entry *add(struct server *s, const char *name, uint32_t mode) +{ + struct entry *e = &s->ent[s->nent]; + + snprintf(e->name, sizeof(e->name), "%.63s", name); + e->nodeid = FUSE_ROOT_ID + 1 + s->nent++; + e->mode = mode; + e->entry_valid = 3600; + return e; +} + +/* A NULL entry is a negative entry, cached like a found one. */ +static void fill_entry(struct fuse_entry_out *out, struct entry *e) +{ + memset(out, 0, sizeof(*out)); + out->entry_valid = e ? e->entry_valid : 3600; + if (!e) + return; + out->nodeid = e->nodeid; + out->attr_valid = 3600; + out->attr.ino = e->nodeid; + out->attr.mode = e->mode; + out->attr.nlink = S_ISDIR(e->mode) ? 2 : 1; + out->attr.blksize = 4096; +} + +static void *server_thread(void *arg) +{ + struct server *s = arg; + static char buf[MAX_WRITE + 4096]; + + for (;;) { + ssize_t n = read(s->fd, buf, sizeof(buf)); + struct fuse_in_header *ih = (void *)buf; + + if (n < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + return NULL; /* device closed on unmount */ + } + 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}; + + out.major = FUSE_KERNEL_VERSION; + out.minor = FUSE_KERNEL_MINOR_VERSION; + out.max_readahead = in->max_readahead; + out.max_write = MAX_WRITE; + out.max_background = 16; + out.congestion_threshold = 12; + out.flags = FUSE_INIT_EXT; + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_GETATTR: + case FUSE_SETATTR: { + struct fuse_attr_out out = {0}; + struct fuse_entry_out eo; + int i; + + if (ih->opcode == FUSE_SETATTR) + s->nr_setattr++; + out.attr_valid = 3600; + if (ih->nodeid == FUSE_ROOT_ID) { + out.attr.ino = FUSE_ROOT_ID; + out.attr.mode = S_IFDIR | 0777; + out.attr.nlink = 2; + } + for (i = 0; i < s->nent; i++) { + if (s->ent[i].nodeid == ih->nodeid) { + fill_entry(&eo, &s->ent[i]); + out.attr = eo.attr; + } + } + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_LOOKUP: { + struct fuse_entry_out out; + + s->nr_lookup++; + fill_entry(&out, find(s, (char *)(ih + 1))); + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_CREATE: + case FUSE_OPEN_ATOMIC: { + struct fuse_create_in *in = (void *)(ih + 1); + const char *name = (char *)(in + 1); + struct entry *e = find(s, name); + struct { + struct fuse_entry_out entry; + struct fuse_open_out open; + } out = {0}; + + if (ih->opcode == FUSE_CREATE) + s->nr_create++; + else + s->nr_open_atomic++; + if (ih->opcode == FUSE_OPEN_ATOMIC && !s->atomic) { + reply(s->fd, ih->unique, -ENOSYS, NULL, 0); + break; + } + if (e && (in->flags & O_CREAT) && + (in->flags & O_EXCL)) { + reply(s->fd, ih->unique, -EEXIST, NULL, 0); + break; + } + if (!e && !(in->flags & O_CREAT)) { + fill_entry(&out.entry, NULL); + reply(s->fd, ih->unique, 0, &out.entry, + sizeof(out.entry)); + break; + } + if (e && (!S_ISREG(e->mode) || (in->flags & O_TRUNC))) { + fill_entry(&out.entry, e); + reply(s->fd, ih->unique, 0, &out.entry, + sizeof(out.entry)); + break; + } + if (!e) { + e = add(s, name, S_IFREG | in->mode); + if (ih->opcode == FUSE_OPEN_ATOMIC) + out.open.open_flags = FOPEN_CREATED; + } + fill_entry(&out.entry, e); + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_TMPFILE: { + struct fuse_create_in *in = (void *)(ih + 1); + struct entry e = { + .nodeid = 100, + .mode = S_IFREG | in->mode, + .entry_valid = 3600, + }; + struct { + struct fuse_entry_out entry; + struct fuse_open_out open; + } out = {0}; + + fill_entry(&out.entry, &e); + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_OPEN: { + struct fuse_open_out out = {0}; + + s->nr_open++; + reply(s->fd, ih->unique, 0, &out, sizeof(out)); + break; + } + case FUSE_FLUSH: + case FUSE_RELEASE: + reply(s->fd, ih->unique, 0, NULL, 0); + break; + case FUSE_FORGET: + case FUSE_BATCH_FORGET: + break; + default: + reply(s->fd, ih->unique, -EOPNOTSUPP, NULL, 0); + break; + } + } +} + +FIXTURE(fuse) +{ + struct server *srv; + char dir[64]; + int mounted; +}; + +FIXTURE_VARIANT(fuse) +{ + int atomic; +}; + +FIXTURE_VARIANT_ADD(fuse, atomic) +{ + .atomic = 1, +}; + +FIXTURE_VARIANT_ADD(fuse, plain) +{ + .atomic = 0, +}; + +FIXTURE_SETUP(fuse) +{ + char opts[128]; + struct stat st; + pthread_t t; + + if (geteuid() != 0) + SKIP(return, "need root to mount fuse"); + + self->srv = mmap(NULL, sizeof(*self->srv), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, self->srv); + self->srv->atomic = variant->atomic; + add(self->srv, "ro", S_IFREG | 0444); + add(self->srv, "rw", S_IFREG | 0644); + add(self->srv, "stale", S_IFREG | 0644)->entry_valid = 0; + + self->srv->fd = open("/dev/fuse", O_RDWR); + ASSERT_GE(self->srv->fd, 0); + + strcpy(self->dir, "/tmp/fuse_aopen_XXXXXX"); + ASSERT_NE(NULL, mkdtemp(self->dir)); + + snprintf(opts, sizeof(opts), + "fd=%d,rootmode=40000,user_id=0,group_id=0,default_permissions,allow_other", + self->srv->fd); + ASSERT_EQ(0, mount("fuse", self->dir, "fuse", 0, opts)); + self->mounted = 1; + + ASSERT_EQ(0, pthread_create(&t, NULL, server_thread, self->srv)); + pthread_detach(t); + + /* Wait for FUSE_INIT: the root GETATTR is queued behind it */ + ASSERT_EQ(0, stat(self->dir, &st)); +} + +FIXTURE_TEARDOWN(fuse) +{ + if (self->mounted) + umount2(self->dir, MNT_DETACH); + if (self->srv && self->srv != MAP_FAILED) { + if (self->srv->fd > 0) + close(self->srv->fd); + munmap(self->srv, sizeof(*self->srv)); + } + if (self->dir[0]) + rmdir(self->dir); +} + +/* Open @name as an unprivileged user and return the errno, or 0. */ +static int open_as_nobody(const char *dir, const char *name, int flags) +{ + char path[128]; + int status, fd; + pid_t pid; + + snprintf(path, sizeof(path), "%s/%s", dir, name); + pid = fork(); + if (pid < 0) + return -1; + if (pid == 0) { + if (setgid(NOBODY) || setuid(NOBODY)) + _exit(255); + fd = open(path, flags, 0644); + _exit(fd < 0 ? errno : 0); + } + if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status)) + return -1; + return WEXITSTATUS(status); +} + +static int open_at(const char *dir, const char *name, int flags) +{ + char path[128]; + + snprintf(path, sizeof(path), "%s/%s", dir, name); + return open(path, flags, 0644); +} + +TEST_F(fuse, create_new) +{ + int fd; + + fd = open_at(self->dir, "a", O_CREAT | O_WRONLY); + ASSERT_GE(fd, 0); + close(fd); + + fd = open_at(self->dir, "b", O_CREAT | O_EXCL | O_WRONLY); + ASSERT_GE(fd, 0); + close(fd); + + fd = open(self->dir, O_TMPFILE | O_RDWR, 0600); + ASSERT_GE(fd, 0); + close(fd); + + EXPECT_EQ(0, self->srv->nr_open); + if (variant->atomic) { + EXPECT_EQ(2, self->srv->nr_open_atomic); + EXPECT_EQ(0, self->srv->nr_lookup); + EXPECT_EQ(0, self->srv->nr_create); + } else { + EXPECT_EQ(1, self->srv->nr_open_atomic); + EXPECT_EQ(2, self->srv->nr_lookup); + EXPECT_EQ(2, self->srv->nr_create); + } +} + +TEST_F(fuse, create_existing) +{ + int fd; + + ASSERT_EQ(-1, open_at(self->dir, "ro", O_CREAT | O_EXCL | O_WRONLY)); + ASSERT_EQ(EEXIST, errno); + ASSERT_EQ(EACCES, open_as_nobody(self->dir, "ro", O_CREAT | O_RDWR)); + + /* Truncation goes through FUSE_OPEN and SETATTR. */ + fd = open_at(self->dir, "rw", O_CREAT | O_TRUNC | O_WRONLY); + ASSERT_GE(fd, 0); + close(fd); + + EXPECT_EQ(0, self->srv->nr_create); + EXPECT_EQ(1, self->srv->nr_open); + EXPECT_EQ(1, self->srv->nr_setattr); + if (variant->atomic) { + EXPECT_EQ(3, self->srv->nr_open_atomic); + EXPECT_EQ(0, self->srv->nr_lookup); + } else { + EXPECT_EQ(1, self->srv->nr_open_atomic); + EXPECT_EQ(2, self->srv->nr_lookup); + } +} + +TEST_F(fuse, open_existing) +{ + int fd; + + ASSERT_EQ(EACCES, open_as_nobody(self->dir, "ro", O_RDWR)); + + fd = open_at(self->dir, "ro", O_RDONLY); + ASSERT_GE(fd, 0); + close(fd); + + ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY)); + ASSERT_EQ(ENOENT, errno); + ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY)); + ASSERT_EQ(ENOENT, errno); + + EXPECT_EQ(1, self->srv->nr_open); + if (variant->atomic) { + EXPECT_EQ(2, self->srv->nr_open_atomic); + EXPECT_EQ(0, self->srv->nr_lookup); + } else { + EXPECT_EQ(1, self->srv->nr_open_atomic); + EXPECT_EQ(2, self->srv->nr_lookup); + } +} + +TEST_F(fuse, open_stale) +{ + struct stat st; + int fd; + + fd = open_at(self->dir, "stale", O_RDONLY); + ASSERT_GE(fd, 0); + close(fd); + + /* The entry has expired, the reopen checks the name. */ + fd = open_at(self->dir, "stale", O_RDONLY); + ASSERT_GE(fd, 0); + close(fd); + + /* Another file takes the name: the open finds the new one. */ + find(self->srv, "stale")->nodeid = 200; + fd = open_at(self->dir, "stale", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(0, fstat(fd, &st)); + EXPECT_EQ(200, st.st_ino); + close(fd); + + if (variant->atomic) { + EXPECT_EQ(4, self->srv->nr_open_atomic); + EXPECT_EQ(0, self->srv->nr_lookup); + EXPECT_EQ(0, self->srv->nr_open); + } else { + EXPECT_EQ(1, self->srv->nr_open_atomic); + EXPECT_EQ(4, self->srv->nr_lookup); + EXPECT_EQ(3, self->srv->nr_open); + } +} + +TEST_F(fuse, open_stale_first) +{ + char path[128]; + struct stat st; + int fd; + + /* The first FUSE_OPEN_ATOMIC comes from reopening a cached name */ + snprintf(path, sizeof(path), "%s/stale", self->dir); + ASSERT_EQ(0, stat(path, &st)); + fd = open(path, O_RDONLY); + ASSERT_GE(fd, 0); + close(fd); + + EXPECT_EQ(1, self->srv->nr_open_atomic); + if (variant->atomic) { + EXPECT_EQ(1, self->srv->nr_lookup); + EXPECT_EQ(0, self->srv->nr_open); + } else { + EXPECT_EQ(2, self->srv->nr_lookup); + EXPECT_EQ(1, self->srv->nr_open); + } +} + +TEST_HARNESS_MAIN -- 2.50.1
