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, the patch
re-uses the paged buffer that was used for the user-mode fallback, and
puts the decompressed content page by page as well as a new kconfig for
enabling the paged buffer support.

Signed-off-by: Takashi Iwai <[email protected]>
---
 drivers/base/firmware_loader/Kconfig    |  18 +++++
 drivers/base/firmware_loader/firmware.h |   8 +-
 drivers/base/firmware_loader/main.c     | 135 +++++++++++++++++++++++++++++---
 3 files changed, 149 insertions(+), 12 deletions(-)

diff --git a/drivers/base/firmware_loader/Kconfig 
b/drivers/base/firmware_loader/Kconfig
index 38f2da6f5c2b..cc86d51d2999 100644
--- a/drivers/base/firmware_loader/Kconfig
+++ b/drivers/base/firmware_loader/Kconfig
@@ -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
@@ -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
@@ -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
+         trying to load the raw file at first.
+
+         Currently only XZ-compressed files are supported, and they have to
+         be compressed with either none or crc32 integrity check type (the
+         -C crc32 option of xz command).
+
 endif # FW_LOADER
 endmenu
diff --git a/drivers/base/firmware_loader/firmware.h 
b/drivers/base/firmware_loader/firmware.h
index 35f4e58b2d98..7048a41973ed 100644
--- a/drivers/base/firmware_loader/firmware.h
+++ b/drivers/base/firmware_loader/firmware.h
@@ -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;
@@ -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);
diff --git a/drivers/base/firmware_loader/main.c 
b/drivers/base/firmware_loader/main.c
index 7e12732f4705..b070d6b4f4f1 100644
--- a/drivers/base/firmware_loader/main.c
+++ b/drivers/base/firmware_loader/main.c
@@ -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>
 
@@ -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;
@@ -335,6 +336,98 @@ int fw_map_paged_buf(struct fw_priv *fw_priv)
 }
 #endif
 
+/*
+ * compressed firmware support
+ */
+#ifdef CONFIG_FW_LOADER_COMPRESS
+/* single-shot decompression onto the pre-allocated buffer */
+static enum xz_ret fw_decompress_single(struct fw_priv *fw_priv,
+                                       struct xz_dec *xz_dec,
+                                       struct xz_buf *xz_buf)
+{
+       enum xz_ret xz_ret;
+
+       xz_buf->out_pos = 0;
+       xz_buf->out = fw_priv->data;
+       xz_buf->out_size = fw_priv->allocated_size;
+       xz_ret = xz_dec_run(xz_dec, xz_buf);
+       fw_priv->size = xz_buf->out_pos;
+       return xz_ret;
+}
+
+/* decompression on paged buffer and map it */
+static enum xz_ret fw_decompress_pages(struct fw_priv *fw_priv,
+                                      struct xz_dec *xz_dec,
+                                      struct xz_buf *xz_buf)
+{
+       struct page *page;
+       enum xz_ret xz_ret;
+
+       fw_priv->is_paged_buf = true;
+       fw_priv->size = 0;
+       do {
+               if (fw_grow_paged_buf(fw_priv, fw_priv->nr_pages + 1))
+                       return XZ_MEM_ERROR;
+
+               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;
+       } while (xz_ret == XZ_OK);
+
+       if (xz_ret == XZ_STREAM_END) {
+               if (fw_map_paged_buf(fw_priv))
+                       return XZ_MEM_ERROR;
+       }
+
+       return xz_ret;
+}
+
+static int fw_decompress_buffer(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;
+       bool singleshot = false;
+
+       /* if the buffer is pre-allocaed, we can perform in single-shot mode */
+       if (fw_priv->data)
+               singleshot = true;
+
+       xz_dec = xz_dec_init(singleshot ? XZ_SINGLE : XZ_DYNALLOC, (u32)-1);
+       if (!xz_dec) {
+               dev_warn(dev, "f/w decompression init failed\n");
+               return -ENOMEM;
+       }
+
+       xz_buf.in_size = in_size;
+       xz_buf.in = in_buffer;
+       xz_buf.in_pos = 0;
+       if (singleshot)
+               xz_ret = fw_decompress_single(fw_priv, xz_dec, &xz_buf);
+       else
+               xz_ret = fw_decompress_pages(fw_priv, xz_dec, &xz_buf);
+       xz_dec_end(xz_dec);
+
+       if (xz_ret != XZ_STREAM_END) {
+               dev_warn(dev, "f/w decompression failed (xz_ret=%d)\n", xz_ret);
+               return xz_ret == XZ_MEM_ERROR ? -ENOMEM : -EINVAL;
+       }
+
+       return 0;
+}
+#else /* CONFIG_FW_LOADER_COMPRESS */
+static inline int fw_decompress_buffer(struct device *dev,  struct fw_priv *fw,
+                                      size_t in_size, const void *in_buffer)
+{
+       return -ENXIO;
+}
+#endif /* CONFIG_FW_LOADER_COMPRESS */
+
 /* direct firmware loading support */
 static char fw_path_para[256];
 static const char * const fw_path[] = {
@@ -354,7 +447,8 @@ 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,
+                          bool decompress)
 {
        loff_t size;
        int i, len;
@@ -362,9 +456,12 @@ fw_get_filesystem_firmware(struct device *device, struct 
fw_priv *fw_priv)
        char *path;
        enum kernel_read_file_id id = READING_FIRMWARE;
        size_t msize = INT_MAX;
+       void *buffer = NULL;
+       const char *suffix = decompress ? ".xz" : "";
 
        /* 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;
        }
@@ -378,15 +475,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)
@@ -397,8 +494,25 @@ 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 = fw_decompress_buffer(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;
        }
@@ -645,7 +759,10 @@ _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, false);
+       if (IS_ENABLED(CONFIG_FW_LOADER_COMPRESS) && ret == -ENOENT)
+               ret = fw_get_filesystem_firmware(device, fw->priv, true);
+
        if (ret) {
                if (!(opt_flags & FW_OPT_NO_WARN))
                        dev_warn(device,
-- 
2.16.4

Reply via email to