mkdir(), mknod(), symlink(), link() and renameat2() with RENAME_NOREPLACE look the new name up with FUSE_LOOKUP before sending the request, which costs a round trip to the server.
Add an INIT flag, FUSE_EXCL_CREATE, with which the server fails FUSE_MKNOD, FUSE_MKDIR, FUSE_SYMLINK, FUSE_LINK, and FUSE_RENAME2 with RENAME_NOREPLACE, with -EEXIST for an existing name. When the server sets it, the lookup of a name that is about to be created returns without sending FUSE_LOOKUP, and an existing name is reported by the -EEXIST of the request. After a failed request the name stays uncached, so the next lookup goes to the server. A rename that may replace its target looks the target up, so that the kernel can check the permission to replace it. Assisted-by: Claude:claude-opus-5-5 [Claude-Code] Signed-off-by: Jimmy Zuber <[email protected]> --- fs/fuse/dir.c | 10 ++++++++++ fs/fuse/fuse_i.h | 3 +++ fs/fuse/inode.c | 4 +++- include/uapi/linux/fuse.h | 10 +++++++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index f48fafccce4b..e491e6b3215a 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -627,6 +627,16 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry, return ERR_PTR(-EIO); fc = get_fuse_conn_super(dir->i_sb); + + /* The request fails with -EEXIST if the name exists */ + if (fc->excl_create && + (flags & (LOOKUP_CREATE | LOOKUP_EXCL)) == + (LOOKUP_CREATE | LOOKUP_EXCL)) { + if (entry->d_name.len > fc->name_max) + return ERR_PTR(-ENAMETOOLONG); + return NULL; + } + epoch = atomic_read(&fc->epoch); locked = fuse_lock_inode(dir); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 8546855386b5..babbacdfb757 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -509,6 +509,9 @@ struct fuse_conn { */ unsigned atomic_o_trunc:1; + /** @excl_create: Create requests check for an existing name */ + unsigned excl_create:1; + /** * @export_support: Filesystem supports NFS exporting. * Only set in INIT diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index cbb10e19e7e8..1f1be0c521a1 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -1432,6 +1432,8 @@ static void process_init_reply(struct fuse_args *args, int error) if (fuse_syncfs_enable(fc, flags)) fc->sync_fs = 1; + if (flags & FUSE_EXCL_CREATE) + fc->excl_create = 1; } else { ra_pages = fc->max_read / PAGE_SIZE; fc->no_lock = 1; @@ -1490,7 +1492,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_EXCL_CREATE; #ifdef CONFIG_FUSE_VDAX if (fm->fc->vdax) flags |= FUSE_MAP_ALIGNMENT; diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index 10a7f31c4bdf..8c079d3080bf 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -251,6 +251,9 @@ * * 7.47 * - add FUSE_HAS_SYNCFS opt-in flag for privileged userspace servers + * + * 7.48 + * - add FUSE_EXCL_CREATE */ #ifndef _LINUX_FUSE_H @@ -286,7 +289,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 47 +#define FUSE_KERNEL_MINOR_VERSION 48 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -473,6 +476,10 @@ struct fuse_file_lock { * with CAP_SYS_ADMIN in the initial user namespace (the same * privilege that mounting virtiofs or fuseblk requires). * Insufficiently privileged servers ignore it. + * FUSE_EXCL_CREATE: FUSE_MKNOD, FUSE_MKDIR, FUSE_SYMLINK, FUSE_LINK and + * FUSE_RENAME2 with RENAME_NOREPLACE fail with -EEXIST for an + * existing name, so the kernel sends them without a LOOKUP of + * the name */ #define FUSE_ASYNC_READ (1 << 0) #define FUSE_POSIX_LOCKS (1 << 1) @@ -522,6 +529,7 @@ struct fuse_file_lock { #define FUSE_REQUEST_TIMEOUT (1ULL << 42) #define FUSE_HAS_IO_URING_BUFPOOL (1ULL << 43) #define FUSE_HAS_SYNCFS (1ULL << 44) +#define FUSE_EXCL_CREATE (1ULL << 45) /** * CUSE INIT request/reply flags -- 2.50.1
