[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