firmware: Add new platform fallback mechanism and firmware_request_platform()

In some cases the platform's main firmware (e.g. the UEFI fw) may contain
an embedded copy of device firmware which needs to be (re)loaded into the
peripheral. Normally such firmware would be part of linux-firmware, but in
some cases this is not feasible, for 2 reasons:

1) The firmware is customized for a specific use-case of the chipset / use
with a specific hardware model, so we cannot have a single firmware file
for the chipset. E.g. touchscreen controller firmwares are compiled
specifically for the hardware model they are used with, as they are
calibrated for a specific model digitizer.

2) Despite repeated attempts we have failed to get permission to
redistribute the firmware. This is especially a problem with customized
firmwares, these get created by the chip vendor for a specific ODM and the
copyright may partially belong with the ODM, so the chip vendor cannot
give a blanket permission to distribute these.

This commit adds a new platform fallback mechanism to the firmware loader
which will try to lookup a device fw copy embedded in the platform's main
firmware if direct filesystem lookup fails.

Drivers which need such embedded fw copies can enable this fallback
mechanism by using the new firmware_request_platform() function.

Note that for now this is only supported on EFI platforms and even on
these platforms firmware_fallback_platform() only works if
CONFIG_EFI_EMBEDDED_FIRMWARE is enabled (this gets selected by drivers
which need this), in all other cases firmware_fallback_platform() simply
always returns -ENOENT.

Reported-by: Dave Olsthoorn <dave@bewaar.me>
Suggested-by: Peter Jones <pjones@redhat.com>
Acked-by: Luis Chamberlain <mcgrof@kernel.org>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
Link: https://lore.kernel.org/r/20200115163554.101315-5-hdegoede@redhat.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Hans de Goede 2020-01-15 17:35:48 +01:00 committed by Greg Kroah-Hartman
parent 4445eb6d94
commit e4c2c0ff00
10 changed files with 198 additions and 0 deletions

View File

@ -202,3 +202,106 @@ the following file:
If you echo 0 into it means MAX_JIFFY_OFFSET will be used. The data type
for the timeout is an int.
EFI embedded firmware fallback mechanism
========================================
On some devices the system's EFI code / ROM may contain an embedded copy
of firmware for some of the system's integrated peripheral devices and
the peripheral's Linux device-driver needs to access this firmware.
Device drivers which need such firmware can use the
firmware_request_platform() function for this, note that this is a
separate fallback mechanism from the other fallback mechanisms and
this does not use the sysfs interface.
A device driver which needs this can describe the firmware it needs
using an efi_embedded_fw_desc struct:
.. kernel-doc:: include/linux/efi_embedded_fw.h
:functions: efi_embedded_fw_desc
The EFI embedded-fw code works by scanning all EFI_BOOT_SERVICES_CODE memory
segments for an eight byte sequence matching prefix; if the prefix is found it
then does a sha256 over length bytes and if that matches makes a copy of length
bytes and adds that to its list with found firmwares.
To avoid doing this somewhat expensive scan on all systems, dmi matching is
used. Drivers are expected to export a dmi_system_id array, with each entries'
driver_data pointing to an efi_embedded_fw_desc.
To register this array with the efi-embedded-fw code, a driver needs to:
1. Always be builtin to the kernel or store the dmi_system_id array in a
separate object file which always gets builtin.
2. Add an extern declaration for the dmi_system_id array to
include/linux/efi_embedded_fw.h.
3. Add the dmi_system_id array to the embedded_fw_table in
drivers/firmware/efi/embedded-firmware.c wrapped in a #ifdef testing that
the driver is being builtin.
4. Add "select EFI_EMBEDDED_FIRMWARE if EFI_STUB" to its Kconfig entry.
The firmware_request_platform() function will always first try to load firmware
with the specified name directly from the disk, so the EFI embedded-fw can
always be overridden by placing a file under /lib/firmware.
Note that:
1. The code scanning for EFI embedded-firmware runs near the end
of start_kernel(), just before calling rest_init(). For normal drivers and
subsystems using subsys_initcall() to register themselves this does not
matter. This means that code running earlier cannot use EFI
embedded-firmware.
2. At the moment the EFI embedded-fw code assumes that firmwares always start at
an offset which is a multiple of 8 bytes, if this is not true for your case
send in a patch to fix this.
3. At the moment the EFI embedded-fw code only works on x86 because other archs
free EFI_BOOT_SERVICES_CODE before the EFI embedded-fw code gets a chance to
scan it.
4. The current brute-force scanning of EFI_BOOT_SERVICES_CODE is an ad-hoc
brute-force solution. There has been discussion to use the UEFI Platform
Initialization (PI) spec's Firmware Volume protocol. This has been rejected
because the FV Protocol relies on *internal* interfaces of the PI spec, and:
1. The PI spec does not define peripheral firmware at all
2. The internal interfaces of the PI spec do not guarantee any backward
compatibility. Any implementation details in FV may be subject to change,
and may vary system to system. Supporting the FV Protocol would be
difficult as it is purposely ambiguous.
Example how to check for and extract embedded firmware
------------------------------------------------------
To check for, for example Silead touchscreen controller embedded firmware,
do the following:
1. Boot the system with efi=debug on the kernel commandline
2. cp /sys/kernel/debug/efi/boot_services_code? to your home dir
3. Open the boot_services_code? files in a hex-editor, search for the
magic prefix for Silead firmware: F0 00 00 00 02 00 00 00, this gives you
the beginning address of the firmware inside the boot_services_code? file.
4. The firmware has a specific pattern, it starts with a 8 byte page-address,
typically F0 00 00 00 02 00 00 00 for the first page followed by 32-bit
word-address + 32-bit value pairs. With the word-address incrementing 4
bytes (1 word) for each pair until a page is complete. A complete page is
followed by a new page-address, followed by more word + value pairs. This
leads to a very distinct pattern. Scroll down until this pattern stops,
this gives you the end of the firmware inside the boot_services_code? file.
5. "dd if=boot_services_code? of=firmware bs=1 skip=<begin-addr> count=<len>"
will extract the firmware for you. Inspect the firmware file in a
hexeditor to make sure you got the dd parameters correct.
6. Copy it to /lib/firmware under the expected name to test it.
7. If the extracted firmware works, you can use the found info to fill an
efi_embedded_fw_desc struct to describe it, run "sha256sum firmware"
to get the sha256sum to put in the sha256 field.

