[PATCH V11 3/9] famfs_fuse: Plumb the GET_FMAP message/response

John Groves <[email protected]>
Newsgroups org.kernel.vger.linux-cxl,dev.linux.lists.fuse-devel,dev.linux.lists.nvdimm,org.kernel.vger.linux-doc,org.kernel.vger.linux-fsdevel,org.kernel.vger.linux-kernel
Message-ID <0100019f7da14ceb-fbd4edd7-ce17-4770-8c58-1296ad3e3fbe-000000@email.amazonses.com>
From: John Groves <[email protected]>

On completion of an OPEN in famfs mode, issue a GET_FMAP request to the
server to retrieve the file's file-offset-to-dax map (fmap) and cache it
on the fuse_inode (fi->famfs_meta). Once the map is cached, read, write
and mmap are resolved directly to dax with no further upcalls.

- uapi: add the FUSE_GET_FMAP opcode.
- famfs.c: add fuse_get_fmap(), which retrieves the fmap into a
  kvmalloc'd buffer. The fmap size is not known in advance, so it uses a
  size probe: it starts with a PAGE_SIZE buffer and passes that size to
  the server (via fuse_getxattr_in.size). If the whole fmap does not
  fit, the server replies with just the header, whose fmap_size field
  reports the required size, and the kernel reallocates exactly that and
  retries once. The reply is bounded by FMAP_BUFSIZE_MAX (16 MiB); a
  larger fmap is rejected with -EFBIG. A famfs file is fixed-size, so a
  reply that reports a different size on the retry is rejected as a
  server bug.
- file.c: hook the OPEN path to fetch the fmap for regular files on a
  famfs connection; failure is fatal to the open.
- fuse_i.h/inode.c: add the fi->famfs_meta pointer and its init/free
  helpers.

The retrieved map is parsed into its in-memory form in the following
patch.

Signed-off-by: John Groves <[email protected]>
---
 MAINTAINERS               |   7 ++
 fs/fuse/Makefile          |   1 +
 fs/fuse/famfs.c           | 134 ++++++++++++++++++++++++++++++++++++++
 fs/fuse/file.c            |  14 +++-
 fs/fuse/fuse_i.h          |  70 ++++++++++++++++++--
 fs/fuse/inode.c           |   8 ++-
 fs/fuse/iomode.c          |   2 +-
 include/uapi/linux/fuse.h |   3 +
 8 files changed, 231 insertions(+), 8 deletions(-)
 create mode 100644 fs/fuse/famfs.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 806bd2d80d15..0d0fded4fddb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10713,6 +10713,13 @@ F:	fs/fuse/backing.c
 F:	fs/fuse/iomode.c
 F:	fs/fuse/passthrough.c
 
+FUSE FILESYSTEM [FAMFS Fabric-Attached Memory File System]
+M:	John Groves <[email protected]>
+L:	[email protected]
+L:	[email protected]
+S:	Supported
+F:	fs/fuse/famfs.c
+
 FUTEX SUBSYSTEM
 M:	Thomas Gleixner <[email protected]>
 M:	Ingo Molnar <[email protected]>
diff --git a/fs/fuse/Makefile b/fs/fuse/Makefile
index 245e67852b03..66507b9cfe1f 100644
--- a/fs/fuse/Makefile
+++ b/fs/fuse/Makefile
@@ -18,5 +18,6 @@ fuse-$(CONFIG_FUSE_DAX) += dax.o
 fuse-$(CONFIG_FUSE_PASSTHROUGH) += passthrough.o backing.o
 fuse-$(CONFIG_SYSCTL) += sysctl.o
 fuse-$(CONFIG_FUSE_IO_URING) += dev_uring.o
+fuse-$(CONFIG_FUSE_FAMFS_DAX) += famfs.o
 
 virtiofs-y := virtio_fs.o
