[RFC PATCH v4 14/36] fuse-bpf: support readdir

From: Daniel Rosenberg
Date: Thu Mar 28 2024 - 22:00:04 EST


This adds backing support for FUSE_READDIR

Signed-off-by: Daniel Rosenberg <drosen@xxxxxxxxxx>
Signed-off-by: Paul Lawrence <paullawrence@xxxxxxxxxx>
---
fs/fuse/backing.c | 202 ++++++++++++++++++++++++++++++++++++++
fs/fuse/fuse_i.h | 6 ++
fs/fuse/readdir.c | 5 +
include/uapi/linux/fuse.h | 6 ++
4 files changed, 219 insertions(+)

diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
index c813237b6599..0182236c2735 100644
--- a/fs/fuse/backing.c
+++ b/fs/fuse/backing.c
@@ -1657,6 +1657,208 @@ int fuse_bpf_unlink(int *out, struct inode *dir, struct dentry *entry)
dir, entry);
}

+struct fuse_read_args {
+ struct fuse_read_in in;
+ struct fuse_read_out out;
+ struct fuse_buffer buffer;
+};
+
+static int fuse_readdir_initialize_in(struct bpf_fuse_args *fa, struct fuse_read_args *args,
+ struct file *file, struct dir_context *ctx,
+ bool *force_again, bool *allow_force, bool is_continued)
+{
+ struct fuse_file *ff = file->private_data;
+
+ *fa = (struct bpf_fuse_args) {
+ .info = (struct bpf_fuse_meta_info) {
+ .nodeid = ff->nodeid,
+ .opcode = FUSE_READDIR,
+ },
+ .in_numargs = 1,
+ .in_args[0] = (struct bpf_fuse_arg) {
+ .size = sizeof(args->in),
+ .value = &args->in,
+ },
+ };
+
+ args->in = (struct fuse_read_in) {
+ .fh = ff->fh,
+ .offset = ctx->pos,
+ .size = PAGE_SIZE,
+ };
+
+ *force_again = false;
+ *allow_force = true;
+ return 0;
+}
+
+static int fuse_readdir_initialize_out(struct bpf_fuse_args *fa, struct fuse_read_args *args,
+ struct file *file, struct dir_context *ctx,
+ bool *force_again, bool *allow_force, bool is_continued)
+{
+ u8 *page = (u8 *)__get_free_page(GFP_KERNEL);
+
+ if (!page)
+ return -ENOMEM;
+
+ fa->flags = FUSE_BPF_OUT_ARGVAR;
+ fa->out_numargs = 2;
+ fa->out_args[0] = (struct bpf_fuse_arg) {
+ .size = sizeof(args->out),
+ .value = &args->out,
+ };
+ fa->out_args[1] = (struct bpf_fuse_arg) {
+ .is_buffer = true,
+ .buffer = &args->buffer,
+ };
+ args->out = (struct fuse_read_out) {
+ .again = 0,
+ .offset = 0,
+ };
+ args->buffer = (struct fuse_buffer) {
+ .data = page,
+ .size = PAGE_SIZE,
+ .alloc_size = PAGE_SIZE,
+ .max_size = PAGE_SIZE,
+ .flags = BPF_FUSE_VARIABLE_SIZE,
+ };
+
+ return 0;
+}
+
+struct fusebpf_ctx {
+ struct dir_context ctx;
+ u8 *addr;
+ size_t offset;
+};
+
+static bool filldir(struct dir_context *ctx, const char *name, int namelen,
+ loff_t offset, u64 ino, unsigned int d_type)
+{
+ struct fusebpf_ctx *ec = container_of(ctx, struct fusebpf_ctx, ctx);
+ struct fuse_dirent *fd = (struct fuse_dirent *)(ec->addr + ec->offset);
+
+ if (ec->offset + sizeof(struct fuse_dirent) + namelen > PAGE_SIZE)
+ return false;
+
+ *fd = (struct fuse_dirent) {
+ .ino = ino,
+ .off = offset,
+ .namelen = namelen,
+ .type = d_type,
+ };
+
+ memcpy(fd->name, name, namelen);
+ ec->offset += FUSE_DIRENT_SIZE(fd);
+
+ return true;
+}
+
+static int parse_dirfile(char *buf, size_t nbytes, struct dir_context *ctx,
+ loff_t next_offset)
+{
+ char *buf_start = buf;
+
+ while (nbytes >= FUSE_NAME_OFFSET) {
+ struct fuse_dirent *dirent = (struct fuse_dirent *) buf;
+ size_t reclen = FUSE_DIRENT_SIZE(dirent);
+
+ if (!dirent->namelen || dirent->namelen > FUSE_NAME_MAX)
+ return -EIO;
+ if (reclen > nbytes)
+ break;
+ if (memchr(dirent->name, '/', dirent->namelen) != NULL)
+ return -EIO;
+
+ ctx->pos = dirent->off;
+ if (!dir_emit(ctx, dirent->name, dirent->namelen, dirent->ino,
+ dirent->type)) {
+ // If we can't make any progress, user buffer is too small
+ if (buf == buf_start)
+ return -EINVAL;
+ else
+ return 0;
+ }
+
+ buf += reclen;
+ nbytes -= reclen;
+ }
+ ctx->pos = next_offset;
+
+ return 0;
+}
+
+static int fuse_readdir_backing(struct bpf_fuse_args *fa, int *out,
+ struct file *file, struct dir_context *ctx,
+ bool *force_again, bool *allow_force, bool is_continued)
+{
+ struct fuse_file *ff = file->private_data;
+ struct file *backing_dir = ff->backing_file;
+ struct fuse_read_out *fro = fa->out_args[0].value;
+ struct fusebpf_ctx ec;
+
+ ec = (struct fusebpf_ctx) {
+ .ctx.actor = filldir,
+ .ctx.pos = ctx->pos,
+ .addr = fa->out_args[1].buffer->data,
+ };
+
+ if (!ec.addr)
+ return -ENOMEM;
+
+ if (!is_continued)
+ backing_dir->f_pos = file->f_pos;
+
+ *out = iterate_dir(backing_dir, &ec.ctx);
+ if (ec.offset == 0)
+ *allow_force = false;
+ fa->out_args[1].buffer->size = ec.offset;
+
+ fro->offset = ec.ctx.pos;
+ fro->again = false;
+
+ return *out;
+}
+
+static int fuse_readdir_finalize(struct bpf_fuse_args *fa, int *out,
+ struct file *file, struct dir_context *ctx,
+ bool *force_again, bool *allow_force, bool is_continued)
+{
+ struct fuse_read_out *fro = fa->out_args[0].value;
+ struct fuse_file *ff = file->private_data;
+ struct file *backing_dir = ff->backing_file;
+
+ *out = parse_dirfile(fa->out_args[1].buffer->data, fa->out_args[1].buffer->size, ctx, fro->offset);
+ *force_again = !!fro->again;
+ if (*force_again && !*allow_force)
+ *out = -EINVAL;
+
+ backing_dir->f_pos = ctx->pos;
+
+ free_page((unsigned long)fa->out_args[1].buffer->data);
+ return *out;
+}
+
+int fuse_bpf_readdir(int *out, struct inode *inode, struct file *file, struct dir_context *ctx)
+{
+ int ret;
+ bool allow_force;
+ bool force_again = false;
+ bool is_continued = false;
+
+again:
+ ret = bpf_fuse_backing(inode, struct fuse_read_args, out,
+ fuse_readdir_initialize_in, fuse_readdir_initialize_out,
+ fuse_readdir_backing, fuse_readdir_finalize,
+ file, ctx, &force_again, &allow_force, is_continued);
+ if (force_again && *out >= 0) {
+ is_continued = true;
+ goto again;
+ }
+
+ return ret;
+}
+
static int fuse_access_initialize_in(struct bpf_fuse_args *fa, struct fuse_access_in *in,
struct inode *inode, int mask)
{
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index bd187dbf20b2..ab52003de194 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1452,6 +1452,7 @@ int fuse_bpf_file_read_iter(ssize_t *out, struct inode *inode, struct kiocb *ioc
int fuse_bpf_file_write_iter(ssize_t *out, struct inode *inode, struct kiocb *iocb, struct iov_iter *from);
int fuse_bpf_file_fallocate(int *out, struct inode *inode, struct file *file, int mode, loff_t offset, loff_t length);
int fuse_bpf_lookup(struct dentry **out, struct inode *dir, struct dentry *entry, unsigned int flags);
+int fuse_bpf_readdir(int *out, struct inode *inode, struct file *file, struct dir_context *ctx);
int fuse_bpf_access(int *out, struct inode *inode, int mask);

#else
@@ -1522,6 +1523,11 @@ static inline int fuse_bpf_lookup(struct dentry **out, struct inode *dir, struct
return 0;
}

+static inline int fuse_bpf_readdir(int *out, struct inode *inode, struct file *file, struct dir_context *ctx)
+{
+ return 0;
+}
+
static inline int fuse_bpf_access(int *out, struct inode *inode, int mask)
{
return 0;
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c66a54d6c7d3..53a1fd756772 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -20,6 +20,8 @@ static bool fuse_use_readdirplus(struct inode *dir, struct dir_context *ctx)

if (!fc->do_readdirplus)
return false;
+ if (fi->nodeid == 0)
+ return false;
if (!fc->readdirplus_auto)
return true;
if (test_and_clear_bit(FUSE_I_ADVISE_RDPLUS, &fi->state))
@@ -592,6 +594,9 @@ int fuse_readdir(struct file *file, struct dir_context *ctx)
if (fuse_is_bad(inode))
return -EIO;

+ if (fuse_bpf_readdir(&err, inode, file, ctx))
+ return err;
+
mutex_lock(&ff->readdir.lock);

err = UNCACHED;
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 8efaa9eecc5f..3417717c1a55 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -811,6 +811,12 @@ struct fuse_read_in {
uint32_t padding;
};

+struct fuse_read_out {
+ uint64_t offset;
+ uint32_t again;
+ uint32_t padding;
+};
+
// This is likely not what we want
struct fuse_read_iter_out {
uint64_t ret;
--
2.44.0.478.gd926399ef9-goog