* [PATCH 0/2] fuse: support per-inode open and release suppression
@ 2026-10-07 23:14 Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 1/2] fuse: add negotiated " Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 2/2] selftests: fuse: test per-inode open suppression Stanislav Kinsburskii
0 siblings, 2 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-10-07 23:14 UTC (permalink / raw)
To: Miklos Szeredi, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Shuah Khan
Cc: Robert Byrnes, fuse-devel, linux-doc, linux-kernel,
linux-kselftest, Stanislav Kinsburskii
For FUSE filesystems serving immutable data, warm-cache workloads can
still spend much of their time sending OPEN/OPENDIR and RELEASE/RELEASEDIR
requests. Data, attributes, lookups and directory entries may already be
cached, but opening each object still requires a userspace round trip.
The existing connection-wide no-open support cannot distinguish these
objects from control files that need the server's open and release
callbacks.
This series adds negotiated per-inode suppression. A server negotiates
FUSE_PER_INODE_NO_OPEN during INIT and marks eligible inodes with
FUSE_ATTR_NO_OPEN in their attribute replies. The kernel then handles
opens locally for those inodes, using a zero file handle and the existing
no-open cache defaults. Subsequent requests identify the object by nodeid.
Release suppression follows how each handle was opened, so changing the
inode's hint does not create an unmatched release or lose the release of
a server-opened handle. Atomic O_TRUNC still sends OPEN to the server,
and CREATE retains its existing open and release lifecycle.
The second patch adds a raw /dev/fuse selftest without requiring libfuse
support for the new protocol flags. It covers capability negotiation,
files and directories, LOOKUP and READDIRPLUS attributes, hint changes
through GETATTR, hint preservation across STATX, atomic truncation and
CREATE. The cases also run with writeback caching enabled.
Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
---
Stanislav Kinsburskii (2):
fuse: add negotiated per-inode open and release suppression
selftests: fuse: test per-inode open suppression
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 +-
.../testing/selftests/filesystems/fuse/.gitignore | 1 +
tools/testing/selftests/filesystems/fuse/Makefile | 2 +
.../selftests/filesystems/fuse/no_open_test.c | 511 +++++++++++++++++++++
10 files changed, 612 insertions(+), 8 deletions(-)
---
base-commit: b018686719706a781c45a874ed51374a2dc4b767
change-id: 20261007-fuse-per-inode-no-open-d40e2b4abb8a
Best regards,
--
Stanislav Kinsburskii <skinsburskii@gmail.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/2] fuse: add negotiated per-inode open and release suppression
2026-10-07 23:14 [PATCH 0/2] fuse: support per-inode open and release suppression Stanislav Kinsburskii
@ 2026-10-07 23:14 ` Stanislav Kinsburskii
2026-10-08 18:22 ` Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 2/2] selftests: fuse: test per-inode open suppression Stanislav Kinsburskii
1 sibling, 1 reply; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-10-07 23:14 UTC (permalink / raw)
To: Miklos Szeredi, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Shuah Khan
Cc: Robert Byrnes, fuse-devel, linux-doc, linux-kernel,
linux-kselftest, Stanislav Kinsburskii
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 <skinsburskii@gmail.com>
---
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 2/2] selftests: fuse: test per-inode open suppression
2026-10-07 23:14 [PATCH 0/2] fuse: support per-inode open and release suppression Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 1/2] fuse: add negotiated " Stanislav Kinsburskii
@ 2026-10-07 23:14 ` Stanislav Kinsburskii
1 sibling, 0 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-10-07 23:14 UTC (permalink / raw)
To: Miklos Szeredi, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Shuah Khan
Cc: Robert Byrnes, fuse-devel, linux-doc, linux-kernel,
linux-kselftest, Stanislav Kinsburskii
Add a raw /dev/fuse server and tests for FUSE_PER_INODE_NO_OPEN, without
requiring libfuse to support the new capability or attribute flag.
Check files and directories marked by LOOKUP and READDIRPLUS, unmarked
control files, zero file handles on reads, and hint changes through
GETATTR while handles remain open. Verify that STATX preserves the hint,
atomic O_TRUNC reaches the server, and CREATE retains its release.
Return an unknown OPEN reply flag to check that wire flags cannot
suppress release of server-opened handles.
Run each case with the capability disabled, enabled, enabled with atomic
truncation, and enabled with atomic truncation and writeback caching.
Unmount and join the server before checking request counts so asynchronous
RELEASE requests are accounted for. Skip when mounting is unavailable or
the kernel does not advertise the requested capability.
The test needs permission to create a mount namespace and mount FUSE.
Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
---
.../testing/selftests/filesystems/fuse/.gitignore | 1 +
tools/testing/selftests/filesystems/fuse/Makefile | 2 +
.../selftests/filesystems/fuse/no_open_test.c | 511 +++++++++++++++++++++
3 files changed, 514 insertions(+)
diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index b5b03db1118c..a4cb079c4bc6 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -3,3 +3,4 @@ fuse_mnt
fusectl_test
test_syncfs
write_extend_eof_test
+no_open_test
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index c2de8d225447..b9682e7a2aef 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -4,6 +4,7 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
TEST_GEN_PROGS := fusectl_test test_syncfs
TEST_GEN_PROGS += write_extend_eof_test
+TEST_GEN_PROGS += no_open_test
TEST_GEN_FILES := fuse_mnt
# fuse_acl_cache_test requires libfuse3; add it only when the library is present.
@@ -16,6 +17,7 @@ endif
include ../../lib.mk
$(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread
+$(OUTPUT)/no_open_test: LDLIBS += -lpthread
VAR_CFLAGS := $(shell pkg-config fuse --cflags 2>/dev/null)
ifeq ($(VAR_CFLAGS),)
diff --git a/tools/testing/selftests/filesystems/fuse/no_open_test.c b/tools/testing/selftests/filesystems/fuse/no_open_test.c
new file mode 100644
index 000000000000..fd36dc96bb98
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/no_open_test.c
@@ -0,0 +1,511 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Exercise the wire protocol without requiring new libfuse interfaces. */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/fuse.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+enum { FILE_ID = 2, DIR_ID, CONTROL_ID, CREATE_ID, NR_INODES };
+
+struct server {
+ int fd;
+ bool negotiate;
+ bool atomic_trunc;
+ bool writeback;
+ atomic_bool supported;
+ atomic_uint attr_flags;
+ unsigned int opens[NR_INODES];
+ unsigned int releases[NR_INODES];
+ unsigned int zero_reads;
+ unsigned int zero_readdirs;
+ unsigned int statx_count;
+ unsigned int lookup_count;
+ unsigned int plus_count;
+ unsigned int truncates;
+ unsigned int size;
+ bool created;
+ int error;
+};
+
+static void reply(struct server *s, uint64_t unique, int error,
+ void *data, size_t len)
+{
+ struct fuse_out_header out = {
+ .len = sizeof(out) + len,
+ .error = error,
+ .unique = unique,
+ };
+ struct iovec iov[] = { { &out, sizeof(out) }, { data, len } };
+
+ if (writev(s->fd, iov, len ? 2 : 1) != out.len)
+ s->error = errno ?: EIO;
+}
+
+static void fill_attr(struct server *s, struct fuse_attr *attr, uint64_t id)
+{
+ bool isdir = id == FUSE_ROOT_ID || id == DIR_ID;
+
+ attr->ino = id;
+ attr->mode = (isdir ? S_IFDIR : S_IFREG) | 0755;
+ attr->nlink = isdir ? 2 : 1;
+ attr->size = isdir ? 0 : s->size;
+ attr->blksize = 4096;
+ if (id != FUSE_ROOT_ID && id != CONTROL_ID)
+ attr->flags = atomic_load(&s->attr_flags);
+}
+
+static void fill_entry(struct server *s, struct fuse_entry_out *out, uint64_t id)
+{
+ out->nodeid = id;
+ out->generation = 1;
+ out->entry_valid = 3600;
+ out->attr_valid = 3600;
+ fill_attr(s, &out->attr, id);
+}
+
+static void *serve(void *arg)
+{
+ struct server *s = arg;
+ union {
+ struct fuse_in_header header;
+ char bytes[16384];
+ } buf;
+
+ for (;;) {
+ struct fuse_in_header *in = &buf.header;
+ void *payload = in + 1;
+ ssize_t len = read(s->fd, &buf, sizeof(buf));
+ uint64_t id;
+
+ if (len < 0 && errno == EINTR)
+ continue;
+ if (len < 0 && errno == ENODEV)
+ return NULL;
+ if (len < (ssize_t)sizeof(*in)) {
+ s->error = EIO;
+ return NULL;
+ }
+ id = in->nodeid;
+ if (id >= NR_INODES) {
+ s->error = EINVAL;
+ reply(s, in->unique, -EINVAL, NULL, 0);
+ continue;
+ }
+ switch (in->opcode) {
+ case FUSE_INIT: {
+ struct fuse_init_in *init = payload;
+ struct fuse_init_out out = {
+ .major = FUSE_KERNEL_VERSION,
+ .minor = FUSE_KERNEL_MINOR_VERSION,
+ .max_write = 4096,
+ .flags = FUSE_INIT_EXT | FUSE_DO_READDIRPLUS,
+ };
+ uint64_t flags = init->flags;
+
+ if (init->flags & FUSE_INIT_EXT)
+ flags |= (uint64_t)init->flags2 << 32;
+ atomic_store(&s->supported, flags & FUSE_PER_INODE_NO_OPEN);
+ if (out.minor > init->minor)
+ out.minor = init->minor;
+ if (s->negotiate)
+ out.flags2 = (flags & FUSE_PER_INODE_NO_OPEN) >> 32;
+ if (s->atomic_trunc)
+ out.flags |= FUSE_ATOMIC_O_TRUNC;
+ if (s->writeback)
+ out.flags |= FUSE_WRITEBACK_CACHE;
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_LOOKUP: {
+ struct fuse_entry_out out = {};
+ const char *name = payload;
+
+ s->lookup_count++;
+ if (!strcmp(name, "file")) {
+ id = FILE_ID;
+ } else if (!strcmp(name, "dir")) {
+ id = DIR_ID;
+ } else if (!strcmp(name, "control")) {
+ id = CONTROL_ID;
+ } else if (!strcmp(name, "created") && s->created) {
+ id = CREATE_ID;
+ } else {
+ reply(s, in->unique, -ENOENT, NULL, 0);
+ break;
+ }
+ fill_entry(s, &out, id);
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_GETATTR:
+ case FUSE_SETATTR: {
+ struct fuse_attr_out out = { .attr_valid = 3600 };
+
+ if (in->opcode == FUSE_SETATTR) {
+ struct fuse_setattr_in *attr = payload;
+
+ if (attr->valid & FATTR_SIZE) {
+ s->size = attr->size;
+ s->truncates++;
+ }
+ }
+ fill_attr(s, &out.attr, id);
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_STATX: {
+ struct fuse_statx_out out = { .attr_valid = 3600 };
+ struct fuse_attr attr = {};
+
+ s->statx_count++;
+ fill_attr(s, &attr, id);
+ out.stat.mask = STATX_BASIC_STATS | STATX_BTIME;
+ out.stat.ino = attr.ino;
+ out.stat.mode = attr.mode;
+ out.stat.nlink = attr.nlink;
+ out.stat.size = attr.size;
+ out.stat.blksize = attr.blksize;
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_OPEN:
+ case FUSE_OPENDIR: {
+ struct fuse_open_in *open = payload;
+ struct fuse_open_out out = {
+ .fh = 100 + id,
+ .open_flags = FOPEN_KEEP_CACHE,
+ };
+
+ s->opens[id]++;
+ /* Unknown wire bits must not act as private release flags. */
+ out.open_flags |= 1U << 30;
+ if (open->flags & O_TRUNC) {
+ s->size = 0;
+ s->truncates++;
+ }
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_CREATE: {
+ struct {
+ struct fuse_entry_out entry;
+ struct fuse_open_out open;
+ } out = {};
+
+ s->created = true;
+ fill_entry(s, &out.entry, CREATE_ID);
+ out.open.fh = 100 + CREATE_ID;
+ out.open.open_flags = FOPEN_KEEP_CACHE;
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_READ: {
+ struct fuse_read_in *read = payload;
+ char data = 'x';
+
+ if (!read->fh)
+ s->zero_reads++;
+ reply(s, in->unique, 0, &data,
+ read->offset < s->size && read->size ? 1 : 0);
+ break;
+ }
+ case FUSE_READDIR:
+ case FUSE_READDIRPLUS: {
+ struct fuse_read_in *read = payload;
+ union {
+ struct fuse_direntplus entry;
+ char data[FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET_DIRENTPLUS + 4)];
+ } out = {};
+ struct fuse_direntplus *entry = &out.entry;
+
+ if (!read->fh)
+ s->zero_readdirs++;
+ if (id != FUSE_ROOT_ID || read->offset ||
+ in->opcode != FUSE_READDIRPLUS) {
+ reply(s, in->unique, 0, NULL, 0);
+ break;
+ }
+ s->plus_count++;
+ fill_entry(s, &entry->entry_out, FILE_ID);
+ entry->dirent.ino = FILE_ID;
+ entry->dirent.off = 1;
+ entry->dirent.namelen = 4;
+ entry->dirent.type = DT_REG;
+ memcpy(entry->dirent.name, "file", 4);
+ reply(s, in->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_RELEASE:
+ case FUSE_RELEASEDIR: {
+ struct fuse_release_in *release = payload;
+
+ s->releases[id]++;
+ if (release->fh != 100 + id)
+ s->error = EINVAL;
+ reply(s, in->unique, 0, NULL, 0);
+ break;
+ }
+ case FUSE_FLUSH:
+ case FUSE_FSYNC:
+ case FUSE_ACCESS:
+ case FUSE_DESTROY:
+ reply(s, in->unique, 0, NULL, 0);
+ break;
+ case FUSE_FORGET:
+ case FUSE_BATCH_FORGET:
+ break;
+ default:
+ reply(s, in->unique, -ENOSYS, NULL, 0);
+ }
+ }
+}
+
+FIXTURE(no_open) {
+ struct server server;
+ pthread_t thread;
+ bool running;
+ bool mounted;
+ char dir[64];
+ int root;
+};
+
+FIXTURE_VARIANT(no_open) {
+ bool negotiate;
+ bool atomic_trunc;
+ bool writeback;
+};
+
+FIXTURE_VARIANT_ADD(no_open, unnegotiated) {};
+
+FIXTURE_VARIANT_ADD(no_open, negotiated) { .negotiate = true };
+
+FIXTURE_VARIANT_ADD(no_open, atomic_trunc) {
+ .negotiate = true, .atomic_trunc = true,
+};
+
+FIXTURE_VARIANT_ADD(no_open, writeback) {
+ .negotiate = true, .atomic_trunc = true, .writeback = true,
+};
+
+static void cleanup(FIXTURE_DATA(no_open) * self)
+{
+ if (self->root >= 0)
+ close(self->root);
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ if (self->running) {
+ pthread_cancel(self->thread);
+ pthread_join(self->thread, NULL);
+ }
+ if (self->server.fd >= 0)
+ close(self->server.fd);
+ if (self->dir[0])
+ rmdir(self->dir);
+}
+
+FIXTURE_SETUP(no_open)
+{
+ struct server *s = &self->server;
+ struct stat st;
+ char opts[128];
+ int err;
+
+ self->root = -1;
+ s->fd = -1;
+ if (unshare(CLONE_NEWNS))
+ SKIP(return, "need a mount namespace: %s", strerror(errno));
+ ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+ s->fd = open("/dev/fuse", O_RDWR | O_CLOEXEC);
+ if (s->fd < 0)
+ SKIP(return, "cannot open /dev/fuse: %s", strerror(errno));
+ strcpy(self->dir, "/tmp/fuse_no_open_XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ snprintf(opts, sizeof(opts),
+ "fd=%d,rootmode=40000,user_id=%u,group_id=%u,default_permissions",
+ s->fd, getuid(), getgid());
+ if (mount("fuse", self->dir, "fuse", MS_NOSUID | MS_NODEV, opts)) {
+ err = errno;
+ cleanup(self);
+ SKIP(return, "cannot mount FUSE: %s", strerror(err));
+ }
+ self->mounted = true;
+ s->negotiate = variant->negotiate;
+ s->atomic_trunc = variant->atomic_trunc;
+ s->writeback = variant->writeback;
+ s->size = 1;
+ atomic_init(&s->attr_flags, FUSE_ATTR_NO_OPEN);
+ atomic_init(&s->supported, false);
+ err = pthread_create(&self->thread, NULL, serve, s);
+ if (err) {
+ cleanup(self);
+ ASSERT_EQ(err, 0);
+ }
+ self->running = true;
+ ASSERT_EQ(stat(self->dir, &st), 0);
+ if (variant->negotiate && !atomic_load(&s->supported)) {
+ cleanup(self);
+ SKIP(return, "kernel lacks FUSE_PER_INODE_NO_OPEN");
+ }
+ self->root = open(self->dir, O_PATH | O_DIRECTORY);
+ ASSERT_GE(self->root, 0);
+}
+
+FIXTURE_TEARDOWN(no_open)
+{
+ cleanup(self);
+}
+
+/* Drain asynchronous releases before checking both presence and absence. */
+static void finish(struct __test_metadata *_metadata, FIXTURE_DATA(no_open) * self)
+{
+ ASSERT_EQ(close(self->root), 0);
+ self->root = -1;
+ ASSERT_EQ(umount(self->dir), 0);
+ self->mounted = false;
+ ASSERT_EQ(pthread_join(self->thread, NULL), 0);
+ self->running = false;
+ ASSERT_EQ(self->server.error, 0);
+}
+
+static void refresh_attr(struct __test_metadata *_metadata,
+ FIXTURE_DATA(no_open) * self, unsigned int mask)
+{
+ struct statx st;
+
+ ASSERT_EQ(statx(self->root, "file", AT_STATX_FORCE_SYNC, mask, &st), 0);
+}
+
+TEST_F(no_open, files_and_directories)
+{
+ int fd = openat(self->root, "file", O_RDONLY);
+ DIR *dir;
+ char data;
+
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(read(fd, &data, 1), 1);
+ EXPECT_EQ(data, 'x');
+ ASSERT_EQ(close(fd), 0);
+ fd = openat(self->root, "dir", O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(fd, 0);
+ dir = fdopendir(fd);
+ ASSERT_NE(dir, NULL);
+ errno = 0;
+ ASSERT_EQ(readdir(dir), NULL);
+ ASSERT_EQ(errno, 0);
+ ASSERT_EQ(closedir(dir), 0);
+ fd = openat(self->root, "control", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ finish(_metadata, self);
+ EXPECT_EQ(self->server.opens[FILE_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.releases[FILE_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.opens[DIR_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.releases[DIR_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.opens[CONTROL_ID], 1);
+ EXPECT_EQ(self->server.releases[CONTROL_ID], 1);
+ EXPECT_EQ(self->server.zero_reads, variant->negotiate);
+ EXPECT_EQ(self->server.zero_readdirs, variant->negotiate);
+}
+
+TEST_F(no_open, hint_changes_with_open_handles)
+{
+ int local, remote, local_again;
+
+ local = openat(self->root, "file", O_RDONLY);
+ ASSERT_GE(local, 0);
+ atomic_store(&self->server.attr_flags, 0);
+ refresh_attr(_metadata, self, STATX_BASIC_STATS);
+ remote = openat(self->root, "file", O_RDONLY);
+ ASSERT_GE(remote, 0);
+ /* Clearing the hint must not cause a RELEASE for the first handle. */
+ ASSERT_EQ(close(local), 0);
+ atomic_store(&self->server.attr_flags, FUSE_ATTR_NO_OPEN);
+ refresh_attr(_metadata, self, STATX_BASIC_STATS);
+ local_again = openat(self->root, "file", O_RDONLY);
+ ASSERT_GE(local_again, 0);
+ /* Setting the hint must not lose the server's existing handle. */
+ ASSERT_EQ(close(remote), 0);
+ ASSERT_EQ(close(local_again), 0);
+ finish(_metadata, self);
+ EXPECT_EQ(self->server.opens[FILE_ID], variant->negotiate ? 1 : 3);
+ EXPECT_EQ(self->server.releases[FILE_ID], variant->negotiate ? 1 : 3);
+}
+
+TEST_F(no_open, statx_preserves_hint)
+{
+ int fd;
+
+ refresh_attr(_metadata, self, STATX_BASIC_STATS | STATX_BTIME);
+ fd = openat(self->root, "file", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ finish(_metadata, self);
+ EXPECT_EQ(self->server.statx_count, 1);
+ EXPECT_EQ(self->server.opens[FILE_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.releases[FILE_ID], !variant->negotiate);
+}
+
+TEST_F(no_open, truncate)
+{
+ int fd = openat(self->root, "file", O_WRONLY | O_TRUNC);
+ bool server_open = !variant->negotiate || variant->atomic_trunc;
+ struct stat st;
+
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_size, 0);
+ ASSERT_EQ(close(fd), 0);
+ finish(_metadata, self);
+ EXPECT_EQ(self->server.size, 0);
+ EXPECT_EQ(self->server.truncates, 1);
+ EXPECT_EQ(self->server.opens[FILE_ID], server_open);
+ EXPECT_EQ(self->server.releases[FILE_ID], server_open);
+}
+
+TEST_F(no_open, create_releases_handle)
+{
+ int fd = openat(self->root, "created", O_CREAT | O_EXCL | O_RDWR, 0600);
+
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ finish(_metadata, self);
+ EXPECT_TRUE(self->server.created);
+ EXPECT_EQ(self->server.opens[CREATE_ID], 0);
+ EXPECT_EQ(self->server.releases[CREATE_ID], 1);
+}
+
+TEST_F(no_open, readdirplus_populates_hint)
+{
+ int fd = openat(self->root, ".", O_RDONLY | O_DIRECTORY);
+ struct dirent *entry;
+ DIR *dir;
+
+ ASSERT_GE(fd, 0);
+ dir = fdopendir(fd);
+ ASSERT_NE(dir, NULL);
+ entry = readdir(dir);
+ ASSERT_NE(entry, NULL);
+ ASSERT_STREQ(entry->d_name, "file");
+ ASSERT_EQ(closedir(dir), 0);
+ fd = openat(self->root, "file", O_RDONLY);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(close(fd), 0);
+ finish(_metadata, self);
+ EXPECT_EQ(self->server.plus_count, 1);
+ EXPECT_EQ(self->server.lookup_count, 0);
+ EXPECT_EQ(self->server.opens[FILE_ID], !variant->negotiate);
+ EXPECT_EQ(self->server.releases[FILE_ID], !variant->negotiate);
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH 1/2] fuse: add negotiated per-inode open and release suppression
2026-10-07 23:14 ` [PATCH 1/2] fuse: add negotiated " Stanislav Kinsburskii
@ 2026-10-08 18:22 ` Stanislav Kinsburskii
0 siblings, 0 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-10-08 18:22 UTC (permalink / raw)
To: Miklos Szeredi, Jonathan Corbet, Shuah Khan, Randy Dunlap,
Shuah Khan
Cc: Robert Byrnes, fuse-devel, linux-doc, linux-kernel,
linux-kselftest
On Wed, Oct 07, 2026 at 04:14:34PM -0700, Stanislav Kinsburskii wrote:
> 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.
>
Sashiko found a bug in this patch: https://sashiko.dev/#/patchset/20261007-fuse-per-inode-no-open-v1-0-be5229fe89f5%40gmail.com
I addressed it in v2.
Thanks,
Stanislav
> Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
> ---
> 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
>
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-08 18:22 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-07 23:14 [PATCH 0/2] fuse: support per-inode open and release suppression Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 1/2] fuse: add negotiated " Stanislav Kinsburskii
2026-10-08 18:22 ` Stanislav Kinsburskii
2026-10-07 23:14 ` [PATCH 2/2] selftests: fuse: test per-inode open suppression Stanislav Kinsburskii
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox