summaryrefslogtreecommitdiff
path: root/security/landlock/task.c
diff options
context:
space:
mode:
Diffstat (limited to 'security/landlock/task.c')
-rw-r--r--security/landlock/task.c257
1 files changed, 190 insertions, 67 deletions
diff --git a/security/landlock/task.c b/security/landlock/task.c
index dc7dab78392e..2385017418ca 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -1,23 +1,29 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * Landlock LSM - Ptrace hooks
+ * Landlock - Ptrace and scope hooks
*
* Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2019-2020 ANSSI
+ * Copyright © 2024-2025 Microsoft Corporation
*/
#include <asm/current.h>
+#include <linux/cleanup.h>
#include <linux/cred.h>
#include <linux/errno.h>
#include <linux/kernel.h>
+#include <linux/lsm_audit.h>
#include <linux/lsm_hooks.h>
#include <linux/rcupdate.h>
#include <linux/sched.h>
+#include <linux/sched/signal.h>
#include <net/af_unix.h>
#include <net/sock.h>
+#include "audit.h"
#include "common.h"
#include "cred.h"
+#include "domain.h"
#include "fs.h"
#include "ruleset.h"
#include "setup.h"
@@ -37,41 +43,29 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
{
const struct landlock_hierarchy *walker;
+ /* Quick return for non-landlocked tasks. */
if (!parent)
return true;
+
if (!child)
return false;
+
for (walker = child->hierarchy; walker; walker = walker->parent) {
if (walker == parent->hierarchy)
/* @parent is in the scoped hierarchy of @child. */
return true;
}
+
/* There is no relationship between @parent and @child. */
return false;
}
-static bool task_is_scoped(const struct task_struct *const parent,
- const struct task_struct *const child)
-{
- bool is_scoped;
- const struct landlock_ruleset *dom_parent, *dom_child;
-
- rcu_read_lock();
- dom_parent = landlock_get_task_domain(parent);
- dom_child = landlock_get_task_domain(child);
- is_scoped = domain_scope_le(dom_parent, dom_child);
- rcu_read_unlock();
- return is_scoped;
-}
-
-static int task_ptrace(const struct task_struct *const parent,
- const struct task_struct *const child)
+static int domain_ptrace(const struct landlock_ruleset *const parent,
+ const struct landlock_ruleset *const child)
{
- /* Quick return for non-landlocked tasks. */
- if (!landlocked(parent))
- return 0;
- if (task_is_scoped(parent, child))
+ if (domain_scope_le(parent, child))
return 0;
+
return -EPERM;
}
@@ -91,7 +85,39 @@ static int task_ptrace(const struct task_struct *const parent,
static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
- return task_ptrace(current, child);
+ const struct landlock_cred_security *parent_subject;
+ const struct landlock_ruleset *child_dom;
+ int err;
+
+ /* Quick return for non-landlocked tasks. */
+ parent_subject = landlock_cred(current_cred());
+ if (!parent_subject)
+ return 0;
+
+ scoped_guard(rcu)
+ {
+ child_dom = landlock_get_task_domain(child);
+ err = domain_ptrace(parent_subject->domain, child_dom);
+ }
+
+ if (!err)
+ return 0;
+
+ /*
+ * For the ptrace_access_check case, we log the current/parent domain
+ * and the child task.
+ */
+ if (!(mode & PTRACE_MODE_NOAUDIT))
+ landlock_log_denial(parent_subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_PTRACE,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = child,
+ },
+ .layer_plus_one = parent_subject->domain->num_layers,
+ });
+
+ return err;
}
/**
@@ -108,7 +134,35 @@ static int hook_ptrace_access_check(struct task_struct *const child,
*/
static int hook_ptrace_traceme(struct task_struct *const parent)
{
- return task_ptrace(parent, current);
+ const struct landlock_cred_security *parent_subject;
+ const struct landlock_ruleset *child_dom;
+ int err;
+
+ child_dom = landlock_get_current_domain();
+
+ guard(rcu)();
+ parent_subject = landlock_cred(__task_cred(parent));
+ err = domain_ptrace(parent_subject->domain, child_dom);
+
+ if (!err)
+ return 0;
+
+ /*
+ * For the ptrace_traceme case, we log the domain which is the cause of
+ * the denial, which means the parent domain instead of the current
+ * domain. This may look unusual because the ptrace_traceme action is a
+ * request to be traced, but the semantic is consistent with
+ * hook_ptrace_access_check().
+ */
+ landlock_log_denial(parent_subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_PTRACE,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = current,
+ },
+ .layer_plus_one = parent_subject->domain->num_layers,
+ });
+ return err;
}
/**
@@ -127,7 +181,7 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
access_mask_t scope)
{
int client_layer, server_layer;
- struct landlock_hierarchy *client_walker, *server_walker;
+ const struct landlock_hierarchy *client_walker, *server_walker;
/* Quick return if client has no domain */
if (WARN_ON_ONCE(!client))
@@ -212,28 +266,43 @@ static int hook_unix_stream_connect(struct sock *const sock,
struct sock *const other,
struct sock *const newsk)
{
- const struct landlock_ruleset *const dom =
- landlock_get_applicable_domain(landlock_get_current_domain(),
- unix_scope);
+ size_t handle_layer;
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), unix_scope,
+ &handle_layer);
/* Quick return for non-landlocked tasks. */
- if (!dom)
+ if (!subject)
+ return 0;
+
+ if (!is_abstract_socket(other))
return 0;
- if (is_abstract_socket(other) && sock_is_scoped(other, dom))
- return -EPERM;
+ if (!sock_is_scoped(other, subject->domain))
+ return 0;
- return 0;
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .audit = {
+ .type = LSM_AUDIT_DATA_NET,
+ .u.net = &(struct lsm_network_audit) {
+ .sk = other,
+ },
+ },
+ .layer_plus_one = handle_layer + 1,
+ });
+ return -EPERM;
}
static int hook_unix_may_send(struct socket *const sock,
struct socket *const other)
{
- const struct landlock_ruleset *const dom =
- landlock_get_applicable_domain(landlock_get_current_domain(),
- unix_scope);
+ size_t handle_layer;
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), unix_scope,
+ &handle_layer);
- if (!dom)
+ if (!subject)
return 0;
/*
@@ -243,10 +312,23 @@ static int hook_unix_may_send(struct socket *const sock,
if (unix_peer(sock->sk) == other->sk)
return 0;
- if (is_abstract_socket(other->sk) && sock_is_scoped(other->sk, dom))
- return -EPERM;
+ if (!is_abstract_socket(other->sk))
+ return 0;
+
+ if (!sock_is_scoped(other->sk, subject->domain))
+ return 0;
- return 0;
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
+ .audit = {
+ .type = LSM_AUDIT_DATA_NET,
+ .u.net = &(struct lsm_network_audit) {
+ .sk = other->sk,
+ },
+ },
+ .layer_plus_one = handle_layer + 1,
+ });
+ return -EPERM;
}
static const struct access_masks signal_scope = {
@@ -255,56 +337,97 @@ static const struct access_masks signal_scope = {
static int hook_task_kill(struct task_struct *const p,
struct kernel_siginfo *const info, const int sig,
- const struct cred *const cred)
+ const struct cred *cred)
{
bool is_scoped;
- const struct landlock_ruleset *dom;
-
- if (cred) {
- /* Dealing with USB IO. */
- dom = landlock_cred(cred)->domain;
- } else {
- dom = landlock_get_current_domain();
+ size_t handle_layer;
+ const struct landlock_cred_security *subject;
+
+ if (!cred) {
+ /*
+ * Always allow sending signals between threads of the same process.
+ * This is required for process credential changes by the Native POSIX
+ * Threads Library and implemented by the set*id(2) wrappers and
+ * libcap(3) with tgkill(2). See nptl(7) and libpsx(3).
+ *
+ * This exception is similar to the __ptrace_may_access() one.
+ */
+ if (same_thread_group(p, current))
+ return 0;
+
+ /* Not dealing with USB IO. */
+ cred = current_cred();
}
- dom = landlock_get_applicable_domain(dom, signal_scope);
+
+ subject = landlock_get_applicable_subject(cred, signal_scope,
+ &handle_layer);
/* Quick return for non-landlocked tasks. */
- if (!dom)
+ if (!subject)
return 0;
- rcu_read_lock();
- is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p),
- LANDLOCK_SCOPE_SIGNAL);
- rcu_read_unlock();
- if (is_scoped)
- return -EPERM;
+ scoped_guard(rcu)
+ {
+ is_scoped = domain_is_scoped(subject->domain,
+ landlock_get_task_domain(p),
+ signal_scope.scope);
+ }
+
+ if (!is_scoped)
+ return 0;
- return 0;
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = p,
+ },
+ .layer_plus_one = handle_layer + 1,
+ });
+ return -EPERM;
}
static int hook_file_send_sigiotask(struct task_struct *tsk,
struct fown_struct *fown, int signum)
{
- const struct landlock_ruleset *dom;
+ const struct landlock_cred_security *subject;
bool is_scoped = false;
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
- dom = landlock_get_applicable_domain(
- landlock_file(fown->file)->fown_domain, signal_scope);
+ subject = &landlock_file(fown->file)->fown_subject;
- /* Quick return for unowned socket. */
- if (!dom)
+ /*
+ * Quick return for unowned socket.
+ *
+ * subject->domain has already been filtered when saved by
+ * hook_file_set_fowner(), so there is no need to call
+ * landlock_get_applicable_subject() here.
+ */
+ if (!subject->domain)
return 0;
- rcu_read_lock();
- is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk),
- LANDLOCK_SCOPE_SIGNAL);
- rcu_read_unlock();
- if (is_scoped)
- return -EPERM;
+ scoped_guard(rcu)
+ {
+ is_scoped = domain_is_scoped(subject->domain,
+ landlock_get_task_domain(tsk),
+ signal_scope.scope);
+ }
+
+ if (!is_scoped)
+ return 0;
- return 0;
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_SIGNAL,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = tsk,
+ },
+#ifdef CONFIG_AUDIT
+ .layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
+#endif /* CONFIG_AUDIT */
+ });
+ return -EPERM;
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {