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 normally omit OPEN/OPENDIR and the corresponding RELEASE/RELEASEDIR. Remember whether each handle was opened locally, so later attribute updates do not determine the release behavior of existing handles. Retain RELEASE after a successful remote flock operation, even when OPEN was skipped, so FUSE_RELEASE_FLOCK_UNLOCK can clean up server-side locks. Servers negotiating remote flock must accept this RELEASE with a zero file handle and no preceding OPEN, including after an explicit unlock. 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. The per-inode hint does not suppress OPEN for atomic O_TRUNC, since the server must perform the truncation. CREATE retains its existing handle lifecycle. Connection-wide no-open/no-opendir behavior selected by ENOSYS continues to take precedence. Update the cached hint from LOOKUP, GETATTR, SETATTR and READDIRPLUS attributes. Preserve it 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 | 49 +++++++++++++++++++++++++ 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 | 13 ++++++- 7 files changed, 110 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..4526512d358c --- /dev/null +++ b/Documentation/filesystems/fuse/fuse-no-open.rst @@ -0,0 +1,49 @@ +.. 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. Whether OPEN was +sent is recorded for each handle 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. + +Remote flock locking is an exception to RELEASE suppression. When +FUSE_FLOCK_LOCKS is negotiated and a handle has successfully performed a +server-side flock operation, its final close sends RELEASE with +FUSE_RELEASE_FLOCK_UNLOCK and the lock owner, even if OPEN was suppressed. +The server must accept this RELEASE with a zero file handle and no preceding +OPEN, and use the node ID and lock owner to remove any remaining flock locks. +This also applies after an explicit unlock, since the kernel records whether +a flock operation succeeded rather than tracking the server's current locks. + +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. Servers supporting +remote flock must implement the RELEASE cleanup described above. 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..31c22ff5c8c1 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 && !ff->flock))) { 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..1b5699407e11 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,15 @@ 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. RELEASE is also sent after a + * successful remote flock operation to clean up locks, + * even when OPEN was skipped and the file handle is zero. */ #define FUSE_ATTR_SUBMOUNT (1 << 0) #define FUSE_ATTR_DAX (1 << 1) +#define FUSE_ATTR_NO_OPEN (1 << 2) /** * Open flags -- 2.43.0

