For a compressed kernel_noload image, bootm_load_os() allocates a
per-image decompression buffer of ALIGN(image_len * 8, SZ_1M). The 8x
multiplier is a heuristic: it comfortably covers what zstd and xz
achieve on real kernels, but any well-compressed payload (say, a big
run of zeros) can exceed it and fail decompression, and no fixed
multiplier is safe against arbitrarily compressible input.

Read the real uncompressed size from the compressor header instead.
Add a small helper image_decomp_get_uncompressed_size() that returns
the uncompressed size when the format carries one: gzip ISIZE, lzma
header uncompressed size, lz4 frame Content_Size when the FLG bit is
set, and zstd Frame_Content_Size. Other formats return -EOPNOTSUPP.
Bootm uses it to size the buffer to ALIGN(hdr_size, SZ_1M), capped at
CONFIG_SYS_BOOTM_LEN because the value is attacker-controlled, and
falls back to the 8x heuristic for formats without a size field
(bzip2, lzo, xz) or when the header lacks the size (some lzma or lz4
streams).

Suggested-by: Simon Glass <[email protected]>
Signed-off-by: Aristo Chen <[email protected]>
---
 boot/bootm.c    | 20 +++++++++----
 boot/image.c    | 79 +++++++++++++++++++++++++++++++++++++++++++++++++
 include/image.h | 25 ++++++++++++++++
 3 files changed, 119 insertions(+), 5 deletions(-)

diff --git a/boot/bootm.c b/boot/bootm.c
index 3bce8586834..6ce98485889 100644
--- a/boot/bootm.c
+++ b/boot/bootm.c
@@ -654,17 +654,28 @@ static int bootm_load_os(struct bootm_headers *images, 
int boot_progress)
        void *load_buf, *image_buf;
        int err;
 
+       image_buf = map_sysmem(os.image_start, image_len);
+
        /*
         * For a "noload" compressed kernel we need to allocate a buffer large
         * enough to decompress in to and use that as the load address now.
-        * Allow up to 8x compression: this comfortably covers what zstd and xz
-        * achieve on real kernels, with headroom for well-compressed payloads.
-        * Use an alignment of 2MB since this might help arm64
+        * Prefer the uncompressed size the compressor header carries (gzip,
+        * lzma, lz4-with-content-size, zstd); the value is attacker-controlled
+        * so cap it at CONFIG_SYS_BOOTM_LEN. Otherwise fall back to an 8x
+        * multiplier, which comfortably covers what zstd and xz achieve on
+        * real kernels with headroom for well-compressed payloads. Align to
+        * 2MB since this might help arm64.
         */
        if (os.type == IH_TYPE_KERNEL_NOLOAD && os.comp != IH_COMP_NONE) {
                phys_addr_t addr;
+               ulong hdr_size = 0;
 
-               decomp_len = ALIGN(image_len * 8, SZ_1M);
+               if (!image_decomp_get_uncompressed_size(os.comp, image_buf,
+                                                       image_len, &hdr_size) &&
+                   hdr_size && hdr_size <= CONFIG_SYS_BOOTM_LEN)
+                       decomp_len = ALIGN(hdr_size, SZ_1M);
+               else
+                       decomp_len = ALIGN(image_len * 8, SZ_1M);
                decomp_limit = BOOTM_DECOMP_LIMIT_PER_IMAGE;
                err = lmb_alloc_mem(LMB_MEM_ALLOC_ANY, SZ_2M, &addr,
                                    decomp_len, LMB_NONE);
@@ -679,7 +690,6 @@ static int bootm_load_os(struct bootm_headers *images, int 
boot_progress)
        }
 
        load_buf = map_sysmem(load, 0);
-       image_buf = map_sysmem(os.image_start, image_len);
        err = image_decomp(os.comp, load, os.image_start, os.type,
                           load_buf, image_buf, image_len,
                           decomp_len, &load_end);
diff --git a/boot/image.c b/boot/image.c
index 185d52ba492..5476c81bb6f 100644
--- a/boot/image.c
+++ b/boot/image.c
@@ -22,6 +22,7 @@
 
 #include <linux/errno.h>
 #include <asm/io.h>
+#include <asm/unaligned.h>
 
 /* Set this if we have less than 4 MB of malloc() space */
 #if CONFIG_SYS_MALLOC_LEN < (4096 * 1024)
@@ -442,6 +443,84 @@ int image_decomp_type(const unsigned char *buf, ulong len)
        return cmagic->comp_id;
 }
 
