Most firmware names are hardcoded strings, or are constructed from fairly constrained format strings where the dynamic parts are just some hex numbers or such.
However, there are a couple codepaths in the kernel where firmware file names contain string components that are passed through from a device or semi-privileged userspace; the ones I could find (not counting interfaces that require root privileges) are:
- lpfc_sli4_request_firmware_update() seems to construct the firmware filename from "ModelName", a string that was previously parsed out of some descriptor ("Vital Product Data") in lpfc_fill_vpd() - nfp_net_fw_find() seems to construct a firmware filename from a model name coming from nfp_hwinfo_lookup(pf->hwinfo, "nffw.partno"), which I think parses some descriptor that was read from the device. (But this case likely isn't exploitable because the format string looks like "netronome/nic_%s", and there shouldn't be any *folders* starting with "netronome/nic_". The previous case was different because there, the "%s" is *at the start* of the format string.) - module_flash_fw_schedule() is reachable from the ETHTOOL_MSG_MODULE_FW_FLASH_ACT netlink command, which is marked as GENL_UNS_ADMIN_PERM (meaning CAP_NET_ADMIN inside a user namespace is enough to pass the privilege check), and takes a userspace-provided firmware name. (But I think to reach this case, you need to have CAP_NET_ADMIN over a network namespace that a special kind of ethernet device is mapped into, so I think this is not a viable attack path in practice.)
Fix it by rejecting any firmware names containing ".." path components.
For what it's worth, I went looking and haven't found any USB device drivers that use the firmware loader dangerously.
Cc: stable@vger.kernel.org Fixes: abb139e75c2c ("firmware: teach the kernel to load firmware files directly from the filesystem") Signed-off-by: Jann Horn jannh@google.com --- Changes in v2: - describe fix in commit message (dakr) - write check more clearly and with comment in separate helper (dakr) - document new restriction in comment above request_firmware() (dakr) - warn when new restriction is triggered - Link to v1: https://lore.kernel.org/r/20240820-firmware-traversal-v1-1-8699ffaa9276@goog... --- drivers/base/firmware_loader/main.c | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+)
diff --git a/drivers/base/firmware_loader/main.c b/drivers/base/firmware_loader/main.c index a03ee4b11134..dd47ce9a761f 100644 --- a/drivers/base/firmware_loader/main.c +++ b/drivers/base/firmware_loader/main.c @@ -849,6 +849,37 @@ static void fw_log_firmware_info(const struct firmware *fw, const char *name, {} #endif
+/* + * Reject firmware file names with ".." path components. + * There are drivers that construct firmware file names from device-supplied + * strings, and we don't want some device to be able to tell us "I would like to + * be sent my firmware from ../../../etc/shadow, please". + * + * Search for ".." surrounded by either '/' or start/end of string. + * + * This intentionally only looks at the firmware name, not at the firmware base + * directory or at symlink contents. + */ +static bool name_contains_dotdot(const char *name) +{ + size_t name_len = strlen(name); + size_t i; + + if (name_len < 2) + return false; + for (i = 0; i < name_len - 1; i++) { + /* do we see a ".." sequence? */ + if (name[i] != '.' || name[i+1] != '.') + continue; + + /* is it a path component? */ + if ((i == 0 || name[i-1] == '/') && + (i == name_len - 2 || name[i+2] == '/')) + return true; + } + return false; +} + /* called from request_firmware() and request_firmware_work_func() */ static int _request_firmware(const struct firmware **firmware_p, const char *name, @@ -869,6 +900,14 @@ _request_firmware(const struct firmware **firmware_p, const char *name, goto out; }
+ if (name_contains_dotdot(name)) { + dev_warn(device, + "Firmware load for '%s' refused, path contains '..' component", + name); + ret = -EINVAL; + goto out; + } + ret = _request_firmware_prepare(&fw, name, device, buf, size, offset, opt_flags); if (ret <= 0) /* error or already assigned */ @@ -946,6 +985,8 @@ _request_firmware(const struct firmware **firmware_p, const char *name, * @name will be used as $FIRMWARE in the uevent environment and * should be distinctive enough not to be confused with any other * firmware image for this or any other device. + * It must not contain any ".." path components - "foo/bar..bin" is + * allowed, but "foo/../bar.bin" is not. * * Caller must hold the reference count of @device. *
--- base-commit: b0da640826ba3b6506b4996a6b23a429235e6923 change-id: 20240820-firmware-traversal-6df8501b0fe4
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Most firmware names are hardcoded strings, or are constructed from fairly constrained format strings where the dynamic parts are just some hex numbers or such.
However, there are a couple codepaths in the kernel where firmware file names contain string components that are passed through from a device or semi-privileged userspace; the ones I could find (not counting interfaces that require root privileges) are:
- lpfc_sli4_request_firmware_update() seems to construct the firmware filename from "ModelName", a string that was previously parsed out of some descriptor ("Vital Product Data") in lpfc_fill_vpd()
- nfp_net_fw_find() seems to construct a firmware filename from a model name coming from nfp_hwinfo_lookup(pf->hwinfo, "nffw.partno"), which I think parses some descriptor that was read from the device. (But this case likely isn't exploitable because the format string looks like "netronome/nic_%s", and there shouldn't be any *folders* starting with "netronome/nic_". The previous case was different because there, the "%s" is *at the start* of the format string.)
- module_flash_fw_schedule() is reachable from the ETHTOOL_MSG_MODULE_FW_FLASH_ACT netlink command, which is marked as GENL_UNS_ADMIN_PERM (meaning CAP_NET_ADMIN inside a user namespace is enough to pass the privilege check), and takes a userspace-provided firmware name. (But I think to reach this case, you need to have CAP_NET_ADMIN over a network namespace that a special kind of ethernet device is mapped into, so I think this is not a viable attack path in practice.)
Fix it by rejecting any firmware names containing ".." path components.
For what it's worth, I went looking and haven't found any USB device drivers that use the firmware loader dangerously.
Cc: stable@vger.kernel.org Fixes: abb139e75c2c ("firmware: teach the kernel to load firmware files directly from the filesystem") Signed-off-by: Jann Horn jannh@google.com
I'm all for this, however a strong rejection outright for the first kernel release is bound to end up with some angry user with some oddball driver that had this for whatever stupid reason. Without a semantic patch assessment to do this (I think its possible with coccinelle) I'd suggest for now we leave the warning in place for one kernel release, and for the one after we enforce this.
Linus might feel differently over it, and may want it right away. I'll let him chime in.
Luis
On Fri, Aug 23, 2024 at 02:13:13PM -0700, Luis Chamberlain wrote:
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Most firmware names are hardcoded strings, or are constructed from fairly constrained format strings where the dynamic parts are just some hex numbers or such.
However, there are a couple codepaths in the kernel where firmware file names contain string components that are passed through from a device or semi-privileged userspace; the ones I could find (not counting interfaces that require root privileges) are:
- lpfc_sli4_request_firmware_update() seems to construct the firmware filename from "ModelName", a string that was previously parsed out of some descriptor ("Vital Product Data") in lpfc_fill_vpd()
- nfp_net_fw_find() seems to construct a firmware filename from a model name coming from nfp_hwinfo_lookup(pf->hwinfo, "nffw.partno"), which I think parses some descriptor that was read from the device. (But this case likely isn't exploitable because the format string looks like "netronome/nic_%s", and there shouldn't be any *folders* starting with "netronome/nic_". The previous case was different because there, the "%s" is *at the start* of the format string.)
- module_flash_fw_schedule() is reachable from the ETHTOOL_MSG_MODULE_FW_FLASH_ACT netlink command, which is marked as GENL_UNS_ADMIN_PERM (meaning CAP_NET_ADMIN inside a user namespace is enough to pass the privilege check), and takes a userspace-provided firmware name. (But I think to reach this case, you need to have CAP_NET_ADMIN over a network namespace that a special kind of ethernet device is mapped into, so I think this is not a viable attack path in practice.)
Fix it by rejecting any firmware names containing ".." path components.
For what it's worth, I went looking and haven't found any USB device drivers that use the firmware loader dangerously.
Cc: stable@vger.kernel.org Fixes: abb139e75c2c ("firmware: teach the kernel to load firmware files directly from the filesystem") Signed-off-by: Jann Horn jannh@google.com
I'm all for this, however a strong rejection outright for the first kernel release is bound to end up with some angry user with some oddball driver that had this for whatever stupid reason. Without a semantic patch assessment to do this (I think its possible with coccinelle) I'd
I don't think we can fully validate it, there are lots of cases, where path names are passed through large call stacks, concatenated with other strings, selected from arrays, etc.
So, if we want to be extra careful, we should indeed just warn for now.
suggest for now we leave the warning in place for one kernel release, and for the one after we enforce this.
I'd expect it to take a bit longer until someone recognizes when drivers for embedded stuff hit this, but that's probably fine, hopefully some vendor discovers it before it goes to end users. :)
Linus might feel differently over it, and may want it right away. I'll let him chime in.
Luis
On Sat, Aug 24, 2024 at 5:13 AM Luis Chamberlain mcgrof@kernel.org wrote:
I'm all for this, however a strong rejection outright for the first kernel release is bound to end up with some angry user with some oddball driver that had this for whatever stupid reason.
I can't actually see a reason why a firmware file would have a ".." component in it, so I think the immediate rejection is fine - particularly since it has a warning printout, so you see what happened and why.
I do wonder if we should just have a LOOKUP_NO_DOTDOT flag, and just use that.
[ Christian - the issue is the firmware loading path not wanting to have ".." in the pathname so that you can't load outside the normal firmware tree. We could also use LOOKUP_BENEATH, except kernel_read_file_from_path_initns() just takes one long path rather than "here's the base, and here's the path". ]
There might be other people who want LOOKUP_NO_DOTDOT for similar reasons. In fact, some people might want an even stronger "normalized path" validation, where empty components or just "." is invalid, just because that makes pathnames ambiguous.
Linus
On Sat, Aug 24, 2024 at 3:14 AM Linus Torvalds torvalds@linux-foundation.org wrote:
On Sat, Aug 24, 2024 at 5:13 AM Luis Chamberlain mcgrof@kernel.org wrote:
I'm all for this, however a strong rejection outright for the first kernel release is bound to end up with some angry user with some oddball driver that had this for whatever stupid reason.
I can't actually see a reason why a firmware file would have a ".." component in it, so I think the immediate rejection is fine - particularly since it has a warning printout, so you see what happened and why.
I do wonder if we should just have a LOOKUP_NO_DOTDOT flag, and just use that.
[ Christian - the issue is the firmware loading path not wanting to have ".." in the pathname so that you can't load outside the normal firmware tree. We could also use LOOKUP_BENEATH, except kernel_read_file_from_path_initns() just takes one long path rather than "here's the base, and here's the path". ]
One other difference between the semantics we need here and LOOKUP_BENEATH is that we need to allow *symlinks* that contain ".." components or absolute paths; just the original path string must not contain them. If root decides to put symlinks to other places on the disk into /lib/firmware, I think that's reasonable, and it's root's decision to make, and we shouldn't break that. (And as an example, on my Debian machine, I see that /lib/firmware/regulatory.db is a symlink to /etc/alternatives/regulatory.db, which in turn is a symlink to /lib/firmware/regulatory.db-debian. I also see a bunch of symlinks in subdirectories of /lib/firmware with ".." in the link destinations, though those don't escape from /lib/firmware.)
So if we do this with a lookup flag, it'd have to be something that only takes effect when nd->depth is 0, or something vaguely along those lines? IDK how exactly that part of the path walking code works.
There might be other people who want LOOKUP_NO_DOTDOT for similar reasons. In fact, some people might want an even stronger "normalized path" validation, where empty components or just "." is invalid, just because that makes pathnames ambiguous.
(For what it's worth, I think I have seen many copies of this kind of string-based checking for ".." components in various pieces of userspace code. I don't think I've seen many places in the kernel that would benefit from that.)
On Sat, 24 Aug 2024 at 09:49, Jann Horn jannh@google.com wrote:
One other difference between the semantics we need here and LOOKUP_BENEATH is that we need to allow *symlinks* that contain ".." components or absolute paths; just the original path string must not contain them.
Yup, fair enough - a LOOKUP_NO_DOTDOT (or LOOKUP_NORMALIZED) flag would only affect the top-most nameidata level. Which makes it different from some of the other nameidata flags.
Not really fundamentally harder, but different - it would involve having to also check nd->depth during the walk.
(For what it's worth, I think I have seen many copies of this kind of string-based checking for ".." components in various pieces of userspace code. I don't think I've seen many places in the kernel that would benefit from that.)
Yeah, the kernel usually has trusted sources for the (relatively few) pathnames it follows. The firmware case is probably fairly unusual, with other sources of kernel path walking tend to be paths that have been set by the administrator (eg the "fw_path" part that is set by a module parameter).
I was indeed thinking of user level possibly finding this useful, having seen a lot of "clean up pathname" code myself (git being one example).
Linus
On Sat, Aug 24, 2024 at 09:14:30AM GMT, Linus Torvalds wrote:
On Sat, Aug 24, 2024 at 5:13 AM Luis Chamberlain mcgrof@kernel.org wrote:
I'm all for this, however a strong rejection outright for the first kernel release is bound to end up with some angry user with some oddball driver that had this for whatever stupid reason.
I can't actually see a reason why a firmware file would have a ".." component in it, so I think the immediate rejection is fine - particularly since it has a warning printout, so you see what happened and why.
I do wonder if we should just have a LOOKUP_NO_DOTDOT flag, and just use that.
[ Christian - the issue is the firmware loading path not wanting to have ".." in the pathname so that you can't load outside the normal firmware tree. We could also use LOOKUP_BENEATH, except kernel_read_file_from_path_initns() just takes one long path rather than "here's the base, and here's the path". ]
There might be other people who want LOOKUP_NO_DOTDOT for similar reasons. In fact, some people might want an even stronger "normalized path" validation, where empty components or just "." is invalid, just because that makes pathnames ambiguous.
I think LOOKUP_NO_DOTDOT is potentially interesting for userspace as RESOLVE_NO_DOTDOT. Though Jann's reply made me wonder: If some userspace project wants to make sure that there are no ".." in the path while allowing symlinks and ".." in symlinks then wouldn't it be easier and cheaper for userspace to just manually check for ".." in the path themselves? I mean RESOLVE_NO_SYMLINKS and RESOLVE_NO_MAGICLINKS alleviate some proper pain. Not just because an appropariate userspace algorithm is very costly but also because it's hard to get right. But with RESOLVE_NO_DOTOT it feels like a pretty mundane string-parsing excercise that could be done in userspace. Although I may just lack imagination.
As long as we only have this very limited in-kernel user I think just using Jann's helper in this patch is probably good enough.
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Most firmware names are hardcoded strings, or are constructed from fairly constrained format strings where the dynamic parts are just some hex numbers or such.
However, there are a couple codepaths in the kernel where firmware file names contain string components that are passed through from a device or semi-privileged userspace; the ones I could find (not counting interfaces that require root privileges) are:
- lpfc_sli4_request_firmware_update() seems to construct the firmware filename from "ModelName", a string that was previously parsed out of some descriptor ("Vital Product Data") in lpfc_fill_vpd()
- nfp_net_fw_find() seems to construct a firmware filename from a model name coming from nfp_hwinfo_lookup(pf->hwinfo, "nffw.partno"), which I think parses some descriptor that was read from the device. (But this case likely isn't exploitable because the format string looks like "netronome/nic_%s", and there shouldn't be any *folders* starting with "netronome/nic_". The previous case was different because there, the "%s" is *at the start* of the format string.)
- module_flash_fw_schedule() is reachable from the ETHTOOL_MSG_MODULE_FW_FLASH_ACT netlink command, which is marked as GENL_UNS_ADMIN_PERM (meaning CAP_NET_ADMIN inside a user namespace is enough to pass the privilege check), and takes a userspace-provided firmware name. (But I think to reach this case, you need to have CAP_NET_ADMIN over a network namespace that a special kind of ethernet device is mapped into, so I think this is not a viable attack path in practice.)
Fix it by rejecting any firmware names containing ".." path components.
For what it's worth, I went looking and haven't found any USB device drivers that use the firmware loader dangerously.
Cc: stable@vger.kernel.org Fixes: abb139e75c2c ("firmware: teach the kernel to load firmware files directly from the filesystem") Signed-off-by: Jann Horn jannh@google.com
Changes in v2:
- describe fix in commit message (dakr)
- write check more clearly and with comment in separate helper (dakr)
- document new restriction in comment above request_firmware() (dakr)
- warn when new restriction is triggered
- Link to v1: https://lore.kernel.org/r/20240820-firmware-traversal-v1-1-8699ffaa9276@goog...
drivers/base/firmware_loader/main.c | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+)
diff --git a/drivers/base/firmware_loader/main.c b/drivers/base/firmware_loader/main.c index a03ee4b11134..dd47ce9a761f 100644 --- a/drivers/base/firmware_loader/main.c +++ b/drivers/base/firmware_loader/main.c @@ -849,6 +849,37 @@ static void fw_log_firmware_info(const struct firmware *fw, const char *name, {} #endif +/*
- Reject firmware file names with ".." path components.
- There are drivers that construct firmware file names from device-supplied
- strings, and we don't want some device to be able to tell us "I would like to
- be sent my firmware from ../../../etc/shadow, please".
- Search for ".." surrounded by either '/' or start/end of string.
- This intentionally only looks at the firmware name, not at the firmware base
- directory or at symlink contents.
- */
+static bool name_contains_dotdot(const char *name) +{
- size_t name_len = strlen(name);
- size_t i;
- if (name_len < 2)
return false;
- for (i = 0; i < name_len - 1; i++) {
/* do we see a ".." sequence? */
if (name[i] != '.' || name[i+1] != '.')
continue;
/* is it a path component? */
if ((i == 0 || name[i-1] == '/') &&
(i == name_len - 2 || name[i+2] == '/'))
return true;
- }
- return false;
+}
Why do you open code it, instead of using strstr() and strncmp() like you did in v1? I think your approach from v1 read way better.
/* called from request_firmware() and request_firmware_work_func() */ static int _request_firmware(const struct firmware **firmware_p, const char *name, @@ -869,6 +900,14 @@ _request_firmware(const struct firmware **firmware_p, const char *name, goto out; }
- if (name_contains_dotdot(name)) {
dev_warn(device,
"Firmware load for '%s' refused, path contains '..' component",
name);
ret = -EINVAL;
goto out;
- }
- ret = _request_firmware_prepare(&fw, name, device, buf, size, offset, opt_flags); if (ret <= 0) /* error or already assigned */
@@ -946,6 +985,8 @@ _request_firmware(const struct firmware **firmware_p, const char *name,
@name will be used as $FIRMWARE in the uevent environment and
should be distinctive enough not to be confused with any other
firmware image for this or any other device.
- It must not contain any ".." path components - "foo/bar..bin" is
- allowed, but "foo/../bar.bin" is not.
- Caller must hold the reference count of @device.
base-commit: b0da640826ba3b6506b4996a6b23a429235e6923 change-id: 20240820-firmware-traversal-6df8501b0fe4 -- Jann Horn jannh@google.com
On Sat, Aug 24, 2024 at 2:31 AM Danilo Krummrich dakr@kernel.org wrote:
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Fix it by rejecting any firmware names containing ".." path components.
[...]
+/*
- Reject firmware file names with ".." path components.
- There are drivers that construct firmware file names from device-supplied
- strings, and we don't want some device to be able to tell us "I would like to
- be sent my firmware from ../../../etc/shadow, please".
- Search for ".." surrounded by either '/' or start/end of string.
- This intentionally only looks at the firmware name, not at the firmware base
- directory or at symlink contents.
- */
+static bool name_contains_dotdot(const char *name) +{
size_t name_len = strlen(name);
size_t i;
if (name_len < 2)
return false;
for (i = 0; i < name_len - 1; i++) {
/* do we see a ".." sequence? */
if (name[i] != '.' || name[i+1] != '.')
continue;
/* is it a path component? */
if ((i == 0 || name[i-1] == '/') &&
(i == name_len - 2 || name[i+2] == '/'))
return true;
}
return false;
+}
Why do you open code it, instead of using strstr() and strncmp() like you did in v1? I think your approach from v1 read way better.
The code in v1 was kinda sloppy - it was probably good enough for this check, but not good enough to put in a function called name_contains_dotdot() that is documented to exactly search for any ".." components.
Basically, the precise regex we have to search for is something like /(^|/)..($|/)/
To implement that by searching for substrings like in v1, we'd have to search for each possible combination of the capture groups in the regex, which gives the following four (pow(2,2)) patterns:
<start>..<end> <start>../ /..<end> /../
So written like in v1, that'd look something like:
if (strcmp(name, "..") == 0 || strncmp(name, "../", 3) == 0 || strstr(name, "/../") != NULL || (name_len >= 3 && strcmp(name+name_len-3, "/..") == 0))) return true;
Compared to that, I prefer the code I wrote in v2, since it is less repetitive. But if you want, I can change it to the expression I wrote just now.
Jann Horn jannh@google.com writes:
On Sat, Aug 24, 2024 at 2:31 AM Danilo Krummrich dakr@kernel.org wrote:
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Fix it by rejecting any firmware names containing ".." path components.
[...]
+/*
- Reject firmware file names with ".." path components.
- There are drivers that construct firmware file names from device-supplied
- strings, and we don't want some device to be able to tell us "I would like to
- be sent my firmware from ../../../etc/shadow, please".
- Search for ".." surrounded by either '/' or start/end of string.
- This intentionally only looks at the firmware name, not at the firmware base
- directory or at symlink contents.
- */
+static bool name_contains_dotdot(const char *name) +{
size_t name_len = strlen(name);
size_t i;
if (name_len < 2)
return false;
for (i = 0; i < name_len - 1; i++) {
/* do we see a ".." sequence? */
if (name[i] != '.' || name[i+1] != '.')
continue;
/* is it a path component? */
if ((i == 0 || name[i-1] == '/') &&
(i == name_len - 2 || name[i+2] == '/'))
return true;
}
return false;
+}
Why do you open code it, instead of using strstr() and strncmp() like you did in v1? I think your approach from v1 read way better.
The code in v1 was kinda sloppy - it was probably good enough for this check, but not good enough to put in a function called name_contains_dotdot() that is documented to exactly search for any ".." components.
Basically, the precise regex we have to search for is something like /(^|/)..($|/)/
To implement that by searching for substrings like in v1, we'd have to search for each possible combination of the capture groups in the regex, which gives the following four (pow(2,2)) patterns:
<start>..<end> <start>../ /..<end> /../
So written like in v1, that'd look something like:
if (strcmp(name, "..") == 0 || strncmp(name, "../", 3) == 0 || strstr(name, "/../") != NULL || (name_len >= 3 && strcmp(name+name_len-3, "/..") == 0))) return true;
Compared to that, I prefer the code I wrote in v2, since it is less repetitive. But if you want, I can change it to the expression I wrote just now.
Maybe
for (p = s; (q = strstr(p, "..")) != NULL; p = q+2) { if ((q == s || q[-1] == '/') && (q[2] == '\0' || q[2] == '/')) return true; } return false;
?
Rasmus
On Sat, Aug 24, 2024 at 03:34:20AM +0200, Jann Horn wrote:
On Sat, Aug 24, 2024 at 2:31 AM Danilo Krummrich dakr@kernel.org wrote:
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Fix it by rejecting any firmware names containing ".." path components.
[...]
+/*
- Reject firmware file names with ".." path components.
- There are drivers that construct firmware file names from device-supplied
- strings, and we don't want some device to be able to tell us "I would like to
- be sent my firmware from ../../../etc/shadow, please".
- Search for ".." surrounded by either '/' or start/end of string.
- This intentionally only looks at the firmware name, not at the firmware base
- directory or at symlink contents.
- */
+static bool name_contains_dotdot(const char *name) +{
size_t name_len = strlen(name);
size_t i;
if (name_len < 2)
return false;
for (i = 0; i < name_len - 1; i++) {
/* do we see a ".." sequence? */
if (name[i] != '.' || name[i+1] != '.')
continue;
/* is it a path component? */
if ((i == 0 || name[i-1] == '/') &&
(i == name_len - 2 || name[i+2] == '/'))
return true;
}
return false;
+}
Why do you open code it, instead of using strstr() and strncmp() like you did in v1? I think your approach from v1 read way better.
The code in v1 was kinda sloppy - it was probably good enough for this check, but not good enough to put in a function called name_contains_dotdot() that is documented to exactly search for any ".." components.
Basically, the precise regex we have to search for is something like /(^|/)..($|/)/
To implement that by searching for substrings like in v1, we'd have to search for each possible combination of the capture groups in the regex, which gives the following four (pow(2,2)) patterns:
<start>..<end> <start>../ /..<end> /../
I see.
So written like in v1, that'd look something like:
if (strcmp(name, "..") == 0 || strncmp(name, "../", 3) == 0 || strstr(name, "/../") != NULL || (name_len >= 3 && strcmp(name+name_len-3, "/..") == 0))) return true;
I think I still slightly prefer this variant, but I think either one is fine.
With one or the other and dev_warn() fixed,
Reviewed-by: Danilo Krummrich dakr@kernel.org
Compared to that, I prefer the code I wrote in v2, since it is less repetitive. But if you want, I can change it to the expression I wrote just now.
On Fri, Aug 23, 2024 at 08:38:55PM +0200, Jann Horn wrote:
Most firmware names are hardcoded strings, or are constructed from fairly constrained format strings where the dynamic parts are just some hex numbers or such.
However, there are a couple codepaths in the kernel where firmware file names contain string components that are passed through from a device or semi-privileged userspace; the ones I could find (not counting interfaces that require root privileges) are:
- lpfc_sli4_request_firmware_update() seems to construct the firmware filename from "ModelName", a string that was previously parsed out of some descriptor ("Vital Product Data") in lpfc_fill_vpd()
- nfp_net_fw_find() seems to construct a firmware filename from a model name coming from nfp_hwinfo_lookup(pf->hwinfo, "nffw.partno"), which I think parses some descriptor that was read from the device. (But this case likely isn't exploitable because the format string looks like "netronome/nic_%s", and there shouldn't be any *folders* starting with "netronome/nic_". The previous case was different because there, the "%s" is *at the start* of the format string.)
- module_flash_fw_schedule() is reachable from the ETHTOOL_MSG_MODULE_FW_FLASH_ACT netlink command, which is marked as GENL_UNS_ADMIN_PERM (meaning CAP_NET_ADMIN inside a user namespace is enough to pass the privilege check), and takes a userspace-provided firmware name. (But I think to reach this case, you need to have CAP_NET_ADMIN over a network namespace that a special kind of ethernet device is mapped into, so I think this is not a viable attack path in practice.)
Fix it by rejecting any firmware names containing ".." path components.
For what it's worth, I went looking and haven't found any USB device drivers that use the firmware loader dangerously.
Cc: stable@vger.kernel.org Fixes: abb139e75c2c ("firmware: teach the kernel to load firmware files directly from the filesystem") Signed-off-by: Jann Horn jannh@google.com
Changes in v2:
- describe fix in commit message (dakr)
- write check more clearly and with comment in separate helper (dakr)
- document new restriction in comment above request_firmware() (dakr)
- warn when new restriction is triggered
- Link to v1: https://lore.kernel.org/r/20240820-firmware-traversal-v1-1-8699ffaa9276@goog...
drivers/base/firmware_loader/main.c | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+)
diff --git a/drivers/base/firmware_loader/main.c b/drivers/base/firmware_loader/main.c index a03ee4b11134..dd47ce9a761f 100644 --- a/drivers/base/firmware_loader/main.c +++ b/drivers/base/firmware_loader/main.c @@ -849,6 +849,37 @@ static void fw_log_firmware_info(const struct firmware *fw, const char *name, {} #endif +/*
- Reject firmware file names with ".." path components.
- There are drivers that construct firmware file names from device-supplied
- strings, and we don't want some device to be able to tell us "I would like to
- be sent my firmware from ../../../etc/shadow, please".
- Search for ".." surrounded by either '/' or start/end of string.
- This intentionally only looks at the firmware name, not at the firmware base
- directory or at symlink contents.
- */
+static bool name_contains_dotdot(const char *name) +{
- size_t name_len = strlen(name);
- size_t i;
- if (name_len < 2)
return false;
- for (i = 0; i < name_len - 1; i++) {
/* do we see a ".." sequence? */
if (name[i] != '.' || name[i+1] != '.')
continue;
/* is it a path component? */
if ((i == 0 || name[i-1] == '/') &&
(i == name_len - 2 || name[i+2] == '/'))
return true;
- }
- return false;
+}
/* called from request_firmware() and request_firmware_work_func() */ static int _request_firmware(const struct firmware **firmware_p, const char *name, @@ -869,6 +900,14 @@ _request_firmware(const struct firmware **firmware_p, const char *name, goto out; }
- if (name_contains_dotdot(name)) {
dev_warn(device,
"Firmware load for '%s' refused, path contains '..' component",
name);
I think you're missing '\n' here.
ret = -EINVAL;
goto out;
- }
- ret = _request_firmware_prepare(&fw, name, device, buf, size, offset, opt_flags); if (ret <= 0) /* error or already assigned */
@@ -946,6 +985,8 @@ _request_firmware(const struct firmware **firmware_p, const char *name,
@name will be used as $FIRMWARE in the uevent environment and
should be distinctive enough not to be confused with any other
firmware image for this or any other device.
- It must not contain any ".." path components - "foo/bar..bin" is
- allowed, but "foo/../bar.bin" is not.
- Caller must hold the reference count of @device.
base-commit: b0da640826ba3b6506b4996a6b23a429235e6923 change-id: 20240820-firmware-traversal-6df8501b0fe4 -- Jann Horn jannh@google.com
linux-stable-mirror@lists.linaro.org