* [PATCH v7 0/2] fuse: atomic open
@ 2026-11-10 15:52 Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 1/2] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 2/2] selftests/fuse: add test for FUSE_OPEN_ATOMIC Jimmy Zuber
0 siblings, 2 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-11-10 15:52 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. FUSE_OPEN_ATOMIC is FUSE_LOOKUP plus FUSE_OPEN or
FUSE_CREATE in one request. With FUSE_OPEN_ATOMIC, the kernel opens a
name that is not in the dcache with one request to the server. This
greatly improves performance for file systems where the bottleneck is
calls between the kernel and the server. Servers that don't support
FUSE_OPEN_ATOMIC are handled gracefully.
The kernel sends FUSE_OPEN_ATOMIC from ->atomic_open(). For a name with
a valid positive dentry, the kernel sends FUSE_OPEN. FUSE_OPEN_ATOMIC
for that would need a VFS change.
If the server opens an existing file, the VFS checks permissions after
the reply, as on other filesystems with ->atomic_open(). If open() fails
after a valid reply with fuse_open_out, the kernel sends FUSE_RELEASE.
The kernel clears O_TRUNC in FUSE_OPEN_ATOMIC, and follows up with a
SETATTR, so it will never truncate files before permission checks.
Earlier series also proposed one request for lookup and open in FUSE
[1][2]. This series covers only ->atomic_open(), with a single new
request and no VFS change.
Note on tmpfile: FUSE_TMPFILE shares fuse_create_open() and its
d_instantiate() with FUSE_CREATE [3], and this series changes
only ->atomic_open() (NeilBrown's question [4]).
This series applies on top of "fuse: fix use-after-free in
fuse_create_open() error path" [5].
I used Claude Code and Sashiko to help write and review the code.
I reviewed and tested every change. Tested with a new selftest under
UML and KVM.
Changes in v7:
- Reword the commit messages and comments after NeilBrown's review of
v6 [6]: the server can open an existing regular file or return only
fuse_entry_out, and it opens each file that it creates.
- Test an existing regular file with a reply of only fuse_entry_out.
- Rebase onto fuse for-next at 7.4-rc1.
Changes in v6 (from the Sashiko review of v5 [7]):
- Send FUSE_LOOKUP for O_EXCL without O_CREAT, so do_open() returns
-EEXIST for an existing name that the caller may not create.
- With FUSE_ATOMIC_O_TRUNC, check O_TRUNC in file->f_flags. On a
read-only mount, the VFS clears O_TRUNC in the flags argument, and
file->f_flags contains O_TRUNC.
- Before fuse_iget(), reject fuse_open_out for a file that is not a
regular file.
- Test read-only mounts.
Changes in v2-v5: see the earlier versions below.
v6: https://lore.kernel.org/fuse-devel/20261029130758.818617-1-jamz@amazon.com/
v5: https://lore.kernel.org/fuse-devel/20261028200459.2922951-1-jamz@amazon.com/
v4: https://lore.kernel.org/fuse-devel/20261026221639.628325-1-jamz@amazon.com/
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] Dharmendra Singh, "FUSE: Implement atomic lookup + open/create"
https://lore.kernel.org/all/20220517100744.26849-1-dharamhans87@gmail.com/
[2] Bernd Schubert, "fuse: full atomic open and atomic-open-revalidate"
https://lore.kernel.org/all/20231023183035.11035-1-bschubert@ddn.com/
[3] https://lore.kernel.org/all/20220922084442.2401223-11-mszeredi@redhat.com/
[4] https://lore.kernel.org/fuse-devel/179264679002.1724252.4479504779598097455@noble.neil.brown.name/
[5] https://lore.kernel.org/fuse-devel/20261026180649.1265825-1-jamz@amazon.com/
[6] https://lore.kernel.org/fuse-devel/179332437497.269288.7305999595462479305@noble.neil.brown.name/
[7] https://sashiko.dev/#/patchset/20261028200459.2922951-1-jamz@amazon.com
Jimmy Zuber (2):
fuse: add FUSE_OPEN_ATOMIC
selftests/fuse: add test for FUSE_OPEN_ATOMIC
fs/fuse/dir.c | 193 ++++++
fs/fuse/fuse_i.h | 6 +
fs/fuse/fuse_trace.h | 1 +
include/uapi/linux/fuse.h | 20 +-
.../selftests/filesystems/fuse/.gitignore | 1 +
.../selftests/filesystems/fuse/Makefile | 3 +-
.../filesystems/fuse/atomic_open_test.c | 583 ++++++++++++++++++
7 files changed, 805 insertions(+), 2 deletions(-)
create mode 100644 tools/testing/selftests/filesystems/fuse/atomic_open_test.c
base-commit: ab9787d7e66ac7e6e94fc975b37c5b56fff98ca9
prerequisite-patch-id: 95a6559bc4490d7d82bc80ba961ac52e80429b34
--
2.50.1
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v7 1/2] fuse: add FUSE_OPEN_ATOMIC
2026-11-10 15:52 [PATCH v7 0/2] fuse: atomic open Jimmy Zuber
@ 2026-11-10 15:52 ` Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 2/2] selftests/fuse: add test for FUSE_OPEN_ATOMIC Jimmy Zuber
1 sibling, 0 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-11-10 15:52 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. FUSE_OPEN_ATOMIC is FUSE_LOOKUP plus FUSE_OPEN or
FUSE_CREATE in one request. With FUSE_OPEN_ATOMIC, the kernel opens a
name that is not in the dcache with one request to the server. This
greatly improves performance for file systems where the bottleneck is
calls between the kernel and the server. Servers that don't support
FUSE_OPEN_ATOMIC are handled gracefully.
After a -ENOSYS reply to FUSE_OPEN_ATOMIC, every open() on the
connection uses FUSE_LOOKUP, FUSE_OPEN and FUSE_CREATE, including the
open() that received -ENOSYS.
The kernel sends FUSE_OPEN_ATOMIC from ->atomic_open(). The VFS calls
->atomic_open() for a name that is not in the dcache, and for open()
with O_CREAT on a negative dentry. For a name with a valid positive
dentry, the kernel sends FUSE_OPEN. FUSE_OPEN_ATOMIC for a name with a
valid positive dentry would need a VFS change.
If the server opens an existing file, the VFS checks permissions after
the reply, as on other filesystems with ->atomic_open(). If open() fails
after a valid reply with fuse_open_out, the kernel sends FUSE_RELEASE.
With default_permissions, the server can receive FUSE_OPEN_ATOMIC and
FUSE_RELEASE for a file that the caller may not open.
The kernel clears O_TRUNC in fuse_create_in.flags, so that the server
truncates a file only after the VFS checks permissions. After the VFS
checks permissions, the kernel sends FUSE_SETATTR to truncate an
existing file. A server with FUSE_ATOMIC_O_TRUNC truncates the file in
FUSE_OPEN. For open() with O_TRUNC on a server with FUSE_ATOMIC_O_TRUNC,
the kernel sends FUSE_OPEN or FUSE_CREATE after the VFS checks
permissions, with FUSE_LOOKUP first for a name that is not in the
dcache.
The request contains fuse_create_in and the name. The reply contains
fuse_entry_out, and fuse_open_out if the server opened the file. The
server replies as follows:
- O_CREAT | O_EXCL on an existing name: the server returns -EEXIST.
- An existing regular file, with or without O_CREAT: the server can
open the file and return fuse_open_out. The server can also return
only fuse_entry_out, and the kernel sends FUSE_OPEN after the VFS
checks permissions.
- An existing directory, symlink or special file: the server must
return only fuse_entry_out. The kernel returns -EIO for fuse_open_out
on any file except a regular file.
- A missing name with O_CREAT: the server creates a regular file, opens
it, and sets FOPEN_CREATED.
- A missing name without O_CREAT: the server returns a zero nodeid. The
kernel caches a negative dentry, as it does for FUSE_LOOKUP.
The server sets FOPEN_CREATED exactly when it created the file. For
FOPEN_CREATED, the kernel sets FMODE_CREATED, and do_open() passes an
acc_mode of 0 to may_open(), as for any file that open() creates.
For O_CREAT | O_EXCL, when the caller may not create the file, for
example on a read-only mount, the VFS clears O_CREAT and leaves O_EXCL.
For O_EXCL without O_CREAT, the kernel sends FUSE_LOOKUP. For an
existing name, do_open() returns -EEXIST, and the server receives only
FUSE_LOOKUP.
Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
fs/fuse/dir.c | 193 ++++++++++++++++++++++++++++++++++++++
fs/fuse/fuse_i.h | 6 ++
fs/fuse/fuse_trace.h | 1 +
include/uapi/linux/fuse.h | 20 +++-
4 files changed, 219 insertions(+), 1 deletion(-)
diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c
index 8a8177a178a20..b6862920f41bc 100644
--- a/fs/fuse/dir.c
+++ b/fs/fuse/dir.c
@@ -938,6 +938,185 @@ static int fuse_create_open(const 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);
+ }
+}
+
+/*
+ * Send FUSE_LOOKUP plus FUSE_OPEN or FUSE_CREATE as one FUSE_OPEN_ATOMIC
+ * request. The server can open an existing regular file, or return only
+ * fuse_entry_out. Without O_CREAT, a missing name gets a zero nodeid.
+ * The VFS checks permissions after the reply. Returns 1 on -ENOSYS.
+ */
+static int fuse_open_atomic(const 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 after may_open() */
+ 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 FUSE_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;
+ /* Only a regular file is opened */
+ if (open_size && (open_size < FUSE_COMPAT_OPEN_OUT_SIZE ||
+ !S_ISREG(outentry.attr.mode)))
+ 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 (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(const struct mnt_idmap *, struct inode *, struct dentry *,
umode_t, dev_t);
static int fuse_atomic_open(struct inode *dir, struct dentry *entry,
@@ -951,6 +1130,20 @@ 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. O_EXCL
+ * without O_CREAT can mean the VFS cleared O_CREAT: with FUSE_LOOKUP,
+ * do_open() fails with -EEXIST and the server sees only FUSE_LOOKUP.
+ */
+ if (!fc->no_open_atomic &&
+ !((file->f_flags & O_TRUNC) && fc->atomic_o_trunc) &&
+ ((flags & O_CREAT) || (d_in_lookup(entry) && !(flags & O_EXCL)))) {
+ 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 a033b0bc45762..05c2da7f86670 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
@@ -618,6 +621,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..ae9b051c1cd80 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: the server created the file for FUSE_OPEN_ATOMIC
*/
#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 is FUSE_LOOKUP plus FUSE_OPEN or FUSE_CREATE in one
+ * request. The request is fuse_create_in, with O_TRUNC cleared, and the
+ * name. The reply is fuse_entry_out, plus fuse_open_out if the server
+ * opened the file. The server can open an existing regular file, and no
+ * other file type. With O_CREAT, the server creates and opens a regular
+ * file for a missing name and sets FOPEN_CREATED; an existing name with
+ * O_EXCL gets -EEXIST. Without O_CREAT, a missing name gets a zero
+ * nodeid. If open() fails after a valid reply with fuse_open_out, the
+ * kernel sends FUSE_RELEASE.
+ */
+ FUSE_OPEN_ATOMIC = 54,
+
/* CUSE specific operations */
CUSE_INIT = 4096,
--
2.50.1
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH v7 2/2] selftests/fuse: add test for FUSE_OPEN_ATOMIC
2026-11-10 15:52 [PATCH v7 0/2] fuse: atomic open Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 1/2] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
@ 2026-11-10 15:52 ` Jimmy Zuber
1 sibling, 0 replies; 3+ messages in thread
From: Jimmy Zuber @ 2026-11-10 15:52 UTC (permalink / raw)
To: Miklos Szeredi
Cc: fuse-devel, linux-kselftest, Shuah Khan, Bernd Schubert,
Jim Harris, Horst Birthelmer, Luis Henriques, NeilBrown
Add atomic_open_test. atomic_open_test contains a FUSE server and
several test cases. The server reads requests from /dev/fuse and counts
the requests that the kernel sends for open().
Each test case checks the result of open(). Most test cases also check
the requests that the kernel sent. The test cases cover:
- open() with O_CREAT on a missing name
- open() with O_CREAT | O_EXCL on an existing name
- open() of an existing file, of a missing name, and of a directory
- open() of an existing file, with a reply of only fuse_entry_out
- open() with O_TRUNC, with and without O_CREAT
- open() with O_CREAT | O_EXCL and with O_TRUNC on a read-only mount
- open() of a file that the server opens and the VFS denies
- fuse_open_out without backing_id_64
- fuse_open_out with a zero nodeid, which the kernel rejects
- a name longer than name_max
- FUSE_LOOKUP after a -ENOSYS reply
Each test case runs with three server configurations:
- the server replies -ENOSYS to FUSE_OPEN_ATOMIC
- the server implements FUSE_OPEN_ATOMIC
- the server implements FUSE_OPEN_ATOMIC and FUSE_ATOMIC_O_TRUNC
Each test case runs in its own mount namespace. Each test case skips
when the kernel's FUSE protocol version is below 7.48, or when the
system refuses /dev/fuse or the fuse mount.
Signed-off-by: Jimmy Zuber <jamz@amazon.com>
---
.../selftests/filesystems/fuse/.gitignore | 1 +
.../selftests/filesystems/fuse/Makefile | 3 +-
.../filesystems/fuse/atomic_open_test.c | 583 ++++++++++++++++++
3 files changed, 586 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..d4b3fdba2a337
--- /dev/null
+++ b/tools/testing/selftests/filesystems/fuse/atomic_open_test.c
@@ -0,0 +1,583 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the requests that open() sends with and without FUSE_OPEN_ATOMIC,
+ * against a raw /dev/fuse server.
+ *
+ * Requires root to mount fuse.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pthread.h>
+#include <sched.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 MAX_WRITE (128 * 1024)
+#define MAX_ENTRIES 16
+#define NOBODY 65534
+
+struct entry {
+ uint64_t parent;
+ char name[64];
+ uint64_t nodeid;
+ uint32_t mode;
+};
+
+/* Server state, shared with the server thread */
+struct server {
+ int fd;
+ int atomic, atomic_o_trunc;
+ /* Protocol minor version of the kernel, from FUSE_INIT */
+ uint32_t minor;
+ struct entry ent[MAX_ENTRIES];
+ int nent;
+ int nr_lookup, nr_create, nr_open, nr_open_atomic, nr_setattr;
+ int nr_enosys;
+ /* Requests that carried O_TRUNC */
+ int nr_trunc;
+ /* Send fuse_open_out without backing_id_64, as older servers do */
+ int short_open;
+ /* Reply to FUSE_OPEN_ATOMIC with an opened file and a zero nodeid */
+ int open_no_nodeid;
+ /* Reply with only fuse_entry_out for an existing file */
+ int entry_only;
+};
+
+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) */
+ if (writev(fd, iov, data ? 2 : 1) < 0)
+ return;
+}
+
+static struct entry *find_in(struct server *s, uint64_t parent,
+ const char *name)
+{
+ int i;
+
+ for (i = 0; i < s->nent; i++)
+ if (s->ent[i].parent == parent && !strcmp(s->ent[i].name, name))
+ return &s->ent[i];
+ return NULL;
+}
+
+static struct entry *add_in(struct server *s, uint64_t parent,
+ const char *name, uint32_t mode)
+{
+ struct entry *e = &s->ent[s->nent];
+
+ if (s->nent >= MAX_ENTRIES)
+ return NULL;
+ e->parent = parent;
+ snprintf(e->name, sizeof(e->name), "%.63s", name);
+ e->nodeid = FUSE_ROOT_ID + 1 + s->nent++;
+ e->mode = mode;
+ return e;
+}
+
+static struct entry *add(struct server *s, const char *name, uint32_t mode)
+{
+ return add_in(s, FUSE_ROOT_ID, name, mode);
+}
+
+/* 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 = 3600;
+ if (!e)
+ return;
+ out->nodeid = e->nodeid;
+ out->attr_valid = 3600;
+ out->attr.ino = e->nodeid;
+ out->attr.mode = e->mode;
+ out->attr.nlink = S_ISDIR(e->mode) ? 2 : 1;
+ out->attr.blksize = 4096;
+}
+
+static void do_open_atomic(struct server *s, struct fuse_in_header *ih)
+{
+ struct fuse_create_in *in = (void *)(ih + 1);
+ const char *name = (char *)(in + 1);
+ struct entry *e = find_in(s, ih->nodeid, name);
+ struct {
+ struct fuse_entry_out entry;
+ struct fuse_open_out open;
+ } out = {0};
+ size_t len = sizeof(out.entry) + open_out_size(s);
+
+ if (ih->opcode == FUSE_OPEN_ATOMIC && !s->atomic) {
+ s->nr_enosys++;
+ return reply(s->fd, ih->unique, -ENOSYS, NULL, 0);
+ }
+ if (ih->opcode == FUSE_CREATE)
+ s->nr_create++;
+ else
+ s->nr_open_atomic++;
+ if (in->flags & O_TRUNC)
+ s->nr_trunc++;
+ if (e && (in->flags & O_CREAT) && (in->flags & O_EXCL))
+ return reply(s->fd, ih->unique, -EEXIST, NULL, 0);
+ if (e && !(in->flags & O_CREAT) && s->open_no_nodeid) {
+ fill_entry(&out.entry, NULL);
+ return reply(s->fd, ih->unique, 0, &out, len);
+ }
+ if (!e && !(in->flags & O_CREAT)) {
+ fill_entry(&out.entry, NULL);
+ return reply(s->fd, ih->unique, 0, &out.entry,
+ sizeof(out.entry));
+ }
+ if (e && (!S_ISREG(e->mode) || s->entry_only)) {
+ fill_entry(&out.entry, e);
+ return reply(s->fd, ih->unique, 0, &out.entry,
+ sizeof(out.entry));
+ }
+ if (!e) {
+ e = add_in(s, ih->nodeid, name, S_IFREG | in->mode);
+ if (!e)
+ return reply(s->fd, ih->unique, -ENOSPC, NULL, 0);
+ if (ih->opcode == FUSE_OPEN_ATOMIC)
+ out.open.open_flags = FOPEN_CREATED;
+ }
+ fill_entry(&out.entry, e);
+ reply(s->fd, ih->unique, 0, &out, len);
+}
+
+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};
+
+ s->minor = in->minor;
+ 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;
+ if (s->atomic_o_trunc)
+ out.flags = FUSE_ATOMIC_O_TRUNC;
+ reply(s->fd, ih->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_GETATTR:
+ case FUSE_SETATTR: {
+ struct fuse_attr_out out = {0};
+ struct fuse_entry_out eo;
+ int i;
+
+ if (ih->opcode == FUSE_SETATTR)
+ s->nr_setattr++;
+ out.attr_valid = 3600;
+ 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;
+ struct entry *e;
+
+ s->nr_lookup++;
+ e = find_in(s, ih->nodeid, (char *)(ih + 1));
+ fill_entry(&out, e);
+ reply(s->fd, ih->unique, 0, &out, sizeof(out));
+ break;
+ }
+ case FUSE_CREATE:
+ case FUSE_OPEN_ATOMIC:
+ do_open_atomic(s, ih);
+ break;
+ case FUSE_OPEN: {
+ struct fuse_open_in *in = (void *)(ih + 1);
+ struct fuse_open_out out = {0};
+
+ s->nr_open++;
+ if (in->flags & O_TRUNC)
+ s->nr_trunc++;
+ 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_RELEASE:
+ case FUSE_FLUSH:
+ 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, -ENOSYS, NULL, 0);
+ break;
+ }
+ }
+}
+
+FIXTURE(fuse)
+{
+ struct server *srv;
+ char dir[64];
+ int mounted;
+};
+
+FIXTURE_VARIANT(fuse)
+{
+ int atomic;
+ int atomic_o_trunc;
+};
+
+FIXTURE_VARIANT_ADD(fuse, atomic)
+{
+ .atomic = 1,
+};
+
+FIXTURE_VARIANT_ADD(fuse, atomic_trunc)
+{
+ .atomic = 1,
+ .atomic_o_trunc = 1,
+};
+
+FIXTURE_VARIANT_ADD(fuse, plain)
+{
+ .atomic = 0,
+};
+
+static void cleanup(FIXTURE_DATA(fuse) *self)
+{
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ self->mounted = 0;
+ /* The detached server thread can outlive cleanup() */
+ if (self->srv && self->srv != MAP_FAILED && self->srv->fd >= 0)
+ close(self->srv->fd);
+ if (self->srv && self->srv != MAP_FAILED)
+ self->srv->fd = -1;
+ if (self->dir[0])
+ rmdir(self->dir);
+ self->dir[0] = '\0';
+}
+
+FIXTURE_SETUP(fuse)
+{
+ char opts[128];
+ struct stat st;
+ pthread_t t;
+ int ret, err;
+
+ if (geteuid() != 0)
+ SKIP(return, "need root to mount fuse");
+
+ /* Mounts made by the test go away with its mount namespace */
+ if (unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ SKIP(return, "no private mount namespace: %s", strerror(errno));
+
+ 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;
+ self->srv->atomic_o_trunc = variant->atomic_o_trunc;
+ add(self->srv, "ro", S_IFREG | 0444);
+ add(self->srv, "rw", S_IFREG | 0644);
+ add(self->srv, "dir", S_IFDIR | 0755);
+
+ self->srv->fd = open("/dev/fuse", O_RDWR);
+ if (self->srv->fd < 0) {
+ err = errno;
+ cleanup(self);
+ SKIP(return, "cannot open /dev/fuse: %s", strerror(err));
+ }
+
+ strcpy(self->dir, "/tmp/fuse_aopen_XXXXXX");
+ if (!mkdtemp(self->dir)) {
+ self->dir[0] = '\0';
+ cleanup(self);
+ }
+ ASSERT_NE('\0', self->dir[0]);
+
+ snprintf(opts, sizeof(opts),
+ "fd=%d,rootmode=40000,user_id=0,group_id=0,default_permissions,allow_other",
+ self->srv->fd);
+ ret = mount("fuse", self->dir, "fuse", 0, opts);
+ err = errno;
+ if (ret)
+ cleanup(self);
+ if (ret && (err == EPERM || err == EACCES || err == ENODEV))
+ SKIP(return, "cannot mount fuse: %s", strerror(err));
+ ASSERT_EQ(0, ret);
+ self->mounted = 1;
+
+ ret = pthread_create(&t, NULL, server_thread, self->srv);
+ if (!ret) {
+ pthread_detach(t);
+ /* Wait for FUSE_INIT: the root GETATTR is queued behind it */
+ ret = stat(self->dir, &st);
+ }
+ /* The harness skips teardown when setup fails or skips */
+ if (ret || self->srv->minor < 48)
+ cleanup(self);
+ ASSERT_EQ(0, ret);
+ if (self->srv->minor < 48)
+ SKIP(return, "kernel without FUSE_OPEN_ATOMIC (protocol 7.48)");
+}
+
+FIXTURE_TEARDOWN(fuse)
+{
+ cleanup(self);
+}
+
+/* Open @name as an unprivileged user and return the errno, or 0 */
+static int open_as_nobody(struct __test_metadata *_metadata, 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();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ if (setgroups(0, NULL) || setgid(NOBODY) || setuid(NOBODY))
+ _exit(255);
+ fd = open(path, flags, 0644);
+ _exit(fd < 0 ? errno : 0);
+ }
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ /* 255: the child could not drop its privileges */
+ ASSERT_NE(255, WEXITSTATUS(status));
+ 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);
+}
+
+static void open_close(struct __test_metadata *_metadata, const char *dir,
+ const char *name, int flags)
+{
+ int fd = open_at(dir, name, flags);
+
+ ASSERT_GE(fd, 0);
+ close(fd);
+}
+
+TEST_F(fuse, create_new)
+{
+ open_close(_metadata, self->dir, "a", O_CREAT | O_WRONLY);
+ open_close(_metadata, self->dir, "b", O_CREAT | O_EXCL | O_WRONLY);
+
+ EXPECT_EQ(0, self->srv->nr_open);
+ EXPECT_EQ(variant->atomic ? 2 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 0 : 2, self->srv->nr_lookup);
+ EXPECT_EQ(variant->atomic ? 0 : 2, self->srv->nr_create);
+}
+
+TEST_F(fuse, create_existing)
+{
+ ASSERT_EQ(-1, open_at(self->dir, "ro", O_CREAT | O_EXCL | O_WRONLY));
+ EXPECT_EQ(EEXIST, errno);
+ /* may_open() runs after the reply */
+ EXPECT_EQ(EACCES, open_as_nobody(_metadata, self->dir, "ro",
+ O_CREAT | O_RDWR));
+ EXPECT_EQ(0, self->srv->nr_create);
+}
+
+TEST_F(fuse, read_only)
+{
+ ASSERT_EQ(0, mount(NULL, self->dir, NULL,
+ MS_REMOUNT | MS_BIND | MS_RDONLY, NULL));
+ /* An existing name gives EEXIST, not EROFS */
+ ASSERT_EQ(-1, open_at(self->dir, "rw", O_CREAT | O_EXCL | O_WRONLY));
+ EXPECT_EQ(EEXIST, errno);
+ ASSERT_EQ(-1, open_at(self->dir, "ro", O_RDONLY | O_TRUNC));
+ EXPECT_EQ(EROFS, errno);
+
+ EXPECT_EQ(0, self->srv->nr_create);
+ if (variant->atomic_o_trunc) {
+ EXPECT_EQ(0, self->srv->nr_open_atomic);
+ EXPECT_EQ(2, self->srv->nr_lookup);
+ } else {
+ EXPECT_EQ(variant->atomic ? 1 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 1 : 2, self->srv->nr_lookup);
+ }
+}
+
+TEST_F(fuse, open_existing)
+{
+ /* may_open() runs after the reply */
+ EXPECT_EQ(EACCES, open_as_nobody(_metadata, self->dir, "ro", O_RDWR));
+ open_close(_metadata, self->dir, "rw", O_RDONLY);
+ ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY));
+ EXPECT_EQ(ENOENT, errno);
+ /* A zero nodeid is cached like a negative LOOKUP */
+ ASSERT_EQ(-1, open_at(self->dir, "missing", O_RDONLY));
+ EXPECT_EQ(ENOENT, errno);
+
+ EXPECT_EQ(variant->atomic ? 0 : 1, self->srv->nr_open);
+ EXPECT_EQ(variant->atomic ? 3 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 0 : 3, self->srv->nr_lookup);
+}
+
+TEST_F(fuse, open_trunc)
+{
+ open_close(_metadata, self->dir, "rw", O_WRONLY | O_TRUNC);
+ /* As a shell "> ro" */
+ open_close(_metadata, self->dir, "ro", O_CREAT | O_TRUNC | O_WRONLY);
+
+ EXPECT_EQ(0, self->srv->nr_create);
+ if (variant->atomic_o_trunc) {
+ /* FUSE_OPEN truncates, after a LOOKUP */
+ EXPECT_EQ(0, self->srv->nr_open_atomic);
+ EXPECT_EQ(2, self->srv->nr_lookup);
+ EXPECT_EQ(2, self->srv->nr_open);
+ EXPECT_EQ(2, self->srv->nr_trunc);
+ EXPECT_EQ(0, self->srv->nr_setattr);
+ } else if (variant->atomic) {
+ /* The server opens the file and SETATTR truncates it */
+ EXPECT_EQ(2, self->srv->nr_open_atomic);
+ EXPECT_EQ(0, self->srv->nr_lookup);
+ EXPECT_EQ(0, self->srv->nr_open);
+ EXPECT_EQ(0, self->srv->nr_trunc);
+ EXPECT_EQ(2, self->srv->nr_setattr);
+ } else {
+ EXPECT_EQ(2, self->srv->nr_lookup);
+ EXPECT_EQ(2, self->srv->nr_open);
+ EXPECT_EQ(2, self->srv->nr_setattr);
+ }
+}
+
+TEST_F(fuse, open_directory)
+{
+ /* The server returns the entry only, and the VFS opens it */
+ open_close(_metadata, self->dir, "dir", O_RDONLY);
+ EXPECT_EQ(variant->atomic ? 1 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 0 : 1, self->srv->nr_lookup);
+}
+
+TEST_F(fuse, open_entry_only)
+{
+ /* The server returns only fuse_entry_out; the kernel sends FUSE_OPEN */
+ self->srv->entry_only = 1;
+ open_close(_metadata, self->dir, "rw", O_RDONLY);
+ EXPECT_EQ(variant->atomic ? 1 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 0 : 1, self->srv->nr_lookup);
+ EXPECT_EQ(1, self->srv->nr_open);
+}
+
+TEST_F(fuse, short_open_out)
+{
+ self->srv->short_open = 1;
+ open_close(_metadata, self->dir, "a", O_CREAT | O_WRONLY);
+ open_close(_metadata, self->dir, "rw", O_RDWR);
+
+ EXPECT_EQ(variant->atomic ? 2 : 0, self->srv->nr_open_atomic);
+ EXPECT_EQ(variant->atomic ? 0 : 1, self->srv->nr_open);
+}
+
+TEST_F(fuse, open_reply_no_nodeid)
+{
+ /* An opened file needs a nodeid */
+ if (!variant->atomic)
+ SKIP(return, "the server replies -ENOSYS to FUSE_OPEN_ATOMIC");
+ self->srv->open_no_nodeid = 1;
+ ASSERT_EQ(-1, open_at(self->dir, "rw", O_RDONLY));
+ EXPECT_EQ(EIO, errno);
+}
+
+TEST_F(fuse, open_atomic_enosys)
+{
+ /* After -ENOSYS the kernel sends FUSE_LOOKUP */
+ if (variant->atomic)
+ SKIP(return, "the server implements FUSE_OPEN_ATOMIC");
+ open_close(_metadata, self->dir, "rw", O_RDONLY);
+ open_close(_metadata, self->dir, "a", O_CREAT | O_WRONLY);
+
+ EXPECT_EQ(1, self->srv->nr_enosys);
+ EXPECT_EQ(0, self->srv->nr_open_atomic);
+ EXPECT_EQ(2, self->srv->nr_lookup);
+ EXPECT_EQ(1, self->srv->nr_open);
+ EXPECT_EQ(1, self->srv->nr_create);
+}
+
+TEST_F(fuse, name_too_long)
+{
+ char path[1200];
+ int len;
+
+ /* Longer than the 1024 byte limit of a server without FUSE_MAX_PAGES */
+ 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_enosys);
+ EXPECT_EQ(0, self->srv->nr_open_atomic);
+ EXPECT_EQ(0, self->srv->nr_create);
+}
+
+TEST_HARNESS_MAIN
--
2.50.1
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-10 15:53 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-10 15:52 [PATCH v7 0/2] fuse: atomic open Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 1/2] fuse: add FUSE_OPEN_ATOMIC Jimmy Zuber
2026-11-10 15:52 ` [PATCH v7 2/2] selftests/fuse: add test for FUSE_OPEN_ATOMIC Jimmy Zuber
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.