git: 2ca7b03d6275 - stable/14 - firmware: load binary firmware files

From: Warner Losh <imp_at_FreeBSD.org>
Date: Wed, 16 Oct 2024 14:32:05 UTC
The branch stable/14 has been updated by imp:

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

commit 2ca7b03d62750bc29d35b4b58dadf7f7a9520a01
Author:     Warner Losh <imp@FreeBSD.org>
AuthorDate: 2024-02-29 16:36:20 +0000
Commit:     Warner Losh <imp@FreeBSD.org>
CommitDate: 2024-10-16 14:19:21 +0000

    firmware: load binary firmware files
    
    When we can't find a .ko module to satisfy the firmware request, try
    harder by looking for a file to read in directly. We compose this file's
    name by appending the imagename parameter to the firmware path
    (currently hard-wired to be /boot/firmware, future plans are for a
    path). Allow this file to be unloaded when firmware_put() releases the
    last reference, but we don't need to do the indirection and dance we
    need to do when unloading the .ko that will unregister the firmware.
    
    Sponsored by:           Netflix
    Reviewed by:            manu, jhb
    Differential Revision:  https://reviews.freebsd.org/D43555
    
    (cherry picked from commit c7b1e980ae16c094e64f318ad2feb98e17bd8f2f)
---
 sys/kern/subr_firmware.c | 122 +++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 114 insertions(+), 8 deletions(-)

diff --git a/sys/kern/subr_firmware.c b/sys/kern/subr_firmware.c
index 6947bcb110e4..138eb6a7fcd8 100644
--- a/sys/kern/subr_firmware.c
+++ b/sys/kern/subr_firmware.c
@@ -30,6 +30,7 @@
 #include <sys/param.h>
 #include <sys/errno.h>
 #include <sys/eventhandler.h>
+#include <sys/fcntl.h>
 #include <sys/firmware.h>
 #include <sys/kernel.h>
 #include <sys/linker.h>
@@ -37,9 +38,12 @@
 #include <sys/malloc.h>
 #include <sys/module.h>
 #include <sys/mutex.h>
+#include <sys/namei.h>
 #include <sys/priv.h>
 #include <sys/proc.h>
 #include <sys/queue.h>
+#include <sys/sbuf.h>
+#include <sys/sysctl.h>
 #include <sys/systm.h>
 #include <sys/taskqueue.h>
 
@@ -86,8 +90,9 @@ struct priv_fw {
 	 */
 	struct priv_fw	*parent;
 
-	int 		flags;	/* record FIRMWARE_UNLOAD requests */
-#define FW_UNLOAD	0x100
+	int 		flags;
+#define FW_BINARY	0x080	/* Firmware directly loaded, file == NULL */
+#define FW_UNLOAD	0x100	/* record FIRMWARE_UNLOAD requests */
 
 	/*
 	 * 'file' is private info managed by the autoload/unload code.
@@ -121,7 +126,8 @@ static LIST_HEAD(, priv_fw) firmware_table;
 
 /*
  * Firmware module operations are handled in a separate task as they
- * might sleep and they require directory context to do i/o.
+ * might sleep and they require directory context to do i/o. We also
+ * use this when loading binaries directly.
  */
 static struct taskqueue *firmware_tq;
 static struct task firmware_unload_task;
@@ -134,6 +140,11 @@ MTX_SYSINIT(firmware, &firmware_mtx, "firmware table", MTX_DEF);
 
 static MALLOC_DEFINE(M_FIRMWARE, "firmware", "device firmware images");
 
+static uint64_t firmware_max_size = 8u << 20; /* Default to 8MB cap */
+SYSCTL_U64(_debug, OID_AUTO, firmware_max_size,
+    CTLFLAG_RWTUN, &firmware_max_size, 0,
+    "Max size permitted for a firmware file.");
+
 /*
  * Helper function to lookup a name.
  * As a side effect, it sets the pointer to a free slot, if any.
@@ -241,6 +252,85 @@ struct fw_loadimage {
 	uint32_t	flags;
 };
 
+static const char *fw_path = "/boot/firmware/";
+
+static void
+try_binary_file(const char *imagename, uint32_t flags)
+{
+	struct nameidata nd;
+	struct thread *td = curthread;
+	struct ucred *cred = td ? td->td_ucred : NULL;
+	struct sbuf *sb;
+	struct priv_fw *fp;
+	const char *fn;
+	struct vattr vattr;
+	void *data = NULL;
+	const struct firmware *fw;
+	int flags;
+	size_t resid;
+	int error;
+	bool warn = flags & FIRMWARE_GET_NOWARN;
+
+	/*
+	 * XXX TODO: Loop over some path instead of a single element path.
+	 * and fetch this path from the 'firmware_path' kenv the loader sets.
+	 */
+	sb = sbuf_new_auto();
+	sbuf_printf(sb, "%s%s", fw_path, imagename);
+	sbuf_finish(sb);
+	fn = sbuf_data(sb);
+	if (bootverbose)
+		printf("Trying to load binary firmware from %s\n", fn);
+
+	NDINIT(&nd, LOOKUP, FOLLOW, UIO_SYSSPACE, fn);
+	flags = FREAD;
+	error = vn_open(&nd, &flags, 0, NULL);
+	if (error)
+		goto err;
+	NDFREE_PNBUF(&nd);
+	if (nd.ni_vp->v_type != VREG)
+		goto err2;
+	error = VOP_GETATTR(nd.ni_vp, &vattr, cred);
+	if (error)
+		goto err2;
+
+	/*
+	 * Limit this to something sane, 8MB by default.
+	 */
+	if (vattr.va_size > firmware_max_size) {
+		printf("Firmware %s is too big: %ld bytes, %ld bytes max.\n",
+		    fn, vattr.va_size, firmware_max_size);
+		goto err2;
+	}
+	data = malloc(vattr.va_size, M_FIRMWARE, M_WAITOK);
+	error = vn_rdwr(UIO_READ, nd.ni_vp, (caddr_t)data, vattr.va_size, 0,
+	    UIO_SYSSPACE, IO_NODELOCKED, cred, NOCRED, &resid, td);
+	/* XXX make data read only? */
+	VOP_UNLOCK(nd.ni_vp);
+	vn_close(nd.ni_vp, FREAD, cred, td);
+	nd.ni_vp = NULL;
+	if (error != 0 || resid != 0)
+		goto err;
+	fw = firmware_register(fn, data, vattr.va_size, 0, NULL);
+	if (fw == NULL)
+		goto err;
+	fp = PRIV_FW(fw);
+	fp->flags |= FW_BINARY;
+	if (bootverbose)
+		printf("%s: Loaded binary firmware using %s\n", imagename, fn);
+	sbuf_delete(sb);
+	return;
+
+err2: /* cleanup in vn_open through vn_close */
+	VOP_UNLOCK(nd.ni_vp);
+	vn_close(nd.ni_vp, FREAD, cred, td);
+err:
+	free(data, M_FIRMWARE);
+	if (bootverbose || warn)
+		printf("%s: could not load binary firmware %s either\n", imagename, fn);
+	sbuf_delete(sb);
+}
+
 static void
 loadimage(void *arg, int npending __unused)
 {
@@ -254,6 +344,7 @@ loadimage(void *arg, int npending __unused)
 		if (bootverbose || (fwli->flags & FIRMWARE_GET_NOWARN) == 0)
 			printf("%s: could not load firmware image, error %d\n",
 			    fwli->imagename, error);
+		try_binary_file(fwli->imagename, fwli->flags);
 		mtx_lock(&firmware_mtx);
 		goto done;
 	}
@@ -409,17 +500,32 @@ EVENTHANDLER_DEFINE(mountroot, firmware_mountroot, NULL, 0);
 static void
 unloadentry(void *unused1, int unused2)
 {
-	struct priv_fw *fp;
+	struct priv_fw *fp, *tmp;
 
 	mtx_lock(&firmware_mtx);
 restart:
-	LIST_FOREACH(fp, &firmware_table, link) {
-		if (fp->file == NULL || fp->refcnt != 0 ||
-		    (fp->flags & FW_UNLOAD) == 0)
+	LIST_FOREACH_SAFE(fp, &firmware_table, link, tmp) {
+		if (((fp->flags & FW_BINARY) == 0 && fp->file == NULL) ||
+		    fp->refcnt != 0 || (fp->flags & FW_UNLOAD) == 0)
+			continue;
+
+		/*
+		 * If we directly loaded the firmware, then we just need to
+		 * remove the entry from the list and free the entry and go to
+		 * the next one.  There's no need for the indirection of the kld
+		 * module case, we free memory and go to the next one.
+		 */
+		if ((fp->flags & FW_BINARY) != 0) {
+			LIST_REMOVE(fp, link);
+			free(__DECONST(char *, fp->fw.data), M_FIRMWARE);
+			free(__DECONST(char *, fp->fw.name), M_FIRMWARE);
+			free(fp, M_FIRMWARE);
 			continue;
+		}
 
 		/*
-		 * Found an entry. Now:
+		 * Found an entry.  This is the kld case, so we have a more
+		 * complex dance.  Now:
 		 * 1. make sure we scan the table again
 		 * 2. clear FW_UNLOAD so we don't try this entry again.
 		 * 3. release the lock while trying to unload the module.