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

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 series applies on top of "fuse: fix use-after-free in
fuse_create_open() error path" [3].

Tested with a new selftest under UML and KVM.

Changes in v4 (from the Sashiko review of v3 [5]):
- Patch 1: when the VFS dropped O_CREAT or O_TRUNC (no write access to
  the directory, read-only mount), send FUSE_LOOKUP, so that O_CREAT |
  O_EXCL fails with EEXIST and O_TRUNC with EROFS. Reject a reply that
  opened an existing file for O_EXCL. On -EACCES from the check before
  opening, refresh the attributes and check again, as fuse_permission()
  does.
- Patch 2: fuse_open() checks the name on every open of an expired, hashed
  regular file, with no flag, so concurrent opens each check it. An open
  that did not look the name up and finds it changed returns
  -EOPENSTALE, and the retry opens the dropped dentry by nodeid. Backing
  files open by nodeid. O_CREAT opens keep the LOOKUP. The check after
  the reply uses inode_permission() and the O_NOATIME owner check, with
  or without default_permissions. Revalidate before fuse_open() prepares
  O_TRUNC and calls generic_file_open(). Keep an entry timeout of zero,
  and leave a name renamed during the request hashed.
- Patch 3: test O_CREAT | O_EXCL without write access to the directory,
  O_TRUNC on a read-only mount, and concurrent reopens of an expired
  name.

Changes in v3 (from the Sashiko review [4]):
- Patch 1: strip FOPEN_CREATED from the open flags, which broke
  passthrough. Check name_max before FUSE_OPEN_ATOMIC. Reject a reply
  whose inode is not a regular file or is bad, and skip page cache
  invalidation for DAX, as fuse_open() does. With default_permissions,
  check the attributes in the reply before opening an existing file.
- Patch 2: only an open that skipped the LOOKUP checks the name, so
  reopens through /proc/self/fd, open_by_handle_at() and other
  vfs_open() callers open by nodeid. Keep the LOOKUP for directory
  opens, and with default_permissions while the cached attributes have
  expired. Check the open again against the attributes in the reply.
  Set the open flags only after the reply checks, so a mismatched
  passthrough reply is released without an inode. Read the parent and
  name under rename_lock, and send RELEASE before FORGET.
- Patch 3: test the above. Skip without /dev/fuse, and leave the server
  state mapped at teardown.

Changes in v2:
- Drop the d_drop() before d_splice_alias() for an entry-only reply, and
  use d_splice_alias() for FUSE_CREATE and FUSE_OPEN_ATOMIC replies.
  FUSE_TMPFILE keeps d_instantiate(), as its dentry stays unhashed
  (NeilBrown).
- Describe what the server does and when the kernel checks permission
  in each case (NeilBrown).
- Check in the selftest that an O_TMPFILE file has no name in its
  directory.
- Rebase onto fuse for-next, which has f1ac607f786f ("VFS: enhance
  d_splice_alias() to handle hashed dentries"). Accept a fuse_open_out
  without backing_id_64, as FUSE_CREATE and FUSE_OPEN now do, and test
  it.

v3: https://lore.kernel.org/fuse-devel/20261026190747.2373775-1-jamz@amazon.com/
v2: https://lore.kernel.org/fuse-devel/20261026151828.562870-1-jamz@amazon.com/
v1: https://lore.kernel.org/fuse-devel/20261020141647.3271341-1-jamz@amazon.com/

[1] https://lore.kernel.org/all/CAJfpeguEK4JsFo60=yHkpLL6XkDGS7cQ5GOhiFL379KsYk0TsQ@mail.gmail.com/
[2] https://lore.kernel.org/all/CAJfpegvXtXY=Pbxv+dMGFR8mvWN0DUwhSo6NwaVexk6Y6sao+w@mail.gmail.com/
[3] https://lore.kernel.org/fuse-devel/20261026180649.1265825-1-jamz@amazon.com/
[4] https://sashiko.dev/#/patchset/20261026151828.562870-1-jamz@amazon.com
[5] https://sashiko.dev/#/patchset/20261026190747.2373775-1-jamz@amazon.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                                 | 499 ++++++++++++--
 fs/fuse/file.c                                |  22 +-
 fs/fuse/fuse_i.h                              |   7 +
 include/uapi/linux/fuse.h                     |   8 +-
 .../selftests/filesystems/fuse/.gitignore     |   1 +
 .../selftests/filesystems/fuse/Makefile       |   3 +-
 .../filesystems/fuse/atomic_open_test.c       | 645 ++++++++++++++++++
 7 files changed, 1120 insertions(+), 65 deletions(-)
 create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c


base-commit: 9b1da7b4a4e197072e630c3b85d4100a399752ff
prerequisite-patch-id: 95a6559bc4490d7d82bc80ba961ac52e80429b34
-- 
2.50.1


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

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

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, if it opened the file, the open
file:

- New name with O_CREAT: the server creates and opens the file and sets
  FOPEN_CREATED. The kernel checks write permission on the parent before
  sending the request.
- Existing regular file without O_TRUNC: the server opens it. The kernel
  checks permission after the reply and releases the file if the check
  fails, as for other ->atomic_open filesystems. With
  default_permissions, it checks the attributes in the reply before it
  opens the file itself. A server can instead return the entry only, to
  have the kernel check permission before FUSE_OPEN.
- Existing file with O_TRUNC, or not a regular file: the server returns
  the entry only, and the VFS checks permission and opens it, so that
  truncation is performed after permissions are checked.
- Name that does not exist, without O_CREAT: the server returns a zero
  nodeid, cacheable like a negative LOOKUP.

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.

If the VFS dropped O_CREAT or O_TRUNC because the open cannot create or
truncate, the kernel sends FUSE_LOOKUP and lets the VFS report the error.

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             | 179 +++++++++++++++++++++++++++++++++++---
 fs/fuse/fuse_i.h          |   6 ++
 include/uapi/linux/fuse.h |   8 +-
 3 files changed, 178 insertions(+), 15 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 4c64a4f812395..50dd6a07cc067 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -814,8 +814,79 @@ static void free_ext_value(struct fuse_args *args)
 		kfree(args->in_args[args->ext_idx].value);
 }
 
+static int fuse_perm_getattr(struct inode *inode, int mask);
+
+/*
+ * Check the open against attributes the server just returned, with the mask
+ * do_open() passes to may_open().  As in fuse_permission(), a denial
+ * refreshes the attributes and checks again.  may_open() still runs the full
+ * check.
+ */
+static int fuse_may_open(struct file *file, struct inode *inode)
+{
+	struct mnt_idmap *idmap = file_mnt_idmap(file);
+	int mask = MAY_OPEN;
+	int err;
+
+	if (file->f_flags & __FMODE_EXEC) {
+		mask |= MAY_EXEC;
+	} else {
+		mask |= ACC_MODE(file->f_flags);
+		if (file->f_flags & O_TRUNC)
+			mask |= MAY_WRITE;
+		if (file->f_flags & O_APPEND)
+			mask |= MAY_APPEND;
+	}
+	err = generic_permission(idmap, inode, mask);
+	if (err == -EACCES) {
+		err = fuse_perm_getattr(inode, mask);
+		if (!err)
+			err = generic_permission(idmap, inode, mask);
+	}
+	return err;
+}
+
 /*
- * 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;
+	}
+
+	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 +904,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);
@@ -880,25 +955,59 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	args.out_args[1].size = sizeof(*outopenp);
 	args.out_args[1].value = outopenp;
 
-	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 < 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 < FUSE_COMPAT_OPEN_OUT_SIZE)
 		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 || (flags & O_EXCL))))
+			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);
@@ -907,29 +1016,46 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 		goto out_err;
 	}
 	kfree(forget);
-	d_instantiate(entry, inode);
+	ff->open_flags = outopenp->open_flags & ~FOPEN_CREATED;
+	fi = get_fuse_inode(inode);
+	err = -EIO;
+	if (!S_ISREG(inode->i_mode) || fuse_is_bad(inode))
+		goto out_release;
+	if (opcode == FUSE_TMPFILE)
+		d_instantiate(entry, inode);
+	else
+		d_splice_alias(inode, entry);
 	fuse_dentry_set_epoch(entry, epoch);
 	fuse_change_entry_timeout(entry, &outentry);
-	fuse_dir_changed(dir);
-	err = generic_file_open(inode, file);
+	if (created)
+		fuse_dir_changed(dir);
+	err = 0;
+	/* Check an existing file before the VFS acts on the open */
+	if (!created && fm->fc->default_permissions)
+		err = fuse_may_open(file, inode);
+	if (!err)
+		err = generic_file_open(inode, file);
 	if (!err) {
 		file->private_data = ff;
 		err = finish_open(file, entry, fuse_finish_open);
 	}
 	if (err) {
 		/* fput() releases a file that finish_open() opened */
-		if (!(file->f_mode & FMODE_OPENED)) {
-			fi = get_fuse_inode(inode);
+		if (!(file->f_mode & FMODE_OPENED))
 			fuse_sync_release(fi, ff, flags);
-		}
 	} else {
 		if (fm->fc->atomic_o_trunc && trunc)
 			truncate_pagecache(inode, 0);
-		else if (!(ff->open_flags & FOPEN_KEEP_CACHE))
+		else if (!(ff->open_flags & FOPEN_KEEP_CACHE) && !IS_DAX(inode))
 			invalidate_inode_pages2(inode->i_mapping);
 	}
 	return err;
 
+out_release:
+	fuse_sync_release(fi, ff, flags);
+	iput(inode);
+	return err;
+
 out_free_ff:
 	fuse_file_free(ff);
 out_put_forget_req:
@@ -938,6 +1064,18 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	return err;
 }
 
+/*
+ * lookup_open() clears O_CREAT, and O_TRUNC without O_EXCL, when the open
+ * cannot create or truncate; do_open() then reports the error.
+ */
+static bool fuse_open_flags_dropped(struct file *file, unsigned int flags)
+{
+	unsigned int dropped = file->f_flags & ~flags;
+
+	return (dropped & O_CREAT) ||
+	       ((dropped & O_TRUNC) && !(flags & O_EXCL));
+}
+
 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,
@@ -951,6 +1089,19 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
 	if (fuse_is_bad(dir))
 		return -EIO;
 
+	if (!fc->no_open_atomic && !fuse_open_flags_dropped(file, flags) &&
+	    (d_in_lookup(entry) || (flags & O_CREAT))) {
+		if (entry->d_name.len > fc->name_max)
+			return -ENAMETOOLONG;
+		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 c91af283aef20..0c7874c5da72b 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -50,6 +50,9 @@
 /** Number of dentries for each connection in the control filesystem */
 #define FUSE_CTL_NUM_DENTRIES 5
 
+/** Size of fuse_open_out without backing_id_64 */
+#define FUSE_COMPAT_OPEN_OUT_SIZE offsetofend(struct fuse_open_out, backing_id)
+
 /*
  * Dentries invalidation workqueue period, in seconds.  The value of this
  * parameter shall be >= FUSE_DENTRY_INVAL_FREQ_MIN seconds, or 0 (zero), in
@@ -617,6 +620,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 616430a2973c6..1fccd4f2f3921 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -257,6 +257,9 @@
  *  - add backing_id_64 to fuse_open_out
  *  - add FUSE_NOTIFY_BACKING_MAP, fuse_notify_backing_map_out, fuse_extent
  *  - add FUSE_BACKING_MAP_CREATE, FUSE_BACKING_MAP_CYCLIC
+ *
+ *  7.48
+ *  - add FUSE_OPEN_ATOMIC and FOPEN_CREATED
  */
 
 #ifndef _LINUX_FUSE_H
@@ -292,7 +295,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
@@ -406,6 +409,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)
@@ -416,6 +420,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
@@ -698,6 +703,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] 4+ messages in thread

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

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.

The LOOKUP is still sent for directories, for O_CREAT opens, and, with
default_permissions, when the cached attributes have expired, since
may_open() checks those attributes before fuse_open() runs. After
FUSE_OPEN_ATOMIC, fuse_open() checks permission again with the
attributes in the reply.

A reopen that does not look up the name, for example through
/proc/self/fd, also sends FUSE_OPEN_ATOMIC. If the name has changed,
the VFS retries and the retry opens the file by its nodeid.

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    | 348 +++++++++++++++++++++++++++++++++++++++--------
 fs/fuse/file.c   |  22 ++-
 fs/fuse/fuse_i.h |   1 +
 3 files changed, 308 insertions(+), 63 deletions(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 50dd6a07cc067..af749846c7093 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -379,6 +379,82 @@ 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;
+}
+
+/*
+ * An open of a regular file whose entry expired leaves the LOOKUP to
+ * fuse_open_revalidate().  With default_permissions, may_open() checks the
+ * cached attributes first, so they must still be valid.
+ */
+static bool fuse_open_skips_lookup(struct fuse_conn *fc, struct dentry *entry,
+				   struct inode *inode, unsigned int flags)
+{
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	u32 perm_mask = STATX_MODE | STATX_UID | STATX_GID;
+
+	if (fc->no_open_atomic || !(flags & LOOKUP_OPEN) ||
+	    (flags & (LOOKUP_CREATE | LOOKUP_REVAL | LOOKUP_RENAME_TARGET |
+		      LOOKUP_DIRECTORY)) ||
+	    !S_ISREG(inode->i_mode) || d_mountpoint(entry))
+		return false;
+
+	return !fc->default_permissions ||
+	       (!(perm_mask & READ_ONCE(fi->inval_mask)) &&
+		!time_before64(fi->i_time, get_jiffies_64()));
+}
+
 /*
  * Check whether the dentry is still valid
  *
@@ -392,7 +468,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 +482,20 @@ 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;
 
+		if (fuse_open_skips_lookup(fc, entry, inode, flags))
+			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 +505,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
 			fuse_advise_use_readdirplus(dir);
 		}
 	}
+valid:
 	ret = 1;
 out:
 	return ret;
@@ -816,27 +855,33 @@ static void free_ext_value(struct fuse_args *args)
 
 static int fuse_perm_getattr(struct inode *inode, int mask);
 
+/* The mask do_open() passes to may_open() */
+static int fuse_open_mask(struct file *file)
+{
+	int mask = MAY_OPEN;
+
+	if (file->f_flags & __FMODE_EXEC)
+		return mask | MAY_EXEC;
+
+	mask |= ACC_MODE(file->f_flags);
+	if (file->f_flags & O_TRUNC)
+		mask |= MAY_WRITE;
+	if (file->f_flags & O_APPEND)
+		mask |= MAY_APPEND;
+	return mask;
+}
+
 /*
- * Check the open against attributes the server just returned, with the mask
- * do_open() passes to may_open().  As in fuse_permission(), a denial
- * refreshes the attributes and checks again.  may_open() still runs the full
- * check.
+ * Check the open against attributes the server just returned.  As in
+ * fuse_permission(), a denial refreshes the attributes and checks again.
+ * may_open() still runs the full check.
  */
 static int fuse_may_open(struct file *file, struct inode *inode)
 {
 	struct mnt_idmap *idmap = file_mnt_idmap(file);
-	int mask = MAY_OPEN;
+	int mask = fuse_open_mask(file);
 	int err;
 
-	if (file->f_flags & __FMODE_EXEC) {
-		mask |= MAY_EXEC;
-	} else {
-		mask |= ACC_MODE(file->f_flags);
-		if (file->f_flags & O_TRUNC)
-			mask |= MAY_WRITE;
-		if (file->f_flags & O_APPEND)
-			mask |= MAY_APPEND;
-	}
 	err = generic_permission(idmap, inode, mask);
 	if (err == -EACCES) {
 		err = fuse_perm_getattr(inode, mask);
@@ -1064,6 +1109,195 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	return err;
 }
 
+/*
+ * Check the open again after fuse_open_revalidate() refreshed the attributes
+ * that may_open() checked.
+ */
+static int fuse_open_recheck(struct file *file, struct inode *inode)
+{
+	struct mnt_idmap *idmap = file_mnt_idmap(file);
+	int err;
+
+	err = inode_permission(idmap, inode, fuse_open_mask(file));
+	if (!err && (file->f_flags & O_NOATIME) &&
+	    !inode_owner_or_capable(idmap, inode))
+		err = -EPERM;
+	return err;
+}
+
+/*
+ * Open a regular file whose entry 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.
+ *
+ * fuse_dentry_revalidate() leaves the LOOKUP of such a file to this
+ * function.  An open that did not look the name up (e.g. through
+ * /proc/self/fd) checks the name as well; a mismatch drops the dentry, and
+ * the retry opens the unhashed dentry by nodeid.
+ */
+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, time;
+	unsigned int seq;
+	int err;
+
+	/* Backing files and unhashed names open by nodeid */
+	if (fc->no_open_atomic || (file->f_mode & FMODE_BACKING) ||
+	    IS_ROOT(entry) || d_unhashed(entry) || !S_ISREG(inode->i_mode))
+		return 1;
+	time = fuse_dentry_time(entry);
+	if (!time_before64(time, 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;
+	}
+
+	do {
+		seq = read_seqbegin(&rename_lock);
+		parent = dget_parent(entry);
+		take_dentry_name_snapshot(&name, entry);
+		if (!read_seqretry(&rename_lock, seq))
+			break;
+		release_dentry_name_snapshot(&name);
+		dput(parent);
+	} while (1);
+
+	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;
+	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);
+		release_dentry_name_snapshot(&name);
+		dput(parent);
+		fuse_file_free(ff);
+		kfree(forget);
+		if (!err) {
+			if (!read_seqretry(&rename_lock, seq))
+				d_drop(entry);
+			return -EOPENSTALE;
+		}
+		if (err > 0)
+			err = fuse_open_recheck(file, inode) ?: 1;
+		return err;
+	}
+	release_dentry_name_snapshot(&name);
+	dput(parent);
+
+	if (err == -ENOENT) {
+		fuse_file_free(ff);
+		kfree(forget);
+		err = -EOPENSTALE;
+		goto out_stale;
+	}
+	if (err < 0)
+		goto out_free;
+
+	if (args.out_args[1].size) {
+		ff->fh = outopenp->fh;
+		ff->nodeid = outentry.nodeid;
+	}
+
+	/* 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))
+		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 < FUSE_COMPAT_OPEN_OUT_SIZE ||
+	     (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);
+	/* Keep an invalidation that raced with the request */
+	if (time && fuse_dentry_time(entry) == time)
+		fuse_change_entry_timeout(entry, &outentry);
+
+	err = fuse_open_recheck(file, inode);
+	if (!args.out_args[1].size) {
+		fuse_file_free(ff);
+		return err ?: 1;
+	}
+	if (err) {
+		fuse_sync_release(fi, ff, file->f_flags);
+		return err;
+	}
+	ff->open_flags = outopenp->open_flags;
+	file->private_data = ff;
+	return 0;
+
+out_release:
+	if (args.out_args[1].size)
+		fuse_sync_release(NULL, ff, file->f_flags);
+	else
+		fuse_file_free(ff);
+	if (outentry.nodeid)
+		fuse_chan_queue_forget(fc->chan, forget, outentry.nodeid, 1);
+	else
+		kfree(forget);
+	if (err != -EOPENSTALE)
+		return err;
+out_stale:
+	if (!read_seqretry(&rename_lock, seq))
+		d_drop(entry);
+	return err;
+
+out_free:
+	fuse_file_free(ff);
+	kfree(forget);
+	return err;
+}
+
 /*
  * lookup_open() clears O_CREAT, and O_TRUNC without O_EXCL, when the open
  * cannot create or truncate; do_open() then reports the error.
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index eb5ae11b6010c..51da3d76606d2 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -257,7 +257,7 @@ static int fuse_open(struct inode *inode, struct file *file)
 	struct fuse_inode *fi = get_fuse_inode(inode);
 	struct fuse_conn *fc = fm->fc;
 	struct fuse_file *ff;
-	int err;
+	int err, need_open;
 	bool is_truncate = (file->f_flags & O_TRUNC) && fc->atomic_o_trunc;
 	bool is_wb_truncate = is_truncate && fc->writeback_cache;
 	bool vdax_truncate = is_truncate && FUSE_IS_VDAX(inode);
@@ -265,13 +265,22 @@ static int fuse_open(struct inode *inode, struct file *file)
 	if (fuse_is_bad(inode))
 		return -EIO;
 
+	/* The checks below see the attributes the revalidation refreshed */
+	need_open = fuse_open_revalidate(file);
+	if (need_open < 0)
+		return need_open;
+
 	/* No truncate for DAX yet */
 	if ((file->f_flags & O_TRUNC) && IS_DAX(inode) && !FUSE_IS_VDAX(inode))
-		return -EINVAL;
-
-	err = generic_file_open(inode, file);
-	if (err)
+		err = -EINVAL;
+	else
+		err = generic_file_open(inode, file);
+	if (err) {
+		if (!need_open)
+			fuse_sync_release(fi, file->private_data,
+					  file->f_flags);
 		return err;
+	}
 
 	if (is_wb_truncate || vdax_truncate)
 		inode_lock(inode);
@@ -286,7 +295,8 @@ 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);
+	if (need_open)
+		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 0c7874c5da72b..7ff13ebcb9c83 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -994,6 +994,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] 4+ messages in thread

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

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. Also check that an O_TMPFILE file has no name in
its directory, that a fuse_open_out without backing_id_64 is accepted,
that a name longer than name_max fails, that a reopen through
/proc/self/fd opens by nodeid, that an expired name sees a directory
replacing it and permissions the server revoked, and that O_CREAT |
O_EXCL without write access to the directory and O_TRUNC on a read-only
mount fail with EEXIST and EROFS.

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       | 645 ++++++++++++++++++
 3 files changed, 648 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 7c787313e69b6..faac6d969ef94 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 e3ae3acff1139..8390d6019513b 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_PROGS += fuse_acl_cache_test
 TEST_GEN_PROGS += fuse_symlink_cache_test
 TEST_GEN_PROGS += fuse_readdir_cache_test
