[RFC PATCH] initramfs: Add size validation to prevent tmpfs exhaustion

From: Stephen Eta Zhou
Date: Fri Mar 14 2025 - 01:05:09 EST


From 3499daeb5caf934f08a485027b5411f9ef82d6be Mon Sep 17 00:00:00 2001
From: Stephen Eta Zhou <stephen.eta.zhou@xxxxxxxxxxx>
Date: Fri, 14 Mar 2025 12:32:59 +0800
Subject: [PATCH] initramfs: Add size validation to prevent tmpfs exhaustion

When initramfs is loaded into a small memory environment, if its size
exceeds the tmpfs max blocks limit, the loading will fail. Additionally,
if the required blocks are close to the tmpfs max blocks boundary,
subsequent drivers or subsystems using tmpfs may fail to initialize.

To prevent this, the size limit is set to half of tmpfs max blocks.
This ensures that initramfs can complete its mission without exhausting
tmpfs resources, as user-space programs may also rely on tmpfs after boot.

This patch adds a validation mechanism to check the decompressed size
of initramfs based on its compression type and ratio. If the required
blocks exceed half of the tmpfs max blocks limit, the loading will be
aborted with an appropriate error message, exposing the issue early
and preventing further escalation.

Signed-off-by: Stephen Eta Zhou <stephen.eta.zhou@xxxxxxxxxxx>
---
 init/initramfs.c | 162 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 162 insertions(+)

diff --git a/init/initramfs.c b/init/initramfs.c
index b2f7583bb1f5..dadda0a42b48 100644
--- a/init/initramfs.c
+++ b/init/initramfs.c
@@ -497,6 +497,157 @@ static unsigned long my_inptr __initdata; /* index of next byte to be processed
 
 #include <linux/decompress/generic.h>
 
+#ifdef CONFIG_TMPFS
+/*
+ * struct compress_info - Describes a compression method.
+ *
+ * @magic: Magic numbers to identify the compression method (e.g., GZIP, XZ, etc.).
+ *         Each magic number is a byte array of maximum length 256.
+ *         The first dimension (2) represents the number of possible magic numbers.
+ * @rate: Compression ratio, calculated as R = (compressed size / original size) * 100.
+ *        The value is in percentage (0-100).
+ * @mark: Name of the compression scheme (e.g., "GZIP", "XZ").
+ * @len: Length of each magic byte array. Used for comparison with memcmp.
+ *       The first dimension (2) corresponds to the number of magic numbers.
+ * @magic_max: Maximum number of magic numbers supported (used when multiple magics are possible).
+ */
+struct compress_info {
+     unsigned char magic[2][256];
+     unsigned long rate;
+     char *mark;
+     size_t len[2];
+     size_t magic_max;
+};
+
+static struct compress_info cfm[] __initdata = {
+     {
+           .mark = "Gzip",
+           .magic = { { 0x1F, 0x8B } },
+           .len = { 2 },
+           .rate = 43,
+           .magic_max = 1,
+     },
+     {
+           .mark = "Bzip2",
+           .magic = { { 0x42, 0x5A, 0x68 } },
+           .len = { 3 },
+           .rate = 22,
+           .magic_max = 1,
+     },
+     {
+           .mark = "LZMA",
+           .magic = { { 0x5D, 0x00, 0x00 }, { 0xFF, 0x5D, 0x00 } },
+           .len = { 3, 3 },
+           .rate = 5,
+           .magic_max = 2,
+     },
+     {
+           .mark = "XZ",
+           .magic = { { 0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00 } },
+           .len = { 6 },
+           .rate = 7,
+           .magic_max = 1,
+     },
+     {
+           .mark = "LZO",
+           .magic = { { 0x89, 0x4C, 0x5A, 0x4F, 0x00, 0x0D, 0x0A, 0x1A, 0x0A } },
+           .len = { 9 },
+           .rate = 47,
+           .magic_max = 1,
+     },
+     {
+           .mark = "LZ4",
+           .magic = {
+                             { 0x04, 0x22, 0x4D, 0x18 },
+                             { 0x02, 0x21, 0x4C, 0x18 }
+                        },
+           .len = { 4 },
+           .rate = 52,
+           .magic_max = 2,
+     },
+     {
+           .mark = "ZSTD",
+           .magic = { { 0x28, 0xB5, 0x2F, 0xFD } },
+           .len = { 4 },
+           .rate = 7,
+           .magic_max = 1,
+     },
+     {
+           .mark = "None",
+           .magic = {
+                             { 0x30, 0x37, 0x30, 0x37, 0x30, 0x31 },
+                             { 0x30, 0x37, 0x30, 0x37, 0x30, 0x32 }
+                        },
+           .len = { 6, 6 },
+           .rate = 0,
+           .magic_max = 2,
+     },
+};
+
+static int __init validate_rootfs_size(char *buf, unsigned long len)
+{
+     unsigned long i, j, result, quotient, half_tmpfs_blocks;
+
+     /*
+      * Calculate how many blocks are needed to decompress
+      * and check if they are within a reasonable range.
+      */
+     for (i = 0; i < ARRAY_SIZE(cfm); ++i) {
+           for (j = 0; j < cfm[i].magic_max; ++j) {
+                 if (memcmp(buf, cfm[i].magic[j], cfm[i].len[j]) == 0) {
+                       pr_debug("Compression method: %\n", cfm[i].mark);
+                       /*
+                        * The calculation is divided into three steps:
+                        * 1. Calculate the decompressed size based on the ratio.
+                        * 2. Check for potential overflow risks and ensure that
+                        *    the temporary decompressed
+                        *    initramfs does not exceed the maximum range of 2^(32/64),
+                        *    ensuring that the initramfs size does not approach the
+                        *    memory addressing limit (this cannot be fully guaranteed).
+                        * 3. Determine whether the required page size exceeds 1/4 of
+                        *    the total memory pages, restricting it from using excessively
+                        *    large amounts of memory pages.
+                        *
+                        * Note1: Here, `len` cannot be directly multiplied by 100,
+                        *        as it may cause overflow.
+                        *        Dividing by `rate` first and then multiplying by 100
+                        *        can effectively reduce the risk of overflow.
+                        *
+                        * Note2: Due to integer division and rounding,
+                        *        the calculated size may deviate by a few MB.
+                        */
+                       quotient = len / cfm[i].rate;
+
+                       if (quotient > ULONG_MAX / 100)
+                             goto err_overflow;
+                       else
+                             result = (quotient * 100) / PAGE_SIZE;
+
+                       /*
+                        * totalram_pages() / 2 = tmpfs max blocks
+                        */
+                       half_tmpfs_blocks = (totalram_pages() / 2) / 2;
+                       if (result > half_tmpfs_blocks)
+                             goto err_nomem;
+
+                       return 0;
+                 }
+           }
+     }
+
+     pr_err("This compression format is not supported.\n");
+     return -EOPNOTSUPP;
+
+err_overflow:
+     pr_err("Decompressed size overflow!\n");
+     return -ERANGE;
+err_nomem:
+     pr_err("Decompressed size exceeds tmpfs max blocks limit!\n");
+     return -ENOMEM;
+
+}
+#endif
+
 static char * __init unpack_to_rootfs(char *buf, unsigned long len)
 {
      long written;
@@ -504,6 +655,17 @@ static char * __init unpack_to_rootfs(char *buf, unsigned long len)
      const char *compress_name;
      static __initdata char msg_buf[64];
 
+#ifdef CONFIG_TMPFS
+     int ret = validate_rootfs_size(buf, len);
+
+     if (ret) {
+           snprintf(msg_buf, sizeof(msg_buf),
+                       "Rootfs does not comply with the rules, error code: %d", ret);
+           message = msg_buf;
+           return message;
+     }
+#endif
+
      header_buf = kmalloc(110, GFP_KERNEL);
      symlink_buf = kmalloc(PATH_MAX + N_ALIGN(PATH_MAX) + 1, GFP_KERNEL);
      name_buf = kmalloc(N_ALIGN(PATH_MAX), GFP_KERNEL);
-- 
2.25.1