Re: [PATCH v8 1/2] module: Extend module_blacklist parameter to built-in modules

Petr Pavlu <[email protected]> Mon, 3 Aug 2026 17:55:28 +0200
Newsgroups org.kernel.vger.linux-modules,org.kernel.vger.linux-arch,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
On 7/24/26 4:47 AM, 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.
> 
> 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.
> 
> 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.  Architectural Safety and Elimination of Runtime Vulnerabilities:
> 
>         By embedding the boot-time blacklist check inside the
>         do_one_initcall_builtin() __init wrapper function, we ensure
>         the metadata lookup logic is exclusively invoked during early
>         boot. This approach provides strict structural guarantees:
>         - It inherently eliminates Use-After-Free (UAF) and race conditions
>           since loadable modules (which execute post-boot and invoke
>           do_one_initcall() directly) bypass this __init wrapper entirely.
>         - It prevents modpost section mismatch warnings since the __init
>           metadata is strictly accessed by other __init functions.
>         - It mitigates Spectre v1 speculative execution vulnerabilities
>           by guaranteeing the unprivileged runtime module loading path
>           cannot speculatively branch into reclaimed .init.text instructions.

I suggest moving these design notes below the --- separator, together
with the diffstat.

> 
> Signed-off-by: Aaron Tomlin <[email protected]>
> ---
>  include/asm-generic/vmlinux.lds.h |  4 ++-
>  include/linux/init.h              | 27 +++++++++++++++-
>  include/linux/module.h            |  4 ++-
>  init/main.c                       | 54 +++++++++++++++++++++++++++++--
>  kernel/module/main.c              | 22 +------------

The Rust module support in rust/macros/module.rs should be updated
similarly to maintain feature parity.

>  5 files changed, 85 insertions(+), 26 deletions(-)
> 
> diff --git a/include/asm-generic/vmlinux.lds.h b/include/asm-generic/vmlinux.lds.h
> index 5659f4b5a125..799d912dcbc0 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()						\
> +	STRUCT_ALIGN();							\
> +	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 40331923b9f4..623d8642797c 100644
> --- a/include/linux/init.h
> +++ b/include/linux/init.h
> @@ -252,6 +252,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 +261,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 +273,30 @@ extern struct module __this_module;
>  		__initcall_name(initcall, __iid, id),		\
>  		__initcall_section(__sec, __iid))
>  
> -#define ___define_initcall(fn, id, __sec)			\
> +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_modname(fn, id, __sec)			\
> +	____define_initcall_modname(fn, id, __sec, __initcall_id(fn))
> +
> +#define __define_initcall_modname(fn, id)				\
> +	___define_initcall_modname(fn, id, .initcall##id)

These two macros can be merged into:

#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 7566815fabbe..ecc4db15ff4a 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(initfn)	__builtin_module_initcall(initfn);

Renaming the macro parameter from `x` to `initfn` is unnecessary in this
patchset and also makes the preceding comment inconsistent, since it
still refers to `x`.

>  
>  /**
>   * module_exit() - driver exit entry point
> @@ -883,6 +883,8 @@ static inline void module_for_each_mod(int(*func)(struct module *mod, void *data
>  }
>  #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 e363232b428b..3747ccaeef42 100644
> --- a/init/main.c
> +++ b/init/main.c
> @@ -1334,6 +1334,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 && !memcmp(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();
> @@ -1406,7 +1456,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)
> @@ -1451,7 +1501,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 46dd8d25a605..5c90ebedbf68 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);
>  

This still leaves two blank lines between module_frob_arch_sections()
and layout_and_allocate(), instead of just one.

>  static struct module *layout_and_allocate(struct load_info *info, int flags)
>  {
> @@ -3391,7 +3371,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;
>  	}

-- 
Thanks,
Petr