+#ifndef USE_HOSTCC
+int image_decomp_get_uncompressed_size(int comp, const void *src, ulong len,
+                                      ulong *sizep)
+{
+       const u8 *bytes = src;
+
+       switch (comp) {
+       case IH_COMP_GZIP: {
+               u32 isize;
+
+               /* Minimum gzip: 10-byte header + 2-byte deflate + 8-byte 
trailer */
+               if (len < 20)
+                       return -EINVAL;
+               if (bytes[0] != 0x1f || bytes[1] != 0x8b)
+                       return -EINVAL;
+               isize = get_unaligned_le32(bytes + len - 4);
+               *sizep = isize;
+               return 0;
+       }
+       case IH_COMP_LZMA:
+               if (CONFIG_IS_ENABLED(LZMA)) {
+                       u64 usize;
+
+                       /* LZMA header: 5-byte props + 8-byte uncompressed size 
*/
+                       if (len < LZMA_PROPS_SIZE + 8)
+                               return -EINVAL;
+                       usize = get_unaligned_le64(bytes + LZMA_PROPS_SIZE);
+                       /* All-ones means "unknown", per the LZMA reference */
+                       if (usize == U64_MAX)
+                               return -EOPNOTSUPP;
+                       if (usize > ULONG_MAX)
+                               return -EINVAL;
+                       *sizep = (ulong)usize;
+                       return 0;
+               }
+               return -EOPNOTSUPP;
+       case IH_COMP_LZ4:
+               if (CONFIG_IS_ENABLED(LZ4)) {
+                       u8 flg;
+
+                       /* LZ4 frame: 4-byte magic + FLG + BD + optional 8-byte 
size */
+                       if (len < 6)
+                               return -EINVAL;
+                       if (get_unaligned_le32(bytes) != LZ4F_MAGIC)
+                               return -EINVAL;
+                       flg = bytes[4];
+                       /* Content-size flag (FLG bit 3): 8 bytes follow BD */
+                       if (!(flg & 0x08))
+                               return -EOPNOTSUPP;
+                       if (len < 14)
+                               return -EINVAL;
+                       *sizep = get_unaligned_le64(bytes + 6);
+                       return 0;
+               }
+               return -EOPNOTSUPP;
+       case IH_COMP_ZSTD:
+               if (CONFIG_IS_ENABLED(ZSTD)) {
+                       zstd_frame_header hdr;
+                       size_t ret;
+
+                       ret = zstd_get_frame_header(&hdr, src, len);
+                       if (zstd_is_error(ret) || ret > 0)
+                               return -EINVAL;
+                       if (hdr.frameContentSize == ZSTD_CONTENTSIZE_UNKNOWN)
+                               return -EOPNOTSUPP;
+                       if (hdr.frameContentSize == ZSTD_CONTENTSIZE_ERROR ||
+                           hdr.frameContentSize > ULONG_MAX)
+                               return -EINVAL;
+                       *sizep = (ulong)hdr.frameContentSize;
+                       return 0;
+               }
+               return -EOPNOTSUPP;
+       default:
+               return -EOPNOTSUPP;
+       }
+}
+#endif /* !USE_HOSTCC */
+
 int image_decomp(int comp, ulong load, ulong image_start, int type,
                 void *load_buf, void *image_buf, ulong image_len,
                 uint unc_len, ulong *load_end)
diff --git a/include/image.h b/include/image.h
index 4149ebbcce9..5d590916208 100644
--- a/include/image.h
+++ b/include/image.h
@@ -1092,6 +1092,31 @@ int image_decomp(int comp, ulong load, ulong 
image_start, int type,
                 void *load_buf, void *image_buf, ulong image_len,
                 uint unc_len, ulong *load_end);
 
+/**
+ * image_decomp_get_uncompressed_size() - Read the uncompressed size from a
+ *                                        compressed stream's header
+ *
+ * Peeks at a compressed image and returns the uncompressed size where the
+ * format carries one: gzip ISIZE, lzma header uncompressed size, lz4 frame
+ * Content_Size (only when the FLG bit is set), zstd Frame_Content_Size. The
+ * value is attacker-controlled, so callers must sanity-check against an
+ * upper bound before using it as an allocation size.
+ *
+ * gzip's ISIZE is the original size modulo 2^32, so this API is only useful
+ * for images up to 4 GiB. That is more than enough for a kernel_noload
+ * decompression hint.
+ *
+ * @comp:      Compression type (IH_COMP_...)
+ * @src:       Compressed data
+ * @len:       Length of @src
+ * @sizep:     Set to the uncompressed size on success
+ * Return: 0 on success, -EOPNOTSUPP if @comp does not carry an uncompressed
+ *         size (or is not enabled in this build), -EINVAL on a malformed or
+ *         truncated header
+ */
+int image_decomp_get_uncompressed_size(int comp, const void *src, ulong len,
+                                      ulong *sizep);
+
 /**
  * Set up properties in the FDT
  *
-- 
2.43.0

Reply via email to