git: f697b9432d9c - main - linuxkpi: drm-kmod debugfs support

From: Emmanuel Vadot <manu_at_FreeBSD.org>
Date: Tue, 20 Sep 2022 18:13:57 UTC
The branch main has been updated by manu:

URL: https://cgit.FreeBSD.org/src/commit/?id=f697b9432d9c7aa4c5ab5f5445ef5dc1bd40ce00

commit f697b9432d9c7aa4c5ab5f5445ef5dc1bd40ce00
Author:     Jake Freeland <jfree@FreeBSD.org>
AuthorDate: 2022-09-20 17:41:10 +0000
Commit:     Emmanuel Vadot <manu@FreeBSD.org>
CommitDate: 2022-09-20 17:41:10 +0000

    linuxkpi: drm-kmod debugfs support
    
    This diff extends LinuxKPI to support simple attribute files in debugfs.
    These simple attributes are an essential component for compiling drm-kmod
    with CONFIG_DEBUG_FS enabled.
    This will allow for easier graphics driver debugging using
    Intel's igt-gpu-tools.
    
    Reviewed by:    hselasky
    Differential Revision:  https://reviews.freebsd.org/D35883
    Sponsored by:   Google, Inc. (GSoC 2022)
---
 share/man/man5/Makefile                            |   1 +
 share/man/man5/lindebugfs.5                        |  95 ++++++++++
 sys/compat/lindebugfs/lindebugfs.c                 | 169 +++++++++++++-----
 sys/compat/linuxkpi/common/include/linux/dcache.h  |   5 +-
 sys/compat/linuxkpi/common/include/linux/debugfs.h |  43 ++++-
 sys/compat/linuxkpi/common/include/linux/fs.h      |  78 +++++++++
 .../linuxkpi/common/include/linux/seq_file.h       |  11 +-
 sys/compat/linuxkpi/common/src/linux_seq_file.c    |  58 +++++--
 sys/compat/linuxkpi/common/src/linux_simple_attr.c | 191 +++++++++++++++++++++
 sys/conf/files                                     |   5 +-
 sys/modules/linuxkpi/Makefile                      |   3 +-
 11 files changed, 593 insertions(+), 66 deletions(-)

diff --git a/share/man/man5/Makefile b/share/man/man5/Makefile
index 9be4c8dc46b0..2d49d981c2f9 100644
--- a/share/man/man5/Makefile
+++ b/share/man/man5/Makefile
@@ -32,6 +32,7 @@ MAN=	acct.5 \
 	hosts.lpd.5 \
 	intro.5 \
 	libmap.conf.5 \
+	lindebugfs.5 \
 	link.5 \
 	linprocfs.5 \
 	linsysfs.5 \
diff --git a/share/man/man5/lindebugfs.5 b/share/man/man5/lindebugfs.5
new file mode 100644
index 000000000000..0d93d6aec6b9
--- /dev/null
+++ b/share/man/man5/lindebugfs.5
@@ -0,0 +1,95 @@
+.\"  SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+.\"
+.\"  Copyright (c) 2022, Jake Freeland <jfree@freebsd.org>
+.\"
+.\"  Redistribution and use in source and binary forms, with or without
+.\"  modification, are permitted provided that the following conditions
+.\"  are met:
+.\"  1. Redistributions of source code must retain the above copyright
+.\"     notice, this list of conditions and the following disclaimer.
+.\"  2. Redistributions in binary form must reproduce the above copyright
+.\"     notice, this list of conditions and the following disclaimer in the
+.\"     documentation and/or other materials provided with the distribution.
+.\"
+.\"  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\"  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\"  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\"  ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\"  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\"  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\"  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\"  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\"  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\"  OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\"  SUCH DAMAGE.
+
+.Dd August 10, 2022
+.Dt LINDEBUGFS 5
+.Os
+.Sh NAME
+.Nm lindebugfs
+.Nd Linux file system for debugging
+.Sh SYNOPSIS
+.Bd -literal
+lindebugfs		/sys/kernel/debug	lindebugfs	rw 0 0
+.Ed
+.Sh DESCRIPTION
+The debug file system, or debugfs, makes process debugging easier by
+providing a simple API for data transfer between the kernel and user
+space.
+Debugfs is not a general-purpose file system and should not be used as
+a storage medium.
+Instead, developers can implement the debugfs interface in their code
+to generate debug information about their program at runtime.
+FreeBSD's
+.Nm
+uses the
+.Xr pseudofs 9
+file system construction kit to model itself after Linux's debugfs.
+The
+.Nm
+API is intended for use with programs that take advantage of FreeBSD's
+LinuxKPI compatibility layer.
+.Pp
+When mounted,
+.Nm
+will populate with pseudo files from any running process that calls
+.Nm debugfs_create_file() .
+Since
+.Nm
+is a pseudo file system, file contents will be generated dynamically
+based on program provided file operations.
+The current
+.Nm
+implementation formally supports seq_file and simple_attr_file virtual
+file formats.
+.Sh EXAMPLES
+Load the
+.Nm
+kernel module:
+.Pp
+.Dl "kldload lindebugfs"
+.Pp
+Mount the
+.Nm
+file system on
+.Pa /sys/kernel/debug :
+.Pp
+.Dl "mount -t lindebugfs lindebugfs /sys/kernel/debug"
+.Sh SEE ALSO
+.Xr linprocfs 5 ,
+.Xr linsysfs 5 ,
+.Xr pseudofs 9 ,
+.Xr linux 4 ,
+.Xr mount 1
+.Sh HISTORY
+The
+.Nm
+file system first appeared in
+.Fx 12.1 .
+.Sh AUTHORS
+.An -nosplit
+The initial implementation for
+.Nm
+was created by Matthew Macy.
+This manual page was written by Jake Freeland.
diff --git a/sys/compat/lindebugfs/lindebugfs.c b/sys/compat/lindebugfs/lindebugfs.c
index cbfdfbbce876..b88caf9c3140 100644
--- a/sys/compat/lindebugfs/lindebugfs.c
+++ b/sys/compat/lindebugfs/lindebugfs.c
@@ -69,9 +69,9 @@ __FBSDID("$FreeBSD$");
 #include <compat/linux/linux_util.h>
 #include <fs/pseudofs/pseudofs.h>
 
-#include <linux/debugfs.h>
-#include <linux/seq_file.h>
 #include <linux/compat.h>
+#include <linux/debugfs.h>
+#include <linux/fs.h>
 
 MALLOC_DEFINE(M_DFSINT, "debugfsint", "Linux debugfs internal");
 
@@ -118,27 +118,17 @@ debugfs_fill(PFS_FILL_ARGS)
 {
 	struct dentry_meta *d;
 	struct linux_file lf = {};
-	struct seq_file *sf;
 	struct vnode vn;
-	void *buf;
+	char *buf;
 	int rc;
-	size_t len;
-	off_t off;
-
-	d = pn->pn_data;
+	off_t off = 0;
 
 	if ((rc = linux_set_current_flags(curthread, M_NOWAIT)))
 		return (rc);
+
+	d = pn->pn_data;
 	vn.v_data = d->dm_data;
-	if (uio->uio_rw == UIO_READ) {
-		buf = uio->uio_iov[0].iov_base;
-		len = min(uio->uio_iov[0].iov_len, uio->uio_resid);
-	} else {
-		sbuf_finish(sb);
-		buf = sbuf_data(sb);
-		len = sbuf_len(sb);
-	}
-	off = 0;
+
 	rc = d->dm_fops->open(&vn, &lf);
 	if (rc < 0) {
 #ifdef INVARIANTS
@@ -146,19 +136,23 @@ debugfs_fill(PFS_FILL_ARGS)
 #endif
 		return (-rc);
 	}
-	sf = lf.private_data;
-	sf->buf = sb;
-	if (uio->uio_rw == UIO_READ) {
-		if (d->dm_fops->read)
-			rc = d->dm_fops->read(&lf, NULL, len, &off);
-		else
-			rc = -ENODEV;
-	} else {
-		if (d->dm_fops->write)
-			rc = d->dm_fops->write(&lf, buf, len, &off);
-		else
-			rc = -ENODEV;
+
+	rc = -ENODEV;
+	if (uio->uio_rw == UIO_READ && d->dm_fops->read) {
+		rc = -ENOMEM;
+		buf = (char *) malloc(sb->s_size, M_DFSINT, M_ZERO | M_NOWAIT);
+		if (buf != NULL) {
+			rc = d->dm_fops->read(&lf, buf, sb->s_size, &off);
+			if (rc > 0)
+				sbuf_bcpy(sb, buf, strlen(buf));
+
+			free(buf, M_DFSINT);
+		}
+	} else if (uio->uio_rw == UIO_WRITE && d->dm_fops->write) {
+		sbuf_finish(sb);
+		rc = d->dm_fops->write(&lf, sbuf_data(sb), sbuf_len(sb), &off);
 	}
+
 	if (d->dm_fops->release)
 		d->dm_fops->release(&vn, &lf);
 	else
@@ -185,8 +179,8 @@ debugfs_fill_data(PFS_FILL_ARGS)
 
 struct dentry *
 debugfs_create_file(const char *name, umode_t mode,
-		    struct dentry *parent, void *data,
-		    const struct file_operations *fops)
+    struct dentry *parent, void *data,
+    const struct file_operations *fops)
 {
 	struct dentry_meta *dm;
 	struct dentry *dnode;
@@ -218,6 +212,43 @@ debugfs_create_file(const char *name, umode_t mode,
 	return (dnode);
 }
 
+/*
+ * NOTE: Files created with the _unsafe moniker will not be protected from
+ * debugfs core file removals. It is the responsibility of @fops to protect
+ * its file using debugfs_file_get() and debugfs_file_put().
+ *
+ * FreeBSD's LinuxKPI lindebugfs does not perform file removals at the time
+ * of writing. Therefore there is no difference between functions with _unsafe
+ * and functions without _unsafe when using lindebugfs. Functions with _unsafe
+ * exist only for Linux compatibility.
+ */
+struct dentry *
+debugfs_create_file_unsafe(const char *name, umode_t mode,
+    struct dentry *parent, void *data,
+    const struct file_operations *fops)
+{
+	return (debugfs_create_file(name, mode, parent, data, fops));
+}
+
+struct dentry *
+debugfs_create_mode_unsafe(const char *name, umode_t mode,
+    struct dentry *parent, void *data,
+    const struct file_operations *fops,
+    const struct file_operations *fops_ro,
+    const struct file_operations *fops_wo)
+{
+	umode_t read = mode & S_IRUGO;
+	umode_t write = mode & S_IWUGO;
+
+	if (read && !write)
+		return (debugfs_create_file_unsafe(name, mode, parent, data, fops_ro));
+
+	if (write && !read)
+		return (debugfs_create_file_unsafe(name, mode, parent, data, fops_wo));
+
+	return (debugfs_create_file_unsafe(name, mode, parent, data, fops));
+}
+
 struct dentry *
 debugfs_create_dir(const char *name, struct dentry *parent)
 {
@@ -247,7 +278,7 @@ debugfs_create_dir(const char *name, struct dentry *parent)
 
 struct dentry *
 debugfs_create_symlink(const char *name, struct dentry *parent,
-	const char *dest)
+    const char *dest)
 {
 	struct dentry_meta *dm;
 	struct dentry *dnode;
@@ -300,7 +331,71 @@ debugfs_remove_recursive(struct dentry *dnode)
 }
 
 static int
-debugfs_init(PFS_INIT_ARGS)
+debugfs_bool_get(void *data, uint64_t *ullval)
+{
+	bool *bval = data;
+
+	if (*bval)
+		*ullval = 1;
+	else
+		*ullval = 0;
+
+	return (0);
+}
+
+static int
+debugfs_bool_set(void *data, uint64_t ullval)
+{
+	bool *bval = data;
+
+	if (ullval)
+		*bval = 1;
+	else
+		*bval = 0;
+
+	return (0);
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_bool, debugfs_bool_get, debugfs_bool_set, "%llu\n");
+DEFINE_DEBUGFS_ATTRIBUTE(fops_bool_ro, debugfs_bool_get, NULL, "%llu\n");
+DEFINE_DEBUGFS_ATTRIBUTE(fops_bool_wo, NULL, debugfs_bool_set, "%llu\n");
+
+void
+debugfs_create_bool(const char *name, umode_t mode, struct dentry *parent, bool *value)
+{
+	debugfs_create_mode_unsafe(name, mode, parent, value, &fops_bool,
+	    &fops_bool_ro, &fops_bool_wo);
+}
+
+static int
+debugfs_ulong_get(void *data, uint64_t *value)
+{
+	uint64_t *uldata = data;
+	*value = *uldata;
+	return (0);
+}
+
+static int
+debugfs_ulong_set(void *data, uint64_t value)
+{
+	uint64_t *uldata = data;
+	*uldata = value;
+	return (0);
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_ulong, debugfs_ulong_get, debugfs_ulong_set, "%llu\n");
+DEFINE_DEBUGFS_ATTRIBUTE(fops_ulong_ro, debugfs_ulong_get, NULL, "%llu\n");
+DEFINE_DEBUGFS_ATTRIBUTE(fops_ulong_wo, NULL, debugfs_ulong_set, "%llu\n");
+
+void
+debugfs_create_ulong(const char *name, umode_t mode, struct dentry *parent, unsigned long *value)
+{
+	debugfs_create_mode_unsafe(name, mode, parent, value, &fops_ulong,
+	    &fops_ulong_ro, &fops_ulong_wo);
+}
+
+static int
+lindebugfs_init(PFS_INIT_ARGS)
 {
 
 	debugfs_root = pi->pi_root;
@@ -311,14 +406,10 @@ debugfs_init(PFS_INIT_ARGS)
 }
 
 static int
-debugfs_uninit(PFS_INIT_ARGS)
+lindebugfs_uninit(PFS_INIT_ARGS)
 {
 	return (0);
 }
 
-#ifdef PR_ALLOW_MOUNT_LINSYSFS
-PSEUDOFS(debugfs, 1, PR_ALLOW_MOUNT_LINSYSFS);
-#else
-PSEUDOFS(debugfs, 1, VFCF_JAIL);
-#endif
+PSEUDOFS(lindebugfs, 1, VFCF_JAIL);
 MODULE_DEPEND(lindebugfs, linuxkpi, 1, 1, 1);
diff --git a/sys/compat/linuxkpi/common/include/linux/dcache.h b/sys/compat/linuxkpi/common/include/linux/dcache.h
index 9f9943a18dc6..512a29ec046a 100644
--- a/sys/compat/linuxkpi/common/include/linux/dcache.h
+++ b/sys/compat/linuxkpi/common/include/linux/dcache.h
@@ -29,8 +29,9 @@
 #ifndef _LINUXKPI_LINUX_DCACHE_H
 #define	_LINUXKPI_LINUX_DCACHE_H
 
-struct vnode;
-struct pfs_node;
+#include <sys/vnode.h>
+
+#include <fs/pseudofs/pseudofs.h>
 
 struct dentry {
 	struct vnode *d_inode;
diff --git a/sys/compat/linuxkpi/common/include/linux/debugfs.h b/sys/compat/linuxkpi/common/include/linux/debugfs.h
index 6a3273cbbf3c..ba1fa009dc62 100644
--- a/sys/compat/linuxkpi/common/include/linux/debugfs.h
+++ b/sys/compat/linuxkpi/common/include/linux/debugfs.h
@@ -31,21 +31,52 @@
 #define _LINUXKPI_LINUX_DEBUGFS_H_
 
 #include <linux/fs.h>
+#include <linux/module.h>
 #include <linux/seq_file.h>
-
 #include <linux/types.h>
 
-void debugfs_remove(struct dentry *dentry);
+MALLOC_DECLARE(M_DFSINT);
+
+struct debugfs_reg32 {
+	char *name;
+	unsigned long offset;
+};
+
+struct debugfs_regset32 {
+	const struct debugfs_reg32 *regs;
+	int nregs;
+};
 
 struct dentry *debugfs_create_file(const char *name, umode_t mode,
-				   struct dentry *parent, void *data,
-				   const struct file_operations *fops);
+    struct dentry *parent, void *data,
+    const struct file_operations *fops);
+
+struct dentry *debugfs_create_file_unsafe(const char *name, umode_t mode,
+    struct dentry *parent, void *data,
+    const struct file_operations *fops);
+
+struct dentry *debugfs_create_mode_unsafe(const char *name, umode_t mode,
+    struct dentry *parent, void *data,
+    const struct file_operations *fops,
+    const struct file_operations *fops_ro,
+    const struct file_operations *fops_wo);
 
 struct dentry *debugfs_create_dir(const char *name, struct dentry *parent);
 
 struct dentry *debugfs_create_symlink(const char *name, struct dentry *parent,
-				      const char *dest);
+    const char *dest);
+
+void debugfs_remove(struct dentry *dentry);
 
 void debugfs_remove_recursive(struct dentry *dentry);
 
-#endif
+#define DEFINE_DEBUGFS_ATTRIBUTE(__fops, __get, __set, __fmt) \
+	DEFINE_SIMPLE_ATTRIBUTE(__fops, __get, __set, __fmt)
+
+void debugfs_create_bool(const char *name, umode_t mode, struct dentry *parent,
+    bool *value);
+
+void debugfs_create_ulong(const char *name, umode_t mode, struct dentry *parent,
+    unsigned long *value);
+
+#endif /* _LINUXKPI_LINUX_DEBUGFS_H_ */
diff --git a/sys/compat/linuxkpi/common/include/linux/fs.h b/sys/compat/linuxkpi/common/include/linux/fs.h
index a21c140d5a9b..55c2ee3e00c6 100644
--- a/sys/compat/linuxkpi/common/include/linux/fs.h
+++ b/sys/compat/linuxkpi/common/include/linux/fs.h
@@ -45,6 +45,8 @@
 #include <linux/dcache.h>
 #include <linux/capability.h>
 #include <linux/wait_bit.h>
+#include <linux/kernel.h>
+#include <linux/mutex.h>
 
 struct module;
 struct kiocb;
@@ -250,6 +252,7 @@ nonseekable_open(struct inode *inode, struct file *filp)
 static inline int
 simple_open(struct inode *inode, struct file *filp)
 {
+	filp->private_data = inode->i_private;
 	return 0;
 }
 
@@ -297,6 +300,12 @@ no_llseek(struct file *file, loff_t offset, int whence)
 	return (-ESPIPE);
 }
 
+static inline loff_t
+default_llseek(struct file *file, loff_t offset, int whence)
+{
+	return (no_llseek(file, offset, whence));
+}
+
 static inline loff_t
 noop_llseek(struct linux_file *file, loff_t offset, int whence)
 {
@@ -318,4 +327,73 @@ call_mmap(struct linux_file *file, struct vm_area_struct *vma)
 	return (file->f_op->mmap(file, vma));
 }
 
+static inline void
+i_size_write(struct inode *inode, loff_t i_size)
+{
+}
+
+/*
+ * simple_read_from_buffer: copy data from kernel-space origin
+ * buffer into user-space destination buffer
+ *
+ * @dest: destination buffer
+ * @read_size: number of bytes to be transferred
+ * @ppos: starting transfer position pointer
+ * @orig: origin buffer
+ * @buf_size: size of destination and origin buffers
+ *
+ * Return value:
+ * On success, total bytes copied with *ppos incremented accordingly.
+ * On failure, negative value.
+ */
+static inline ssize_t
+simple_read_from_buffer(void __user *dest, size_t read_size, loff_t *ppos,
+    void *orig, size_t buf_size)
+{
+	void *read_pos = ((char *) orig) + *ppos;
+	size_t buf_remain = buf_size - *ppos;
+	ssize_t num_read;
+
+	if (buf_remain < 0 || buf_remain > buf_size)
+		return -EINVAL;
+
+	if (read_size > buf_remain)
+		read_size = buf_remain;
+
+	/* copy_to_user returns number of bytes NOT read */
+	num_read = read_size - copy_to_user(dest, read_pos, read_size);
+	if (num_read == 0)
+		return -EFAULT;
+	*ppos += num_read;
+
+	return (num_read);
+}
+
+MALLOC_DECLARE(M_LSATTR);
+
+#define DEFINE_SIMPLE_ATTRIBUTE(__fops, __get, __set, __fmt)		\
+static inline int							\
+__fops ## _open(struct inode *inode, struct file *filp)			\
+{									\
+	return (simple_attr_open(inode, filp, __get, __set, __fmt));	\
+}									\
+static const struct file_operations __fops = {				\
+	.owner	 = THIS_MODULE,						\
+	.open	 = __fops ## _open,					\
+	.release = simple_attr_release,					\
+	.read	 = simple_attr_read,					\
+	.write	 = simple_attr_write,					\
+	.llseek	 = no_llseek						\
+}
+
+int simple_attr_open(struct inode *inode, struct file *filp,
+    int (*get)(void *, uint64_t *), int (*set)(void *, uint64_t),
+    const char *fmt);
+
+int simple_attr_release(struct inode *inode, struct file *filp);
+
+ssize_t simple_attr_read(struct file *filp, char *buf, size_t read_size, loff_t *ppos);
+
+ssize_t simple_attr_write(struct file *filp, const char *buf, size_t write_size, loff_t *ppos);
+
 #endif /* _LINUXKPI_LINUX_FS_H_ */
diff --git a/sys/compat/linuxkpi/common/include/linux/seq_file.h b/sys/compat/linuxkpi/common/include/linux/seq_file.h
index 9ab5ecce7768..f40b7984aa50 100644
--- a/sys/compat/linuxkpi/common/include/linux/seq_file.h
+++ b/sys/compat/linuxkpi/common/include/linux/seq_file.h
@@ -32,11 +32,12 @@
 
 #include <linux/types.h>
 #include <linux/fs.h>
-#include <sys/sbuf.h>
 
 #undef file
 #define inode vnode
 
+MALLOC_DECLARE(M_LSEQ);
+
 #define	DEFINE_SHOW_ATTRIBUTE(__name)					\
 static int __name ## _open(struct inode *inode, struct linux_file *file)	\
 {									\
@@ -51,11 +52,8 @@ static const struct file_operations __name ## _fops = {			\
 	.release	= single_release,				\
 }
 
-struct seq_operations;
-
 struct seq_file {
-	struct sbuf	*buf;
-
+	struct sbuf *buf;
 	const struct seq_operations *op;
 	const struct linux_file *file;
 	void *private;
@@ -78,7 +76,8 @@ off_t seq_lseek(struct linux_file *file, off_t offset, int whence);
 int single_open(struct linux_file *, int (*)(struct seq_file *, void *), void *);
 int single_release(struct inode *, struct linux_file *);
 
-#define seq_printf(m, fmt, ...) sbuf_printf((m)->buf, (fmt), ##__VA_ARGS__)
+void seq_vprintf(struct seq_file *m, const char *fmt, va_list args);
+void seq_printf(struct seq_file *m, const char *fmt, ...);
 
 #define seq_puts(m, str)	sbuf_printf((m)->buf, str)
 #define seq_putc(m, str)	sbuf_putc((m)->buf, str)
diff --git a/sys/compat/linuxkpi/common/src/linux_seq_file.c b/sys/compat/linuxkpi/common/src/linux_seq_file.c
index ed23bf8d010f..4099c9b4d062 100644
--- a/sys/compat/linuxkpi/common/src/linux_seq_file.c
+++ b/sys/compat/linuxkpi/common/src/linux_seq_file.c
@@ -45,16 +45,35 @@ MALLOC_DEFINE(M_LSEQ, "seq_file", "seq_file");
 ssize_t
 seq_read(struct linux_file *f, char *ubuf, size_t size, off_t *ppos)
 {
-	struct seq_file *m = f->private_data;
+	struct seq_file *m;
+	struct sbuf *sbuf;
 	void *p;
-	int rc;
-	off_t pos = 0;
+	ssize_t rc;
+
+	m = f->private_data;
+	sbuf = m->buf;
 
-	p = m->op->start(m, &pos);
+	p = m->op->start(m, ppos);
 	rc = m->op->show(m, p);
 	if (rc)
 		return (rc);
-	return (size);
+
+	rc = sbuf_finish(sbuf);
+	if (rc)
+		return (rc);
+
+	rc = sbuf_len(sbuf);
+	if (*ppos >= rc || size < 1)
+		return (-EINVAL);
+
+	size = min(rc - *ppos, size);
+	rc = strscpy(ubuf, sbuf_data(sbuf) + *ppos, size);
+
+	/* add 1 for null terminator */
+	if (rc > 0)
+		rc += 1;
+
+	return (rc);
 }
 
 int
@@ -101,15 +120,13 @@ seq_open(struct linux_file *f, const struct seq_operations *op)
 {
 	struct seq_file *p;
 
-	if (f->private_data != NULL)
-		log(LOG_WARNING, "%s private_data not NULL", __func__);
-
 	if ((p = malloc(sizeof(*p), M_LSEQ, M_NOWAIT|M_ZERO)) == NULL)
 		return (-ENOMEM);
 
-	f->private_data = p;
-	p->op = op;
+	p->buf = sbuf_new_auto();
 	p->file = f;
+	p->op = op;
+	f->private_data = (void *) p;
 	return (0);
 }
 
@@ -138,9 +155,14 @@ int
 seq_release(struct inode *inode __unused, struct linux_file *file)
 {
 	struct seq_file *m;
+	struct sbuf *s;
 
 	m = file->private_data;
+	s = m->buf;
+
+	sbuf_delete(s);
 	free(m, M_LSEQ);
+
 	return (0);
 }
 
@@ -160,3 +182,19 @@ single_release(struct vnode *v, struct linux_file *f)
 	free(__DECONST(void *, op), M_LSEQ);
 	return (rc);
 }
+
+void
+seq_vprintf(struct seq_file *m, const char *fmt, va_list args)
+{
+	sbuf_vprintf(m->buf, fmt, args);
+}
+
+void
+seq_printf(struct seq_file *m, const char *fmt, ...)
+{
+	va_list args;
+
+	va_start(args, fmt);
+	seq_vprintf(m, fmt, args);
+	va_end(args);
+}
diff --git a/sys/compat/linuxkpi/common/src/linux_simple_attr.c b/sys/compat/linuxkpi/common/src/linux_simple_attr.c
new file mode 100644
index 000000000000..5cc0c0984755
--- /dev/null
+++ b/sys/compat/linuxkpi/common/src/linux_simple_attr.c
@@ -0,0 +1,191 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2022, Jake Freeland <jfree@freebsd.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <sys/types.h>
+#include <linux/fs.h>
+
+MALLOC_DEFINE(M_LSATTR, "simple_attr", "Linux Simple Attribute File");
+
+struct simple_attr {
+	int (*get)(void *, uint64_t *);
+	int (*set)(void *, uint64_t);
+	void *data;
+	const char *fmt;
+	struct mutex mutex;
+};
+
+/*
+ * simple_attr_open: open and populate simple attribute data
+ *
+ * @inode: file inode
+ * @filp: file pointer
+ * @get: ->get() for reading file data
+ * @set: ->set() for writing file data
+ * @fmt: format specifier for data returned by @get
+ *
+ * Memory allocate a simple_attr and appropriately initialize its members.
+ * The simple_attr must be stored in filp->private_data.
+ * Simple attr files do not support seeking. Open the file as nonseekable.
+ *
+ * Return value: simple attribute file descriptor
+ */
+int
+simple_attr_open(struct inode *inode, struct file *filp,
+    int (*get)(void *, uint64_t *), int (*set)(void *, uint64_t),
+    const char *fmt)
+{
+	struct simple_attr *sattr;
+	sattr = malloc(sizeof(*sattr), M_LSATTR, M_ZERO | M_NOWAIT);
+	if (sattr == NULL)
+		return (-ENOMEM);
+
+	sattr->get = get;
+	sattr->set = set;
+	sattr->data = inode->i_private;
+	sattr->fmt = fmt;
+	mutex_init(&sattr->mutex);
+
+	filp->private_data = (void *) sattr;
+
+	return (nonseekable_open(inode, filp));
+}
+
+int
+simple_attr_release(struct inode *inode, struct file *filp)
+{
+	free(filp->private_data, M_LSATTR);
+	return (0);
+}
+
+/*
+ * simple_attr_read: read simple attr data and transfer into buffer
+ *
+ * @filp: file pointer
+ * @buf: kernel space buffer
+ * @read_size: number of bytes to be transferred
+ * @ppos: starting pointer position for transfer
+ *
+ * The simple_attr structure is stored in filp->private_data.
+ * ->get() retrieves raw file data.
+ * The ->fmt specifier can format this data to be human readable.
+ * This output is then transferred into the @buf buffer.
+ *
+ * Return value:
+ * On success, number of bytes transferred
+ * On failure, negative signed ERRNO
+ */
+ssize_t
+simple_attr_read(struct file *filp, char *buf, size_t read_size, loff_t *ppos)
+{
+	struct simple_attr *sattr;
+	uint64_t data;
+	ssize_t ret;
+	char prebuf[24];
+
+	sattr = filp->private_data;
+
+	if (sattr->get == NULL)
+		return (-EFAULT);
+
+	mutex_lock(&sattr->mutex);
+
+	ret = sattr->get(sattr->data, &data);
+	if (ret)
+		goto unlock;
+
+	scnprintf(prebuf, sizeof(prebuf), sattr->fmt, data);
+
+	ret = strlen(prebuf) + 1;
+	if (*ppos >= ret || read_size < 1) {
+		ret = -EINVAL;
+		goto unlock;
+	}
+
+	read_size = min(ret - *ppos, read_size);
+	ret = strscpy(buf, prebuf + *ppos, read_size);
+
+	/* add 1 for null terminator */
+	if (ret > 0)
+		ret += 1;
+
+unlock:
+	mutex_unlock(&sattr->mutex);
+	return (ret);
+}
+
+/*
+ * simple_attr_write: write contents of buffer into simple attribute file
+ *
+ * @filp: file pointer
+ * @buf: kernel space buffer
+ * @write_size: number bytes to be transferred
+ * @ppos: starting pointer position for transfer
+ *
+ * The simple_attr structure is stored in filp->private_data.
+ * Convert the @buf string to unsigned long long.
+ * ->set() writes unsigned long long data into the simple attr file.
+ *
+ * Return value:
+ * On success, number of bytes written to simple attr
+ * On failure, negative signed ERRNO
+ */
+ssize_t
+simple_attr_write(struct file *filp, const char *buf, size_t write_size, loff_t *ppos)
+{
+	struct simple_attr *sattr;
+	unsigned long long data;
+	size_t bufsize;
+	ssize_t ret;
+
+	sattr = filp->private_data;
+	bufsize = strlen(buf) + 1;
+
+	if (sattr->set == NULL)
+		return (-EFAULT);
+
+	if (*ppos >= bufsize || write_size < 1)
+		return (-EINVAL);
+
+	mutex_lock(&sattr->mutex);
+
+	ret = kstrtoull(buf + *ppos, 0, &data);
+	if (ret)
+		goto unlock;
+
+	ret = sattr->set(sattr->data, data);
+	if (ret)
+		goto unlock;
+
+	ret = bufsize - *ppos;
+
+unlock:
+	mutex_unlock(&sattr->mutex);
+	return (ret);
+}
diff --git a/sys/conf/files b/sys/conf/files
index 4397dcba280a..725e140dca19 100644
--- a/sys/conf/files
+++ b/sys/conf/files
@@ -4607,9 +4607,10 @@ compat/linuxkpi/common/src/linux_xarray.c	optional compat_linuxkpi \
 	compile-with "${LINUXKPI_C}"
 compat/linuxkpi/common/src/lkpi_iic_if.m	optional compat_linuxkpi
 
-compat/linuxkpi/common/src/linux_seq_file.c		optional compat_linuxkpi | lindebugfs \
+compat/linuxkpi/common/src/linux_seq_file.c	optional compat_linuxkpi | lindebugfs \
+	compile-with "${LINUXKPI_C}"
+compat/linuxkpi/common/src/linux_simple_attr.c	optional compat_linuxkpi | lindebugfs \
 	compile-with "${LINUXKPI_C}"
-
 compat/lindebugfs/lindebugfs.c			optional lindebugfs \
 	compile-with "${LINUXKPI_C}"
 
diff --git a/sys/modules/linuxkpi/Makefile b/sys/modules/linuxkpi/Makefile
index 21663a078027..aa2c452db03a 100644
--- a/sys/modules/linuxkpi/Makefile
+++ b/sys/modules/linuxkpi/Makefile
@@ -22,10 +22,11 @@ SRCS=	linux_compat.c \
 	linux_pci.c \
 	linux_radix.c \
 	linux_rcu.c \
-	linux_seq_file.c \
 	linux_schedule.c \
+	linux_seq_file.c \
 	linux_shmemfs.c \
 	linux_shrinker.c \
+	linux_simple_attr.c \
 	linux_skbuff.c \
 	linux_slab.c \
 	linux_tasklet.c \