Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions libkmod/libkmod-config.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const char *kmod_blacklist_get_modname(const struct kmod_list *l)
return l->data;
}

const char *kmod_whitelist_get_modname(const struct kmod_list *l)
{
return l->data;
}

const char *kmod_alias_get_name(const struct kmod_list *l)
{
const struct kmod_alias *alias = l->data;
Expand Down Expand Up @@ -231,6 +236,27 @@ static int kmod_config_add_blacklist(struct kmod_config *config, const char *mod
return 0;
}

static int kmod_config_add_whitelist(struct kmod_config *config, const char *modname)
{
_cleanup_free_ char *p;
struct kmod_list *list;

DBG(config->ctx, "modname=%s\n", modname);

_clang_suppress_alloc_ p = strdup(modname);
if (!p)
return -ENOMEM;

list = kmod_list_append(config->whitelists, p);
if (!list)
return -ENOMEM;

TAKE_PTR(p);
config->whitelists = list;

return 0;
}

static int kmod_config_add_softdep(struct kmod_config *config, const char *modname,
const char *line)
{
Expand Down Expand Up @@ -806,6 +832,17 @@ static int kmod_config_parse(struct kmod_config *config, int fd, const char *fil
goto syntax_error;

kmod_config_add_blacklist(config, modname);
} else if (streq(cmd, "whitelist")) {
char *modname = strtok_r(NULL, "\t ", &saveptr);

if (underscores(modname) < 0)
goto syntax_error;

kmod_config_add_whitelist(config, modname);
} else if (streq(cmd, "whitelist-enable")) {
config->whitelist_active = true;
} else if (streq(cmd, "whitelist-test-mode")) {
config->whitelist_test_mode = true;
} else if (streq(cmd, "options")) {
char *modname = strtok_r(NULL, "\t ", &saveptr);
char *options = strtok_r(NULL, "\0", &saveptr);
Expand Down Expand Up @@ -875,6 +912,7 @@ void kmod_config_free(struct kmod_config *config)
kmod_list_release(config->remove_commands, free);
kmod_list_release(config->softdeps, free);
kmod_list_release(config->weakdeps, free);
kmod_list_release(config->whitelists, free);
kmod_list_release(config->paths, free);
free(config);
}
Expand Down
4 changes: 4 additions & 0 deletions libkmod/libkmod-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,17 @@ struct kmod_config {
struct kmod_list *install_commands;
struct kmod_list *softdeps;
struct kmod_list *weakdeps;
struct kmod_list *whitelists;
bool whitelist_active;
bool whitelist_test_mode;

struct kmod_list *paths;
};

_nonnull_all_ int kmod_config_new(struct kmod_ctx *ctx, struct kmod_config **config, const char *const *config_paths);
_nonnull_all_ void kmod_config_free(struct kmod_config *config);
_nonnull_all_ const char *kmod_blacklist_get_modname(const struct kmod_list *l);
_nonnull_all_ const char *kmod_whitelist_get_modname(const struct kmod_list *l);
_nonnull_all_ const char *kmod_alias_get_name(const struct kmod_list *l);
_nonnull_all_ const char *kmod_alias_get_modname(const struct kmod_list *l);
_nonnull_all_ const char *kmod_option_get_options(const struct kmod_list *l);
Expand Down
38 changes: 38 additions & 0 deletions libkmod/libkmod-module.c
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,23 @@ static bool module_is_blacklisted(const struct kmod_module *mod)
return false;
}

static bool module_is_whitelisted(const struct kmod_module *mod)
{
const struct kmod_ctx *ctx = mod->ctx;
const struct kmod_config *config = kmod_get_config(ctx);
const struct kmod_list *wl = config->whitelists;
const struct kmod_list *l;

kmod_list_foreach(l, wl) {
const char *modname = kmod_whitelist_get_modname(l);

if (streq(modname, mod->name))
return true;
}

return false;
}

KMOD_EXPORT int kmod_module_apply_filter(const struct kmod_ctx *ctx,
enum kmod_filter filter_type,
const struct kmod_list *input,
Expand Down Expand Up @@ -1001,6 +1018,24 @@ static int kmod_module_get_probe_list(struct kmod_module *mod, bool ignorecmd,
return err;
}

static bool module_whitelist_check(struct kmod_module *mod)
{
const struct kmod_config *config = kmod_get_config(mod->ctx);

if (!config->whitelist_active || module_is_whitelisted(mod))
return true;

if (config->whitelist_test_mode) {
NOTICE(mod->ctx,
"module '%s' would be denied (test mode active, load permitted)\n",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have a clear/unique prefix like the original patch. Eg. whitelist: module %s....

Here and throughout.

mod->name);
return true;
}

NOTICE(mod->ctx, "module '%s' not in whitelist, denied\n", mod->name);
return false;
}

KMOD_EXPORT int kmod_module_probe_insert_module(
struct kmod_module *mod, unsigned int flags, const char *extra_options,
int (*run_install)(struct kmod_module *m, const char *cmd, void *data),
Expand Down Expand Up @@ -1032,6 +1067,9 @@ KMOD_EXPORT int kmod_module_probe_insert_module(
return KMOD_PROBE_APPLY_BLACKLIST;
}

if (!module_whitelist_check(mod))
return -EPERM;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about the train of thought here - do we want the check prior to any of the others - already_loaded, blacklist - or why not?

err = kmod_module_get_probe_list(mod, !!(flags & KMOD_PROBE_IGNORE_COMMAND),
&list);
if (err < 0)
Expand Down
2 changes: 1 addition & 1 deletion libkmod/libkmod.h
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ enum kmod_probe {
* output or in dry-run mode.
*
* Insert a module in the kernel resolving dependencies, soft dependencies,
* install commands and applying blacklist.
* install commands and applying blacklist and whitelist.
*
* If @run_install is NULL, this function will fork and exec by calling
* system(3). Don't pass a NULL argument in @run_install if your binary is
Expand Down
21 changes: 21 additions & 0 deletions man/modprobe.d.5.scd
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,27 @@ weakdep _modulename_ _modules_
required/desired at runtime. When c is loaded and is being probed, it
may issue calls to request_module() causing a or b to also be loaded.

whitelist _modulename_
Adds _modulename_ to the whitelist of modules that are permitted to
load. This command only has an effect once *whitelist-enable* is
also given (in this or any other configuration file); by itself, a
*whitelist* entry is parsed but has no effect.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly related to this PR: Ideally we would have a way to validate the modulename(s) listed in the config. Think typos, people accidentally adding multiple on the same line (space, comma, other-separated), etc.

None of this is a blocker for this feature, but if you feel like sending separate PR that would be really appreciated.


whitelist-enable
Enables whitelist enforcement: once this directive appears in any
configuration file, only modules listed via *whitelist* commands
(anywhere in the configuration) are permitted to load, and all other
modules are denied. If *whitelist-enable* is given but no
*whitelist* entries exist, every module is denied. Without
*whitelist-enable*, no whitelist is enforced regardless of any
*whitelist* entries present, and all modules may load as usual.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's shorten this up a bit - my somewhat ADHD brain stopped reading past "configuration file" the first time.

Enables whitelist enforcement, where only modules listed via
*whitelist* commands are permitted to load, and all other
modules are denied.


whitelist-test-mode
Used together with *whitelist-enable*. Modules that the whitelist
would otherwise deny are still permitted to load; a notice is logged
instead, reporting which module would have been denied. This allows
auditing the effect of a whitelist before enforcing it.

# COMPATIBILITY

A future version of kmod will come with a strong warning to avoid use of the
Expand Down
3 changes: 3 additions & 0 deletions scripts/setup-rootfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ map=(
["test-modprobe/external/lib/modules/external/"]="mod-simple.ko"
["test-modprobe/module-from-abspath/home/foo/"]="mod-simple.ko"
["test-modprobe/module-from-relpath/home/foo/"]="mod-simple.ko"
["test-whitelist/no-directives$MODULE_DIRECTORY/3.3.3/kernel/"]="mod-simple.ko"
["test-whitelist/allowed$MODULE_DIRECTORY/3.3.3/kernel/"]="mod-simple.ko"
["test-whitelist/test-mode$MODULE_DIRECTORY/3.3.3/kernel/"]="mod-simple.ko"
["test-depmod/modules-order-compressed$MODULE_DIRECTORY/4.4.4/kernel/drivers/block/cciss.ko"]="mod-fake-cciss.ko"
["test-depmod/modules-order-compressed$MODULE_DIRECTORY/4.4.4/kernel/drivers/scsi/hpsa.ko"]="mod-fake-hpsa.ko"
["test-depmod/modules-order-compressed$MODULE_DIRECTORY/4.4.4/kernel/drivers/scsi/scsi_mod.ko"]="mod-fake-scsi-mod.ko"
Expand Down
1 change: 1 addition & 0 deletions testsuite/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ _testsuite = [
'test-testsuite',
'test-util',
'test-weakdep',
'test-whitelist',
]

if get_option('b_sanitize') != 'none'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
whitelist-enable
whitelist mod-simple
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases extracted from modules themselves.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kernel/mod-simple.ko:
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Device nodes to trigger on-demand module loading.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Soft dependencies extracted from modules themselves.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases for symbols, used by symbol_request().
Binary file not shown.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
whitelist-enable
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases extracted from modules themselves.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kernel/mod-simple.ko:
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Device nodes to trigger on-demand module loading.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Soft dependencies extracted from modules themselves.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases for symbols, used by symbol_request().
Binary file not shown.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases extracted from modules themselves.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kernel/mod-simple.ko:
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Device nodes to trigger on-demand module loading.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Soft dependencies extracted from modules themselves.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases for symbols, used by symbol_request().
Binary file not shown.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
whitelist-enable
whitelist-test-mode
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases extracted from modules themselves.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kernel/mod-simple.ko:
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Device nodes to trigger on-demand module loading.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Soft dependencies extracted from modules themselves.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Aliases for symbols, used by symbol_request().
Binary file not shown.
Empty file.
66 changes: 66 additions & 0 deletions testsuite/test-whitelist.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/*
* Copyright © 2026 Donald Buczek
*/

#include <stdlib.h>

#include "testsuite.h"

static int whitelist_inactive(void)
{
return EXEC_TOOL(modprobe, "mod-simple");
}
DEFINE_TEST(whitelist_inactive,
.description = "check that modules load normally when no whitelist directive is configured",
.config = {
[TC_UNAME_R] = "3.3.3",
[TC_ROOTFS] = TESTSUITE_ROOTFS "test-whitelist/no-directives",
[TC_INIT_MODULE_RETCODES] = "",
},
.modules_loaded = "mod-simple",
);

static int whitelist_deny_all(void)
{
return EXEC_TOOL(modprobe, "mod-simple");
}
DEFINE_TEST(whitelist_deny_all,
.description = "check that modules are denied when whitelist-enable is set with no entries",
.config = {
[TC_UNAME_R] = "3.3.3",
[TC_ROOTFS] = TESTSUITE_ROOTFS "test-whitelist/deny-all",
[TC_INIT_MODULE_RETCODES] = "",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the empty TC_INIT_MODULE_RETCODES here?

},
.expected_fail = true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to your work: we should get a exit-status field, since expected_fail effectively masks any failure - crash, sanitizers, etc.

);

static int whitelist_allowed(void)
{
return EXEC_TOOL(modprobe, "mod-simple");
}
DEFINE_TEST(whitelist_allowed,
.description = "check that a listed module loads, also verifying hyphen/underscore normalisation",
.config = {
[TC_UNAME_R] = "3.3.3",
[TC_ROOTFS] = TESTSUITE_ROOTFS "test-whitelist/allowed",
[TC_INIT_MODULE_RETCODES] = "",
},
.modules_loaded = "mod-simple",
);

static int whitelist_test_mode(void)
{
return EXEC_TOOL(modprobe, "mod-simple");
}
DEFINE_TEST(whitelist_test_mode,
.description = "check that whitelist-test-mode permits loading a module that would otherwise be denied",
.config = {
[TC_UNAME_R] = "3.3.3",
[TC_ROOTFS] = TESTSUITE_ROOTFS "test-whitelist/test-mode",
[TC_INIT_MODULE_RETCODES] = "",
},
.modules_loaded = "mod-simple",
);

TESTSUITE_MAIN();