Linux Kernel Selftest 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 v5 1/2] fuse: add FUSE_OPEN_ATOMIC
Date: Wed, 28 Oct 2026 20:04:58 +0000	[thread overview]
Message-ID: <20261028200459.2922951-2-jamz@amazon.com> (raw)
In-Reply-To: <20261028200459.2922951-1-jamz@amazon.com>

Add FUSE_OPEN_ATOMIC, which looks up a name, creates it for O_CREAT, and
opens it in one request, so that ->atomic_open() needs no FUSE_LOOKUP
before FUSE_OPEN or FUSE_CREATE. After a -ENOSYS reply the kernel sends
FUSE_LOOKUP, as it does for FUSE_CREATE.

The request carries fuse_create_in and the name. The server replies with
the entry and, if it opened the file, fuse_open_out:

- an existing regular file: the server opens it;
- a new name with O_CREAT: the server creates and opens it, and sets
  FOPEN_CREATED;
- any other file: the server returns the entry only, and the VFS opens
  it;
- a missing name without O_CREAT: the server returns a zero nodeid,
  cached like a negative LOOKUP.

The VFS checks the open after the reply, as for other ->atomic_open()
filesystems, and the kernel releases the file if the check fails.
FMODE_CREATED is set only when the server reports FOPEN_CREATED.

The request carries the open flags without O_TRUNC: the VFS truncates
the opened file with SETATTR after its checks. With FUSE_ATOMIC_O_TRUNC,
where FUSE_OPEN truncates, an O_TRUNC open keeps FUSE_LOOKUP.

Assisted-by: Claude:claude-opus-5-5 [Claude-Code]
Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
 fs/fuse/dir.c             | 185 ++++++++++++++++++++++++++++++++++++++
 fs/fuse/fuse_i.h          |   6 ++
 fs/fuse/fuse_trace.h      |   1 +
 include/uapi/linux/fuse.h |  20 ++++-
 4 files changed, 211 insertions(+), 1 deletion(-)

diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 4c64a4f812395..99014ac82ce49 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -938,6 +938,182 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
 	return err;
 }
 
+/* Release the file the server opened for a reply the kernel rejects */
+static void fuse_open_atomic_release(struct fuse_file *ff, u64 nodeid,
+				     unsigned int open_size,
+				     unsigned int flags)
+{
+	if (open_size >= FUSE_COMPAT_OPEN_OUT_SIZE && !invalid_nodeid(nodeid)) {
+		ff->fh = ff->args->open_outarg.fh;
+		ff->nodeid = nodeid;
+		fuse_sync_release(NULL, ff, flags & ~(O_CREAT | O_EXCL));
+	} else {
+		fuse_file_free(ff);
+	}
+}
+
+/*
+ * Look the name up, create it for O_CREAT, and open it in one request.  The
+ * server opens an existing regular file, returns only the entry of other
+ * files, and returns a zero nodeid for a missing name.  The VFS checks the
+ * open afterwards.  Returns 1 if the server replies -ENOSYS.
+ */
+static int fuse_open_atomic(struct mnt_idmap *idmap, struct inode *dir,
+			    struct dentry *entry, struct file *file,
+			    unsigned int flags, umode_t mode)
+{
+	struct fuse_mount *fm = get_fuse_mount(dir);
+	struct fuse_conn *fc = fm->fc;
+	FUSE_ARGS(args);
+	struct fuse_forget_link *forget;
+	struct fuse_create_in inarg;
+	struct fuse_open_out *outopen;
+	struct fuse_entry_out outentry;
+	struct inode *inode = NULL;
+	unsigned int open_size;
+	u64 attr_version, evict_ctr;
+	struct fuse_file *ff;
+	struct dentry *res;
+	bool locked = false;
+	int epoch, err;
+
+	if (entry->d_name.len > fc->name_max)
+		return -ENAMETOOLONG;
+	forget = fuse_alloc_forget();
+	ff = fuse_file_alloc(fm, true);
+	if (!forget || !ff) {
+		kfree(forget);
+		if (ff)
+			fuse_file_free(ff);
+		return -ENOMEM;
+	}
+	outopen = &ff->args->open_outarg;
+	if (!fc->dont_mask)
+		mode &= ~current_umask();
+
+	/* The VFS truncates the file after its checks */
+	memset(&inarg, 0, sizeof(inarg));
+	memset(&outentry, 0, sizeof(outentry));
+	inarg.flags = flags & ~(O_NOCTTY | O_TRUNC);
+	inarg.mode = mode;
+	inarg.umask = current_umask();
+	args.opcode = FUSE_OPEN_ATOMIC;
+	args.nodeid = get_node_id(dir);
+	args.in_numargs = 2;
+	args.in_args[0].size = sizeof(inarg);
+	args.in_args[0].value = &inarg;
+	args.in_args[1].size = entry->d_name.len + 1;
+	args.in_args[1].value = entry->d_name.name;
+	args.out_numargs = 2;
+	args.out_args[0].size = sizeof(outentry);
+	args.out_args[0].value = &outentry;
+	args.out_argvar = true;
+	args.out_args[1].size = sizeof(*outopen);
+	args.out_args[1].value = outopen;
+
+	if (flags & O_CREAT) {
+		err = get_create_ext(idmap, &args, dir, entry, mode);
+		if (err)
+			goto out_free;
+	} else {
+		/* Locked as for a lookup */
+		idmap = &invalid_mnt_idmap;
+		locked = fuse_lock_inode(dir);
+	}
+	epoch = atomic_read(&fc->epoch);
+	attr_version = fuse_get_attr_version(fc);
+	evict_ctr = fuse_get_evict_ctr(fc);
+	err = fuse_simple_idmap_request(idmap, fm, &args);
+	fuse_unlock_inode(dir, locked);
+	free_ext_value(&args);
+	if (err < 0) {
+		if (err == -ENOSYS) {
+			fc->no_open_atomic = 1;
+			err = 1;
+		} else if (err == -EEXIST) {
+			fuse_invalidate_entry(entry);
+		}
+		goto out_free;
+	}
+
+	open_size = args.out_args[1].size;
+	if (open_size && (outopen->open_flags & FOPEN_CREATED))
+		fuse_dir_changed(dir);
+	err = -EIO;
+	if (outentry.nodeid && (invalid_nodeid(outentry.nodeid) ||
+				fuse_invalid_attr(&outentry.attr)))
+		goto out_reject;
+	/* A zero nodeid: the name does not exist and nothing was opened */
+	if (!outentry.nodeid && (open_size || (flags & O_CREAT)))
+		goto out_reject;
+	if (outentry.nodeid) {
+		inode = fuse_iget(dir->i_sb, outentry.nodeid,
+				  outentry.generation, &outentry.attr,
+				  ATTR_TIMEOUT(&outentry), attr_version,
+				  evict_ctr);
+		err = -ENOMEM;
+		if (!inode)
+			goto out_reject;
+		fuse_advise_use_readdirplus(dir);
+	}
+	kfree(forget);
+
+	if (!open_size) {
+		fuse_file_free(ff);
+		res = d_splice_alias(inode, entry);
+		if (!IS_ERR(res)) {
+			fuse_dentry_set_epoch(res ?: entry, epoch);
+			fuse_change_entry_timeout(res ?: entry, &outentry);
+		}
+		return finish_no_open(file, res);
+	}
+
+	if (open_size < FUSE_COMPAT_OPEN_OUT_SIZE ||
+	    !S_ISREG(inode->i_mode) || fuse_is_bad(inode)) {
+		fuse_open_atomic_release(ff, outentry.nodeid, open_size,
+					 inarg.flags);
+		iput(inode);
+		return -EIO;
+	}
+	if ((flags & O_CREAT) && (outopen->open_flags & FOPEN_CREATED))
+		file->f_mode |= FMODE_CREATED;
+	ff->fh = outopen->fh;
+	ff->nodeid = outentry.nodeid;
+	ff->open_flags = outopen->open_flags & ~FOPEN_CREATED;
+	d_splice_alias(inode, entry);
+	fuse_dentry_set_epoch(entry, epoch);
+	fuse_change_entry_timeout(entry, &outentry);
+
+	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))
+			fuse_sync_release(get_fuse_inode(inode), ff,
+					  inarg.flags & ~(O_CREAT | O_EXCL));
+	} else if (!(ff->open_flags & FOPEN_KEEP_CACHE) && !IS_DAX(inode)) {
+		invalidate_inode_pages2(inode->i_mapping);
+	}
+	return err;
+
+out_reject:
+	fuse_open_atomic_release(ff, outentry.nodeid, open_size,
+				 inarg.flags);
+	if (!invalid_nodeid(outentry.nodeid))
+		fuse_chan_queue_forget(fc->chan, forget, outentry.nodeid, 1);
+	else
+		kfree(forget);
+	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,
@@ -951,6 +1127,15 @@ static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
 	if (fuse_is_bad(dir))
 		return -EIO;
 
+	/* With atomic_o_trunc, FUSE_OPEN truncates after a FUSE_LOOKUP */
+	if (!fc->no_open_atomic && !((flags & O_TRUNC) && fc->atomic_o_trunc) &&
+	    (d_in_lookup(entry) || (flags & O_CREAT))) {
+		err = fuse_open_atomic(idmap, dir, entry, file, flags,
+				       mode | S_IFREG);
+		if (err <= 0)
+			return err;
+	}
+
 	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..c680e106bf77a 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 FUSE_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/fs/fuse/fuse_trace.h b/fs/fuse/fuse_trace.h
index 60baa10bbcb99..54d7a1891c382 100644
--- a/fs/fuse/fuse_trace.h
+++ b/fs/fuse/fuse_trace.h
@@ -58,6 +58,7 @@
 	EM( FUSE_SYNCFS,		"FUSE_SYNCFS")		\
 	EM( FUSE_TMPFILE,		"FUSE_TMPFILE")		\
 	EM( FUSE_STATX,			"FUSE_STATX")		\
+	EM( FUSE_OPEN_ATOMIC,		"FUSE_OPEN_ATOMIC")	\
 	EMe(CUSE_INIT,			"CUSE_INIT")
 
 /*
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 616430a2973c6..2986b247db615 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
@@ -699,6 +704,19 @@ enum fuse_opcode {
 	FUSE_STATX		= 52,
 	FUSE_COPY_FILE_RANGE_64	= 53,
 
+	/*
+	 * FUSE_OPEN_ATOMIC looks up a name, creates it for O_CREAT, and opens
+	 * it. The request carries fuse_create_in, without O_TRUNC, and the
+	 * name. The reply is fuse_entry_out, followed by fuse_open_out if the
+	 * server opened the file, with FOPEN_CREATED if it created it. The
+	 * server opens an existing regular file, and returns the entry only
+	 * for other files, -EEXIST for an existing name with O_CREAT | O_EXCL,
+	 * and a zero nodeid for a missing name without O_CREAT. The kernel
+	 * checks the open after the reply and releases the file if the check
+	 * fails. After a -ENOSYS reply the kernel sends FUSE_LOOKUP instead.
+	 */
+	FUSE_OPEN_ATOMIC	= 54,
+
 	/* CUSE specific operations */
 	CUSE_INIT		= 4096,
 
-- 
2.50.1


  reply	other threads:[~2026-10-28 20:05 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28 20:04 [PATCH v5 0/2] fuse: atomic open Jimmy Zuber
2026-10-28 20:04 ` Jimmy Zuber [this message]
2026-10-28 20:04 ` [PATCH v5 2/2] 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=20261028200459.2922951-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