* [PATCH v12 1/3] module: Treat dashes and underscores interchangeably in module_blacklist
2026-11-03 21:42 [PATCH v12 0/3] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
@ 2026-11-03 21:42 ` Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 2/3] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 3/3] module: Rename module_blacklist to module_denylist Aaron Tomlin
2 siblings, 0 replies; 4+ messages in thread
From: Aaron Tomlin @ 2026-11-03 21:42 UTC (permalink / raw)
To: arnd, mcgrof, petr.pavlu, da.gomez, samitolvanen, peterz, ojeda
Cc: akpm, gregkh, mhiramat, boqun, atomlin, neelx, da.anzani, sean,
chjohnst, mproche, nick.lane, linux-arch, linux-modules,
rust-for-linux, linux-kernel
The "module_blacklist=" command-line parameter allows administrators to
prevent specific modules from loading by specifying a comma-separated
list of module names. The blacklisted() helper checks candidate modules
against this list using a strict byte-for-byte comparison via memcmp().
However, the kernel build system normalises module names to use
underscores (e.g., "my_module"), whereas administrators and user-space
utilities may use hyphens (e.g., "my-module") interchangeably.
Because of the strict memcmp(), specifying a module name with a hyphen
on the kernel command line (e.g., "module_blacklist=my-module") fails
to match the internal module name ("my_module"). Consequently, the
module is loaded despite having been explicitly blacklisted, defeating
the intended administrative mitigation.
Replace memcmp() with parameqn(), which treats dashes and underscores as
equivalent. This ensures that module names match correctly regardless of
whether hyphens or underscores are supplied, matching standard kernel
parameter and modprobe behaviour.
Fixes: be7de5f91fdc ("modules: Add kernel parameter to blacklist modules")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Cc: stable@vger.kernel.org
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
kernel/module/main.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 34e0204ea37c..6b1604018cc9 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2958,7 +2958,7 @@ static bool blacklisted(const char *module_name)
for (p = module_blacklist; *p; p += len) {
len = strcspn(p, ",");
- if (strlen(module_name) == len && !memcmp(module_name, p, len))
+ if (strlen(module_name) == len && parameqn(module_name, p, len))
return true;
if (p[len] == ',')
len++;
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH v12 2/3] module: Extend module_blacklist parameter to built-in modules
2026-11-03 21:42 [PATCH v12 0/3] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 1/3] module: Treat dashes and underscores interchangeably in module_blacklist Aaron Tomlin
@ 2026-11-03 21:42 ` Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 3/3] module: Rename module_blacklist to module_denylist Aaron Tomlin
2 siblings, 0 replies; 4+ messages in thread
From: Aaron Tomlin @ 2026-11-03 21:42 UTC (permalink / raw)
To: arnd, mcgrof, petr.pavlu, da.gomez, samitolvanen, peterz, ojeda
Cc: akpm, gregkh, mhiramat, boqun, atomlin, neelx, da.anzani, sean,
chjohnst, mproche, nick.lane, linux-arch, linux-modules,
rust-for-linux, linux-kernel
Currently, the "module_blacklist=" command-line parameter only applies
to loadable modules. If a module is built-in, the parameter is silently
ignored. This patch extends the blacklisting functionality to built-in
modules by intercepting their initialisation routines during early boot.
To achieve this, we introduce a new ".initcall.modnames" memory section.
For each built-in module, we use a standard C structure (i.e., struct
initcall_modname) to map its initcall function pointer to its associated
KBUILD_MODNAME string. This mapping is restricted only to files implementing
built-in modules via module_init() to avoid mapping core kernel subsystems
and save memory.
During boot, built-in initcalls are executed sequentially via
do_initcall_level() and do_pre_smp_initcalls(). We introduce a new
wrapper function, do_one_initcall_builtin(), to cross-reference the
initcall function pointer against the ".initcall.modnames" table. If
a match is found and the module is present in the blacklist, the
initcall is skipped.
To make the blacklist functional on monolithic kernels, the command-line
parameter parsing and the module_is_blacklisted() lookup function are
decoupled from the loadable module subsystem and moved to init/main.c.
This enables "module_blacklist=" to intercept built-in modules even on
kernels built with CONFIG_MODULES=n.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
include/asm-generic/vmlinux.lds.h | 4 ++-
include/linux/init.h | 25 +++++++++++++-
include/linux/module.h | 4 ++-
init/main.c | 54 +++++++++++++++++++++++++++++--
kernel/module/main.c | 22 +------------
rust/bindings/bindings_helper.h | 1 +
rust/kernel/module.rs | 18 +++++++++++
rust/macros/module.rs | 23 +++++++++++++
8 files changed, 125 insertions(+), 26 deletions(-)
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index 3eb732942d69..96559b67293f 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -765,7 +765,9 @@
EARLYCON_TABLE() \
LSM_TABLE() \
EARLY_LSM_TABLE() \
- KUNIT_INIT_TABLE()
+ KUNIT_INIT_TABLE() \
+ . = ALIGN(8); \
+ BOUNDED_SECTION_BY(.initcall.modnames, _initcall_modnames)
#define INIT_TEXT \
*(.init.text .init.text.*) \
diff --git a/include/linux/init.h b/include/linux/init.h
index 6326c61e2332..08cb8a294375 100644
--- a/include/linux/init.h
+++ b/include/linux/init.h
@@ -109,6 +109,11 @@
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
+struct initcall_modname {
+ initcall_t initcall_fn;
+ const char *modname;
+};
+
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
typedef int initcall_entry_t;
@@ -252,6 +257,7 @@ extern struct module __this_module;
#endif
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
+#define __initcall_fn_ptr(fn, __iid, id) __initcall_stub(fn, __iid, id)
#define ____define_initcall(fn, __stub, __name, __sec) \
__define_initcall_stub(__stub, fn) \
asm(".section \"" __sec "\", \"a\" \n" \
@@ -260,6 +266,7 @@ extern struct module __this_module;
".previous \n"); \
static_assert(__same_type(initcall_t, &fn));
#else
+#define __initcall_fn_ptr(fn, __iid, id) fn
#define ____define_initcall(fn, __unused, __name, __sec) \
static initcall_t __name __used \
__attribute__((__section__(__sec))) = fn;
@@ -271,7 +278,23 @@ extern struct module __this_module;
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))
-#define ___define_initcall(fn, id, __sec) \
+#define ____define_initcall_modname(fn, id, __sec, __iid) \
+ __unique_initcall(fn, id, __sec, __iid) \
+ static const char __initstr_##fn[] __used __aligned(1) \
+ __section(".init.rodata") = KBUILD_MODNAME; \
+ static const struct initcall_modname __modname_##fn __used \
+ __section(".initcall.modnames") \
+ __aligned(__alignof__(struct initcall_modname)) = { \
+ .initcall_fn = __initcall_fn_ptr(fn, __iid, id),\
+ .modname = __initstr_##fn \
+ };
+
+#define __define_initcall_modname(fn, id) \
+ ____define_initcall_modname(fn, id, .initcall##id, __initcall_id(fn))
+
+#define __builtin_module_initcall(fn) __define_initcall_modname(fn, 6)
+
+#define ___define_initcall(fn, id, __sec) \
__unique_initcall(fn, id, __sec, __initcall_id(fn))
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
diff --git a/include/linux/module.h b/include/linux/module.h
index 76f4f3596477..98196204fe27 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -86,7 +86,7 @@ extern void cleanup_module(void);
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
-#define module_init(x) __initcall(x);
+#define module_init(x) __builtin_module_initcall(x);
/**
* module_exit() - driver exit entry point
@@ -892,6 +892,8 @@ static inline void add_taint_module(struct module *mod, unsigned flag,
}
#endif /* CONFIG_MODULES */
+bool module_is_blacklisted(const char *module_name);
+
#ifdef CONFIG_SYSFS
extern struct kset *module_kset;
extern const struct kobj_type module_ktype;
diff --git a/init/main.c b/init/main.c
index 7b0c8c34e6a9..1302fa71a733 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1406,6 +1406,56 @@ 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[];
+
+/* module_blacklist is a comma-separated list of module names */
+static char *module_blacklist;
+bool __init_or_module module_is_blacklisted(const char *module_name)
+{
+ const char *p;
+ size_t len;
+
+ if (!module_blacklist)
+ return false;
+
+ for (p = module_blacklist; *p; p += len) {
+ len = strcspn(p, ",");
+ if (strlen(module_name) == len && parameqn(module_name, p, len))
+ return true;
+ if (p[len] == ',')
+ len++;
+ }
+ return false;
+}
+core_param(module_blacklist, module_blacklist, charp, 0400);
+
+static const char *__init get_builtin_modname(initcall_t fn)
+{
+ struct initcall_modname *p;
+
+ for (p = __start_initcall_modnames; p < __stop_initcall_modnames; p++) {
+ if (p->initcall_fn == fn)
+ return p->modname;
+ }
+ return NULL;
+}
+
+static void __init do_one_initcall_builtin(initcall_t fn)
+{
+ const char *modname;
+
+ if (module_blacklist) {
+ modname = get_builtin_modname(fn);
+ if (modname && module_is_blacklisted(modname)) {
+ pr_info("Skipping initcall for blacklisted built-in module %s\n",
+ modname);
+ return;
+ }
+ }
+ do_one_initcall(fn);
+}
+
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
@@ -1478,7 +1528,7 @@ static void __init do_initcall_level(int level, char *command_line)
do_trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
- do_one_initcall(initcall_from_entry(fn));
+ do_one_initcall_builtin(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
@@ -1523,7 +1573,7 @@ static void __init do_pre_smp_initcalls(void)
do_trace_initcall_level("early");
for (fn = __initcall_start; fn < __initcall0_start; fn++)
- do_one_initcall(initcall_from_entry(fn));
+ do_one_initcall_builtin(initcall_from_entry(fn));
}
static int run_init_process(const char *init_filename)
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 6b1604018cc9..1787c8784aa3 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2946,26 +2946,6 @@ int __weak module_frob_arch_sections(Elf_Ehdr *hdr,
return 0;
}
-/* module_blacklist is a comma-separated list of module names */
-static char *module_blacklist;
-static bool blacklisted(const char *module_name)
-{
- const char *p;
- size_t len;
-
- if (!module_blacklist)
- return false;
-
- for (p = module_blacklist; *p; p += len) {
- len = strcspn(p, ",");
- if (strlen(module_name) == len && parameqn(module_name, p, len))
- return true;
- if (p[len] == ',')
- len++;
- }
- return false;
-}
-core_param(module_blacklist, module_blacklist, charp, 0400);
static struct module *layout_and_allocate(struct load_info *info, int flags)
{
@@ -3421,7 +3401,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;
}
diff --git a/rust/bindings/bindings_helper.h b/rust/bindings/bindings_helper.h
index 3791af0aadc7..d86872912eee 100644
--- a/rust/bindings/bindings_helper.h
+++ b/rust/bindings/bindings_helper.h
@@ -64,6 +64,7 @@
#include <linux/fwctl.h>
#include <linux/fs.h>
#include <linux/i2c.h>
+#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/io-pgtable.h>
#include <linux/ioport.h>
diff --git a/rust/kernel/module.rs b/rust/kernel/module.rs
index d71370598447..3f0545c4d7dc 100644
--- a/rust/kernel/module.rs
+++ b/rust/kernel/module.rs
@@ -78,3 +78,21 @@ pub const fn as_ptr(&self) -> *mut crate::bindings::module {
self.0
}
}
+
+/// Newtype to make `bindings::initcall_modname` [`Sync`].
+#[repr(transparent)]
+#[doc(hidden)]
+pub struct InitcallModname(crate::bindings::initcall_modname);
+
+impl InitcallModname {
+ /// Creates a new [`InitcallModname`] wrapper for `.initcall.modnames`.
+ #[doc(hidden)]
+ #[inline]
+ pub const fn new(val: crate::bindings::initcall_modname) -> Self {
+ Self(val)
+ }
+}
+
+// SAFETY: `.initcall.modnames` entries are immutable metadata only read by the
+// C initcall runner during early boot.
+unsafe impl Sync for InitcallModname {}
diff --git a/rust/macros/module.rs b/rust/macros/module.rs
index 0a7b9e24c960..df1d0eb89c16 100644
--- a/rust/macros/module.rs
+++ b/rust/macros/module.rs
@@ -480,6 +480,8 @@ pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
let ident_init = format_ident!("__{ident}_init");
let ident_exit = format_ident!("__{ident}_exit");
let ident_initcall = format_ident!("__{ident}_initcall");
+ let ident_modname = format_ident!("__{ident}_modname");
+ let ident_modname_str = format_ident!("__{ident}_modname_str");
let initcall_section = ".initcall6.init";
let global_asm = format!(
@@ -491,6 +493,9 @@ pub(crate) fn module(info: ModuleInfo) -> Result<TokenStream> {
);
let name_cstr = CString::new(name.value()).expect("name contains NUL-terminator");
+ let name_bytes = name_cstr.to_bytes_with_nul();
+ let name_len = name_bytes.len();
+ let name_byte_literal = Literal::byte_string(name_bytes);
Ok(quote! {
/// The module name.
@@ -591,6 +596,24 @@ pub extern "C" fn cleanup_module() {
#[cfg(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS)]
::core::arch::global_asm!(#global_asm);
+ #[cfg(not(MODULE))]
+ #[allow(non_upper_case_globals)]
+ #[used(compiler)]
+ #[link_section = ".init.rodata"]
+ static #ident_modname_str: [u8; #name_len] = *#name_byte_literal;
+
+ #[cfg(not(MODULE))]
+ #[allow(non_upper_case_globals)]
+ #[used(compiler)]
+ #[link_section = ".initcall.modnames"]
+ static #ident_modname: ::kernel::module::InitcallModname =
+ ::kernel::module::InitcallModname::new(
+ ::kernel::bindings::initcall_modname {
+ initcall_fn: Some(#ident_init),
+ modname: #ident_modname_str.as_ptr().cast(),
+ },
+ );
+
#[cfg(not(MODULE))]
#[no_mangle]
pub extern "C" fn #ident_init() -> ::kernel::ffi::c_int {
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH v12 3/3] module: Rename module_blacklist to module_denylist
2026-11-03 21:42 [PATCH v12 0/3] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 1/3] module: Treat dashes and underscores interchangeably in module_blacklist Aaron Tomlin
2026-11-03 21:42 ` [PATCH v12 2/3] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
@ 2026-11-03 21:42 ` Aaron Tomlin
2 siblings, 0 replies; 4+ messages in thread
From: Aaron Tomlin @ 2026-11-03 21:42 UTC (permalink / raw)
To: arnd, mcgrof, petr.pavlu, da.gomez, samitolvanen, peterz, ojeda
Cc: akpm, gregkh, mhiramat, boqun, atomlin, neelx, da.anzani, sean,
chjohnst, mproche, nick.lane, linux-arch, linux-modules,
rust-for-linux, linux-kernel
To preserve the existing user-space ABI, "module_blacklist=" is kept
as a legacy alias pointing to the same module_denylist variable.
This patch addresses the documentation by marking "module_blacklist="
as deprecated in admin-guide/kernel-parameters.txt, and documents
the new "module_denylist=" parameter. All internal symbols, such as
module_is_blacklisted(), have been renamed to use "denylist" and all
log messages now use "denylisted".
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
.../admin-guide/kernel-parameters.txt | 6 +++++-
include/linux/module.h | 2 +-
init/main.c | 19 ++++++++++---------
kernel/module/main.c | 4 ++--
4 files changed, 18 insertions(+), 13 deletions(-)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 0b3c140babd9..096c65d6507f 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -4226,7 +4226,11 @@ Kernel parameters
Note that if CONFIG_MODULE_SIG_FORCE is set, that
is always true, so this option does nothing.
- module_blacklist= [KNL] Do not load a comma-separated list of
+ module_blacklist= [KNL] (deprecated)
+ This parameter has been renamed to module_denylist=.
+ Please use module_denylist= instead.
+
+ module_denylist= [KNL] Do not load a comma-separated list of
modules. Useful for debugging problem modules.
mousedev.tap_time=
diff --git a/include/linux/module.h b/include/linux/module.h
index 98196204fe27..0fa43723429f 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -892,7 +892,7 @@ static inline void add_taint_module(struct module *mod, unsigned flag,
}
#endif /* CONFIG_MODULES */
-bool module_is_blacklisted(const char *module_name);
+bool module_is_denylisted(const char *module_name);
#ifdef CONFIG_SYSFS
extern struct kset *module_kset;
diff --git a/init/main.c b/init/main.c
index 1302fa71a733..70b5642da3d8 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1409,17 +1409,17 @@ static inline void do_trace_initcall_level(const char *level)
extern struct initcall_modname __start_initcall_modnames[];
extern struct initcall_modname __stop_initcall_modnames[];
-/* module_blacklist is a comma-separated list of module names */
-static char *module_blacklist;
-bool __init_or_module module_is_blacklisted(const char *module_name)
+/* module_denylist is a comma-separated list of module names */
+static char *module_denylist;
+bool __init_or_module module_is_denylisted(const char *module_name)
{
const char *p;
size_t len;
- if (!module_blacklist)
+ if (!module_denylist)
return false;
- for (p = module_blacklist; *p; p += len) {
+ for (p = module_denylist; *p; p += len) {
len = strcspn(p, ",");
if (strlen(module_name) == len && parameqn(module_name, p, len))
return true;
@@ -1428,7 +1428,8 @@ bool __init_or_module module_is_blacklisted(const char *module_name)
}
return false;
}
-core_param(module_blacklist, module_blacklist, charp, 0400);
+core_param(module_denylist, module_denylist, charp, 0400);
+core_param(module_blacklist, module_denylist, charp, 0400);
static const char *__init get_builtin_modname(initcall_t fn)
{
@@ -1445,10 +1446,10 @@ static void __init do_one_initcall_builtin(initcall_t fn)
{
const char *modname;
- if (module_blacklist) {
+ if (module_denylist) {
modname = get_builtin_modname(fn);
- if (modname && module_is_blacklisted(modname)) {
- pr_info("Skipping initcall for blacklisted built-in module %s\n",
+ if (modname && module_is_denylisted(modname)) {
+ pr_info("Skipping initcall for denylisted built-in module %s\n",
modname);
return;
}
diff --git a/kernel/module/main.c b/kernel/module/main.c
index 1787c8784aa3..f52af42f5337 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -3401,8 +3401,8 @@ 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 (module_is_blacklisted(info->name)) {
- pr_err("Module %s is blacklisted\n", info->name);
+ if (module_is_denylisted(info->name)) {
+ pr_err("Module %s is denylisted\n", info->name);
return -EPERM;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread