[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250617221456.888231-2-paullawrence@google.com>
Date: Tue, 17 Jun 2025 15:14:53 -0700
From: Paul Lawrence <paullawrence@...gle.com>
To: Miklos Szeredi <miklos@...redi.hu>
Cc: linux-kernel@...r.kernel.org, linux-fsdevel@...r.kernel.org,
Paul Lawrence <paullawrence@...gle.com>
Subject: [PATCH v1 1/2] fuse: Add backing file option to lookup
This commit starts the process of adding directory passthrough to fuse.
* Add the Kconfig option CONFIG_FUSE_PASSTHROUGH_DIR
* Add struct fuse_entry_backing_out, an optional second out arg to
FUSE_LOOKUP. This will contain the fd to the backing file or
directory
* Synchronously store the backing file in the args
* Add backing inode to fuse inode and backing path to fuse dentry
* If FUSE_LOOKUP returns a backing fd, store path and inode
Signed-off-by: Paul Lawrence <paullawrence@...gle.com>
---
fs/fuse/Kconfig | 13 +++++
fs/fuse/Makefile | 1 +
fs/fuse/backing.c | 29 ++++++++++
fs/fuse/dev.c | 14 +++++
fs/fuse/dir.c | 114 +++++++++++++++++++++++++++++---------
fs/fuse/fuse_i.h | 22 +++++++-
fs/fuse/inode.c | 114 ++++++++++++++++++++++++++++++++++----
include/uapi/linux/fuse.h | 4 ++
8 files changed, 273 insertions(+), 38 deletions(-)
create mode 100644 fs/fuse/backing.c
diff --git a/fs/fuse/Kconfig b/fs/fuse/Kconfig
index ca215a3cba3e..b8a05cb427df 100644
--- a/fs/fuse/Kconfig
+++ b/fs/fuse/Kconfig
@@ -75,3 +75,16 @@ config FUSE_IO_URING
If you want to allow fuse server/client communication through io-uring,
answer Y
+
+config FUSE_PASSTHROUGH_DIR
+ bool "FUSE directory passthrough operations support"
+ depends on FUSE_FS
+ select FS_STACK
+ help
+ This allows bypassing FUSE server on a specified directory by mapping
+ all FUSE operations to be performed directly on a backing directory.
+
+ The bypass will automatically extend to all sub directories and files
+ in that directory.
+
+ If you want to allow directory passthrough operations, answer Y.
diff --git a/fs/fuse/Makefile b/fs/fuse/Makefile
index 3f0f312a31c1..79ca3a68b993 100644
--- a/fs/fuse/Makefile
+++ b/fs/fuse/Makefile
@@ -16,5 +16,6 @@ fuse-$(CONFIG_FUSE_DAX) += dax.o
fuse-$(CONFIG_FUSE_PASSTHROUGH) += passthrough.o
fuse-$(CONFIG_SYSCTL) += sysctl.o
fuse-$(CONFIG_FUSE_IO_URING) += dev_uring.o
+fuse-$(CONFIG_FUSE_PASSTHROUGH_DIR) += backing.o
virtiofs-y := virtio_fs.o
diff --git a/fs/fuse/backing.c b/fs/fuse/backing.c
new file mode 100644
index 000000000000..1dcc617bf660
--- /dev/null
+++ b/fs/fuse/backing.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * FUSE-BPF: Filesystem in Userspace with BPF
+ * Copyright (c) 2021 Google LLC
+ */
+
+#include "fuse_i.h"
+
+int fuse_handle_backing(struct fuse_entry_backing *feb,
+ struct inode **backing_inode, struct path *backing_path)
+{
+ struct file *backing_file = feb->backing_file;
+
+ if (!backing_file)
+ return -EINVAL;
+ if (IS_ERR(backing_file))
+ return PTR_ERR(backing_file);
+
+ if (backing_inode)
+ iput(*backing_inode);
+ *backing_inode = backing_file->f_inode;
+ ihold(*backing_inode);
+
+ path_put(backing_path);
+ *backing_path = backing_file->f_path;
+ path_get(backing_path);
+
+ return 0;
+}
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 6dcbaa218b7a..db1fbd1fdb85 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -2181,6 +2181,20 @@ static ssize_t fuse_dev_do_write(struct fuse_dev *fud,
err = fuse_copy_out_args(cs, req->args, nbytes);
fuse_copy_finish(cs);
+ if (!err && req->in.h.opcode == FUSE_LOOKUP &&
+ req->args->out_args[1].size ==
+ sizeof(struct fuse_entry_backing_out)) {
+ struct fuse_entry_backing_out *febo =
+ (struct fuse_entry_backing_out *)
+ req->args->out_args[1].value;
+ struct fuse_entry_backing *feb =
+ container_of(febo, struct fuse_entry_backing, out);
+
+ feb->backing_file = fget(febo->backing_fd);
+ if (!feb->backing_file)
+ err = -EBADFD;
+ }
+
spin_lock(&fpq->lock);
clear_bit(FR_LOCKED, &req->flags);
if (!fpq->connected)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 83ac192e7fdd..909463fae94d 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -34,7 +34,7 @@ static void fuse_advise_use_readdirplus(struct inode *dir)
set_bit(FUSE_I_ADVISE_RDPLUS, &fi->state);
}
-#if BITS_PER_LONG >= 64
+#if BITS_PER_LONG >= 64 && !defined(CONFIG_FUSE_PASSTHROUGH_DIR)
static inline void __fuse_dentry_settime(struct dentry *entry, u64 time)
{
entry->d_fsdata = (void *) time;
@@ -47,7 +47,12 @@ static inline u64 fuse_dentry_time(const struct dentry *entry)
#else
union fuse_dentry {
- u64 time;
+ struct {
+ u64 time;
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ struct path backing_path;
+#endif
+ };
struct rcu_head rcu;
};
@@ -60,6 +65,11 @@ static inline u64 fuse_dentry_time(const struct dentry *entry)
{
return ((union fuse_dentry *) entry->d_fsdata)->time;
}
+
+static inline union fuse_dentry *get_fuse_dentry(const struct dentry *entry)
+{
+ return entry->d_fsdata;
+}
#endif
static void fuse_dentry_settime(struct dentry *dentry, u64 time)
@@ -170,7 +180,8 @@ static void fuse_invalidate_entry(struct dentry *entry)
static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args,
u64 nodeid, const struct qstr *name,
- struct fuse_entry_out *outarg)
+ struct fuse_entry_out *outarg,
+ struct fuse_entry_backing_out *febo)
{
memset(outarg, 0, sizeof(struct fuse_entry_out));
args->opcode = FUSE_LOOKUP;
@@ -181,9 +192,12 @@ static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args,
args->in_args[1].value = name->name;
args->in_args[2].size = 1;
args->in_args[2].value = "";
- args->out_numargs = 1;
+ args->out_argvar = true;
+ args->out_numargs = 2;
args->out_args[0].size = sizeof(struct fuse_entry_out);
args->out_args[0].value = outarg;
+ args->out_args[1].size = sizeof(struct fuse_entry_backing_out);
+ args->out_args[1].value = febo;
}
/*
@@ -209,6 +223,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
(flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
struct fuse_entry_out outarg;
+ struct fuse_entry_backing feb;
FUSE_ARGS(args);
struct fuse_forget_link *forget;
u64 attr_version;
@@ -231,7 +246,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
attr_version = fuse_get_attr_version(fm->fc);
fuse_lookup_init(fm->fc, &args, get_node_id(dir),
- name, &outarg);
+ name, &outarg, &feb.out);
ret = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT */
if (!ret && !outarg.nodeid)
@@ -278,7 +293,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
goto out;
}
-#if BITS_PER_LONG < 64
+#if BITS_PER_LONG < 64 || defined(CONFIG_FUSE_PASSTHROUGH_DIR)
static int fuse_dentry_init(struct dentry *dentry)
{
dentry->d_fsdata = kzalloc(sizeof(union fuse_dentry),
@@ -290,6 +305,11 @@ static void fuse_dentry_release(struct dentry *dentry)
{
union fuse_dentry *fd = dentry->d_fsdata;
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ if (fd && fd->backing_path.dentry)
+ path_put(&fd->backing_path);
+#endif
+
kfree_rcu(fd, rcu);
}
#endif
@@ -329,7 +349,7 @@ static struct vfsmount *fuse_dentry_automount(struct path *path)
const struct dentry_operations fuse_dentry_operations = {
.d_revalidate = fuse_dentry_revalidate,
.d_delete = fuse_dentry_delete,
-#if BITS_PER_LONG < 64
+#if BITS_PER_LONG < 64 || defined(CONFIG_FUSE_PASSTHROUGH_DIR)
.d_init = fuse_dentry_init,
.d_release = fuse_dentry_release,
#endif
@@ -360,10 +380,12 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
}
int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
- struct fuse_entry_out *outarg, struct inode **inode)
+ struct fuse_entry_out *outarg, struct dentry *entry,
+ struct inode **inode)
{
struct fuse_mount *fm = get_fuse_mount_super(sb);
FUSE_ARGS(args);
+ struct fuse_entry_backing backing_arg = {0};
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
int err;
@@ -382,23 +404,61 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
attr_version = fuse_get_attr_version(fm->fc);
evict_ctr = fuse_get_evict_ctr(fm->fc);
- fuse_lookup_init(fm->fc, &args, nodeid, name, outarg);
+ fuse_lookup_init(fm->fc, &args, nodeid, name, outarg, &backing_arg.out);
err = fuse_simple_request(fm, &args);
- /* Zero nodeid is same as -ENOENT, but with valid timeout */
- if (err || !outarg->nodeid)
- goto out_put_forget;
- err = -EIO;
- if (fuse_invalid_attr(&outarg->attr))
- goto out_put_forget;
- if (outarg->nodeid == FUSE_ROOT_ID && outarg->generation != 0) {
- pr_warn_once("root generation should be zero\n");
- outarg->generation = 0;
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ if (err == sizeof(backing_arg.out)) {
+ struct file *backing_file;
+ struct inode *backing_inode;
+
+ err = -ENOENT;
+ if (!entry)
+ goto out_put_forget;
+
+ err = -EINVAL;
+ backing_file = backing_arg.backing_file;
+ if (!backing_file)
+ goto out_put_forget;
+
+ if (IS_ERR(backing_file)) {
+ err = PTR_ERR(backing_file);
+ goto out_put_forget;
+ }
+
+ backing_inode = backing_file->f_inode;
+ *inode = fuse_iget_backing(sb, backing_inode);
+ if (!*inode)
+ goto out_put_forget;
+
+ err = fuse_handle_backing(&backing_arg,
+ &get_fuse_inode(*inode)->backing_inode,
+ &get_fuse_dentry(entry)->backing_path);
+ if (err) {
+ iput(*inode);
+ *inode = NULL;
+ goto out_put_forget;
+ }
+ } else
+#endif
+ {
+ /* Zero nodeid is same as -ENOENT, but with valid timeout */
+ if (err || !outarg->nodeid)
+ goto out_put_forget;
+
+ err = -EIO;
+ if (fuse_invalid_attr(&outarg->attr))
+ goto out_put_forget;
+ if (outarg->nodeid == FUSE_ROOT_ID && outarg->generation != 0) {
+ pr_warn_once("root generation should be zero\n");
+ outarg->generation = 0;
+ }
+
+ *inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
+ &outarg->attr, ATTR_TIMEOUT(outarg),
+ attr_version, evict_ctr);
}
- *inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, ATTR_TIMEOUT(outarg),
- attr_version, evict_ctr);
err = -ENOMEM;
if (!*inode) {
fuse_queue_forget(fm->fc, forget, outarg->nodeid, 1);
@@ -406,9 +466,11 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
}
err = 0;
- out_put_forget:
+out_put_forget:
kfree(forget);
- out:
+out:
+ if (backing_arg.backing_file)
+ fput(backing_arg.backing_file);
return err;
}
@@ -427,7 +489,7 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
locked = fuse_lock_inode(dir);
err = fuse_lookup_name(dir->i_sb, get_node_id(dir), &entry->d_name,
- &outarg, &inode);
+ &outarg, entry, &inode);
fuse_unlock_inode(dir, locked);
if (err == -ENOENT) {
outarg_valid = false;
@@ -455,9 +517,9 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
fuse_advise_use_readdirplus(dir);
return newent;
- out_iput:
+out_iput:
iput(inode);
- out_err:
+out_err:
return ERR_PTR(err);
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index d56d4fd956db..1dc04bc6ac49 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -106,6 +106,11 @@ struct fuse_backing {
struct rcu_head rcu;
};
+struct fuse_entry_backing {
+ struct fuse_entry_backing_out out;
+ struct file *backing_file;
+};
+
/** FUSE inode */
struct fuse_inode {
/** Inode data */
@@ -213,6 +218,14 @@ struct fuse_inode {
/** Reference to backing file in passthrough mode */
struct fuse_backing *fb;
#endif
+
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ /**
+ * Backing inode, if this inode is from a backing file system.
+ * If this is set, nodeid is 0.
+ */
+ struct inode *backing_inode;
+#endif
};
/** FUSE inode state bits */
@@ -1114,13 +1127,17 @@ extern const struct dentry_operations fuse_root_dentry_operations;
/**
* Get a filled in inode
*/
+struct inode *fuse_iget_backing(struct super_block *sb,
+ struct inode *backing_inode);
+
struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
int generation, struct fuse_attr *attr,
u64 attr_valid, u64 attr_version,
u64 evict_ctr);
int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
- struct fuse_entry_out *outarg, struct inode **inode);
+ struct fuse_entry_out *outarg, struct dentry *entry,
+ struct inode **inode);
/**
* Send FORGET command
@@ -1577,4 +1594,7 @@ extern void fuse_sysctl_unregister(void);
#define fuse_sysctl_unregister() do { } while (0)
#endif /* CONFIG_SYSCTL */
+/* backing.c */
+int fuse_handle_backing(struct fuse_entry_backing *feb,
+ struct inode **backing_inode, struct path *backing_path);
#endif /* _FS_FUSE_I_H */
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index fd48e8d37f2e..404d8cbe5f25 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -35,6 +35,8 @@ struct list_head fuse_conn_list;
DEFINE_MUTEX(fuse_mutex);
static int set_global_limit(const char *val, const struct kernel_param *kp);
+static void fuse_fill_attr_from_inode(struct fuse_attr *attr,
+ const struct fuse_inode *fi);
unsigned int fuse_max_pages_limit = 256;
/* default is no timeout */
@@ -195,6 +197,16 @@ static void fuse_evict_inode(struct inode *inode)
}
}
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+static void fuse_destroy_inode(struct inode *inode)
+{
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ if (fi->backing_inode)
+ iput(fi->backing_inode);
+}
+#endif
+
static int fuse_reconfigure(struct fs_context *fsc)
{
struct super_block *sb = fsc->root->d_sb;
@@ -443,22 +455,93 @@ static void fuse_init_inode(struct inode *inode, struct fuse_attr *attr,
inode->i_acl = inode->i_default_acl = ACL_DONT_CACHE;
}
+struct fuse_inode_identifier {
+ u64 nodeid;
+ struct inode *backing_inode;
+};
+
static int fuse_inode_eq(struct inode *inode, void *_nodeidp)
{
- u64 nodeid = *(u64 *) _nodeidp;
- if (get_node_id(inode) == nodeid)
- return 1;
- else
- return 0;
+ struct fuse_inode_identifier *fii =
+ (struct fuse_inode_identifier *) _nodeidp;
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ return fii->nodeid == fi->nodeid;
+}
+
+static int fuse_inode_backing_eq(struct inode *inode, void *_nodeidp)
+{
+ struct fuse_inode_identifier *fii =
+ (struct fuse_inode_identifier *) _nodeidp;
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ return fii->nodeid == fi->nodeid
+ && fii->backing_inode == fi->backing_inode;
}
static int fuse_inode_set(struct inode *inode, void *_nodeidp)
{
- u64 nodeid = *(u64 *) _nodeidp;
- get_fuse_inode(inode)->nodeid = nodeid;
+ struct fuse_inode_identifier *fii =
+ (struct fuse_inode_identifier *) _nodeidp;
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ fi->nodeid = fii->nodeid;
+
+ return 0;
+}
+
+static int fuse_inode_backing_dir_set(struct inode *inode, void *_nodeidp)
+{
+ struct fuse_inode_identifier *fii =
+ (struct fuse_inode_identifier *) _nodeidp;
+ struct fuse_inode *fi = get_fuse_inode(inode);
+
+ fi->nodeid = fii->nodeid;
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ fi->backing_inode = fii->backing_inode;
+ if (fi->backing_inode)
+ ihold(fi->backing_inode);
+#endif
+
return 0;
}
+struct inode *fuse_iget_backing(struct super_block *sb,
+ struct inode *backing_inode)
+{
+ struct inode *inode;
+ struct fuse_inode *fi;
+ struct fuse_conn *fc = get_fuse_conn_super(sb);
+ struct fuse_inode_identifier fii = {
+ .nodeid = 0,
+ .backing_inode = backing_inode,
+ };
+ struct fuse_attr attr;
+ unsigned long hash = (unsigned long) backing_inode;
+
+ fuse_fill_attr_from_inode(&attr, get_fuse_inode(backing_inode));
+ inode = iget5_locked(sb, hash, fuse_inode_backing_eq,
+ fuse_inode_backing_dir_set, &fii);
+ if (!inode)
+ return NULL;
+
+ if ((inode->i_state & I_NEW)) {
+ inode->i_flags |= S_NOATIME;
+ if (!fc->writeback_cache)
+ inode->i_flags |= S_NOCMTIME;
+ fuse_init_common(inode);
+ unlock_new_inode(inode);
+ }
+
+ fi = get_fuse_inode(inode);
+ fuse_init_inode(inode, &attr, fc);
+ spin_lock(&fi->lock);
+ fi->nlookup++;
+ spin_unlock(&fi->lock);
+
+ return inode;
+}
+
struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
int generation, struct fuse_attr *attr,
u64 attr_valid, u64 attr_version,
@@ -467,6 +550,9 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
struct inode *inode;
struct fuse_inode *fi;
struct fuse_conn *fc = get_fuse_conn_super(sb);
+ struct fuse_inode_identifier fii = {
+ .nodeid = nodeid,
+ };
/*
* Auto mount points get their node id from the submount root, which is
@@ -498,7 +584,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
}
retry:
- inode = iget5_locked(sb, nodeid, fuse_inode_eq, fuse_inode_set, &nodeid);
+ inode = iget5_locked(sb, nodeid, fuse_inode_eq, fuse_inode_set, &fii);
if (!inode)
return NULL;
@@ -533,13 +619,16 @@ struct inode *fuse_ilookup(struct fuse_conn *fc, u64 nodeid,
{
struct fuse_mount *fm_iter;
struct inode *inode;
+ struct fuse_inode_identifier fii = {
+ .nodeid = nodeid,
+ };
WARN_ON(!rwsem_is_locked(&fc->killsb));
list_for_each_entry(fm_iter, &fc->mounts, fc_entry) {
if (!fm_iter->sb)
continue;
- inode = ilookup5(fm_iter->sb, nodeid, fuse_inode_eq, &nodeid);
+ inode = ilookup5(fm_iter->sb, nodeid, fuse_inode_eq, &fii);
if (inode) {
if (fm)
*fm = fm_iter;
@@ -1072,7 +1161,7 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
goto out_err;
err = fuse_lookup_name(sb, handle->nodeid, &name, &outarg,
- &inode);
+ NULL, &inode);
if (err && err != -ENOENT)
goto out_err;
if (err || !inode) {
@@ -1173,7 +1262,7 @@ static struct dentry *fuse_get_parent(struct dentry *child)
return ERR_PTR(-ESTALE);
err = fuse_lookup_name(child_inode->i_sb, get_node_id(child_inode),
- &dotdot_name, &outarg, &inode);
+ &dotdot_name, &outarg, NULL, &inode);
if (err) {
if (err == -ENOENT)
return ERR_PTR(-ESTALE);
@@ -1201,6 +1290,9 @@ static const struct export_operations fuse_export_operations = {
static const struct super_operations fuse_super_operations = {
.alloc_inode = fuse_alloc_inode,
+#ifdef CONFIG_FUSE_PASSTHROUGH_DIR
+ .destroy_inode = fuse_destroy_inode,
+#endif
.free_inode = fuse_free_inode,
.evict_inode = fuse_evict_inode,
.write_inode = fuse_write_inode,
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 5ec43ecbceb7..f1bd8e7734ec 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -690,6 +690,10 @@ struct fuse_entry_out {
struct fuse_attr attr;
};
+struct fuse_entry_backing_out {
+ uint64_t backing_fd;
+};
+
struct fuse_forget_in {
uint64_t nlookup;
};
--
2.49.0.1112.g889b7c5bd8-goog
Powered by blists - more mailing lists