On Thu, Oct 1, 2026 at 5:09 PM Miklos Szeredi <[email protected]> wrote:
>
> Conditions are triggered by a buggy fuse server will result in -EIO which
> is hard to interpret without any context.
>
> Add helpers that print a short message to the kernel log as well as
> returning the error value.  This serves a dual purpose:
>
>  - documents the error condition inside the code
>
>  - allows the implementor of the fuse server to get the context for the
>    error
>
> Some debug messages are removed in favor of this.
>
> This also changes the mmap return value from -ETXTBSY to -ENODEV if a
> passthrough open raced with the prior check.  This makes both cases return
> -ENODEV if the file is in passthrough mode.
>
> Additionally change the return value of fuse_inode_uncached_io_start() and
> fuse_file_cached_io_open() from an error value to a bool (true on success)
> as the error value is translated anyway.
>
> Currently these use the pr_notice_once() variant.  Possibly should be
> changed to a ratelimit of e.g. once per minute.
>
> Signed-off-by: Miklos Szeredi <[email protected]>
> ---
>  fs/fuse/file.c        |  8 ++---
>  fs/fuse/fuse_i.h      |  6 ++--
>  fs/fuse/iomode.c      | 74 ++++++++++++++++---------------------------
>  fs/fuse/passthrough.c | 19 ++++-------
>  4 files changed, 42 insertions(+), 65 deletions(-)
>
> diff --git a/fs/fuse/file.c b/fs/fuse/file.c
> index 6d707f2b3bff..92976906ab05 100644
> --- a/fs/fuse/file.c
> +++ b/fs/fuse/file.c
> @@ -1478,7 +1478,7 @@ static void fuse_dio_lock(struct kiocb *iocb, struct 
> iov_iter *from,
>                  * have raced, so check it again.
>                  */
>                 if (fuse_io_past_eof(iocb, from) ||
> -                   fuse_inode_uncached_io_start(fi, NULL) != 0) {
> +                   !fuse_inode_uncached_io_start(fi, NULL)) {
>                         inode_unlock_shared(inode);
>                         inode_lock(inode);
>                         *exclusive = true;
> @@ -2415,7 +2415,6 @@ static int fuse_file_mmap(struct file *file, struct 
> vm_area_struct *vma)
>         struct fuse_file *ff = file->private_data;
>         struct fuse_conn *fc = ff->fm->fc;
>         struct inode *inode = file_inode(file);
> -       int rc;
>
>         /* DAX mmap is superior to direct_io mmap */
>         if (FUSE_IS_VDAX(inode))
> @@ -2457,9 +2456,8 @@ static int fuse_file_mmap(struct file *file, struct 
> vm_area_struct *vma)
>                  * After first mmap, the inode stays in caching io mode until
>                  * the direct_io file release.
>                  */
> -               rc = fuse_file_cached_io_open(inode, ff);
> -               if (rc)
> -                       return rc;
> +               if (!fuse_file_cached_io_open(inode, ff))
> +                       return -ENODEV;
>         }
>
>         if ((vma->vm_flags & VM_SHARED) && (vma->vm_flags & VM_MAYWRITE))
> diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
> index 8546855386b5..3dd4dff24c50 100644
> --- a/fs/fuse/fuse_i.h
> +++ b/fs/fuse/fuse_i.h
> @@ -11,6 +11,8 @@
>  # define pr_fmt(fmt) "fuse: " fmt
>  #endif
>
> +#define fuse_EIO(fmt, ...) (pr_notice_once("%s: " fmt "\n", __func__, 
> ##__VA_ARGS__), -EIO)
> +
>  #include "args.h"
>  #include <linux/fuse.h>
>  #include <linux/fs.h>
> @@ -1251,8 +1253,8 @@ int fuse_fileattr_set(struct mnt_idmap *idmap,
>                       struct dentry *dentry, struct file_kattr *fa);
>
>  /* iomode.c */
> -int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
> -int fuse_inode_uncached_io_start(struct fuse_inode *fi,
> +bool fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff);
> +bool fuse_inode_uncached_io_start(struct fuse_inode *fi,
>                                  struct fuse_backing *fb);
>  void fuse_inode_uncached_io_end(struct fuse_inode *fi);
>
> diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
> index 79637c09e883..1a10bc65fb31 100644
> --- a/fs/fuse/iomode.c
> +++ b/fs/fuse/iomode.c
> @@ -27,15 +27,15 @@ static inline bool fuse_is_io_cache_wait(struct 
> fuse_inode *fi)
>   * Blocks new parallel dio writes and waits for the in-progress parallel dio
>   * writes to complete.
>   */
> -int fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
> +bool fuse_file_cached_io_open(struct inode *inode, struct fuse_file *ff)
>  {
>         struct fuse_inode *fi = get_fuse_inode(inode);
>
>         /* There are no io modes if server does not implement open */
>         if (!ff->args)
> -               return 0;
> +               return true;
>
> -       spin_lock(&fi->lock);
> +       guard(spinlock)(&fi->lock);
>         /*
>          * Setting the bit advises new direct-io writes to use an exclusive
>          * lock - without it the wait below might be forever.
> @@ -53,8 +53,7 @@ int fuse_file_cached_io_open(struct inode *inode, struct 
> fuse_file *ff)
>          */
>         if (fuse_inode_backing(fi)) {
>                 clear_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
> -               spin_unlock(&fi->lock);
> -               return -ETXTBSY;
> +               return false;
>         }
>
>         WARN_ON(ff->iomode == IOM_UNCACHED);
> @@ -64,8 +63,7 @@ int fuse_file_cached_io_open(struct inode *inode, struct 
> fuse_file *ff)
>                         set_bit(FUSE_I_CACHE_IO_MODE, &fi->state);
>                 fi->iocachectr++;
>         }
> -       spin_unlock(&fi->lock);
> -       return 0;
> +       return true;
>  }
>
>  static void fuse_file_cached_io_release(struct fuse_file *ff,
> @@ -82,22 +80,19 @@ static void fuse_file_cached_io_release(struct fuse_file 
> *ff,
>  }
>
>  /* Start strictly uncached io mode where cache access is not allowed */
> -int fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing 
> *fb)
> +bool fuse_inode_uncached_io_start(struct fuse_inode *fi, struct fuse_backing 
> *fb)
>  {
>         struct fuse_backing *oldfb;
> -       int err = 0;
>
> -       spin_lock(&fi->lock);
> +       guard(spinlock)(&fi->lock);
>         /* deny conflicting backing files on same fuse inode */
>         oldfb = fuse_inode_backing(fi);
> -       if (fb && oldfb && oldfb != fb) {
> -               err = -EBUSY;
> -               goto unlock;
> -       }
> -       if (fi->iocachectr > 0) {
> -               err = -ETXTBSY;
> -               goto unlock;
> -       }
> +       if (fb && oldfb && oldfb != fb)
> +               return false;
> +
> +       if (fi->iocachectr > 0)
> +               return false;
> +
>         fi->iocachectr--;
>
>         /* fuse inode holds a single refcount of backing file */
> @@ -107,9 +102,7 @@ int fuse_inode_uncached_io_start(struct fuse_inode *fi, 
> struct fuse_backing *fb)
>         } else {
>                 fuse_backing_put(fb);
>         }
> -unlock:
> -       spin_unlock(&fi->lock);
> -       return err;
> +       return true;
>  }
>
>  /* Takes uncached_io inode mode reference to be dropped on file release */
> @@ -118,11 +111,9 @@ static int fuse_file_uncached_io_open(struct inode 
> *inode,
>                                       struct fuse_backing *fb)
>  {
>         struct fuse_inode *fi = get_fuse_inode(inode);
> -       int err;
>
> -       err = fuse_inode_uncached_io_start(fi, fb);
> -       if (err)
> -               return err;
> +       if (!fuse_inode_uncached_io_start(fi, fb))
> +               return fuse_EIO("failed to start uncached I/O");

Why lose the internal error code?
If server implementation is wrong that is a good hint of what went wrong.
ETXTBSY is trying to mix cache mode and passthrough mode.
EBUSY is trying to passthrough to two different backing inodes.
I am not claiming this is a good API but at least we (or LLM) could do
something useful with this lost information.

>
>         WARN_ON(ff->iomode != IOM_NONE);
>         ff->iomode = IOM_UNCACHED;
> @@ -173,9 +164,11 @@ static int fuse_file_passthrough_open(struct inode 
> *inode, struct file *file)
>         int err;
>
>         /* Check allowed conditions for file open in passthrough mode */
> -       if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH) || !fc->passthrough ||
> -           (ff->open_flags & ~FOPEN_PASSTHROUGH_MASK))
> -               return -EINVAL;
> +       if (!IS_ENABLED(CONFIG_FUSE_PASSTHROUGH) || !fc->passthrough)
> +               return fuse_EIO("passthrough not enabled");
> +
> +       if (ff->open_flags & ~FOPEN_PASSTHROUGH_MASK)
> +               return fuse_EIO("conflicting open flags");
>
>         fb = fuse_passthrough_open(file, ff->args->open_outarg.backing_id);
>         if (IS_ERR(fb))
> @@ -208,11 +201,12 @@ int fuse_file_io_open(struct file *file, struct inode 
> *inode)
>
>         /*
>          * Server is expected to use FOPEN_PASSTHROUGH for all opens of an 
> inode
> -        * which is already open for passthrough.
> +        * which is already open for passthrough.  Using incorrect open mode 
> is
> +        * a server mistake, which results in user visible failure of open()
> +        * with EIO error.
>          */
> -       err = -EINVAL;
>         if (fuse_inode_backing(fi) && !(ff->open_flags & FOPEN_PASSTHROUGH))
> -               goto fail;
> +               return fuse_EIO("FOPEN_PASSTHROUGH expected");
>
>         /*
>          * FOPEN_PARALLEL_DIRECT_WRITES requires FOPEN_DIRECT_IO.
> @@ -234,22 +228,10 @@ int fuse_file_io_open(struct file *file, struct inode 
> *inode)
>
>         if (ff->open_flags & FOPEN_PASSTHROUGH)
>                 err = fuse_file_passthrough_open(inode, file);
> -       else
> -               err = fuse_file_cached_io_open(inode, ff);
> -       if (err)
> -               goto fail;
> +       else if (!fuse_file_cached_io_open(inode, ff))
> +               err = fuse_EIO("conflicting passthrough open");

Same here.
If we can keep this bit of information and print it - it is helpful IMO,
even if we don't go all the way to annotate every error case with
a pretty log.

Thanks,
Amir.

Reply via email to