From: John Groves <[email protected]>

Add a dedicated ioctl, FUSE_DEV_IOC_DAXDEV_OPEN, by which a fuse server
registers the devdax devices that back an fs-dax (famfs) filesystem: the
server hands the kernel an fd to a /dev/daxN.N device plus its
cluster-invariant famfs index.

A dedicated ioctl, rather than overloading FUSE_DEV_IOC_BACKING_OPEN,
avoids a dependency on CONFIG_FUSE_PASSTHROUGH and the backing-file
machinery, which famfs does not use.

- struct fuse_backing_map is reused as the argument; the index rides on
  the reserved 'padding' field (daxdev_index).
- fuse_dev_ioctl_daxdev_open() copies the map and calls
  famfs_daxdev_open(), which resolves the fd to a dax device by its inode
  i_rdev. The daxdev table store is added in the following patch.

Access control:

- The ioctl is gated on famfs mode (enabled at FUSE_INIT only when the
  server held CAP_SYS_RAWIO), and additionally re-checks
  capable(CAP_SYS_RAWIO) on each call. The famfs-mode flag attests only
  to the session founder's privilege; the fuse device fd may be inherited
  across fork() or SCM_RIGHTS-passed to a less-privileged task, so the
  capability is re-checked against the task actually performing the
  registration.
- famfs_daxdev_open() resolves the fd with fget(), not fget_raw(), so
  O_PATH fds are rejected. A successful lookup then proves the caller
  holds an fd from a real open() of the daxdev -- i.e. it already passed
  may_open_dev() and the device node's DAC checks -- rather than an
  O_PATH reference that bypasses them.

Signed-off-by: John Groves <[email protected]>
---
 fs/fuse/dev.c             |  31 ++++
 fs/fuse/dev.h             |   1 +
 fs/fuse/famfs.c           | 330 ++++++++++++++++++++++++++++++++++++++
 fs/fuse/famfs_kfmap.h     |  27 ++++
 fs/fuse/fuse_i.h          |  28 ++++
 fs/fuse/inode.c           |   7 +-
 include/uapi/linux/fuse.h |   7 +-
 7 files changed, 429 insertions(+), 2 deletions(-)

diff --git a/fs/fuse/dev.c b/fs/fuse/dev.c
index 5763a7cd3b37..3e6aa15e0346 100644
--- a/fs/fuse/dev.c
+++ b/fs/fuse/dev.c
@@ -2330,6 +2330,34 @@ static long fuse_dev_ioctl_backing_close(struct file 
*file, __u32 __user *argp)
        return fuse_backing_close(fud->chan->conn, backing_id);
 }
 
+static long fuse_dev_ioctl_daxdev_open(struct file *file,
+                                      struct fuse_backing_map __user *argp)
+{
+       struct fuse_dev *fud = fuse_get_dev(file);
+       struct fuse_backing_map map;
+
+       if (IS_ERR(fud))
+               return PTR_ERR(fud);
+
+       if (!IS_ENABLED(CONFIG_FUSE_FAMFS_DAX))
+               return -EOPNOTSUPP;
+
+       /*
+        * The famfs-mode gate (fc->famfs_iomap) lives in famfs_daxdev_open(),
+        * which has the full fuse_conn definition. Here, re-check CAP_SYS_RAWIO
+        * against the task performing the registration: famfs mode being 
enabled
+        * only attests that the session founder held it at FUSE_INIT, and the
+        * fuse device fd may have been passed to a less-privileged process.
+        */
+       if (!capable(CAP_SYS_RAWIO))
+               return -EPERM;
+
+       if (copy_from_user(&map, argp, sizeof(map)))
+               return -EFAULT;
+
+       return famfs_daxdev_open(fud->chan->conn, &map);
+}
+
 static long fuse_dev_ioctl_sync_init(struct file *file)
 {
        struct fuse_dev *fud = fuse_file_to_fud(file);
@@ -2359,6 +2387,9 @@ static long fuse_dev_ioctl(struct file *file, unsigned 
int cmd,
        case FUSE_DEV_IOC_SYNC_INIT:
                return fuse_dev_ioctl_sync_init(file);
 
+       case FUSE_DEV_IOC_DAXDEV_OPEN:
+               return fuse_dev_ioctl_daxdev_open(file, argp);
+
        default:
                return -ENOTTY;
        }
diff --git a/fs/fuse/dev.h b/fs/fuse/dev.h
index aed69fd14c41..545940b635cc 100644
--- a/fs/fuse/dev.h
+++ b/fs/fuse/dev.h
@@ -87,6 +87,7 @@ int fuse_notify(struct fuse_conn *fc, enum fuse_notify_code 
code,
 
 int fuse_backing_open(struct fuse_conn *fc, struct fuse_backing_map *map);
 int fuse_backing_close(struct fuse_conn *fc, int backing_id);
+int famfs_daxdev_open(struct fuse_conn *fc, struct fuse_backing_map *map);
 
 int fuse_copy_one(struct fuse_copy_state *cs, void *val, unsigned size);
 int fuse_copy_folio(struct fuse_copy_state *cs, struct folio **foliop,
diff --git a/fs/fuse/famfs.c b/fs/fuse/famfs.c
index 8f7ee7d6151b..a2a7dd631dc0 100644
--- a/fs/fuse/famfs.c
+++ b/fs/fuse/famfs.c
@@ -11,6 +11,7 @@
 
 #include <linux/cleanup.h>
 #include <linux/fs.h>
+#include <linux/file.h>
 #include <linux/mm.h>
 #include <linux/dax.h>
 #include <linux/iomap.h>
@@ -22,6 +23,331 @@
 #include "famfs_kfmap.h"
 #include "fuse_i.h"
 
+static void famfs_set_daxdev_err(
+       struct fuse_conn *fc, struct dax_device *dax_devp);
+
+static int
+famfs_dax_notify_failure(struct dax_device *dax_devp, u64 offset,
+                       u64 len, int mf_flags)
+{
+       struct fuse_conn *fc = dax_holder(dax_devp);
+
+       famfs_set_daxdev_err(fc, dax_devp);
+
+       return 0;
+}
+
+static const struct dax_holder_operations famfs_fuse_dax_holder_ops = {
+       .notify_failure         = famfs_dax_notify_failure,
+};
+
+/*****************************************************************************/
+
+/*
+ * famfs_teardown()
+ *
+ * Deallocate famfs metadata for a fuse_conn
+ */
+void
+famfs_teardown(struct fuse_conn *fc)
+{
+       struct famfs_dax_devlist *devlist __free(kfree) = NULL;
+       int i;
+
+       /*
+        * Detach the table under the same lock famfs_set_daxdev_err() takes, so
+        * a notify_failure racing teardown either runs first against the live
+        * table or observes dax_devlist == NULL and bails, rather than
+        * dereferencing it after we clear it. The daxdev holders are dropped
+        * below, after which no further notify_failure can arrive.
+        */
+       scoped_guard(rwsem_write, &fc->famfs_devlist_sem) {
+               devlist = fc->dax_devlist;
+               fc->dax_devlist = NULL;
+       }
+
+       if (!devlist)
+               return;
+
+       if (!devlist->devlist)
+               return;
+
+       /* Close & release all the daxdevs in our table */
+       for (i = 0; i < devlist->nslots; i++) {
+               struct famfs_daxdev *dd = &devlist->devlist[i];
+
+               if (!dd->valid)
+                       continue;
+
+               /* Only call fs_put_dax if fs_dax_get succeeded */
+               if (dd->devp) {
+                       if (!dd->dax_err)
+                               fs_put_dax(dd->devp, fc);
+                       put_dax(dd->devp);
+               }
+
+               kfree(dd->name);
+       }
+       kfree(devlist->devlist);
+}
+
+/* Allocate the daxdev table on first use (idempotent via cmpxchg) */
+static int famfs_devlist_alloc(struct fuse_conn *fc)
+{
+       struct famfs_dax_devlist *devlist;
+
+       if (fc->dax_devlist)
+               return 0;
+
+       devlist = kcalloc(1, sizeof(*devlist), GFP_KERNEL);
+       if (!devlist)
+               return -ENOMEM;
+
+       devlist->nslots = MAX_DAXDEVS;
+       devlist->devlist = kcalloc(MAX_DAXDEVS, sizeof(struct famfs_daxdev),
+                                  GFP_KERNEL);
+       if (!devlist->devlist) {
+               kfree(devlist);
+               return -ENOMEM;
+       }
+
+       /* If another thread allocated it first, drop ours */
+       if (cmpxchg(&fc->dax_devlist, NULL, devlist) != NULL) {
+               kfree(devlist->devlist);
+               kfree(devlist);
+       }
+
+       return 0;
+}
+
+/*
+ * famfs_install_daxdev() - exclusively acquire a resolved daxdev and publish
+ * it in the table at @index. Shared by the GET_DAXDEV (pull) and
+ * DAXDEV_OPEN (push) registration paths.
+ *
+ * Serializes with concurrent installers under famfs_devlist_sem and rechecks
+ * ->valid. A daxdev is entered in the table only once it has been exclusively
+ * acquired via fs_dax_get(); on failure the dax_dev_get() reference is
+ * released and the slot is left invalid, so the referencing fmap is rejected
+ * rather than mapped without an exclusive holder. @name may be NULL (the push
+ * path passes no pathname).
+ */
+static int famfs_install_daxdev(struct fuse_conn *fc, u64 index, dev_t devno,
+                               const char *name)
+{
+       struct famfs_daxdev *daxdev;
+       int rc = 0;
+
+       if (index >= fc->dax_devlist->nslots) {
+               pr_err("%s: index(%llu) >= nslots(%d)\n",
+                      __func__, index, fc->dax_devlist->nslots);
+               return -EINVAL;
+       }
+
+       scoped_guard(rwsem_write, &fc->famfs_devlist_sem) {
+               daxdev = &fc->dax_devlist->devlist[index];
+
+               /* Installed already by a concurrent push/pull */
+               if (daxdev->valid)
+                       return 0;
+
+               /*
+                * A prior attempt already determined this daxdev cannot be
+                * exclusively acquired (see the fs_dax_get() failure handling
+                * below). Don't thrash on GET_DAXDEV/fs_dax_get(); fail fast.
+                */
+               if (daxdev->dax_err)
+                       return -EIO;
+
+               /*
+                * Temporary: dax_dev_get() is the exported upstream lookup, but
+                * unlike dax_dev_find() it allocates for any dev_t and does not
+                * reject non-dax devices. Restore dax_dev_find() (and that
+                * rejection) once it is upstream.
+                */
+               daxdev->devp = dax_dev_get(devno);
+               if (!daxdev->devp) {
+                       pr_warn("%s: device %u:%u not found or not dax\n",
+                               __func__, MAJOR(devno), MINOR(devno));
+                       return -ENODEV;
+               }
+
+               rc = fs_dax_get(daxdev->devp, fc, &famfs_fuse_dax_holder_ops);
+               if (rc) {
+                       /*
+                        * Distinguish a lost race from a real failure. -EBUSY
+                        * with the daxdev already held by *this* fuse_conn
+                        * means a concurrent acquire won and will publish the
+                        * slot valid: not an error, and must not be cached as
+                        * dax_err. Any other failure (foreign holder, not a dax
+                        * device, wrong driver type) is permanent for this
+                        * connection, so record dax_err to stop re-fetching and
+                        * re-acquiring it.
+                        */
+                       if (!(rc == -EBUSY && dax_holder(daxdev->devp) == fc)) {
+                               pr_err("%s: fs_dax_get(%u:%u) failed rc=%d\n",
+                                      __func__, MAJOR(devno), MINOR(devno), 
rc);
+                               daxdev->dax_err = true;
+                       }
+                       put_dax(daxdev->devp);
+                       daxdev->devp = NULL;
+                       return rc;
+               }
+
+               daxdev->devno = devno;
+               if (name) {
+                       daxdev->name = kstrdup(name, GFP_KERNEL);
+                       if (!daxdev->name) {
+                               fs_put_dax(daxdev->devp, fc);
+                               put_dax(daxdev->devp);
+                               daxdev->devp = NULL;
+                               return -ENOMEM;
+                       }
+               }
+
+               wmb(); /* All other fields must be visible before valid */
+               daxdev->valid = 1;
+       }
+
+       return 0;
+}
+
+/**
+ * famfs_daxdev_open() - Register a daxdev via FUSE_DEV_IOC_DAXDEV_OPEN
+ * @fc:   fuse_conn
+ * @map:  fuse_backing_map; @map->fd is an fd to the devdax device and
+ *        @map->daxdev_index is the (cluster-invariant) famfs index.
+ *
+ * The server pushes a daxdev to the kernel by reference (an fd), rather than
+ * the kernel pulling it by name via GET_DAXDEV. The resolved daxdev is
+ * exclusively acquired and entered in the table at @map->daxdev_index.
+ *
+ * Return: 0=success
+ *         -errno=failure
+ */
+int famfs_daxdev_open(struct fuse_conn *fc, struct fuse_backing_map *map)
+{
+       struct inode *inode;
+       struct file *file;
+       dev_t devno;
+       int rc;
+
+       /* Only fs-dax (famfs) mode accepts daxdev registration */
+       if (!fc->famfs_iomap)
+               return -EOPNOTSUPP;
+
+       file = fget(map->fd);
+       if (!file)
+               return -EBADF;
+
+       inode = file_inode(file);
+       if (!S_ISCHR(inode->i_mode)) {
+               fput(file);
+               return -EINVAL;
+       }
+       devno = inode->i_rdev;
+       fput(file);
+
+       rc = famfs_devlist_alloc(fc);
+       if (rc)
+               return rc;
+
+       rc = famfs_install_daxdev(fc, map->daxdev_index, devno, NULL);
+       if (rc)
+               pr_err("%s: failed to install daxdev\n", __func__);
+
+       return rc;
+}
+
+/**
+ * famfs_check_daxdev_table() - Verify an fmap's referenced daxdevs are 
installed
+ * @fm:   fuse_mount
+ * @meta: famfs_file_meta, in-memory format, built from a GET_FMAP response
+ *
+ * Called for each new file fmap. Every daxdev the fmap references must already
+ * be installed in the table, having been pushed in via 
FUSE_DEV_IOC_DAXDEV_OPEN
+ * before any file that uses it is accessed. If any referenced daxdev is not
+ * present, the fmap is rejected so the file is never mapped against a daxdev
+ * that has no exclusive holder.
+ *
+ * Return: 0=success (all referenced daxdevs present)
+ *         <0=a referenced daxdev is missing from the table
+ */
+static int
+famfs_check_daxdev_table(
+       struct fuse_mount *fm,
+       const struct famfs_file_meta *meta)
+{
+       struct fuse_conn *fc = fm->fc;
+       int nmissing = 0;
+       int err;
+
+       err = famfs_devlist_alloc(fc);
+       if (err)
+               return err;
+
+       /* Count missing daxdevs while holding the reader lock */
+       scoped_guard(rwsem_read, &fc->famfs_devlist_sem) {
+               unsigned long i;
+
+               for_each_set_bit(i, (unsigned long *)&meta->dev_bitmap,
+                                MAX_DAXDEVS) {
+                       struct famfs_daxdev *dd = &fc->dax_devlist->devlist[i];
+
+                       /*
+                        * Skip daxdevs already installed (valid) or already
+                        * known to be unusable (dax_err). Re-fetching either
+                        * just thrashes on GET_DAXDEV and fs_dax_get().
+                        */
+                       if (!dd->valid && !dd->dax_err)
+                               nmissing++;
+               }
+       }
+
+       if (nmissing > 0) {
+               /* this file referenced at least one daxdev that is not in
+                * the table. Daxdevs must be known before any file that
+                * uses them is accessed
+                */
+               pr_err("%s: %d missing daxdev(s)\n", __func__, nmissing);
+               return -ENODEV;
+       }
+
+       return 0;
+}
+
+static void
+famfs_set_daxdev_err(
+       struct fuse_conn *fc,
+       struct dax_device *dax_devp)
+{
+       int i;
+
+       /*
+        * Search the list by dax_devp under the write lock: we set dd->error,
+        * and it serializes against famfs_teardown() clearing the table.
+        */
+       scoped_guard(rwsem_write, &fc->famfs_devlist_sem) {
+               if (!fc->dax_devlist)
+                       return;
+               for (i = 0; i < fc->dax_devlist->nslots; i++) {
+                       if (fc->dax_devlist->devlist[i].valid) {
+                               struct famfs_daxdev *dd;
+
+                               dd = &fc->dax_devlist->devlist[i];
+                               if (dd->devp != dax_devp)
+                                       continue;
+
+                               dd->error = true;
+
+                               pr_err("%s: memory error on daxdev %s (%d)\n",
+                                      __func__, dd->name, i);
+                               return;
+                       }
+               }
+       }
+       pr_err("%s: memory err on unrecognized daxdev\n", __func__);
+}
 
 /***************************************************************************/
 
@@ -228,6 +554,10 @@ famfs_file_init_dax(
        if (rc)
                goto errout;
 
+       /* Make sure this fmap doesn't reference any unknown daxdevs */
+       if (famfs_check_daxdev_table(fm, meta))
+               meta->error = true;
+
        /* Publish the famfs metadata on fi->famfs_meta */
        inode_lock(inode);
 
diff --git a/fs/fuse/famfs_kfmap.h b/fs/fuse/famfs_kfmap.h
index d87b065e8ac8..6b78e78325c8 100644
--- a/fs/fuse/famfs_kfmap.h
+++ b/fs/fuse/famfs_kfmap.h
@@ -61,4 +61,31 @@ struct famfs_file_meta {
        struct famfs_meta_simple_ext  *se;
 };
 
+/*
+ * famfs_daxdev - tracking struct for a daxdev within a famfs file system
+ *
+ * This is the in-memory daxdev metadata that is populated by parsing
+ * the responses to GET_FMAP messages
+ */
+struct famfs_daxdev {
+       /* Include dev uuid? */
+       bool valid;
+       bool error; /* Dax has reported a memory error (probably poison) */
+       bool dax_err; /* fs_dax_get() failed */
+       dev_t devno;
+       struct dax_device *devp;
+       char *name;
+};
+
+#define MAX_DAXDEVS 24
+
+/*
+ * famfs_dax_devlist - list of famfs_daxdev's
+ */
+struct famfs_dax_devlist {
+       int nslots;
+       int ndevs;
+       struct famfs_daxdev *devlist;
+};
+
 #endif /* FAMFS_KFMAP_H */
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 46a7040b38dc..5394aae9dbac 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -773,6 +773,11 @@ struct fuse_conn {
        /** @backing_files_map: IDR for backing files ids */
        struct idr backing_files_map;
 #endif
+
+#if IS_ENABLED(CONFIG_FUSE_FAMFS_DAX)
+       struct rw_semaphore famfs_devlist_sem;
+       struct famfs_dax_devlist *dax_devlist;
+#endif
 };
 
 /*
@@ -1352,6 +1357,8 @@ int famfs_file_init_dax(struct fuse_mount *fm,
                        size_t fmap_size);
 void __famfs_meta_free(void *map);
 
+void famfs_teardown(struct fuse_conn *fc);
+
 /* Set fi->famfs_meta = NULL regardless of prior value */
 static inline void famfs_meta_init(struct fuse_inode *fi)
 {
@@ -1373,6 +1380,11 @@ static inline void famfs_meta_free(struct fuse_inode *fi)
        }
 }
 
+static inline void famfs_init_devlist_sem(struct fuse_conn *fc)
+{
+       init_rwsem(&fc->famfs_devlist_sem);
+}
+
 static inline int fuse_file_famfs(struct fuse_inode *fi)
 {
        return (READ_ONCE(fi->famfs_meta) != NULL);
@@ -1380,8 +1392,14 @@ static inline int fuse_file_famfs(struct fuse_inode *fi)
 
 int fuse_get_fmap(struct fuse_mount *fm, struct inode *inode);
 
+int famfs_daxdev_open(struct fuse_conn *fc, struct fuse_backing_map *map);
+
 #else /* !CONFIG_FUSE_FAMFS_DAX */
 
+static inline void famfs_teardown(struct fuse_conn *fc)
+{
+}
+
 static inline struct fuse_backing *famfs_meta_set(struct fuse_inode *fi,
                                                  void *meta)
 {
@@ -1392,6 +1410,10 @@ static inline void famfs_meta_free(struct fuse_inode *fi)
 {
 }
 
+static inline void famfs_init_devlist_sem(struct fuse_conn *fc)
+{
+}
+
 static inline int fuse_file_famfs(struct fuse_inode *fi)
 {
        return 0;
@@ -1403,6 +1425,12 @@ fuse_get_fmap(struct fuse_mount *fm, struct inode *inode)
        return 0;
 }
 
+static inline int
+famfs_daxdev_open(struct fuse_conn *fc, struct fuse_backing_map *map)
+{
+       return -EOPNOTSUPP;
+}
+
 #endif /* CONFIG_FUSE_FAMFS_DAX */
 
 #endif /* _FS_FUSE_I_H */
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 78ffc5fd50d0..9fc37015fb11 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -1021,6 +1021,9 @@ void fuse_conn_put(struct fuse_conn *fc)
                WARN_ON(atomic_read(&bucket->count) != 1);
                kfree(bucket);
        }
+       if (IS_ENABLED(CONFIG_FUSE_FAMFS_DAX))
+               famfs_teardown(fc);
+
        if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
                fuse_backing_files_free(fc);
        call_rcu(&fc->rcu, delayed_release);
@@ -1427,8 +1430,10 @@ static void process_init_reply(struct fuse_args *args, 
int error)
                                u64 in_flags = FIELD_PREP(GENMASK_ULL(63, 32), 
ia->in.flags2)
                                                | ia->in.flags;
 
-                               if (in_flags & FUSE_DAX_FMAP)
+                               if (in_flags & FUSE_DAX_FMAP) {
+                                       famfs_init_devlist_sem(fc);
                                        fc->famfs_iomap = 1;
+                               }
                        }
                } else {
                        ra_pages = fc->max_read / PAGE_SIZE;
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 4b84a58a8f1c..a143e6818416 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -1143,7 +1143,10 @@ struct fuse_notify_prune_out {
 struct fuse_backing_map {
        int32_t         fd;
        uint32_t        flags;
-       uint64_t        padding;
+       union {
+               uint64_t        padding;
+               uint64_t        daxdev_index;   /* FUSE_DEV_IOC_DAXDEV_OPEN */
+       };
 };
 
 /* Device ioctls: */
@@ -1153,6 +1156,8 @@ struct fuse_backing_map {
                                             struct fuse_backing_map)
 #define FUSE_DEV_IOC_BACKING_CLOSE     _IOW(FUSE_DEV_IOC_MAGIC, 2, uint32_t)
 #define FUSE_DEV_IOC_SYNC_INIT         _IO(FUSE_DEV_IOC_MAGIC, 3)
+#define FUSE_DEV_IOC_DAXDEV_OPEN       _IOW(FUSE_DEV_IOC_MAGIC, 4, \
+                                            struct fuse_backing_map)
 
 struct fuse_lseek_in {
        uint64_t        fh;
-- 
2.53.0



Reply via email to