View File

@ -12,6 +12,8 @@ a driver issues a firmware API call.
return it immediately
* The ''Direct filesystem lookup'' is performed next, if found we
return it immediately
* The ''Platform firmware fallback'' is performed next, but only when
firmware_request_platform() is used, if found we return it immediately
* If no firmware has been found and the fallback mechanism was enabled
the sysfs interface is created. After this either a kobject uevent
is issued or the custom firmware loading is relied upon for firmware

View File

@ -25,6 +25,11 @@ firmware_request_nowarn
.. kernel-doc:: drivers/base/firmware_loader/main.c
:functions: firmware_request_nowarn
firmware_request_platform
-------------------------
.. kernel-doc:: drivers/base/firmware_loader/main.c
:functions: firmware_request_platform
request_firmware_direct
-----------------------
.. kernel-doc:: drivers/base/firmware_loader/main.c

View File

@ -5,5 +5,6 @@ obj-$(CONFIG_FW_LOADER_USER_HELPER) += fallback_table.o
obj-$(CONFIG_FW_LOADER) += firmware_class.o
firmware_class-objs := main.o
firmware_class-$(CONFIG_FW_LOADER_USER_HELPER) += fallback.o
firmware_class-$(CONFIG_EFI_EMBEDDED_FIRMWARE) += fallback_platform.o
obj-y += builtin/

View File

@ -66,4 +66,14 @@ static inline void unregister_sysfs_loader(void)
}
#endif /* CONFIG_FW_LOADER_USER_HELPER */
#ifdef CONFIG_EFI_EMBEDDED_FIRMWARE
int firmware_fallback_platform(struct fw_priv *fw_priv, enum fw_opt opt_flags);
#else
static inline int firmware_fallback_platform(struct fw_priv *fw_priv,
enum fw_opt opt_flags)
{
return -ENOENT;
}
#endif
#endif /* __FIRMWARE_FALLBACK_H */

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/efi_embedded_fw.h>
#include <linux/property.h>
#include <linux/security.h>
#include <linux/vmalloc.h>
#include "fallback.h"
#include "firmware.h"
int firmware_fallback_platform(struct fw_priv *fw_priv, enum fw_opt opt_flags)
{
const u8 *data;
size_t size;
int rc;
if (!(opt_flags & FW_OPT_FALLBACK_PLATFORM))
return -ENOENT;
rc = security_kernel_load_data(LOADING_FIRMWARE_EFI_EMBEDDED);
if (rc)
return rc;
rc = efi_get_embedded_fw(fw_priv->fw_name, &data, &size);
if (rc)
return rc; /* rc == -ENOENT when the fw was not found */
fw_priv->data = vmalloc(size);
if (!fw_priv->data)
return -ENOMEM;
memcpy(fw_priv->data, data, size);
fw_priv->size = size;
fw_state_done(fw_priv);
return 0;
}

View File

@ -29,6 +29,9 @@
* firmware caching mechanism.
* @FW_OPT_NOFALLBACK_SYSFS: Disable the sysfs fallback mechanism. Takes
* precedence over &FW_OPT_UEVENT and &FW_OPT_USERHELPER.
* @FW_OPT_FALLBACK_PLATFORM: Enable fallback to device fw copy embedded in
* the platform's main firmware. If both this fallback and the sysfs
* fallback are enabled, then this fallback will be tried first.
*/
enum fw_opt {
FW_OPT_UEVENT = BIT(0),
@ -37,6 +40,7 @@ enum fw_opt {
FW_OPT_NO_WARN = BIT(3),
FW_OPT_NOCACHE = BIT(4),
FW_OPT_NOFALLBACK_SYSFS = BIT(5),
FW_OPT_FALLBACK_PLATFORM = BIT(6),
};
enum fw_status {

View File

@ -778,6 +778,9 @@ _request_firmware(const struct firmware **firmware_p, const char *name,
fw_decompress_xz);
#endif
if (ret == -ENOENT)
ret = firmware_fallback_platform(fw->priv, opt_flags);
if (ret) {
if (!(opt_flags & FW_OPT_NO_WARN))
dev_warn(device,
@ -885,6 +888,30 @@ int request_firmware_direct(const struct firmware **firmware_p,
}
EXPORT_SYMBOL_GPL(request_firmware_direct);
/**
* firmware_request_platform() - request firmware with platform-fw fallback
* @firmware: pointer to firmware image
* @name: name of firmware file
* @device: device for which firmware is being loaded
*
* This function is similar in behaviour to request_firmware, except that if
* direct filesystem lookup fails, it will fallback to looking for a copy of the
* requested firmware embedded in the platform's main (e.g. UEFI) firmware.
**/
int firmware_request_platform(const struct firmware **firmware,
const char *name, struct device *device)
{
int ret;
/* Need to pin this module until return */
__module_get(THIS_MODULE);
ret = _request_firmware(firmware, name, device, NULL, 0,
FW_OPT_UEVENT | FW_OPT_FALLBACK_PLATFORM);
module_put(THIS_MODULE);
return ret;
}
EXPORT_SYMBOL_GPL(firmware_request_platform);
/**
* firmware_request_cache() - cache firmware for suspend so resume can use it
* @name: name of firmware file

View File

@ -44,6 +44,8 @@ int request_firmware(const struct firmware **fw, const char *name,
struct device *device);
int firmware_request_nowarn(const struct firmware **fw, const char *name,
struct device *device);
int firmware_request_platform(const struct firmware **fw, const char *name,
struct device *device);
int request_firmware_nowait(
struct module *module, bool uevent,
const char *name, struct device *device, gfp_t gfp, void *context,
@ -69,6 +71,13 @@ static inline int firmware_request_nowarn(const struct firmware **fw,
return -EINVAL;
}
static inline int firmware_request_platform(const struct firmware **fw,
const char *name,
struct device *device)
{
return -EINVAL;
}
static inline int request_firmware_nowait(
struct module *module, bool uevent,
const char *name, struct device *device, gfp_t gfp, void *context,

View File

@ -2982,6 +2982,7 @@ extern int do_pipe_flags(int *, int);
id(UNKNOWN, unknown) \
id(FIRMWARE, firmware) \
id(FIRMWARE_PREALLOC_BUFFER, firmware) \
id(FIRMWARE_EFI_EMBEDDED, firmware) \
id(MODULE, kernel-module) \
id(KEXEC_IMAGE, kexec-image) \
id(KEXEC_INITRAMFS, kexec-initramfs) \