Filesystems serving cached content may not need per-open state for most
inodes, while still relying on OPEN for control files. The connection-wide
no-open behavior selected by ENOSYS cannot express this distinction.

Add FUSE_PER_INODE_NO_OPEN to INIT negotiation and FUSE_ATTR_NO_OPEN to
inode attributes. For marked inodes, use the existing zero-handle defaults
and omit OPEN/OPENDIR and the corresponding RELEASE/RELEASEDIR. Store the
release decision in the file, so attribute changes cannot suppress release
of a server-opened handle or cause release of a locally opened one.

Keep the release argument allocation for regular files, which pins the
inode while asynchronous I/O completes. Honor the hint for internal opens
used by file-attribute ioctls as well. The capability check excludes CUSE
before accessing its non-FUSE inode as a fuse_inode.

Continue sending OPEN for atomic O_TRUNC, since the server must perform
the truncation. CREATE retains its existing handle lifecycle. Preserve the
cached hint across STATX replies, which do not carry fuse_attr.flags.

Document negotiation, cache and handle semantics, and the server's
responsibilities. No additional access-time or open-reference accounting
is introduced.

Signed-off-by: Stanislav Kinsburskii <[email protected]>
---
 Documentation/filesystems/fuse/fuse-no-open.rst | 39 +++++++++++++++++++++++++
 Documentation/filesystems/fuse/index.rst        |  1 +
 fs/fuse/file.c                                  | 26 +++++++++++++----
 fs/fuse/fuse_i.h                                | 17 ++++++++++-
 fs/fuse/inode.c                                 |  9 ++++++
 fs/fuse/ioctl.c                                 |  3 +-
 include/uapi/linux/fuse.h                       | 11 ++++++-
 7 files changed, 98 insertions(+), 8 deletions(-)

diff --git a/Documentation/filesystems/fuse/fuse-no-open.rst 
b/Documentation/filesystems/fuse/fuse-no-open.rst
new file mode 100644
index 000000000000..314b5adb289b
--- /dev/null
+++ b/Documentation/filesystems/fuse/fuse-no-open.rst
@@ -0,0 +1,39 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+Per-inode open suppression
+=========================
+
+A filesystem can avoid OPEN and RELEASE requests for individual inodes by
+negotiating FUSE_PER_INODE_NO_OPEN in INIT and setting FUSE_ATTR_NO_OPEN in
+``fuse_attr.flags``.  For directories, the flag suppresses OPENDIR and
+RELEASEDIR instead.  This allows, for example, cached content files to avoid
+open round trips while control files on the same connection retain their
+open handlers.  Without the negotiated capability the attribute is ignored.
+
+The kernel updates the hint when it accepts attributes in replies such as
+LOOKUP, GETATTR, SETATTR and READDIRPLUS.  STATX replies do not carry
+``fuse_attr.flags`` and leave the hint unchanged.  The hint is cached inode
+state; it is not independently revalidated on every open.  A server changing
+the hint must arrange for fresh attributes to reach the kernel and tolerate
+concurrent opens using the previous value.
+
+For an open served locally, the file handle is zero, FOPEN_KEEP_CACHE is
+set, and directories also have FOPEN_CACHE_DIR set.  Subsequent requests
+identify the object by the node ID and may carry a zero file handle.  The
+server must support these requests without per-open state.  The decision to
+omit RELEASE is recorded for each open and is not changed by later attribute
+updates.  An existing server-opened handle still receives its matching
+RELEASE if the inode hint subsequently becomes set.
+
+When FUSE_ATOMIC_O_TRUNC is negotiated, an open with O_TRUNC still sends
+OPEN and receives a matching RELEASE, so the server can perform truncation.
+CREATE also retains its usual open and release semantics.  Connection-wide
+no-open behavior selected by an ENOSYS response continues to take precedence.
+
+The hint does not make an inode immutable, grant permissions, or suppress
+other operations such as FLUSH, FSYNC, locking or data I/O.  A server must
+only set it when its access policy and file semantics permit the default
+open behavior described above.  Servers needing per-open authorization,
+nonzero handles, direct I/O, passthrough or other OPEN reply flags must keep
+handling OPEN for those inodes.  No additional access-time or open-reference
+accounting is performed by this feature.
diff --git a/Documentation/filesystems/fuse/index.rst 
b/Documentation/filesystems/fuse/index.rst
index 3dada6c4057a..6dd9192f74fe 100644
--- a/Documentation/filesystems/fuse/index.rst
+++ b/Documentation/filesystems/fuse/index.rst
@@ -12,4 +12,5 @@ FUSE (Filesystem in Userspace) Technical Documentation
    fuse-io
    fuse-io-uring
    fuse-passthrough
+   fuse-no-open
    uapi/fuse-uapi-io-uring
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 3d209e2b71ba..6d57228acd1b 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -108,8 +108,9 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
                        fuse_file_io_release(ff, ra->inode);
 
                if (!args) {
-                       /* Do nothing when server does not implement 'opendir' 
*/
-               } else if (args->opcode == FUSE_RELEASE && ff->fm->fc->no_open) 
{
+                       /* No release needed when OPENDIR was skipped. */
+               } else if (args->opcode == FUSE_RELEASE &&
+                          (ff->fm->fc->no_open || ff->no_open)) {
                        fuse_release_end(args, 0);
                } else if (sync) {
                        fuse_simple_request(ff->fm, args);
@@ -130,13 +131,26 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
        }
 }
 
+static bool fuse_open_needed(struct fuse_conn *fc, unsigned int open_flags,
+                            bool isdir, bool no_open)
+{
+       if (isdir ? fc->no_opendir : fc->no_open)
+               return false;
+
+       /* Atomic truncation must still be performed by the server's OPEN. */
+       if ((open_flags & O_TRUNC) && fc->atomic_o_trunc)
+               return true;
+
+       return !no_open;
+}
+
 struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
-                                unsigned int open_flags, bool isdir)
+                                unsigned int open_flags, bool isdir, bool 
no_open)
 {
        struct fuse_conn *fc = fm->fc;
        struct fuse_file *ff;
        int opcode = isdir ? FUSE_OPENDIR : FUSE_OPEN;
-       bool open = isdir ? !fc->no_opendir : !fc->no_open;
+       bool open = fuse_open_needed(fc, open_flags, isdir, no_open);
        bool release = !isdir || open;
 
        /*
@@ -152,6 +166,7 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 
nodeid,
                return ERR_PTR(-ENOMEM);
 
        ff->fh = 0;
+       ff->no_open = !open;
        /* Default for no-open */
        ff->open_flags = FOPEN_KEEP_CACHE | (isdir ? FOPEN_CACHE_DIR : 0);
        if (open) {
@@ -189,7 +204,8 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 
nodeid,
 int fuse_do_open(struct fuse_mount *fm, u64 nodeid, struct file *file,
                 bool isdir)
 {
-       struct fuse_file *ff = fuse_file_open(fm, nodeid, file->f_flags, isdir);
+       struct fuse_file *ff = fuse_file_open(fm, nodeid, file->f_flags, isdir,
+                                           fuse_inode_no_open(fm->fc, 
file_inode(file)));
 
        if (!IS_ERR(ff))
                file->private_data = ff;
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 87e2bd9d4bb1..76157a84db22 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -257,6 +257,8 @@ enum {
         * or the fuse server has an exclusive "lease" on distributed fs
         */
        FUSE_I_EXCLUSIVE,
+       /* Server does not need OPEN/OPENDIR for this inode */
+       FUSE_I_NO_OPEN,
 };
 
 struct fuse_conn;
@@ -322,6 +324,9 @@ struct fuse_file {
 
        /** @flock: Has flock been performed on this file? */
        bool flock:1;
+
+       /** @no_open: Open was served locally without an OPEN/OPENDIR request */
+       bool no_open:1;
 };
 
 struct fuse_release_args {
@@ -556,6 +561,9 @@ struct fuse_conn {
        /** @no_opendir: Is opendir/releasedir not implemented by fs? */
        unsigned no_opendir:1;
 
+       /** @per_inode_no_open: Honor FUSE_ATTR_NO_OPEN */
+       unsigned per_inode_no_open:1;
+
        /** @no_fsync: Is fsync not implemented by fs? */
        unsigned no_fsync:1;
 
@@ -833,6 +841,13 @@ static inline struct fuse_inode *get_fuse_inode(const 
struct inode *inode)
        return container_of(inode, struct fuse_inode, inode);
 }
 
+static inline bool fuse_inode_no_open(struct fuse_conn *fc, struct inode 
*inode)
+{
+       /* CUSE uses a non-FUSE inode and cannot negotiate this capability. */
+       return fc->per_inode_no_open &&
+               test_bit(FUSE_I_NO_OPEN, &get_fuse_inode(inode)->state);
+}
+
 static inline u64 get_node_id(struct inode *inode)
 {
        return get_fuse_inode(inode)->nodeid;
@@ -1261,7 +1276,7 @@ void fuse_file_io_release(struct fuse_file *ff, struct 
inode *inode);
 
 /* file.c */
 struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
-                                unsigned int open_flags, bool isdir);
+                                unsigned int open_flags, bool isdir, bool 
no_open);
 void fuse_file_release(struct inode *inode, struct fuse_file *ff,
                       unsigned int open_flags, fl_owner_t id, bool isdir);
 
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index cbb10e19e7e8..63924d95caa3 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -299,6 +299,11 @@ void fuse_change_attributes_common(struct inode *inode, 
struct fuse_attr *attr,
         * anyway. Its less efficient but should be safe.
         */
        inode->i_flags &= ~S_NOSEC;
+
+       /* STATX replies do not carry fuse_attr.flags. */
+       if (fc->per_inode_no_open && !sx)
+               assign_bit(FUSE_I_NO_OPEN, &fi->state,
+                          attr->flags & FUSE_ATTR_NO_OPEN);
 }
 
 u32 fuse_get_cache_mask(struct inode *inode)
@@ -1432,6 +1437,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_PER_INODE_NO_OPEN)
+                               fc->per_inode_no_open = 1;
                } else {
                        ra_pages = fc->max_read / PAGE_SIZE;
                        fc->no_lock = 1;
@@ -1510,6 +1517,8 @@ static struct fuse_init_args *fuse_new_init(struct 
fuse_mount *fm)
        if (fuse_uring_enabled())
                flags |= FUSE_OVER_IO_URING | FUSE_HAS_IO_URING_BUFPOOL;
 
+       flags |= FUSE_PER_INODE_NO_OPEN;
+
        ia->in.flags = flags;
        ia->in.flags2 = flags >> 32;
 
diff --git a/fs/fuse/ioctl.c b/fs/fuse/ioctl.c
index ce1807704da6..7fc11bb3eee9 100644
--- a/fs/fuse/ioctl.c
+++ b/fs/fuse/ioctl.c
@@ -492,7 +492,8 @@ static struct fuse_file *fuse_priv_ioctl_prepare(struct 
inode *inode)
        if (!S_ISREG(inode->i_mode) && !isdir)
                return ERR_PTR(-ENOTTY);
 
-       return fuse_file_open(fm, get_node_id(inode), O_RDONLY, isdir);
+       return fuse_file_open(fm, get_node_id(inode), O_RDONLY, isdir,
+                             fuse_inode_no_open(fm->fc, inode));
 }
 
 static void fuse_priv_ioctl_cleanup(struct inode *inode, struct fuse_file *ff)
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 10a7f31c4bdf..784a641596be 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_PER_INODE_NO_OPEN and FUSE_ATTR_NO_OPEN
  */
 
 #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,7 @@ 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_PER_INODE_NO_OPEN: honor FUSE_ATTR_NO_OPEN in inode attributes
  */
 #define FUSE_ASYNC_READ                (1 << 0)
 #define FUSE_POSIX_LOCKS       (1 << 1)
@@ -522,6 +526,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_PER_INODE_NO_OPEN (1ULL << 45)
 
 /**
  * CUSE INIT request/reply flags
@@ -605,9 +610,13 @@ struct fuse_file_lock {
  *
  * FUSE_ATTR_SUBMOUNT: Object is a submount root
  * FUSE_ATTR_DAX: Enable DAX for this file in per inode DAX mode
+ * FUSE_ATTR_NO_OPEN: Skip OPEN/OPENDIR and matching RELEASE/RELEASEDIR;
+ *                   requires FUSE_PER_INODE_NO_OPEN. Atomic O_TRUNC opens
+ *                   still go to the server.
  */
 #define FUSE_ATTR_SUBMOUNT      (1 << 0)
 #define FUSE_ATTR_DAX          (1 << 1)
+#define FUSE_ATTR_NO_OPEN      (1 << 2)
 
 /**
  * Open flags

-- 
2.43.0


Reply via email to