* [PATCH v2] module: Extend module_blacklist parameter to built-in modules
@ 2026-06-22 14:02 Aaron Tomlin
2026-06-22 14:17 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Aaron Tomlin @ 2026-06-22 14:02 UTC (permalink / raw)
To: arnd, mcgrof, petr.pavlu, da.gomez, samitolvanen, peterz
Cc: mhiramat, atomlin, neelx, da.anzani, sean, chjohnst, steve,
mproche, nick.lane, linux-arch, linux-modules, linux-kernel
Currently, the module_blacklist= parameter only prevents
the dynamic loading of external modules. It possesses no mechanism to
intercept or prevent the initialisation of built-in modules, as their
associated initcalls are invoked unconditionally during system boot.
This patch extends the blacklisting behaviour to encompass built-in
modules. It introduces a dedicated ".initcall.modnames" section into
the linker script, systematically mapping each initcall to its
originating module name. During the boot sequence, do_one_initcall()
interrogates this mapping; should the executing initcall belong to a
blacklisted module, its execution is explicitly bypassed.
Furthermore, to preserve the efficacy of Kernel Address Space Layout
Randomisation (KASLR) and prevent binary bloat, the mapping mechanism
rigorously adheres to CONFIG_HAVE_ARCH_PREL32_RELOCATIONS. Rather than
storing absolute function pointers, it employs 32-bit relative offsets,
successfully avoiding the generation of thousands of absolute
relocations.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
--
Changes since v1:
- Pivoted entirely from exposing built-in initcalls and their blacklist
status via a debugfs interface to directly extending the existing
"module_blacklist=" to intercept built-in modules at boot (Petr Pavlu)
- Implemented 32-bit relative offsets (CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)
to store the mappings, preventing binary bloat and preserving KASLR
efficacy
- Linked to v1: https://lore.kernel.org/lkml/20260510061301.41341-1-atomlin@atomlin.com/
---
include/asm-generic/vmlinux.lds.h | 7 +++++++
include/linux/init.h | 31 +++++++++++++++++++++++++++++--
include/linux/module.h | 2 ++
init/main.c | 27 +++++++++++++++++++++++++++
kernel/module/main.c | 4 ++--
5 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index 5659f4b5a125..ac0e5f4f2893 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -957,6 +957,12 @@
#define CON_INITCALL \
BOUNDED_SECTION_POST_LABEL(.con_initcall.init, __con_initcall, _start, _end)
+#define INITCALL_MODNAMES \
+ . = ALIGN(8); \
+ __start_initcall_modnames = .; \
+ KEEP(*(.initcall.modnames)) \
+ __stop_initcall_modnames = .;
+
#define NAMED_SECTION(name) \
. = ALIGN(8); \
name : AT(ADDR(name) - LOAD_OFFSET) \
@@ -1166,6 +1172,7 @@
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
+ INITCALL_MODNAMES \
INIT_RAM_FS \
}
diff --git a/include/linux/init.h b/include/linux/init.h
index 40331923b9f4..212f64a07c73 100644
--- a/include/linux/init.h
+++ b/include/linux/init.h
@@ -125,6 +125,16 @@ static inline initcall_t initcall_from_entry(initcall_entry_t *entry)
}
#endif
+struct initcall_modname {
+#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
+ initcall_entry_t initcall_fn;
+ int modname_offset;
+#else
+ initcall_t initcall_fn;
+ const char *modname;
+#endif
+};
+
extern initcall_entry_t __con_initcall_start[], __con_initcall_end[];
/* Used for constructor calls. */
@@ -270,9 +280,26 @@ extern struct module __this_module;
__initcall_stub(fn, __iid, id), \
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))
-
+#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
+#define ___define_initcall(fn, id, __sec) \
+ __unique_initcall(fn, id, __sec, __initcall_id(fn)); \
+ asm(".pushsection \".initcall.modnames\", \"a\"\n" \
+ ".balign 4\n" \
+ ".long " #fn " - .\n" \
+ ".long __initcall_modstr_" #fn #id " - .\n" \
+ ".popsection\n" \
+ ".pushsection .init.rodata, \"a\"\n" \
+ "__initcall_modstr_" #fn #id ": .string \"" KBUILD_MODNAME "\"\n" \
+ ".popsection\n");
+#else
#define ___define_initcall(fn, id, __sec) \
- __unique_initcall(fn, id, __sec, __initcall_id(fn))
+ __unique_initcall(fn, id, __sec, __initcall_id(fn)); \
+ static struct initcall_modname __initcall_modname_##fn##id __used \
+ __section(".initcall.modnames") = { \
+ .initcall_fn = fn, \
+ .modname = KBUILD_MODNAME \
+ };
+#endif
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
diff --git a/include/linux/module.h b/include/linux/module.h
index 7566815fabbe..7e25fcd61b50 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -756,6 +756,8 @@ void *dereference_module_function_descriptor(struct module *mod, void *ptr);
int register_module_notifier(struct notifier_block *nb);
int unregister_module_notifier(struct notifier_block *nb);
+extern bool module_is_blacklisted(const char *module_name);
+
extern void print_modules(void);
static inline bool module_requested_async_probing(struct module *module)
diff --git a/init/main.c b/init/main.c
index e363232b428b..fbdc42859791 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1334,12 +1334,39 @@ static inline void do_trace_initcall_level(const char *level)
}
#endif /* !TRACEPOINTS_ENABLED */
+extern struct initcall_modname __start_initcall_modnames[];
+extern struct initcall_modname __stop_initcall_modnames[];
+
+static const char *initcall_get_modname(initcall_t fn)
+{
+ struct initcall_modname *p;
+
+ for (p = __start_initcall_modnames; p < __stop_initcall_modnames; p++) {
+ if (initcall_from_entry(&p->initcall_fn) == fn) {
+#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
+ return (const char *)offset_to_ptr(&p->modname_offset);
+#else
+ return p->modname;
+#endif
+ }
+ }
+ return NULL;
+}
+
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
char msgbuf[64];
+ const char *modname;
int ret;
+ modname = initcall_get_modname(fn);
+ if (modname && module_is_blacklisted(modname)) {
+ pr_info("Skipping initcall for blacklisted built-in module %s\n",
+ modname);
+ return 0;
+ }
+
if (initcall_blacklisted(fn))
return -EPERM;
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 46dd8d25a605..02deee5a7480 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2921,7 +2921,7 @@ int __weak module_frob_arch_sections(Elf_Ehdr *hdr,
/* module_blacklist is a comma-separated list of module names */
static char *module_blacklist;
-static bool blacklisted(const char *module_name)
+bool module_is_blacklisted(const char *module_name)
{
const char *p;
size_t len;
@@ -3391,7 +3391,7 @@ static int early_mod_check(struct load_info *info, int flags)
* Now that we know we have the correct module name, check
* if it's blacklisted.
*/
- if (blacklisted(info->name)) {
+ if (module_is_blacklisted(info->name)) {
pr_err("Module %s is blacklisted\n", info->name);
return -EPERM;
}
--
2.51.0
^ permalink raw reply related [flat|nested] 2+ messages in thread* Re: [PATCH v2] module: Extend module_blacklist parameter to built-in modules
2026-06-22 14:02 [PATCH v2] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
@ 2026-06-22 14:17 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-06-22 14:17 UTC (permalink / raw)
To: Aaron Tomlin; +Cc: linux-modules
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] Accessing the .initcall.modnames array during loadable module initialization reads freed .init memory, causing a Use-After-Free.
- [High] Built-in module blacklisting silently bypasses or fails to compile under CONFIG_LTO_CLANG / CONFIG_CFI_CLANG due to function pointer mismatch.
- [Low] Kernel build fails when CONFIG_MODULES is disabled due to missing module_is_blacklisted() definition.
--
commit 5c29fcaa43d56c5e3cb5426f55d85a536519732f
Author: Aaron Tomlin <atomlin@atomlin.com>
module: Extend module_blacklist parameter to built-in modules
This patch extends the module_blacklist parameter to intercept and prevent
the initialization of built-in modules during system boot. It creates a
.initcall.modnames section mapping initcalls to module names, using 32-bit
relative offsets to preserve KASLR.
Link: https://lore.kernel.org/lkml/20260510061301.41341-1-atomlin@atomlin.com/
> diff --git a/include/linux/init.h b/include/linux/init.h
> index 40331923b9f4a..212f64a07c739 100644
> --- a/include/linux/init.h
> +++ b/include/linux/init.h
[ ... ]
> @@ -270,9 +280,26 @@ extern struct module __this_module;
> __initcall_stub(fn, __iid, id), \
> __initcall_name(initcall, __iid, id), \
> __initcall_section(__sec, __iid))
> +#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
> +#define ___define_initcall(fn, id, __sec) \
> + __unique_initcall(fn, id, __sec, __initcall_id(fn)); \
> + asm(".pushsection \".initcall.modnames\", \"a\"\n" \
> + ".balign 4\n" \
> + ".long " #fn " - .\n" \
[Severity: High]
When compiled with CONFIG_LTO_CLANG or CONFIG_CFI_CLANG, the kernel wraps
initcalls in an __initcall_stub function, and stores the stub's address in
the initcall array.
Since ___define_initcall explicitly stores the original function's address
here, will initcall_get_modname() fail to match the runtime CFI/LTO stub
address passed by do_one_initcall()?
Also, could using #fn in inline assembly for a static function renamed by
LTO cause an undefined reference link error?
> + ".long __initcall_modstr_" #fn #id " - .\n" \
> + ".popsection\n" \
> + ".pushsection .init.rodata, \"a\"\n" \
> + "__initcall_modstr_" #fn #id ": .string \"" KBUILD_MODNAME "\"\n" \
> + ".popsection\n");
[ ... ]
> diff --git a/include/linux/module.h b/include/linux/module.h
> index d80c3ea574726..21668ef1430a5 100644
> --- a/include/linux/module.h
> +++ b/include/linux/module.h
> @@ -754,6 +754,8 @@ void *dereference_module_function_descriptor(struct module *mod, void *ptr);
> int register_module_notifier(struct notifier_block *nb);
> int unregister_module_notifier(struct notifier_block *nb);
>
> +extern bool module_is_blacklisted(const char *module_name);
[Severity: Low]
Does this cause a build failure when CONFIG_MODULES is disabled?
The function module_is_blacklisted() is defined in kernel/module/main.c,
but it is declared here unconditionally and called from do_one_initcall()
in init/main.c. If CONFIG_MODULES is disabled, could the linker fail with
an undefined reference?
> +
> extern void print_modules(void);
[ ... ]
> diff --git a/init/main.c b/init/main.c
> index b84818ad9685f..ce6d769b99e17 100644
> --- a/init/main.c
> +++ b/init/main.c
> @@ -1365,12 +1365,39 @@ static inline void do_trace_initcall_level(const char *level)
> }
> #endif /* !TRACEPOINTS_ENABLED */
>
> +extern struct initcall_modname __start_initcall_modnames[];
> +extern struct initcall_modname __stop_initcall_modnames[];
> +
> +static const char *initcall_get_modname(initcall_t fn)
> +{
> + struct initcall_modname *p;
> +
> + for (p = __start_initcall_modnames; p < __stop_initcall_modnames; p++) {
> + if (initcall_from_entry(&p->initcall_fn) == fn) {
> +#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
> + return (const char *)offset_to_ptr(&p->modname_offset);
> +#else
> + return p->modname;
> +#endif
> + }
> + }
> + return NULL;
> +}
> +
> int __init_or_module do_one_initcall(initcall_t fn)
> {
> int count = preempt_count();
> char msgbuf[64];
> + const char *modname;
> int ret;
>
> + modname = initcall_get_modname(fn);
[Severity: Critical]
Is there a use-after-free risk here when loading dynamic modules?
The .initcall.modnames section is placed in INIT_DATA_SECTION, mapping it to
.init.data. Memory in the .init.* sections is freed by free_initmem() after
the system boots.
Since do_one_initcall() is marked __init_or_module, it is also called by
do_init_module() whenever a loadable module is initialized after boot:
do_init_module()
do_one_initcall()
initcall_get_modname()
Does this unconditionally iterate over the __start_initcall_modnames array
residing in the freed .init.data section?
> + if (modname && module_is_blacklisted(modname)) {
> + pr_info("Skipping initcall for blacklisted built-in module %s\n",
> + modname);
> + return 0;
> + }
> +
> if (initcall_blacklisted(fn))
> return -EPERM;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260622140259.2974-1-atomlin@atomlin.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-06-22 14:17 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-22 14:02 [PATCH v2] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-06-22 14:17 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.