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


Reply via email to