@@ -12,6 +12,7 @@ TEST_GEN_FILES := fuse_mnt
 include ../../lib.mk
 
 $(OUTPUT)/write_extend_eof_test: LDLIBS += -lpthread
+$(OUTPUT)/atomic_open_test: LDLIBS += -lpthread
 
 VAR_CFLAGS := $(shell pkg-config fuse3 --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 0000000000000..075162a9e8804
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c
@@ -0,0 +1,645 @@
+// 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 <limits.h>
+#include <pthread.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.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;
+	uint64_t attr_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;
+	int nr_getattr;
+	/* Send fuse_open_out without backing_id_64, as older servers do */
+	int short_open;
+};
+
+static size_t open_out_size(struct server *s)
+{
+	if (s->short_open)
+		return offsetof(struct fuse_open_out, backing_id_64);
+	return sizeof(struct fuse_open_out);
+}
+
+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;
+	e->attr_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 = e->attr_valid;
+	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++;
+			else
+				s->nr_getattr++;
+			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;
+					out.attr_valid = eo.attr_valid;
+				}
+			}
+			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.entry) + open_out_size(s));
+			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,
+				.attr_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.entry) + open_out_size(s));
+			break;
+		}
+		case FUSE_OPEN: {
+			struct fuse_open_out out = {0};
+
+			s->nr_open++;
+			reply(s->fd, ih->unique, 0, &out, open_out_size(s));
+			break;
+		}
+		case FUSE_OPENDIR: {
+			struct fuse_open_out out = {0};
+
+			reply(s->fd, ih->unique, 0, &out, open_out_size(s));
+			break;
+		}
+		case FUSE_FLUSH:
+		case FUSE_RELEASE:
+		case FUSE_RELEASEDIR:
+			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)
+{
+	struct entry *e;
+	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->fd = -1;
+	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;
+	add(self->srv, "rodir", S_IFDIR | 0755);
+	add(self->srv, "shared", S_IFREG | 0666)->entry_valid = 0;
+	e = add(self->srv, "noac", S_IFREG | 0644);
+	e->entry_valid = 0;
+	e->attr_valid = 0;
+
+	self->srv->fd = open("/dev/fuse", O_RDWR);
+	if (self->srv->fd < 0 && (errno == ENOENT || errno == ENODEV))
+		SKIP(return, "no /dev/fuse");
+	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);
+	/* The detached server thread may still use self->srv */
+	if (self->srv && self->srv != MAP_FAILED && self->srv->fd >= 0)
+		close(self->srv->fd);
+	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)
+{
+	char link[64], path[PATH_MAX];
+	ssize_t len;
+	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);
+	snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+	len = readlink(link, path, sizeof(path) - 1);
+	ASSERT_GT(len, 0);
+	path[len] = '\0';
+	/* The tmpfile has no name in the directory */
+	EXPECT_NE(NULL, strstr(path, " (deleted)"));
+	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_F(fuse, short_open_out)
+{
+	int fd;
+
+	self->srv->short_open = 1;
+
+	fd = open_at(self->dir, "a", O_CREAT | O_WRONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	fd = open_at(self->dir, "rw", O_RDWR);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* The entry has expired, so the second open checks the name */
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+}
+
+TEST_F(fuse, name_too_long)
+{
+	char path[1200];
+	int len;
+
+	len = snprintf(path, sizeof(path), "%s/", self->dir);
+	memset(path + len, 'x', 1100);
+	path[len + 1100] = '\0';
+	ASSERT_EQ(-1, open(path, O_CREAT | O_WRONLY, 0644));
+	EXPECT_EQ(ENAMETOOLONG, errno);
+	EXPECT_EQ(0, self->srv->nr_open_atomic);
+	EXPECT_EQ(0, self->srv->nr_create);
+}
+
+TEST_F(fuse, reopen_by_fd)
+{
+	char link[64];
+	int fd, fd2;
+
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+
+	/*
+	 * The server renames the file: the reopen finds the name gone, and the
+	 * retry opens the file by nodeid
+	 */
+	strcpy(find(self->srv, "stale")->name, "moved");
+	snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
+	fd2 = open(link, O_RDONLY);
+	ASSERT_GE(fd2, 0);
+	close(fd2);
+	close(fd);
+
+	if (variant->atomic) {
+		EXPECT_EQ(2, self->srv->nr_open_atomic);
+		EXPECT_EQ(1, self->srv->nr_open);
+	}
+}
+
+TEST_F(fuse, open_directory_replaced)
+{
+	struct entry *e;
+	int fd;
+
+	fd = open_at(self->dir, "stale", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* The server replaces the file with a directory */
+	e = find(self->srv, "stale");
+	e->nodeid = 300;
+	e->mode = S_IFDIR | 0755;
+	fd = open_at(self->dir, "stale", O_RDONLY | O_DIRECTORY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+}
+
+TEST_F(fuse, open_permission_revoked)
+{
+	int fd;
+
+	fd = open_at(self->dir, "shared", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* The cached attributes still allow writing; the server's do not */
+	find(self->srv, "shared")->mode = S_IFREG | 0644;
+	EXPECT_EQ(EACCES, open_as_nobody(self->dir, "shared", O_WRONLY));
+}
+
+TEST_F(fuse, open_attrs_expired)
+{
+	int fd;
+
+	fd = open_at(self->dir, "noac", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+	fd = open_at(self->dir, "noac", O_RDONLY);
+	ASSERT_GE(fd, 0);
+	close(fd);
+
+	/* may_open() needs fresh attributes, so the reopen looks the name up */
+	if (variant->atomic) {
+		EXPECT_EQ(1, self->srv->nr_lookup);
+		EXPECT_EQ(0, self->srv->nr_open);
+	}
+}
+
+TEST_F(fuse, create_excl_no_write)
+{
+	/* Without write access to the directory, O_EXCL still sees the name */
+	EXPECT_EQ(EEXIST, open_as_nobody(self->dir, "rodir/rw",
+					 O_CREAT | O_EXCL | O_WRONLY));
+}
+
+TEST_F(fuse, trunc_read_only)
+{
+	ASSERT_EQ(0, mount(NULL, self->dir, NULL,
+			   MS_REMOUNT | MS_BIND | MS_RDONLY, NULL));
+	ASSERT_EQ(-1, open_at(self->dir, "rw", O_RDONLY | O_TRUNC));
+	EXPECT_EQ(EROFS, errno);
+	/* The server leaves the file unopened */
+	EXPECT_EQ(0, self->srv->nr_open_atomic);
+	EXPECT_EQ(0, self->srv->nr_open);
+}
+
+TEST_HARNESS_MAIN
-- 
2.50.1


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

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

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

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