* [PATCH v3 0/3] fuse: atomic open
@ 2026-10-26 19:07 Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 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 19:07 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 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.
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
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 | 456 +++++++++++--
fs/fuse/file.c | 4 +-
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 | 624 ++++++++++++++++++
7 files changed, 1043 insertions(+), 60 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 v3 1/3] fuse: add FUSE_OPEN_ATOMIC
2026-10-26 19:07 [PATCH v3 0/3] fuse: atomic open Jimmy Zuber
@ 2026-10-26 19:07 ` Jimmy Zuber
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
2 siblings, 0 replies; 4+ messages in thread
From: Jimmy Zuber @ 2026-10-26 19:07 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.
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC
2026-10-26 19:07 [PATCH v3 0/3] fuse: atomic open Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 1/3] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
@ 2026-10-26 19:07 ` Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 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 19:07 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.
Only an open that skipped the LOOKUP checks the name. Other opens of the
file, such as through /proc/self/fd, open the cached inode as before.
Directory opens keep the LOOKUP. With default_permissions, may_open()
checks the cached attributes before fuse_open(), so the LOOKUP is
skipped only while they are valid, and fuse_open() checks the open
again against the attributes in the reply.
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 | 303 ++++++++++++++++++++++++++++++++++++++++-------
fs/fuse/file.c | 4 +-
fs/fuse/fuse_i.h | 1 +
3 files changed, 264 insertions(+), 44 deletions(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 30cd42247dd4d..ac04e24ea73f5 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -97,6 +97,7 @@ static void fuse_advise_use_readdirplus(struct inode *dir)
struct fuse_dentry {
u64 time;
u64 epoch;
+ unsigned long flags;
union {
struct rcu_head rcu;
struct rb_node node;
@@ -104,6 +105,12 @@ struct fuse_dentry {
struct dentry *dentry;
};
+/* fuse_dentry flags */
+enum {
+ /* An open skipped LOOKUP, so fuse_open() checks the name */
+ FUSE_D_OPEN_REVAL,
+};
+
static void __fuse_dentry_tree_del_node(struct fuse_dentry *fd,
struct dentry_bucket *bucket)
{
@@ -379,6 +386,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_EXCL | 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 +475,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 +489,22 @@ 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)) {
+ set_bit(FUSE_D_OPEN_REVAL, &fd->flags);
+ goto valid;
+ }
+
ret = -ECHILD;
if (flags & LOOKUP_RCU)
goto out;
- fm = get_fuse_mount(inode);
-
- forget = fuse_alloc_forget();
- ret = -ENOMEM;
- if (!forget)
+ ret = fuse_revalidate_lookup(dir, name, entry);
+ if (ret <= 0)
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)
- 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 +514,7 @@ static int fuse_dentry_revalidate(struct inode *dir, const struct qstr *name,
fuse_advise_use_readdirplus(dir);
}
}
+valid:
ret = 1;
out:
return ret;
@@ -1051,6 +1099,175 @@ static int fuse_create_open(struct mnt_idmap *idmap, struct inode *dir,
return err;
}
+/*
+ * Open a regular file whose LOOKUP fuse_dentry_revalidate() skipped 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.
+ *
+ * The flag is per dentry, so a concurrent open that did not look the name up
+ * (e.g. through /proc/self/fd) can take it from an open that did. The latter
+ * then opens the cached inode, as for an entry that has not expired.
+ */
+int fuse_open_revalidate(struct file *file)
+{
+ struct dentry *entry = file->f_path.dentry;
+ struct fuse_dentry *fd = entry->d_fsdata;
+ 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;
+
+ if (!test_and_clear_bit(FUSE_D_OPEN_REVAL, &fd->flags))
+ 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 = fc->no_open_atomic ? -ENOSYS : 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;
+ } else if (err > 0 && fc->default_permissions) {
+ err = fuse_may_open(file, inode) ?: 1;
+ }
+ 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;
+ }
+
+ /* 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 < 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 (fuse_dentry_time(entry) == time)
+ fuse_change_entry_timeout(entry, &outentry);
+
+ /* may_open() checked the attributes from before the request */
+ err = 0;
+ if (fc->default_permissions)
+ err = fuse_may_open(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);
+ 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 eb5ae11b6010c..a79aec76ecbea 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -286,7 +286,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 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 v3 3/3] selftests/fuse: add test for FUSE_OPEN_ATOMIC
2026-10-26 19:07 [PATCH v3 0/3] fuse: atomic open Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 1/3] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 2/3] fuse: open cached files with FUSE_OPEN_ATOMIC Jimmy Zuber
@ 2026-10-26 19:07 ` Jimmy Zuber
2 siblings, 0 replies; 4+ messages in thread
From: Jimmy Zuber @ 2026-10-26 19:07 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, that opening an existing read-only file is still
permission checked, that an O_TMPFILE file has no name in its
directory, and that a fuse_open_out without backing_id_64 is accepted.
Also check that a name longer than name_max fails, that a reopen
through /proc/self/fd opens by nodeid, and that an expired name sees a
directory replacing it and permissions the server revoked.
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 | 624 ++++++++++++++++++
3 files changed, 627 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..50b82f41520e8
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c
@@ -0,0 +1,624 @@
+// 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, "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 opens it 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(1, 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_open_atomic);
+ EXPECT_EQ(1, self->srv->nr_lookup);
+ EXPECT_EQ(1, 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 19:09 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-26 19:07 [PATCH v3 0/3] fuse: atomic open Jimmy Zuber
2026-10-26 19:07 ` [PATCH v3 1/3] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox