Re: [PATCH v4 15/17] module: Introduce hash-based integrity checking
From: Thomas Weißschuh <linux@weissschuh.net>
Date: 2026-02-23 21:43:33
Also in:
linux-arch, linux-doc, linux-integrity, linux-kbuild, linux-modules, linux-security-module, lkml
On 2026-02-23 19:41:52+0100, Nicolas Schier wrote:
On Mon, Feb 23, 2026 at 08:53:29AM +0100, Thomas Weißschuh wrote:quoted
On 2026-02-21 22:38:29+0100, Nicolas Schier wrote:quoted
On Tue, Jan 13, 2026 at 01:28:59PM +0100, Thomas Weißschuh wrote:quoted
The current signature-based module integrity checking has some drawbacks in combination with reproducible builds. Either the module signing key is generated at build time, which makes the build unreproducible, or a static signing key is used, which precludes rebuilds by third parties and makes the whole build and packaging process much more complicated. The goal is to reach bit-for-bit reproducibility. Excluding certain parts of the build output from the reproducibility analysis would be error-prone and force each downstream consumer to introduce new tooling. Introduce a new mechanism to ensure only well-known modules are loaded by embedding a merkle tree root of all modules built as part of the full kernel build into vmlinux. Non-builtin modules can be validated as before through signatures. Normally the .ko module files depend on a fully built vmlinux to be available for modpost validation and BTF generation. With CONFIG_MODULE_HASHES, vmlinux now depends on the modules to build a merkle tree. This introduces a dependency cycle which is impossible to satisfy. Work around this by building the modules during link-vmlinux.sh, after vmlinux is complete enough for modpost and BTF but before the final module hashes are The PKCS7 format which is used for regular module signatures can not represent Merkle proofs, so a new kind of module signature is introduced. As this signature type is only ever used for builtin modules, no compatibility issues can arise. Signed-off-by: Thomas Weißschuh <linux@weissschuh.net> --- .gitignore | 1 + Documentation/kbuild/reproducible-builds.rst | 5 +- Makefile | 8 +- include/asm-generic/vmlinux.lds.h | 11 + include/linux/module_hashes.h | 25 ++ include/linux/module_signature.h | 1 + kernel/module/Kconfig | 21 +- kernel/module/Makefile | 1 + kernel/module/hashes.c | 92 ++++++ kernel/module/hashes_root.c | 6 + kernel/module/internal.h | 1 + kernel/module/main.c | 4 +- scripts/.gitignore | 1 + scripts/Makefile | 3 + scripts/Makefile.modfinal | 11 + scripts/Makefile.modinst | 13 + scripts/Makefile.vmlinux | 5 + scripts/link-vmlinux.sh | 14 +- scripts/modules-merkle-tree.c | 467 +++++++++++++++++++++++++++ security/lockdown/Kconfig | 2 +- 20 files changed, 685 insertions(+), 7 deletions(-)[...]quoted
diff --git a/kernel/module/hashes_root.c b/kernel/module/hashes_root.c new file mode 100644 index 000000000000..1abfcd3aa679 --- /dev/null +++ b/kernel/module/hashes_root.c@@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <linux/module_hashes.h> + +/* Blank dummy data. Will be overridden by link-vmlinux.sh */ +const struct module_hashes_root module_hashes_root __module_hashes_section = {};diff --git a/kernel/module/internal.h b/kernel/module/internal.h index e2d49122c2a1..e22837d3ac76 100644 --- a/kernel/module/internal.h +++ b/kernel/module/internal.h@@ -338,6 +338,7 @@ void module_mark_ro_after_init(const Elf_Ehdr *hdr, Elf_Shdr *sechdrs, const char *secstrings); int module_sig_check(struct load_info *info, const u8 *sig, size_t sig_len); +int module_hash_check(struct load_info *info, const u8 *sig, size_t sig_len); #ifdef CONFIG_DEBUG_KMEMLEAK void kmemleak_load_module(const struct module *mod, const struct load_info *info);diff --git a/kernel/module/main.c b/kernel/module/main.c index 2a28a0ece809..fa30b6387936 100644 --- a/kernel/module/main.c +++ b/kernel/module/main.c@@ -3362,8 +3362,10 @@ static int module_integrity_check(struct load_info *info, int flags) if (IS_ENABLED(CONFIG_MODULE_SIG) && sig_type == PKEY_ID_PKCS7) { err = module_sig_check(info, sig, sig_len); + } else if (IS_ENABLED(CONFIG_MODULE_HASHES) && sig_type == PKEY_ID_MERKLE) { + err = module_hash_check(info, sig, sig_len); } else { - pr_err("module: not signed with expected PKCS#7 message\n"); + pr_err("module: not signed with signature mechanism\n"); err = -ENOPKG;To prevent others from running into the same issue: My first test got stuck here, as I tested with virtme-ng, which symlinks modules from build tree to /lib/modules/$(uname -r)/..., resulting in [ 15.956855] module: not signed with signature mechanism modprobe: ERROR: could not insert 'efivarfs': Package not installed As the modules_install step was missing, modules were not being signed.Currently the signing is deferred to installation time to keep in sync with regular module signing and to keep the logic simpler by not having to gracefully handle previously-signed files. But this could be changed.I did not want to suggest changing the behaviour, that would make things more complicated to prevent needless rebuilds. I just wanted to mention it here to prevent others from burning time.
Understood.
quoted
quoted
[...]quoted
diff --git a/scripts/modules-merkle-tree.c b/scripts/modules-merkle-tree.c new file mode 100644 index 000000000000..a6ec0e21213b --- /dev/null +++ b/scripts/modules-merkle-tree.c@@ -0,0 +1,467 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Compute hashes for modules files and build a merkle tree. + * + * Copyright (C) 2025 Sebastian Andrzej Siewior <sebastian@breakpoint.cc> + * Copyright (C) 2025 Thomas Weißschuh <linux@weissschuh.net> + * + */ +#define _GNU_SOURCE 1 +#include <arpa/inet.h> +#include <err.h> +#include <unistd.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <stdbool.h> +#include <stdlib.h> + +#include <sys/stat.h> +#include <sys/mman.h> + +#include <openssl/evp.h> +#include <openssl/err.h> + +#include "ssl-common.h" + +static int hash_size; +static EVP_MD_CTX *ctx; + +struct module_signature { + uint8_t algo; /* Public-key crypto algorithm [0] */ + uint8_t hash; /* Digest algorithm [0] */ + uint8_t id_type; /* Key identifier type [PKEY_ID_PKCS7] */ + uint8_t signer_len; /* Length of signer's name [0] */ + uint8_t key_id_len; /* Length of key identifier [0] */ + uint8_t __pad[3]; + uint32_t sig_len; /* Length of signature data */ +}; + +#define PKEY_ID_MERKLE 3 + +static const char magic_number[] = "~Module signature appended~\n";This here will be the forth definition of struct module_signature, increasing the risk of unwanted diversion. I second Petr's suggestion to reuse a _common_ definition instead.Ack.quoted
(Here, even include/linux/module_signature.h could be included itself.)I'd like to avoid including internal headers from other components. We could move it to an UAPI header. Various other subsystems use those for not-really-UAPI but still ABI definitions.Yeah, ack.
What exactly is the 'ack' for? * Avoiding to include internal headers? * Moving the definition to UAPI headers? (...)
quoted
quoted
Can you verify if I get the mechanics roughly correct? * Modules are merkle tree leaves. Modules are built and logically paired by the order from modules.order; a single left-over module is paired with itself. * Hashes of paired modules are hashed again (branch node hash); hashes of pairs of branch nodes' hashes are hashed again; repeat until we reach the single merkle tree root hash * The final merkle tree root hash (and the count of tree levels) is included in vmlinuxThe merkle tree code was written by Sebastian so he will have the best knowledge about it. But this is also my understanding.I'd like to see some (rough) description in Documentation or in a commit message at least, otherwise future me will have to ask that again.
Ack in general. I'd prefer to document it in a source code comment, though. That feels like the best fit to me. Thomas