diff --git a/fs/fuse/famfs.c b/fs/fuse/famfs.c
new file mode 100644
index 000000000000..80e6640ac970
--- /dev/null
+++ b/fs/fuse/famfs.c
@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * famfs - dax file system for shared fabric-attached memory
+ *
+ * Copyright 2023-2026 Micron Technology, Inc.
+ *
+ * This file system, originally based on ramfs the dax support from xfs,
+ * is intended to allow multiple host systems to mount a common file system
+ * view of dax files that map to shared memory.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/dax.h>
+#include <linux/iomap.h>
+#include <linux/path.h>
+#include <linux/namei.h>
+#include <linux/string.h>
+
+#include "fuse_i.h"
+
+
+#define FMAP_BUFSIZE_INIT PAGE_SIZE
+/*
+ * Largest GET_FMAP reply buffer we will kvmalloc. Any fmap whose whole message
+ * fits in this buffer is handled; there is no separate extent-count cap, so the
+ * effective extent limit is just this size / sizeof(simple_ext) (~699k extents
+ * => ~1.3 TiB per striped file at a 2 MiB chunk). kvmalloc-backed, so it may
+ * exceed the contiguous kmalloc limit. Matches the server's reply-buffer cap.
+ */
+#define FMAP_BUFSIZE_MAX (16 * 1024 * 1024)
+
+int fuse_get_fmap(struct fuse_mount *fm, struct inode *inode)
+{
+	struct fuse_inode *fi = get_fuse_inode(inode);
+	u64 nodeid = get_node_id(inode);
+	size_t bufsize = FMAP_BUFSIZE_INIT;
+	void *fmap_buf = NULL;
+	ssize_t fmap_size;
+	int attempt;
+	int rc;
+
+	/* Don't retrieve if we already have the famfs metadata */
+	if (fi->famfs_meta)
+		return 0;
+
+	/*
+	 * The fmap size is not known in advance. Start with a modest buffer and,
+	 * if the server reports (via the returned header's fmap_size) that the
+	 * whole fmap did not fit, reallocate exactly that size and retry once.
+	 * The server learns our buffer size from the request's
+	 * fuse_getxattr_in.size (GETXATTR-style size probe).
+	 */
+	for (attempt = 0; ; attempt++) {
+		struct fuse_getxattr_in in = { .size = bufsize };
+		struct fuse_famfs_fmap_header *fmh;
+		u32 required;
+
+		FUSE_ARGS(args);
+
+		fmap_buf = kvmalloc(bufsize, GFP_KERNEL);
+		if (!fmap_buf)
+			return -ENOMEM;
+
+		args.opcode = FUSE_GET_FMAP;
+		args.nodeid = nodeid;
+		args.in_numargs = 1;
+		args.in_args[0].size = sizeof(in);
+		args.in_args[0].value = &in;
+		/*
+		 * Variable-sized output buffer; fuse_simple_request() returns
+		 * the size of the output payload.
+		 */
+		args.out_argvar = true;
+		args.out_numargs = 1;
+		args.out_args[0].size = bufsize;
+		args.out_args[0].value = fmap_buf;
+
+		rc = fuse_simple_request(fm, &args);
+		if (rc < 0) {
+			pr_err("%s: err=%d from fuse_simple_request()\n",
+			       __func__, rc);
+			kvfree(fmap_buf);
+			return rc;
+		}
+		fmap_size = rc;
+
+		/* Need at least a header to learn the required size */
+		if (fmap_size < (ssize_t)sizeof(*fmh)) {
+			pr_err("%s: short fmap reply %zd\n", __func__, fmap_size);
+			kvfree(fmap_buf);
+			return -EIO;
+		}
+
+		fmh = fmap_buf;
+		required = fmh->fmap_size;
+
+		/* Whole fmap fit in the buffer -> parse it */
+		if (required <= bufsize)
+			break;
+
+		/* Too small: server sent only the header. Grow and retry once. */
+		kvfree(fmap_buf);
+		fmap_buf = NULL;
+
+		if (required > FMAP_BUFSIZE_MAX) {
+			pr_err("%s: fmap size %u exceeds max %zu\n",
+			       __func__, required, (size_t)FMAP_BUFSIZE_MAX);
+			return -EFBIG;
+		}
+		if (attempt >= 1) {
+			/*
+			 * A famfs file is fixed-size, so the server must report
+			 * the same fmap_size on the retry as on the first
+			 * request. A larger value means the file's size/fmap
+			 * changed between the two GET_FMAPs -- a server bug.
+			 */
+			pr_err("%s: fmap grew %zu -> %u across GET_FMAP retries; famfs file size must not change (server bug)\n",
+			       __func__, bufsize, required);
+			return -EINVAL;
+		}
+		bufsize = required;
+	}
+
+	/* We retrieved the "fmap" (the file's map to memory), but
+	 * we haven't used it yet. A call to famfs_file_init_dax() will be added
+	 * here in a subsequent patch, when we add the ability to attach
+	 * fmaps to files.
+	 */
+
+	kvfree(fmap_buf);
+	return 0;
+}
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 995e37c935c6..b4e7b6a64587 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -282,6 +282,16 @@ static int fuse_open(struct inode *inode, struct file *file)
 	err = fuse_do_open(fm, get_node_id(inode), file, false);
 	if (!err) {
 		ff = file->private_data;
+
+		if ((fm->fc->famfs_iomap) && (S_ISREG(inode->i_mode))) {
+			/* Get the famfs fmap - failure is fatal */
+			err = fuse_get_fmap(fm, inode);
+			if (err) {
+				fuse_sync_release(fi, ff, file->f_flags);
+				goto out_nowrite;
+			}
+		}
+
 		err = fuse_finish_open(inode, file);
 		if (err)
 			fuse_sync_release(fi, ff, file->f_flags);
@@ -289,12 +299,14 @@ static int fuse_open(struct inode *inode, struct file *file)
 			fuse_truncate_update_attr(inode, file);
 	}
 
+out_nowrite:
 	if (is_wb_truncate || dax_truncate)
 		fuse_release_nowrite(inode);
 	if (!err) {
 		if (is_truncate)
 			truncate_pagecache(inode, 0);
-		else if (!(ff->open_flags & FOPEN_KEEP_CACHE))
+		else if (!(ff->open_flags & FOPEN_KEEP_CACHE) &&
+			 !fuse_file_famfs(fi))
 			invalidate_inode_pages2(inode->i_mapping);
 	}
 	if (dax_truncate)
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 9c354118c931..5bacc5098620 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -236,6 +236,14 @@ struct fuse_inode {
 	 * be modified, so preserve the blocksize specified by the server.
 	 */
 	u8 cached_i_blkbits;
+
+#if IS_ENABLED(CONFIG_FUSE_FAMFS_DAX)
+	/* Pointer to the file's famfs metadata. Primary content is the
+	 * in-memory version of the fmap - the map from file's offset range
+	 * to DAX memory
+	 */
+	void *famfs_meta;
+#endif
 };
 
 /** FUSE inode state bits */
@@ -1218,11 +1226,8 @@ void fuse_free_conn(struct fuse_conn *fc);
 
 /* dax.c */
 
-static inline bool fuse_file_famfs(struct fuse_inode *fuse_inode) /* Will be superseded */
-{
-	(void)fuse_inode;
-	return false;
-}
+static inline int fuse_file_famfs(struct fuse_inode *fi); /* forward */
+
 #define FUSE_IS_VIRTIO_DAX(fuse_inode) (IS_ENABLED(CONFIG_FUSE_DAX)	\
 					&& IS_DAX(&(fuse_inode)->inode)  \
 					&& !fuse_file_famfs(fuse_inode))
@@ -1339,4 +1344,59 @@ extern void fuse_sysctl_unregister(void);
 #define fuse_sysctl_unregister()	do { } while (0)
 #endif /* CONFIG_SYSCTL */
 
+/* famfs.c */
+
+#if IS_ENABLED(CONFIG_FUSE_FAMFS_DAX)
+void __famfs_meta_free(void *map);
+
+/* Set fi->famfs_meta = NULL regardless of prior value */
+static inline void famfs_meta_init(struct fuse_inode *fi)
+{
+	fi->famfs_meta = NULL;
+}
+
+/* Set fi->famfs_meta iff the current value is NULL */
+static inline struct fuse_backing *famfs_meta_set(struct fuse_inode *fi,
+						  void *meta)
+{
+	return cmpxchg(&fi->famfs_meta, NULL, meta);
+}
+
+static inline void famfs_meta_free(struct fuse_inode *fi)
+{
+	famfs_meta_set(fi, NULL);
+}
+
+static inline int fuse_file_famfs(struct fuse_inode *fi)
+{
+	return (READ_ONCE(fi->famfs_meta) != NULL);
+}
+
+int fuse_get_fmap(struct fuse_mount *fm, struct inode *inode);
+
+#else /* !CONFIG_FUSE_FAMFS_DAX */
+
+static inline struct fuse_backing *famfs_meta_set(struct fuse_inode *fi,
+						  void *meta)
+{
+	return NULL;
+}
+
+static inline void famfs_meta_free(struct fuse_inode *fi)
+{
+}
+
+static inline int fuse_file_famfs(struct fuse_inode *fi)
+{
+	return 0;
+}
+
+static inline int
+fuse_get_fmap(struct fuse_mount *fm, struct inode *inode)
+{
+	return 0;
+}
+
+#endif /* CONFIG_FUSE_FAMFS_DAX */
+
 #endif /* _FS_FUSE_I_H */
diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index c347471d04b6..e030a302120f 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -106,6 +106,9 @@ static struct inode *fuse_alloc_inode(struct super_block *sb)
 	if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
 		fuse_inode_backing_set(fi, NULL);
 
+	if (IS_ENABLED(CONFIG_FUSE_FAMFS_DAX))
+		famfs_meta_set(fi, NULL);
+
 	return &fi->inode;
 
 out_free_forget:
@@ -127,6 +130,9 @@ static void fuse_free_inode(struct inode *inode)
 	if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))
 		fuse_backing_put(fuse_inode_backing(fi));
 
+	if (S_ISREG(inode->i_mode) && fuse_file_famfs(fi))
+		famfs_meta_free(fi);
+
 	kmem_cache_free(fuse_inode_cachep, fi);
 }
 
@@ -148,7 +154,7 @@ static void fuse_evict_inode(struct inode *inode)
 	/* Will write inode on close/munmap and in all other dirtiers */
 	WARN_ON(inode_state_read_once(inode) & I_DIRTY_INODE);
 
-	if (FUSE_IS_VIRTIO_DAX(fi))
+	if (FUSE_IS_VIRTIO_DAX(fi) || fuse_file_famfs(fi))
 		dax_break_layout_final(inode);
 
 	truncate_inode_pages_final(&inode->i_data);
diff --git a/fs/fuse/iomode.c b/fs/fuse/iomode.c
index 31ee7f3304c6..948148316ef0 100644
--- a/fs/fuse/iomode.c
+++ b/fs/fuse/iomode.c
@@ -203,7 +203,7 @@ int fuse_file_io_open(struct file *file, struct inode *inode)
 	 * io modes are not relevant with DAX and with server that does not
 	 * implement open.
 	 */
-	if (FUSE_IS_VIRTIO_DAX(fi) || !ff->args)
+	if (FUSE_IS_VIRTIO_DAX(fi) || fuse_file_famfs(fi) || !ff->args)
 		return 0;
 
 	/*
diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h
index 25686f088e6a..d323c20e79bd 100644
--- a/include/uapi/linux/fuse.h
+++ b/include/uapi/linux/fuse.h
@@ -669,6 +669,9 @@ enum fuse_opcode {
 	FUSE_STATX		= 52,
 	FUSE_COPY_FILE_RANGE_64	= 53,
 
+	/* Famfs / devdax opcodes */
+	FUSE_GET_FMAP           = 54,
+
 	/* CUSE specific operations */
 	CUSE_INIT		= 4096,
 
-- 
2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.