From: Jimmy Zuber <[email protected]> Add FUSE_OPEN_ATOMIC, allowing the kernel to skip LOOKUP to improve performance. A server implementing FUSE_OPEN_ATOMIC will look up the name, create it if O_CREAT is supplied, and open it in one request.
The server replies with the entry and the open file. If the file was created, the server returns FOPEN_CREATED, or just the entry if it was not created. If the name does not exist, the server returns a zero nodeid, cacheable like a negative LOOKUP. An existing file opened with O_TRUNC is returned unopened, so that truncation can be performed after permissions are checked. FMODE_CREATED is set only when the server reports that it created the file. Therefore a file created by someone else between LOOKUP and CREATE is no longer opened without a permission check. A server without the request replies -ENOSYS, and the kernel falls back to FUSE_LOOKUP, keeping backwards compatibility. Assisted-by: Claude:claude-opus-5-5 [Claude-Code] Signed-off-by: Jimmy Zuber <[email protected]> --- fs/fuse/dir.c | 112 +++++++++++++++++++++++++++++++++++--- fs/fuse/fuse_i.h | 3 + include/uapi/linux/fuse.h | 8 ++- 3 files changed, 113 insertions(+), 10 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index f48fafccce4b..34d2271eb248 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -815,7 +815,47 @@ static void free_ext_value(struct fuse_args *args) } /* - * Atomic create+open operation + * FUSE_OPEN_ATOMIC found an existing name and left the open to the VFS. A + * zero nodeid means the name does not exist. + */ +static int fuse_open_atomic_found(struct inode *dir, struct dentry *entry, + struct file *file, + struct fuse_entry_out *outarg, + struct fuse_forget_link *forget, int epoch, + u64 attr_version, u64 evict_ctr) +{ + struct fuse_conn *fc = get_fuse_conn(dir); + struct inode *inode = NULL; + struct dentry *res; + + if (outarg->nodeid) { + inode = fuse_iget(dir->i_sb, outarg->nodeid, outarg->generation, + &outarg->attr, ATTR_TIMEOUT(outarg), + attr_version, evict_ctr); + if (!inode) { + fuse_chan_queue_forget(fc->chan, forget, + outarg->nodeid, 1); + return -ENOMEM; + } + } + kfree(forget); + if (inode && get_node_id(inode) == FUSE_ROOT_ID) { + iput(inode); + return -EIO; + } + + d_drop(entry); + res = d_splice_alias(inode, entry); + if (!IS_ERR(res)) { + fuse_dentry_set_epoch(res ?: entry, epoch); + fuse_change_entry_timeout(res ?: entry, outarg); + } + return finish_no_open(file, res); +} + +/* + * Atomic create+open operation, or lookup with optional create and open for + * FUSE_OPEN_ATOMIC * * If the filesystem doesn't support this, then fall back to separate * 'mknod' + 'open' requests. @@ -833,8 +873,12 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, struct fuse_entry_out outentry; struct fuse_inode *fi; struct fuse_file *ff; + u64 attr_version, evict_ctr; int epoch, err; bool trunc = flags & O_TRUNC; + bool atomic = opcode == FUSE_OPEN_ATOMIC; + bool create = !atomic || (flags & O_CREAT); + bool locked, created; /* Userspace expects S_IFREG in create mode */ BUG_ON((mode & S_IFMT) != S_IFREG); @@ -878,26 +922,62 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, outopenp = &ff->args->open_outarg; args.out_args[1].size = sizeof(*outopenp); args.out_args[1].value = outopenp; + /* FUSE_OPEN_ATOMIC leaves out fuse_open_out if it did not open */ + args.out_argvar = atomic; - err = get_create_ext(idmap, &args, dir, entry, mode); - if (err) - goto out_free_ff; + if (create) { + err = get_create_ext(idmap, &args, dir, entry, mode); + if (err) + goto out_free_ff; + } else { + idmap = &invalid_mnt_idmap; + } + attr_version = fuse_get_attr_version(fm->fc); + evict_ctr = fuse_get_evict_ctr(fm->fc); + + /* Without O_CREAT the parent is locked shared, as for a lookup */ + locked = !create && fuse_lock_inode(dir); err = fuse_simple_idmap_request(idmap, fm, &args); + fuse_unlock_inode(dir, locked); free_ext_value(&args); - if (err) + if (err < 0) goto out_free_ff; err = -EIO; + if (atomic && !args.out_args[1].size) { + /* A zero nodeid: the name does not exist and was not created */ + if (!outentry.nodeid && create) + goto out_free_ff; + if (outentry.nodeid && (invalid_nodeid(outentry.nodeid) || + fuse_invalid_attr(&outentry.attr))) + goto out_free_ff; + fuse_file_free(ff); + return fuse_open_atomic_found(dir, entry, file, &outentry, + forget, epoch, attr_version, + evict_ctr); + } + if (!S_ISREG(outentry.attr.mode) || invalid_nodeid(outentry.nodeid) || - fuse_invalid_attr(&outentry.attr)) + fuse_invalid_attr(&outentry.attr) || + args.out_args[1].size != sizeof(*outopenp)) goto out_free_ff; + created = !atomic || (outopenp->open_flags & FOPEN_CREATED); + if (atomic) { + /* Only O_CREAT creates; O_TRUNC opens are left to the VFS */ + if ((created && !create) || (!created && trunc)) + goto out_free_ff; + if (created) + file->f_mode |= FMODE_CREATED; + } + ff->fh = outopenp->fh; 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), + created ? 0 : attr_version, created ? 0 : evict_ctr); if (!inode) { flags &= ~(O_CREAT | O_EXCL | O_TRUNC); fuse_sync_release(NULL, ff, flags); @@ -906,10 +986,14 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir, goto out_err; } kfree(forget); - d_instantiate(entry, inode); + if (d_in_lookup(entry)) + d_splice_alias(inode, entry); + else + d_instantiate(entry, inode); fuse_dentry_set_epoch(entry, epoch); fuse_change_entry_timeout(entry, &outentry); - fuse_dir_changed(dir); + if (created) + fuse_dir_changed(dir); err = generic_file_open(inode, file); if (!err) { file->private_data = ff; @@ -947,6 +1031,16 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry, if (fuse_is_bad(dir)) return -EIO; + if (!fc->no_open_atomic && (d_in_lookup(entry) || (flags & O_CREAT))) { + err = fuse_create_open(idmap, dir, entry, file, flags, + mode | S_IFREG, FUSE_OPEN_ATOMIC); + if (err == -EEXIST) + fuse_invalidate_entry(entry); + if (err != -ENOSYS) + return err; + fc->no_open_atomic = 1; + } + if (d_in_lookup(entry)) { struct dentry *res = fuse_lookup(dir, entry, 0); if (res || d_really_is_positive(entry)) diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 8546855386b5..ba6dc4c43a1f 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -589,6 +589,9 @@ struct fuse_conn { /** @no_create: Is create not implemented by fs? */ unsigned no_create:1; + /** @no_open_atomic: Is open atomic not implemented by fs? */ + unsigned no_open_atomic:1; + /** @no_bmap: Is bmap not implemented by fs? */ unsigned no_bmap:1; diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index 10a7f31c4bdf..745b686c468f 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_OPEN_ATOMIC and FOPEN_CREATED */ #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 @@ -400,6 +403,7 @@ struct fuse_file_lock { * (FUSE_URING_ZERO_COPY) and the request carries page * payload. Otherwise reads/writes fall back to * copying. + * FOPEN_CREATED: FUSE_OPEN_ATOMIC created the file */ #define FOPEN_DIRECT_IO (1 << 0) #define FOPEN_KEEP_CACHE (1 << 1) @@ -410,6 +414,7 @@ struct fuse_file_lock { #define FOPEN_PARALLEL_DIRECT_WRITES (1 << 6) #define FOPEN_PASSTHROUGH (1 << 7) #define FOPEN_IO_URING_ZERO_COPY (1 << 8) +#define FOPEN_CREATED (1 << 9) /** * INIT request/reply flags @@ -690,6 +695,7 @@ enum fuse_opcode { FUSE_TMPFILE = 51, FUSE_STATX = 52, FUSE_COPY_FILE_RANGE_64 = 53, + FUSE_OPEN_ATOMIC = 54, /* CUSE specific operations */ CUSE_INIT = 4096, -- 2.50.1
