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

John Groves <[email protected]> Mon, 20 Jul 2026 03:45:50 +0000
Newsgroups dev.linux.lists.fuse-devel,dev.linux.lists.nvdimm,org.kernel.vger.linux-cxl,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]>=0D=0A=0D=0AOn completion of an OPEN i=
n famfs mode, issue a GET_FMAP request to the=0D=0Aserver to retrieve the=
 file's file-offset-to-dax map (fmap) and cache it=0D=0Aon the fuse_inode=
 (fi->famfs_meta). Once the map is cached, read, write=0D=0Aand mmap are =
resolved directly to dax with no further upcalls.=0D=0A=0D=0A- uapi: add =
the FUSE_GET_FMAP opcode.=0D=0A- famfs.c: add fuse_get_fmap(), which retr=
ieves the fmap into a=0D=0A  kvmalloc'd buffer. The fmap size is not know=
n in advance, so it uses a=0D=0A  size probe: it starts with a PAGE_SIZE =
buffer and passes that size to=0D=0A  the server (via fuse_getxattr_in.si=
ze). If the whole fmap does not=0D=0A  fit, the server replies with just =
the header, whose fmap_size field=0D=0A  reports the required size, and t=
he kernel reallocates exactly that and=0D=0A  retries once. The reply is =
bounded by FMAP_BUFSIZE_MAX (16 MiB); a=0D=0A  larger fmap is rejected wi=
th -EFBIG. A famfs file is fixed-size, so a=0D=0A  reply that reports a d=
ifferent size on the retry is rejected as a=0D=0A  server bug.=0D=0A- fil=
e.c: hook the OPEN path to fetch the fmap for regular files on a=0D=0A  f=
amfs connection; failure is fatal to the open.=0D=0A- fuse_i.h/inode.c: a=
dd the fi->famfs_meta pointer and its init/free=0D=0A  helpers.=0D=0A=0D=0A=
The retrieved map is parsed into its in-memory form in the following=0D=0A=
patch.=0D=0A=0D=0ASigned-off-by: John Groves <[email protected]>=0D=0A---=0D=
=0A MAINTAINERS               |   7 ++=0D=0A fs/fuse/Makefile          | =
  1 +=0D=0A fs/fuse/famfs.c           | 134 +++++++++++++++++++++++++++++=
+++++++++=0D=0A fs/fuse/file.c            |  14 +++-=0D=0A fs/fuse/fuse_i=
=2Eh          |  70 ++++++++++++++++++--=0D=0A fs/fuse/inode.c           =
|   8 ++-=0D=0A fs/fuse/iomode.c          |   2 +-=0D=0A include/uapi/lin=
ux/fuse.h |   3 +=0D=0A 8 files changed, 231 insertions(+), 8 deletions(-=
)=0D=0A create mode 100644 fs/fuse/famfs.c=0D=0A=0D=0Adiff --git a/MAINTA=
INERS b/MAINTAINERS=0D=0Aindex 806bd2d80d15..0d0fded4fddb 100644=0D=0A---=
 a/MAINTAINERS=0D=0A+++ b/MAINTAINERS=0D=0A@@ -10713,6 +10713,13 @@ F:=09=
fs/fuse/backing.c=0D=0A F:=09fs/fuse/iomode.c=0D=0A F:=09fs/fuse/passthro=
ugh.c=0D=0A=20=0D=0A+FUSE FILESYSTEM [FAMFS Fabric-Attached Memory File S=
ystem]=0D=0A+M:=09John Groves <[email protected]>=0D=0A+L:=09linux-cxl@vger=
=2Ekernel.org=0D=0A+L:[email protected]=0D=0A+S:=09Support=
ed=0D=0A+F:=09fs/fuse/famfs.c=0D=0A+=0D=0A FUTEX SUBSYSTEM=0D=0A M:=09Tho=
mas Gleixner <[email protected]>=0D=0A M:=09Ingo Molnar <[email protected]>=0D=
=0Adiff --git a/fs/fuse/Makefile b/fs/fuse/Makefile=0D=0Aindex 245e67852b=
03..66507b9cfe1f 100644=0D=0A--- a/fs/fuse/Makefile=0D=0A+++ b/fs/fuse/Ma=
kefile=0D=0A@@ -18,5 +18,6 @@ fuse-$(CONFIG_FUSE_DAX) +=3D dax.o=0D=0A fu=
se-$(CONFIG_FUSE_PASSTHROUGH) +=3D passthrough.o backing.o=0D=0A fuse-$(C=
ONFIG_SYSCTL) +=3D sysctl.o=0D=0A fuse-$(CONFIG_FUSE_IO_URING) +=3D dev_u=
ring.o=0D=0A+fuse-$(CONFIG_FUSE_FAMFS_DAX) +=3D famfs.o=0D=0A=20=0D=0A vi=
rtiofs-y :=3D virtio_fs.o=0D=0Adiff --git a/fs/fuse/famfs.c b/fs/fuse/fam=
fs.c=0D=0Anew file mode 100644=0D=0Aindex 000000000000..80e6640ac970=0D=0A=
--- /dev/null=0D=0A+++ b/fs/fuse/famfs.c=0D=0A@@ -0,0 +1,134 @@=0D=0A+// =
SPDX-License-Identifier: GPL-2.0=0D=0A+/*=0D=0A+ * famfs - dax file syste=
m for shared fabric-attached memory=0D=0A+ *=0D=0A+ * Copyright 2023-2026=
 Micron Technology, Inc.=0D=0A+ *=0D=0A+ * This file system, originally b=
ased on ramfs the dax support from xfs,=0D=0A+ * is intended to allow mul=
tiple host systems to mount a common file system=0D=0A+ * view of dax fil=
es that map to shared memory.=0D=0A+ */=0D=0A+=0D=0A+#include <linux/clea=
nup.h>=0D=0A+#include <linux/fs.h>=0D=0A+#include <linux/mm.h>=0D=0A+#inc=
lude <linux/dax.h>=0D=0A+#include <linux/iomap.h>=0D=0A+#include <linux/p=
ath.h>=0D=0A+#include <linux/namei.h>=0D=0A+#include <linux/string.h>=0D=0A=
+=0D=0A+#include "fuse_i.h"=0D=0A+=0D=0A+=0D=0A+#define FMAP_BUFSIZE_INIT=
 PAGE_SIZE=0D=0A+/*=0D=0A+ * Largest GET_FMAP reply buffer we will kvmall=
oc. Any fmap whose whole message=0D=0A+ * fits in this buffer is handled;=
 there is no separate extent-count cap, so the=0D=0A+ * effective extent =
limit is just this size / sizeof(simple_ext) (~699k extents=0D=0A+ * =3D>=
 ~1.3 TiB per striped file at a 2 MiB chunk). kvmalloc-backed, so it may=0D=
=0A+ * exceed the contiguous kmalloc limit. Matches the server's reply-bu=
ffer cap.=0D=0A+ */=0D=0A+#define FMAP_BUFSIZE_MAX (16 * 1024 * 1024)=0D=0A=
+=0D=0A+int fuse_get_fmap(struct fuse_mount *fm, struct inode *inode)=0D=0A=
+{=0D=0A+=09struct fuse_inode *fi =3D get_fuse_inode(inode);=0D=0A+=09u64=
 nodeid =3D get_node_id(inode);=0D=0A+=09size_t bufsize =3D FMAP_BUFSIZE_=
INIT;=0D=0A+=09void *fmap_buf =3D NULL;=0D=0A+=09ssize_t fmap_size;=0D=0A=
+=09int attempt;=0D=0A+=09int rc;=0D=0A+=0D=0A+=09/* Don't retrieve if we=
 already have the famfs metadata */=0D=0A+=09if (fi->famfs_meta)=0D=0A+=09=
=09return 0;=0D=0A+=0D=0A+=09/*=0D=0A+=09 * The fmap size is not known in=
 advance. Start with a modest buffer and,=0D=0A+=09 * if the server repor=
ts (via the returned header's fmap_size) that the=0D=0A+=09 * whole fmap =
did not fit, reallocate exactly that size and retry once.=0D=0A+=09 * The=
 server learns our buffer size from the request's=0D=0A+=09 * fuse_getxat=
tr_in.size (GETXATTR-style size probe).=0D=0A+=09 */=0D=0A+=09for (attemp=
t =3D 0; ; attempt++) {=0D=0A+=09=09struct fuse_getxattr_in in =3D { .siz=
e =3D bufsize };=0D=0A+=09=09struct fuse_famfs_fmap_header *fmh;=0D=0A+=09=
=09u32 required;=0D=0A+=0D=0A+=09=09FUSE_ARGS(args);=0D=0A+=0D=0A+=09=09f=
map_buf =3D kvmalloc(bufsize, GFP_KERNEL);=0D=0A+=09=09if (!fmap_buf)=0D=0A=
+=09=09=09return -ENOMEM;=0D=0A+=0D=0A+=09=09args.opcode =3D FUSE_GET_FMA=
P;=0D=0A+=09=09args.nodeid =3D nodeid;=0D=0A+=09=09args.in_numargs =3D 1;=
=0D=0A+=09=09args.in_args[0].size =3D sizeof(in);=0D=0A+=09=09args.in_arg=
s[0].value =3D &in;=0D=0A+=09=09/*=0D=0A+=09=09 * Variable-sized output b=
uffer; fuse_simple_request() returns=0D=0A+=09=09 * the size of the outpu=
t payload.=0D=0A+=09=09 */=0D=0A+=09=09args.out_argvar =3D true;=0D=0A+=09=
=09args.out_numargs =3D 1;=0D=0A+=09=09args.out_args[0].size =3D bufsize;=
=0D=0A+=09=09args.out_args[0].value =3D fmap_buf;=0D=0A+=0D=0A+=09=09rc =3D=
 fuse_simple_request(fm, &args);=0D=0A+=09=09if (rc < 0) {=0D=0A+=09=09=09=
pr_err("%s: err=3D%d from fuse_simple_request()\n",=0D=0A+=09=09=09      =
 __func__, rc);=0D=0A+=09=09=09kvfree(fmap_buf);=0D=0A+=09=09=09return rc=
;=0D=0A+=09=09}=0D=0A+=09=09fmap_size =3D rc;=0D=0A+=0D=0A+=09=09/* Need =
at least a header to learn the required size */=0D=0A+=09=09if (fmap_size=
 < (ssize_t)sizeof(*fmh)) {=0D=0A+=09=09=09pr_err("%s: short fmap reply %=
zd\n", __func__, fmap_size);=0D=0A+=09=09=09kvfree(fmap_buf);=0D=0A+=09=09=
=09return -EIO;=0D=0A+=09=09}=0D=0A+=0D=0A+=09=09fmh =3D fmap_buf;=0D=0A+=
=09=09required =3D fmh->fmap_size;=0D=0A+=0D=0A+=09=09/* Whole fmap fit i=
n the buffer -> parse it */=0D=0A+=09=09if (required <=3D bufsize)=0D=0A+=
=09=09=09break;=0D=0A+=0D=0A+=09=09/* Too small: server sent only the hea=
der. Grow and retry once. */=0D=0A+=09=09kvfree(fmap_buf);=0D=0A+=09=09fm=
ap_buf =3D NULL;=0D=0A+=0D=0A+=09=09if (required > FMAP_BUFSIZE_MAX) {=0D=
=0A+=09=09=09pr_err("%s: fmap size %u exceeds max %zu\n",=0D=0A+=09=09=09=
       __func__, required, (size_t)FMAP_BUFSIZE_MAX);=0D=0A+=09=09=09retu=
rn -EFBIG;=0D=0A+=09=09}=0D=0A+=09=09if (attempt >=3D 1) {=0D=0A+=09=09=09=
/*=0D=0A+=09=09=09 * A famfs file is fixed-size, so the server must repor=
t=0D=0A+=09=09=09 * the same fmap_size on the retry as on the first=0D=0A=
+=09=09=09 * request. A larger value means the file's size/fmap=0D=0A+=09=
=09=09 * changed between the two GET_FMAPs -- a server bug.=0D=0A+=09=09=09=
 */=0D=0A+=09=09=09pr_err("%s: fmap grew %zu -> %u across GET_FMAP retrie=
s; famfs file size must not change (server bug)\n",=0D=0A+=09=09=09      =
 __func__, bufsize, required);=0D=0A+=09=09=09return -EINVAL;=0D=0A+=09=09=
}=0D=0A+=09=09bufsize =3D required;=0D=0A+=09}=0D=0A+=0D=0A+=09/* We retr=
ieved the "fmap" (the file's map to memory), but=0D=0A+=09 * we haven't u=
sed it yet. A call to famfs_file_init_dax() will be added=0D=0A+=09 * her=
e in a subsequent patch, when we add the ability to attach=0D=0A+=09 * fm=
aps to files.=0D=0A+=09 */=0D=0A+=0D=0A+=09kvfree(fmap_buf);=0D=0A+=09ret=
urn 0;=0D=0A+}=0D=0Adiff --git a/fs/fuse/file.c b/fs/fuse/file.c=0D=0Aind=
ex 995e37c935c6..b4e7b6a64587 100644=0D=0A--- a/fs/fuse/file.c=0D=0A+++ b=
/fs/fuse/file.c=0D=0A@@ -282,6 +282,16 @@ static int fuse_open(struct ino=
de *inode, struct file *file)=0D=0A =09err =3D fuse_do_open(fm, get_node_=
id(inode), file, false);=0D=0A =09if (!err) {=0D=0A =09=09ff =3D file->pr=
ivate_data;=0D=0A+=0D=0A+=09=09if ((fm->fc->famfs_iomap) && (S_ISREG(inod=
e->i_mode))) {=0D=0A+=09=09=09/* Get the famfs fmap - failure is fatal */=
=0D=0A+=09=09=09err =3D fuse_get_fmap(fm, inode);=0D=0A+=09=09=09if (err)=
 {=0D=0A+=09=09=09=09fuse_sync_release(fi, ff, file->f_flags);=0D=0A+=09=09=
=09=09goto out_nowrite;=0D=0A+=09=09=09}=0D=0A+=09=09}=0D=0A+=0D=0A =09=09=
err =3D fuse_finish_open(inode, file);=0D=0A =09=09if (err)=0D=0A =09=09=09=
fuse_sync_release(fi, ff, file->f_flags);=0D=0A@@ -289,12 +299,14 @@ stat=
ic int fuse_open(struct inode *inode, struct file *file)=0D=0A =09=09=09f=
use_truncate_update_attr(inode, file);=0D=0A =09}=0D=0A=20=0D=0A+out_nowr=
ite:=0D=0A =09if (is_wb_truncate || dax_truncate)=0D=0A =09=09fuse_releas=
e_nowrite(inode);=0D=0A =09if (!err) {=0D=0A =09=09if (is_truncate)=0D=0A=
 =09=09=09truncate_pagecache(inode, 0);=0D=0A-=09=09else if (!(ff->open_f=
lags & FOPEN_KEEP_CACHE))=0D=0A+=09=09else if (!(ff->open_flags & FOPEN_K=
EEP_CACHE) &&=0D=0A+=09=09=09 !fuse_file_famfs(fi))=0D=0A =09=09=09invali=
date_inode_pages2(inode->i_mapping);=0D=0A =09}=0D=0A =09if (dax_truncate=
)=0D=0Adiff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h=0D=0Aindex 9c3541=
18c931..5bacc5098620 100644=0D=0A--- a/fs/fuse/fuse_i.h=0D=0A+++ b/fs/fus=
e/fuse_i.h=0D=0A@@ -236,6 +236,14 @@ struct fuse_inode {=0D=0A =09 * be m=
odified, so preserve the blocksize specified by the server.=0D=0A =09 */=0D=
=0A =09u8 cached_i_blkbits;=0D=0A+=0D=0A+#if IS_ENABLED(CONFIG_FUSE_FAMFS=
_DAX)=0D=0A+=09/* Pointer to the file's famfs metadata. Primary content i=
s the=0D=0A+=09 * in-memory version of the fmap - the map from file's off=
set range=0D=0A+=09 * to DAX memory=0D=0A+=09 */=0D=0A+=09void *famfs_met=
a;=0D=0A+#endif=0D=0A };=0D=0A=20=0D=0A /** FUSE inode state bits */=0D=0A=
@@ -1218,11 +1226,8 @@ void fuse_free_conn(struct fuse_conn *fc);=0D=0A=20=
=0D=0A /* dax.c */=0D=0A=20=0D=0A-static inline bool fuse_file_famfs(stru=
ct fuse_inode *fuse_inode) /* Will be superseded */=0D=0A-{=0D=0A-=09(voi=
d)fuse_inode;=0D=0A-=09return false;=0D=0A-}=0D=0A+static inline int fuse=
_file_famfs(struct fuse_inode *fi); /* forward */=0D=0A+=0D=0A #define FU=
SE_IS_VIRTIO_DAX(fuse_inode) (IS_ENABLED(CONFIG_FUSE_DAX)=09\=0D=0A =09=09=
=09=09=09&& IS_DAX(&(fuse_inode)->inode)  \=0D=0A =09=09=09=09=09&& !fuse=
_file_famfs(fuse_inode))=0D=0A@@ -1339,4 +1344,59 @@ extern void fuse_sys=
ctl_unregister(void);=0D=0A #define fuse_sysctl_unregister()=09do { } whi=
le (0)=0D=0A #endif /* CONFIG_SYSCTL */=0D=0A=20=0D=0A+/* famfs.c */=0D=0A=
+=0D=0A+#if IS_ENABLED(CONFIG_FUSE_FAMFS_DAX)=0D=0A+void __famfs_meta_fre=
e(void *map);=0D=0A+=0D=0A+/* Set fi->famfs_meta =3D NULL regardless of p=
rior value */=0D=0A+static inline void famfs_meta_init(struct fuse_inode =
*fi)=0D=0A+{=0D=0A+=09fi->famfs_meta =3D NULL;=0D=0A+}=0D=0A+=0D=0A+/* Se=
t fi->famfs_meta iff the current value is NULL */=0D=0A+static inline str=
uct fuse_backing *famfs_meta_set(struct fuse_inode *fi,=0D=0A+=09=09=09=09=
=09=09  void *meta)=0D=0A+{=0D=0A+=09return cmpxchg(&fi->famfs_meta, NULL=
, meta);=0D=0A+}=0D=0A+=0D=0A+static inline void famfs_meta_free(struct f=
use_inode *fi)=0D=0A+{=0D=0A+=09famfs_meta_set(fi, NULL);=0D=0A+}=0D=0A+=0D=
=0A+static inline int fuse_file_famfs(struct fuse_inode *fi)=0D=0A+{=0D=0A=
+=09return (READ_ONCE(fi->famfs_meta) !=3D NULL);=0D=0A+}=0D=0A+=0D=0A+in=
t fuse_get_fmap(struct fuse_mount *fm, struct inode *inode);=0D=0A+=0D=0A=
+#else /* !CONFIG_FUSE_FAMFS_DAX */=0D=0A+=0D=0A+static inline struct fus=
e_backing *famfs_meta_set(struct fuse_inode *fi,=0D=0A+=09=09=09=09=09=09=
  void *meta)=0D=0A+{=0D=0A+=09return NULL;=0D=0A+}=0D=0A+=0D=0A+static i=
nline void famfs_meta_free(struct fuse_inode *fi)=0D=0A+{=0D=0A+}=0D=0A+=0D=
=0A+static inline int fuse_file_famfs(struct fuse_inode *fi)=0D=0A+{=0D=0A=
+=09return 0;=0D=0A+}=0D=0A+=0D=0A+static inline int=0D=0A+fuse_get_fmap(=
struct fuse_mount *fm, struct inode *inode)=0D=0A+{=0D=0A+=09return 0;=0D=
=0A+}=0D=0A+=0D=0A+#endif /* CONFIG_FUSE_FAMFS_DAX */=0D=0A+=0D=0A #endif=
 /* _FS_FUSE_I_H */=0D=0Adiff --git a/fs/fuse/inode.c b/fs/fuse/inode.c=0D=
=0Aindex c347471d04b6..e030a302120f 100644=0D=0A--- a/fs/fuse/inode.c=0D=0A=
+++ b/fs/fuse/inode.c=0D=0A@@ -106,6 +106,9 @@ static struct inode *fuse_=
alloc_inode(struct super_block *sb)=0D=0A =09if (IS_ENABLED(CONFIG_FUSE_P=
ASSTHROUGH))=0D=0A =09=09fuse_inode_backing_set(fi, NULL);=0D=0A=20=0D=0A=
+=09if (IS_ENABLED(CONFIG_FUSE_FAMFS_DAX))=0D=0A+=09=09famfs_meta_set(fi,=
 NULL);=0D=0A+=0D=0A =09return &fi->inode;=0D=0A=20=0D=0A out_free_forget=
:=0D=0A@@ -127,6 +130,9 @@ static void fuse_free_inode(struct inode *inod=
e)=0D=0A =09if (IS_ENABLED(CONFIG_FUSE_PASSTHROUGH))=0D=0A =09=09fuse_bac=
king_put(fuse_inode_backing(fi));=0D=0A=20=0D=0A+=09if (S_ISREG(inode->i_=
mode) && fuse_file_famfs(fi))=0D=0A+=09=09famfs_meta_free(fi);=0D=0A+=0D=0A=
 =09kmem_cache_free(fuse_inode_cachep, fi);=0D=0A }=0D=0A=20=0D=0A@@ -148=
,7 +154,7 @@ static void fuse_evict_inode(struct inode *inode)=0D=0A =09/=
* Will write inode on close/munmap and in all other dirtiers */=0D=0A =09=
WARN_ON(inode_state_read_once(inode) & I_DIRTY_INODE);=0D=0A=20=0D=0A-=09=
if (FUSE_IS_VIRTIO_DAX(fi))=0D=0A+=09if (FUSE_IS_VIRTIO_DAX(fi) || fuse_f=
ile_famfs(fi))=0D=0A =09=09dax_break_layout_final(inode);=0D=0A=20=0D=0A =
=09truncate_inode_pages_final(&inode->i_data);=0D=0Adiff --git a/fs/fuse/=
iomode.c b/fs/fuse/iomode.c=0D=0Aindex 31ee7f3304c6..948148316ef0 100644=0D=
=0A--- a/fs/fuse/iomode.c=0D=0A+++ b/fs/fuse/iomode.c=0D=0A@@ -203,7 +203=
,7 @@ int fuse_file_io_open(struct file *file, struct inode *inode)=0D=0A=
 =09 * io modes are not relevant with DAX and with server that does not=0D=
=0A =09 * implement open.=0D=0A =09 */=0D=0A-=09if (FUSE_IS_VIRTIO_DAX(fi=
) || !ff->args)=0D=0A+=09if (FUSE_IS_VIRTIO_DAX(fi) || fuse_file_famfs(fi=
) || !ff->args)=0D=0A =09=09return 0;=0D=0A=20=0D=0A =09/*=0D=0Adiff --gi=
t a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h=0D=0Aindex 2568=
6f088e6a..d323c20e79bd 100644=0D=0A--- a/include/uapi/linux/fuse.h=0D=0A+=
++ b/include/uapi/linux/fuse.h=0D=0A@@ -669,6 +669,9 @@ enum fuse_opcode =
{=0D=0A =09FUSE_STATX=09=09=3D 52,=0D=0A =09FUSE_COPY_FILE_RANGE_64=09=3D=
 53,=0D=0A=20=0D=0A+=09/* Famfs / devdax opcodes */=0D=0A+=09FUSE_GET_FMA=
P           =3D 54,=0D=0A+=0D=0A =09/* CUSE specific operations */=0D=0A =
=09CUSE_INIT=09=09=3D 4096,=0D=0A=20=0D=0A--=20=0D=0A2.53.0=0D=0A=0D=0A