FILESYSTEM IN USERSPACE (FUSE) development
 help / color / mirror / Atom feed
From: Jimmy Zuber <jamz@amazon.com>
To: Miklos Szeredi <miklos@szeredi.hu>
Cc: <fuse-devel@lists.linux.dev>, <linux-kselftest@vger.kernel.org>,
	"Shuah Khan" <shuah@kernel.org>,
	Bernd Schubert <bernd@bsbernd.com>,
	Jim Harris <jiharris@nvidia.com>,
	Horst Birthelmer <horst@birthelmer.de>,
	Luis Henriques <luis@igalia.com>, NeilBrown <neil@brown.name>
Subject: [PATCH v3 1/3] fuse: add FUSE_OPEN_ATOMIC
Date: Mon, 26 Oct 2026 19:07:45 +0000	[thread overview]
Message-ID: <20261026190747.2373775-2-jamz@amazon.com> (raw)
In-Reply-To: <20261026190747.2373775-1-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, 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.

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

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 4c64a4f812395..30cd42247dd4d 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -815,7 +815,66 @@ static void free_ext_value(struct fuse_args *args)
 }
 
 /*
- * Atomic create+open operation
+ * Check the open against attributes the server just returned, with the mask
+ * do_open() passes to may_open().  may_open() still runs the full check.
+ */
+static int fuse_may_open(struct file *file, struct inode *inode)
+{
+	int mask = MAY_OPEN;
+
+	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;
+	}
+	return generic_permission(file_mnt_idmap(file), inode, mask);
+}
+
+/*
+ * 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 +892,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 +943,58 @@ 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))
+			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 +1003,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:
@@ -951,6 +1064,18 @@ 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))) {
+		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


  reply	other threads:[~2026-10-26 19:08 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-26 19:07 [PATCH v3 0/3] fuse: atomic open Jimmy Zuber
2026-10-26 19:07 ` Jimmy Zuber [this message]
2026-10-26 19:07 ` [PATCH v3 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC Jimmy Zuber

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261026190747.2373775-2-jamz@amazon.com \
    --to=jamz@amazon.com \
    --cc=bernd@bsbernd.com \
    --cc=fuse-devel@lists.linux.dev \
    --cc=horst@birthelmer.de \
    --cc=jiharris@nvidia.com \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=luis@igalia.com \
    --cc=miklos@szeredi.hu \
    --cc=neil@brown.name \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox