Re: [PATCH v2 09/21] fuse: handle partial io passthrough for read/write, splice, and mmap

Amir Goldstein <[email protected]> Sat, 16 May 2026 18:57:24 +0200
Newsgroups org.kernel.vger.linux-unionfs,dev.linux.lists.fuse-devel
Message-ID <CAOQ4uxiWhWcNbKDo49vWR7HrN4p84JdvjgYyhptDqKxJ7dwoFQ@mail.gmail.com>
On Sat, May 16, 2026 at 2:52 AM Joanne Koong <[email protected]> wrote:
>
> Servers can now pass through only reads or writes instead of being
> required to pass through both. When the read or write operation is not
> passed through, fall back to direct io for handling it. This avoids
> cache coherency issues from mixing the backing file's page cache with
> the fuse inode's page cache.
>
> Reject mmap when passthrough is partial or not set for read/writes since
> mmap requires both read and write access to the backing page cache.
>
> The FOPEN_DIRECT_IO check in the read/write fallback is not strictly
> needed as the caller already handles it, but including it here makes
> the fallback logic self-contained.
>
> Signed-off-by: Joanne Koong <[email protected]>
> ---
>  fs/fuse/file.c        |  7 ++++---
>  fs/fuse/fuse_i.h      |  3 +++
>  fs/fuse/passthrough.c | 18 ++++++++++++++++++
>  3 files changed, 25 insertions(+), 3 deletions(-)
>
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index 1173811b2ea7..5b70d8272700 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -1781,7 +1781,7 @@ static ssize_t __fuse_direct_read(struct fuse_io_priv *io,
>
>  static ssize_t fuse_direct_IO(struct kiocb *iocb, struct iov_iter *iter);
>
> -static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
> +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
>  {
>         ssize_t res;
>
> @@ -1796,7 +1796,7 @@ static ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to)
>         return res;
>  }
>
> -static ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
> +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from)
>  {
>         struct inode *inode = file_inode(iocb->ki_filp);
>         ssize_t res;
> @@ -1884,7 +1884,8 @@ static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
>         struct fuse_file *ff = out->private_data;
>
>         /* FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH */
> -       if (fuse_file_passthrough(ff) && !(ff->open_flags & FOPEN_DIRECT_IO))
> +       if (fuse_passthrough_op(file_inode(out), FUSE_WRITE) &&

Removing fuse_file_passthrough(ff) makes me uncomfortable.
It's true that if server sets PASSTHROUGH_OP_WRITE on lookup
the file has to be opened with FOPEN_PASSTHROUGH, but this
not easily understood by passing by reviewers.

It's be nicer if we keep this logic contained in a helper where it can be
properly documented, something like this (following the changes to
FUSE_PASSTHROUGH_OP() discussed earlier):

static inline u64 fuse_passthrough_op(struct inode *inode, u64 op_mask)
{
        struct fuse_inode *fi = get_fuse_inode(inode);
        struct fuse_backing *fb = fuse_inode_passthrough(fi);

        return !fb ? 0 : fb->ops_mask & op_mask;
}

static inline struct file *fuse_file_passthrough_op(struct file *file,
                                                    u64 op_mask)
{
        struct fuse_file *ff = file->private_data;

        /*
         * FOPEN_DIRECT_IO can be used to opt-out of passthrough per file and
         * ops_mask can be used to opt-out of passthrough per operation type.
         */
        return fuse_file_passthrough(ff) &&
                !(ff->open_flags & FOPEN_DIRECT_IO) &&
                fuse_passthrough_op(file_inode(file), op_mask) == op_mask;
}

...

static ssize_t fuse_splice_write(struct pipe_inode_info *pipe, struct file *out,
                                 loff_t *ppos, size_t len, unsigned int flags)
{
        if (fuse_file_passthrough_op(out, FUSE_PASSTHROUGH_OP_WRITE))
                return fuse_passthrough_splice_write(pipe, out, ppos,
len, flags);
        else
                return iter_file_splice_write(pipe, out, ppos, len, flags);
}


> +           !(ff->open_flags & FOPEN_DIRECT_IO))
>                 return fuse_passthrough_splice_write(pipe, out, ppos, len, flags);
>         else
>                 return iter_file_splice_write(pipe, out, ppos, len, flags);
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 9e5142a94a09..a1034533ce60 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -1261,6 +1261,9 @@ struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
>  void fuse_file_release(struct inode *inode, struct fuse_file *ff,
>                        unsigned int open_flags, fl_owner_t id, bool isdir);
>
> +ssize_t fuse_direct_read_iter(struct kiocb *iocb, struct iov_iter *to);
> +ssize_t fuse_direct_write_iter(struct kiocb *iocb, struct iov_iter *from);
> +
>  /* passthrough.c */
>
>  /* READ/WRITE are implied by FOPEN_PASSTHROUGH, but defined for completeness */
> diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
> index f2025772c9c1..d62a1c751157 100644
> --- a/fs/fuse/passthrough.c
> +++ b/fs/fuse/passthrough.c
> @@ -44,6 +44,10 @@ ssize_t fuse_passthrough_read_iter(struct kiocb *iocb, struct iov_iter *iter)
>         if (!count)
>                 return 0;
>
> +       if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> +           !fuse_passthrough_op(file_inode(file), FUSE_READ))
> +               return fuse_direct_read_iter(iocb, iter);
> +
>         ret = backing_file_read_iter(backing_file, iter, iocb, iocb->ki_flags,
>                                      &ctx);
>
> @@ -70,6 +74,10 @@ ssize_t fuse_passthrough_write_iter(struct kiocb *iocb,
>         if (!count)
>                 return 0;
>
> +       if ((ff->open_flags & FOPEN_DIRECT_IO) ||
> +           !fuse_passthrough_op(inode, FUSE_WRITE))
> +               return fuse_direct_write_iter(iocb, iter);
> +
>         inode_lock(inode);
>         ret = backing_file_write_iter(backing_file, iter, iocb, iocb->ki_flags,
>                                       &ctx);
> @@ -91,6 +99,9 @@ ssize_t fuse_passthrough_splice_read(struct file *in, loff_t *ppos,
>         struct kiocb iocb;
>         ssize_t ret;
>
> +       if (!fuse_passthrough_op(file_inode(in), FUSE_READ))
> +               return copy_splice_read(in, ppos, pipe, len, flags);
> +
>         pr_debug("%s: backing_file=0x%p, pos=%lld, len=%zu, flags=0x%x\n", __func__,
>                  backing_file, *ppos, len, flags);
>

I understand why you brought this "jump back to direct" into the passthrough
helpers to reduce complexity in the call sites, but I think that if you use
the fuse_file_passthrough_op() helper in the call sites code will look
better and will not need to "jump back to direct io" in passthrough.c.

> @@ -133,6 +144,9 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
>  {
>         struct fuse_file *ff = file->private_data;
>         struct file *backing_file = fuse_file_passthrough(ff);
> +       struct inode *inode = file_inode(file);
> +       struct fuse_inode *fi = get_fuse_inode(inode);
> +       struct fuse_backing *fb = fuse_inode_backing(fi);
>         struct backing_file_ctx ctx = {
>                 .cred = ff->cred,
>                 .accessed = fuse_file_accessed,
> @@ -141,6 +155,10 @@ ssize_t fuse_passthrough_mmap(struct file *file, struct vm_area_struct *vma)
>         pr_debug("%s: backing_file=0x%p, start=%lu, end=%lu\n", __func__,
>                  backing_file, vma->vm_start, vma->vm_end);
>
> +       /* mmap requires both read and write passthrough */
> +       if ((fb->ops_mask & FUSE_PASSTHROUGH_RW_OPS) != FUSE_PASSTHROUGH_RW_OPS)
> +               return -ENODEV;
> +

Changing the helper in call site in nicer and already hits the
existing ENODEV case:

        if (fuse_file_passthrough_op(file, FUSE_PASSTHROUGH_RW_OPS))
                return fuse_passthrough_mmap(file, vma);
        else if (fuse_inode_backing(get_fuse_inode(inode)))
                return -ENODEV;

Thanks,
Amir.