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 --- 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 + */ + +#ifndef _LINUX_LANDLOCK_H +#define _LINUX_LANDLOCK_H + +#include +#include +#include +#include +#include + +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 #include +#include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #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 #include +#include +#include #include #include #include @@ -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