* [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation
@ 2026-02-25 11:24 Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface Luis Henriques
` (8 more replies)
0 siblings, 9 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Hi,
I'm sending a new version of my work on lookup_handle, even though it's
still incomplete. As suggested elsewhere, it is now based on compound
commands and thus it sits on top of Horst's patchset [0]. Also, because
this version is a complete re-write of the approach presented in my previous
RFC [1] I'm not going to detail what changed.
Here's a few notes:
- The code isn't yet fully testable as there are several pieces missing.
For example, the FUSE_TMPFILE and FUSE_READDIRPLUS operations are not yet
implemented. The NFS-related changes have also been dropped in this
revision.
- There are several details still to be sorted out in the compound
operations. For example, the nodeid for the statx operation in the
lookup+statx is set to FUSE_ROOT_ID.
- The second operation (mkobj_handle+statx+open) is still draft (or maybe
just wrong!). It's not handling flags correctly, and the error handling
has to be better thought out.
- Some of the patches in this set could probably be picked independently
(e.g. patch 4 or even patch 1)
So, why am I sending this broken and incomplete patchset? Well, simply
because I'd feel more confidence getting this approach validated. I don't
expect any through review, but I would appreciate feedback on anything that
would help me correct major flaws.
[0] https://lore.kernel.org/all/20260210-fuse-compounds-upstream-v5-0-ea0585f62daa@ddn.com
[1] https://lore.kernel.org/all/20251212181254.59365-1-luis@igalia.com
Cheers,
--
Luis
Luis Henriques (8):
fuse: simplify fuse_lookup_name() interface
fuse: export extend_arg() and factor out fuse_ext_size()
fuse: store index of the variable length argument
fuse: drop unnecessary argument from fuse_lookup_init()
fuse: extract helper functions from fuse_do_statx()
fuse: implementation of lookup_handle+statx compound operation
fuse: export fuse_open_args_fill() helper function
fuse: implementation of mkobj_handle+statx+open compound operation
fs/fuse/compound.c | 1 +
fs/fuse/cuse.c | 1 +
fs/fuse/dev.c | 4 +-
fs/fuse/dir.c | 650 +++++++++++++++++++++++++++++++++-----
fs/fuse/file.c | 3 +-
fs/fuse/fuse_i.h | 42 ++-
fs/fuse/inode.c | 50 ++-
fs/fuse/ioctl.c | 1 +
fs/fuse/readdir.c | 2 +-
fs/fuse/virtio_fs.c | 6 +-
fs/fuse/xattr.c | 2 +
include/uapi/linux/fuse.h | 25 +-
12 files changed, 679 insertions(+), 108 deletions(-)
^ permalink raw reply [flat|nested] 26+ messages in thread
* [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-27 15:46 ` Miklos Szeredi
2026-02-25 11:24 ` [RFC PATCH v3 2/8] fuse: export extend_arg() and factor out fuse_ext_size() Luis Henriques
` (7 subsequent siblings)
8 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
fuse_lookup_name() requires a struct fuse_entry_out to be passed in.
However, the only caller that really needs it is fuse_lookup(). And even
this function only cares about the dentry time.
This patch simplifies fuse_lookup_name() so that it doesn't require a struct
as a parameter, replacing it by a local variable. Instead, there'll be an
(optional) out argument, that can be used to return the dentry time.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 36 +++++++++++++++++++-----------------
fs/fuse/fuse_i.h | 2 +-
fs/fuse/inode.c | 7 ++-----
3 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index ef297b867060..e3000affff88 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -548,10 +548,11 @@ 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)
+ u64 *time, struct inode **inode)
{
struct fuse_mount *fm = get_fuse_mount_super(sb);
FUSE_ARGS(args);
+ struct fuse_entry_out outarg;
struct fuse_forget_link *forget;
u64 attr_version, evict_ctr;
int err;
@@ -570,30 +571,34 @@ 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);
err = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT, but with valid timeout */
- if (err || !outarg->nodeid)
+ if (err || !outarg.nodeid)
goto out_put_forget;
err = -EIO;
- if (fuse_invalid_attr(&outarg->attr))
+ if (fuse_invalid_attr(&outarg.attr))
goto out_put_forget;
- if (outarg->nodeid == FUSE_ROOT_ID && outarg->generation != 0) {
+ if (outarg.nodeid == FUSE_ROOT_ID && outarg.generation != 0) {
pr_warn_once("root generation should be zero\n");
- outarg->generation = 0;
+ outarg.generation = 0;
}
- *inode = fuse_iget(sb, outarg->nodeid, outarg->generation,
- &outarg->attr, ATTR_TIMEOUT(outarg),
+ *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);
+ fuse_queue_forget(fm->fc, forget, outarg.nodeid, 1);
goto out;
}
err = 0;
+ if (time)
+ *time = fuse_time_to_jiffies(outarg.entry_valid,
+ outarg.entry_valid_nsec);
+
out_put_forget:
kfree(forget);
out:
@@ -603,12 +608,11 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
unsigned int flags)
{
- struct fuse_entry_out outarg;
struct fuse_conn *fc;
struct inode *inode;
struct dentry *newent;
+ u64 time = 0;
int err, epoch;
- bool outarg_valid = true;
bool locked;
if (fuse_is_bad(dir))
@@ -619,12 +623,10 @@ 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);
+ &time, &inode);
fuse_unlock_inode(dir, locked);
- if (err == -ENOENT) {
- outarg_valid = false;
+ if (err == -ENOENT)
err = 0;
- }
if (err)
goto out_err;
@@ -639,8 +641,8 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
entry = newent ? newent : entry;
entry->d_time = epoch;
- if (outarg_valid)
- fuse_change_entry_timeout(entry, &outarg);
+ if (time)
+ fuse_dentry_settime(entry, time);
else
fuse_invalidate_entry_cache(entry);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 3184ef864cf0..6178a012f36c 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1149,7 +1149,7 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
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);
+ u64 *time, struct inode **inode);
/**
* Send FORGET command
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 38ca362ee2ca..8231c207abea 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1089,14 +1089,12 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
inode = ilookup5(sb, handle->nodeid, fuse_inode_eq, &handle->nodeid);
if (!inode) {
- struct fuse_entry_out outarg;
const struct qstr name = QSTR_INIT(".", 1);
if (!fc->export_support)
goto out_err;
- err = fuse_lookup_name(sb, handle->nodeid, &name, &outarg,
- &inode);
+ err = fuse_lookup_name(sb, handle->nodeid, &name, NULL, &inode);
if (err && err != -ENOENT)
goto out_err;
if (err || !inode) {
@@ -1190,14 +1188,13 @@ static struct dentry *fuse_get_parent(struct dentry *child)
struct fuse_conn *fc = get_fuse_conn(child_inode);
struct inode *inode;
struct dentry *parent;
- struct fuse_entry_out outarg;
int err;
if (!fc->export_support)
return ERR_PTR(-ESTALE);
err = fuse_lookup_name(child_inode->i_sb, get_node_id(child_inode),
- &dotdot_name, &outarg, &inode);
+ &dotdot_name, NULL, &inode);
if (err) {
if (err == -ENOENT)
return ERR_PTR(-ESTALE);
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 2/8] fuse: export extend_arg() and factor out fuse_ext_size()
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 3/8] fuse: store index of the variable length argument Luis Henriques
` (6 subsequent siblings)
8 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Export (and rename) extend_arg() and fuse_ext_size() as these functions
are useful for using extension headers in other places.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 9 ++-------
fs/fuse/fuse_i.h | 7 +++++++
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index e3000affff88..f5eacea44896 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -713,7 +713,7 @@ static int get_security_context(struct dentry *entry, umode_t mode,
return err;
}
-static void *extend_arg(struct fuse_in_arg *buf, u32 bytes)
+void *fuse_extend_arg(struct fuse_in_arg *buf, u32 bytes)
{
void *p;
u32 newlen = buf->size + bytes;
@@ -733,11 +733,6 @@ static void *extend_arg(struct fuse_in_arg *buf, u32 bytes)
return p + newlen - bytes;
}
-static u32 fuse_ext_size(size_t size)
-{
- return FUSE_REC_ALIGN(sizeof(struct fuse_ext_header) + size);
-}
-
/*
* This adds just a single supplementary group that matches the parent's group.
*/
@@ -758,7 +753,7 @@ static int get_create_supp_group(struct mnt_idmap *idmap,
!vfsgid_in_group_p(vfsgid))
return 0;
- xh = extend_arg(ext, sg_len);
+ xh = fuse_extend_arg(ext, sg_len);
if (!xh)
return -ENOMEM;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 6178a012f36c..135027efec7a 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1410,6 +1410,13 @@ int fuse_valid_type(int m);
bool fuse_invalid_attr(struct fuse_attr *attr);
+void *fuse_extend_arg(struct fuse_in_arg *buf, u32 bytes);
+
+static inline u32 fuse_ext_size(size_t size)
+{
+ return FUSE_REC_ALIGN(sizeof(struct fuse_ext_header) + size);
+}
+
/**
* Is current process allowed to perform filesystem operation?
*/
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 3/8] fuse: store index of the variable length argument
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 2/8] fuse: export extend_arg() and factor out fuse_ext_size() Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-27 15:41 ` Miklos Szeredi
2026-02-25 11:24 ` [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init() Luis Henriques
` (5 subsequent siblings)
8 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Operations that have a variable length argument assume that it will always
be the last argument on the list. This patch allows this assumption to be
removed by keeping track of the index of variable length argument.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/compound.c | 1 +
fs/fuse/cuse.c | 1 +
fs/fuse/dev.c | 4 ++--
fs/fuse/dir.c | 1 +
fs/fuse/file.c | 1 +
fs/fuse/fuse_i.h | 2 ++
fs/fuse/inode.c | 1 +
fs/fuse/ioctl.c | 1 +
fs/fuse/virtio_fs.c | 6 +++---
fs/fuse/xattr.c | 2 ++
10 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/fs/fuse/compound.c b/fs/fuse/compound.c
index 1a85209f4e99..2dc024301aad 100644
--- a/fs/fuse/compound.c
+++ b/fs/fuse/compound.c
@@ -153,6 +153,7 @@ ssize_t fuse_compound_send(struct fuse_compound_req *compound)
.in_numargs = 2,
.out_numargs = 2,
.out_argvar = true,
+ .out_argvar_idx = 1,
};
unsigned int req_count = compound->compound_header.count;
size_t total_expected_out_size = 0;
diff --git a/fs/fuse/cuse.c b/fs/fuse/cuse.c
index dfcb98a654d8..3ce8ee9a4275 100644
--- a/fs/fuse/cuse.c
+++ b/fs/fuse/cuse.c
@@ -460,6 +460,7 @@ static int cuse_send_init(struct cuse_conn *cc)
ap->args.out_args[0].value = &ia->out;
ap->args.out_args[1].size = CUSE_INIT_INFO_MAX;
ap->args.out_argvar = true;
+ ap->args.out_argvar_idx = 1;
ap->args.out_pages = true;
ap->num_folios = 1;
ap->folios = &ia->folio;
diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 0b0241f47170..5b02724f4377 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -694,7 +694,7 @@ ssize_t __fuse_simple_request(struct mnt_idmap *idmap,
ret = req->out.h.error;
if (!ret && args->out_argvar) {
BUG_ON(args->out_numargs == 0);
- ret = args->out_args[args->out_numargs - 1].size;
+ ret = args->out_args[args->out_argvar_idx].size;
}
fuse_put_request(req);
@@ -2157,7 +2157,7 @@ int fuse_copy_out_args(struct fuse_copy_state *cs, struct fuse_args *args,
if (reqsize < nbytes || (reqsize > nbytes && !args->out_argvar))
return -EINVAL;
else if (reqsize > nbytes) {
- struct fuse_arg *lastarg = &args->out_args[args->out_numargs-1];
+ struct fuse_arg *lastarg = &args->out_args[args->out_argvar_idx];
unsigned diffsize = reqsize - nbytes;
if (diffsize > lastarg->size)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index f5eacea44896..a1121feb63ee 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1835,6 +1835,7 @@ static int fuse_readlink_folio(struct inode *inode, struct folio *folio)
ap.args.nodeid = get_node_id(inode);
ap.args.out_pages = true;
ap.args.out_argvar = true;
+ ap.args.out_argvar_idx = 0;
ap.args.page_zeroing = true;
ap.args.out_numargs = 1;
ap.args.out_args[0].size = desc.length;
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 49c21498230d..1045d74dd95f 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -682,6 +682,7 @@ void fuse_read_args_fill(struct fuse_io_args *ia, struct file *file, loff_t pos,
args->in_args[0].size = sizeof(ia->read.in);
args->in_args[0].value = &ia->read.in;
args->out_argvar = true;
+ args->out_argvar_idx = 0;
args->out_numargs = 1;
args->out_args[0].size = count;
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 135027efec7a..04f09e2ccfd0 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -331,6 +331,8 @@ struct fuse_args {
uint32_t opcode;
uint8_t in_numargs;
uint8_t out_numargs;
+ /* The index of the variable length out arg */
+ uint8_t out_argvar_idx;
uint8_t ext_idx;
bool force:1;
bool noreply:1;
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 8231c207abea..006436a3ad06 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1540,6 +1540,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
with interface version < 7.5. Rest of init_out is zeroed
by do_get_request(), so a short reply is not a problem */
ia->args.out_argvar = true;
+ ia->args.out_argvar_idx = 0;
ia->args.out_args[0].size = sizeof(ia->out);
ia->args.out_args[0].value = &ia->out;
ia->args.force = true;
diff --git a/fs/fuse/ioctl.c b/fs/fuse/ioctl.c
index 07a02e47b2c3..7eb8d7a59edc 100644
--- a/fs/fuse/ioctl.c
+++ b/fs/fuse/ioctl.c
@@ -337,6 +337,7 @@ long fuse_do_ioctl(struct file *file, unsigned int cmd, unsigned long arg,
ap.args.out_args[1].size = out_size;
ap.args.out_pages = true;
ap.args.out_argvar = true;
+ ap.args.out_argvar_idx = 1;
transferred = fuse_send_ioctl(fm, &ap.args, &outarg);
err = transferred;
diff --git a/fs/fuse/virtio_fs.c b/fs/fuse/virtio_fs.c
index 057e65b51b99..dd681bc672b8 100644
--- a/fs/fuse/virtio_fs.c
+++ b/fs/fuse/virtio_fs.c
@@ -738,7 +738,7 @@ static void copy_args_from_argbuf(struct fuse_args *args, struct fuse_req *req)
unsigned int argsize = args->out_args[i].size;
if (args->out_argvar &&
- i == args->out_numargs - 1 &&
+ i == args->out_argvar_idx &&
argsize > remaining) {
argsize = remaining;
}
@@ -746,13 +746,13 @@ static void copy_args_from_argbuf(struct fuse_args *args, struct fuse_req *req)
memcpy(args->out_args[i].value, req->argbuf + offset, argsize);
offset += argsize;
- if (i != args->out_numargs - 1)
+ if (i != args->out_argvar_idx)
remaining -= argsize;
}
/* Store the actual size of the variable-length arg */
if (args->out_argvar)
- args->out_args[args->out_numargs - 1].size = remaining;
+ args->out_args[args->out_argvar_idx].size = remaining;
kfree(req->argbuf);
req->argbuf = NULL;
diff --git a/fs/fuse/xattr.c b/fs/fuse/xattr.c
index 93dfb06b6cea..f123446fe537 100644
--- a/fs/fuse/xattr.c
+++ b/fs/fuse/xattr.c
@@ -73,6 +73,7 @@ ssize_t fuse_getxattr(struct inode *inode, const char *name, void *value,
args.out_numargs = 1;
if (size) {
args.out_argvar = true;
+ args.out_argvar_idx = 0;
args.out_args[0].size = size;
args.out_args[0].value = value;
} else {
@@ -135,6 +136,7 @@ ssize_t fuse_listxattr(struct dentry *entry, char *list, size_t size)
args.out_numargs = 1;
if (size) {
args.out_argvar = true;
+ args.out_argvar_idx = 0;
args.out_args[0].size = size;
args.out_args[0].value = list;
} else {
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init()
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (2 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 3/8] fuse: store index of the variable length argument Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-27 15:57 ` Miklos Szeredi
2026-02-25 11:24 ` [RFC PATCH v3 5/8] fuse: extract helper functions from fuse_do_statx() Luis Henriques
` (4 subsequent siblings)
8 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Remove the fuse_conn argument from function fuse_lookup_init() as it isn't
used since commit 21f621741a77 ("fuse: fix LOOKUP vs INIT compat handling").
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index a1121feb63ee..92c9ebfb4985 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -354,8 +354,8 @@ static void fuse_invalidate_entry(struct dentry *entry)
fuse_invalidate_entry_cache(entry);
}
-static void fuse_lookup_init(struct fuse_conn *fc, struct fuse_args *args,
- u64 nodeid, const struct qstr *name,
+static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
+ const struct qstr *name,
struct fuse_entry_out *outarg)
{
memset(outarg, 0, sizeof(struct fuse_entry_out));
@@ -421,8 +421,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);
+ fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
ret = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT */
if (!ret && !outarg.nodeid)
@@ -571,7 +570,7 @@ 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(&args, nodeid, name, &outarg);
err = fuse_simple_request(fm, &args);
/* Zero nodeid is same as -ENOENT, but with valid timeout */
if (err || !outarg.nodeid)
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 5/8] fuse: extract helper functions from fuse_do_statx()
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (3 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init() Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation Luis Henriques
` (3 subsequent siblings)
8 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Split function fuse_do_statx(), so that we get two helper functions: one
to initialise the arguments and another to update the attributes and
statistics. This will allow compound operations to re-use these two helpers.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 88 +++++++++++++++++++++++++++++++--------------------
1 file changed, 53 insertions(+), 35 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 92c9ebfb4985..5c0f1364c392 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1406,43 +1406,37 @@ static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr)
attr->blksize = sx->blksize;
}
-static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
- struct file *file, struct kstat *stat)
+static void fuse_statx_init(struct fuse_args *args, struct fuse_statx_in *inarg,
+ u64 nodeid, struct fuse_file *ff,
+ struct fuse_statx_out *outarg)
{
- int err;
- struct fuse_attr attr;
- struct fuse_statx *sx;
- struct fuse_statx_in inarg;
- struct fuse_statx_out outarg;
- struct fuse_mount *fm = get_fuse_mount(inode);
- u64 attr_version = fuse_get_attr_version(fm->fc);
- FUSE_ARGS(args);
-
- memset(&inarg, 0, sizeof(inarg));
- memset(&outarg, 0, sizeof(outarg));
- /* Directories have separate file-handle space */
- if (file && S_ISREG(inode->i_mode)) {
- struct fuse_file *ff = file->private_data;
+ memset(inarg, 0, sizeof(*inarg));
+ memset(outarg, 0, sizeof(*outarg));
- inarg.getattr_flags |= FUSE_GETATTR_FH;
- inarg.fh = ff->fh;
+ if (ff) {
+ inarg->getattr_flags |= FUSE_GETATTR_FH;
+ inarg->fh = ff->fh;
}
/* For now leave sync hints as the default, request all stats. */
- inarg.sx_flags = 0;
- inarg.sx_mask = STATX_BASIC_STATS | STATX_BTIME;
- args.opcode = FUSE_STATX;
- args.nodeid = get_node_id(inode);
- args.in_numargs = 1;
- args.in_args[0].size = sizeof(inarg);
- args.in_args[0].value = &inarg;
- args.out_numargs = 1;
- args.out_args[0].size = sizeof(outarg);
- args.out_args[0].value = &outarg;
- err = fuse_simple_request(fm, &args);
- if (err)
- return err;
+ inarg->sx_flags = 0;
+ inarg->sx_mask = STATX_BASIC_STATS | STATX_BTIME;
+ args->opcode = FUSE_STATX;
+ args->nodeid = nodeid;
+ args->in_numargs = 1;
+ args->in_args[0].size = sizeof(*inarg);
+ args->in_args[0].value = inarg;
+ args->out_numargs = 1;
+ args->out_args[0].size = sizeof(*outarg);
+ args->out_args[0].value = outarg;
+}
+
+static int fuse_statx_update(struct mnt_idmap *idmap,
+ struct fuse_statx_out *outarg, struct inode *inode,
+ u64 attr_version, struct kstat *stat)
+{
+ struct fuse_statx *sx = &outarg->stat;
+ struct fuse_attr attr;
- sx = &outarg.stat;
if (((sx->mask & STATX_SIZE) && !fuse_valid_size(sx->size)) ||
((sx->mask & STATX_TYPE) && (!fuse_valid_type(sx->mode) ||
inode_wrong_type(inode, sx->mode)))) {
@@ -1450,10 +1444,10 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
return -EIO;
}
- fuse_statx_to_attr(&outarg.stat, &attr);
+ fuse_statx_to_attr(sx, &attr);
if ((sx->mask & STATX_BASIC_STATS) == STATX_BASIC_STATS) {
- fuse_change_attributes(inode, &attr, &outarg.stat,
- ATTR_TIMEOUT(&outarg), attr_version);
+ fuse_change_attributes(inode, &attr, sx,
+ ATTR_TIMEOUT(outarg), attr_version);
}
if (stat) {
@@ -1467,6 +1461,30 @@ static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
return 0;
}
+static int fuse_do_statx(struct mnt_idmap *idmap, struct inode *inode,
+ struct file *file, struct kstat *stat)
+{
+ int err;
+ struct fuse_statx_in inarg;
+ struct fuse_statx_out outarg;
+ struct fuse_mount *fm = get_fuse_mount(inode);
+ struct fuse_file *ff = NULL;
+ u64 attr_version = fuse_get_attr_version(fm->fc);
+ FUSE_ARGS(args);
+
+ /* Directories have separate file-handle space */
+ if (file && S_ISREG(inode->i_mode))
+ ff = file->private_data;
+
+ fuse_statx_init(&args, &inarg, get_node_id(inode), ff, &outarg);
+
+ err = fuse_simple_request(fm, &args);
+ if (err)
+ return err;
+
+ return fuse_statx_update(idmap, &outarg, inode, attr_version, stat);
+}
+
/*
* Helper function to initialize fuse_args for GETATTR operations
*/
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (4 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 5/8] fuse: extract helper functions from fuse_do_statx() Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-25 18:06 ` Amir Goldstein
2026-02-25 11:24 ` [RFC PATCH v3 7/8] fuse: export fuse_open_args_fill() helper function Luis Henriques
` (2 subsequent siblings)
8 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
The implementation of lookup_handle+statx compound operation extends the
lookup operation so that a file handle is be passed into the kernel. It
also needs to include an extra inarg, so that the parent directory file
handle can be sent to user-space. This extra inarg is added as an extension
header to the request.
By having a separate statx including in a compound operation allows the
attr to be dropped from the lookup_handle request, simplifying the
traditional FUSE lookup operation.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 294 +++++++++++++++++++++++++++++++++++---
fs/fuse/fuse_i.h | 23 ++-
fs/fuse/inode.c | 48 +++++--
fs/fuse/readdir.c | 2 +-
include/uapi/linux/fuse.h | 23 ++-
5 files changed, 355 insertions(+), 35 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 5c0f1364c392..7fa8c405f1a3 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -21,6 +21,7 @@
#include <linux/security.h>
#include <linux/types.h>
#include <linux/kernel.h>
+#include <linux/exportfs.h>
static bool __read_mostly allow_sys_admin_access;
module_param(allow_sys_admin_access, bool, 0644);
@@ -372,6 +373,47 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
args->out_args[0].value = outarg;
}
+static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
+ struct inode *parent_inode,
+ const struct qstr *name,
+ struct fuse_entry2_out *lookup_out,
+ struct fuse_statx_out *statx_out,
+ struct fuse_file_handle **fh);
+static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr);
+static int do_reval_lookup(struct fuse_mount *fm, u64 parent_nodeid,
+ const struct qstr *name, u64 *nodeid,
+ u64 *generation, u64 *attr_valid,
+ struct fuse_attr *attr, struct fuse_file_handle **fh)
+{
+ struct fuse_entry_out entry_out;
+ struct fuse_entry2_out lookup_out;
+ struct fuse_statx_out statx_out;
+ FUSE_ARGS(lookup_args);
+ int ret = 0;
+
+ if (fm->fc->lookup_handle) {
+ ret = do_lookup_handle_statx(fm, parent_nodeid, NULL, name,
+ &lookup_out, &statx_out, fh);
+ if (!ret) {
+ *nodeid = lookup_out.nodeid;
+ *generation = lookup_out.generation;
+ *attr_valid = fuse_time_to_jiffies(lookup_out.entry_valid,
+ lookup_out.entry_valid_nsec);
+ fuse_statx_to_attr(&statx_out.stat, attr);
+ }
+ } else {
+ fuse_lookup_init(&lookup_args, parent_nodeid, name, &entry_out);
+ ret = fuse_simple_request(fm, &lookup_args);
+ if (!ret) {
+ *nodeid = entry_out.nodeid;
+ *generation = entry_out.generation;
+ *attr_valid = ATTR_TIMEOUT(&entry_out);
+ memcpy(attr, &entry_out.attr, sizeof(*attr));
+ }
+ }
+
+ return ret;
+}
/*
* Check whether the dentry is still valid
*
@@ -399,10 +441,11 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
goto invalid;
else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
(flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
- struct fuse_entry_out outarg;
- FUSE_ARGS(args);
struct fuse_forget_link *forget;
+ struct fuse_file_handle *fh = NULL;
u64 attr_version;
+ u64 nodeid, generation, attr_valid;
+ struct fuse_attr attr;
/* For negative dentries, always do a fresh lookup */
if (!inode)
@@ -421,35 +464,36 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
attr_version = fuse_get_attr_version(fm->fc);
- fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
- ret = fuse_simple_request(fm, &args);
+ ret = do_reval_lookup(fm, get_node_id(dir), name, &nodeid,
+ &generation, &attr_valid, &attr, &fh);
/* Zero nodeid is same as -ENOENT */
- if (!ret && !outarg.nodeid)
+ if (!ret && !nodeid)
ret = -ENOENT;
if (!ret) {
fi = get_fuse_inode(inode);
- if (outarg.nodeid != get_node_id(inode) ||
- (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
- fuse_queue_forget(fm->fc, forget,
- outarg.nodeid, 1);
+ if (!fuse_file_handle_is_equal(fm->fc, fi->fh, fh) ||
+ nodeid != get_node_id(inode) ||
+ (bool) IS_AUTOMOUNT(inode) != (bool) (attr.flags & FUSE_ATTR_SUBMOUNT)) {
+ fuse_queue_forget(fm->fc, forget, nodeid, 1);
+ kfree(fh);
goto invalid;
}
spin_lock(&fi->lock);
fi->nlookup++;
spin_unlock(&fi->lock);
}
+ kfree(fh);
kfree(forget);
if (ret == -ENOMEM || ret == -EINTR)
goto out;
- if (ret || fuse_invalid_attr(&outarg.attr) ||
- fuse_stale_inode(inode, outarg.generation, &outarg.attr))
+ if (ret || fuse_invalid_attr(&attr) ||
+ fuse_stale_inode(inode, generation, &attr))
goto invalid;
forget_all_cached_acls(inode);
- fuse_change_attributes(inode, &outarg.attr, NULL,
- ATTR_TIMEOUT(&outarg),
+ fuse_change_attributes(inode, &attr, NULL, attr_valid,
attr_version);
- fuse_change_entry_timeout(entry, &outarg);
+ fuse_dentry_settime(entry, attr_valid);
} else if (inode) {
fi = get_fuse_inode(inode);
if (flags & LOOKUP_RCU) {
@@ -546,8 +590,215 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size);
}
-int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
- u64 *time, struct inode **inode)
+static int create_ext_handle(struct fuse_in_arg *ext, struct fuse_inode *fi)
+{
+ struct fuse_ext_header *xh;
+ struct fuse_file_handle *fh;
+ u32 len;
+
+ len = fuse_ext_size(sizeof(*fi->fh) + fi->fh->size);
+ xh = fuse_extend_arg(ext, len);
+ if (!xh)
+ return -ENOMEM;
+
+ xh->size = len;
+ xh->type = FUSE_EXT_HANDLE;
+ fh = (struct fuse_file_handle *)&xh[1];
+ fh->size = fi->fh->size;
+ memcpy(fh->handle, fi->fh->handle, fh->size);
+
+ return 0;
+}
+
+static int fuse_lookup_handle_init(struct fuse_args *args, u64 nodeid,
+ struct fuse_inode *fi,
+ const struct qstr *name,
+ struct fuse_entry2_out *outarg)
+{
+ struct fuse_file_handle *fh;
+ size_t fh_size = sizeof(*fh) + MAX_HANDLE_SZ;
+ int ret = -ENOMEM;
+
+ fh = kzalloc(fh_size, GFP_KERNEL);
+ if (!fh)
+ return ret;
+
+ memset(outarg, 0, sizeof(struct fuse_entry2_out));
+ args->opcode = FUSE_LOOKUP_HANDLE;
+ args->nodeid = nodeid;
+ args->in_numargs = 3;
+ fuse_set_zero_arg0(args);
+ args->in_args[1].size = name->len;
+ args->in_args[1].value = name->name;
+ args->in_args[2].size = 1;
+ args->in_args[2].value = "";
+ if (fi && fi->fh) {
+ args->is_ext = true;
+ args->ext_idx = args->in_numargs++;
+ args->in_args[args->ext_idx].size = 0;
+ ret = create_ext_handle(&args->in_args[args->ext_idx], fi);
+ if (ret) {
+ kfree(fh);
+ return ret;
+ }
+ }
+ args->out_numargs = 2;
+ args->out_argvar = true;
+ args->out_argvar_idx = 1;
+ args->out_args[0].size = sizeof(struct fuse_entry2_out);
+ args->out_args[0].value = outarg;
+
+ /* XXX do allocation to the actual size of the handle */
+ args->out_args[1].size = fh_size;
+ args->out_args[1].value = fh;
+
+ return 0;
+}
+
+static void fuse_req_free_argvar_ext(struct fuse_args *args)
+{
+ if (args->out_argvar)
+ kfree(args->out_args[args->out_argvar_idx].value);
+ if (args->is_ext)
+ kfree(args->in_args[args->ext_idx].value);
+}
+
+static void fuse_statx_init(struct fuse_args *args, struct fuse_statx_in *inarg,
+ u64 nodeid, struct fuse_file *ff,
+ struct fuse_statx_out *outarg);
+static int fuse_statx_update(struct mnt_idmap *idmap,
+ struct fuse_statx_out *outarg, struct inode *inode,
+ u64 attr_version, struct kstat *stat);
+static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
+ struct inode *parent_inode,
+ const struct qstr *name,
+ struct fuse_entry2_out *lookup_out,
+ struct fuse_statx_out *statx_out,
+ struct fuse_file_handle **fh)
+{
+ struct fuse_compound_req *compound;
+ struct fuse_inode *fi = NULL;
+ struct fuse_statx_in statx_in;
+ FUSE_ARGS(lookup_args);
+ FUSE_ARGS(statx_args);
+ int ret;
+
+ if (parent_inode)
+ fi = get_fuse_inode(parent_inode);
+
+ compound = fuse_compound_alloc(fm, 0);
+ if (!compound)
+ return -ENOMEM;
+
+ ret = fuse_lookup_handle_init(&lookup_args, parent_nodeid, fi,
+ name, lookup_out);
+ if (ret)
+ goto out_compound;
+
+ ret = fuse_compound_add(compound, &lookup_args);
+ if (ret)
+ goto out_args;
+
+ /*
+ * XXX nodeid is the parent of the inode we want! At this point
+ * we still don't have the nodeid. Using FUSE_ROOT_ID for now.
+ */
+ fuse_statx_init(&statx_args, &statx_in, FUSE_ROOT_ID, NULL, statx_out);
+ ret = fuse_compound_add(compound, &statx_args);
+ if (ret)
+ goto out_args;
+
+ ret = fuse_compound_send(compound);
+ if (ret)
+ goto out_args;
+
+ ret = fuse_compound_get_error(compound, 0);
+ if (ret || !lookup_out->nodeid)
+ goto out_args;
+ if (lookup_out->nodeid == FUSE_ROOT_ID &&
+ lookup_out->generation != 0) {
+ pr_warn_once("root generation should be zero\n");
+ lookup_out->generation = 0;
+ }
+ if ((lookup_args.out_args[1].size > 0) &&
+ (lookup_args.out_args[1].value)) {
+ struct fuse_file_handle *h = lookup_args.out_args[1].value;
+
+ *fh = kzalloc(sizeof(*fh) + h->size, GFP_KERNEL);
+ if (!*fh) {
+ ret = -ENOMEM;
+ goto out_args;
+ }
+ (*fh)->size = h->size;
+ memcpy((*fh)->handle, h->handle, (*fh)->size);
+ }
+
+ ret = fuse_compound_get_error(compound, 1);
+ if (ret) {
+ kfree(*fh);
+ *fh = NULL;
+ }
+
+out_args:
+ fuse_req_free_argvar_ext(&lookup_args);
+out_compound:
+ kfree(compound);
+
+ return ret;
+}
+
+static int fuse_lookup_handle_name(struct super_block *sb, u64 nodeid,
+ struct inode *dir, const struct qstr *name,
+ u64 *time, struct inode **inode)
+{
+ struct fuse_mount *fm = get_fuse_mount_super(sb);
+ struct fuse_file_handle *fh = NULL;
+ struct fuse_entry2_out lookup_out;
+ struct fuse_statx_out statx_out;
+ struct fuse_attr attr;
+ struct fuse_forget_link *forget;
+ u64 attr_version, evict_ctr;
+ int ret;
+
+ forget = fuse_alloc_forget();
+ if (!forget)
+ return -ENOMEM;
+
+ attr_version = fuse_get_attr_version(fm->fc);
+ evict_ctr = fuse_get_evict_ctr(fm->fc);
+
+ ret = do_lookup_handle_statx(fm, nodeid, dir, name, &lookup_out,
+ &statx_out, &fh);
+ if (ret)
+ goto out_forget;
+
+ fuse_statx_to_attr(&statx_out.stat, &attr);
+ WARN_ON(fuse_invalid_attr(&attr));
+
+ *inode = fuse_iget(sb, lookup_out.nodeid, lookup_out.generation,
+ &attr, ATTR_TIMEOUT(&statx_out),
+ attr_version, evict_ctr, fh);
+ ret = -ENOMEM;
+ if (!*inode) {
+ fuse_queue_forget(fm->fc, forget, lookup_out.nodeid, 1);
+ goto out_forget;
+ }
+ if (time)
+ *time = fuse_time_to_jiffies(lookup_out.entry_valid,
+ lookup_out.entry_valid_nsec);
+
+ /* XXX idmap? */
+ ret = fuse_statx_update(&nop_mnt_idmap, &statx_out, *inode,
+ attr_version, NULL);
+
+out_forget:
+ kfree(forget);
+
+ return ret;
+}
+
+int fuse_lookup_name(struct super_block *sb, u64 nodeid, struct inode *dir,
+ const struct qstr *name, u64 *time, struct inode **inode)
{
struct fuse_mount *fm = get_fuse_mount_super(sb);
FUSE_ARGS(args);
@@ -561,6 +812,9 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
if (name->len > fm->fc->name_max)
goto out;
+ if (fm->fc->lookup_handle)
+ return fuse_lookup_handle_name(sb, nodeid, dir, name, time,
+ inode);
forget = fuse_alloc_forget();
err = -ENOMEM;
@@ -586,7 +840,7 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name
*inode = fuse_iget(sb, outarg.nodeid, outarg.generation,
&outarg.attr, ATTR_TIMEOUT(&outarg),
- attr_version, evict_ctr);
+ attr_version, evict_ctr, NULL);
err = -ENOMEM;
if (!*inode) {
fuse_queue_forget(fm->fc, forget, outarg.nodeid, 1);
@@ -621,7 +875,7 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry,
epoch = atomic_read(&fc->epoch);
locked = fuse_lock_inode(dir);
- err = fuse_lookup_name(dir->i_sb, get_node_id(dir), &entry->d_name,
+ err = fuse_lookup_name(dir->i_sb, get_node_id(dir), dir, &entry->d_name,
&time, &inode);
fuse_unlock_inode(dir, locked);
if (err == -ENOENT)
@@ -882,7 +1136,7 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
ff->nodeid = outentry.nodeid;
ff->open_flags = outopenp->open_flags;
inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation,
- &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0);
+ &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0, NULL);
if (!inode) {
flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
fuse_sync_release(NULL, ff, flags);
@@ -1009,7 +1263,7 @@ static struct dentry *create_new_entry(struct mnt_idmap *idmap, struct fuse_moun
goto out_put_forget_req;
inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation,
- &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0);
+ &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0, NULL);
if (!inode) {
fuse_queue_forget(fm->fc, forget, outarg.nodeid, 1);
return ERR_PTR(-ENOMEM);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 04f09e2ccfd0..173032887fc2 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -223,6 +223,8 @@ struct fuse_inode {
* so preserve the blocksize specified by the server.
*/
u8 cached_i_blkbits;
+
+ struct fuse_file_handle *fh;
};
/** FUSE inode state bits */
@@ -923,6 +925,9 @@ struct fuse_conn {
/* Is synchronous FUSE_INIT allowed? */
unsigned int sync_init:1;
+ /** Is LOOKUP_HANDLE implemented by the fs? */
+ unsigned int lookup_handle:1;
+
/* Use io_uring for communication */
unsigned int io_uring;
@@ -1072,6 +1077,18 @@ static inline int invalid_nodeid(u64 nodeid)
return !nodeid || nodeid == FUSE_ROOT_ID;
}
+static inline bool fuse_file_handle_is_equal(struct fuse_conn *fc,
+ struct fuse_file_handle *fh1,
+ struct fuse_file_handle *fh2)
+{
+ if (!fc->lookup_handle ||
+ ((fh1 == fh2) && !fh1) || /* both NULL */
+ (fh1 && fh2 && (fh1->size == fh2->size) &&
+ (!memcmp(fh1->handle, fh2->handle, fh1->size))))
+ return true;
+ return false;
+}
+
static inline u64 fuse_get_attr_version(struct fuse_conn *fc)
{
return atomic64_read(&fc->attr_version);
@@ -1148,10 +1165,10 @@ extern const struct dentry_operations fuse_dentry_operations;
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);
+ u64 evict_ctr, struct fuse_file_handle *fh);
-int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
- u64 *time, struct inode **inode);
+int fuse_lookup_name(struct super_block *sb, u64 nodeid, struct inode *dir,
+ const struct qstr *name, u64 *time, struct inode **inode);
/**
* Send FORGET command
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 006436a3ad06..9f2c0c9e877c 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -120,6 +120,8 @@ static struct inode *fuse_alloc_inode(struct super_block *sb)
if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
fuse_inode_backing_set(fi, NULL);
+ fi->fh = NULL;
+
return &fi->inode;
out_free_forget:
@@ -141,6 +143,8 @@ static void fuse_free_inode(struct inode *inode)
if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
fuse_backing_put(fuse_inode_backing(fi));
+ kfree(fi->fh);
+
kmem_cache_free(fuse_inode_cachep, fi);
}
@@ -465,7 +469,7 @@ static int fuse_inode_set(struct inode *inode, void *_nodeidp)
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)
+ u64 evict_ctr, struct fuse_file_handle *fh)
{
struct inode *inode;
struct fuse_inode *fi;
@@ -505,6 +509,26 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
if (!inode)
return NULL;
+ fi = get_fuse_inode(inode);
+ if (fc->lookup_handle && !fi->fh) {
+ if (!fh && (nodeid != FUSE_ROOT_ID)) {
+ pr_err("NULL file handle for nodeid %llu\n", nodeid);
+ WARN_ON_ONCE(1);
+ } else {
+ size_t sz = sizeof(struct fuse_file_handle);
+
+ if (fh)
+ sz += fh->size;
+
+ fi->fh = kzalloc(sz, GFP_KERNEL);
+ if (!fi->fh) {
+ iput(inode);
+ return NULL; // ENOMEM
+ }
+ if (fh)
+ memcpy(fi->fh, fh, sz);
+ }
+ }
if ((inode_state_read_once(inode) & I_NEW)) {
inode->i_flags |= S_NOATIME;
if (!fc->writeback_cache || !S_ISREG(attr->mode))
@@ -512,7 +536,8 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
inode->i_generation = generation;
fuse_init_inode(inode, attr, fc);
unlock_new_inode(inode);
- } else if (fuse_stale_inode(inode, generation, attr)) {
+ } else if (fuse_stale_inode(inode, generation, attr) ||
+ (!fuse_file_handle_is_equal(fc, fi->fh, fh))) {
/* nodeid was reused, any I/O on the old inode should fail */
fuse_make_bad(inode);
if (inode != d_inode(sb->s_root)) {
@@ -521,7 +546,6 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid,
goto retry;
}
}
- fi = get_fuse_inode(inode);
spin_lock(&fi->lock);
fi->nlookup++;
spin_unlock(&fi->lock);
@@ -1068,7 +1092,7 @@ static struct inode *fuse_get_root_inode(struct super_block *sb, unsigned int mo
attr.mode = mode;
attr.ino = FUSE_ROOT_ID;
attr.nlink = 1;
- return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, 0, 0, 0);
+ return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, 0, 0, 0, NULL);
}
struct fuse_inode_handle {
@@ -1094,7 +1118,8 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
if (!fc->export_support)
goto out_err;
- err = fuse_lookup_name(sb, handle->nodeid, &name, NULL, &inode);
+ err = fuse_lookup_name(sb, handle->nodeid, NULL, &name, NULL,
+ &inode);
if (err && err != -ENOENT)
goto out_err;
if (err || !inode) {
@@ -1115,9 +1140,9 @@ static struct dentry *fuse_get_dentry(struct super_block *sb,
return entry;
- out_iput:
+out_iput:
iput(inode);
- out_err:
+out_err:
return ERR_PTR(err);
}
@@ -1194,7 +1219,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, NULL, &inode);
+ child_inode, &dotdot_name, NULL, &inode);
if (err) {
if (err == -ENOENT)
return ERR_PTR(-ESTALE);
@@ -1459,6 +1484,9 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args,
if (flags & FUSE_REQUEST_TIMEOUT)
timeout = arg->request_timeout;
+
+ if (flags & FUSE_HAS_LOOKUP_HANDLE)
+ fc->lookup_handle = 1;
} else {
ra_pages = fc->max_read / PAGE_SIZE;
fc->no_lock = 1;
@@ -1509,7 +1537,7 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm)
FUSE_SECURITY_CTX | FUSE_CREATE_SUPP_GROUP |
FUSE_HAS_EXPIRE_ONLY | FUSE_DIRECT_IO_ALLOW_MMAP |
FUSE_NO_EXPORT_SUPPORT | FUSE_HAS_RESEND | FUSE_ALLOW_IDMAP |
- FUSE_REQUEST_TIMEOUT;
+ FUSE_REQUEST_TIMEOUT | FUSE_LOOKUP_HANDLE;
#ifdef CONFIG_FUSE_DAX
if (fm->fc->dax)
flags |= FUSE_MAP_ALIGNMENT;
@@ -1745,7 +1773,7 @@ static int fuse_fill_super_submount(struct super_block *sb,
fuse_fill_attr_from_inode(&root_attr, parent_fi);
root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, 0, 0,
- fuse_get_evict_ctr(fm->fc));
+ fuse_get_evict_ctr(fm->fc), NULL);
/*
* This inode is just a duplicate, so it is not looked up and
* its nlookup should not be incremented. fuse_iget() does
diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c
index c2aae2eef086..2b59a196bcbf 100644
--- a/fs/fuse/readdir.c
+++ b/fs/fuse/readdir.c
@@ -235,7 +235,7 @@ static int fuse_direntplus_link(struct file *file,
} else {
inode = fuse_iget(dir->i_sb, o->nodeid, o->generation,
&o->attr, ATTR_TIMEOUT(o),
- attr_version, evict_ctr);
+ attr_version, evict_ctr, NULL);
if (!inode)
inode = ERR_PTR(-ENOMEM);
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 113583c4efb6..89e6176abe25 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -240,6 +240,9 @@
* - add FUSE_COPY_FILE_RANGE_64
* - add struct fuse_copy_file_range_out
* - add FUSE_NOTIFY_PRUNE
+ *
+ * 7.46
+ * - add FUSE_LOOKUP_HANDLE
*/
#ifndef _LINUX_FUSE_H
@@ -275,7 +278,7 @@
#define FUSE_KERNEL_VERSION 7
/** Minor version number of this interface */
-#define FUSE_KERNEL_MINOR_VERSION 45
+#define FUSE_KERNEL_MINOR_VERSION 46
/** The node ID of the root inode */
#define FUSE_ROOT_ID 1
@@ -495,6 +498,7 @@ struct fuse_file_lock {
#define FUSE_ALLOW_IDMAP (1ULL << 40)
#define FUSE_OVER_IO_URING (1ULL << 41)
#define FUSE_REQUEST_TIMEOUT (1ULL << 42)
+#define FUSE_HAS_LOOKUP_HANDLE (1ULL << 43)
/**
* CUSE INIT request/reply flags
@@ -609,6 +613,7 @@ enum fuse_ext_type {
/* Types 0..31 are reserved for fuse_secctx_header */
FUSE_MAX_NR_SECCTX = 31,
FUSE_EXT_GROUPS = 32,
+ FUSE_EXT_HANDLE = 33,
};
enum fuse_opcode {
@@ -671,6 +676,8 @@ enum fuse_opcode {
*/
FUSE_COMPOUND = 54,
+ FUSE_LOOKUP_HANDLE = 55,
+
/* CUSE specific operations */
CUSE_INIT = 4096,
@@ -707,6 +714,20 @@ struct fuse_entry_out {
struct fuse_attr attr;
};
+struct fuse_entry2_out {
+ uint64_t nodeid;
+ uint64_t generation;
+ uint64_t entry_valid;
+ uint32_t entry_valid_nsec;
+ uint32_t flags;
+ uint64_t spare;
+};
+
+struct fuse_file_handle {
+ uint16_t size;
+ uint8_t handle[];
+};
+
struct fuse_forget_in {
uint64_t nlookup;
};
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 7/8] fuse: export fuse_open_args_fill() helper function
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (5 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation Luis Henriques
2026-02-25 15:14 ` [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Horst Birthelmer
8 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
Allow fuse_open_args_fill() fuction to be used from different files.
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/file.c | 2 +-
fs/fuse/fuse_i.h | 10 +++++++---
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 1045d74dd95f..ea9023150a38 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -26,7 +26,7 @@
/*
* Helper function to initialize fuse_args for OPEN/OPENDIR operations
*/
-static void fuse_open_args_fill(struct fuse_args *args, u64 nodeid, int opcode,
+void fuse_open_args_fill(struct fuse_args *args, u64 nodeid, int opcode,
struct fuse_open_in *inarg, struct fuse_open_out *outarg)
{
args->opcode = opcode;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 173032887fc2..f37959c76412 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1588,10 +1588,14 @@ int fuse_file_io_open(struct file *file, struct inode *inode);
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
/* file.c */
+void fuse_open_args_fill(struct fuse_args *args, u64 nodeid, int opcode,
+ struct fuse_open_in *inarg,
+ struct fuse_open_out *outarg);
+
struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
- struct inode *inode,
- unsigned int open_flags,
- bool isdir);
+ struct inode *inode,
+ unsigned int open_flags,
+ bool isdir);
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
unsigned int open_flags, fl_owner_t id, bool isdir);
^ permalink raw reply related [flat|nested] 26+ messages in thread
* [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (6 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 7/8] fuse: export fuse_open_args_fill() helper function Luis Henriques
@ 2026-02-25 11:24 ` Luis Henriques
2026-02-25 15:08 ` Horst Birthelmer
2026-02-25 15:14 ` [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Horst Birthelmer
8 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 11:24 UTC (permalink / raw)
To: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen
Cc: linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev,
Luis Henriques
The implementation of this compound operation allows atomic_open() to use
file handle. It also introduces a new MKOBJ_HANDLE operation that will
handle the file system object creation and will return the file handle.
The atomicity of the operation (create + open) needs to be handled in
user-space (e.g. the handling of the O_EXCL flag).
Signed-off-by: Luis Henriques <luis@igalia.com>
---
fs/fuse/dir.c | 219 +++++++++++++++++++++++++++++++++++++-
include/uapi/linux/fuse.h | 2 +
2 files changed, 220 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 7fa8c405f1a3..b5beb1d62c3d 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -1173,6 +1173,220 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
return err;
}
+static int fuse_mkobj_handle_init(struct fuse_mount *fm, struct fuse_args *args,
+ struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *entry, unsigned int flags,
+ umode_t mode,
+ struct fuse_create_in *inarg,
+ struct fuse_entry2_out *outarg,
+ struct fuse_file_handle **fh)
+{
+ struct fuse_inode *fi;
+ size_t fh_size = sizeof(struct fuse_file_handle) + MAX_HANDLE_SZ;
+ int err = 0;
+
+ *fh = kzalloc(fh_size, GFP_KERNEL);
+ if (!*fh)
+ return -ENOMEM;
+
+ memset(inarg, 0, sizeof(*inarg));
+ memset(outarg, 0, sizeof(*outarg));
+
+ inarg->flags = flags;
+ inarg->mode = mode;
+ inarg->umask = current_umask();
+
+ if (fm->fc->handle_killpriv_v2 && (flags & O_TRUNC) &&
+ !(flags & O_EXCL) && !capable(CAP_FSETID))
+ inarg->open_flags |= FUSE_OPEN_KILL_SUIDGID;
+
+ args->opcode = FUSE_MKOBJ_HANDLE;
+ args->nodeid = get_node_id(dir);
+ args->in_numargs = 2;
+ args->in_args[0].size = sizeof(*inarg);
+ args->in_args[0].value = inarg;
+ args->in_args[1].size = entry->d_name.len + 1;
+ args->in_args[1].value = entry->d_name.name;
+
+ err = get_create_ext(idmap, args, dir, entry, mode);
+ if (err)
+ goto out_err;
+ fi = get_fuse_inode(dir);
+ if (fi && fi->fh) {
+ if (!args->is_ext) {
+ args->is_ext = true;
+ args->ext_idx = args->in_numargs++;
+ }
+ err = create_ext_handle(&args->in_args[args->ext_idx], fi);
+ if (err)
+ goto out_err;
+ }
+
+ args->out_numargs = 2;
+ args->out_args[0].size = sizeof(*outarg);
+ args->out_args[0].value = outarg;
+ args->out_args[1].size = fh_size;
+ args->out_args[1].value = *fh;
+
+out_err:
+ if (err) {
+ kfree(*fh);
+ free_ext_value(args);
+ }
+
+ return err;
+}
+
+static int fuse_mkobj_statx_open(struct mnt_idmap *idmap, struct inode *dir,
+ struct dentry *entry, struct file *file,
+ unsigned int flags, umode_t mode)
+{
+ struct fuse_compound_req *compound;
+ struct fuse_mount *fm = get_fuse_mount(dir);
+ struct fuse_inode *fi = NULL;
+ struct fuse_create_in mkobj_in;
+ struct fuse_entry2_out mkobj_out;
+ struct fuse_statx_in statx_in;
+ struct fuse_statx_out statx_out;
+ struct fuse_open_in open_in;
+ struct fuse_open_out *open_outp;
+ FUSE_ARGS(mkobj_args);
+ FUSE_ARGS(statx_args);
+ FUSE_ARGS(open_args);
+ struct fuse_forget_link *forget;
+ struct fuse_file *ff;
+ struct fuse_attr attr;
+ struct fuse_file_handle *fh = NULL;
+ struct inode *inode;
+ int epoch, ret = -EIO;
+ int i;
+
+ epoch = atomic_read(&fm->fc->epoch);
+
+ ret = -ENOMEM;
+ forget = fuse_alloc_forget();
+ if (!forget)
+ return -ENOMEM;
+ ff = fuse_file_alloc(fm, true);
+ if (!ff)
+ goto out_forget;
+
+ if (!fm->fc->dont_mask)
+ mode &= ~current_umask();
+
+ flags &= ~O_NOCTTY;
+
+ compound = fuse_compound_alloc(fm, FUSE_COMPOUND_ATOMIC);
+ if (!compound)
+ goto out_free_ff;
+
+ fi = get_fuse_inode(dir);
+ if (!fi) {
+ ret = -EIO;
+ goto out_compound;
+ }
+ ret = fuse_mkobj_handle_init(fm, &mkobj_args, idmap, dir, entry, flags,
+ mode, &mkobj_in, &mkobj_out, &fh);
+ if (ret)
+ goto out_compound;
+
+ ret = fuse_compound_add(compound, &mkobj_args);
+ if (ret)
+ goto out_mkobj_args;
+
+ fuse_statx_init(&statx_args, &statx_in, FUSE_ROOT_ID, NULL, &statx_out);
+ ret = fuse_compound_add(compound, &statx_args);
+ if (ret)
+ goto out_mkobj_args;
+
+ ff->fh = 0;
+ ff->open_flags = FOPEN_KEEP_CACHE;
+ memset(&open_in, 0, sizeof(open_in));
+
+ /* XXX flags handling */
+ open_in.flags = ff->open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
+ if (!fm->fc->atomic_o_trunc)
+ open_in.flags &= ~O_TRUNC;
+ if (fm->fc->handle_killpriv_v2 &&
+ (open_in.flags & O_TRUNC) && !capable(CAP_FSETID))
+ open_in.open_flags |= FUSE_OPEN_KILL_SUIDGID;
+
+ open_outp = &ff->args->open_outarg;
+ fuse_open_args_fill(&open_args, FUSE_ROOT_ID, FUSE_OPEN, &open_in,
+ open_outp);
+
+ ret = fuse_compound_add(compound, &open_args);
+ if (ret)
+ goto out_mkobj_args;
+
+ ret = fuse_compound_send(compound);
+ if (ret)
+ goto out_mkobj_args;
+
+ for (i = 0; i < 3; i++) {
+ int err;
+
+ err = fuse_compound_get_error(compound, i);
+ if (err && !ret)
+ ret = err;
+ }
+ if (ret)
+ goto out_mkobj_args;
+
+ fuse_statx_to_attr(&statx_out.stat, &attr);
+ WARN_ON(fuse_invalid_attr(&attr));
+ ret = -EIO;
+ if (!S_ISREG(attr.mode) || invalid_nodeid(mkobj_out.nodeid) ||
+ fuse_invalid_attr(&attr))
+ goto out_mkobj_args;
+
+ ff->fh = open_outp->fh;
+ ff->nodeid = mkobj_out.nodeid;
+ ff->open_flags = open_outp->open_flags;
+ inode = fuse_iget(dir->i_sb, mkobj_out.nodeid, mkobj_out.generation,
+ &attr, ATTR_TIMEOUT(&statx_out), 0, 0, fh);
+ if (!inode) {
+ flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
+ fuse_sync_release(NULL, ff, flags);
+ fuse_queue_forget(fm->fc, forget, mkobj_out.nodeid, 1);
+ ret = -ENOMEM;
+ goto out_mkobj_args;
+ }
+ d_instantiate(entry, inode);
+
+ entry->d_time = epoch;
+ fuse_dentry_settime(entry,
+ fuse_time_to_jiffies(mkobj_out.entry_valid,
+ mkobj_out.entry_valid_nsec));
+ fuse_dir_changed(dir);
+ ret = generic_file_open(inode, file);
+ if (!ret) {
+ file->private_data = ff;
+ ret = finish_open(file, entry, fuse_finish_open);
+ }
+ if (ret) {
+ fuse_sync_release(get_fuse_inode(inode), ff, flags);
+ } else {
+ if (fm->fc->atomic_o_trunc && (flags & O_TRUNC))
+ truncate_pagecache(inode, 0);
+ else if (!(ff->open_flags & FOPEN_KEEP_CACHE))
+ invalidate_inode_pages2(inode->i_mapping);
+ }
+
+out_mkobj_args:
+ fuse_req_free_argvar_ext(&mkobj_args);
+out_compound:
+ kfree(compound);
+out_free_ff:
+ if (ret)
+ fuse_file_free(ff);
+out_forget:
+ kfree(forget);
+ kfree(fh);
+
+ return ret;
+}
+
static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
umode_t, dev_t);
static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
@@ -1201,7 +1415,10 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
if (fc->no_create)
goto mknod;
- err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
+ if (fc->lookup_handle)
+ err = fuse_mkobj_statx_open(idmap, dir, entry, file, flags, mode);
+ else
+ err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
if (err == -ENOSYS) {
fc->no_create = 1;
goto mknod;
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 89e6176abe25..f49eb1b8f2f3 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -243,6 +243,7 @@
*
* 7.46
* - add FUSE_LOOKUP_HANDLE
+ * - add FUSE_MKOBJ_HANDLE
*/
#ifndef _LINUX_FUSE_H
@@ -677,6 +678,7 @@ enum fuse_opcode {
FUSE_COMPOUND = 54,
FUSE_LOOKUP_HANDLE = 55,
+ FUSE_MKOBJ_HANDLE = 56,
/* CUSE specific operations */
CUSE_INIT = 4096,
^ permalink raw reply related [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation
2026-02-25 11:24 ` [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation Luis Henriques
@ 2026-02-25 15:08 ` Horst Birthelmer
2026-02-25 17:26 ` Luis Henriques
0 siblings, 1 reply; 26+ messages in thread
From: Horst Birthelmer @ 2026-02-25 15:08 UTC (permalink / raw)
To: Luis Henriques
Cc: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen,
linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev
Hi Luis,
On Wed, Feb 25, 2026 at 11:24:39AM +0000, Luis Henriques wrote:
> The implementation of this compound operation allows atomic_open() to use
> file handle. It also introduces a new MKOBJ_HANDLE operation that will
> handle the file system object creation and will return the file handle.
>
> The atomicity of the operation (create + open) needs to be handled in
> user-space (e.g. the handling of the O_EXCL flag).
>
> Signed-off-by: Luis Henriques <luis@igalia.com>
> ---
> fs/fuse/dir.c | 219 +++++++++++++++++++++++++++++++++++++-
> include/uapi/linux/fuse.h | 2 +
> 2 files changed, 220 insertions(+), 1 deletion(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 7fa8c405f1a3..b5beb1d62c3d 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -1173,6 +1173,220 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
> return err;
> }
>
> +static int fuse_mkobj_handle_init(struct fuse_mount *fm, struct fuse_args *args,
> + struct mnt_idmap *idmap, struct inode *dir,
> + struct dentry *entry, unsigned int flags,
> + umode_t mode,
> + struct fuse_create_in *inarg,
> + struct fuse_entry2_out *outarg,
> + struct fuse_file_handle **fh)
> +{
> + struct fuse_inode *fi;
> + size_t fh_size = sizeof(struct fuse_file_handle) + MAX_HANDLE_SZ;
> + int err = 0;
> +
> + *fh = kzalloc(fh_size, GFP_KERNEL);
> + if (!*fh)
> + return -ENOMEM;
> +
> + memset(inarg, 0, sizeof(*inarg));
> + memset(outarg, 0, sizeof(*outarg));
> +
> + inarg->flags = flags;
> + inarg->mode = mode;
> + inarg->umask = current_umask();
> +
> + if (fm->fc->handle_killpriv_v2 && (flags & O_TRUNC) &&
> + !(flags & O_EXCL) && !capable(CAP_FSETID))
> + inarg->open_flags |= FUSE_OPEN_KILL_SUIDGID;
> +
> + args->opcode = FUSE_MKOBJ_HANDLE;
> + args->nodeid = get_node_id(dir);
> + args->in_numargs = 2;
> + args->in_args[0].size = sizeof(*inarg);
> + args->in_args[0].value = inarg;
> + args->in_args[1].size = entry->d_name.len + 1;
> + args->in_args[1].value = entry->d_name.name;
> +
> + err = get_create_ext(idmap, args, dir, entry, mode);
> + if (err)
> + goto out_err;
> + fi = get_fuse_inode(dir);
> + if (fi && fi->fh) {
> + if (!args->is_ext) {
> + args->is_ext = true;
> + args->ext_idx = args->in_numargs++;
> + }
> + err = create_ext_handle(&args->in_args[args->ext_idx], fi);
> + if (err)
> + goto out_err;
> + }
> +
> + args->out_numargs = 2;
> + args->out_args[0].size = sizeof(*outarg);
> + args->out_args[0].value = outarg;
> + args->out_args[1].size = fh_size;
> + args->out_args[1].value = *fh;
> +
> +out_err:
> + if (err) {
> + kfree(*fh);
> + free_ext_value(args);
> + }
> +
> + return err;
> +}
> +
> +static int fuse_mkobj_statx_open(struct mnt_idmap *idmap, struct inode *dir,
> + struct dentry *entry, struct file *file,
> + unsigned int flags, umode_t mode)
> +{
> + struct fuse_compound_req *compound;
> + struct fuse_mount *fm = get_fuse_mount(dir);
> + struct fuse_inode *fi = NULL;
> + struct fuse_create_in mkobj_in;
> + struct fuse_entry2_out mkobj_out;
> + struct fuse_statx_in statx_in;
> + struct fuse_statx_out statx_out;
> + struct fuse_open_in open_in;
> + struct fuse_open_out *open_outp;
> + FUSE_ARGS(mkobj_args);
> + FUSE_ARGS(statx_args);
> + FUSE_ARGS(open_args);
> + struct fuse_forget_link *forget;
> + struct fuse_file *ff;
> + struct fuse_attr attr;
> + struct fuse_file_handle *fh = NULL;
> + struct inode *inode;
> + int epoch, ret = -EIO;
> + int i;
> +
> + epoch = atomic_read(&fm->fc->epoch);
> +
> + ret = -ENOMEM;
> + forget = fuse_alloc_forget();
> + if (!forget)
> + return -ENOMEM;
> + ff = fuse_file_alloc(fm, true);
> + if (!ff)
> + goto out_forget;
> +
> + if (!fm->fc->dont_mask)
> + mode &= ~current_umask();
> +
> + flags &= ~O_NOCTTY;
> +
> + compound = fuse_compound_alloc(fm, FUSE_COMPOUND_ATOMIC);
> + if (!compound)
> + goto out_free_ff;
> +
Just to clarify for myself and maybe others.
You want this to be processed atomic on the fuse server and never
be separated by the upcoming 'decode and send separate' code in the
kernel?
Is that really necessarry? What would the consequences be,
if this is not really atomic?
> + fi = get_fuse_inode(dir);
> + if (!fi) {
> + ret = -EIO;
> + goto out_compound;
> + }
> + ret = fuse_mkobj_handle_init(fm, &mkobj_args, idmap, dir, entry, flags,
> + mode, &mkobj_in, &mkobj_out, &fh);
> + if (ret)
> + goto out_compound;
> +
> + ret = fuse_compound_add(compound, &mkobj_args);
> + if (ret)
> + goto out_mkobj_args;
> +
> + fuse_statx_init(&statx_args, &statx_in, FUSE_ROOT_ID, NULL, &statx_out);
> + ret = fuse_compound_add(compound, &statx_args);
> + if (ret)
> + goto out_mkobj_args;
> +
> + ff->fh = 0;
> + ff->open_flags = FOPEN_KEEP_CACHE;
> + memset(&open_in, 0, sizeof(open_in));
> +
> + /* XXX flags handling */
> + open_in.flags = ff->open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
> + if (!fm->fc->atomic_o_trunc)
> + open_in.flags &= ~O_TRUNC;
> + if (fm->fc->handle_killpriv_v2 &&
> + (open_in.flags & O_TRUNC) && !capable(CAP_FSETID))
> + open_in.open_flags |= FUSE_OPEN_KILL_SUIDGID;
> +
> + open_outp = &ff->args->open_outarg;
> + fuse_open_args_fill(&open_args, FUSE_ROOT_ID, FUSE_OPEN, &open_in,
> + open_outp);
> +
> + ret = fuse_compound_add(compound, &open_args);
> + if (ret)
> + goto out_mkobj_args;
> +
> + ret = fuse_compound_send(compound);
Your compound looks good so far ;-)
> + if (ret)
> + goto out_mkobj_args;
> +
> + for (i = 0; i < 3; i++) {
> + int err;
> +
> + err = fuse_compound_get_error(compound, i);
> + if (err && !ret)
> + ret = err;
> + }
this is probably why you opted for that 'give me any occurred error'
functionality?
> + if (ret)
> + goto out_mkobj_args;
> +
> + fuse_statx_to_attr(&statx_out.stat, &attr);
> + WARN_ON(fuse_invalid_attr(&attr));
> + ret = -EIO;
> + if (!S_ISREG(attr.mode) || invalid_nodeid(mkobj_out.nodeid) ||
> + fuse_invalid_attr(&attr))
> + goto out_mkobj_args;
> +
> + ff->fh = open_outp->fh;
> + ff->nodeid = mkobj_out.nodeid;
> + ff->open_flags = open_outp->open_flags;
> + inode = fuse_iget(dir->i_sb, mkobj_out.nodeid, mkobj_out.generation,
> + &attr, ATTR_TIMEOUT(&statx_out), 0, 0, fh);
> + if (!inode) {
> + flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
> + fuse_sync_release(NULL, ff, flags);
> + fuse_queue_forget(fm->fc, forget, mkobj_out.nodeid, 1);
> + ret = -ENOMEM;
> + goto out_mkobj_args;
> + }
> + d_instantiate(entry, inode);
> +
> + entry->d_time = epoch;
> + fuse_dentry_settime(entry,
> + fuse_time_to_jiffies(mkobj_out.entry_valid,
> + mkobj_out.entry_valid_nsec));
> + fuse_dir_changed(dir);
> + ret = generic_file_open(inode, file);
> + if (!ret) {
> + file->private_data = ff;
> + ret = finish_open(file, entry, fuse_finish_open);
> + }
> + if (ret) {
> + fuse_sync_release(get_fuse_inode(inode), ff, flags);
> + } else {
> + if (fm->fc->atomic_o_trunc && (flags & O_TRUNC))
> + truncate_pagecache(inode, 0);
> + else if (!(ff->open_flags & FOPEN_KEEP_CACHE))
> + invalidate_inode_pages2(inode->i_mapping);
> + }
> +
> +out_mkobj_args:
> + fuse_req_free_argvar_ext(&mkobj_args);
> +out_compound:
> + kfree(compound);
> +out_free_ff:
> + if (ret)
> + fuse_file_free(ff);
> +out_forget:
> + kfree(forget);
> + kfree(fh);
> +
> + return ret;
> +}
> +
> static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
> umode_t, dev_t);
> static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
> @@ -1201,7 +1415,10 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
> if (fc->no_create)
> goto mknod;
>
> - err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
> + if (fc->lookup_handle)
> + err = fuse_mkobj_statx_open(idmap, dir, entry, file, flags, mode);
> + else
> + err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
> if (err == -ENOSYS) {
> fc->no_create = 1;
> goto mknod;
> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
> index 89e6176abe25..f49eb1b8f2f3 100644
> --- a/include/uapi/linux/fuse.h
> +++ b/include/uapi/linux/fuse.h
> @@ -243,6 +243,7 @@
> *
> * 7.46
> * - add FUSE_LOOKUP_HANDLE
> + * - add FUSE_MKOBJ_HANDLE
> */
>
> #ifndef _LINUX_FUSE_H
> @@ -677,6 +678,7 @@ enum fuse_opcode {
> FUSE_COMPOUND = 54,
>
> FUSE_LOOKUP_HANDLE = 55,
> + FUSE_MKOBJ_HANDLE = 56,
>
> /* CUSE specific operations */
> CUSE_INIT = 4096,
>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
` (7 preceding siblings ...)
2026-02-25 11:24 ` [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation Luis Henriques
@ 2026-02-25 15:14 ` Horst Birthelmer
2026-02-25 17:06 ` Luis Henriques
8 siblings, 1 reply; 26+ messages in thread
From: Horst Birthelmer @ 2026-02-25 15:14 UTC (permalink / raw)
To: Luis Henriques
Cc: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen,
linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev
On Wed, Feb 25, 2026 at 11:24:31AM +0000, Luis Henriques wrote:
> Hi,
>
> I'm sending a new version of my work on lookup_handle, even though it's
> still incomplete. As suggested elsewhere, it is now based on compound
> commands and thus it sits on top of Horst's patchset [0]. Also, because
> this version is a complete re-write of the approach presented in my previous
> RFC [1] I'm not going to detail what changed.
>
> Here's a few notes:
>
> - The code isn't yet fully testable as there are several pieces missing.
> For example, the FUSE_TMPFILE and FUSE_READDIRPLUS operations are not yet
> implemented. The NFS-related changes have also been dropped in this
> revision.
>
> - There are several details still to be sorted out in the compound
> operations. For example, the nodeid for the statx operation in the
> lookup+statx is set to FUSE_ROOT_ID.
>
> - The second operation (mkobj_handle+statx+open) is still draft (or maybe
> just wrong!). It's not handling flags correctly, and the error handling
> has to be better thought out.
>
> - Some of the patches in this set could probably be picked independently
> (e.g. patch 4 or even patch 1)
>
> So, why am I sending this broken and incomplete patchset? Well, simply
> because I'd feel more confidence getting this approach validated. I don't
> expect any through review, but I would appreciate feedback on anything that
> would help me correct major flaws.
I, personally, appreciate the fact that you sent this out, so I can understand how
you are using the compounds for this real world problem, and it gives me confidence
that I'm not completely off with the compounds.
Do you by any chance have implemented the fuse server part, too, or looked at it?
I'm just curious.
>
> [0] https://lore.kernel.org/all/20260210-fuse-compounds-upstream-v5-0-ea0585f62daa@ddn.com
> [1] https://lore.kernel.org/all/20251212181254.59365-1-luis@igalia.com
>
> Cheers,
> --
> Luis
Thanks,
Horst
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation
2026-02-25 15:14 ` [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Horst Birthelmer
@ 2026-02-25 17:06 ` Luis Henriques
0 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 17:06 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen,
linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev
Hey Horst,
On Wed, Feb 25 2026, Horst Birthelmer wrote:
> On Wed, Feb 25, 2026 at 11:24:31AM +0000, Luis Henriques wrote:
>> Hi,
>>
>> I'm sending a new version of my work on lookup_handle, even though it's
>> still incomplete. As suggested elsewhere, it is now based on compound
>> commands and thus it sits on top of Horst's patchset [0]. Also, because
>> this version is a complete re-write of the approach presented in my previous
>> RFC [1] I'm not going to detail what changed.
>>
>> Here's a few notes:
>>
>> - The code isn't yet fully testable as there are several pieces missing.
>> For example, the FUSE_TMPFILE and FUSE_READDIRPLUS operations are not yet
>> implemented. The NFS-related changes have also been dropped in this
>> revision.
>>
>> - There are several details still to be sorted out in the compound
>> operations. For example, the nodeid for the statx operation in the
>> lookup+statx is set to FUSE_ROOT_ID.
>>
>> - The second operation (mkobj_handle+statx+open) is still draft (or maybe
>> just wrong!). It's not handling flags correctly, and the error handling
>> has to be better thought out.
>>
>> - Some of the patches in this set could probably be picked independently
>> (e.g. patch 4 or even patch 1)
>>
>> So, why am I sending this broken and incomplete patchset? Well, simply
>> because I'd feel more confidence getting this approach validated. I don't
>> expect any through review, but I would appreciate feedback on anything that
>> would help me correct major flaws.
>
> I, personally, appreciate the fact that you sent this out, so I can understand how
> you are using the compounds for this real world problem, and it gives me confidence
> that I'm not completely off with the compounds.
>
> Do you by any chance have implemented the fuse server part, too, or looked at it?
> I'm just curious.
I do have _something_ for testing, yes. Obviously, I will eventually
share it but I wasn't planning to share it at this stage yet (it's ugly
and full of debug code). But if you'd like to have a look I can do a quick
clean-up and push it somewhere.
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation
2026-02-25 15:08 ` Horst Birthelmer
@ 2026-02-25 17:26 ` Luis Henriques
0 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-25 17:26 UTC (permalink / raw)
To: Horst Birthelmer
Cc: Miklos Szeredi, Amir Goldstein, Bernd Schubert, Bernd Schubert,
Darrick J. Wong, Horst Birthelmer, Joanne Koong, Kevin Chen,
linux-fsdevel, linux-kernel, Matt Harvey, kernel-dev
Hi!
On Wed, Feb 25 2026, Horst Birthelmer wrote:
> Hi Luis,
>
> On Wed, Feb 25, 2026 at 11:24:39AM +0000, Luis Henriques wrote:
>> The implementation of this compound operation allows atomic_open() to use
>> file handle. It also introduces a new MKOBJ_HANDLE operation that will
>> handle the file system object creation and will return the file handle.
>>
>> The atomicity of the operation (create + open) needs to be handled in
>> user-space (e.g. the handling of the O_EXCL flag).
>>
>> Signed-off-by: Luis Henriques <luis@igalia.com>
>> ---
>> fs/fuse/dir.c | 219 +++++++++++++++++++++++++++++++++++++-
>> include/uapi/linux/fuse.h | 2 +
>> 2 files changed, 220 insertions(+), 1 deletion(-)
>>
>> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
>> index 7fa8c405f1a3..b5beb1d62c3d 100644
>> --- a/fs/fuse/dir.c
>> +++ b/fs/fuse/dir.c
>> @@ -1173,6 +1173,220 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
>> return err;
>> }
>>
>> +static int fuse_mkobj_handle_init(struct fuse_mount *fm, struct fuse_args *args,
>> + struct mnt_idmap *idmap, struct inode *dir,
>> + struct dentry *entry, unsigned int flags,
>> + umode_t mode,
>> + struct fuse_create_in *inarg,
>> + struct fuse_entry2_out *outarg,
>> + struct fuse_file_handle **fh)
>> +{
>> + struct fuse_inode *fi;
>> + size_t fh_size = sizeof(struct fuse_file_handle) + MAX_HANDLE_SZ;
>> + int err = 0;
>> +
>> + *fh = kzalloc(fh_size, GFP_KERNEL);
>> + if (!*fh)
>> + return -ENOMEM;
>> +
>> + memset(inarg, 0, sizeof(*inarg));
>> + memset(outarg, 0, sizeof(*outarg));
>> +
>> + inarg->flags = flags;
>> + inarg->mode = mode;
>> + inarg->umask = current_umask();
>> +
>> + if (fm->fc->handle_killpriv_v2 && (flags & O_TRUNC) &&
>> + !(flags & O_EXCL) && !capable(CAP_FSETID))
>> + inarg->open_flags |= FUSE_OPEN_KILL_SUIDGID;
>> +
>> + args->opcode = FUSE_MKOBJ_HANDLE;
>> + args->nodeid = get_node_id(dir);
>> + args->in_numargs = 2;
>> + args->in_args[0].size = sizeof(*inarg);
>> + args->in_args[0].value = inarg;
>> + args->in_args[1].size = entry->d_name.len + 1;
>> + args->in_args[1].value = entry->d_name.name;
>> +
>> + err = get_create_ext(idmap, args, dir, entry, mode);
>> + if (err)
>> + goto out_err;
>> + fi = get_fuse_inode(dir);
>> + if (fi && fi->fh) {
>> + if (!args->is_ext) {
>> + args->is_ext = true;
>> + args->ext_idx = args->in_numargs++;
>> + }
>> + err = create_ext_handle(&args->in_args[args->ext_idx], fi);
>> + if (err)
>> + goto out_err;
>> + }
>> +
>> + args->out_numargs = 2;
>> + args->out_args[0].size = sizeof(*outarg);
>> + args->out_args[0].value = outarg;
>> + args->out_args[1].size = fh_size;
>> + args->out_args[1].value = *fh;
>> +
>> +out_err:
>> + if (err) {
>> + kfree(*fh);
>> + free_ext_value(args);
>> + }
>> +
>> + return err;
>> +}
>> +
>> +static int fuse_mkobj_statx_open(struct mnt_idmap *idmap, struct inode *dir,
>> + struct dentry *entry, struct file *file,
>> + unsigned int flags, umode_t mode)
>> +{
>> + struct fuse_compound_req *compound;
>> + struct fuse_mount *fm = get_fuse_mount(dir);
>> + struct fuse_inode *fi = NULL;
>> + struct fuse_create_in mkobj_in;
>> + struct fuse_entry2_out mkobj_out;
>> + struct fuse_statx_in statx_in;
>> + struct fuse_statx_out statx_out;
>> + struct fuse_open_in open_in;
>> + struct fuse_open_out *open_outp;
>> + FUSE_ARGS(mkobj_args);
>> + FUSE_ARGS(statx_args);
>> + FUSE_ARGS(open_args);
>> + struct fuse_forget_link *forget;
>> + struct fuse_file *ff;
>> + struct fuse_attr attr;
>> + struct fuse_file_handle *fh = NULL;
>> + struct inode *inode;
>> + int epoch, ret = -EIO;
>> + int i;
>> +
>> + epoch = atomic_read(&fm->fc->epoch);
>> +
>> + ret = -ENOMEM;
>> + forget = fuse_alloc_forget();
>> + if (!forget)
>> + return -ENOMEM;
>> + ff = fuse_file_alloc(fm, true);
>> + if (!ff)
>> + goto out_forget;
>> +
>> + if (!fm->fc->dont_mask)
>> + mode &= ~current_umask();
>> +
>> + flags &= ~O_NOCTTY;
>> +
>> + compound = fuse_compound_alloc(fm, FUSE_COMPOUND_ATOMIC);
>> + if (!compound)
>> + goto out_free_ff;
>> +
>
> Just to clarify for myself and maybe others.
> You want this to be processed atomic on the fuse server and never
> be separated by the upcoming 'decode and send separate' code in the
> kernel?
> Is that really necessarry? What would the consequences be,
> if this is not really atomic?
No, you're right -- it's unlikely that this flag is required. If I
remember correctly from the discussion, the flags and what they mean is
one of the things still not written in stone for the compound operations,
right?
Regarding this compound specifically, what I wanted was to ensure is that:
- The operations are serialised as they are interdependent(*), and
- If one operation fails, the others can be aborted.
(*) Actually, the last 2 ops (statx and open) could be parallelised.
>> + fi = get_fuse_inode(dir);
>> + if (!fi) {
>> + ret = -EIO;
>> + goto out_compound;
>> + }
>> + ret = fuse_mkobj_handle_init(fm, &mkobj_args, idmap, dir, entry, flags,
>> + mode, &mkobj_in, &mkobj_out, &fh);
>> + if (ret)
>> + goto out_compound;
>> +
>> + ret = fuse_compound_add(compound, &mkobj_args);
>> + if (ret)
>> + goto out_mkobj_args;
>> +
>> + fuse_statx_init(&statx_args, &statx_in, FUSE_ROOT_ID, NULL, &statx_out);
>> + ret = fuse_compound_add(compound, &statx_args);
>> + if (ret)
>> + goto out_mkobj_args;
>> +
>> + ff->fh = 0;
>> + ff->open_flags = FOPEN_KEEP_CACHE;
>> + memset(&open_in, 0, sizeof(open_in));
>> +
>> + /* XXX flags handling */
>> + open_in.flags = ff->open_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
>> + if (!fm->fc->atomic_o_trunc)
>> + open_in.flags &= ~O_TRUNC;
>> + if (fm->fc->handle_killpriv_v2 &&
>> + (open_in.flags & O_TRUNC) && !capable(CAP_FSETID))
>> + open_in.open_flags |= FUSE_OPEN_KILL_SUIDGID;
>> +
>> + open_outp = &ff->args->open_outarg;
>> + fuse_open_args_fill(&open_args, FUSE_ROOT_ID, FUSE_OPEN, &open_in,
>> + open_outp);
>> +
>> + ret = fuse_compound_add(compound, &open_args);
>> + if (ret)
>> + goto out_mkobj_args;
>> +
>> + ret = fuse_compound_send(compound);
>
> Your compound looks good so far ;-)
Yey!
>> + if (ret)
>> + goto out_mkobj_args;
>> +
>> + for (i = 0; i < 3; i++) {
>> + int err;
>> +
>> + err = fuse_compound_get_error(compound, i);
>> + if (err && !ret)
>> + ret = err;
>> + }
>
> this is probably why you opted for that 'give me any occurred error'
> functionality?
Right, since there are interdependencies between the operations I only
care about the first failure. Probably a pr_warning() could be used here
to log each of them.
Anyway, it's not clear that this pattern will be common enough in order to
think about an helper for it. I guess that if we are bundling a bunch of
operations that can be parallelised, then error handling needs to be done
differently.
Cheers,
--
Luís
>> + if (ret)
>> + goto out_mkobj_args;
>> +
>> + fuse_statx_to_attr(&statx_out.stat, &attr);
>> + WARN_ON(fuse_invalid_attr(&attr));
>> + ret = -EIO;
>> + if (!S_ISREG(attr.mode) || invalid_nodeid(mkobj_out.nodeid) ||
>> + fuse_invalid_attr(&attr))
>> + goto out_mkobj_args;
>> +
>> + ff->fh = open_outp->fh;
>> + ff->nodeid = mkobj_out.nodeid;
>> + ff->open_flags = open_outp->open_flags;
>> + inode = fuse_iget(dir->i_sb, mkobj_out.nodeid, mkobj_out.generation,
>> + &attr, ATTR_TIMEOUT(&statx_out), 0, 0, fh);
>> + if (!inode) {
>> + flags &= ~(O_CREAT | O_EXCL | O_TRUNC);
>> + fuse_sync_release(NULL, ff, flags);
>> + fuse_queue_forget(fm->fc, forget, mkobj_out.nodeid, 1);
>> + ret = -ENOMEM;
>> + goto out_mkobj_args;
>> + }
>> + d_instantiate(entry, inode);
>> +
>> + entry->d_time = epoch;
>> + fuse_dentry_settime(entry,
>> + fuse_time_to_jiffies(mkobj_out.entry_valid,
>> + mkobj_out.entry_valid_nsec));
>> + fuse_dir_changed(dir);
>> + ret = generic_file_open(inode, file);
>> + if (!ret) {
>> + file->private_data = ff;
>> + ret = finish_open(file, entry, fuse_finish_open);
>> + }
>> + if (ret) {
>> + fuse_sync_release(get_fuse_inode(inode), ff, flags);
>> + } else {
>> + if (fm->fc->atomic_o_trunc && (flags & O_TRUNC))
>> + truncate_pagecache(inode, 0);
>> + else if (!(ff->open_flags & FOPEN_KEEP_CACHE))
>> + invalidate_inode_pages2(inode->i_mapping);
>> + }
>> +
>> +out_mkobj_args:
>> + fuse_req_free_argvar_ext(&mkobj_args);
>> +out_compound:
>> + kfree(compound);
>> +out_free_ff:
>> + if (ret)
>> + fuse_file_free(ff);
>> +out_forget:
>> + kfree(forget);
>> + kfree(fh);
>> +
>> + return ret;
>> +}
>> +
>> static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
>> umode_t, dev_t);
>> static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
>> @@ -1201,7 +1415,10 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
>> if (fc->no_create)
>> goto mknod;
>>
>> - err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
>> + if (fc->lookup_handle)
>> + err = fuse_mkobj_statx_open(idmap, dir, entry, file, flags, mode);
>> + else
>> + err = fuse_create_open(idmap, dir, entry, file, flags, mode, FUSE_CREATE);
>> if (err == -ENOSYS) {
>> fc->no_create = 1;
>> goto mknod;
>> diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
>> index 89e6176abe25..f49eb1b8f2f3 100644
>> --- a/include/uapi/linux/fuse.h
>> +++ b/include/uapi/linux/fuse.h
>> @@ -243,6 +243,7 @@
>> *
>> * 7.46
>> * - add FUSE_LOOKUP_HANDLE
>> + * - add FUSE_MKOBJ_HANDLE
>> */
>>
>> #ifndef _LINUX_FUSE_H
>> @@ -677,6 +678,7 @@ enum fuse_opcode {
>> FUSE_COMPOUND = 54,
>>
>> FUSE_LOOKUP_HANDLE = 55,
>> + FUSE_MKOBJ_HANDLE = 56,
>>
>> /* CUSE specific operations */
>> CUSE_INIT = 4096,
>>
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-25 11:24 ` [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation Luis Henriques
@ 2026-02-25 18:06 ` Amir Goldstein
2026-02-26 9:54 ` Luis Henriques
0 siblings, 1 reply; 26+ messages in thread
From: Amir Goldstein @ 2026-02-25 18:06 UTC (permalink / raw)
To: Luis Henriques
Cc: Miklos Szeredi, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Wed, Feb 25, 2026 at 12:25 PM Luis Henriques <luis@igalia.com> wrote:
>
> The implementation of lookup_handle+statx compound operation extends the
> lookup operation so that a file handle is be passed into the kernel. It
> also needs to include an extra inarg, so that the parent directory file
> handle can be sent to user-space. This extra inarg is added as an extension
> header to the request.
>
> By having a separate statx including in a compound operation allows the
> attr to be dropped from the lookup_handle request, simplifying the
> traditional FUSE lookup operation.
>
> Signed-off-by: Luis Henriques <luis@igalia.com>
> ---
> fs/fuse/dir.c | 294 +++++++++++++++++++++++++++++++++++---
> fs/fuse/fuse_i.h | 23 ++-
> fs/fuse/inode.c | 48 +++++--
> fs/fuse/readdir.c | 2 +-
> include/uapi/linux/fuse.h | 23 ++-
> 5 files changed, 355 insertions(+), 35 deletions(-)
>
> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> index 5c0f1364c392..7fa8c405f1a3 100644
> --- a/fs/fuse/dir.c
> +++ b/fs/fuse/dir.c
> @@ -21,6 +21,7 @@
> #include <linux/security.h>
> #include <linux/types.h>
> #include <linux/kernel.h>
> +#include <linux/exportfs.h>
>
> static bool __read_mostly allow_sys_admin_access;
> module_param(allow_sys_admin_access, bool, 0644);
> @@ -372,6 +373,47 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
> args->out_args[0].value = outarg;
> }
>
> +static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
> + struct inode *parent_inode,
> + const struct qstr *name,
> + struct fuse_entry2_out *lookup_out,
> + struct fuse_statx_out *statx_out,
> + struct fuse_file_handle **fh);
> +static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr);
> +static int do_reval_lookup(struct fuse_mount *fm, u64 parent_nodeid,
> + const struct qstr *name, u64 *nodeid,
> + u64 *generation, u64 *attr_valid,
> + struct fuse_attr *attr, struct fuse_file_handle **fh)
> +{
> + struct fuse_entry_out entry_out;
> + struct fuse_entry2_out lookup_out;
> + struct fuse_statx_out statx_out;
> + FUSE_ARGS(lookup_args);
> + int ret = 0;
> +
> + if (fm->fc->lookup_handle) {
> + ret = do_lookup_handle_statx(fm, parent_nodeid, NULL, name,
> + &lookup_out, &statx_out, fh);
> + if (!ret) {
> + *nodeid = lookup_out.nodeid;
> + *generation = lookup_out.generation;
> + *attr_valid = fuse_time_to_jiffies(lookup_out.entry_valid,
> + lookup_out.entry_valid_nsec);
> + fuse_statx_to_attr(&statx_out.stat, attr);
> + }
> + } else {
> + fuse_lookup_init(&lookup_args, parent_nodeid, name, &entry_out);
> + ret = fuse_simple_request(fm, &lookup_args);
> + if (!ret) {
> + *nodeid = entry_out.nodeid;
> + *generation = entry_out.generation;
> + *attr_valid = ATTR_TIMEOUT(&entry_out);
> + memcpy(attr, &entry_out.attr, sizeof(*attr));
> + }
> + }
> +
> + return ret;
> +}
> /*
> * Check whether the dentry is still valid
> *
> @@ -399,10 +441,11 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> goto invalid;
> else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
> (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
> - struct fuse_entry_out outarg;
> - FUSE_ARGS(args);
> struct fuse_forget_link *forget;
> + struct fuse_file_handle *fh = NULL;
> u64 attr_version;
> + u64 nodeid, generation, attr_valid;
> + struct fuse_attr attr;
>
> /* For negative dentries, always do a fresh lookup */
> if (!inode)
> @@ -421,35 +464,36 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>
> attr_version = fuse_get_attr_version(fm->fc);
>
> - fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
> - ret = fuse_simple_request(fm, &args);
> + ret = do_reval_lookup(fm, get_node_id(dir), name, &nodeid,
> + &generation, &attr_valid, &attr, &fh);
> /* Zero nodeid is same as -ENOENT */
> - if (!ret && !outarg.nodeid)
> + if (!ret && !nodeid)
> ret = -ENOENT;
> if (!ret) {
> fi = get_fuse_inode(inode);
> - if (outarg.nodeid != get_node_id(inode) ||
> - (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
> - fuse_queue_forget(fm->fc, forget,
> - outarg.nodeid, 1);
> + if (!fuse_file_handle_is_equal(fm->fc, fi->fh, fh) ||
> + nodeid != get_node_id(inode) ||
> + (bool) IS_AUTOMOUNT(inode) != (bool) (attr.flags & FUSE_ATTR_SUBMOUNT)) {
> + fuse_queue_forget(fm->fc, forget, nodeid, 1);
> + kfree(fh);
> goto invalid;
> }
> spin_lock(&fi->lock);
> fi->nlookup++;
> spin_unlock(&fi->lock);
> }
> + kfree(fh);
> kfree(forget);
> if (ret == -ENOMEM || ret == -EINTR)
> goto out;
> - if (ret || fuse_invalid_attr(&outarg.attr) ||
> - fuse_stale_inode(inode, outarg.generation, &outarg.attr))
> + if (ret || fuse_invalid_attr(&attr) ||
> + fuse_stale_inode(inode, generation, &attr))
> goto invalid;
>
> forget_all_cached_acls(inode);
> - fuse_change_attributes(inode, &outarg.attr, NULL,
> - ATTR_TIMEOUT(&outarg),
> + fuse_change_attributes(inode, &attr, NULL, attr_valid,
> attr_version);
> - fuse_change_entry_timeout(entry, &outarg);
> + fuse_dentry_settime(entry, attr_valid);
> } else if (inode) {
> fi = get_fuse_inode(inode);
> if (flags & LOOKUP_RCU) {
> @@ -546,8 +590,215 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
> return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size);
> }
>
> -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
> - u64 *time, struct inode **inode)
> +static int create_ext_handle(struct fuse_in_arg *ext, struct fuse_inode *fi)
> +{
> + struct fuse_ext_header *xh;
> + struct fuse_file_handle *fh;
> + u32 len;
> +
> + len = fuse_ext_size(sizeof(*fi->fh) + fi->fh->size);
> + xh = fuse_extend_arg(ext, len);
> + if (!xh)
> + return -ENOMEM;
> +
> + xh->size = len;
> + xh->type = FUSE_EXT_HANDLE;
> + fh = (struct fuse_file_handle *)&xh[1];
> + fh->size = fi->fh->size;
> + memcpy(fh->handle, fi->fh->handle, fh->size);
> +
> + return 0;
> +}
> +
> +static int fuse_lookup_handle_init(struct fuse_args *args, u64 nodeid,
> + struct fuse_inode *fi,
> + const struct qstr *name,
> + struct fuse_entry2_out *outarg)
> +{
> + struct fuse_file_handle *fh;
Considering that fuse has long used uint64_t fh as the convention
for a file id all over the code, it would be better to pick a different
convention for fuse file handle, perhaps ffh, or fhandle?
> + size_t fh_size = sizeof(*fh) + MAX_HANDLE_SZ;
I don't remember what we concluded last time, but
shouldn't the server request max_handle_sz at init?
This constant is quite arbitrary.
> + int ret = -ENOMEM;
> +
> + fh = kzalloc(fh_size, GFP_KERNEL);
> + if (!fh)
> + return ret;
> +
> + memset(outarg, 0, sizeof(struct fuse_entry2_out));
> + args->opcode = FUSE_LOOKUP_HANDLE;
> + args->nodeid = nodeid;
> + args->in_numargs = 3;
> + fuse_set_zero_arg0(args);
> + args->in_args[1].size = name->len;
> + args->in_args[1].value = name->name;
> + args->in_args[2].size = 1;
> + args->in_args[2].value = "";
> + if (fi && fi->fh) {
Same here fi->ffh? or fi->fhandle
> + args->is_ext = true;
> + args->ext_idx = args->in_numargs++;
> + args->in_args[args->ext_idx].size = 0;
> + ret = create_ext_handle(&args->in_args[args->ext_idx], fi);
> + if (ret) {
> + kfree(fh);
> + return ret;
> + }
> + }
> + args->out_numargs = 2;
> + args->out_argvar = true;
> + args->out_argvar_idx = 1;
> + args->out_args[0].size = sizeof(struct fuse_entry2_out);
> + args->out_args[0].value = outarg;
> +
> + /* XXX do allocation to the actual size of the handle */
> + args->out_args[1].size = fh_size;
> + args->out_args[1].value = fh;
> +
> + return 0;
> +}
> +
> +static void fuse_req_free_argvar_ext(struct fuse_args *args)
> +{
> + if (args->out_argvar)
> + kfree(args->out_args[args->out_argvar_idx].value);
> + if (args->is_ext)
> + kfree(args->in_args[args->ext_idx].value);
> +}
> +
Just wanted to point out that statx_out is > 256 bytes on stack
so allocating 127+4 and the added complexity of ext arg
seem awkward.
Unless we really want to support huge file handles (we don't?)
maybe the allocation can be restricted to fi->handle?
Not sure.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-25 18:06 ` Amir Goldstein
@ 2026-02-26 9:54 ` Luis Henriques
2026-02-26 10:08 ` Amir Goldstein
0 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-26 9:54 UTC (permalink / raw)
To: Amir Goldstein
Cc: Miklos Szeredi, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
Hi Amir,
On Wed, Feb 25 2026, Amir Goldstein wrote:
> On Wed, Feb 25, 2026 at 12:25 PM Luis Henriques <luis@igalia.com> wrote:
>>
>> The implementation of lookup_handle+statx compound operation extends the
>> lookup operation so that a file handle is be passed into the kernel. It
>> also needs to include an extra inarg, so that the parent directory file
>> handle can be sent to user-space. This extra inarg is added as an extension
>> header to the request.
>>
>> By having a separate statx including in a compound operation allows the
>> attr to be dropped from the lookup_handle request, simplifying the
>> traditional FUSE lookup operation.
>>
>> Signed-off-by: Luis Henriques <luis@igalia.com>
>> ---
>> fs/fuse/dir.c | 294 +++++++++++++++++++++++++++++++++++---
>> fs/fuse/fuse_i.h | 23 ++-
>> fs/fuse/inode.c | 48 +++++--
>> fs/fuse/readdir.c | 2 +-
>> include/uapi/linux/fuse.h | 23 ++-
>> 5 files changed, 355 insertions(+), 35 deletions(-)
>>
>> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
>> index 5c0f1364c392..7fa8c405f1a3 100644
>> --- a/fs/fuse/dir.c
>> +++ b/fs/fuse/dir.c
>> @@ -21,6 +21,7 @@
>> #include <linux/security.h>
>> #include <linux/types.h>
>> #include <linux/kernel.h>
>> +#include <linux/exportfs.h>
>>
>> static bool __read_mostly allow_sys_admin_access;
>> module_param(allow_sys_admin_access, bool, 0644);
>> @@ -372,6 +373,47 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
>> args->out_args[0].value = outarg;
>> }
>>
>> +static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
>> + struct inode *parent_inode,
>> + const struct qstr *name,
>> + struct fuse_entry2_out *lookup_out,
>> + struct fuse_statx_out *statx_out,
>> + struct fuse_file_handle **fh);
>> +static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr);
>> +static int do_reval_lookup(struct fuse_mount *fm, u64 parent_nodeid,
>> + const struct qstr *name, u64 *nodeid,
>> + u64 *generation, u64 *attr_valid,
>> + struct fuse_attr *attr, struct fuse_file_handle **fh)
>> +{
>> + struct fuse_entry_out entry_out;
>> + struct fuse_entry2_out lookup_out;
>> + struct fuse_statx_out statx_out;
>> + FUSE_ARGS(lookup_args);
>> + int ret = 0;
>> +
>> + if (fm->fc->lookup_handle) {
>> + ret = do_lookup_handle_statx(fm, parent_nodeid, NULL, name,
>> + &lookup_out, &statx_out, fh);
>> + if (!ret) {
>> + *nodeid = lookup_out.nodeid;
>> + *generation = lookup_out.generation;
>> + *attr_valid = fuse_time_to_jiffies(lookup_out.entry_valid,
>> + lookup_out.entry_valid_nsec);
>> + fuse_statx_to_attr(&statx_out.stat, attr);
>> + }
>> + } else {
>> + fuse_lookup_init(&lookup_args, parent_nodeid, name, &entry_out);
>> + ret = fuse_simple_request(fm, &lookup_args);
>> + if (!ret) {
>> + *nodeid = entry_out.nodeid;
>> + *generation = entry_out.generation;
>> + *attr_valid = ATTR_TIMEOUT(&entry_out);
>> + memcpy(attr, &entry_out.attr, sizeof(*attr));
>> + }
>> + }
>> +
>> + return ret;
>> +}
>> /*
>> * Check whether the dentry is still valid
>> *
>> @@ -399,10 +441,11 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>> goto invalid;
>> else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
>> (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
>> - struct fuse_entry_out outarg;
>> - FUSE_ARGS(args);
>> struct fuse_forget_link *forget;
>> + struct fuse_file_handle *fh = NULL;
>> u64 attr_version;
>> + u64 nodeid, generation, attr_valid;
>> + struct fuse_attr attr;
>>
>> /* For negative dentries, always do a fresh lookup */
>> if (!inode)
>> @@ -421,35 +464,36 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>>
>> attr_version = fuse_get_attr_version(fm->fc);
>>
>> - fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
>> - ret = fuse_simple_request(fm, &args);
>> + ret = do_reval_lookup(fm, get_node_id(dir), name, &nodeid,
>> + &generation, &attr_valid, &attr, &fh);
>> /* Zero nodeid is same as -ENOENT */
>> - if (!ret && !outarg.nodeid)
>> + if (!ret && !nodeid)
>> ret = -ENOENT;
>> if (!ret) {
>> fi = get_fuse_inode(inode);
>> - if (outarg.nodeid != get_node_id(inode) ||
>> - (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
>> - fuse_queue_forget(fm->fc, forget,
>> - outarg.nodeid, 1);
>> + if (!fuse_file_handle_is_equal(fm->fc, fi->fh, fh) ||
>> + nodeid != get_node_id(inode) ||
>> + (bool) IS_AUTOMOUNT(inode) != (bool) (attr.flags & FUSE_ATTR_SUBMOUNT)) {
>> + fuse_queue_forget(fm->fc, forget, nodeid, 1);
>> + kfree(fh);
>> goto invalid;
>> }
>> spin_lock(&fi->lock);
>> fi->nlookup++;
>> spin_unlock(&fi->lock);
>> }
>> + kfree(fh);
>> kfree(forget);
>> if (ret == -ENOMEM || ret == -EINTR)
>> goto out;
>> - if (ret || fuse_invalid_attr(&outarg.attr) ||
>> - fuse_stale_inode(inode, outarg.generation, &outarg.attr))
>> + if (ret || fuse_invalid_attr(&attr) ||
>> + fuse_stale_inode(inode, generation, &attr))
>> goto invalid;
>>
>> forget_all_cached_acls(inode);
>> - fuse_change_attributes(inode, &outarg.attr, NULL,
>> - ATTR_TIMEOUT(&outarg),
>> + fuse_change_attributes(inode, &attr, NULL, attr_valid,
>> attr_version);
>> - fuse_change_entry_timeout(entry, &outarg);
>> + fuse_dentry_settime(entry, attr_valid);
>> } else if (inode) {
>> fi = get_fuse_inode(inode);
>> if (flags & LOOKUP_RCU) {
>> @@ -546,8 +590,215 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
>> return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size);
>> }
>>
>> -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
>> - u64 *time, struct inode **inode)
>> +static int create_ext_handle(struct fuse_in_arg *ext, struct fuse_inode *fi)
>> +{
>> + struct fuse_ext_header *xh;
>> + struct fuse_file_handle *fh;
>> + u32 len;
>> +
>> + len = fuse_ext_size(sizeof(*fi->fh) + fi->fh->size);
>> + xh = fuse_extend_arg(ext, len);
>> + if (!xh)
>> + return -ENOMEM;
>> +
>> + xh->size = len;
>> + xh->type = FUSE_EXT_HANDLE;
>> + fh = (struct fuse_file_handle *)&xh[1];
>> + fh->size = fi->fh->size;
>> + memcpy(fh->handle, fi->fh->handle, fh->size);
>> +
>> + return 0;
>> +}
>> +
>> +static int fuse_lookup_handle_init(struct fuse_args *args, u64 nodeid,
>> + struct fuse_inode *fi,
>> + const struct qstr *name,
>> + struct fuse_entry2_out *outarg)
>> +{
>> + struct fuse_file_handle *fh;
>
> Considering that fuse has long used uint64_t fh as the convention
> for a file id all over the code, it would be better to pick a different
> convention for fuse file handle, perhaps ffh, or fhandle?
Good point, I'll make sure next revision will follow a different
convention.
>> + size_t fh_size = sizeof(*fh) + MAX_HANDLE_SZ;
>
> I don't remember what we concluded last time, but
> shouldn't the server request max_handle_sz at init?
> This constant is quite arbitrary.
You're right, I should have pointed that out in the cover letter at least.
In the previous version that maximum size was indeed provided by the
server. But from the discussion here [0] I understood that this
negotiation should be dropped. Here's what Miklos suggested:
> How about allocating variable length arguments on demand? That would
> allow getting rid of max_handle_size negotiation.
>
> args->out_var_alloc = true;
> args->out_args[1].size = MAX_HANDLE_SZ;
> args->out_args[1].value = NULL; /* Will be allocated to the actual size of the handle */
Obviously that's not what the code is currently doing. The plan is to
eventually set the .value to NULL and do the allocation elsewhere,
according to the actual size returned.
Because I didn't yet thought how/where the allocation could be done
instead, this code is currently simplifying things, and that's why I
picked this MAX_HANDLE_SZ.
Sorry, I should have pointed that out at in a comment as well.
[0] https://lore.kernel.org/all/CAJfpegszP+2XA=vADK4r09KU30BQd-r9sNu2Dog88yLG8iV7WQ@mail.gmail.com
>> + int ret = -ENOMEM;
>> +
>> + fh = kzalloc(fh_size, GFP_KERNEL);
>> + if (!fh)
>> + return ret;
>> +
>> + memset(outarg, 0, sizeof(struct fuse_entry2_out));
>> + args->opcode = FUSE_LOOKUP_HANDLE;
>> + args->nodeid = nodeid;
>> + args->in_numargs = 3;
>> + fuse_set_zero_arg0(args);
>> + args->in_args[1].size = name->len;
>> + args->in_args[1].value = name->name;
>> + args->in_args[2].size = 1;
>> + args->in_args[2].value = "";
>> + if (fi && fi->fh) {
>
> Same here fi->ffh? or fi->fhandle
Ack!
>> + args->is_ext = true;
>> + args->ext_idx = args->in_numargs++;
>> + args->in_args[args->ext_idx].size = 0;
>> + ret = create_ext_handle(&args->in_args[args->ext_idx], fi);
>> + if (ret) {
>> + kfree(fh);
>> + return ret;
>> + }
>> + }
>> + args->out_numargs = 2;
>> + args->out_argvar = true;
>> + args->out_argvar_idx = 1;
>> + args->out_args[0].size = sizeof(struct fuse_entry2_out);
>> + args->out_args[0].value = outarg;
>> +
>> + /* XXX do allocation to the actual size of the handle */
>> + args->out_args[1].size = fh_size;
>> + args->out_args[1].value = fh;
>> +
>> + return 0;
>> +}
>> +
>> +static void fuse_req_free_argvar_ext(struct fuse_args *args)
>> +{
>> + if (args->out_argvar)
>> + kfree(args->out_args[args->out_argvar_idx].value);
>> + if (args->is_ext)
>> + kfree(args->in_args[args->ext_idx].value);
>> +}
>> +
>
> Just wanted to point out that statx_out is > 256 bytes on stack
> so allocating 127+4 and the added complexity of ext arg
> seem awkward.
>
> Unless we really want to support huge file handles (we don't?)
> maybe the allocation can be restricted to fi->handle?
> Not sure.
If I understand you correctly, you're suggesting that the out_arg that
will return the handle should be handled on the stack as well and then it
would be copied to an allocated fi->handle. Sure, that can be done.
On the other hand, as I mentioned above, the outarg allocation is just a
simplification. So maybe the actual allocation of the handle may be done
elsewhere with the _actual_ fh size, and then simply used in fh->handle.
Please let me know if I got your comment right.
(And thanks for the comments, by the way!)
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 9:54 ` Luis Henriques
@ 2026-02-26 10:08 ` Amir Goldstein
2026-02-26 10:29 ` Miklos Szeredi
2026-02-26 10:33 ` Luis Henriques
0 siblings, 2 replies; 26+ messages in thread
From: Amir Goldstein @ 2026-02-26 10:08 UTC (permalink / raw)
To: Luis Henriques
Cc: Miklos Szeredi, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, Feb 26, 2026 at 10:54 AM Luis Henriques <luis@igalia.com> wrote:
>
> Hi Amir,
>
> On Wed, Feb 25 2026, Amir Goldstein wrote:
>
> > On Wed, Feb 25, 2026 at 12:25 PM Luis Henriques <luis@igalia.com> wrote:
> >>
> >> The implementation of lookup_handle+statx compound operation extends the
> >> lookup operation so that a file handle is be passed into the kernel. It
> >> also needs to include an extra inarg, so that the parent directory file
> >> handle can be sent to user-space. This extra inarg is added as an extension
> >> header to the request.
> >>
> >> By having a separate statx including in a compound operation allows the
> >> attr to be dropped from the lookup_handle request, simplifying the
> >> traditional FUSE lookup operation.
> >>
> >> Signed-off-by: Luis Henriques <luis@igalia.com>
> >> ---
> >> fs/fuse/dir.c | 294 +++++++++++++++++++++++++++++++++++---
> >> fs/fuse/fuse_i.h | 23 ++-
> >> fs/fuse/inode.c | 48 +++++--
> >> fs/fuse/readdir.c | 2 +-
> >> include/uapi/linux/fuse.h | 23 ++-
> >> 5 files changed, 355 insertions(+), 35 deletions(-)
> >>
> >> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
> >> index 5c0f1364c392..7fa8c405f1a3 100644
> >> --- a/fs/fuse/dir.c
> >> +++ b/fs/fuse/dir.c
> >> @@ -21,6 +21,7 @@
> >> #include <linux/security.h>
> >> #include <linux/types.h>
> >> #include <linux/kernel.h>
> >> +#include <linux/exportfs.h>
> >>
> >> static bool __read_mostly allow_sys_admin_access;
> >> module_param(allow_sys_admin_access, bool, 0644);
> >> @@ -372,6 +373,47 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
> >> args->out_args[0].value = outarg;
> >> }
> >>
> >> +static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
> >> + struct inode *parent_inode,
> >> + const struct qstr *name,
> >> + struct fuse_entry2_out *lookup_out,
> >> + struct fuse_statx_out *statx_out,
> >> + struct fuse_file_handle **fh);
> >> +static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr);
> >> +static int do_reval_lookup(struct fuse_mount *fm, u64 parent_nodeid,
> >> + const struct qstr *name, u64 *nodeid,
> >> + u64 *generation, u64 *attr_valid,
> >> + struct fuse_attr *attr, struct fuse_file_handle **fh)
> >> +{
> >> + struct fuse_entry_out entry_out;
> >> + struct fuse_entry2_out lookup_out;
> >> + struct fuse_statx_out statx_out;
> >> + FUSE_ARGS(lookup_args);
> >> + int ret = 0;
> >> +
> >> + if (fm->fc->lookup_handle) {
> >> + ret = do_lookup_handle_statx(fm, parent_nodeid, NULL, name,
> >> + &lookup_out, &statx_out, fh);
> >> + if (!ret) {
> >> + *nodeid = lookup_out.nodeid;
> >> + *generation = lookup_out.generation;
> >> + *attr_valid = fuse_time_to_jiffies(lookup_out.entry_valid,
> >> + lookup_out.entry_valid_nsec);
> >> + fuse_statx_to_attr(&statx_out.stat, attr);
> >> + }
> >> + } else {
> >> + fuse_lookup_init(&lookup_args, parent_nodeid, name, &entry_out);
> >> + ret = fuse_simple_request(fm, &lookup_args);
> >> + if (!ret) {
> >> + *nodeid = entry_out.nodeid;
> >> + *generation = entry_out.generation;
> >> + *attr_valid = ATTR_TIMEOUT(&entry_out);
> >> + memcpy(attr, &entry_out.attr, sizeof(*attr));
> >> + }
> >> + }
> >> +
> >> + return ret;
> >> +}
> >> /*
> >> * Check whether the dentry is still valid
> >> *
> >> @@ -399,10 +441,11 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> >> goto invalid;
> >> else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
> >> (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
> >> - struct fuse_entry_out outarg;
> >> - FUSE_ARGS(args);
> >> struct fuse_forget_link *forget;
> >> + struct fuse_file_handle *fh = NULL;
> >> u64 attr_version;
> >> + u64 nodeid, generation, attr_valid;
> >> + struct fuse_attr attr;
> >>
> >> /* For negative dentries, always do a fresh lookup */
> >> if (!inode)
> >> @@ -421,35 +464,36 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
> >>
> >> attr_version = fuse_get_attr_version(fm->fc);
> >>
> >> - fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
> >> - ret = fuse_simple_request(fm, &args);
> >> + ret = do_reval_lookup(fm, get_node_id(dir), name, &nodeid,
> >> + &generation, &attr_valid, &attr, &fh);
> >> /* Zero nodeid is same as -ENOENT */
> >> - if (!ret && !outarg.nodeid)
> >> + if (!ret && !nodeid)
> >> ret = -ENOENT;
> >> if (!ret) {
> >> fi = get_fuse_inode(inode);
> >> - if (outarg.nodeid != get_node_id(inode) ||
> >> - (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
> >> - fuse_queue_forget(fm->fc, forget,
> >> - outarg.nodeid, 1);
> >> + if (!fuse_file_handle_is_equal(fm->fc, fi->fh, fh) ||
> >> + nodeid != get_node_id(inode) ||
> >> + (bool) IS_AUTOMOUNT(inode) != (bool) (attr.flags & FUSE_ATTR_SUBMOUNT)) {
> >> + fuse_queue_forget(fm->fc, forget, nodeid, 1);
> >> + kfree(fh);
> >> goto invalid;
> >> }
> >> spin_lock(&fi->lock);
> >> fi->nlookup++;
> >> spin_unlock(&fi->lock);
> >> }
> >> + kfree(fh);
> >> kfree(forget);
> >> if (ret == -ENOMEM || ret == -EINTR)
> >> goto out;
> >> - if (ret || fuse_invalid_attr(&outarg.attr) ||
> >> - fuse_stale_inode(inode, outarg.generation, &outarg.attr))
> >> + if (ret || fuse_invalid_attr(&attr) ||
> >> + fuse_stale_inode(inode, generation, &attr))
> >> goto invalid;
> >>
> >> forget_all_cached_acls(inode);
> >> - fuse_change_attributes(inode, &outarg.attr, NULL,
> >> - ATTR_TIMEOUT(&outarg),
> >> + fuse_change_attributes(inode, &attr, NULL, attr_valid,
> >> attr_version);
> >> - fuse_change_entry_timeout(entry, &outarg);
> >> + fuse_dentry_settime(entry, attr_valid);
> >> } else if (inode) {
> >> fi = get_fuse_inode(inode);
> >> if (flags & LOOKUP_RCU) {
> >> @@ -546,8 +590,215 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
> >> return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size);
> >> }
> >>
> >> -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
> >> - u64 *time, struct inode **inode)
> >> +static int create_ext_handle(struct fuse_in_arg *ext, struct fuse_inode *fi)
> >> +{
> >> + struct fuse_ext_header *xh;
> >> + struct fuse_file_handle *fh;
> >> + u32 len;
> >> +
> >> + len = fuse_ext_size(sizeof(*fi->fh) + fi->fh->size);
> >> + xh = fuse_extend_arg(ext, len);
> >> + if (!xh)
> >> + return -ENOMEM;
> >> +
> >> + xh->size = len;
> >> + xh->type = FUSE_EXT_HANDLE;
> >> + fh = (struct fuse_file_handle *)&xh[1];
> >> + fh->size = fi->fh->size;
> >> + memcpy(fh->handle, fi->fh->handle, fh->size);
> >> +
> >> + return 0;
> >> +}
> >> +
> >> +static int fuse_lookup_handle_init(struct fuse_args *args, u64 nodeid,
> >> + struct fuse_inode *fi,
> >> + const struct qstr *name,
> >> + struct fuse_entry2_out *outarg)
> >> +{
> >> + struct fuse_file_handle *fh;
> >
> > Considering that fuse has long used uint64_t fh as the convention
> > for a file id all over the code, it would be better to pick a different
> > convention for fuse file handle, perhaps ffh, or fhandle?
>
> Good point, I'll make sure next revision will follow a different
> convention.
>
> >> + size_t fh_size = sizeof(*fh) + MAX_HANDLE_SZ;
> >
> > I don't remember what we concluded last time, but
> > shouldn't the server request max_handle_sz at init?
> > This constant is quite arbitrary.
>
> You're right, I should have pointed that out in the cover letter at least.
> In the previous version that maximum size was indeed provided by the
> server. But from the discussion here [0] I understood that this
> negotiation should be dropped. Here's what Miklos suggested:
>
> > How about allocating variable length arguments on demand? That would
> > allow getting rid of max_handle_size negotiation.
> >
> > args->out_var_alloc = true;
> > args->out_args[1].size = MAX_HANDLE_SZ;
> > args->out_args[1].value = NULL; /* Will be allocated to the actual size of the handle */
>
> Obviously that's not what the code is currently doing. The plan is to
> eventually set the .value to NULL and do the allocation elsewhere,
> according to the actual size returned.
>
> Because I didn't yet thought how/where the allocation could be done
> instead, this code is currently simplifying things, and that's why I
> picked this MAX_HANDLE_SZ.
>
> Sorry, I should have pointed that out at in a comment as well.
>
> [0] https://lore.kernel.org/all/CAJfpegszP+2XA=vADK4r09KU30BQd-r9sNu2Dog88yLG8iV7WQ@mail.gmail.com
>
> >> + int ret = -ENOMEM;
> >> +
> >> + fh = kzalloc(fh_size, GFP_KERNEL);
> >> + if (!fh)
> >> + return ret;
> >> +
> >> + memset(outarg, 0, sizeof(struct fuse_entry2_out));
> >> + args->opcode = FUSE_LOOKUP_HANDLE;
> >> + args->nodeid = nodeid;
> >> + args->in_numargs = 3;
> >> + fuse_set_zero_arg0(args);
> >> + args->in_args[1].size = name->len;
> >> + args->in_args[1].value = name->name;
> >> + args->in_args[2].size = 1;
> >> + args->in_args[2].value = "";
> >> + if (fi && fi->fh) {
> >
> > Same here fi->ffh? or fi->fhandle
>
> Ack!
>
> >> + args->is_ext = true;
> >> + args->ext_idx = args->in_numargs++;
> >> + args->in_args[args->ext_idx].size = 0;
> >> + ret = create_ext_handle(&args->in_args[args->ext_idx], fi);
> >> + if (ret) {
> >> + kfree(fh);
> >> + return ret;
> >> + }
> >> + }
> >> + args->out_numargs = 2;
> >> + args->out_argvar = true;
> >> + args->out_argvar_idx = 1;
> >> + args->out_args[0].size = sizeof(struct fuse_entry2_out);
> >> + args->out_args[0].value = outarg;
> >> +
> >> + /* XXX do allocation to the actual size of the handle */
> >> + args->out_args[1].size = fh_size;
> >> + args->out_args[1].value = fh;
> >> +
> >> + return 0;
> >> +}
> >> +
> >> +static void fuse_req_free_argvar_ext(struct fuse_args *args)
> >> +{
> >> + if (args->out_argvar)
> >> + kfree(args->out_args[args->out_argvar_idx].value);
> >> + if (args->is_ext)
> >> + kfree(args->in_args[args->ext_idx].value);
> >> +}
> >> +
> >
> > Just wanted to point out that statx_out is > 256 bytes on stack
> > so allocating 127+4 and the added complexity of ext arg
> > seem awkward.
> >
> > Unless we really want to support huge file handles (we don't?)
> > maybe the allocation can be restricted to fi->handle?
> > Not sure.
>
> If I understand you correctly, you're suggesting that the out_arg that
> will return the handle should be handled on the stack as well and then it
> would be copied to an allocated fi->handle. Sure, that can be done.
>
> On the other hand, as I mentioned above, the outarg allocation is just a
> simplification. So maybe the actual allocation of the handle may be done
> elsewhere with the _actual_ fh size, and then simply used in fh->handle.
>
> Please let me know if I got your comment right.
> (And thanks for the comments, by the way!)
file handle on stack only makes sense for small pre allocated size.
If the server has full control over handle size, then that is not relevant.
At some point we will need to address the fact that the most common
case is for very small file handles.
In struct fanotify_fid_event, we used a small inline buffer to optimize this
case. This could also be done for fuse_inode::handle, but we can worry about
that later.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 10:08 ` Amir Goldstein
@ 2026-02-26 10:29 ` Miklos Szeredi
2026-02-26 15:06 ` Luis Henriques
2026-02-26 10:33 ` Luis Henriques
1 sibling, 1 reply; 26+ messages in thread
From: Miklos Szeredi @ 2026-02-26 10:29 UTC (permalink / raw)
To: Amir Goldstein
Cc: Luis Henriques, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, 26 Feb 2026 at 11:08, Amir Goldstein <amir73il@gmail.com> wrote:
> file handle on stack only makes sense for small pre allocated size.
> If the server has full control over handle size, then that is not relevant.
I thought the point was that the file handle is available in
fi->handle and doesn't need to be allocated/copied. Instead
extensions could be done with an argument vector, like this:
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -326,6 +326,12 @@ struct fuse_folio_desc {
unsigned int offset;
};
+struct fuse_ext_arg {
+ u32 type;
+ u32 size;
+ const void *value;
+};
+
struct fuse_args {
uint64_t nodeid;
uint32_t opcode;
@@ -346,6 +352,7 @@ struct fuse_args {
bool is_pinned:1;
bool invalidate_vmap:1;
struct fuse_in_arg in_args[4];
+ struct fuse_ext_arg ext_args[2];
struct fuse_arg out_args[2];
void (*end)(struct fuse_mount *fm, struct fuse_args *args, int error);
/* Used for kvec iter backed by vmalloc address */
Thanks,
Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 10:08 ` Amir Goldstein
2026-02-26 10:29 ` Miklos Szeredi
@ 2026-02-26 10:33 ` Luis Henriques
1 sibling, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-26 10:33 UTC (permalink / raw)
To: Amir Goldstein
Cc: Miklos Szeredi, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, Feb 26 2026, Amir Goldstein wrote:
> On Thu, Feb 26, 2026 at 10:54 AM Luis Henriques <luis@igalia.com> wrote:
>>
>> Hi Amir,
>>
>> On Wed, Feb 25 2026, Amir Goldstein wrote:
>>
>> > On Wed, Feb 25, 2026 at 12:25 PM Luis Henriques <luis@igalia.com> wrote:
>> >>
>> >> The implementation of lookup_handle+statx compound operation extends the
>> >> lookup operation so that a file handle is be passed into the kernel. It
>> >> also needs to include an extra inarg, so that the parent directory file
>> >> handle can be sent to user-space. This extra inarg is added as an extension
>> >> header to the request.
>> >>
>> >> By having a separate statx including in a compound operation allows the
>> >> attr to be dropped from the lookup_handle request, simplifying the
>> >> traditional FUSE lookup operation.
>> >>
>> >> Signed-off-by: Luis Henriques <luis@igalia.com>
>> >> ---
>> >> fs/fuse/dir.c | 294 +++++++++++++++++++++++++++++++++++---
>> >> fs/fuse/fuse_i.h | 23 ++-
>> >> fs/fuse/inode.c | 48 +++++--
>> >> fs/fuse/readdir.c | 2 +-
>> >> include/uapi/linux/fuse.h | 23 ++-
>> >> 5 files changed, 355 insertions(+), 35 deletions(-)
>> >>
>> >> diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
>> >> index 5c0f1364c392..7fa8c405f1a3 100644
>> >> --- a/fs/fuse/dir.c
>> >> +++ b/fs/fuse/dir.c
>> >> @@ -21,6 +21,7 @@
>> >> #include <linux/security.h>
>> >> #include <linux/types.h>
>> >> #include <linux/kernel.h>
>> >> +#include <linux/exportfs.h>
>> >>
>> >> static bool __read_mostly allow_sys_admin_access;
>> >> module_param(allow_sys_admin_access, bool, 0644);
>> >> @@ -372,6 +373,47 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
>> >> args->out_args[0].value = outarg;
>> >> }
>> >>
>> >> +static int do_lookup_handle_statx(struct fuse_mount *fm, u64 parent_nodeid,
>> >> + struct inode *parent_inode,
>> >> + const struct qstr *name,
>> >> + struct fuse_entry2_out *lookup_out,
>> >> + struct fuse_statx_out *statx_out,
>> >> + struct fuse_file_handle **fh);
>> >> +static void fuse_statx_to_attr(struct fuse_statx *sx, struct fuse_attr *attr);
>> >> +static int do_reval_lookup(struct fuse_mount *fm, u64 parent_nodeid,
>> >> + const struct qstr *name, u64 *nodeid,
>> >> + u64 *generation, u64 *attr_valid,
>> >> + struct fuse_attr *attr, struct fuse_file_handle **fh)
>> >> +{
>> >> + struct fuse_entry_out entry_out;
>> >> + struct fuse_entry2_out lookup_out;
>> >> + struct fuse_statx_out statx_out;
>> >> + FUSE_ARGS(lookup_args);
>> >> + int ret = 0;
>> >> +
>> >> + if (fm->fc->lookup_handle) {
>> >> + ret = do_lookup_handle_statx(fm, parent_nodeid, NULL, name,
>> >> + &lookup_out, &statx_out, fh);
>> >> + if (!ret) {
>> >> + *nodeid = lookup_out.nodeid;
>> >> + *generation = lookup_out.generation;
>> >> + *attr_valid = fuse_time_to_jiffies(lookup_out.entry_valid,
>> >> + lookup_out.entry_valid_nsec);
>> >> + fuse_statx_to_attr(&statx_out.stat, attr);
>> >> + }
>> >> + } else {
>> >> + fuse_lookup_init(&lookup_args, parent_nodeid, name, &entry_out);
>> >> + ret = fuse_simple_request(fm, &lookup_args);
>> >> + if (!ret) {
>> >> + *nodeid = entry_out.nodeid;
>> >> + *generation = entry_out.generation;
>> >> + *attr_valid = ATTR_TIMEOUT(&entry_out);
>> >> + memcpy(attr, &entry_out.attr, sizeof(*attr));
>> >> + }
>> >> + }
>> >> +
>> >> + return ret;
>> >> +}
>> >> /*
>> >> * Check whether the dentry is still valid
>> >> *
>> >> @@ -399,10 +441,11 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>> >> goto invalid;
>> >> else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
>> >> (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
>> >> - struct fuse_entry_out outarg;
>> >> - FUSE_ARGS(args);
>> >> struct fuse_forget_link *forget;
>> >> + struct fuse_file_handle *fh = NULL;
>> >> u64 attr_version;
>> >> + u64 nodeid, generation, attr_valid;
>> >> + struct fuse_attr attr;
>> >>
>> >> /* For negative dentries, always do a fresh lookup */
>> >> if (!inode)
>> >> @@ -421,35 +464,36 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
>> >>
>> >> attr_version = fuse_get_attr_version(fm->fc);
>> >>
>> >> - fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
>> >> - ret = fuse_simple_request(fm, &args);
>> >> + ret = do_reval_lookup(fm, get_node_id(dir), name, &nodeid,
>> >> + &generation, &attr_valid, &attr, &fh);
>> >> /* Zero nodeid is same as -ENOENT */
>> >> - if (!ret && !outarg.nodeid)
>> >> + if (!ret && !nodeid)
>> >> ret = -ENOENT;
>> >> if (!ret) {
>> >> fi = get_fuse_inode(inode);
>> >> - if (outarg.nodeid != get_node_id(inode) ||
>> >> - (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
>> >> - fuse_queue_forget(fm->fc, forget,
>> >> - outarg.nodeid, 1);
>> >> + if (!fuse_file_handle_is_equal(fm->fc, fi->fh, fh) ||
>> >> + nodeid != get_node_id(inode) ||
>> >> + (bool) IS_AUTOMOUNT(inode) != (bool) (attr.flags & FUSE_ATTR_SUBMOUNT)) {
>> >> + fuse_queue_forget(fm->fc, forget, nodeid, 1);
>> >> + kfree(fh);
>> >> goto invalid;
>> >> }
>> >> spin_lock(&fi->lock);
>> >> fi->nlookup++;
>> >> spin_unlock(&fi->lock);
>> >> }
>> >> + kfree(fh);
>> >> kfree(forget);
>> >> if (ret == -ENOMEM || ret == -EINTR)
>> >> goto out;
>> >> - if (ret || fuse_invalid_attr(&outarg.attr) ||
>> >> - fuse_stale_inode(inode, outarg.generation, &outarg.attr))
>> >> + if (ret || fuse_invalid_attr(&attr) ||
>> >> + fuse_stale_inode(inode, generation, &attr))
>> >> goto invalid;
>> >>
>> >> forget_all_cached_acls(inode);
>> >> - fuse_change_attributes(inode, &outarg.attr, NULL,
>> >> - ATTR_TIMEOUT(&outarg),
>> >> + fuse_change_attributes(inode, &attr, NULL, attr_valid,
>> >> attr_version);
>> >> - fuse_change_entry_timeout(entry, &outarg);
>> >> + fuse_dentry_settime(entry, attr_valid);
>> >> } else if (inode) {
>> >> fi = get_fuse_inode(inode);
>> >> if (flags & LOOKUP_RCU) {
>> >> @@ -546,8 +590,215 @@ bool fuse_invalid_attr(struct fuse_attr *attr)
>> >> return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size);
>> >> }
>> >>
>> >> -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name,
>> >> - u64 *time, struct inode **inode)
>> >> +static int create_ext_handle(struct fuse_in_arg *ext, struct fuse_inode *fi)
>> >> +{
>> >> + struct fuse_ext_header *xh;
>> >> + struct fuse_file_handle *fh;
>> >> + u32 len;
>> >> +
>> >> + len = fuse_ext_size(sizeof(*fi->fh) + fi->fh->size);
>> >> + xh = fuse_extend_arg(ext, len);
>> >> + if (!xh)
>> >> + return -ENOMEM;
>> >> +
>> >> + xh->size = len;
>> >> + xh->type = FUSE_EXT_HANDLE;
>> >> + fh = (struct fuse_file_handle *)&xh[1];
>> >> + fh->size = fi->fh->size;
>> >> + memcpy(fh->handle, fi->fh->handle, fh->size);
>> >> +
>> >> + return 0;
>> >> +}
>> >> +
>> >> +static int fuse_lookup_handle_init(struct fuse_args *args, u64 nodeid,
>> >> + struct fuse_inode *fi,
>> >> + const struct qstr *name,
>> >> + struct fuse_entry2_out *outarg)
>> >> +{
>> >> + struct fuse_file_handle *fh;
>> >
>> > Considering that fuse has long used uint64_t fh as the convention
>> > for a file id all over the code, it would be better to pick a different
>> > convention for fuse file handle, perhaps ffh, or fhandle?
>>
>> Good point, I'll make sure next revision will follow a different
>> convention.
>>
>> >> + size_t fh_size = sizeof(*fh) + MAX_HANDLE_SZ;
>> >
>> > I don't remember what we concluded last time, but
>> > shouldn't the server request max_handle_sz at init?
>> > This constant is quite arbitrary.
>>
>> You're right, I should have pointed that out in the cover letter at least.
>> In the previous version that maximum size was indeed provided by the
>> server. But from the discussion here [0] I understood that this
>> negotiation should be dropped. Here's what Miklos suggested:
>>
>> > How about allocating variable length arguments on demand? That would
>> > allow getting rid of max_handle_size negotiation.
>> >
>> > args->out_var_alloc = true;
>> > args->out_args[1].size = MAX_HANDLE_SZ;
>> > args->out_args[1].value = NULL; /* Will be allocated to the actual size of the handle */
>>
>> Obviously that's not what the code is currently doing. The plan is to
>> eventually set the .value to NULL and do the allocation elsewhere,
>> according to the actual size returned.
>>
>> Because I didn't yet thought how/where the allocation could be done
>> instead, this code is currently simplifying things, and that's why I
>> picked this MAX_HANDLE_SZ.
>>
>> Sorry, I should have pointed that out at in a comment as well.
>>
>> [0] https://lore.kernel.org/all/CAJfpegszP+2XA=vADK4r09KU30BQd-r9sNu2Dog88yLG8iV7WQ@mail.gmail.com
>>
>> >> + int ret = -ENOMEM;
>> >> +
>> >> + fh = kzalloc(fh_size, GFP_KERNEL);
>> >> + if (!fh)
>> >> + return ret;
>> >> +
>> >> + memset(outarg, 0, sizeof(struct fuse_entry2_out));
>> >> + args->opcode = FUSE_LOOKUP_HANDLE;
>> >> + args->nodeid = nodeid;
>> >> + args->in_numargs = 3;
>> >> + fuse_set_zero_arg0(args);
>> >> + args->in_args[1].size = name->len;
>> >> + args->in_args[1].value = name->name;
>> >> + args->in_args[2].size = 1;
>> >> + args->in_args[2].value = "";
>> >> + if (fi && fi->fh) {
>> >
>> > Same here fi->ffh? or fi->fhandle
>>
>> Ack!
>>
>> >> + args->is_ext = true;
>> >> + args->ext_idx = args->in_numargs++;
>> >> + args->in_args[args->ext_idx].size = 0;
>> >> + ret = create_ext_handle(&args->in_args[args->ext_idx], fi);
>> >> + if (ret) {
>> >> + kfree(fh);
>> >> + return ret;
>> >> + }
>> >> + }
>> >> + args->out_numargs = 2;
>> >> + args->out_argvar = true;
>> >> + args->out_argvar_idx = 1;
>> >> + args->out_args[0].size = sizeof(struct fuse_entry2_out);
>> >> + args->out_args[0].value = outarg;
>> >> +
>> >> + /* XXX do allocation to the actual size of the handle */
>> >> + args->out_args[1].size = fh_size;
>> >> + args->out_args[1].value = fh;
>> >> +
>> >> + return 0;
>> >> +}
>> >> +
>> >> +static void fuse_req_free_argvar_ext(struct fuse_args *args)
>> >> +{
>> >> + if (args->out_argvar)
>> >> + kfree(args->out_args[args->out_argvar_idx].value);
>> >> + if (args->is_ext)
>> >> + kfree(args->in_args[args->ext_idx].value);
>> >> +}
>> >> +
>> >
>> > Just wanted to point out that statx_out is > 256 bytes on stack
>> > so allocating 127+4 and the added complexity of ext arg
>> > seem awkward.
>> >
>> > Unless we really want to support huge file handles (we don't?)
>> > maybe the allocation can be restricted to fi->handle?
>> > Not sure.
>>
>> If I understand you correctly, you're suggesting that the out_arg that
>> will return the handle should be handled on the stack as well and then it
>> would be copied to an allocated fi->handle. Sure, that can be done.
>>
>> On the other hand, as I mentioned above, the outarg allocation is just a
>> simplification. So maybe the actual allocation of the handle may be done
>> elsewhere with the _actual_ fh size, and then simply used in fh->handle.
>>
>> Please let me know if I got your comment right.
>> (And thanks for the comments, by the way!)
>
> file handle on stack only makes sense for small pre allocated size.
> If the server has full control over handle size, then that is not relevant.
>
> At some point we will need to address the fact that the most common
> case is for very small file handles.
>
> In struct fanotify_fid_event, we used a small inline buffer to optimize this
> case. This could also be done for fuse_inode::handle, but we can worry about
> that later.
Thanks, I had took a look into it before -- I think you had pointed it to
me!. But I agree that this is something that can be handled once I have
most of the other things sorted out.
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 10:29 ` Miklos Szeredi
@ 2026-02-26 15:06 ` Luis Henriques
2026-02-26 15:44 ` Miklos Szeredi
0 siblings, 1 reply; 26+ messages in thread
From: Luis Henriques @ 2026-02-26 15:06 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, Feb 26 2026, Miklos Szeredi wrote:
> On Thu, 26 Feb 2026 at 11:08, Amir Goldstein <amir73il@gmail.com> wrote:
>
>> file handle on stack only makes sense for small pre allocated size.
>> If the server has full control over handle size, then that is not relevant.
>
> I thought the point was that the file handle is available in
> fi->handle and doesn't need to be allocated/copied. Instead
> extensions could be done with an argument vector, like this:
Right now the code is using extensions in the lookup_handle operation
inargs, and only if a file handle is available for the parent inode.
Are you saying that outargs should also use extensions for getting the
file handle in a lookup_handle?
Cheers,
--
Luís
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -326,6 +326,12 @@ struct fuse_folio_desc {
> unsigned int offset;
> };
>
> +struct fuse_ext_arg {
> + u32 type;
> + u32 size;
> + const void *value;
> +};
> +
> struct fuse_args {
> uint64_t nodeid;
> uint32_t opcode;
> @@ -346,6 +352,7 @@ struct fuse_args {
> bool is_pinned:1;
> bool invalidate_vmap:1;
> struct fuse_in_arg in_args[4];
> + struct fuse_ext_arg ext_args[2];
> struct fuse_arg out_args[2];
> void (*end)(struct fuse_mount *fm, struct fuse_args *args, int error);
> /* Used for kvec iter backed by vmalloc address */
>
> Thanks,
> Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 15:06 ` Luis Henriques
@ 2026-02-26 15:44 ` Miklos Szeredi
2026-02-26 16:17 ` Luis Henriques
0 siblings, 1 reply; 26+ messages in thread
From: Miklos Szeredi @ 2026-02-26 15:44 UTC (permalink / raw)
To: Luis Henriques
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, 26 Feb 2026 at 16:07, Luis Henriques <luis@igalia.com> wrote:
> Are you saying that outargs should also use extensions for getting the
> file handle in a lookup_handle?
No.
I'm saying that extend_arg() thing is messy and a using vectored args
for extensions (same as we do for normal input arguments) might be
better.
Thanks,
Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation
2026-02-26 15:44 ` Miklos Szeredi
@ 2026-02-26 16:17 ` Luis Henriques
0 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-26 16:17 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Thu, Feb 26 2026, Miklos Szeredi wrote:
> On Thu, 26 Feb 2026 at 16:07, Luis Henriques <luis@igalia.com> wrote:
>
>> Are you saying that outargs should also use extensions for getting the
>> file handle in a lookup_handle?
>
> No.
>
> I'm saying that extend_arg() thing is messy and a using vectored args
> for extensions (same as we do for normal input arguments) might be
> better.
It is indeed a messy interface. I'll have a look into it and see how to
modify it to accommodate your suggestion. And thanks for your patience ;-)
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 3/8] fuse: store index of the variable length argument
2026-02-25 11:24 ` [RFC PATCH v3 3/8] fuse: store index of the variable length argument Luis Henriques
@ 2026-02-27 15:41 ` Miklos Szeredi
2026-02-28 14:50 ` Luis Henriques
0 siblings, 1 reply; 26+ messages in thread
From: Miklos Szeredi @ 2026-02-27 15:41 UTC (permalink / raw)
To: Luis Henriques
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Wed, 25 Feb 2026 at 12:25, Luis Henriques <luis@igalia.com> wrote:
>
> Operations that have a variable length argument assume that it will always
> be the last argument on the list. This patch allows this assumption to be
> removed by keeping track of the index of variable length argument.
Example please.
Thanks,
Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface
2026-02-25 11:24 ` [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface Luis Henriques
@ 2026-02-27 15:46 ` Miklos Szeredi
2026-02-28 14:42 ` Luis Henriques
0 siblings, 1 reply; 26+ messages in thread
From: Miklos Szeredi @ 2026-02-27 15:46 UTC (permalink / raw)
To: Luis Henriques
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Wed, 25 Feb 2026 at 12:25, Luis Henriques <luis@igalia.com> wrote:
> @@ -570,30 +571,34 @@ 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);
> err = fuse_simple_request(fm, &args);
> /* Zero nodeid is same as -ENOENT, but with valid timeout */
> - if (err || !outarg->nodeid)
> + if (err || !outarg.nodeid)
> goto out_put_forget;
And now the timeout is skipped for the !outarg.nodeid case...
Thanks,
Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init()
2026-02-25 11:24 ` [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init() Luis Henriques
@ 2026-02-27 15:57 ` Miklos Szeredi
0 siblings, 0 replies; 26+ messages in thread
From: Miklos Szeredi @ 2026-02-27 15:57 UTC (permalink / raw)
To: Luis Henriques
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Wed, 25 Feb 2026 at 12:25, Luis Henriques <luis@igalia.com> wrote:
>
> Remove the fuse_conn argument from function fuse_lookup_init() as it isn't
> used since commit 21f621741a77 ("fuse: fix LOOKUP vs INIT compat handling").
>
> Signed-off-by: Luis Henriques <luis@igalia.com>
Applied, thanks.
Miklos
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface
2026-02-27 15:46 ` Miklos Szeredi
@ 2026-02-28 14:42 ` Luis Henriques
0 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-28 14:42 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Fri, Feb 27 2026, Miklos Szeredi wrote:
> On Wed, 25 Feb 2026 at 12:25, Luis Henriques <luis@igalia.com> wrote:
>
>> @@ -570,30 +571,34 @@ 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);
>> err = fuse_simple_request(fm, &args);
>> /* Zero nodeid is same as -ENOENT, but with valid timeout */
>> - if (err || !outarg->nodeid)
>> + if (err || !outarg.nodeid)
>> goto out_put_forget;
>
> And now the timeout is skipped for the !outarg.nodeid case...
Oops, yeah I missed that. I'll fix it for the next iteration, thanks!
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
* Re: [RFC PATCH v3 3/8] fuse: store index of the variable length argument
2026-02-27 15:41 ` Miklos Szeredi
@ 2026-02-28 14:50 ` Luis Henriques
0 siblings, 0 replies; 26+ messages in thread
From: Luis Henriques @ 2026-02-28 14:50 UTC (permalink / raw)
To: Miklos Szeredi
Cc: Amir Goldstein, Bernd Schubert, Bernd Schubert, Darrick J. Wong,
Horst Birthelmer, Joanne Koong, Kevin Chen, linux-fsdevel,
linux-kernel, Matt Harvey, kernel-dev
On Fri, Feb 27 2026, Miklos Szeredi wrote:
> On Wed, 25 Feb 2026 at 12:25, Luis Henriques <luis@igalia.com> wrote:
>>
>> Operations that have a variable length argument assume that it will always
>> be the last argument on the list. This patch allows this assumption to be
>> removed by keeping track of the index of variable length argument.
>
> Example please.
OK, I see this is probably no longer required. In v2 the changes to
FUSE_CREATE (still without using compounds) would require the 1st outarg
to be of variable length. v3 doesn't seem to require it any more and I
failed to see that. So I guess this patch can be dropped.
Cheers,
--
Luís
^ permalink raw reply [flat|nested] 26+ messages in thread
end of thread, other threads:[~2026-02-28 14:50 UTC | newest]
Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-25 11:24 [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 1/8] fuse: simplify fuse_lookup_name() interface Luis Henriques
2026-02-27 15:46 ` Miklos Szeredi
2026-02-28 14:42 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 2/8] fuse: export extend_arg() and factor out fuse_ext_size() Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 3/8] fuse: store index of the variable length argument Luis Henriques
2026-02-27 15:41 ` Miklos Szeredi
2026-02-28 14:50 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 4/8] fuse: drop unnecessary argument from fuse_lookup_init() Luis Henriques
2026-02-27 15:57 ` Miklos Szeredi
2026-02-25 11:24 ` [RFC PATCH v3 5/8] fuse: extract helper functions from fuse_do_statx() Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 6/8] fuse: implementation of lookup_handle+statx compound operation Luis Henriques
2026-02-25 18:06 ` Amir Goldstein
2026-02-26 9:54 ` Luis Henriques
2026-02-26 10:08 ` Amir Goldstein
2026-02-26 10:29 ` Miklos Szeredi
2026-02-26 15:06 ` Luis Henriques
2026-02-26 15:44 ` Miklos Szeredi
2026-02-26 16:17 ` Luis Henriques
2026-02-26 10:33 ` Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 7/8] fuse: export fuse_open_args_fill() helper function Luis Henriques
2026-02-25 11:24 ` [RFC PATCH v3 8/8] fuse: implementation of mkobj_handle+statx+open compound operation Luis Henriques
2026-02-25 15:08 ` Horst Birthelmer
2026-02-25 17:26 ` Luis Henriques
2026-02-25 15:14 ` [RFC PATCH v3 0/8] fuse: LOOKUP_HANDLE operation Horst Birthelmer
2026-02-25 17:06 ` Luis Henriques
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox