[RFC PATCH 01/20] landlock: Move operations from syscall into ruleset code

From: Justin Suess

Date: Tue Apr 07 2026 - 16:03:24 EST


Refactor syscall restriction code, associated constants and helpers,
into ruleset.h/c. This helps increase consistency by making syscall.c a
consumer of ruleset.h/c's logic. Subsequent patches in this series add
consumers of this logic.

Functions for getting and putting references on a landlock ruleset were
also exposed in the patch for the subsequent consumers, transitioning
them from static to linked functions with headers.

Signed-off-by: Justin Suess <utilityemal77@xxxxxxxxx>
---
include/linux/landlock.h | 92 ++++++++++++++++++
security/landlock/ruleset.c | 179 +++++++++++++++++++++++++++++++++++
security/landlock/ruleset.h | 19 ++--
security/landlock/syscalls.c | 151 +++--------------------------
4 files changed, 296 insertions(+), 145 deletions(-)
create mode 100644 include/linux/landlock.h

diff --git a/include/linux/landlock.h b/include/linux/landlock.h
new file mode 100644
index 000000000000..fae7d138ef8b
--- /dev/null
+++ b/include/linux/landlock.h
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Internal cross subsystem header
+ *
+ * Copyright © 2026 Justin Suess <utilityemal77@xxxxxxxxx>
+ */
+
+#ifndef _LINUX_LANDLOCK_H
+#define _LINUX_LANDLOCK_H
+
+#include <linux/cred.h>
+#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/types.h>
+#include <uapi/linux/landlock.h>
+
+struct landlock_ruleset;
+
+#ifdef CONFIG_SECURITY_LANDLOCK
+
+/*
+ * Returns an owned ruleset from a FD. It is thus needed to call
+ * landlock_put_ruleset() on the returned value.
+ */
+struct landlock_ruleset *landlock_get_ruleset_from_fd(int fd, fmode_t mode);
+
+/*
+ * Acquires an additional reference to a ruleset if it is still alive.
+ */
+bool landlock_try_get_ruleset(struct landlock_ruleset *ruleset);
+
+/*
+ * Releases a previously acquired ruleset.
+ */
+void landlock_put_ruleset(struct landlock_ruleset *ruleset);
+
+/*
+ * Releases a previously acquired ruleset after an RCU-safe deferral.
+ */
+void landlock_put_ruleset_deferred(struct landlock_ruleset *ruleset);
+
+/*
+ * Restricts @cred with @ruleset and the supplied @flags.
+ *
+ * landlock_restrict_cred_precheck() must be called first.
+ *
+ * The caller owns @cred and is responsible for committing or aborting it.
+ * @ruleset may be NULL only with LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
+ */
+int landlock_restrict_cred_precheck(__u32 flags, bool in_task_context);
+
+int landlock_restrict_cred(struct cred *cred, struct landlock_ruleset *ruleset,
+ __u32 flags);
+
+#else /* !CONFIG_SECURITY_LANDLOCK */
+
+static inline struct landlock_ruleset *
+landlock_get_ruleset_from_fd(int fd, fmode_t mode)
+{
+ return ERR_PTR(-EOPNOTSUPP);
+}
+
+static inline bool landlock_try_get_ruleset(struct landlock_ruleset *ruleset)
+{
+ return false;
+}
+
+static inline void landlock_put_ruleset(struct landlock_ruleset *ruleset)
+{
+}
+
+static inline void
+landlock_put_ruleset_deferred(struct landlock_ruleset *ruleset)
+{
+}
+
+static inline int landlock_restrict_cred(struct cred *cred,
+ struct landlock_ruleset *ruleset,
+ __u32 flags)
+{
+ return -EOPNOTSUPP;
+}
+
+static inline int landlock_restrict_cred_precheck(__u32 flags,
+ bool in_task_context)
+{
+ return -EOPNOTSUPP;
+}
+
+#endif /* !CONFIG_SECURITY_LANDLOCK */
+
+#endif /* _LINUX_LANDLOCK_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 181df7736bb9..2333a3dc5f33 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -8,25 +8,204 @@

#include <linux/bits.h>
#include <linux/bug.h>
+#include <linux/capability.h>
#include <linux/cleanup.h>
#include <linux/compiler_types.h>
#include <linux/err.h>
#include <linux/errno.h>
+#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/lockdep.h>
#include <linux/mutex.h>
#include <linux/overflow.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
+#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>

#include "access.h"
+#include "cred.h"
#include "domain.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
+#include "setup.h"
+#include "tsync.h"
+
+static int fop_ruleset_release(struct inode *const inode,
+ struct file *const filp)
+{
+ struct landlock_ruleset *ruleset = filp->private_data;
+
+ landlock_put_ruleset(ruleset);
+ return 0;
+}
+
+static ssize_t fop_dummy_read(struct file *const filp, char __user *const buf,
+ const size_t size, loff_t *const ppos)
+{
+ /* Dummy handler to enable FMODE_CAN_READ. */
+ return -EINVAL;
+}
+
+static ssize_t fop_dummy_write(struct file *const filp,
+ const char __user *const buf, const size_t size,
+ loff_t *const ppos)
+{
+ /* Dummy handler to enable FMODE_CAN_WRITE. */
+ return -EINVAL;
+}
+
+/*
+ * A ruleset file descriptor enables to build a ruleset by adding (i.e.
+ * writing) rule after rule, without relying on the task's context. This
+ * reentrant design is also used in a read way to enforce the ruleset on the
+ * current task.
+ */
+const struct file_operations ruleset_fops = {
+ .release = fop_ruleset_release,
+ .read = fop_dummy_read,
+ .write = fop_dummy_write,
+};
+
+/*
+ * Returns an owned ruleset from a FD. It is thus needed to call
+ * landlock_put_ruleset() on the return value.
+ */
+struct landlock_ruleset *landlock_get_ruleset_from_fd(const int fd,
+ const fmode_t mode)
+{
+ CLASS(fd, ruleset_f)(fd);
+ struct landlock_ruleset *ruleset;
+
+ if (fd_empty(ruleset_f))
+ return ERR_PTR(-EBADF);
+
+ /* Checks FD type and access right. */
+ if (fd_file(ruleset_f)->f_op != &ruleset_fops)
+ return ERR_PTR(-EBADFD);
+ if (!(fd_file(ruleset_f)->f_mode & mode))
+ return ERR_PTR(-EPERM);
+ ruleset = fd_file(ruleset_f)->private_data;
+ if (WARN_ON_ONCE(ruleset->num_layers != 1))
+ return ERR_PTR(-EINVAL);
+ landlock_get_ruleset(ruleset);
+ return ruleset;
+}
+
+void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
+{
+ if (ruleset)
+ refcount_inc(&ruleset->usage);
+}
+
+bool landlock_try_get_ruleset(struct landlock_ruleset *const ruleset)
+{
+ return ruleset && refcount_inc_not_zero(&ruleset->usage);
+}
+
+int landlock_restrict_cred_precheck(const __u32 flags,
+ const bool in_task_context)
+{
+ if (!landlock_initialized)
+ return -EOPNOTSUPP;
+
+ /*
+ * LANDLOCK_RESTRICT_SELF_TSYNC requires that the current task is
+ * the target of restriction.
+ */
+ if ((flags & LANDLOCK_RESTRICT_SELF_TSYNC) && !in_task_context)
+ return -EINVAL;
+
+ /*
+ * Similar checks as for seccomp(2), except that an -EPERM may be
+ * returned.
+ */
+ if (!task_no_new_privs(current) &&
+ !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) {
+ return -EPERM;
+ }
+
+ if (flags & ~LANDLOCK_MASK_RESTRICT_SELF)
+ return -EINVAL;
+
+ return 0;
+}
+
+int landlock_restrict_cred(struct cred *const cred,
+ struct landlock_ruleset *const ruleset,
+ const __u32 flags)
+{
+ struct landlock_cred_security *new_llcred;
+ bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
+ prev_log_subdomains;
+
+ /*
+ * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF without
+ * a ruleset, optionally combined with LANDLOCK_RESTRICT_SELF_TSYNC, but
+ * no other flag must be set.
+ */
+ if (!ruleset &&
+ (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) !=
+ LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)
+ return -EINVAL;
+
+ /* Translates "off" flag to boolean. */
+ log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
+ /* Translates "on" flag to boolean. */
+ log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
+ /* Translates "off" flag to boolean. */
+ log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF);
+
+ new_llcred = landlock_cred(cred);
+
+#ifdef CONFIG_AUDIT
+ prev_log_subdomains = !new_llcred->log_subdomains_off;
+ new_llcred->log_subdomains_off = !prev_log_subdomains ||
+ !log_subdomains;
+#endif /* CONFIG_AUDIT */
+
+ /*
+ * The only case when a ruleset may not be set is if
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set, optionally combined
+ * with LANDLOCK_RESTRICT_SELF_TSYNC.
+ * We could optimize this case by not committing @cred if this flag was
+ * already set, but it is not worth the complexity.
+ */
+ if (ruleset) {
+ struct landlock_ruleset *const new_dom =
+ landlock_merge_ruleset(new_llcred->domain, ruleset);
+
+ if (IS_ERR(new_dom))
+ return PTR_ERR(new_dom);
+
+#ifdef CONFIG_AUDIT
+ new_dom->hierarchy->log_same_exec = log_same_exec;
+ new_dom->hierarchy->log_new_exec = log_new_exec;
+ if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
+ new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
+#endif /* CONFIG_AUDIT */
+
+ landlock_put_ruleset(new_llcred->domain);
+ new_llcred->domain = new_dom;
+
+#ifdef CONFIG_AUDIT
+ new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
+#endif /* CONFIG_AUDIT */
+ }
+
+ if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
+ const int tsync_err =
+ landlock_restrict_sibling_threads(current_cred(), cred);
+
+ if (tsync_err)
+ return tsync_err;
+ }
+
+ return 0;
+}

static struct landlock_ruleset *create_ruleset(const u32 num_layers)
{
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 889f4b30301a..0facc5cb6555 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -11,6 +11,8 @@

#include <linux/cleanup.h>
#include <linux/err.h>
+#include <linux/fs.h>
+#include <linux/landlock.h>
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
@@ -20,6 +22,8 @@
#include "limits.h"
#include "object.h"

+extern const struct file_operations ruleset_fops;
+
struct landlock_hierarchy;

/**
@@ -194,6 +198,8 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
const access_mask_t access_mask_net,
const access_mask_t scope_mask);

+void landlock_get_ruleset(struct landlock_ruleset *ruleset);
+
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);

@@ -204,6 +210,13 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access);

+int landlock_restrict_cred_precheck(const __u32 flags,
+ const bool in_task_context);
+
+int landlock_restrict_cred(struct cred *const cred,
+ struct landlock_ruleset *const ruleset,
+ const __u32 flags);
+
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
@@ -212,12 +225,6 @@ const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct landlock_id id);

-static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
-{
- if (ruleset)
- refcount_inc(&ruleset->usage);
-}
-
/**
* landlock_union_access_masks - Return all access rights handled in the
* domain
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index accfd2e5a0cd..c710e8b16150 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -121,42 +121,6 @@ static void build_check_abi(void)

/* Ruleset handling */

-static int fop_ruleset_release(struct inode *const inode,
- struct file *const filp)
-{
- struct landlock_ruleset *ruleset = filp->private_data;
-
- landlock_put_ruleset(ruleset);
- return 0;
-}
-
-static ssize_t fop_dummy_read(struct file *const filp, char __user *const buf,
- const size_t size, loff_t *const ppos)
-{
- /* Dummy handler to enable FMODE_CAN_READ. */
- return -EINVAL;
-}
-
-static ssize_t fop_dummy_write(struct file *const filp,
- const char __user *const buf, const size_t size,
- loff_t *const ppos)
-{
- /* Dummy handler to enable FMODE_CAN_WRITE. */
- return -EINVAL;
-}
-
-/*
- * A ruleset file descriptor enables to build a ruleset by adding (i.e.
- * writing) rule after rule, without relying on the task's context. This
- * reentrant design is also used in a read way to enforce the ruleset on the
- * current task.
- */
-static const struct file_operations ruleset_fops = {
- .release = fop_ruleset_release,
- .read = fop_dummy_read,
- .write = fop_dummy_write,
-};
-
/*
* The Landlock ABI version should be incremented for each new Landlock-related
* user space visible change (e.g. Landlock syscalls). This version should
@@ -264,31 +228,6 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
return ruleset_fd;
}

-/*
- * Returns an owned ruleset from a FD. It is thus needed to call
- * landlock_put_ruleset() on the return value.
- */
-static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
- const fmode_t mode)
-{
- CLASS(fd, ruleset_f)(fd);
- struct landlock_ruleset *ruleset;
-
- if (fd_empty(ruleset_f))
- return ERR_PTR(-EBADF);
-
- /* Checks FD type and access right. */
- if (fd_file(ruleset_f)->f_op != &ruleset_fops)
- return ERR_PTR(-EBADFD);
- if (!(fd_file(ruleset_f)->f_mode & mode))
- return ERR_PTR(-EPERM);
- ruleset = fd_file(ruleset_f)->private_data;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return ERR_PTR(-EINVAL);
- landlock_get_ruleset(ruleset);
- return ruleset;
-}
-
/* Path handling */

/*
@@ -437,7 +376,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
return -EINVAL;

/* Gets and checks the ruleset. */
- ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_WRITE);
+ ruleset = landlock_get_ruleset_from_fd(ruleset_fd, FMODE_CAN_WRITE);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);

@@ -487,33 +426,13 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
- struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
struct cred *new_cred;
- struct landlock_cred_security *new_llcred;
- bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
- prev_log_subdomains;
-
- if (!is_initialized())
- return -EOPNOTSUPP;
-
- /*
- * Similar checks as for seccomp(2), except that an -EPERM may be
- * returned.
- */
- if (!task_no_new_privs(current) &&
- !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
- return -EPERM;
-
- if ((flags | LANDLOCK_MASK_RESTRICT_SELF) !=
- LANDLOCK_MASK_RESTRICT_SELF)
- return -EINVAL;
+ struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
+ int err;

- /* Translates "off" flag to boolean. */
- log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF);
- /* Translates "on" flag to boolean. */
- log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
- /* Translates "off" flag to boolean. */
- log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF);
+ err = landlock_restrict_cred_precheck(flags, true);
+ if (err)
+ return err;

/*
* It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
@@ -525,7 +444,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
(flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
/* Gets and checks the ruleset. */
- ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
+ ruleset = landlock_get_ruleset_from_fd(ruleset_fd,
+ FMODE_CAN_READ);
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
}
@@ -535,57 +455,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
if (!new_cred)
return -ENOMEM;

- new_llcred = landlock_cred(new_cred);
-
-#ifdef CONFIG_AUDIT
- prev_log_subdomains = !new_llcred->log_subdomains_off;
- new_llcred->log_subdomains_off = !prev_log_subdomains ||
- !log_subdomains;
-#endif /* CONFIG_AUDIT */
-
- /*
- * The only case when a ruleset may not be set is if
- * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
- * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1. We could
- * optimize this case by not calling commit_creds() if this flag was
- * already set, but it is not worth the complexity.
- */
- if (ruleset) {
- /*
- * There is no possible race condition while copying and
- * manipulating the current credentials because they are
- * dedicated per thread.
- */
- struct landlock_ruleset *const new_dom =
- landlock_merge_ruleset(new_llcred->domain, ruleset);
- if (IS_ERR(new_dom)) {
- abort_creds(new_cred);
- return PTR_ERR(new_dom);
- }
-
-#ifdef CONFIG_AUDIT
- new_dom->hierarchy->log_same_exec = log_same_exec;
- new_dom->hierarchy->log_new_exec = log_new_exec;
- if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
- new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
-#endif /* CONFIG_AUDIT */
-
- /* Replaces the old (prepared) domain. */
- landlock_put_ruleset(new_llcred->domain);
- new_llcred->domain = new_dom;
-
-#ifdef CONFIG_AUDIT
- new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
-#endif /* CONFIG_AUDIT */
- }
-
- if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
- const int err = landlock_restrict_sibling_threads(
- current_cred(), new_cred);
- if (err) {
- abort_creds(new_cred);
- return err;
- }
+ err = landlock_restrict_cred(new_cred, ruleset, flags);
+ if (err) {
+ abort_creds(new_cred);
+ return err;
}

return commit_creds(new_cred);
--
2.53.0