From: Bradley Morgan <bradley.morgan@linux.dev>
To: Paul Menzel <pmenzel@molgen.mpg.de>
Cc: atomlin@atomlin.com, da.gomez@kernel.org, dsterba@suse.com,
linux-kbuild@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-modules@vger.kernel.org, mcgrof@kernel.org,
nathan@kernel.org, nsc@kernel.org, petr.pavlu@suse.com,
samitolvanen@google.com, terrelln@fb.com
Subject: Re: [PATCH v2] module: Add LZ4 module compression
Date: Sun, 15 Nov 2026 21:16:58 +0000 [thread overview]
Message-ID: <5AB07CE7-6CB0-4CBE-A61F-7C444C9DF4E9@linux.dev> (raw)
In-Reply-To: <74f6ca7a-6171-4d21-b382-49dc4c4753e7@molgen.mpg.de>
On 15 November 2026 21:09:09 GMT, Paul Menzel <pmenzel@molgen.mpg.de>
wrote:
>Dear Bradley,
>
>
>Thank you for commenting on v2.
>
>
>Am 15.11.26 um 18:35 schrieb Bradley Morgan:
>> On 15 November 2026 12:04:13 GMT, Paul Menzel wrote:
>>> XZ modules are small but slow to decompress in the kernel. Therefore,
>>> support modules compressed in the LZ4 frame format (.ko.lz4), as
>written
>>> by lz4(1). Decode block by block into a scratch buffer and copy into
>the
>>> module pages. Linked blocks keep the last 64 KiB of output in front of
>>> the scratch buffer as the dictionary. Verify the header checksum, block
>>> checksums, content checksum and content size when the frame has them.
>>> Frames with a dictionary ID are rejected.
>>
>> .ko.lz4? when would this be used..?
>
>With the first sentence and the table below, it would be used, if fast
>boot times are more important than space.
>
>>> modules_install uses `lz4 -12 -B4 -BD`: 64 KiB linked blocks are as
>>> small as 4 MiB blocks (i915.ko: 3587140 vs 3585364 bytes) and keep the
>>> scratch buffer at 128 KiB.
>>>
>>> Userspace needs kmod with LZ4 support for depmod and modprobe [1].
>>>
>>> On a Dell XPS 13 9310 with an Intel Core i7-1165G7 (Tiger Lake) and an
>>> NVMe SSD, decompressing and checking the signatures of the 237 modules
>>> loaded during boot takes 624 ms of CPU time with XZ (i915: 91 ms) and
>>> 131 ms with LZ4 (i915: 12 ms), measured by calling finit_module() again
>>> for each loaded module. The LZ4 modules take 20.3 MB instead of 11.0
>MB.
>>> Both kernels are v7.4-rc1 with the same patches, including pending
>>> lib/xz decoder speed-ups. Ten alternating boots of each kernel:
>>>
>>> | ms (median) | XZ | LZ4 | change |
>>> +-------------------------------------------+------+------+--------+
>>> | early-i915 (modprobe i915 + dependencies) | 295 | 90 | -205 |
>>> | first i915 message | 738 | 547 | -192 |
>>> | GDM autologin | 973 | 876 | -96 |
>>> | GRUB start to autologin | 1292 | 1197 | -94 |
>>>
>>> QEMU 11.1.2 (KVM, AMD EPYC 7763, 8 vCPUs, module.sig_enforce=1, 10
>>> rounds), loading the same 237 modules in dependency order:
>>>
>>> | median | XZ | LZ4 | uncompressed |
>>> +--------------------------+---------+---------+--------------+
>>> | one at a time | 2082 ms | 1499 ms | 1461 ms |
>>> | 8 in parallel, like udev | 619 ms | 573 ms | 593 ms |
>>
>> This table doesn't look very nicely formatted for me.
>
>“nicely” is quite subjective, so it’d be great if you suggested your
>preferred format.
It looks very mangled to me.
>
>>> Link: https://github.com/kmod-project/kmod/pull/505 [1]
>>> Signed-off-by: Paul Menzel <pmenzel@molgen.mpg.de>
>>> ---
>>>
>>> Notes:
>>> Changes in v2:
>>> - Declare retval as ssize_t, so output of 2 GiB or more is not
>>> truncated (Sashiko). The zstd decoder has the same bug, fixed by
>>> the separate patch "module/decompress: Use ssize_t for the zstd
>>> decompressed size".
>>> - Reject empty frames with -EINVAL; v1 failed vmap() of 0 pages
>with
>>> -ENOMEM (Sashiko).
>>> - Describe the decoder and link the frame format specification,
>group
>>> the definitions by FLG byte, BD byte and block size field,
>compare the
>>> version with FIELD_GET(), rename LZ4F_DICT_SIZE to
>LZ4F_WINDOW_SIZE,
>>> look up the block maximum size in a table, shorten the dictionary
>>> comment (Bradley Morgan).
>>> v1:
>https://lore.kernel.org/all/20261113144233.193782-1-pmenzel@molgen.mpg.de/
>>> Claude Opus 5.5 (Claude Code) wrote the decoder, the v2
>>> changes and this message. Tests: W=1 compile (GCC 16.2, Debian
>config
>>> for x86_64, tinyconfig for i386); QEMU with module.sig_enforce=1:
>11 LZ4
>>> frame variants (64 KiB-4 MiB, linked/independent, block/content
>>> checksums, content size, stored blocks) load, 9 corrupt, truncated
>or
>>> empty files are rejected with EINVAL or EKEYREJECTED (v1: ENOMEM
>for the
>>> empty ones); the 237-module boot set loads in QEMU. v1 also loaded
>on a
>>> Dell XPS 13 9310 (20 boots); v2 was not booted on hardware.
>>
>> Could you do a favour and test patches you submit, just in case of the
>> edgecases :)
>
>v1 was tested on real hardware, and for the v2 changes, I deem the QEMU
>tests enough.
>
>>> kernel/module/Kconfig | 13 +-
>>> kernel/module/decompress.c | 243 +++++++++++++++++++++++++++++++++++++
>>> scripts/Makefile.modinst | 6 +
>>> 3 files changed, 261 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/kernel/module/Kconfig b/kernel/module/Kconfig
>>> index 43b1bb01fd273..36f12612c74dd 100644
>>> --- a/kernel/module/Kconfig
>>> +++ b/kernel/module/Kconfig
>>> @@ -344,7 +344,7 @@ config MODULE_COMPRESS
>>> This is fully compatible with signed modules.
>>>
>>> The tool used to work with modules needs to support the selected
>>> - compression type. kmod MAY support gzip, xz and zstd. Other tools
>>> + compression type. kmod MAY support gzip, xz, zstd and lz4. Other tools
>>> might have a limited selection of the supported types.
>>>
>>> Note that for modules inside an initrd or initramfs, it's more
>>> @@ -377,6 +377,15 @@ config MODULE_COMPRESS_ZSTD
>>> Support modules compressed with ZSTD. The installed modules are
>>> suffixed with .ko.zst.
>>>
>>> +config MODULE_COMPRESS_LZ4
>>> + bool "LZ4"
>>> + help
>>> + Support modules compressed with LZ4. The installed modules are
>>> + suffixed with .ko.lz4.
>>> +
>>> + LZ4 modules are larger than XZ or ZSTD ones but decompress
>>> + several times faster.
>>> +
>>> endchoice
>>>
>>> config MODULE_COMPRESS_ALL
>>> @@ -394,6 +403,8 @@ config MODULE_DECOMPRESS
>>> select ZLIB_INFLATE if MODULE_COMPRESS_GZIP
>>> select XZ_DEC if MODULE_COMPRESS_XZ
>>> select ZSTD_DECOMPRESS if MODULE_COMPRESS_ZSTD
>>> + select LZ4_DECOMPRESS if MODULE_COMPRESS_LZ4
>>> + select XXHASH if MODULE_COMPRESS_LZ4
>>> help
>>> Support for decompressing kernel modules by the kernel itself
>>> instead of relying on userspace to perform this task. Useful when
>>> diff --git a/kernel/module/decompress.c b/kernel/module/decompress.c
>>> index cce098671be97..028b411015f21 100644
>>> --- a/kernel/module/decompress.c
>>> +++ b/kernel/module/decompress.c
>>> @@ -287,6 +287,249 @@ static ssize_t module_zstd_decompress(struct
>load_info *info,
>>> kvfree(wksp);
>>> return retval;
>>> }
>>> +#elif defined(CONFIG_MODULE_COMPRESS_LZ4)
>>> +#include <linux/bitfield.h>
>>> +#include <linux/lz4.h>
>>> +#include <linux/sizes.h>
>>> +#include <linux/unaligned.h>
>>> +#include <linux/xxhash.h>
>>> +#define MODULE_COMPRESSION lz4
>>> +#define MODULE_DECOMPRESS_FN module_lz4_decompress
>>> +
>>
>> Oh these includes mid file are a MESS! No, please no. Good to if
>defined,
>> but I'm sure the maintainer wouldn't mind if you ifdefed those includes
>on
>> the TOP of the file. Like any other normal kernel file.
>
>It’s done the same way for the other variants, so I keep it.
>
>Why so many includes? Unlike zstd, xz and zlib, lib/lz4 has only the block
>API (LZ4_decompress_safe*()), no frame decoder, so this parses the LZ4
>frame format (v1.6.x) itself: frame descriptor, block sizes and the xxh32
>header, block and content checksums. I am going to add that to the commit
>message of v3.
>
>>> +/*
>>> + * Decoder for the LZ4 frame format written by lz4(1), see
>>> + * https://github.com/lz4/lz4/blob/dev/doc/lz4_Frame_format.md
>>> + *
>>> + * Each block is decoded into a scratch buffer and copied into the
>module
>>> + * pages. A linked block may reference up to 64 KiB of the output
>before
>>> + * it, so that much output is kept in front of the scratch buffer.
>>> + */
>>
>> Well, I suggested something better,
>>
>> /* Decoder for the lz4 frame format,
>> * written by lz4 [1]
>> * This code adds support for lz4
>> * modules inside the kernel, so
>> * that lz4 modules can be used.
>> *
>> * [1] - (your link, lz4 frame format src??)
>> */
>>
>> What about this?
>>
>> I say bikeshed, but let's not make it TOO sloppy, you reckon?
>
>No, I don’t. The other variants don’t have such a generic comment. So, I’d
>keep.
fine.
>
>>> +#define LZ4F_MAGIC 0x184d2204
>>> +
>>> +/* Frame descriptor, FLG byte */
>>> +#define LZ4F_FLG_VERSION GENMASK(7, 6)
>>> +#define LZ4F_FLG_BLOCK_INDEP BIT(5)
>>> +#define LZ4F_FLG_BLOCK_CSUM BIT(4)
>>> +#define LZ4F_FLG_CONTENT_SIZE BIT(3)
>>> +#define LZ4F_FLG_CONTENT_CSUM BIT(2)
>>> +#define LZ4F_FLG_RESERVED BIT(1)
>>> +#define LZ4F_FLG_DICT_ID BIT(0)
>>> +
>>> +/* Frame descriptor, BD byte */
>>> +#define LZ4F_BD_BLOCK_MAXSIZE GENMASK(6, 4)
>>> +#define LZ4F_BD_RESERVED (BIT(7) | GENMASK(3, 0))
>>> +
>>> +/* Block size field: the top bit marks a block stored uncompressed */
>>> +#define LZ4F_BLOCK_UNCOMPRESSED BIT(31)
>>> +
>>> +#define LZ4F_WINDOW_SIZE SZ_64K
>>> +
>>> +/* Block maximum sizes for the BD byte's Block MaxSize values 4 to 7
>*/
>>> +static const u32 lz4f_block_max[] = { SZ_64K, SZ_256K, SZ_1M, SZ_4M };
>>> +
>>> +static int module_lz4_copy_out(struct load_info *info, const u8 *src,
>>> + size_t len, size_t *total)
>>> +{
>>> + while (len) {
>>> + size_t offset = *total % PAGE_SIZE;
>>> + size_t chunk = min_t(size_t, len, PAGE_SIZE - offset);
>>> + struct page *page;
>>> +
>>> + if (offset)
>>> + page = info->pages[info->used_pages - 1];
>>> + else
>>> + page = module_get_next_page(info);
>>> + if (IS_ERR(page))
>>> + return PTR_ERR(page);
>>> +
>>> + memcpy_to_page(page, offset, src, chunk);
>>> + src += chunk;
>>> + len -= chunk;
>>> + *total += chunk;
>>> + }
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static int module_lz4_verify_content(struct load_info *info, size_t
>total,
>>> + u32 expected)
>>> +{
>>> + void *data;
>>> + u32 csum;
>>> +
>>> + data = vmap(info->pages, info->used_pages, VM_MAP, PAGE_KERNEL_RO);
>>> + if (!data)
>>> + return -ENOMEM;
>>> + csum = xxh32(data, total, 0);
>>> + vunmap(data);
>>> +
>>> + if (csum != expected) {
>>> + pr_err("LZ4-compressed data is corrupt (content checksum)\n");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static ssize_t module_lz4_decompress(struct load_info *info,
>>> + const void *buf, size_t size)
>>> +{
>>> + const u8 *in = buf, *end = in + size, *desc;
>>> + size_t block_max, total = 0, dict_len = 0;
>>> + u64 content_size = 0;
>>> + u8 *scratch, *out;
>>> + bool linked;
>>> + u8 flg, bd;
>>> + ssize_t retval;
>>> +
>>> + if (size < 7 || get_unaligned_le32(in) != LZ4F_MAGIC) {
>>> + pr_err("not an lz4 compressed module\n");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + desc = in + 4;
>>> + flg = desc[0];
>>> + bd = desc[1];
>>> + in = desc + 2;
>>> + if (FIELD_GET(LZ4F_FLG_VERSION, flg) != 1 ||
>>> + (flg & LZ4F_FLG_RESERVED) || (bd & LZ4F_BD_RESERVED) ||
>>> + FIELD_GET(LZ4F_BD_BLOCK_MAXSIZE, bd) < 4) {
>>> + pr_err("LZ4-compressed data has an unsupported frame descriptor\n");
>>
>> When would this be reached?
>
>This would only be reached for input lz4/liblz4 never write. The frame
>format specification requires a decoder to reject. The Block MaxSize part
>also guards the lz4f_block_max[] index below, and as decompression runs
>before the signature check, the header has to be validated here. I'll
>split the bounds check out with its own comment in v3 to make this
>clearer.
Thx
>
>>> + return -EINVAL;
>>> + }
>>> + if (flg & LZ4F_FLG_DICT_ID) {
>>> + pr_err("LZ4-compressed data needs a dictionary\n");
>>> + return -EINVAL;
>>> + }
>>> + if (flg & LZ4F_FLG_CONTENT_SIZE) {
>>> + if (end - in < 8)
>>> + goto truncated_header;
>>> + content_size = get_unaligned_le64(in);
>>> + in += 8;
>>> + }
>>> + if (end - in < 1)
>>> + goto truncated_header;
>>> + if (*in != ((xxh32(desc, in - desc, 0) >> 8) & 0xff)) {
>>> + pr_err("LZ4-compressed data has a corrupt frame descriptor\n");
>>
>> Is there a mechanism in the future in order to understand the garbled
>crap
>> and fix it to make it manageable? (This can be future patch)
>
>No mechanism is planned.
Ok.
>
>>> + return -EINVAL;
>>> + }
>>> + in++;
>>> +
>>> + block_max = lz4f_block_max[FIELD_GET(LZ4F_BD_BLOCK_MAXSIZE, bd) - 4];
>>> + linked = !(flg & LZ4F_FLG_BLOCK_INDEP);
>>> +
>>> + scratch = kvmalloc((linked ? LZ4F_WINDOW_SIZE : 0) + block_max,
>>> + GFP_KERNEL);
>>> + if (!scratch)
>>> + return -ENOMEM;
>>> + out = scratch + (linked ? LZ4F_WINDOW_SIZE : 0);
>>> +
>>> + for (;;) {
>>> + u32 block_size;
>>> + size_t len;
>>> + int ret;
>>> +
>>> + if (end - in < 4)
>>> + goto truncated;
>>> + block_size = get_unaligned_le32(in);
>>> + in += 4;
>>> + if (!block_size)
>>> + break;
>>> +
>>> + len = block_size & ~LZ4F_BLOCK_UNCOMPRESSED;
>>> + if (len > block_max) {
>>> + pr_err("LZ4-compressed data has an oversized block\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> + if ((size_t)(end - in) < len +
>>> + (flg & LZ4F_FLG_BLOCK_CSUM ? 4 : 0))
>>> + goto truncated;
>>> +
>>> + if ((flg & LZ4F_FLG_BLOCK_CSUM) &&
>>> + xxh32(in, len, 0) != get_unaligned_le32(in + len)) {
>>> + pr_err("LZ4-compressed data is corrupt (block checksum)\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> +
>>> + if (block_size & LZ4F_BLOCK_UNCOMPRESSED) {
>>> + memcpy(out, in, len);
>>> + ret = len;
>>> + } else if (linked) {
>>> + ret = LZ4_decompress_safe_usingDict(in, out, len,
>>> + block_max,
>>> + out - dict_len,
>>> + dict_len);
>>> + } else {
>>> + ret = LZ4_decompress_safe(in, out, len, block_max);
>>> + }
>>> + if (ret < 0) {
>>> + pr_err("LZ4-decompression failed\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> +
>>> + retval = module_lz4_copy_out(info, out, ret, &total);
>>> + if (retval)
>>> + goto out;
>>> +
>>> + if (linked) {
>>> + /* Keep the last 64 KiB as the next block's dictionary. */
>>> + size_t keep = min_t(size_t, dict_len + ret,
>>> + LZ4F_WINDOW_SIZE);
>>> +
>>> + memmove(out - keep, out + ret - keep, keep);
>>> + dict_len = keep;
>>> + }
>>> +
>>> + in += len;
>>> + if (flg & LZ4F_FLG_BLOCK_CSUM)
>>> + in += 4;
>>> + }
>>> +
>>> + if (!total) {
>>> + pr_err("LZ4-compressed module is empty\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> +
>>> + if (flg & LZ4F_FLG_CONTENT_CSUM) {
>>> + if (end - in < 4)
>>> + goto truncated;
>>> + retval = module_lz4_verify_content(info, total,
>>> + get_unaligned_le32(in));
>>> + if (retval)
>>> + goto out;
>>> + in += 4;
>>> + }
>>> +
>>> + if (in != end) {
>>> + pr_err("LZ4-compressed module has trailing data\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> +
>>> + if ((flg & LZ4F_FLG_CONTENT_SIZE) && content_size != total) {
>>> + pr_err("LZ4-compressed data has a wrong content size\n");
>>> + retval = -EINVAL;
>>> + goto out;
>>> + }
>>> +
>>> + retval = total;
>>> + goto out;
>>> +
>>> +truncated:
>>> + pr_err("LZ4-compressed data is truncated\n");
>>> + retval = -EINVAL;
>>> +out:
>>> + kvfree(scratch);
>>> + return retval;
>>> +
>>> +truncated_header:
>>> + pr_err("LZ4-compressed data is truncated\n");
>>> + return -EINVAL;
>>> +}
>>> #else
>>> #error "Unexpected configuration for CONFIG_MODULE_DECOMPRESS"
>>> #endif
>>
>> My concern here is that some of these gnarly functions are uncommented,
>so
>> when a noob reads this code, "Oh no! What does this do! Oops... I broke
>my
>> module"
>>
>> Maybe comment for functions that are gnarly and you think a reader would
>> need to understand to be able to operate/contribute.
>
>I think the decompressor is straight forward, and the other compression
>variants don’t do either, so I won’t add comments unless the maintainers
>ask me to.
>
>>> diff --git a/scripts/Makefile.modinst b/scripts/Makefile.modinst
>>> index 9ba45e5b32b18..aebc5e89d0e75 100644
>>> --- a/scripts/Makefile.modinst
>>> +++ b/scripts/Makefile.modinst
>>> @@ -57,6 +57,7 @@ ifdef CONFIG_MODULE_COMPRESS_ALL
>>> suffix-$(CONFIG_MODULE_COMPRESS_GZIP) := .gz
>>> suffix-$(CONFIG_MODULE_COMPRESS_XZ) := .xz
>>> suffix-$(CONFIG_MODULE_COMPRESS_ZSTD) := .zst
>>> +suffix-$(CONFIG_MODULE_COMPRESS_LZ4) := .lz4
>>> endif
>>>
>>> modules := $(patsubst %.o, $(dst)/%.ko$(suffix-y), $(modules))
>>> @@ -151,6 +152,8 @@ quiet_cmd_xz = XZ $@
>>> cmd_xz = $(XZ) --check=crc32 --lzma2=dict=1MiB -f $<
>>> quiet_cmd_zstd = ZSTD $@
>>> cmd_zstd = $(ZSTD) --rm -f -q $<
>>> +quiet_cmd_lz4 = LZ4 $@
>>> + cmd_lz4 = $(LZ4) -12 -B4 -BD --rm -f -q $< $@
>>>
>>> $(dst)/%.ko.gz: $(dst)/%.ko FORCE
>>> $(call cmd,gzip)
>>> @@ -161,6 +164,9 @@ $(dst)/%.ko.xz: $(dst)/%.ko FORCE
>>> $(dst)/%.ko.zst: $(dst)/%.ko FORCE
>>> $(call cmd,zstd)
>>>
>>> +$(dst)/%.ko.lz4: $(dst)/%.ko FORCE
>>> + $(call cmd,lz4)
>>> +
>>> PHONY += FORCE
>>> FORCE:
>
>Thanks,
>
>Paul
>
>
>PS: Just a wish, that you’d use a more motivating commenting style. Still: Thank you for taking time to review patches.
Wdym?
>
--- Thanks!
prev parent reply other threads:[~2026-11-15 21:17 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-15 12:04 Paul Menzel
2026-11-15 17:35 ` Bradley Morgan
2026-11-15 21:09 ` Paul Menzel
2026-11-15 21:16 ` Bradley Morgan [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=5AB07CE7-6CB0-4CBE-A61F-7C444C9DF4E9@linux.dev \
--to=bradley.morgan@linux.dev \
--cc=atomlin@atomlin.com \
--cc=da.gomez@kernel.org \
--cc=dsterba@suse.com \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-modules@vger.kernel.org \
--cc=mcgrof@kernel.org \
--cc=nathan@kernel.org \
--cc=nsc@kernel.org \
--cc=petr.pavlu@suse.com \
--cc=pmenzel@molgen.mpg.de \
--cc=samitolvanen@google.com \
--cc=terrelln@fb.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®