* [PATCH v4] module: Extend module_blacklist parameter to built-in modules
@ 2026-07-08 2:00 Aaron Tomlin
2026-07-10 15:42 ` Sami Tolvanen
2026-07-10 15:59 ` Arnd Bergmann
0 siblings, 2 replies; 7+ messages in thread
From: Aaron Tomlin @ 2026-07-08 2:00 UTC (permalink / raw)
To: arnd, mcgrof, petr.pavlu, da.gomez, samitolvanen, peterz
Cc: akpm, mhiramat, atomlin, neelx, da.anzani, sean, chjohnst, steve,
mproche, nick.lane, linux-arch, linux-modules, 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 preserve the existing user-space ABI, "module_blacklist=" is kept
as a legacy alias pointing to the same module_denylist variable.
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.
During boot, do_one_initcall() cross-references the initcall function
pointer against this table. If a match is found and the module is
present in the denylist, the initcall is skipped.
To make the denylist functional on monolithic kernels, the command-line
parameter parsing and the module_is_denylisted() lookup function are
decoupled from the loadable module subsystem and moved to init/main.c.
This enables "module_denylist=" and "module_blacklist=" to intercept
built-in modules even on kernels built with CONFIG_MODULES=n.
Design Considerations and Trade-offs:
1. LTO and CFI Compatibility vs. PREL32
Previous iterations of this patch attempted to use top-level
inline assembly to generate 32-bit relative offsets (PREL32) to
save memory. However, raw inline assembly operates blindly
outside of the C compiler's visibility. When compiled with
CONFIG_LTO_CLANG or CONFIG_CFI_CLANG, the compiler applies
symbol renaming and generates Control Flow Integrity stubs.
The raw assembly string-matching fails to track these changes,
resulting in undefined references or runtime address mismatches.
To resolve this, we strictly use standard C structures to hold
the function pointers. This natively allows the compiler to
resolve LTO renaming and map CFI stubs correctly. We trade the
minor spatial optimisation of PREL32 (using absolute 64-bit
pointers instead) to guarantee architectural safety under modern
compiler protections. Because this metadata is placed in an
".init" section and freed entirely after boot, the temporary
memory overhead is negligible.
2. Prevention of UAF (Use-After-Free) via temporal and spatial
boundaries:
Because do_one_initcall() is a shared path invoked by both the
early boot process and runtime module loading (i.e.,
do_init_module()), we must prevent loadable modules from
attempting to scan the ".initcall.modnames" section after it has
been reclaimed by free_initmem().
To ensure safety, we employ a two-fold validation check:
- A temporal check using 'system_state >= SYSTEM_FREEING_INITMEM'
to immediately return NULL once init memory is freed.
- A spatial check using 'is_kernel_text()' and
'is_kernel_inittext()' to confirm the function resides in core
kernel text.
Since dynamically loaded modules reside in separately allocated
module memory outside these ranges, they bypass the table lookup
entirely. This makes the lookup lockless, race-free, and safe
from UAF vulnerabilities.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
Changes since v3:
- Renamed the external function prototype and internal helper to
module_is_denylisted(), while updating the backing variable in
main.c to module_denylist. To preserve user-space compatibility
while adopting modern terminology, separate core_param entries have
been introduced, allowing both the preferred module_denylist=
parameter and the legacy module_blacklist= parameter to resolve to
the same underlying variable (Andrew Morton)
- I introduced the __initcall_fn_ptr() macro helper to dynamically
resolve the initcall pointer configuration:
- For architectures with relative 32-bit relocations
(CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y), it resolves to the
relocation stub pointer __initcall_stub(fn, __iid, id)
- For architectures without PREL32 relocations, it resolves
directly to the function pointer fn
- Decoupled the module_denylist parameter parsing and the
module_is_denylisted() function from CONFIG_MODULES, moving the
logic to init/main.c. This ensures the denylist works for built-in
modules even on monolithic kernels built without loadable module
support (CONFIG_MODULES=n)
- Removed the conditional stub implementation of
module_is_denylisted() in module.h and replaced it with a single,
unconditional declaration outside of the #ifdef CONFIG_MODULES block.
This prevents compiler warnings about missing prototypes and ensures
visibility under a monolithic configuration
- Replaced the initmem_freed state variable and its synchronisation
logic in kernel_init() with race-free spatial boundary checks using
is_kernel_text() and is_kernel_inittext() in initcall_get_modname()
- Aligned the .initcall_modnames table with relocations by assigning
.initcall_fn using the __initcall_stub() helper in
___define_initcall(). This ensures the lookup matches the actual stub
pointer passed to do_one_initcall() when
CONFIG_HAVE_ARCH_PREL32_RELOCATIONS is enabled. Passed the preprocessor
__iid argument to ____define_initcall_modname once to avoid double
evaluation of __COUNTER__ (which caused build failures with LTO)
- Updated initcall_get_modname() in main.c to resolve the function
pointer fn using dereference_function_descriptor(fn) prior to
checking the .text and .init.text boundaries, and dereference both
fn and p->initcall_fn in the comparison loop to support descriptor-based
architectures (e.g., PPC64)
- Linked to v3: https://lore.kernel.org/lkml/20260706050337.7613-1-atomlin@atomlin.com/
Changes since v2:
- Avoided relative 32-bit offsets (PREL32) with inline assembly, opting
instead for standard C structures with absolute pointers. This fixes LTO
and CFI compatibility issues (e.g., under Clang) where raw inline assembly
fails to track compiler-generated symbols and CFI stubs
- Placed module name strings into the ".init.rodata" section via a dedicated
static array to ensure they are freed from memory after boot
- Avoided Use-After-Free (UAF) bugs post-boot when loading dynamic modules:
- Added an 'initmem_freed' flag, marked as '__ro_after_init', set after
free_initmem() to skip table lookups for dynamically loaded modules
- Added a blacklist check in do_init_module() for dynamic modules
- Simplified the linker script using the BOUNDED_SECTION_PRE_LABEL() macro
to define the ".initcall.modnames" section boundary
- Added a dummy/stub implementation of module_is_blacklisted() when
CONFIG_MODULES is disabled to avoid build errors
- Linked to v2: https://lore.kernel.org/lkml/20260622140259.2974-1-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=" and new "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 | 4 ++-
include/linux/init.h | 26 +++++++++++++--
include/linux/module.h | 2 ++
init/main.c | 53 +++++++++++++++++++++++++++++++
kernel/module/main.c | 24 ++------------
5 files changed, 84 insertions(+), 25 deletions(-)
diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
index 5659f4b5a125..fc863595743e 100644
--- a/include/asm-generic/vmlinux.lds.h
+++ b/include/asm-generic/vmlinux.lds.h
@@ -734,7 +734,9 @@
EARLYCON_TABLE() \
LSM_TABLE() \
EARLY_LSM_TABLE() \
- KUNIT_INIT_TABLE()
+ KUNIT_INIT_TABLE() \
+ . = ALIGN(8); \
+ BOUNDED_SECTION_PRE_LABEL(.initcall.modnames, initcall_modnames, __start_, __stop_)
#define INIT_TEXT \
*(.init.text .init.text.*) \
diff --git a/include/linux/init.h b/include/linux/init.h
index 40331923b9f4..9c78b6c30361 100644
--- a/include/linux/init.h
+++ b/include/linux/init.h
@@ -271,8 +271,30 @@ extern struct module __this_module;
__initcall_name(initcall, __iid, id), \
__initcall_section(__sec, __iid))
-#define ___define_initcall(fn, id, __sec) \
- __unique_initcall(fn, id, __sec, __initcall_id(fn))
+#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
+#define __initcall_fn_ptr(fn, __iid, id) __initcall_stub(fn, __iid, id)
+#else
+#define __initcall_fn_ptr(fn, __iid, id) fn
+#endif
+
+struct initcall_modname {
+ initcall_t initcall_fn;
+ const char *modname;
+};
+
+#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") = { \
+ .initcall_fn = __initcall_fn_ptr(fn, __iid, id), \
+ .modname = __initstr_##fn \
+ };
+
+#define ___define_initcall(fn, id, __sec) \
+ ____define_initcall_modname(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 7566815fabbe..bc2968c225e1 100644
--- a/include/linux/module.h
+++ b/include/linux/module.h
@@ -883,6 +883,8 @@ static inline void module_for_each_mod(int(*func)(struct module *mod, void *data
}
#endif /* CONFIG_MODULES */
+extern bool module_is_denylisted(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 e363232b428b..af71811d24e3 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1334,12 +1334,65 @@ 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_denylist is a comma-separated list of module names */
+static char *module_denylist;
+bool module_is_denylisted(const char *module_name)
+{
+ const char *p;
+ size_t len;
+
+ if (!module_denylist)
+ return false;
+
+ for (p = module_denylist; *p; p += len) {
+ len = strcspn(p, ",");
+ if (strlen(module_name) == len && !memcmp(module_name, p, len))
+ return true;
+ if (p[len] == ',')
+ len++;
+ }
+ return false;
+}
+core_param(module_denylist, module_denylist, charp, 0400);
+core_param(module_blacklist, module_denylist, charp, 0400);
+
+static const char *initcall_get_modname(initcall_t fn)
+{
+ struct initcall_modname *p;
+ unsigned long addr = (unsigned long)dereference_function_descriptor(fn);
+
+ if (system_state >= SYSTEM_FREEING_INITMEM)
+ return NULL;
+
+ if (!is_kernel_text(addr) &&
+ !is_kernel_inittext(addr))
+ return NULL;
+
+ for (p = __start_initcall_modnames; p < __stop_initcall_modnames; p++) {
+ if (dereference_function_descriptor(p->initcall_fn) ==
+ dereference_function_descriptor(fn))
+ return p->modname;
+ }
+ 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_denylisted(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..e6d9c52b9786 100644
--- a/kernel/module/main.c
+++ b/kernel/module/main.c
@@ -2919,26 +2919,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 && !memcmp(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)
{
@@ -3389,9 +3369,9 @@ 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 it's denylisted.
*/
- if (blacklisted(info->name)) {
+ if (module_is_denylisted(info->name)) {
pr_err("Module %s is blacklisted\n", info->name);
return -EPERM;
}
--
2.54.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-08 2:00 [PATCH v4] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
@ 2026-07-10 15:42 ` Sami Tolvanen
2026-07-10 15:59 ` Arnd Bergmann
1 sibling, 0 replies; 7+ messages in thread
From: Sami Tolvanen @ 2026-07-10 15:42 UTC (permalink / raw)
To: Aaron Tomlin
Cc: arnd, mcgrof, petr.pavlu, da.gomez, peterz, akpm, mhiramat, neelx,
da.anzani, sean, chjohnst, steve, mproche, nick.lane, linux-arch,
linux-modules, linux-kernel
Hi Aaron,
On Tue, Jul 7, 2026 at 7:00 PM Aaron Tomlin <atomlin@atomlin.com> wrote:
>
> +static const char *initcall_get_modname(initcall_t fn)
> +{
> + struct initcall_modname *p;
> + unsigned long addr = (unsigned long)dereference_function_descriptor(fn);
> +
> + if (system_state >= SYSTEM_FREEING_INITMEM)
> + return NULL;
> +
> + if (!is_kernel_text(addr) &&
> + !is_kernel_inittext(addr))
> + return NULL;
> +
> + for (p = __start_initcall_modnames; p < __stop_initcall_modnames; p++) {
> + if (dereference_function_descriptor(p->initcall_fn) ==
> + dereference_function_descriptor(fn))
> + return p->modname;
> + }
> + 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 I'm reading this correctly, this ends up scanning the
initcall_modnames list for every initcall. Have you measured whether
this has any boot time impact? Can we at least skip this scan if no
module denylist is provided?
Sami
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-08 2:00 [PATCH v4] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-07-10 15:42 ` Sami Tolvanen
@ 2026-07-10 15:59 ` Arnd Bergmann
2026-07-10 23:13 ` Aaron Tomlin
1 sibling, 1 reply; 7+ messages in thread
From: Arnd Bergmann @ 2026-07-10 15:59 UTC (permalink / raw)
To: Aaron Tomlin, Luis Chamberlain, Petr Pavlu, da.gomez,
Sami Tolvanen, Peter Zijlstra
Cc: Andrew Morton, Masami Hiramatsu, neelx, da.anzani, sean, chjohnst,
steve, mproche, nick.lane, Linux-Arch, linux-modules,
linux-kernel
On Wed, Jul 8, 2026, at 04:00, Aaron Tomlin wrote:
> 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.
Andrew already asked you to provide more background on what you need
this part for. Do you have a specific driver you need to disable?
Can't you do the same thing using initcall_blacklist?
> To preserve the existing user-space ABI, "module_blacklist=" is kept
> as a legacy alias pointing to the same module_denylist variable.
It looks like the denylist is only introduced in the same patch?
That sounds more useful, but would better be done in a separate
change, and also needs a proper changelog text.
Arnd
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-10 15:59 ` Arnd Bergmann
@ 2026-07-10 23:13 ` Aaron Tomlin
2026-07-13 8:32 ` Arnd Bergmann
0 siblings, 1 reply; 7+ messages in thread
From: Aaron Tomlin @ 2026-07-10 23:13 UTC (permalink / raw)
To: Arnd Bergmann
Cc: Luis Chamberlain, Petr Pavlu, da.gomez, Sami Tolvanen,
Peter Zijlstra, Andrew Morton, Masami Hiramatsu, neelx, da.anzani,
sean, chjohnst, steve, mproche, nick.lane, Linux-Arch,
linux-modules, linux-kernel
On Fri, Jul 10, 2026 at 05:59:59PM +0200, Arnd Bergmann wrote:
> On Wed, Jul 8, 2026, at 04:00, Aaron Tomlin wrote:
> > 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.
>
> Andrew already asked you to provide more background on what you need
> this part for. Do you have a specific driver you need to disable?
>
> Can't you do the same thing using initcall_blacklist?
Hi Arnd,
Thank you for your feedback and for highlighting Andrew's request for
further context.
The primary motivation for this patch is to provide consistent
administrative control. From a system administrator's perspective, whether
a specific driver is configured as loadable or built-in is often an opaque,
distribution-level decision. If an administrator applies
module_blacklist=foo (or the modern module_denylist=foo) to disable a
problematic driver across a fleet of machines, they rightfully expect it to
be disabled. Currently, if a distribution later changes that module's
configuration from loadable to built-in, the boot parameter is silently
ignored. The driver will subsequently initialise, which can cause
unexpected operational regressions or security policy violations.
Regarding your suggestion to use initcall_blacklist=, while it is certainly
a capable mechanism, it is fundamentally considered a debugging facility
intended for developers. To utilise it, an administrator must know the
exact internal C function name of the driver's initialisation routine
(e.g., initcall_blacklist=foo_driver_init). This requires inspecting the
kernel source code and relies on internal symbols that are subject to
change between releases. Conversely, the module name itself provides a
stable, user-facing administrative interface.
Furthermore, I must credit Petr Pavlu, who offered excellent advice [1]
suggesting that extending module_blacklist= to encompass built-in modules
is the most logical and robust approach to solving this discrepancy.
[1] https://lore.kernel.org/lkml/79ace94f-31d3-4a5e-9a47-3fad69304fe5@suse.com/
> > To preserve the existing user-space ABI, "module_blacklist=" is kept
> > as a legacy alias pointing to the same module_denylist variable.
>
> It looks like the denylist is only introduced in the same patch?
Correct.
> That sounds more useful, but would better be done in a separate
> change, and also needs a proper changelog text.
Understood.
Kind regards,
--
Aaron Tomlin
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-10 23:13 ` Aaron Tomlin
@ 2026-07-13 8:32 ` Arnd Bergmann
2026-07-13 12:13 ` Aaron Tomlin
0 siblings, 1 reply; 7+ messages in thread
From: Arnd Bergmann @ 2026-07-13 8:32 UTC (permalink / raw)
To: Aaron Tomlin
Cc: Luis Chamberlain, Petr Pavlu, da.gomez, Sami Tolvanen,
Peter Zijlstra, Andrew Morton, Masami Hiramatsu, neelx, da.anzani,
sean, chjohnst, steve, mproche, nick.lane, Linux-Arch,
linux-modules, linux-kernel
On Sat, Jul 11, 2026, at 01:13, Aaron Tomlin wrote:
> On Fri, Jul 10, 2026 at 05:59:59PM +0200, Arnd Bergmann wrote:
>> On Wed, Jul 8, 2026, at 04:00, Aaron Tomlin wrote:
>> > 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.
>>
>> Andrew already asked you to provide more background on what you need
>> this part for. Do you have a specific driver you need to disable?
>>
>> Can't you do the same thing using initcall_blacklist?
>
> The primary motivation for this patch is to provide consistent
> administrative control.
Ok, it sounds like you don't actually need it then.
> Regarding your suggestion to use initcall_blacklist=, while it is certainly
> a capable mechanism, it is fundamentally considered a debugging facility
> intended for developers.
I don't see much of a difference here, it's clearly still only a
debugging tool to me, not a general administrative interface: turning
off a random built-in driver likely causes undefined behavior later
if there are any other drivers (built-in or loaded) that depend on it.
Overall I don't think it's worth the added complexity.
Arnd
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-13 8:32 ` Arnd Bergmann
@ 2026-07-13 12:13 ` Aaron Tomlin
2026-07-13 16:02 ` Petr Pavlu
0 siblings, 1 reply; 7+ messages in thread
From: Aaron Tomlin @ 2026-07-13 12:13 UTC (permalink / raw)
To: Arnd Bergmann
Cc: Luis Chamberlain, Petr Pavlu, da.gomez, Sami Tolvanen,
Peter Zijlstra, Andrew Morton, Masami Hiramatsu, neelx, da.anzani,
sean, chjohnst, steve, mproche, nick.lane, Linux-Arch,
linux-modules, linux-kernel
On Mon, Jul 13, 2026 at 10:32:14AM +0200, Arnd Bergmann wrote:
> On Sat, Jul 11, 2026, at 01:13, Aaron Tomlin wrote:
> > On Fri, Jul 10, 2026 at 05:59:59PM +0200, Arnd Bergmann wrote:
> >> On Wed, Jul 8, 2026, at 04:00, Aaron Tomlin wrote:
> >> > 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.
> >>
> >> Andrew already asked you to provide more background on what you need
> >> this part for. Do you have a specific driver you need to disable?
> >>
> >> Can't you do the same thing using initcall_blacklist?
> >
> > The primary motivation for this patch is to provide consistent
> > administrative control.
>
> Ok, it sounds like you don't actually need it then.
>
> > Regarding your suggestion to use initcall_blacklist=, while it is certainly
> > a capable mechanism, it is fundamentally considered a debugging facility
> > intended for developers.
>
> I don't see much of a difference here, it's clearly still only a
> debugging tool to me, not a general administrative interface: turning
> off a random built-in driver likely causes undefined behavior later
> if there are any other drivers (built-in or loaded) that depend on it.
>
> Overall I don't think it's worth the added complexity.
Hi Arnd,
I appreciate your candour, but I must respectfully disagree with the
assessment that this is merely a debugging tool with no practical
necessity.
The requirement stems from large-scale infrastructure management and
configuration consistency. System administrators rely on standard
provisioning scripts across diverse hardware. If a distribution arbitrarily
alters a kernel configuration, changing a module from loadable (=m) to
built-in (=y), the administrator's module_blacklist= directive is suddenly
and silently ignored. This creates a severe policy enforcement gap.
Regarding the distinction between initcall_blacklist= and
module_blacklist=, the difference lies entirely in ABI stability.
To use initcall_blacklist= requires the administrator to know the exact
internal C function name of the initialisation routine (e.g.,
foo_driver_init). This is an internal kernel implementation detail, subject
to change without notice, and entirely undocumented for users. Conversely,
the module name is a stable, well-known, and documented user-facing
identifier. Providing a stable interface for administrative policy is the
very definition of a general administrative tool, rather than a developer
debugging facility.
For example, consider CVE-2021-43267. A system administrator can now use
module_blacklist=tipc which would cover both built-in and loadable module
configurations.
To address your concern regarding undefined behaviour, disabling a built-in
driver carries the exact same dependency risks as preventing a loadable
module from loading via the traditional blacklist. In both scenarios,
dependent drivers will naturally fail to probe or initialise. System
administrators who apply denylists are already expected to understand the
hardware and software dependencies of the modules they are explicitly
disabling.
I understand your hesitation regarding added complexity. However, with the
fast-path optimisation suggested by Sami (which bypassing the scan entirely
if the parameter is unused), the overhead is essentially zero. I believe
bridging the logical gap between how we treat =m and =y modules is well
worth that minimal footprint.
Kind regards,
--
Aaron Tomlin
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4] module: Extend module_blacklist parameter to built-in modules
2026-07-13 12:13 ` Aaron Tomlin
@ 2026-07-13 16:02 ` Petr Pavlu
0 siblings, 0 replies; 7+ messages in thread
From: Petr Pavlu @ 2026-07-13 16:02 UTC (permalink / raw)
To: Aaron Tomlin
Cc: Arnd Bergmann, Luis Chamberlain, da.gomez, Sami Tolvanen,
Peter Zijlstra, Andrew Morton, Masami Hiramatsu, neelx, da.anzani,
sean, chjohnst, steve, mproche, nick.lane, Linux-Arch,
linux-modules, linux-kernel
On 7/13/26 2:13 PM, Aaron Tomlin wrote:
> On Mon, Jul 13, 2026 at 10:32:14AM +0200, Arnd Bergmann wrote:
>> On Sat, Jul 11, 2026, at 01:13, Aaron Tomlin wrote:
>>> On Fri, Jul 10, 2026 at 05:59:59PM +0200, Arnd Bergmann wrote:
>>>> On Wed, Jul 8, 2026, at 04:00, Aaron Tomlin wrote:
>>>>> 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.
>>>>
>>>> Andrew already asked you to provide more background on what you need
>>>> this part for. Do you have a specific driver you need to disable?
>>>>
>>>> Can't you do the same thing using initcall_blacklist?
>>>
>>> The primary motivation for this patch is to provide consistent
>>> administrative control.
>>
>> Ok, it sounds like you don't actually need it then.
>>
>>> Regarding your suggestion to use initcall_blacklist=, while it is certainly
>>> a capable mechanism, it is fundamentally considered a debugging facility
>>> intended for developers.
>>
>> I don't see much of a difference here, it's clearly still only a
>> debugging tool to me, not a general administrative interface: turning
>> off a random built-in driver likely causes undefined behavior later
>> if there are any other drivers (built-in or loaded) that depend on it.
>>
>> Overall I don't think it's worth the added complexity.
>
> Hi Arnd,
>
> I appreciate your candour, but I must respectfully disagree with the
> assessment that this is merely a debugging tool with no practical
> necessity.
>
> The requirement stems from large-scale infrastructure management and
> configuration consistency. System administrators rely on standard
> provisioning scripts across diverse hardware. If a distribution arbitrarily
> alters a kernel configuration, changing a module from loadable (=m) to
> built-in (=y), the administrator's module_blacklist= directive is suddenly
> and silently ignored. This creates a severe policy enforcement gap.
Note that a gap will still exist if the administrator previously
blacklisted a specific loadable module using the blacklist command in
/etc/modprobe.d/ instead of the module_blacklist kernel parameter.
>
> Regarding the distinction between initcall_blacklist= and
> module_blacklist=, the difference lies entirely in ABI stability.
> To use initcall_blacklist= requires the administrator to know the exact
> internal C function name of the initialisation routine (e.g.,
> foo_driver_init). This is an internal kernel implementation detail, subject
> to change without notice, and entirely undocumented for users. Conversely,
> the module name is a stable, well-known, and documented user-facing
> identifier. Providing a stable interface for administrative policy is the
> very definition of a general administrative tool, rather than a developer
> debugging facility.
> For example, consider CVE-2021-43267. A system administrator can now use
> module_blacklist=tipc which would cover both built-in and loadable module
> configurations.
>
> To address your concern regarding undefined behaviour, disabling a built-in
> driver carries the exact same dependency risks as preventing a loadable
> module from loading via the traditional blacklist. In both scenarios,
> dependent drivers will naturally fail to probe or initialise. System
> administrators who apply denylists are already expected to understand the
> hardware and software dependencies of the modules they are explicitly
> disabling.
The dependency risk is somewhat greater with built-in modules. Consider
two modules, A and B. Module A exports fun_a() that is used by module B.
fun_a() depends on state initialized in A's init function.
If both modules are loadable, then B can be inserted only after A has
been loaded successfully, that is, A is not blacklisted and its init
function completes successfully.
If A is built-in and blacklisted, nothing prevents B from calling
fun_a() and potentially encountering an undefined state.
That said, it seems the same problem can already occur today if A is
built-in and fails to initialize, since do_initcall_level() doesn't
check the return value from do_one_initcall().
>
> I understand your hesitation regarding added complexity. However, with the
> fast-path optimisation suggested by Sami (which bypassing the scan entirely
> if the parameter is unused), the overhead is essentially zero. I believe
> bridging the logical gap between how we treat =m and =y modules is well
> worth that minimal footprint.
I see the point from a consistency perspective.
--
Cheers,
Petr
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-07-13 16:02 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-08 2:00 [PATCH v4] module: Extend module_blacklist parameter to built-in modules Aaron Tomlin
2026-07-10 15:42 ` Sami Tolvanen
2026-07-10 15:59 ` Arnd Bergmann
2026-07-10 23:13 ` Aaron Tomlin
2026-07-13 8:32 ` Arnd Bergmann
2026-07-13 12:13 ` Aaron Tomlin
2026-07-13 16:02 ` Petr Pavlu
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox