* [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts
@ 2026-05-29 3:19 Russ Fellows
2026-05-29 3:19 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
` (2 more replies)
0 siblings, 3 replies; 12+ messages in thread
From: Russ Fellows @ 2026-05-29 3:19 UTC (permalink / raw)
To: linux-fsdevel; +Cc: miklos, linux-kernel, Russ Fellows
These two patches fix a bug that causes FOPEN_PARALLEL_DIRECT_WRITES to have
no effect on FUSE passthrough mounts, preventing write IOPS from scaling with
concurrency.
The bug has two sides that must be fixed together (patch 1/2):
(a) fuse_passthrough_write_iter() calls inode_lock() directly, bypassing
the fuse_dio_lock() function that checks FOPEN_PARALLEL_DIRECT_WRITES.
(b) fuse_file_io_open() strips FOPEN_PARALLEL_DIRECT_WRITES from any open
that lacks FOPEN_DIRECT_IO, including passthrough opens where the flag
is redundant and should not be required.
Either bug alone is sufficient to serialize all writers. Together they ensure
the flag can never take effect on a passthrough-backed file.
Patch 2/2 is an independent performance follow-on: once parallel writes are
unblocked, the per-inode spinlock (fi->lock) becomes the next measurable cost.
This patch converts iocachectr to atomic_t and adds lockless fast paths to
fuse_inode_uncached_io_start/end and fuse_write_update_attr.
Tested with fio randwrite 4K direct, numjobs=1/2/4/8, iodepth=64, on a FUSE
passthrough mount backed by XFS on a RAM-backed null_blk device (kernel 6.17.13,
AMD EPYC 9B45, 16 threads):
numjobs | Before (IOPS) | After patch 1 | After patch 2
--------|---------------|---------------|---------------
1 | ~470K | ~470K | ~470K
2 | ~650K | ~930K | ~930K
4 | ~640K | ~1,530K | ~1,530K
8 | ~635K | ~1,707K | ~1,707K
Raw XFS throughput on the same device at numjobs=8: ~1,702K IOPS.
Russ Fellows (2):
fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
fuse: reduce fi->lock contention on parallel direct I/O
fs/fuse/file.c | 49 ++++++++++++++++++++++-----
fs/fuse/fuse_i.h | 11 +++++-
fs/fuse/inode.c | 2 +-
fs/fuse/iomode.c | 61 +++++++++++++++++++++++++++------
fs/fuse/passthrough.c | 6 ++--
5 files changed, 104 insertions(+), 25 deletions(-)
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
2026-05-29 3:19 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
@ 2026-05-29 3:19 ` Russ Fellows
2026-06-01 18:52 ` Amir Goldstein
2026-05-29 3:19 ` [PATCH 2/2] fuse: reduce fi->lock contention on parallel direct I/O Russ Fellows
2026-06-16 1:44 ` [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series Russ Fellows
2 siblings, 1 reply; 12+ messages in thread
From: Russ Fellows @ 2026-05-29 3:19 UTC (permalink / raw)
To: linux-fsdevel; +Cc: miklos, linux-kernel, Russ Fellows, stable
FOPEN_PARALLEL_DIRECT_WRITES has no effect on passthrough-backed FUSE
files due to two independent bugs that each prevent it from working.
Both must be fixed to restore parallel write concurrency.
Bug 1: fuse_passthrough_write_iter() acquires the exclusive inode lock
directly:
inode_lock(inode);
ret = backing_file_write_iter(...);
inode_unlock(inode);
This serializes all concurrent writers regardless of whether the server
set FOPEN_PARALLEL_DIRECT_WRITES. The flag is checked by
fuse_dio_wr_exclusive_lock(), called from fuse_dio_lock(), called from
fuse_direct_write_iter() -- the non-passthrough O_DIRECT path.
fuse_file_write_iter() routes passthrough opens to
fuse_passthrough_write_iter() instead, bypassing the flag check entirely.
Bug 2: fuse_file_io_open() in iomode.c strips FOPEN_PARALLEL_DIRECT_WRITES
from any open that lacks FOPEN_DIRECT_IO:
if (!(ff->open_flags & FOPEN_DIRECT_IO))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
This is correct for regular direct-IO opens where FOPEN_DIRECT_IO ensures
O_DIRECT is actually in effect. It is wrong for passthrough opens: a
passthrough file already bypasses the FUSE page cache by definition, so
FOPEN_DIRECT_IO is redundant and should not be required to preserve the
parallel-writes flag.
Note: adding FOPEN_DIRECT_IO to the daemon's open flags is not a valid
workaround. fuse_file_write_iter() checks FOPEN_DIRECT_IO before
FOPEN_PASSTHROUGH, so setting both causes writes to be routed through
fuse_direct_write_iter() (requiring a userspace round-trip) instead of
fuse_passthrough_write_iter() (zero-copy kernel path).
Combined effect: a daemon that opens with FOPEN_PASSTHROUGH |
FOPEN_PARALLEL_DIRECT_WRITES (without FOPEN_DIRECT_IO) has the parallel
flag stripped by Bug 2 before Bug 1 is even reached. Both bugs must be
fixed together.
Fix Bug 1: make fuse_dio_lock() and fuse_dio_unlock() non-static and call
them from fuse_passthrough_write_iter(), replacing the open-coded
inode_lock/inode_unlock. This reuses the existing logic that handles
FOPEN_PARALLEL_DIRECT_WRITES, append writes, writes past EOF, and
page-cache IO mode transitions.
Fix Bug 2: skip the FOPEN_PARALLEL_DIRECT_WRITES strip when
FOPEN_PASSTHROUGH is set. The flag remains stripped for non-passthrough
opens without FOPEN_DIRECT_IO, preserving existing behaviour.
Safety: backing_file_write_iter() calls into the backing filesystem's
write_iter (e.g. xfs_file_write_iter), which acquires the backing inode's
own lock independently. The FUSE inode lock and the backing inode lock are
entirely separate; using inode_lock_shared on the FUSE inode does not
affect the backing filesystem's concurrency control.
Fixes: 4d99ff8f6b85 ("fuse: implement open/create with FOPEN_PASSTHROUGH")
Cc: stable@vger.kernel.org
Signed-off-by: Russ Fellows <russ.fellows@gmail.com>
---
fs/fuse/file.c | 6 +++---
fs/fuse/fuse_i.h | 2 ++
fs/fuse/iomode.c | 8 ++++++--
fs/fuse/passthrough.c | 6 +++---
4 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index f94f3dc082c6..602c3f18676e 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1428,8 +1428,8 @@ static bool fuse_dio_wr_exclusive_lock(struct kiocb *iocb, struct iov_iter *from
return false;
}
-static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
- bool *exclusive)
+void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
+ bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
struct fuse_inode *fi = get_fuse_inode(inode);
@@ -1455,7 +1455,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
}
}
-static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
+void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
struct fuse_inode *fi = get_fuse_inode(inode);
@@ -1469,7 +1469,7 @@ static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
}
}
-static const struct iomap_write_ops fuse_iomap_write_ops = {
+static const struct iomap_write_ops fuse_iomap_write_ops = { /* unchanged */
.read_folio_range = fuse_iomap_read_folio_range,
};
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 17423d4e3cfa..120de517cea0 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1541,6 +1541,8 @@ int fuse_file_io_open(struct file *file, struct inode *inode);
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
/* file.c */
+void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from, bool *exclusive);
+void fuse_dio_unlock(struct kiocb *iocb, bool exclusive);
struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
unsigned int open_flags, bool isdir);
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c99e285f3..b3f51e3d1 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -214,10 +214,14 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
if (fuse_inode_backing(fi) && !(ff->open_flags & FOPEN_PASSTHROUGH))
goto fail;
- /*
- * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
- */
- if (!(ff->open_flags & FOPEN_DIRECT_IO))
+ /*
+ * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO, except for
+ * passthrough opens which bypass the page cache regardless and do not
+ * need FOPEN_DIRECT_IO to guarantee direct I/O semantics.
+ */
+ if (!(ff->open_flags & FOPEN_DIRECT_IO) &&
+ !(ff->open_flags & FOPEN_PASSTHROUGH))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
/*
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index f2d08ac2459b..f83d0a27cfb9 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -54,11 +54,11 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
- struct inode *inode = file_inode(file);
struct fuse_file *ff = file->private_data;
struct file *backing_file = fuse_file_passthrough(ff);
size_t count = iov_iter_count(iter);
ssize_t ret;
+ bool exclusive;
struct backing_file_ctx ctx = {
.cred = ff->cred,
.end_write = fuse_passthrough_end_write,
@@ -70,10 +70,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
if (!count)
return 0;
- inode_lock(inode);
+ fuse_dio_lock(iocb, iter, &exclusive);
ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
- inode_unlock(inode);
+ fuse_dio_unlock(iocb, exclusive);
return ret;
}
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 2/2] fuse: reduce fi->lock contention on parallel direct I/O
2026-05-29 3:19 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
2026-05-29 3:19 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
@ 2026-05-29 3:19 ` Russ Fellows
2026-06-16 1:44 ` [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series Russ Fellows
2 siblings, 0 replies; 12+ messages in thread
From: Russ Fellows @ 2026-05-29 3:19 UTC (permalink / raw)
To: linux-fsdevel; +Cc: miklos, linux-kernel, Russ Fellows
On the parallel passthrough write path, fi->lock was acquired three
times per I/O under the original code:
1. fuse_inode_uncached_io_start() -- decrement iocachectr
2. fuse_write_update_attr() -- bump attr_version, check i_size
3. fuse_inode_uncached_io_end() -- increment iocachectr, wake waiters
At 1.7M IOPS (numjobs=8, iodepth=64, 4K) this amounts to ~5.1M spinlock
acquisitions/second on a single cache line. While the parallel-writes fix
(patch 1/2) is the primary bottleneck, this patch eliminates the remaining
fi->lock overhead on the hot path.
Convert iocachectr from int to atomic_t and add lockless fast paths:
fuse_inode_uncached_io_start(fb=NULL): use an atomic_try_cmpxchg loop to
check-and-decrement without fi->lock. The lock is still taken for the
first open (0→-1 transition) and for backing-file manipulation.
fuse_inode_uncached_io_end(): use atomic_inc_return to detect the
still-inflight case (counter still negative after increment) without a
lock. fi->lock is only taken when the counter reaches zero, to serialize
wake_up and backing-file clear with concurrent opens.
fuse_write_update_attr(): skip fi->lock for the common in-EOF case.
Use WRITE_ONCE for fi->attr_version (some readers already access it
without fi->lock, e.g. inode.c:355 and dir.c:2069). fi->lock is only
taken when pos > i_size, with a double-check inside to handle races near
EOF. Parallel direct writes are gated on fuse_io_past_eof() returning
false upstream, so this slow path is not taken on the hot path.
All existing callsites that access iocachectr under fi->lock are updated
to use the atomic API (atomic_read/inc/dec), which are no-ops with the
lock held.
Signed-off-by: Russ Fellows <russ.fellows@gmail.com>
---
fs/fuse/file.c | 31 +++++++++++++++++++++++--------
fs/fuse/fuse_i.h | 9 +++++++--
fs/fuse/inode.c | 2 +-
fs/fuse/iomode.c | 53 +++++++++++++++++++++++++++++++++++++++--------------
4 files changed, 71 insertions(+), 24 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 602c3f18676e..73f870099 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1115,16 +1115,29 @@ bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written)
struct fuse_inode *fi = get_fuse_inode(inode);
bool ret = false;
- spin_lock(&fi->lock);
- fi->attr_version = atomic64_inc_return(&fc->attr_version);
- if (written > 0 && pos > inode->i_size) {
- i_size_write(inode, pos);
- ret = true;
- }
- spin_unlock(&fi->lock);
-
+ /*
+ * Bump the global attr version so stale cached attrs are detected.
+ * WRITE_ONCE is sufficient: some readers don't hold fi->lock, and
+ * on x86_64 the store is naturally atomic. fi->lock is only needed
+ * for the i_size extension case below.
+ */
+ WRITE_ONCE(fi->attr_version, atomic64_inc_return(&fc->attr_version));
fuse_invalidate_attr_mask(inode, FUSE_STATX_MODSIZE);
+ /*
+ * Only take fi->lock when the write may extend the file. Parallel
+ * direct writes are gated on fuse_io_past_eof() returning false, so
+ * this slow path is not taken on the hot parallel-write path.
+ */
+ if (written > 0 && pos > READ_ONCE(inode->i_size)) {
+ spin_lock(&fi->lock);
+ if (pos > inode->i_size) {
+ i_size_write(inode, pos);
+ ret = true;
+ }
+ spin_unlock(&fi->lock);
+ }
+
return ret;
}
@@ -3154,7 +3154,7 @@ void fuse_init_file_inode(struct inode *inode, unsigned int flags)
INIT_LIST_HEAD(&fi->write_files);
INIT_LIST_HEAD(&fi->queued_writes);
fi->writectr = 0;
- fi->iocachectr = 0;
+ atomic_set(&fi->iocachectr, 0);
init_waitqueue_head(&fi->page_waitq);
init_waitqueue_head(&fi->direct_io_waitq);
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 120de517cea0..67077afb3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -153,8 +153,13 @@ struct fuse_inode {
* (FUSE_NOWRITE) means more writes are blocked */
int writectr;
- /** Number of files/maps using page cache */
- int iocachectr;
+ /**
+ * Refcount for inode I/O mode: > 0 means cached I/O
+ * users, 0 is idle, < 0 means parallel uncached I/Os
+ * in flight. Use atomic ops; fi->lock only needed
+ * for the 0↔±1 boundary transitions.
+ */
+ atomic_t iocachectr;
/* Waitq for writepage completion */
wait_queue_head_t page_waitq;
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 7c0403a00..81e01cb55 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -190,7 +190,7 @@ static void fuse_evict_inode(struct inode *inode)
atomic64_inc(&fc->evict_ctr);
}
if (S_ISREG(inode->i_mode) && !fuse_is_bad(inode)) {
- WARN_ON(fi->iocachectr != 0);
+ WARN_ON(atomic_read(&fi->iocachectr) != 0);
WARN_ON(!list_empty(&fi->write_files));
WARN_ON(!list_empty(&fi->queued_writes));
}
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c99e285f3..611baacf9 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -17,7 +17,7 @@
*/
static inline bool fuse_is_io_cache_wait(struct fuse_inode *fi)
{
- return READ_ONCE(fi->iocachectr) < 0 && !fuse_inode_backing(fi);
+ return atomic_read(&fi->iocachectr) < 0 && !fuse_inode_backing(fi);
}
/*
@@ -60,9 +60,9 @@ int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
WARN_ON(ff->iomode == IOM_UNCACHED);
if (ff->iomode == IOM_NONE) {
ff->iomode = IOM_CACHED;
- if (fi->iocachectr == 0)
- set_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
- fi->iocachectr++;
+ if (!atomic_read(&fi->iocachectr))
+ set_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
+ atomic_inc(&fi->iocachectr);
}
spin_unlock(&fi->lock);
return 0;
@@ -72,11 +72,10 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
struct fuse_inode *fi)
{
spin_lock(&fi->lock);
- WARN_ON(fi->iocachectr <= 0);
+ WARN_ON(atomic_read(&fi->iocachectr) <= 0);
WARN_ON(ff->iomode != IOM_CACHED);
ff->iomode = IOM_NONE;
- fi->iocachectr--;
- if (fi->iocachectr == 0)
+ if (!atomic_dec_return(&fi->iocachectr))
clear_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
spin_unlock(&fi->lock);
}
@@ -85,23 +84,37 @@ static void fuse_file_cached_io_release(struct fuse_file *ff,
int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing *fb)
{
struct fuse_backing *oldfb;
- int err = 0;
+ int old, err = 0;
+
+ /*
+ * Fast lockless path for per-I/O calls (fb=NULL, no backing file).
+ * Use a CAS loop to atomically verify no cached users are present
+ * and decrement the refcount in one step.
+ */
+ if (!fb) {
+ old = atomic_read(&fi->iocachectr);
+ do {
+ if (old > 0)
+ return -ETXTBSY;
+ } while (!atomic_try_cmpxchg(&fi->iocachectr, &old, old - 1));
+ return 0;
+ }
spin_lock(&fi->lock);
/* deny conflicting backing files on same fuse inode */
oldfb = fuse_inode_backing(fi);
- if (fb && oldfb && oldfb != fb) {
+ if (oldfb && oldfb != fb) {
err = -EBUSY;
goto unlock;
}
- if (fi->iocachectr > 0) {
+ if (atomic_read(&fi->iocachectr) > 0) {
err = -ETXTBSY;
goto unlock;
}
- fi->iocachectr--;
+ atomic_dec(&fi->iocachectr);
/* fuse inode holds a single refcount of backing file */
- if (fb && !oldfb) {
+ if (!oldfb) {
oldfb = fuse_inode_backing_set(fi, fb);
WARN_ON_ONCE(oldfb != NULL);
} else {
@@ -133,10 +146,20 @@ void fuse_inode_uncached_io_end(struct fuse_inode *fi)
{
struct fuse_backing *oldfb = NULL;
+ /*
+ * Fast path: other uncached I/Os still in flight -- just increment
+ * and return without taking fi->lock.
+ */
+ if (atomic_inc_return(&fi->iocachectr) < 0)
+ return;
+
+ /*
+ * This may be the last uncached I/O. Take the lock and re-check:
+ * a new uncached I/O may have started between the atomic_inc_return
+ * and the spin_lock, so only wake/clear if iocachectr is still zero.
+ */
spin_lock(&fi->lock);
- WARN_ON(fi->iocachectr >= 0);
- fi->iocachectr++;
- if (!fi->iocachectr) {
+ if (!atomic_read(&fi->iocachectr)) {
wake_up(&fi->direct_io_waitq);
oldfb = fuse_inode_backing_set(fi, NULL);
}
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
2026-05-29 3:19 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
@ 2026-06-01 18:52 ` Amir Goldstein
2026-06-01 19:25 ` Russ Fellows
0 siblings, 1 reply; 12+ messages in thread
From: Amir Goldstein @ 2026-06-01 18:52 UTC (permalink / raw)
To: Russ Fellows; +Cc: linux-fsdevel, miklos, linux-kernel, fuse-devel
Removing stable list - this is definetly NOT a bug fix
Please CC fuse-devel@lists.linux.dev for future fuse patches
On Fri, May 29, 2026 at 03:19:15AM +0000, Russ Fellows wrote:
> FOPEN_PARALLEL_DIRECT_WRITES has no effect on passthrough-backed FUSE
> files due to two independent bugs that each prevent it from working.
> Both must be fixed to restore parallel write concurrency.
>
> Bug 1: fuse_passthrough_write_iter() acquires the exclusive inode lock
> directly:
>
> inode_lock(inode);
> ret = backing_file_write_iter(...);
> inode_unlock(inode);
>
> This serializes all concurrent writers regardless of whether the server
> set FOPEN_PARALLEL_DIRECT_WRITES. The flag is checked by
> fuse_dio_wr_exclusive_lock(), called from fuse_dio_lock(), called from
> fuse_direct_write_iter() -- the non-passthrough O_DIRECT path.
> fuse_file_write_iter() routes passthrough opens to
> fuse_passthrough_write_iter() instead, bypassing the flag check entirely.
>
> Bug 2: fuse_file_io_open() in iomode.c strips FOPEN_PARALLEL_DIRECT_WRITES
> from any open that lacks FOPEN_DIRECT_IO:
>
> if (!(ff->open_flags & FOPEN_DIRECT_IO))
> ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
>
> This is correct for regular direct-IO opens where FOPEN_DIRECT_IO ensures
> O_DIRECT is actually in effect. It is wrong for passthrough opens: a
> passthrough file already bypasses the FUSE page cache by definition, so
> FOPEN_DIRECT_IO is redundant and should not be required to preserve the
> parallel-writes flag.
>
> Note: adding FOPEN_DIRECT_IO to the daemon's open flags is not a valid
> workaround. fuse_file_write_iter() checks FOPEN_DIRECT_IO before
> FOPEN_PASSTHROUGH, so setting both causes writes to be routed through
> fuse_direct_write_iter() (requiring a userspace round-trip) instead of
> fuse_passthrough_write_iter() (zero-copy kernel path).
>
> Combined effect: a daemon that opens with FOPEN_PASSTHROUGH |
> FOPEN_PARALLEL_DIRECT_WRITES (without FOPEN_DIRECT_IO) has the parallel
> flag stripped by Bug 2 before Bug 1 is even reached. Both bugs must be
> fixed together.
>
> Fix Bug 1: make fuse_dio_lock() and fuse_dio_unlock() non-static and call
> them from fuse_passthrough_write_iter(), replacing the open-coded
> inode_lock/inode_unlock. This reuses the existing logic that handles
> FOPEN_PARALLEL_DIRECT_WRITES, append writes, writes past EOF, and
> page-cache IO mode transitions.
>
> Fix Bug 2: skip the FOPEN_PARALLEL_DIRECT_WRITES strip when
> FOPEN_PASSTHROUGH is set. The flag remains stripped for non-passthrough
> opens without FOPEN_DIRECT_IO, preserving existing behaviour.
>
> Safety: backing_file_write_iter() calls into the backing filesystem's
> write_iter (e.g. xfs_file_write_iter), which acquires the backing inode's
> own lock independently. The FUSE inode lock and the backing inode lock are
> entirely separate; using inode_lock_shared on the FUSE inode does not
> affect the backing filesystem's concurrency control.
>
> Fixes: 4d99ff8f6b85 ("fuse: implement open/create with FOPEN_PASSTHROUGH")
Not a fix, because this was very intentional.
It is a new feature that you are proposing to support parallel
passthrough dio.
Anyway, this patch has many problems
> Cc: stable@vger.kernel.org
> Signed-off-by: Russ Fellows <russ.fellows@gmail.com>
> ---
> fs/fuse/file.c | 6 +++---
> fs/fuse/fuse_i.h | 2 ++
> fs/fuse/iomode.c | 8 ++++++--
> fs/fuse/passthrough.c | 6 +++---
> 4 files changed, 14 insertions(+), 8 deletions(-)
>
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index f94f3dc082c6..602c3f18676e 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -1428,8 +1428,8 @@ static bool fuse_dio_wr_exclusive_lock(struct kiocb *iocb, struct iov_iter *from
> return false;
> }
>
> -static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
> - bool *exclusive)
> +void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
> + bool *exclusive)
> {
> struct inode *inode = file_inode(iocb->ki_filp);
> struct fuse_inode *fi = get_fuse_inode(inode);
> @@ -1455,7 +1455,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
> }
> }
>
> -static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
> +void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
> {
> struct inode *inode = file_inode(iocb->ki_filp);
> struct fuse_inode *fi = get_fuse_inode(inode);
> @@ -1469,7 +1469,7 @@ static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
> }
> }
>
> -static const struct iomap_write_ops fuse_iomap_write_ops = {
> +static const struct iomap_write_ops fuse_iomap_write_ops = { /* unchanged */
> .read_folio_range = fuse_iomap_read_folio_range,
> };
>
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 17423d4e3cfa..120de517cea0 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1541,6 +1541,8 @@ int fuse_file_io_open(struct file *file, struct inode *inode);
> void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
>
> /* file.c */
> +void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from, bool *exclusive);
> +void fuse_dio_unlock(struct kiocb *iocb, bool exclusive);
> struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
> unsigned int open_flags, bool isdir);
> void fuse_file_release(struct inode *inode, struct fuse_file *ff,
> diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> index c99e285f3..b3f51e3d1 100644
> --- a/fs/fuse/iomode.c
> +++ b/fs/fuse/iomode.c
> @@ -214,10 +214,14 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
> if (fuse_inode_backing(fi) && !(ff->open_flags & FOPEN_PASSTHROUGH))
> goto fail;
>
> - /*
> - * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
> - */
> - if (!(ff->open_flags & FOPEN_DIRECT_IO))
> + /*
> + * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO, except for
> + * passthrough opens which bypass the page cache regardless and do not
> + * need FOPEN_DIRECT_IO to guarantee direct I/O semantics.
> + */
> + if (!(ff->open_flags & FOPEN_DIRECT_IO) &&
> + !(ff->open_flags & FOPEN_PASSTHROUGH))
> ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
>
> /*
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index f2d08ac2459b..f83d0a27cfb9 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -54,11 +54,11 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> struct iov_iter *iter)
> {
> struct file *file = iocb->ki_filp;
> - struct inode *inode = file_inode(file);
> struct fuse_file *ff = file->private_data;
> struct file *backing_file = fuse_file_passthrough(ff);
> size_t count = iov_iter_count(iter);
> ssize_t ret;
> + bool exclusive;
> struct backing_file_ctx ctx = {
> .cred = ff->cred,
> .end_write = fuse_passthrough_end_write,
> @@ -70,10 +70,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> if (!count)
> return 0;
>
> - inode_lock(inode);
> + fuse_dio_lock(iocb, iter, &exclusive);
You can't just use fuse_dio_lock() like this
it plays nasty games with the iomode.
It does not even check for O_DIRECT mode, so this would do
parallel buffered write (at least until it meets the filesystem lock
but its not something we would want.
You should study this code better.
> ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> &ctx);
The problem is that while backing_file_write_iter() does not seem to
directly require an exclusive lock (apart from maybe file_remove_privs)
the fuse_passthrough_end_write() callback does I think rely on this
exclusive lock.
So proving that this can work will require more research and more effort
and I am not really sure how that is going to work out.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
2026-06-01 18:52 ` Amir Goldstein
@ 2026-06-01 19:25 ` Russ Fellows
2026-06-01 20:23 ` Amir Goldstein
0 siblings, 1 reply; 12+ messages in thread
From: Russ Fellows @ 2026-06-01 19:25 UTC (permalink / raw)
To: Amir Goldstein; +Cc: linux-fsdevel, miklos, linux-kernel, fuse-devel
Amir,
I appreciate your feedback. I will review and revise my proposed changes.
I will also remove the “fix” tag and mark this as an enhancement to FUSE parallel I/O.
As an aside, I have tested the kernel changes quite a bit and have had no crashes or corruption, and the write performance is 3x. But, I will use your input to focus these changes more appropriately and eliminate their unintended consequences.
Regards,
—Russ
> On Jun 1, 2026, at 12:52 PM, Amir Goldstein <amir73il@gmail.com> wrote:
>
> Removing stable list - this is definetly NOT a bug fix
> Please CC fuse-devel@lists.linux.dev for future fuse patches
>
>
> Not a fix, because this was very intentional.
> It is a new feature that you are proposing to support parallel
> passthrough dio.
>
> Anyway, this patch has many problems
>
>>
>
> You can't just use fuse_dio_lock() like this
> it plays nasty games with the iomode.
> It does not even check for O_DIRECT mode, so this would do
> parallel buffered write (at least until it meets the filesystem lock
> but its not something we would want.
> You should study this code better.
>
>> ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
>> &ctx);
>
> The problem is that while backing_file_write_iter() does not seem to
> directly require an exclusive lock (apart from maybe file_remove_privs)
> the fuse_passthrough_end_write() callback does I think rely on this
> exclusive lock.
>
> So proving that this can work will require more research and more effort
> and I am not really sure how that is going to work out.
>
> Thanks,
> Amir.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
2026-06-01 19:25 ` Russ Fellows
@ 2026-06-01 20:23 ` Amir Goldstein
0 siblings, 0 replies; 12+ messages in thread
From: Amir Goldstein @ 2026-06-01 20:23 UTC (permalink / raw)
To: Russ Fellows; +Cc: linux-fsdevel, miklos, linux-kernel, fuse-devel
On Mon, Jun 1, 2026 at 9:25 PM Russ Fellows <russ.fellows@gmail.com> wrote:
> > On Jun 1, 2026, at 12:52 PM, Amir Goldstein <amir73il@gmail.com> wrote:
> >
> > Removing stable list - this is definetly NOT a bug fix
> > Please CC fuse-devel@lists.linux.dev for future fuse patches
> >
> >
> > Not a fix, because this was very intentional.
> > It is a new feature that you are proposing to support parallel
> > passthrough dio.
> >
> > Anyway, this patch has many problems
> >
> >>
> >
> > You can't just use fuse_dio_lock() like this
> > it plays nasty games with the iomode.
> > It does not even check for O_DIRECT mode, so this would do
> > parallel buffered write (at least until it meets the filesystem lock
> > but its not something we would want.
> > You should study this code better.
> >
> >> ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> >> &ctx);
> >
> > The problem is that while backing_file_write_iter() does not seem to
> > directly require an exclusive lock (apart from maybe file_remove_privs)
> > the fuse_passthrough_end_write() callback does I think rely on this
> > exclusive lock.
> >
> > So proving that this can work will require more research and more effort
> > and I am not really sure how that is going to work out.
> >
> > Thanks,
> > Amir.
>
No "top posting" in the mailing list please :)
>
> Amir,
>
> I appreciate your feedback. I will review and revise my proposed changes.
>
> I will also remove the “fix” tag and mark this as an enhancement to FUSE parallel I/O.
>
> As an aside, I have tested the kernel changes quite a bit and have had no crashes or corruption,
I believe that but the corner cases of parallel dio with extending
writes are the
devil in the details and its unlikely that you covered those without
very deliberate testing
I am not saying there is a race but takes a lot of proof to remove a
lock and this
proof is on the one trying to remove it.
> and the write performance is 3x.
I believe that but can't just remove the lock and show the performance
> But, I will use your input to focus these changes more appropriately and eliminate their unintended consequences.
>
My instinct is that it might be easier to do this with PASSTHROUGH_INO [1]
where the attributes are always read from the backing inode, so less
races to worry about.
Good luck,
Amir.
[1] https://lore.kernel.org/fuse-devel/20260516004004.1455526-2-joannelkoong@gmail.com/
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series
2026-05-29 3:19 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
2026-05-29 3:19 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
2026-05-29 3:19 ` [PATCH 2/2] fuse: reduce fi->lock contention on parallel direct I/O Russ Fellows
@ 2026-06-16 1:44 ` Russ Fellows
2026-06-16 1:44 ` [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens Russ Fellows
2026-06-16 1:44 ` [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter Russ Fellows
2 siblings, 2 replies; 12+ messages in thread
From: Russ Fellows @ 2026-06-16 1:44 UTC (permalink / raw)
To: miklos; +Cc: fuse-devel, linux-fsdevel, linux-kernel, amir73il, Russ Fellows
From: Russ Fellows <rfellows@xlrait.dev>
This series contains the current minimal submit-ready fix for passthrough write
parallelism.
The first patch preserves FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens.
Without that, fuse_file_io_open() strips the flag before passthrough writes see
it, so the kernel never reaches the shared-lock path.
The second patch keeps the lock decision local to passthrough.c and allows
shared inode locking only for safe direct overwrite writes. Append writes,
buffered writes, and writes that may extend EOF remain serialized.
This intentionally replaces the older broader patch direction. The current
minimal series does not export fuse_dio_lock()/fuse_dio_unlock() from file.c,
does not add declarations to fuse_i.h, and does not include the separate
iocachectr/fi->lock cleanup work.
Regarding fuse_passthrough_end_write() safety: the shared lock is only taken
for within-EOF direct overwrites, where i_size does not change. In that case
fuse_passthrough_end_write() -> fuse_write_update_attr() only updates mtime/
ctime, which is safe under a shared inode lock. Past-EOF writes still take
the exclusive lock, so i_size updates are always serialized correctly.
Validated on kernel 6.17.13-p4min with fio 4K random direct writes on a FUSE
passthrough mount backed by XFS on a RAM-backed null_blk device:
numjobs | FUSE IOPS
--------|----------
1 | 481,837
2 | 931,883
4 | 1,533,058
8 | 1,730,477
Raw XFS on the same device at numjobs=8: 1,693,406 IOPS.
Changes since v1:
- Removed use of fuse_dio_lock()/fuse_dio_unlock() from file.c; the lock
decision is now self-contained in passthrough.c using inode_lock_shared()
and inode_unlock_shared() directly.
- Added explicit IOCB_DIRECT check: parallel locking is only allowed for
direct I/O, preventing accidental shared locking on buffered writes.
- Added explicit IOCB_APPEND check: append writes always serialize.
- Added explicit past-EOF check: writes that extend i_size always serialize.
- Removed all symbol exports from file.c (fuse_dio_lock, fuse_dio_unlock).
- Removed all declarations added to fuse_i.h.
- Split into two patches: iomode.c flag preservation as a prerequisite
(patch 1) and passthrough.c locking logic as the main change (patch 2).
Russ Fellows (2):
fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens
fuse: allow parallel direct writes in passthrough write_iter
fs/fuse/iomode.c | 7 +++++--
fs/fuse/passthrough.c | 46 ++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 49 insertions(+), 4 deletions(-)
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens
2026-06-16 1:44 ` [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series Russ Fellows
@ 2026-06-16 1:44 ` Russ Fellows
2026-06-16 11:06 ` Amir Goldstein
2026-06-16 1:44 ` [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter Russ Fellows
1 sibling, 1 reply; 12+ messages in thread
From: Russ Fellows @ 2026-06-16 1:44 UTC (permalink / raw)
To: miklos; +Cc: fuse-devel, linux-fsdevel, linux-kernel, amir73il, Russ Fellows
From: Russ Fellows <rfellows@xlrait.dev>
fuse_file_io_open() currently strips FOPEN_PARALLEL_DIRECT_WRITES whenever FOPEN_DIRECT_IO is absent. That rule is correct for the regular direct-IO path, but it is too strict for passthrough opens.
Passthrough I/O already bypasses the page cache through the backing file, so it does not need FOPEN_DIRECT_IO to preserve direct-I/O semantics. Clearing the parallel-write flag for passthrough opens causes the kernel to drop the server's request for parallel direct writes before the passthrough write path ever sees it.
Keep FOPEN_PARALLEL_DIRECT_WRITES for opens that also carry FOPEN_PASSTHROUGH, and continue to clear it for non-passthrough opens that lack FOPEN_DIRECT_IO.
This is a prerequisite for passthrough write parallelism: without it, the subsequent shared-lock path never activates.
Signed-off-by: Russ Fellows <rfellows@xlrait.dev>
---
fs/fuse/iomode.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c99e285f3..0301a48d8 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -216,9 +216,12 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
goto fail;
/*
- * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
+ * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO, except for
+ * passthrough opens which bypass the page cache regardless and do not
+ * need FOPEN_DIRECT_IO to guarantee direct I/O semantics.
*/
- if (!(ff->open_flags & FOPEN_DIRECT_IO))
+ if (!(ff->open_flags & FOPEN_DIRECT_IO) &&
+ !(ff->open_flags & FOPEN_PASSTHROUGH))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
/*
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter
2026-06-16 1:44 ` [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series Russ Fellows
2026-06-16 1:44 ` [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens Russ Fellows
@ 2026-06-16 1:44 ` Russ Fellows
2026-06-16 11:50 ` Amir Goldstein
1 sibling, 1 reply; 12+ messages in thread
From: Russ Fellows @ 2026-06-16 1:44 UTC (permalink / raw)
To: miklos; +Cc: fuse-devel, linux-fsdevel, linux-kernel, amir73il, Russ Fellows
From: Russ Fellows <rfellows@xlrait.dev>
fuse_passthrough_write_iter() currently relies on the generic fuse_dio_lock() path to decide whether writes may run under a shared inode lock. That couples passthrough to the regular direct-IO helpers and does not make the passthrough-specific safety conditions explicit.
Keep the decision local to passthrough.c instead. Allow shared inode locking only when all of the following are true:
- the open carries FOPEN_PARALLEL_DIRECT_WRITES
- the write is direct I/O
- the write is not append
- the write does not extend past EOF
Buffered writes, append writes, and EOF-extending writes remain serialized under the exclusive inode lock.
This preserves the parallel direct-write win for safe overwrite I/O without exporting extra helper APIs from file.c.
Signed-off-by: Russ Fellows <rfellows@xlrait.dev>
---
fs/fuse/passthrough.c | 46 +++++++++++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index ee822a983..c32b35a6d 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -25,6 +25,38 @@ static void fuse_passthrough_end_write(struct kiocb *iocb, ssize_t ret)
fuse_write_update_attr(inode, iocb->ki_pos, ret);
}
+static bool fuse_passthrough_io_past_eof(struct kiocb *iocb,
+ struct iov_iter *iter)
+{
+ struct inode *inode = file_inode(iocb->ki_filp);
+
+ return iocb->ki_pos + iov_iter_count(iter) > i_size_read(inode);
+}
+
+static bool fuse_passthrough_write_exclusive(struct kiocb *iocb,
+ struct iov_iter *iter)
+{
+ struct file *file = iocb->ki_filp;
+ struct fuse_file *ff = file->private_data;
+
+ if (!(ff->open_flags & FOPEN_PARALLEL_DIRECT_WRITES))
+ return true;
+
+ /*
+ * Passthrough writes do not use the regular FUSE direct-IO iomode path.
+ * Allow shared locking only for direct overwrite I/O; buffered writes,
+ * append writes, and writes that may extend EOF remain serialized.
+ */
+ if (!(iocb->ki_flags & IOCB_DIRECT))
+ return true;
+ if (iocb->ki_flags & IOCB_APPEND)
+ return true;
+ if (fuse_passthrough_io_past_eof(iocb, iter))
+ return true;
+
+ return false;
+}
+
ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
@@ -54,6 +86,7 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
+ struct inode *inode = file_inode(file);
struct fuse_file *ff = file->private_data;
struct file *backing_file = fuse_file_passthrough(ff);
size_t count = iov_iter_count(iter);
@@ -70,10 +103,19 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
if (!count)
return 0;
- fuse_dio_lock(iocb, iter, &exclusive);
+ exclusive = fuse_passthrough_write_exclusive(iocb, iter);
+ if (exclusive)
+ inode_lock(inode);
+ else
+ inode_lock_shared(inode);
+
ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
- fuse_dio_unlock(iocb, exclusive);
+
+ if (exclusive)
+ inode_unlock(inode);
+ else
+ inode_unlock_shared(inode);
return ret;
}
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens
2026-06-16 1:44 ` [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens Russ Fellows
@ 2026-06-16 11:06 ` Amir Goldstein
0 siblings, 0 replies; 12+ messages in thread
From: Amir Goldstein @ 2026-06-16 11:06 UTC (permalink / raw)
To: Russ Fellows
Cc: miklos, fuse-devel, linux-fsdevel, linux-kernel, Russ Fellows
On Tue, Jun 16, 2026 at 3:44 AM Russ Fellows <russ.fellows@gmail.com> wrote:
>
> From: Russ Fellows <rfellows@xlrait.dev>
>
> fuse_file_io_open() currently strips FOPEN_PARALLEL_DIRECT_WRITES whenever FOPEN_DIRECT_IO is absent. That rule is correct for the regular direct-IO path, but it is too strict for passthrough opens.
>
> Passthrough I/O already bypasses the page cache through the backing file, so it does not need FOPEN_DIRECT_IO to preserve direct-I/O semantics. Clearing the parallel-write flag for passthrough opens causes the kernel to drop the server's request for parallel direct writes before the passthrough write path ever sees it.
>
> Keep FOPEN_PARALLEL_DIRECT_WRITES for opens that also carry FOPEN_PASSTHROUGH, and continue to clear it for non-passthrough opens that lack FOPEN_DIRECT_IO.
>
> This is a prerequisite for passthrough write parallelism: without it, the subsequent shared-lock path never activates.
>
> Signed-off-by: Russ Fellows <rfellows@xlrait.dev>
> ---
> fs/fuse/iomode.c | 7 +++++--
> 1 file changed, 5 insertions(+), 2 deletions(-)
>
> diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> index c99e285f3..0301a48d8 100644
> --- a/fs/fuse/iomode.c
> +++ b/fs/fuse/iomode.c
> @@ -216,9 +216,12 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
> goto fail;
>
> /*
> - * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
> + * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO, except for
> + * passthrough opens which bypass the page cache regardless and do not
> + * need FOPEN_DIRECT_IO to guarantee direct I/O semantics.
That's a very round about way to say FOPEN_PARALLEL_DIRECT_WRITES requires
open in either direct_io or passthrough mode, but an even better way
of saying this:
#define FOPEN_IOMODE(oflags) ((oflags & (FOPEN_DIRECT_IO | FOPEN_PASSTHROUGH))
#define FOPEN_IOMODE_IS_CACHED(oflags) (FOPEN_IOMODE(oflags) == 0)
#define FOPEN_IOMODE_IS_DIRECT(oflags) (FOPEN_IOMODE(oflags) & FOPEN_DIRECT_IO)
#define FOPEN_IOMODE_IS_PASSTHRPUGH(oflags) (FOPEN_IOMODE(oflags) ==
FOPEN_PASSTHROUGH)
> */
> - if (!(ff->open_flags & FOPEN_DIRECT_IO))
> + if (!(ff->open_flags & FOPEN_DIRECT_IO) &&
> + !(ff->open_flags & FOPEN_PASSTHROUGH))
/* FOPEN_PARALLEL_DIRECT_WRITES is not supported with cached iomode */
if (FOPEN_IOMODE_IS_CACHED(ff->open_flags))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
...
if (FOPEN_IOMODE_IS_DIRECT(ff->open_flags))
return 0;
if (FOPEN_IOMODE_IS_PASSTHROUGH(ff->open_flags))
err = fuse_file_passthrough_open(inode, file);
else
err = fuse_file_cached_io_open(inode, ff);
or some variation of this which makes those modes and comments easier
to understand.
Thanks,
Amir.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter
2026-06-16 1:44 ` [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter Russ Fellows
@ 2026-06-16 11:50 ` Amir Goldstein
0 siblings, 0 replies; 12+ messages in thread
From: Amir Goldstein @ 2026-06-16 11:50 UTC (permalink / raw)
To: Russ Fellows
Cc: miklos, fuse-devel, linux-fsdevel, linux-kernel, Russ Fellows
On Tue, Jun 16, 2026 at 3:44 AM Russ Fellows <russ.fellows@gmail.com> wrote:
>
> From: Russ Fellows <rfellows@xlrait.dev>
>
> fuse_passthrough_write_iter() currently relies on the generic fuse_dio_lock() path to decide whether writes may run under a shared inode lock. That couples passthrough to the regular direct-IO helpers and does not make the passthrough-specific safety conditions explicit.
>
> Keep the decision local to passthrough.c instead. Allow shared inode locking only when all of the following are true:
>
> - the open carries FOPEN_PARALLEL_DIRECT_WRITES
> - the write is direct I/O
> - the write is not append
> - the write does not extend past EOF
>
> Buffered writes, append writes, and EOF-extending writes remain serialized under the exclusive inode lock.
>
> This preserves the parallel direct-write win for safe overwrite I/O without exporting extra helper APIs from file.c.
>
> Signed-off-by: Russ Fellows <rfellows@xlrait.dev>
> ---
> fs/fuse/passthrough.c | 46 +++++++++++++++++++++++++++++++++++++++++--
> 1 file changed, 44 insertions(+), 2 deletions(-)
>
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index ee822a983..c32b35a6d 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -25,6 +25,38 @@ static void fuse_passthrough_end_write(struct kiocb *iocb, ssize_t ret)
> fuse_write_update_attr(inode, iocb->ki_pos, ret);
> }
>
> +static bool fuse_passthrough_io_past_eof(struct kiocb *iocb,
> + struct iov_iter *iter)
> +{
> + struct inode *inode = file_inode(iocb->ki_filp);
> +
> + return iocb->ki_pos + iov_iter_count(iter) > i_size_read(inode);
> +}
> +
> +static bool fuse_passthrough_write_exclusive(struct kiocb *iocb,
> + struct iov_iter *iter)
> +{
> + struct file *file = iocb->ki_filp;
> + struct fuse_file *ff = file->private_data;
> +
> + if (!(ff->open_flags & FOPEN_PARALLEL_DIRECT_WRITES))
> + return true;
> +
> + /*
> + * Passthrough writes do not use the regular FUSE direct-IO iomode path.
> + * Allow shared locking only for direct overwrite I/O; buffered writes,
> + * append writes, and writes that may extend EOF remain serialized.
> + */
> + if (!(iocb->ki_flags & IOCB_DIRECT))
> + return true;
> + if (iocb->ki_flags & IOCB_APPEND)
> + return true;
> + if (fuse_passthrough_io_past_eof(iocb, iter))
> + return true;
> +
> + return false;
> +}
> +
> ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
> {
> struct file *file = iocb->ki_filp;
> @@ -54,6 +86,7 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> struct iov_iter *iter)
> {
> struct file *file = iocb->ki_filp;
> + struct inode *inode = file_inode(file);
> struct fuse_file *ff = file->private_data;
> struct file *backing_file = fuse_file_passthrough(ff);
> size_t count = iov_iter_count(iter);
> @@ -70,10 +103,19 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
> if (!count)
> return 0;
>
> - fuse_dio_lock(iocb, iter, &exclusive);
> + exclusive = fuse_passthrough_write_exclusive(iocb, iter);
> + if (exclusive)
> + inode_lock(inode);
> + else
> + inode_lock_shared(inode);
> +
This is TOCTOU racy
You are missing this important part:
/*
* This check
* should be performed only after taking shared inode lock.
* Previous past eof check was without inode lock and might
* have raced, so check it again.
*/
if (fuse_io_past_eof(iocb, from) ||
Either you duplicate fuse_dio_lock() -> fuse_passthrough_lock()
or you find a way to have the two share common helper (prefered)
> ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
> &ctx);
> - fuse_dio_unlock(iocb, exclusive);
> +
> + if (exclusive)
> + inode_unlock(inode);
> + else
> + inode_unlock_shared(inode);
In any case it is ugly to open code this so at least a thin wrapper
or even a scoped class would be in order (e.g. fuse_passthrough_unlock)
Thanks,
Amir.
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes
2026-05-29 3:12 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
@ 2026-05-29 3:12 ` Russ Fellows
0 siblings, 0 replies; 12+ messages in thread
From: Russ Fellows @ 2026-05-29 3:12 UTC (permalink / raw)
To: linux-fuse; +Cc: miklos, linux-kernel, Russ Fellows, stable
FOPEN_PARALLEL_DIRECT_WRITES has no effect on passthrough-backed FUSE
files due to two independent bugs that each prevent it from working.
Both must be fixed to restore parallel write concurrency.
Bug 1: fuse_passthrough_write_iter() acquires the exclusive inode lock
directly:
inode_lock(inode);
ret = backing_file_write_iter(...);
inode_unlock(inode);
This serializes all concurrent writers regardless of whether the server
set FOPEN_PARALLEL_DIRECT_WRITES. The flag is checked by
fuse_dio_wr_exclusive_lock(), called from fuse_dio_lock(), called from
fuse_direct_write_iter() -- the non-passthrough O_DIRECT path.
fuse_file_write_iter() routes passthrough opens to
fuse_passthrough_write_iter() instead, bypassing the flag check entirely.
Bug 2: fuse_file_io_open() in iomode.c strips FOPEN_PARALLEL_DIRECT_WRITES
from any open that lacks FOPEN_DIRECT_IO:
if (!(ff->open_flags & FOPEN_DIRECT_IO))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
This is correct for regular direct-IO opens where FOPEN_DIRECT_IO ensures
O_DIRECT is actually in effect. It is wrong for passthrough opens: a
passthrough file already bypasses the FUSE page cache by definition, so
FOPEN_DIRECT_IO is redundant and should not be required to preserve the
parallel-writes flag.
Note: adding FOPEN_DIRECT_IO to the daemon's open flags is not a valid
workaround. fuse_file_write_iter() checks FOPEN_DIRECT_IO before
FOPEN_PASSTHROUGH, so setting both causes writes to be routed through
fuse_direct_write_iter() (requiring a userspace round-trip) instead of
fuse_passthrough_write_iter() (zero-copy kernel path).
Combined effect: a daemon that opens with FOPEN_PASSTHROUGH |
FOPEN_PARALLEL_DIRECT_WRITES (without FOPEN_DIRECT_IO) has the parallel
flag stripped by Bug 2 before Bug 1 is even reached. Both bugs must be
fixed together.
Fix Bug 1: make fuse_dio_lock() and fuse_dio_unlock() non-static and call
them from fuse_passthrough_write_iter(), replacing the open-coded
inode_lock/inode_unlock. This reuses the existing logic that handles
FOPEN_PARALLEL_DIRECT_WRITES, append writes, writes past EOF, and
page-cache IO mode transitions.
Fix Bug 2: skip the FOPEN_PARALLEL_DIRECT_WRITES strip when
FOPEN_PASSTHROUGH is set. The flag remains stripped for non-passthrough
opens without FOPEN_DIRECT_IO, preserving existing behaviour.
Safety: backing_file_write_iter() calls into the backing filesystem's
write_iter (e.g. xfs_file_write_iter), which acquires the backing inode's
own lock independently. The FUSE inode lock and the backing inode lock are
entirely separate; using inode_lock_shared on the FUSE inode does not
affect the backing filesystem's concurrency control.
Fixes: 4d99ff8f6b85 ("fuse: implement open/create with FOPEN_PASSTHROUGH")
Cc: stable@vger.kernel.org
Signed-off-by: Russ Fellows <russ.fellows@gmail.com>
---
fs/fuse/file.c | 6 +++---
fs/fuse/fuse_i.h | 2 ++
fs/fuse/iomode.c | 8 ++++++--
fs/fuse/passthrough.c | 6 +++---
4 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index f94f3dc082c6..602c3f18676e 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -1428,8 +1428,8 @@ static bool fuse_dio_wr_exclusive_lock(struct kiocb *iocb, struct iov_iter *from
return false;
}
-static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
- bool *exclusive)
+void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
+ bool *exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
struct fuse_inode *fi = get_fuse_inode(inode);
@@ -1455,7 +1455,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from,
}
}
-static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
+void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
{
struct inode *inode = file_inode(iocb->ki_filp);
struct fuse_inode *fi = get_fuse_inode(inode);
@@ -1469,7 +1469,7 @@ static void fuse_dio_unlock(struct kiocb *iocb, bool exclusive)
}
}
-static const struct iomap_write_ops fuse_iomap_write_ops = {
+static const struct iomap_write_ops fuse_iomap_write_ops = { /* unchanged */
.read_folio_range = fuse_iomap_read_folio_range,
};
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 17423d4e3cfa..120de517cea0 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1541,6 +1541,8 @@ int fuse_file_io_open(struct file *file, struct inode *inode);
void fuse_file_io_release(struct fuse_file *ff, struct inode *inode);
/* file.c */
+void fuse_dio_lock(struct kiocb *iocb, struct iov_iter *from, bool *exclusive);
+void fuse_dio_unlock(struct kiocb *iocb, bool exclusive);
struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
unsigned int open_flags, bool isdir);
void fuse_file_release(struct inode *inode, struct fuse_file *ff,
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index c99e285f3..b3f51e3d1 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -214,10 +214,14 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
if (fuse_inode_backing(fi) && !(ff->open_flags & FOPEN_PASSTHROUGH))
goto fail;
- /*
- * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
- */
- if (!(ff->open_flags & FOPEN_DIRECT_IO))
+ /*
+ * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO, except for
+ * passthrough opens which bypass the page cache regardless and do not
+ * need FOPEN_DIRECT_IO to guarantee direct I/O semantics.
+ */
+ if (!(ff->open_flags & FOPEN_DIRECT_IO) &&
+ !(ff->open_flags & FOPEN_PASSTHROUGH))
ff->open_flags &= ~FOPEN_PARALLEL_DIRECT_WRITES;
/*
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index f2d08ac2459b..f83d0a27cfb9 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -54,11 +54,11 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
struct iov_iter *iter)
{
struct file *file = iocb->ki_filp;
- struct inode *inode = file_inode(file);
struct fuse_file *ff = file->private_data;
struct file *backing_file = fuse_file_passthrough(ff);
size_t count = iov_iter_count(iter);
ssize_t ret;
+ bool exclusive;
struct backing_file_ctx ctx = {
.cred = ff->cred,
.end_write = fuse_passthrough_end_write,
@@ -70,10 +70,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
if (!count)
return 0;
- inode_lock(inode);
+ fuse_dio_lock(iocb, iter, &exclusive);
ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
&ctx);
- inode_unlock(inode);
+ fuse_dio_unlock(iocb, exclusive);
return ret;
}
--
2.51.0
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-06-16 11:50 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-29 3:19 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
2026-05-29 3:19 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
2026-06-01 18:52 ` Amir Goldstein
2026-06-01 19:25 ` Russ Fellows
2026-06-01 20:23 ` Amir Goldstein
2026-05-29 3:19 ` [PATCH 2/2] fuse: reduce fi->lock contention on parallel direct I/O Russ Fellows
2026-06-16 1:44 ` [PATCH v2 0/2] fuse: fix passthrough parallel direct writes with a minimal series Russ Fellows
2026-06-16 1:44 ` [PATCH v2 1/2] fuse: preserve FOPEN_PARALLEL_DIRECT_WRITES for passthrough opens Russ Fellows
2026-06-16 11:06 ` Amir Goldstein
2026-06-16 1:44 ` [PATCH v2 2/2] fuse: allow parallel direct writes in passthrough write_iter Russ Fellows
2026-06-16 11:50 ` Amir Goldstein
-- strict thread matches above, loose matches on Subject: below --
2026-05-29 3:12 [PATCH 0/2] fuse: fix and optimize parallel writes on passthrough mounts Russ Fellows
2026-05-29 3:12 ` [PATCH 1/2] fuse: fix FOPEN_PARALLEL_DIRECT_WRITES being ignored for passthrough writes Russ Fellows
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Powered by JetHome