[RFC PATCH 4/9] Loadpol LSM: add a file in securityfs to read/modify the policy

Simon THOBY git at nightmared.fr
Wed May 21 14:01:08 UTC 2025


Privileged users (CAP_SYS_ADMIN in the root namespace) can read and
update the module policy.

Signed-off-by: Simon THOBY <git at nightmared.fr>
---
 security/loadpol/Makefile         |   2 +-
 security/loadpol/loadpol.h        |   8 +
 security/loadpol/loadpol_fs.c     | 108 +++++++++++++
 security/loadpol/loadpol_policy.c | 244 +++++++++++++++++++++++++++++-
 4 files changed, 360 insertions(+), 2 deletions(-)
 create mode 100644 security/loadpol/loadpol_fs.c

diff --git a/security/loadpol/Makefile b/security/loadpol/Makefile
index 062215e1f831..3351a4e90c1d 100644
--- a/security/loadpol/Makefile
+++ b/security/loadpol/Makefile
@@ -1 +1 @@
-obj-$(CONFIG_SECURITY_LOADPOL) := loadpol.o loadpol_policy.o
+obj-$(CONFIG_SECURITY_LOADPOL) := loadpol.o loadpol_policy.o loadpol_fs.o
diff --git a/security/loadpol/loadpol.h b/security/loadpol/loadpol.h
index a81d52f6d4da..e81aa322e178 100644
--- a/security/loadpol/loadpol.h
+++ b/security/loadpol/loadpol.h
@@ -4,6 +4,7 @@
 #define _SECURITY_LOADPOL_LOADPOL_H
 
 #include "linux/list.h"
+#include <linux/seq_file.h>
 
 #define LOADPOL_NAME "loadpol"
 
@@ -29,6 +30,13 @@ struct loadpol_policy_entry {
 
 extern struct list_head __rcu *loadpol_policy;
 
+void *loadpol_policy_start(struct seq_file *m, loff_t *pos);
+void *loadpol_policy_next(struct seq_file *m, void *v, loff_t *pos);
+void loadpol_policy_stop(struct seq_file *m, void *v);
+int loadpol_policy_show(struct seq_file *m, void *v);
+
+ssize_t loadpol_parse_ruleset(char *data);
+
 // evaluate if a kernel module called 'kmod' is allowed to be loaded in the kernel
 int loadpol_kernel_module_load(const char *kmod);
 
diff --git a/security/loadpol/loadpol_fs.c b/security/loadpol/loadpol_fs.c
new file mode 100644
index 000000000000..9134d11718a0
--- /dev/null
+++ b/security/loadpol/loadpol_fs.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include "linux/array_size.h"
+#include <linux/security.h>
+
+#include "loadpol.h"
+
+static struct dentry *securityfs_dir;
+static struct dentry *securityfs_policy;
+
+static DEFINE_MUTEX(policy_write_mutex);
+
+static const struct seq_operations loadpol_policy_seqops = {
+	.start = loadpol_policy_start,
+	.next = loadpol_policy_next,
+	.stop = loadpol_policy_stop,
+	.show = loadpol_policy_show,
+};
+
+static int loadpol_open_policy(struct inode *inode, struct file *filp)
+{
+	// Check permissions when accessing with writable flags
+	if ((filp->f_flags & O_ACCMODE) != O_RDONLY) {
+		if (!capable(CAP_SYS_ADMIN))
+			return -EPERM;
+	}
+	return seq_open(filp, &loadpol_policy_seqops);
+}
+
+static ssize_t loadpol_write_policy(struct file *file, const char __user *buf,
+				    size_t count, loff_t *ppos)
+{
+	char *data;
+	ssize_t ret;
+
+	/*
+	 * arbitrary size limit (to prevent a DoS but still allow loading a policy with a few
+	 * thousands of entries)
+	 */
+	if (count >= 64 * PAGE_SIZE) {
+		ret = -ENOSPC;
+		goto out;
+	}
+
+	/* Partial writes are not permitted */
+	if (*ppos != 0) {
+		ret = -ESPIPE;
+		goto out;
+	}
+
+	data = memdup_user_nul(buf, count);
+	if (IS_ERR(data)) {
+		ret = PTR_ERR(data);
+		goto out;
+	}
+
+	ret = mutex_lock_interruptible(&policy_write_mutex);
+	if (ret < 0) {
+		ret = -EBUSY;
+		goto out_free;
+	}
+
+	ret = loadpol_parse_ruleset(data);
+	/* the policy was injested, return the write as having been completed */
+	if (!ret)
+		ret = count;
+
+	mutex_unlock(&policy_write_mutex);
+out_free:
+	kfree(data);
+out:
+	return ret;
+}
+
+static const struct file_operations loadpol_policy_ops = {
+	.open = loadpol_open_policy,
+	.write = loadpol_write_policy,
+	.read = seq_read,
+	.llseek = seq_lseek,
+};
+
+static int __init loadpol_init_fs(void)
+{
+	int ret;
+
+	securityfs_dir = securityfs_create_dir(LOADPOL_NAME, NULL);
+	if (IS_ERR(securityfs_dir)) {
+		ret = PTR_ERR(securityfs_dir);
+		goto err;
+	}
+
+	securityfs_policy = securityfs_create_file(
+		"policy", 0600, securityfs_dir, NULL, &loadpol_policy_ops
+	);
+	if (IS_ERR(securityfs_policy)) {
+		ret = PTR_ERR(securityfs_policy);
+		goto err;
+	}
+
+	return 0;
+err:
+	securityfs_remove(securityfs_policy);
+	securityfs_remove(securityfs_dir);
+	return ret;
+}
+
+/* only create debugfs entries once the filesystem is available */
+fs_initcall(loadpol_init_fs);
diff --git a/security/loadpol/loadpol_policy.c b/security/loadpol/loadpol_policy.c
index 6ba5ab600e3e..366046f00959 100644
--- a/security/loadpol/loadpol_policy.c
+++ b/security/loadpol/loadpol_policy.c
@@ -1,5 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0-only
 
+#define pr_fmt(fmt) "loadpol: " fmt
+
 #include "linux/rculist.h"
 #include <linux/sched.h>
 #include <linux/sysctl.h>
@@ -12,6 +14,244 @@ static LIST_HEAD(loadpol_policy_a);
 static LIST_HEAD(loadpol_policy_b);
 struct list_head __rcu *loadpol_policy = (struct list_head __rcu *)(&loadpol_policy_a);
 
+enum loadpol_options {
+	Opt_action,
+	Opt_allowed,
+	Opt_denied,
+	Opt_kernel,
+	Opt_module,
+	Opt_origin,
+	Opt_userspace,
+	Opt_err,
+};
+
+static const match_table_t policy_options = {
+	{Opt_action, "action=%s"},
+	{Opt_allowed, "allow"},
+	{Opt_denied, "deny"},
+	{Opt_kernel, "kernel"},
+	{Opt_module, "module==%s"},
+	{Opt_origin, "origin==%s"},
+	{Opt_userspace, "user"},
+	{Opt_err, NULL},
+};
+
+#define opt(o) policy_options[o].pattern
+
+static void loadpol_free_entry(struct loadpol_policy_entry *entry)
+{
+	kfree(entry->module_name);
+	kfree(entry);
+}
+
+static void loadpol_free_ruleset(struct list_head *policy)
+{
+	struct loadpol_policy_entry *entry, *next_entry;
+
+	list_for_each_entry_safe(entry, next_entry, policy, list) {
+		list_del(&entry->list);
+		loadpol_free_entry(entry);
+	}
+}
+
+void *loadpol_policy_start(struct seq_file *m, loff_t *pos)
+{
+	struct list_head *entry_list;
+
+	rcu_read_lock();
+	entry_list = seq_list_start_rcu(rcu_dereference(loadpol_policy), *pos);
+	rcu_read_unlock();
+
+	if (!entry_list)
+		return NULL;
+
+	return container_of(entry_list, struct loadpol_policy_entry, list);
+}
+
+void *loadpol_policy_next(struct seq_file *m, void *v, loff_t *pos)
+{
+	struct list_head *entry_list;
+
+	rcu_read_lock();
+	entry_list = seq_list_next_rcu(v, rcu_dereference(loadpol_policy), pos);
+	rcu_read_unlock();
+
+	if (!entry_list)
+		return NULL;
+
+	return container_of(entry_list, struct loadpol_policy_entry, list);
+}
+
+void loadpol_policy_stop(struct seq_file *m, void *v)
+{
+}
+
+int loadpol_policy_show(struct seq_file *m, void *v)
+{
+	struct loadpol_policy_entry *entry = v;
+
+	seq_printf(m, opt(Opt_origin), "");
+	if (entry->origin & ORIGIN_KERNEL)
+		seq_puts(m, opt(Opt_kernel));
+	if (entry->origin & ORIGIN_KERNEL && entry->origin & ORIGIN_USERSPACE)
+		seq_puts(m, ",");
+	if (entry->origin & ORIGIN_USERSPACE)
+		seq_puts(m, opt(Opt_userspace));
+
+	seq_puts(m, " ");
+
+	if (entry->module_name) {
+		seq_printf(m, opt(Opt_module), entry->module_name);
+		seq_puts(m, " ");
+	}
+
+	seq_printf(m, opt(Opt_action),
+		   (entry->action == ACTION_ALLOW) ? opt(Opt_allowed) : opt(Opt_denied));
+
+	seq_puts(m, "\n");
+	return 0;
+}
+
+static struct loadpol_policy_entry *process_policy_rule(char *line)
+{
+	char *token, *subtoken;
+	struct loadpol_policy_entry *entry;
+	int ret = -EINVAL;
+
+	entry = kzalloc(sizeof(*entry), GFP_KERNEL);
+	if (!entry)
+		return ERR_PTR(-ENOMEM);
+
+	// not strictly necessary since we zero-initialize entry, but explicitness is good
+	entry->module_name = NULL;
+	entry->origin = ORIGIN_KERNEL | ORIGIN_USERSPACE;
+	entry->action = ACTION_UNDEFINED;
+
+	while ((token = strsep(&line, " \t"))) {
+		int token_id;
+		substring_t args[MAX_OPT_ARGS];
+
+		if (!strlen(token))
+			continue;
+
+		token_id = match_token(token, policy_options, args);
+		switch (token_id) {
+		case Opt_module:
+			if (entry->module_name) {
+				pr_warn("cannot define two names in the same entry: '%s'", line);
+				goto err;
+			}
+
+			if (!strlen(args[0].from)) {
+				pr_warn("empty module names are not supported: '%s'", line);
+				goto err;
+			}
+
+			entry->module_name = kstrdup(args[0].from, GFP_KERNEL);
+			if (!entry->module_name) {
+				ret = -ENOMEM;
+				goto err;
+			}
+
+			break;
+		case Opt_origin:
+			entry->origin = 0;
+
+			while ((subtoken = strsep(&args[0].from, ","))) {
+				if (!strcmp(subtoken, opt(Opt_kernel))) {
+					entry->origin |= ORIGIN_KERNEL;
+					continue;
+				}
+
+				if (!strcmp(subtoken, opt(Opt_userspace))) {
+					entry->origin |= ORIGIN_USERSPACE;
+					continue;
+				}
+
+				pr_warn("Unsupported origin '%s'", subtoken);
+				goto err;
+			}
+			break;
+		case Opt_action:
+			if (entry->action != ACTION_UNDEFINED) {
+				pr_warn("cannot define two action in the same entry: '%s'", line);
+				goto err;
+			}
+
+			if (!strcmp(args[0].from, opt(Opt_denied))) {
+				entry->action = ACTION_DENY;
+				continue;
+			}
+
+			if (!strcmp(args[0].from, opt(Opt_allowed))) {
+				entry->action = ACTION_ALLOW;
+				continue;
+			}
+
+			pr_warn("Loadpol: Unsupported action '%s'", args[0].from);
+			goto err;
+		case Opt_err:
+			pr_warn("Unsupported token %d: %s\n", token_id, token);
+			return ERR_PTR(-EINVAL);
+		}
+	}
+
+	return entry;
+err:
+	loadpol_free_entry(entry);
+
+	return ERR_PTR(ret);
+}
+
+ssize_t loadpol_parse_ruleset(char *data)
+{
+	struct list_head *new_policy_list;
+	struct loadpol_policy_entry *entry;
+	char *sep_ptr, *line;
+
+	rcu_read_lock();
+	new_policy_list = (rcu_dereference(loadpol_policy) == &loadpol_policy_a) ?
+			   &loadpol_policy_b : &loadpol_policy_a;
+	rcu_read_unlock();
+
+	/* wait for the RCU previous critical section to be over */
+	synchronize_rcu();
+
+	/*
+	 * At this point, we know that nobody else is iterating over new_policy_list: we are
+	 * inside a lock so we have no concurrent writer, and we called synchronize_rcu which ensure
+	 * that current readers are reading the other policy list
+	 * (policy_a if we operate on policy_b, or vice-versa).
+	 */
+
+	/* free the old policy entries */
+	loadpol_free_ruleset(new_policy_list);
+
+	sep_ptr = data;
+	while ((line = strsep(&sep_ptr, "\n"))) {
+		// ignore empty lines
+		if (!strlen(line))
+			continue;
+
+		entry = process_policy_rule(line);
+		if (IS_ERR(entry))
+			goto err;
+
+		list_add_tail(&entry->list, new_policy_list);
+	}
+
+	/* switch to policy */
+	rcu_assign_pointer(loadpol_policy, new_policy_list);
+
+	return 0;
+
+err:
+	/* free the newly created entries */
+	loadpol_free_ruleset(new_policy_list);
+
+	return -EINVAL;
+}
+
 int loadpol_kernel_module_load(const char *kmod)
 {
 	struct task_struct *parent_task;
@@ -53,7 +293,9 @@ int loadpol_kernel_module_load(const char *kmod)
 unlock_and_exit:
 	rcu_read_unlock();
 
-	pr_debug("Loadpol: load of module '%s' %s", kmod, allowed ? "allowed" : "blocked");
+	pr_debug("Loadpol: load of module '%s' %s",
+		 kmod,
+		 allowed ? "allowed" : "blocked");
 
 	return allowed ? 0 : -EPERM;
 }
-- 
2.49.0




More information about the Linux-security-module-archive mailing list