Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH 0/3] fuse: atomic open
@ 2026-10-20 14:16 jamz
  2026-10-20 14:16 ` [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC jamz
                   ` (3 more replies)
  0 siblings, 4 replies; 10+ messages in thread
From: jamz @ 2026-10-20 14:16 UTC (permalink / raw)
  To: Miklos Szeredi
  Cc: Jimmy Zuber, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques,
	NeilBrown

From: Jimmy Zuber <jamz@amazon.com>

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 reply says whether the file was created, as Miklos suggested in [1].
The overall pattern follows his sketch in [2]. For a cached
name, the kernel compares the returned nodeid with the cached one and
looks the name up again on a mismatch.

A server without FUSE_OPEN_ATOMIC replies -ENOSYS and keeps today's
behaviour.

Tested with a new selftest under UML and KVM.

[1] https://lore.kernel.org/all/CAJfpeguEK4JsFo60=yHkpLL6XkDGS7cQ5GOhiFL379KsYk0TsQ@mail.gmail.com/
[2] https://lore.kernel.org/all/CAJfpegvXtXY=Pbxv+dMGFR8mvWN0DUwhSo6NwaVexk6Y6sao+w@mail.gmail.com/


Jimmy Zuber (3):
  fuse: add FUSE_OPEN_ATOMIC
  fuse: open cached files with FUSE_OPEN_ATOMIC
  selftests/fuse: add test for FUSE_OPEN_ATOMIC

 fs/fuse/dir.c                                 | 359 +++++++++++--
 fs/fuse/file.c                                |   4 +-
 fs/fuse/fuse_i.h                              |   4 +
 include/uapi/linux/fuse.h                     |   8 +-
 .../selftests/filesystems/fuse/.gitignore     |   1 +
 .../selftests/filesystems/fuse/Makefile       |   3 +-
 .../filesystems/fuse/atomic_open_test.c       | 473 ++++++++++++++++++
 7 files changed, 797 insertions(+), 55 deletions(-)
 create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c


base-commit: b776cbe24cc1b79c7f5101762b0069b3f48b6a4b
-- 
2.50.1


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC
  2026-10-20 14:16 [PATCH 0/3] fuse: atomic open jamz
@ 2026-10-20 14:16 ` jamz
  2026-10-20 23:23   ` NeilBrown
  2026-10-20 14:16 ` [PATCH 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC jamz
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 10+ messages in thread
From: jamz @ 2026-10-20 14:16 UTC (permalink / raw)
  To: Miklos Szeredi
  Cc: Jimmy Zuber, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques,
	NeilBrown

From: Jimmy Zuber <jamz@amazon.com>

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 <jamz@amazon.com>
---
 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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC
  2026-10-20 14:16 [PATCH 0/3] fuse: atomic open jamz
  2026-10-20 14:16 ` [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC jamz
@ 2026-10-20 14:16 ` jamz
  2026-10-20 14:16 ` [PATCH 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC jamz
  2026-10-20 23:33 ` [PATCH 0/3] fuse: atomic open NeilBrown
  3 siblings, 0 replies; 10+ messages in thread
From: jamz @ 2026-10-20 14:16 UTC (permalink / raw)
  To: Miklos Szeredi
  Cc: Jimmy Zuber, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques,
	NeilBrown

From: Jimmy Zuber <jamz@amazon.com>

When the server implements FUSE_OPEN_ATOMIC, skip LOOKUP for a
regular file being opened, opening it by name from fuse_open() instead.
If the name now refers to a different file or to nothing, drop the
dentry and return -EOPENSTALE, so that the VFS looks the name up again.
A matching reply refreshes the attributes and the entry timeout.

If the server replies -ENOSYS, fuse_open() sends the skipped LOOKUP
itself.

Assisted-by: Claude:claude-opus-5-5 [Claude-Code]
Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
 fs/fuse/dir.c    | 247 ++++++++++++++++++++++++++++++++++++++---------
 fs/fuse/file.c   |   4 +-
 fs/fuse/fuse_i.h |   1 +
 3 files changed, 208 insertions(+), 44 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 34d2271eb248..ea265a8d179f 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -379,6 +379,60 @@ static void fuse_lookup_init(struct fuse_args *args, u64 nodeid,
 	args->out_args[0].value = outarg;
 }
 
+/*
+ * Look the name of @entry up again.  Returns 1 if it still refers to the
+ * inode of @entry, refreshing its attributes and entry timeout, 0 if it does
+ * not, or -ENOMEM / -EINTR.
+ */
+static int fuse_revalidate_lookup(struct inode *dir, const struct qstr *name,
+				  struct dentry *entry)
+{
+	struct inode *inode = d_inode(entry);
+	struct fuse_mount *fm = get_fuse_mount(inode);
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	struct fuse_entry_out outarg;
+	FUSE_ARGS(args);
+	struct fuse_forget_link *forget;
+	u64 attr_version;
+	int ret;
+
+	forget = fuse_alloc_forget();
+	if (!forget)
+		return -ENOMEM;
+
+	attr_version = fuse_get_attr_version(fm->fc);
+
+	fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
+	ret = fuse_simple_request(fm, &args);
+	/* Zero nodeid is same as -ENOENT */
+	if (!ret && !outarg.nodeid)
+		ret = -ENOENT;
+	if (!ret) {
+		if (outarg.nodeid != get_node_id(inode) ||
+		    (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
+			fuse_chan_queue_forget(fm->fc->chan, forget,
+					       outarg.nodeid, 1);
+			return 0;
+		}
+		spin_lock(&fi->lock);
+		fi->nlookup++;
+		spin_unlock(&fi->lock);
+	}
+	kfree(forget);
+	if (ret == -ENOMEM || ret == -EINTR)
+		return ret;
+	if (ret || fuse_invalid_attr(&outarg.attr) ||
+	    fuse_stale_inode(inode, outarg.generation, &outarg.attr))
+		return 0;
+
+	forget_all_cached_acls(inode);
+	fuse_change_attributes(inode, &outarg.attr, NULL,
+			       ATTR_TIMEOUT(&outarg),
+			       attr_version);
+	fuse_change_entry_timeout(entry, &outarg);
+	return 1;
+}
+
 /*
  * Check whether the dentry is still valid
  *
@@ -392,7 +446,6 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 				  struct dentry *entry, unsigned int flags)
 {
 	struct inode *inode;
-	struct fuse_mount *fm;
 	struct fuse_conn *fc;
 	struct fuse_inode *fi;
 	struct fuse_dentry *fd = entry->d_fsdata;
@@ -407,57 +460,24 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 		goto invalid;
 	else if (time_before64(fuse_dentry_time(entry), get_jiffies_64()) ||
 		 (flags & (LOOKUP_EXCL | LOOKUP_REVAL | LOOKUP_RENAME_TARGET))) {
-		struct fuse_entry_out outarg;
-		FUSE_ARGS(args);
-		struct fuse_forget_link *forget;
-		u64 attr_version;
-
 		/* For negative dentries, always do a fresh lookup */
 		if (!inode)
 			goto invalid;
 
+		/* fuse_open_revalidate() checks the name of a file it opens */
+		if (!fc->no_open_atomic && (flags & LOOKUP_OPEN) &&
+		    !(flags & (LOOKUP_EXCL | LOOKUP_REVAL |
+			       LOOKUP_RENAME_TARGET)) &&
+		    S_ISREG(inode->i_mode) && !d_mountpoint(entry))
+			goto valid;
+
 		ret = -ECHILD;
 		if (flags & LOOKUP_RCU)
 			goto out;
 
-		fm = get_fuse_mount(inode);
-
-		forget = fuse_alloc_forget();
-		ret = -ENOMEM;
-		if (!forget)
-			goto out;
-
-		attr_version = fuse_get_attr_version(fm->fc);
-
-		fuse_lookup_init(&args, get_node_id(dir), name, &outarg);
-		ret = fuse_simple_request(fm, &args);
-		/* Zero nodeid is same as -ENOENT */
-		if (!ret && !outarg.nodeid)
-			ret = -ENOENT;
-		if (!ret) {
-			fi = get_fuse_inode(inode);
-			if (outarg.nodeid != get_node_id(inode) ||
-			    (bool) IS_AUTOMOUNT(inode) != (bool) (outarg.attr.flags & FUSE_ATTR_SUBMOUNT)) {
-				fuse_chan_queue_forget(fm->fc->chan, forget,
-						  outarg.nodeid, 1);
-				goto invalid;
-			}
-			spin_lock(&fi->lock);
-			fi->nlookup++;
-			spin_unlock(&fi->lock);
-		}
-		kfree(forget);
-		if (ret == -ENOMEM || ret == -EINTR)
+		ret = fuse_revalidate_lookup(dir, name, entry);
+		if (ret <= 0)
 			goto out;
-		if (ret || fuse_invalid_attr(&outarg.attr) ||
-		    fuse_stale_inode(inode, outarg.generation, &outarg.attr))
-			goto invalid;
-
-		forget_all_cached_acls(inode);
-		fuse_change_attributes(inode, &outarg.attr, NULL,
-				       ATTR_TIMEOUT(&outarg),
-				       attr_version);
-		fuse_change_entry_timeout(entry, &outarg);
 	} else if (inode) {
 		fi = get_fuse_inode(inode);
 		if (flags & LOOKUP_RCU) {
@@ -467,6 +487,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 			fuse_advise_use_readdirplus(dir);
 		}
 	}
+valid:
 	ret = 1;
 out:
 	return ret;
@@ -1018,6 +1039,146 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	return err;
 }
 
+/*
+ * Open a regular file whose dentry has expired with FUSE_OPEN_ATOMIC, which
+ * revalidates the name in the same request.  Returns 1 if the file still
+ * needs FUSE_OPEN, or -EOPENSTALE if the name no longer refers to it.
+ */
+int fuse_open_revalidate(struct file *file)
+{
+	struct dentry *entry = file->f_path.dentry;
+	struct inode *inode = d_inode(entry);
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	struct fuse_mount *fm = get_fuse_mount(inode);
+	struct fuse_conn *fc = fm->fc;
+	FUSE_ARGS(args);
+	struct fuse_forget_link *forget;
+	struct fuse_create_in inarg;
+	struct fuse_entry_out outentry;
+	struct fuse_open_out *outopenp;
+	struct name_snapshot name;
+	struct dentry *parent;
+	struct fuse_file *ff;
+	u64 attr_version;
+	int err;
+
+	if (fc->no_open_atomic || IS_ROOT(entry) || !S_ISREG(inode->i_mode) ||
+	    !time_before64(fuse_dentry_time(entry), get_jiffies_64()))
+		return 1;
+
+	forget = fuse_alloc_forget();
+	if (!forget)
+		return -ENOMEM;
+	ff = fuse_file_alloc(fm, true);
+	if (!ff) {
+		kfree(forget);
+		return -ENOMEM;
+	}
+
+	memset(&inarg, 0, sizeof(inarg));
+	memset(&outentry, 0, sizeof(outentry));
+	inarg.flags = file->f_flags & ~(O_CREAT | O_EXCL | O_NOCTTY);
+	inarg.mode = S_IFREG;
+	parent = dget_parent(entry);
+	take_dentry_name_snapshot(&name, entry);
+	args.opcode = FUSE_OPEN_ATOMIC;
+	args.nodeid = get_node_id(d_inode(parent));
+	args.in_numargs = 2;
+	args.in_args[0].size = sizeof(inarg);
+	args.in_args[0].value = &inarg;
+	args.in_args[1].size = name.name.len + 1;
+	args.in_args[1].value = name.name.name;
+	args.out_numargs = 2;
+	args.out_args[0].size = sizeof(outentry);
+	args.out_args[0].value = &outentry;
+	outopenp = &ff->args->open_outarg;
+	args.out_args[1].size = sizeof(*outopenp);
+	args.out_args[1].value = outopenp;
+	args.out_argvar = true;
+
+	attr_version = fuse_get_attr_version(fc);
+	err = fuse_simple_request(fm, &args);
+	if (err == -ENOSYS) {
+		/* d_revalidate() skipped the LOOKUP, so send it here */
+		fc->no_open_atomic = 1;
+		err = fuse_revalidate_lookup(d_inode(parent), &name.name,
+					     entry);
+		if (!err) {
+			d_drop(entry);
+			err = -EOPENSTALE;
+		}
+		release_dentry_name_snapshot(&name);
+		dput(parent);
+		fuse_file_free(ff);
+		kfree(forget);
+		return err;
+	}
+	release_dentry_name_snapshot(&name);
+	dput(parent);
+
+	if (err == -ENOENT) {
+		d_drop(entry);
+		err = -EOPENSTALE;
+	}
+	if (err < 0)
+		goto out_free;
+
+	if (args.out_args[1].size) {
+		ff->fh = outopenp->fh;
+		ff->nodeid = outentry.nodeid;
+		ff->open_flags = outopenp->open_flags;
+	}
+
+	/* The retry looks the name up again */
+	err = -EOPENSTALE;
+	if (outentry.nodeid != get_node_id(inode) ||
+	    fuse_invalid_attr(&outentry.attr) ||
+	    fuse_stale_inode(inode, outentry.generation, &outentry.attr)) {
+		d_drop(entry);
+		goto out_release;
+	}
+
+	/* Only O_CREAT creates; O_TRUNC leaves the file unopened */
+	err = -EIO;
+	if (args.out_args[1].size &&
+	    (args.out_args[1].size != sizeof(*outopenp) ||
+	     (outopenp->open_flags & FOPEN_CREATED) ||
+	     (file->f_flags & O_TRUNC)))
+		goto out_release;
+
+	spin_lock(&fi->lock);
+	fi->nlookup++;
+	spin_unlock(&fi->lock);
+	kfree(forget);
+	forget_all_cached_acls(inode);
+	fuse_change_attributes(inode, &outentry.attr, NULL,
+			       ATTR_TIMEOUT(&outentry), attr_version);
+	fuse_change_entry_timeout(entry, &outentry);
+
+	if (!args.out_args[1].size) {
+		fuse_file_free(ff);
+		return 1;
+	}
+	file->private_data = ff;
+	return 0;
+
+out_release:
+	if (outentry.nodeid)
+		fuse_chan_queue_forget(fc->chan, forget, outentry.nodeid, 1);
+	else
+		kfree(forget);
+	if (args.out_args[1].size)
+		fuse_sync_release(NULL, ff, file->f_flags);
+	else
+		fuse_file_free(ff);
+	return err;
+
+out_free:
+	fuse_file_free(ff);
+	kfree(forget);
+	return err;
+}
+
 static int fuse_mknod(struct mnt_idmap *, struct inode *, struct dentry *,
 		      umode_t, dev_t);
 static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 6d707f2b3bff..8f4a14de760a 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -278,7 +278,9 @@ static int fuse_open(struct inode *inode, struct file *file)
 	if (is_wb_truncate || vdax_truncate)
 		fuse_set_nowrite(inode);
 
-	err = fuse_do_open(fm, get_node_id(inode), file, false);
+	err = fuse_open_revalidate(file);
+	if (err > 0)
+		err = fuse_do_open(fm, get_node_id(inode), file, false);
 	if (!err) {
 		ff = file->private_data;
 		err = fuse_finish_open(inode, file);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index ba6dc4c43a1f..eabbb36c8dfd 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -954,6 +954,7 @@ void fuse_read_args_fill(struct fuse_io_args *ia, struct file *file, loff_t pos,
 struct fuse_file *fuse_file_alloc(struct fuse_mount *fm, bool release);
 void fuse_file_free(struct fuse_file *ff);
 int fuse_finish_open(struct inode *inode, struct file *file);
+int fuse_open_revalidate(struct file *file);
 
 void fuse_sync_release(struct fuse_inode *fi, struct fuse_file *ff,
 		       unsigned int flags);
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC
  2026-10-20 14:16 [PATCH 0/3] fuse: atomic open jamz
  2026-10-20 14:16 ` [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC jamz
  2026-10-20 14:16 ` [PATCH 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC jamz
@ 2026-10-20 14:16 ` jamz
  2026-10-20 23:33 ` [PATCH 0/3] fuse: atomic open NeilBrown
  3 siblings, 0 replies; 10+ messages in thread
From: jamz @ 2026-10-20 14:16 UTC (permalink / raw)
  To: Miklos Szeredi
  Cc: Jimmy Zuber, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques,
	NeilBrown

From: Jimmy Zuber <jamz@amazon.com>

Check the requests sent when opening and creating new, existing, missing
and expired names, against a raw /dev/fuse server with and without
FUSE_OPEN_ATOMIC, and that opening an existing read-only file is still
permission checked.

Assisted-by: Claude:claude-opus-5-5 [Claude-Code]
Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
 .../selftests/filesystems/fuse/.gitignore     |   1 +
 .../selftests/filesystems/fuse/Makefile       |   3 +-
 .../filesystems/fuse/atomic_open_test.c       | 473 ++++++++++++++++++
 3 files changed, 476 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c

diff --git a/tools/testing/selftests/filesystems/fuse/.gitignore b/tools/testing/selftests/filesystems/fuse/.gitignore
index b5b03db1118c..6e529fee96d4 100644
--- a/tools/testing/selftests/filesystems/fuse/.gitignore
+++ b/tools/testing/selftests/filesystems/fuse/.gitignore
@@ -1,4 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0-only
+atomic_open_test
 fuse_mnt
 fusectl_test
 test_syncfs
diff --git a/tools/testing/selftests/filesystems/fuse/Makefile b/tools/testing/selftests/filesystems/fuse/Makefile
index c2de8d225447..2fe20f0e9143 100644
--- a/tools/testing/selftests/filesystems/fuse/Makefile
+++ b/tools/testing/selftests/filesystems/fuse/Makefile
@@ -3,7 +3,7 @@
 CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS := fusectl_test test_syncfs
-TEST_GEN_PROGS += write_extend_eof_test
+TEST_GEN_PROGS += write_extend_eof_test atomic_open_test
 TEST_GEN_FILES := fuse_mnt
 
 # fuse_acl_cache_test requires libfuse3; add it only when the library is present.
@@ -16,6 +16,7 @@ endif
 include ../../lib.mk
 
 $(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread
+$(OUTPUT)/atomic_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/atomic_open_test.c b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c
new file mode 100644
index 000000000000..880367d6cb96
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c
@@ -0,0 +1,473 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test open() and create with and without FUSE_OPEN_ATOMIC.
+ *
+ * Requires root to mount fuse.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <linux/fuse.h>
+
+#include "../../kselftest_harness.h"
+
+#define FUSE_ROOT_ID	1
+#define MAX_WRITE	(128 * 1024)
+#define MAX_ENTRIES	16
+#define NOBODY		65534
+
+struct entry {
+	char name[64];
+	uint64_t nodeid;
+	uint32_t mode;
+	uint64_t entry_valid;
+};
+
+/* Server state, shared with the responder thread. */
+struct server {
+	int fd;
+	int atomic;
+	struct entry ent[MAX_ENTRIES];
+	int nent;
+	int nr_lookup, nr_create, nr_open, nr_open_atomic, nr_setattr;
+};
+
+static void reply(int fd, uint64_t unique, int error, void *data, size_t len)
+{
+	struct fuse_out_header oh = {
+		.len = sizeof(oh) + (data ? len : 0),
+		.error = error,
+		.unique = unique,
+	};
+	struct iovec iov[2] = { { &oh, sizeof(oh) }, { data, len } };
+
+	/* Errors here are teardown races (device closed on unmount); ignore. */
+	if (writev(fd, iov, data ? 2 : 1) < 0)
+		return;
+}
+
+static struct entry *find(struct server *s, const char *name)
+{
+	int i;
+
+	for (i = 0; i < s->nent; i++)
+		if (!strcmp(s->ent[i].name, name))
+			return &s->ent[i];
+	return NULL;
+}
+
+static struct entry *add(struct server *s, const char *name, uint32_t mode)
+{
+	struct entry *e = &s->ent[s->nent];
+
+	snprintf(e->name, sizeof(e->name), "%.63s", name);
+	e->nodeid = FUSE_ROOT_ID + 1 + s->nent++;
+	e->mode = mode;
+	e->entry_valid = 3600;
+	return e;
+}
+
+/* A NULL entry is a negative entry, cached like a found one. */
+static void fill_entry(struct fuse_entry_out *out, struct entry *e)
+{
+	memset(out, 0, sizeof(*out));
+	out->entry_valid = e ? e->entry_valid : 3600;
+	if (!e)
+		return;
+	out->nodeid = e->nodeid;
+	out->attr_valid = 3600;
+	out->attr.ino = e->nodeid;
+	out->attr.mode = e->mode;
+	out->attr.nlink = S_ISDIR(e->mode) ? 2 : 1;
+	out->attr.blksize = 4096;
+}
+
+static void *server_thread(void *arg)
+{
+	struct server *s = arg;
+	static char buf[MAX_WRITE + 4096];
+
+	for (;;) {
+		ssize_t n = read(s->fd, buf, sizeof(buf));
+		struct fuse_in_header *ih = (void *)buf;
+
+		if (n < 0) {
+			if (errno == EINTR || errno == EAGAIN)
+				continue;
+			return NULL;	/* device closed on unmount */
+		}
+		if (n < (ssize_t)sizeof(*ih))
+			continue;
+
+		switch (ih->opcode) {
+		case FUSE_INIT: {
+			struct fuse_init_in *in = (void *)(ih + 1);
+			struct fuse_init_out out = {0};
+
+			out.major = FUSE_KERNEL_VERSION;
+			out.minor = FUSE_KERNEL_MINOR_VERSION;
+			out.max_readahead = in->max_readahead;
+			out.max_write = MAX_WRITE;
+			out.max_background = 16;
+			out.congestion_threshold = 12;
+			out.flags = FUSE_INIT_EXT;
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_GETATTR:
+		case FUSE_SETATTR: {
+			struct fuse_attr_out out = {0};
+			struct fuse_entry_out eo;
+			int i;
+
+			if (ih->opcode == FUSE_SETATTR)
+				s->nr_setattr++;
+			out.attr_valid = 3600;
+			if (ih->nodeid == FUSE_ROOT_ID) {
+				out.attr.ino = FUSE_ROOT_ID;
+				out.attr.mode = S_IFDIR | 0777;
+				out.attr.nlink = 2;
+			}
+			for (i = 0; i < s->nent; i++) {
+				if (s->ent[i].nodeid == ih->nodeid) {
+					fill_entry(&eo, &s->ent[i]);
+					out.attr = eo.attr;
+				}
+			}
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_LOOKUP: {
+			struct fuse_entry_out out;
+
+			s->nr_lookup++;
+			fill_entry(&out, find(s, (char *)(ih + 1)));
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_CREATE:
+		case FUSE_OPEN_ATOMIC: {
+			struct fuse_create_in *in = (void *)(ih + 1);
+			const char *name = (char *)(in + 1);
+			struct entry *e = find(s, name);
+			struct {
+				struct fuse_entry_out entry;
+				struct fuse_open_out open;
+			} out = {0};
+
+			if (ih->opcode == FUSE_CREATE)
+				s->nr_create++;
+			else
+				s->nr_open_atomic++;
+			if (ih->opcode == FUSE_OPEN_ATOMIC && !s->atomic) {
+				reply(s->fd, ih->unique, -ENOSYS, NULL, 0);
+				break;
+			}
+			if (e && (in->flags & O_CREAT) &&
+			    (in->flags & O_EXCL)) {
+				reply(s->fd, ih->unique, -EEXIST, NULL, 0);
+				break;
+			}
+			if (!e && !(in->flags & O_CREAT)) {
+				fill_entry(&out.entry, NULL);
+				reply(s->fd, ih->unique, 0, &out.entry,
+				      sizeof(out.entry));
+				break;
+			}
+			if (e && (!S_ISREG(e->mode) || (in->flags & O_TRUNC))) {
+				fill_entry(&out.entry, e);
+				reply(s->fd, ih->unique, 0, &out.entry,
+				      sizeof(out.entry));
+				break;
+			}
+			if (!e) {
+				e = add(s, name, S_IFREG | in->mode);
+				if (ih->opcode == FUSE_OPEN_ATOMIC)
+					out.open.open_flags = FOPEN_CREATED;
+			}
+			fill_entry(&out.entry, e);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_TMPFILE: {
+			struct fuse_create_in *in = (void *)(ih + 1);
+			struct entry e = {
+				.nodeid = 100,
+				.mode = S_IFREG | in->mode,
+				.entry_valid = 3600,
+			};
+			struct {
+				struct fuse_entry_out entry;
+				struct fuse_open_out open;
+			} out = {0};
+
+			fill_entry(&out.entry, &e);
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_OPEN: {
+			struct fuse_open_out out = {0};
+
+			s->nr_open++;
+			reply(s->fd, ih->unique, 0, &out, sizeof(out));
+			break;
+		}
+		case FUSE_FLUSH:
+		case FUSE_RELEASE:
+			reply(s->fd, ih->unique, 0, NULL, 0);
+			break;
+		case FUSE_FORGET:
+		case FUSE_BATCH_FORGET:
+			break;
+		default:
+			reply(s->fd, ih->unique, -EOPNOTSUPP, NULL, 0);
+			break;
+		}
+	}
+}
+
+FIXTURE(fuse)
+{
+	struct server *srv;
+	char dir[64];
+	int mounted;
+};
+
+FIXTURE_VARIANT(fuse)
+{
+	int atomic;
+};
+
+FIXTURE_VARIANT_ADD(fuse, atomic)
+{
+	.atomic = 1,
+};
+
+FIXTURE_VARIANT_ADD(fuse, plain)
+{
+	.atomic = 0,
+};
+
+FIXTURE_SETUP(fuse)
+{
+	char opts[128];
+	struct stat st;
+	pthread_t t;
+
+	if (geteuid() != 0)
+		SKIP(return, "need root to mount fuse");
+
+	self->srv = mmap(NULL, sizeof(*self->srv), PROT_READ | PROT_WRITE,
+			 MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(MAP_FAILED, self->srv);
+	self->srv->atomic = variant->atomic;
+	add(self->srv, "ro", S_IFREG | 0444);
+	add(self->srv, "rw", S_IFREG | 0644);
+	add(self->srv, "stale", S_IFREG | 0644)->entry_valid = 0;
+
+	self->srv->fd = open("/dev/fuse", O_RDWR);
+	ASSERT_GE(self->srv->fd, 0);
+
+	strcpy(self->dir, "/tmp/fuse_aopen_XXXXXX");
+	ASSERT_NE(NULL, mkdtemp(self->dir));
+
+	snprintf(opts, sizeof(opts),
+		 "fd=%d,rootmode=40000,user_id=0,group_id=0,default_permissions,allow_other",
+		 self->srv->fd);
+	ASSERT_EQ(0, mount("fuse", self->dir, "fuse", 0, opts));
+	self->mounted = 1;
+
+	ASSERT_EQ(0, pthread_create(&t, NULL, server_thread, self->srv));
+	pthread_detach(t);
+
+	/* Wait for FUSE_INIT: the root GETATTR is queued behind it */
+	ASSERT_EQ(0, stat(self->dir, &st));
+}
+
+FIXTURE_TEARDOWN(fuse)
+{
+	if (self->mounted)
+		umount2(self->dir, MNT_DETACH);
+	if (self->srv && self->srv != MAP_FAILED) {
+		if (self->srv->fd > 0)
+			close(self->srv->fd);
+		munmap(self->srv, sizeof(*self->srv));
+	}
+	if (self->dir[0])
+		rmdir(self->dir);
+}
+
+/* Open @name as an unprivileged user and return the errno, or 0. */
+static int open_as_nobody(const char *dir, const char *name, int flags)
+{
+	char path[128];
+	int status, fd;
+	pid_t pid;
+
+	snprintf(path, sizeof(path), "%s/%s", dir, name);
+	pid = fork();
+	if (pid < 0)
+		return -1;
+	if (pid == 0) {
+		if (setgid(NOBODY) || setuid(NOBODY))
+			_exit(255);
+		fd = open(path, flags, 0644);
+		_exit(fd < 0 ? errno : 0);
+	}
+	if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+		return -1;
+	return WEXITSTATUS(status);
+}
+
+static int open_at(const char *dir, const char *name, int flags)
+{
+	char path[128];
+
+	snprintf(path, sizeof(path), "%s/%s", dir, name);
+	return open(path, flags, 0644);
+}
+
+TEST_F(fuse, create_new)
+{
+	int fd;
+
+	fd = open_at(self->dir, "a", O_CREAT | O_WRONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	fd = open_at(self->dir, "b", O_CREAT | O_EXCL | O_WRONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	fd = open(self->dir, O_TMPFILE | O_RDWR, 0600);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	EXPECT_EQ(0, self->srv->nr_open);
+	if (variant->atomic) {
+		EXPECT_EQ(2, self->srv->nr_open_atomic);
+		EXPECT_EQ(0, self->srv->nr_lookup);
+		EXPECT_EQ(0, self->srv->nr_create);
+	} else {
+		EXPECT_EQ(1, self->srv->nr_open_atomic);
+		EXPECT_EQ(2, self->srv->nr_lookup);
+		EXPECT_EQ(2, self->srv->nr_create);
+	}
+}
+
+TEST_F(fuse, create_existing)
+{
+	int fd;
+
+	ASSERT_EQ(-1, open_at(self->dir, "ro", O_CREAT | O_EXCL | O_WRONLY));
+	ASSERT_EQ(EEXIST, errno);
+	ASSERT_EQ(EACCES, open_as_nobody(self->dir, "ro", O_CREAT | O_RDWR));
+
+	/* Truncation goes through FUSE_OPEN and SETATTR. */
+	fd = open_at(self->dir, "rw", O_CREAT | O_TRUNC | O_WRONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	EXPECT_EQ(0, self->srv->nr_create);
+	EXPECT_EQ(1, self->srv->nr_open);
+	EXPECT_EQ(1, self->srv->nr_setattr);
+	if (variant->atomic) {
+		EXPECT_EQ(3, self->srv->nr_open_atomic);
+		EXPECT_EQ(0, self->srv->nr_lookup);
+	} else {
+		EXPECT_EQ(1, self->srv->nr_open_atomic);
+		EXPECT_EQ(2, self->srv->nr_lookup);
+	}
+}
+
+TEST_F(fuse, open_existing)
+{
+	int fd;
+
+	ASSERT_EQ(EACCES, open_as_nobody(self->dir, "ro", O_RDWR));
+
+	fd = open_at(self->dir, "ro", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY));
+	ASSERT_EQ(ENOENT, errno);
+	ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY));
+	ASSERT_EQ(ENOENT, errno);
+
+	EXPECT_EQ(1, self->srv->nr_open);
+	if (variant->atomic) {
+		EXPECT_EQ(2, self->srv->nr_open_atomic);
+		EXPECT_EQ(0, self->srv->nr_lookup);
+	} else {
+		EXPECT_EQ(1, self->srv->nr_open_atomic);
+		EXPECT_EQ(2, self->srv->nr_lookup);
+	}
+}
+
+TEST_F(fuse, open_stale)
+{
+	struct stat st;
+	int fd;
+
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* The entry has expired, the reopen checks the name. */
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* Another file takes the name: the open finds the new one. */
+	find(self->srv, "stale")->nodeid = 200;
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	ASSERT_EQ(0, fstat(fd, &st));
+	EXPECT_EQ(200, st.st_ino);
+	close(fd);
+
+	if (variant->atomic) {
+		EXPECT_EQ(4, self->srv->nr_open_atomic);
+		EXPECT_EQ(0, self->srv->nr_lookup);
+		EXPECT_EQ(0, self->srv->nr_open);
+	} else {
+		EXPECT_EQ(1, self->srv->nr_open_atomic);
+		EXPECT_EQ(4, self->srv->nr_lookup);
+		EXPECT_EQ(3, self->srv->nr_open);
+	}
+}
+
+TEST_F(fuse, open_stale_first)
+{
+	char path[128];
+	struct stat st;
+	int fd;
+
+	/* The first FUSE_OPEN_ATOMIC comes from reopening a cached name */
+	snprintf(path, sizeof(path), "%s/stale", self->dir);
+	ASSERT_EQ(0, stat(path, &st));
+	fd = open(path, O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	EXPECT_EQ(1, self->srv->nr_open_atomic);
+	if (variant->atomic) {
+		EXPECT_EQ(1, self->srv->nr_lookup);
+		EXPECT_EQ(0, self->srv->nr_open);
+	} else {
+		EXPECT_EQ(2, self->srv->nr_lookup);
+		EXPECT_EQ(1, self->srv->nr_open);
+	}
+}
+
+TEST_HARNESS_MAIN
-- 
2.50.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC
  2026-10-20 14:16 ` [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC jamz
@ 2026-10-20 23:23   ` NeilBrown
  2026-10-22  1:45     ` Jimmy Zuber
  0 siblings, 1 reply; 10+ messages in thread
From: NeilBrown @ 2026-10-20 23:23 UTC (permalink / raw)
  To: jamz
  Cc: Miklos Szeredi, Jimmy Zuber, fuse-devel, linux-kselftest,
	Shuah Khan, Bernd Schubert, Jim Harris, Horst Birthelmer,
	Luis Henriques

On Wed, 21 Oct 2026, jamz wrote:
> From: Jimmy Zuber <jamz@amazon.com>
> 
> 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 <jamz@amazon.com>
> ---
>  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);

As of f1ac607f786f d_splice_alias() handles hashed dentries
and the d_drop() is not needed and not wanted.


> +	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);

Similar to above, d_splice_alias() is now sufficient for all cases.

Thanks,
NeilBrown

>  	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
> 
> 


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 0/3] fuse: atomic open
  2026-10-20 14:16 [PATCH 0/3] fuse: atomic open jamz
                   ` (2 preceding siblings ...)
  2026-10-20 14:16 ` [PATCH 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC jamz
@ 2026-10-20 23:33 ` NeilBrown
  2026-10-22  1:46   ` Jimmy Zuber
  3 siblings, 1 reply; 10+ messages in thread
From: NeilBrown @ 2026-10-20 23:33 UTC (permalink / raw)
  To: jamz
  Cc: Miklos Szeredi, Jimmy Zuber, fuse-devel, linux-kselftest,
	Shuah Khan, Bernd Schubert, Jim Harris, Horst Birthelmer,
	Luis Henriques

On Wed, 21 Oct 2026, jamz wrote:
> From: Jimmy Zuber <jamz@amazon.com>
> 
> 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 reply says whether the file was created, as Miklos suggested in [1].
> The overall pattern follows his sketch in [2]. For a cached
> name, the kernel compares the returned nodeid with the cached one and
> looks the name up again on a mismatch.
> 
> A server without FUSE_OPEN_ATOMIC replies -ENOSYS and keeps today's
> behaviour.

This all looks like a reasonably sensible design. As permission checking
happens in the client, not on the server, it is appropriate that
FUSE_OPEN_ATOMIC only opens the file if it actually created it.
So it doesn't make sense to implement O_TRUNC.

However the code seems to suggest that the server might open the file if
it was found to already exist.  In that case, were does the permission
check happen...

Maybe I need a more explicit statement of what FUSE_OPEN_ATOMIC is meant
to do in the various cases.  Or maybe I just don't know enough about
fuse.

There is work underway to support O_CREAT|O_DIRECTORY so that a
directory can be created and opened in a single operation.  Would it
make sense to include this in FUSE_OPEN_ATOMIC ??

Thanks,
NeilBrown



> 
> Tested with a new selftest under UML and KVM.
> 
> [1] https://lore.kernel.org/all/CAJfpeguEK4JsFo60=yHkpLL6XkDGS7cQ5GOhiFL379KsYk0TsQ@mail.gmail.com/
> [2] https://lore.kernel.org/all/CAJfpegvXtXY=Pbxv+dMGFR8mvWN0DUwhSo6NwaVexk6Y6sao+w@mail.gmail.com/
> 
> 
> Jimmy Zuber (3):
>   fuse: add FUSE_OPEN_ATOMIC
>   fuse: open cached files with FUSE_OPEN_ATOMIC
>   selftests/fuse: add test for FUSE_OPEN_ATOMIC
> 
>  fs/fuse/dir.c                                 | 359 +++++++++++--
>  fs/fuse/file.c                                |   4 +-
>  fs/fuse/fuse_i.h                              |   4 +
>  include/uapi/linux/fuse.h                     |   8 +-
>  .../selftests/filesystems/fuse/.gitignore     |   1 +
>  .../selftests/filesystems/fuse/Makefile       |   3 +-
>  .../filesystems/fuse/atomic_open_test.c       | 473 ++++++++++++++++++
>  7 files changed, 797 insertions(+), 55 deletions(-)
>  create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c
> 
> 
> base-commit: b776cbe24cc1b79c7f5101762b0069b3f48b6a4b
> -- 
> 2.50.1
> 
> 


^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC
  2026-10-20 23:23   ` NeilBrown
@ 2026-10-22  1:45     ` Jimmy Zuber
  2026-10-22  5:26       ` NeilBrown
  0 siblings, 1 reply; 10+ messages in thread
From: Jimmy Zuber @ 2026-10-22  1:45 UTC (permalink / raw)
  To: NeilBrown
  Cc: Miklos Szeredi, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques

On Wed, 21 Oct 2026, NeilBrown wrote:
> > +	d_drop(entry);
> > +	res = d_splice_alias(inode, entry);
>
> As of f1ac607f786f d_splice_alias() handles hashed dentries
> and the d_drop() is not needed and not wanted.

Thanks. I'll rebase onto 7.4-rc1 (will make note of the dependency), which
has the commit f1ac607f786f.

> > -	d_instantiate(entry, inode);
> > +	if (d_in_lookup(entry))
> > +		d_splice_alias(inode, entry);
> > +	else
> > +		d_instantiate(entry, inode);
>
> Similar to above, d_splice_alias() is now sufficient for all cases.

Thanks, fuse_create_open() also handles FUSE_TMPFILE, which needs
d_instantiate() for its dentry to stay unhashed. Otherwise I can have it use
d_splice_alias().

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 0/3] fuse: atomic open
  2026-10-20 23:33 ` [PATCH 0/3] fuse: atomic open NeilBrown
@ 2026-10-22  1:46   ` Jimmy Zuber
  2026-10-22  5:30     ` NeilBrown
  0 siblings, 1 reply; 10+ messages in thread
From: Jimmy Zuber @ 2026-10-22  1:46 UTC (permalink / raw)
  To: NeilBrown
  Cc: Miklos Szeredi, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques

On Wed, 21 Oct 2026, NeilBrown wrote:
> This all looks like a reasonably sensible design. As permission checking
> happens in the client, not on the server, it is appropriate that
> FUSE_OPEN_ATOMIC only opens the file if it actually created it.
> So it doesn't make sense to implement O_TRUNC.
>
> However the code seems to suggest that the server might open the file if
> it was found to already exist.  In that case, were does the permission
> check happen...
>
> Maybe I need a more explicit statement of what FUSE_OPEN_ATOMIC is meant
> to do in the various cases.  Or maybe I just don't know enough about
> fuse.

FUSE_OPEN_ATOMIC will open an existing regular file unless O_TRUNC is
specified. If the server opens an existing file, the kernel will perform a
permission check after the reply (do_open() calls may_open()). If that
permission check fails, we will release the file. This matches other file
systems that support atomic_open(), such as ceph, where a file can be opened
before a kernel check is performed. Servers that don't want this should just
return the entry only to have the kernel check permissions and open. I'll try
to make the expected behavior of FUSE_OPEN_ATOMIC implementations more clear
in v2.

> There is work underway to support O_CREAT|O_DIRECTORY so that a
> directory can be created and opened in a single operation.  Would it
> make sense to include this in FUSE_OPEN_ATOMIC ??

Oh thanks for the heads up. This would be a nice fit. If that series is
accepted I can update this one accordingly. That said, even with that series
it might be nice to have an init flag in case implementations want to
selectively support atomic file but not directory open/create (ENOSYS is too
coarse grained).

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC
  2026-10-22  1:45     ` Jimmy Zuber
@ 2026-10-22  5:26       ` NeilBrown
  0 siblings, 0 replies; 10+ messages in thread
From: NeilBrown @ 2026-10-22  5:26 UTC (permalink / raw)
  To: Jimmy Zuber
  Cc: Miklos Szeredi, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques

On Thu, 22 Oct 2026, Jimmy Zuber wrote:
> On Wed, 21 Oct 2026, NeilBrown wrote:
> > > +	d_drop(entry);
> > > +	res = d_splice_alias(inode, entry);
> >
> > As of f1ac607f786f d_splice_alias() handles hashed dentries
> > and the d_drop() is not needed and not wanted.
> 
> Thanks. I'll rebase onto 7.4-rc1 (will make note of the dependency), which
> has the commit f1ac607f786f.
> 
> > > -	d_instantiate(entry, inode);
> > > +	if (d_in_lookup(entry))
> > > +		d_splice_alias(inode, entry);
> > > +	else
> > > +		d_instantiate(entry, inode);
> >
> > Similar to above, d_splice_alias() is now sufficient for all cases.
> 
> Thanks, fuse_create_open() also handles FUSE_TMPFILE, which needs
> d_instantiate() for its dentry to stay unhashed. Otherwise I can have it use
> d_splice_alias().
> 

Almost all other filesystems use d_tmpfile() for tmp-files (which calls
d_instantiate). Is there a reason that fuse cannot use d_tmpfile()?

Thanks,
NeilBrown

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 0/3] fuse: atomic open
  2026-10-22  1:46   ` Jimmy Zuber
@ 2026-10-22  5:30     ` NeilBrown
  0 siblings, 0 replies; 10+ messages in thread
From: NeilBrown @ 2026-10-22  5:30 UTC (permalink / raw)
  To: Jimmy Zuber
  Cc: Miklos Szeredi, fuse-devel, linux-kselftest, Shuah Khan,
	Bernd Schubert, Jim Harris, Horst Birthelmer, Luis Henriques

On Thu, 22 Oct 2026, Jimmy Zuber wrote:
> On Wed, 21 Oct 2026, NeilBrown wrote:
> > This all looks like a reasonably sensible design. As permission checking
> > happens in the client, not on the server, it is appropriate that
> > FUSE_OPEN_ATOMIC only opens the file if it actually created it.
> > So it doesn't make sense to implement O_TRUNC.
> >
> > However the code seems to suggest that the server might open the file if
> > it was found to already exist.  In that case, were does the permission
> > check happen...
> >
> > Maybe I need a more explicit statement of what FUSE_OPEN_ATOMIC is meant
> > to do in the various cases.  Or maybe I just don't know enough about
> > fuse.
> 
> FUSE_OPEN_ATOMIC will open an existing regular file unless O_TRUNC is
> specified. If the server opens an existing file, the kernel will perform a
> permission check after the reply (do_open() calls may_open()). If that
> permission check fails, we will release the file. This matches other file
> systems that support atomic_open(), such as ceph, where a file can be opened
> before a kernel check is performed. Servers that don't want this should just
> return the entry only to have the kernel check permissions and open. I'll try
> to make the expected behavior of FUSE_OPEN_ATOMIC implementations more clear
> in v2.

It seems a little odd performing the "open" before any permission check,
but I can see how it might work, and as you say the server an refuse if
it has reason too.

Thanks for the clarification.

NeilBrown


> 
> > There is work underway to support O_CREAT|O_DIRECTORY so that a
> > directory can be created and opened in a single operation.  Would it
> > make sense to include this in FUSE_OPEN_ATOMIC ??
> 
> Oh thanks for the heads up. This would be a nice fit. If that series is
> accepted I can update this one accordingly. That said, even with that series
> it might be nice to have an init flag in case implementations want to
> selectively support atomic file but not directory open/create (ENOSYS is too
> coarse grained).
> 


^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-22  5:31 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20 14:16 [PATCH 0/3] fuse: atomic open jamz
2026-10-20 14:16 ` [PATCH 1/3] fuse: add FUSE_OPEN_ATOMIC jamz
2026-10-20 23:23   ` NeilBrown
2026-10-22  1:45     ` Jimmy Zuber
2026-10-22  5:26       ` NeilBrown
2026-10-20 14:16 ` [PATCH 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC jamz
2026-10-20 14:16 ` [PATCH 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC jamz
2026-10-20 23:33 ` [PATCH 0/3] fuse: atomic open NeilBrown
2026-10-22  1:46   ` Jimmy Zuber
2026-10-22  5:30     ` NeilBrown

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox