From: Jeff Layton <jlayton@kernel.org>
To: Luis Chamberlain <mcgrof@kernel.org>,
Russ Weight <russ.weight@linux.dev>,
Danilo Krummrich <dakr@kernel.org>,
Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
"Rafael J. Wysocki" <rafael@kernel.org>,
Shuah Khan <shuah@kernel.org>
Cc: Michal Grzedzicki <mge@meta.com>,
driver-core@lists.linux.dev, linux-kernel@vger.kernel.org,
linux-kselftest@vger.kernel.org,
Jeff Layton <jlayton@kernel.org>
Subject: [PATCH v5 1/2] firmware_loader: add search= module option for multi-path firmware lookup
Date: Mon, 23 Mar 2026 10:39:30 -0400 [thread overview]
Message-ID: <20260323-fw-path-v5-1-e88b2fe145f3@kernel.org> (raw)
In-Reply-To: <20260323-fw-path-v5-0-e88b2fe145f3@kernel.org>
Refactor fw_get_filesystem_firmware() by extracting the per-path
firmware loading logic into a new fw_try_firmware_path() helper.
Add a new firmware_class.search= module option that accepts a
':'-separated list of firmware search directories. The input is
preprocessed at set time (boot or sysfs write) into a NUL-separated
sequence of paths, avoiding per-load parsing overhead. Backslash
escapes are supported: '\:' for literal ':', '\\' for literal '\'.
The firmware lookup order is:
1. firmware_class.path= (single legacy path)
2. firmware_class.search= (colon-separated paths)
3. Built-in default paths (/lib/firmware/updates/..., /lib/firmware)
Example:
firmware_class.search=/custom/path1:/custom/path2
Suggested-by: Michal Grzedzicki <mge@meta.com>
Signed-off-by: Jeff Layton <jlayton@kernel.org>
---
drivers/base/firmware_loader/main.c | 273 ++++++++++++++++++++++++++----------
1 file changed, 197 insertions(+), 76 deletions(-)
diff --git a/drivers/base/firmware_loader/main.c b/drivers/base/firmware_loader/main.c
index a11b30dda23be563bd55f25474ceff2153ddd667..c86f86977aa436ce6ccc19506cb0a774e06d019c 100644
--- a/drivers/base/firmware_loader/main.c
+++ b/drivers/base/firmware_loader/main.c
@@ -469,8 +469,9 @@ static int fw_decompress_xz(struct device *dev, struct fw_priv *fw_priv,
/* direct firmware loading support */
static char fw_path_para[256];
+static char fw_search_para[4096];
+static int fw_search_len;
static const char * const fw_path[] = {
- fw_path_para,
"/lib/firmware/updates/" UTS_RELEASE,
"/lib/firmware/updates",
"/lib/firmware/" UTS_RELEASE,
@@ -485,6 +486,159 @@ static const char * const fw_path[] = {
module_param_string(path, fw_path_para, sizeof(fw_path_para), 0644);
MODULE_PARM_DESC(path, "customized firmware image search path with a higher priority than default path");
+/*
+ * fw_search_set - preprocess a colon-separated search path string
+ *
+ * Converts the input into a NUL-separated sequence of paths stored in
+ * fw_search_para, with fw_search_len tracking the total used length.
+ * Backslash escapes '\:' (literal ':') and '\\' (literal '\').
+ * Trailing newlines on each component are stripped.
+ */
+static int fw_search_set(const char *val, const struct kernel_param *kp)
+{
+ const char *p;
+ int len = 0;
+
+ if (!val) {
+ fw_search_para[0] = '\0';
+ fw_search_len = 0;
+ return 0;
+ }
+
+ for (p = val; *p; p++) {
+ if (p[0] == '\\' && (p[1] == ':' || p[1] == '\\')) {
+ p++;
+ if (len < sizeof(fw_search_para) - 2)
+ fw_search_para[len++] = *p;
+ } else if (*p == ':') {
+ /* strip trailing newline before the separator */
+ if (len > 0 && fw_search_para[len - 1] == '\n')
+ len--;
+ if (len > 0 && fw_search_para[len - 1] != '\0') {
+ if (len < sizeof(fw_search_para) - 2)
+ fw_search_para[len++] = '\0';
+ }
+ } else {
+ if (len < sizeof(fw_search_para) - 2)
+ fw_search_para[len++] = *p;
+ }
+ }
+
+ /* strip trailing newline from last component */
+ if (len > 0 && fw_search_para[len - 1] == '\n')
+ len--;
+
+ /* ensure double-NUL termination */
+ fw_search_para[len] = '\0';
+ fw_search_len = len;
+
+ return 0;
+}
+
+/*
+ * fw_search_get - reconstruct colon-separated string for sysfs reads
+ */
+static int fw_search_get(char *buffer, const struct kernel_param *kp)
+{
+ const char *p;
+ int pos = 0;
+
+ p = fw_search_para;
+ while (p < fw_search_para + fw_search_len) {
+ int slen = strlen(p);
+
+ if (!slen)
+ break;
+ if (pos > 0)
+ buffer[pos++] = ':';
+ memcpy(buffer + pos, p, slen);
+ pos += slen;
+ p += slen + 1;
+ }
+ buffer[pos] = '\0';
+ return pos;
+}
+
+static const struct kernel_param_ops fw_search_ops = {
+ .set = fw_search_set,
+ .get = fw_search_get,
+};
+module_param_cb(search, &fw_search_ops, NULL, 0644);
+MODULE_PARM_DESC(search, "colon-separated list of firmware search paths, tried after path= (use '\\:' for literal ':', '\\\\' for literal '\\')");
+
+static int
+fw_try_firmware_path(struct device *device, struct fw_priv *fw_priv,
+ const char *suffix,
+ int (*decompress)(struct device *dev,
+ struct fw_priv *fw_priv,
+ size_t in_size,
+ const void *in_buffer),
+ const char *dir, int dirlen,
+ char *path, void **bufp, size_t msize)
+{
+ size_t file_size = 0;
+ size_t *file_size_ptr = NULL;
+ size_t size;
+ int len, rc;
+
+ len = snprintf(path, PATH_MAX, "%.*s/%s%s",
+ dirlen, dir, fw_priv->fw_name, suffix);
+ if (len >= PATH_MAX)
+ return -ENAMETOOLONG;
+
+ fw_priv->size = 0;
+
+ /*
+ * The total file size is only examined when doing a partial
+ * read; the "full read" case needs to fail if the whole
+ * firmware was not completely loaded.
+ */
+ if ((fw_priv->opt_flags & FW_OPT_PARTIAL) && *bufp)
+ file_size_ptr = &file_size;
+
+ /* load firmware files from the mount namespace of init */
+ rc = kernel_read_file_from_path_initns(path, fw_priv->offset,
+ bufp, msize,
+ file_size_ptr,
+ READING_FIRMWARE);
+ if (rc < 0) {
+ if (!(fw_priv->opt_flags & FW_OPT_NO_WARN)) {
+ if (rc != -ENOENT)
+ dev_warn(device,
+ "loading %s failed with error %d\n",
+ path, rc);
+ else
+ dev_dbg(device,
+ "loading %s failed for no such file or directory.\n",
+ path);
+ }
+ return rc;
+ }
+ size = rc;
+
+ dev_dbg(device, "Loading firmware from %s\n", path);
+ if (decompress) {
+ dev_dbg(device, "f/w decompressing %s\n",
+ fw_priv->fw_name);
+ rc = decompress(device, fw_priv, size, *bufp);
+ /* discard the superfluous original content */
+ vfree(*bufp);
+ *bufp = NULL;
+ if (rc) {
+ fw_free_paged_buf(fw_priv);
+ return rc;
+ }
+ } else {
+ dev_dbg(device, "direct-loading %s\n",
+ fw_priv->fw_name);
+ if (!fw_priv->data)
+ fw_priv->data = *bufp;
+ fw_priv->size = size;
+ }
+ fw_state_done(fw_priv);
+ return 0;
+}
+
static int
fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv,
const char *suffix,
@@ -493,10 +647,9 @@ fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv,
size_t in_size,
const void *in_buffer))
{
- size_t size;
- int i, len, maxlen = 0;
+ int i;
int rc = -ENOENT;
- char *path, *nt = NULL;
+ char *path;
size_t msize = INT_MAX;
void *buffer = NULL;
@@ -511,83 +664,51 @@ fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv,
return -ENOMEM;
wait_for_initramfs();
- for (i = 0; i < ARRAY_SIZE(fw_path); i++) {
- size_t file_size = 0;
- size_t *file_size_ptr = NULL;
-
- /* skip the unset customized path */
- if (!fw_path[i][0])
- continue;
-
- /* strip off \n from customized path */
- maxlen = strlen(fw_path[i]);
- if (i == 0) {
- nt = strchr(fw_path[i], '\n');
- if (nt)
- maxlen = nt - fw_path[i];
- }
- len = snprintf(path, PATH_MAX, "%.*s/%s%s",
- maxlen, fw_path[i],
- fw_priv->fw_name, suffix);
- if (len >= PATH_MAX) {
- rc = -ENAMETOOLONG;
- break;
- }
+ /* Try the customized path first */
+ if (fw_path_para[0]) {
+ int dirlen = strlen(fw_path_para);
- fw_priv->size = 0;
+ /* strip trailing newline */
+ if (fw_path_para[dirlen - 1] == '\n')
+ dirlen--;
- /*
- * The total file size is only examined when doing a partial
- * read; the "full read" case needs to fail if the whole
- * firmware was not completely loaded.
- */
- if ((fw_priv->opt_flags & FW_OPT_PARTIAL) && buffer)
- file_size_ptr = &file_size;
-
- /* load firmware files from the mount namespace of init */
- rc = kernel_read_file_from_path_initns(path, fw_priv->offset,
- &buffer, msize,
- file_size_ptr,
- READING_FIRMWARE);
- if (rc < 0) {
- if (!(fw_priv->opt_flags & FW_OPT_NO_WARN)) {
- if (rc != -ENOENT)
- dev_warn(device,
- "loading %s failed with error %d\n",
- path, rc);
- else
- dev_dbg(device,
- "loading %s failed for no such file or directory.\n",
- path);
- }
- continue;
- }
- size = rc;
- rc = 0;
-
- dev_dbg(device, "Loading firmware from %s\n", path);
- if (decompress) {
- dev_dbg(device, "f/w decompressing %s\n",
- fw_priv->fw_name);
- rc = decompress(device, fw_priv, size, buffer);
- /* discard the superfluous original content */
- vfree(buffer);
- buffer = NULL;
- if (rc) {
- fw_free_paged_buf(fw_priv);
- continue;
- }
- } else {
- dev_dbg(device, "direct-loading %s\n",
- fw_priv->fw_name);
- if (!fw_priv->data)
- fw_priv->data = buffer;
- fw_priv->size = size;
+ rc = fw_try_firmware_path(device, fw_priv, suffix, decompress,
+ fw_path_para, dirlen,
+ path, &buffer, msize);
+ if (!rc)
+ goto done;
+ }
+
+ /* Try each preprocessed NUL-separated path in fw_search_para */
+ if (fw_search_len > 0) {
+ const char *p = fw_search_para;
+
+ while (p < fw_search_para + fw_search_len) {
+ int dirlen = strlen(p);
+
+ if (!dirlen)
+ break;
+ rc = fw_try_firmware_path(device, fw_priv,
+ suffix, decompress,
+ p, dirlen,
+ path, &buffer, msize);
+ if (!rc)
+ goto done;
+ p += dirlen + 1;
}
- fw_state_done(fw_priv);
- break;
}
+
+ /* Try default firmware paths */
+ for (i = 0; i < ARRAY_SIZE(fw_path); i++) {
+ rc = fw_try_firmware_path(device, fw_priv, suffix, decompress,
+ fw_path[i], strlen(fw_path[i]),
+ path, &buffer, msize);
+ if (!rc)
+ break;
+ }
+
+done:
__putname(path);
return rc;
--
2.53.0
next prev parent reply other threads:[~2026-03-23 14:39 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-23 14:39 [PATCH v5 0/2] firmware_loader: allow firmware_class.path to take multiple paths Jeff Layton
2026-03-23 14:39 ` Jeff Layton [this message]
2026-03-23 14:39 ` [PATCH v5 2/2] selftests/firmware: add search path test for firmware_class.search= Jeff Layton
2026-03-23 14:45 ` [PATCH v5 0/2] firmware_loader: allow firmware_class.path to take multiple paths Jeff Layton
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260323-fw-path-v5-1-e88b2fe145f3@kernel.org \
--to=jlayton@kernel.org \
--cc=dakr@kernel.org \
--cc=driver-core@lists.linux.dev \
--cc=gregkh@linuxfoundation.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=mcgrof@kernel.org \
--cc=mge@meta.com \
--cc=rafael@kernel.org \
--cc=russ.weight@linux.dev \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.