Skip to content

Commit

Permalink
firmware: Add support for loading compressed files
Browse files Browse the repository at this point in the history
This patch adds the support for loading compressed firmware files.
The primary motivation is to reduce the storage size; e.g. currently
the files in /lib/firmware on my machine counts up to 419MB, while
they can be reduced to 130MB by file compression.

The patch introduces a new kconfig option CONFIG_FW_LOADER_COMPRESS.
Even with this option set, the firmware loader still tries to load the
original firmware file as-is at first, but then falls back to the file
with ".xz" extension when it's not found, and the decompressed file
content is returned to the caller of request_firmware().  So, no
change is needed for the rest.

Currently only XZ format is supported.  A caveat is that the kernel XZ
helper code supports only CRC32 (or none) integrity check type, so
you'll have to compress the files via xz -C crc32 option.

Since we can't determine the expanded size immediately from an XZ
file, the patch re-uses the paged buffer that was used for the
user-mode fallback; it puts the decompressed content page, which are
vmapped at the end.  The paged buffer code is conditionally built with
a new Kconfig that is selected automatically.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
  • Loading branch information
Takashi Iwai authored and Greg Kroah-Hartman committed Jun 18, 2019
1 parent 5342e70 commit 82fd7a8
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 12 deletions.
18 changes: 18 additions & 0 deletions drivers/base/firmware_loader/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ config FW_LOADER

if FW_LOADER

config FW_LOADER_PAGED_BUF
bool

config EXTRA_FIRMWARE
string "Build named firmware blobs into the kernel binary"
help
Expand Down Expand Up @@ -67,6 +70,7 @@ config EXTRA_FIRMWARE_DIR

config FW_LOADER_USER_HELPER
bool "Enable the firmware sysfs fallback mechanism"
select FW_LOADER_PAGED_BUF
help
This option enables a sysfs loading facility to enable firmware
loading to the kernel through userspace as a fallback mechanism
Expand Down Expand Up @@ -151,5 +155,19 @@ config FW_LOADER_USER_HELPER_FALLBACK

If you are unsure about this, say N here.

config FW_LOADER_COMPRESS
bool "Enable compressed firmware support"
select FW_LOADER_PAGED_BUF
select XZ_DEC
help
This option enables the support for loading compressed firmware
files. The caller of firmware API receives the decompressed file
content. The compressed file is loaded as a fallback, only after
loading the raw file failed at first.

Currently only XZ-compressed files are supported, and they have to
be compressed with either none or crc32 integrity check type (pass
"-C crc32" option to xz command).

endif # FW_LOADER
endmenu
8 changes: 5 additions & 3 deletions drivers/base/firmware_loader/firmware.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ struct fw_priv {
void *data;
size_t size;
size_t allocated_size;
#ifdef CONFIG_FW_LOADER_USER_HELPER
#ifdef CONFIG_FW_LOADER_PAGED_BUF
bool is_paged_buf;
bool need_uevent;
struct page **pages;
int nr_pages;
int page_array_size;
#endif
#ifdef CONFIG_FW_LOADER_USER_HELPER
bool need_uevent;
struct list_head pending_list;
#endif
const char *fw_name;
Expand Down Expand Up @@ -133,7 +135,7 @@ static inline void fw_state_done(struct fw_priv *fw_priv)
int assign_fw(struct firmware *fw, struct device *device,
enum fw_opt opt_flags);

#ifdef CONFIG_FW_LOADER_USER_HELPER
#ifdef CONFIG_FW_LOADER_PAGED_BUF
void fw_free_paged_buf(struct fw_priv *fw_priv);
int fw_grow_paged_buf(struct fw_priv *fw_priv, int pages_needed);
int fw_map_paged_buf(struct fw_priv *fw_priv);
Expand Down
147 changes: 138 additions & 9 deletions drivers/base/firmware_loader/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <linux/syscore_ops.h>
#include <linux/reboot.h>
#include <linux/security.h>
#include <linux/xz.h>

#include <generated/utsrelease.h>

Expand Down Expand Up @@ -266,7 +267,7 @@ static void free_fw_priv(struct fw_priv *fw_priv)
spin_unlock(&fwc->lock);
}

#ifdef CONFIG_FW_LOADER_USER_HELPER
#ifdef CONFIG_FW_LOADER_PAGED_BUF
void fw_free_paged_buf(struct fw_priv *fw_priv)
{
int i;
Expand Down Expand Up @@ -335,6 +336,105 @@ int fw_map_paged_buf(struct fw_priv *fw_priv)
}
#endif

/*
* XZ-compressed firmware support
*/
#ifdef CONFIG_FW_LOADER_COMPRESS
/* show an error and return the standard error code */
static int fw_decompress_xz_error(struct device *dev, enum xz_ret xz_ret)
{
if (xz_ret != XZ_STREAM_END) {
dev_warn(dev, "xz decompression failed (xz_ret=%d)\n", xz_ret);
return xz_ret == XZ_MEM_ERROR ? -ENOMEM : -EINVAL;
}
return 0;
}

/* single-shot decompression onto the pre-allocated buffer */
static int fw_decompress_xz_single(struct device *dev, struct fw_priv *fw_priv,
size_t in_size, const void *in_buffer)
{
struct xz_dec *xz_dec;
struct xz_buf xz_buf;
enum xz_ret xz_ret;

xz_dec = xz_dec_init(XZ_SINGLE, (u32)-1);
if (!xz_dec)
return -ENOMEM;

xz_buf.in_size = in_size;
xz_buf.in = in_buffer;
xz_buf.in_pos = 0;
xz_buf.out_size = fw_priv->allocated_size;
xz_buf.out = fw_priv->data;
xz_buf.out_pos = 0;

xz_ret = xz_dec_run(xz_dec, &xz_buf);
xz_dec_end(xz_dec);

fw_priv->size = xz_buf.out_pos;
return fw_decompress_xz_error(dev, xz_ret);
}

/* decompression on paged buffer and map it */
static int fw_decompress_xz_pages(struct device *dev, struct fw_priv *fw_priv,
size_t in_size, const void *in_buffer)
{
struct xz_dec *xz_dec;
struct xz_buf xz_buf;
enum xz_ret xz_ret;
struct page *page;
int err = 0;

xz_dec = xz_dec_init(XZ_DYNALLOC, (u32)-1);
if (!xz_dec)
return -ENOMEM;

xz_buf.in_size = in_size;
xz_buf.in = in_buffer;
xz_buf.in_pos = 0;

fw_priv->is_paged_buf = true;
fw_priv->size = 0;
do {
if (fw_grow_paged_buf(fw_priv, fw_priv->nr_pages + 1)) {
err = -ENOMEM;
goto out;
}

/* decompress onto the new allocated page */
page = fw_priv->pages[fw_priv->nr_pages - 1];
xz_buf.out = kmap(page);
xz_buf.out_pos = 0;
xz_buf.out_size = PAGE_SIZE;
xz_ret = xz_dec_run(xz_dec, &xz_buf);
kunmap(page);
fw_priv->size += xz_buf.out_pos;
/* partial decompression means either end or error */
if (xz_buf.out_pos != PAGE_SIZE)
break;
} while (xz_ret == XZ_OK);

err = fw_decompress_xz_error(dev, xz_ret);
if (!err)
err = fw_map_paged_buf(fw_priv);

out:
xz_dec_end(xz_dec);
return err;
}

static int fw_decompress_xz(struct device *dev, struct fw_priv *fw_priv,
size_t in_size, const void *in_buffer)
{
/* if the buffer is pre-allocated, we can perform in single-shot mode */
if (fw_priv->data)
return fw_decompress_xz_single(dev, fw_priv, in_size, in_buffer);
else
return fw_decompress_xz_pages(dev, fw_priv, in_size, in_buffer);
}
#endif /* CONFIG_FW_LOADER_COMPRESS */

/* direct firmware loading support */
static char fw_path_para[256];
static const char * const fw_path[] = {
Expand All @@ -354,17 +454,24 @@ 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");

static int
fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv)
fw_get_filesystem_firmware(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))
{
loff_t size;
int i, len;
int rc = -ENOENT;
char *path;
enum kernel_read_file_id id = READING_FIRMWARE;
size_t msize = INT_MAX;
void *buffer = NULL;

/* Already populated data member means we're loading into a buffer */
if (fw_priv->data) {
if (!decompress && fw_priv->data) {
buffer = fw_priv->data;
id = READING_FIRMWARE_PREALLOC_BUFFER;
msize = fw_priv->allocated_size;
}
Expand All @@ -378,15 +485,15 @@ fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv)
if (!fw_path[i][0])
continue;

len = snprintf(path, PATH_MAX, "%s/%s",
fw_path[i], fw_priv->fw_name);
len = snprintf(path, PATH_MAX, "%s/%s%s",
fw_path[i], fw_priv->fw_name, suffix);
if (len >= PATH_MAX) {
rc = -ENAMETOOLONG;
break;
}

fw_priv->size = 0;
rc = kernel_read_file_from_path(path, &fw_priv->data, &size,
rc = kernel_read_file_from_path(path, &buffer, &size,
msize, id);
if (rc) {
if (rc != -ENOENT)
Expand All @@ -397,8 +504,24 @@ fw_get_filesystem_firmware(struct device *device, struct fw_priv *fw_priv)
path);
continue;
}
dev_dbg(device, "direct-loading %s\n", fw_priv->fw_name);
fw_priv->size = size;
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;
}
fw_state_done(fw_priv);
break;
}
Expand Down Expand Up @@ -645,7 +768,13 @@ _request_firmware(const struct firmware **firmware_p, const char *name,
if (ret <= 0) /* error or already assigned */
goto out;

ret = fw_get_filesystem_firmware(device, fw->priv);
ret = fw_get_filesystem_firmware(device, fw->priv, "", NULL);
#ifdef CONFIG_FW_LOADER_COMPRESS
if (ret == -ENOENT)
ret = fw_get_filesystem_firmware(device, fw->priv, ".xz",
fw_decompress_xz);
#endif

if (ret) {
if (!(opt_flags & FW_OPT_NO_WARN))
dev_warn(device,
Expand Down

0 comments on commit 82fd7a8

Please sign in to comment.