summaryrefslogtreecommitdiffstats
path: root/security/landlock/task.c
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
commit3f1fe48a36b0b6722dc3fd421d93512bac138e9a (patch)
treeb767d7f6e26bc64334d3f14f8422d187239dc45d /security/landlock/task.c
downloadlinux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.tar.gz
linux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.zip
Merge tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linuxgrafted
Pull io_uring fixes from Jens Axboe: - Fix a task_work add use-after-free with SQPOLL. The sqpoll thread could pop and complete the last request while io_req_normal_work_add() was still looking at them after the mpscq push. Use the same approach as DEFER_TASKRUN to protect from that, holding an RCU read lock across the add, and have exit wait for an RCU grace period for SQPOLL rings as well. - CQE32 ring fixes: correct the free entry check for 32b CQEs, zero the big_cqe for aux CQEs, and only post the dummy skip CQE on CQE_MIXED rings - Mark the source filter table as COW when cloning bpf filters, so registering another filter on the source doesn't modify the shared table in place - Initialize the task context before running the BPF loop - Requeue zcrx multishot receives stopped by a local resource - End a TX_TIMESTAMP multishot cmd when the CQ is full (lollipopkit) * tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux: io_uring: fix task_work add use-after-free with SQPOLL io_uring/cmd_net: end TX_TIMESTAMP multishot when the CQ is full io_uring/zcrx: requeue multishot receives stopped by a local resource io_uring: initialize task context before running the BPF loop io_uring: zero big_cqe for aux CQEs on CQE32 rings io_uring: fix free entry check for 32b CQEs on CQE32 rings io_uring: only post the dummy skip CQE on CQE_MIXED rings io_uring/bpf_filter: mark source as COW when cloning filters
Diffstat (limited to 'security/landlock/task.c')
-rw-r--r--security/landlock/task.c540
1 files changed, 540 insertions, 0 deletions
diff --git a/security/landlock/task.c b/security/landlock/task.c
new file mode 100644
index 000000000..d9eae86fc
--- /dev/null
+++ b/security/landlock/task.c
@@ -0,0 +1,540 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * 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 "common.h"
+#include "cred.h"
+#include "domain.h"
+#include "fs.h"
+#include "log.h"
+#include "ruleset.h"
+#include "setup.h"
+#include "task.h"
+
+/**
+ * domain_scope_le - Checks domain ordering for scoped ptrace
+ *
+ * @parent: Parent domain.
+ * @child: Potential child of @parent.
+ *
+ * Checks if the @parent domain is less or equal to (i.e. an ancestor, which
+ * means a subset of) the @child domain.
+ *
+ * Return: True if @parent is an ancestor of or equal to @child, false
+ * otherwise.
+ */
+static bool domain_scope_le(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
+{
+ 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 int domain_ptrace(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
+{
+ if (domain_scope_le(parent, child))
+ return 0;
+
+ return -EPERM;
+}
+
+/**
+ * hook_ptrace_access_check - Determines whether the current process may access
+ * another
+ *
+ * @child: Process to be accessed.
+ * @mode: Mode of attachment.
+ *
+ * If the current task has Landlock rules, then the child must have at least
+ * the same rules. Else denied.
+ *
+ * Return: 0 if permission is granted, -errno if denied.
+ */
+static int hook_ptrace_access_check(struct task_struct *const child,
+ const unsigned int mode)
+{
+ const struct landlock_cred_security *parent_subject;
+#ifdef CONFIG_TRACEPOINTS
+ u64 tracee_domain_id = 0;
+#endif /* CONFIG_TRACEPOINTS */
+ int err;
+
+ /* Quick return for non-landlocked tasks. */
+ parent_subject = landlock_cred(current_cred());
+ if (!parent_subject)
+ return 0;
+
+ scoped_guard(rcu) {
+ const struct landlock_domain *const child_dom =
+ landlock_get_task_domain(child);
+ err = domain_ptrace(parent_subject->domain, child_dom);
+#ifdef CONFIG_TRACEPOINTS
+ if (child_dom)
+ tracee_domain_id = child_dom->hierarchy->id;
+#endif /* CONFIG_TRACEPOINTS */
+ }
+
+ 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,
+#ifdef CONFIG_TRACEPOINTS
+ .trace_ptrace = &(struct landlock_ptrace_trace) {
+ .tracee_domain_id = tracee_domain_id,
+ .tracer = current,
+ },
+#endif /* CONFIG_TRACEPOINTS */
+ });
+
+ return err;
+}
+
+/**
+ * hook_ptrace_traceme - Determines whether another process may trace the
+ * current one
+ *
+ * @parent: Task proposed to be the tracer.
+ *
+ * If the parent has Landlock rules, then the current task must have the same
+ * or more rules. Else denied.
+ *
+ * Return: 0 if permission is granted, -errno if denied.
+ */
+static int hook_ptrace_traceme(struct task_struct *const parent)
+{
+ const struct landlock_cred_security *parent_subject;
+ const struct landlock_domain *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,
+#ifdef CONFIG_TRACEPOINTS
+ .trace_ptrace = &(struct landlock_ptrace_trace) {
+ /* The current task's domain is stable here. */
+ .tracee_domain_id = child_dom ? child_dom->hierarchy->id : 0,
+ .tracer = parent,
+ },
+#endif /* CONFIG_TRACEPOINTS */
+ });
+ return err;
+}
+
+/**
+ * domain_is_scoped - Check if an interaction from a client/sender to a
+ * server/receiver should be restricted based on scope controls.
+ *
+ * @client: IPC sender domain.
+ * @server: IPC receiver domain.
+ * @scope: The scope restriction criteria.
+ *
+ * Return: True if @server is in a different domain from @client and @client
+ * is scoped to access @server (i.e. access should be denied), false otherwise.
+ */
+static bool domain_is_scoped(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
+ access_mask_t scope)
+{
+ int client_layer, server_layer;
+ const struct landlock_hierarchy *client_walker, *server_walker;
+
+ /* Quick return if client has no domain */
+ if (WARN_ON_ONCE(!client))
+ return false;
+
+ client_layer = client->num_layers - 1;
+ client_walker = client->hierarchy;
+ /*
+ * client_layer must be able to represent all numbers from
+ * LANDLOCK_MAX_NUM_LAYERS - 1 to -1 for the loop below to terminate.
+ * (It must be large enough, and it must be signed.)
+ */
+ BUILD_BUG_ON(!is_signed_type(typeof(client_layer)));
+ BUILD_BUG_ON(LANDLOCK_MAX_NUM_LAYERS - 1 >
+ type_max(typeof(client_layer)));
+
+ server_layer = server ? (server->num_layers - 1) : -1;
+ server_walker = server ? server->hierarchy : NULL;
+
+ /*
+ * Walks client's parent domains down to the same hierarchy level
+ * as the server's domain, and checks that none of these client's
+ * parent domains are scoped.
+ */
+ for (; client_layer > server_layer; client_layer--) {
+ if (landlock_get_scope_mask(client, client_layer) & scope)
+ return true;
+
+ client_walker = client_walker->parent;
+ }
+ /*
+ * Walks server's parent domains down to the same hierarchy level as
+ * the client's domain.
+ */
+ for (; server_layer > client_layer; server_layer--)
+ server_walker = server_walker->parent;
+
+ for (; client_layer >= 0; client_layer--) {
+ if (landlock_get_scope_mask(client, client_layer) & scope) {
+ /*
+ * Client and server are at the same level in the
+ * hierarchy. If the client is scoped, the request is
+ * only allowed if this domain is also a server's
+ * ancestor.
+ */
+ return server_walker != client_walker;
+ }
+ client_walker = client_walker->parent;
+ server_walker = server_walker->parent;
+ }
+ return false;
+}
+
+static bool sock_is_scoped(struct sock *const other,
+ const struct landlock_domain *const domain)
+{
+ const struct landlock_domain *dom_other;
+
+ /* The credentials will not change. */
+ lockdep_assert_held(&unix_sk(other)->lock);
+
+ /*
+ * A live kernel socket (e.g. from sock_create_kern()) has no backing
+ * file, hence no Landlock domain, so treat it as unscoped. The
+ * sk_socket check only guards that dereference; sk_socket is NULL
+ * solely for a dead peer, which the caller already excludes under the
+ * held lock, so no separate SOCK_DEAD check is needed.
+ */
+ if (unlikely(!other->sk_socket || !other->sk_socket->file))
+ return false;
+
+ dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+ return domain_is_scoped(domain, dom_other,
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+}
+
+#ifdef CONFIG_TRACEPOINTS
+
+static u64 get_socket_domain_id(const struct sock *const other)
+{
+ const struct landlock_domain *domain;
+
+ lockdep_assert_held(&unix_sk(other)->lock);
+ domain = landlock_cred(other->sk_socket->file->f_cred)->domain;
+ return domain ? domain->hierarchy->id : 0;
+}
+
+#endif /* CONFIG_TRACEPOINTS */
+
+static bool is_abstract_socket(struct sock *const sock)
+{
+ struct unix_address *addr = unix_sk(sock)->addr;
+
+ if (!addr)
+ return false;
+
+ if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 &&
+ addr->name->sun_path[0] == '\0')
+ return true;
+
+ return false;
+}
+
+static const struct access_masks unix_scope = {
+ .scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+};
+
+static int hook_unix_stream_connect(struct sock *const sock,
+ struct sock *const other,
+ struct sock *const newsk)
+{
+ 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 (!subject)
+ return 0;
+
+ if (!is_abstract_socket(other))
+ return 0;
+
+ if (!sock_is_scoped(other, subject->domain))
+ 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,
+#ifdef CONFIG_TRACEPOINTS
+ .other_domain_id = get_socket_domain_id(other),
+#endif /* CONFIG_TRACEPOINTS */
+ });
+ return -EPERM;
+}
+
+static int hook_unix_may_send(struct socket *const sock,
+ struct socket *const other)
+{
+ size_t handle_layer;
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), unix_scope,
+ &handle_layer);
+
+ if (!subject)
+ return 0;
+
+ /*
+ * Checks if this datagram socket was already allowed to be connected
+ * to other.
+ */
+ if (unix_peer(sock->sk) == other->sk)
+ return 0;
+
+ if (!is_abstract_socket(other->sk))
+ return 0;
+
+ if (!sock_is_scoped(other->sk, subject->domain))
+ 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,
+#ifdef CONFIG_TRACEPOINTS
+ .other_domain_id = get_socket_domain_id(other->sk),
+#endif /* CONFIG_TRACEPOINTS */
+ });
+ return -EPERM;
+}
+
+static const struct access_masks signal_scope = {
+ .scope = LANDLOCK_SCOPE_SIGNAL,
+};
+
+static int hook_task_kill(struct task_struct *const p,
+ struct kernel_siginfo *const info, const int sig,
+ const struct cred *cred)
+{
+ bool is_scoped;
+ size_t handle_layer;
+#ifdef CONFIG_TRACEPOINTS
+ u64 target_domain_id = 0;
+#endif /* CONFIG_TRACEPOINTS */
+ 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();
+ }
+
+ subject = landlock_get_applicable_subject(cred, signal_scope,
+ &handle_layer);
+
+ /* Quick return for non-landlocked tasks. */
+ if (!subject)
+ return 0;
+
+ scoped_guard(rcu) {
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(p);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
+ signal_scope.scope);
+#ifdef CONFIG_TRACEPOINTS
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_TRACEPOINTS */
+ }
+
+ if (!is_scoped)
+ 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,
+#ifdef CONFIG_TRACEPOINTS
+ .trace_signal = &(struct landlock_signal_trace) {
+ .target_domain_id = target_domain_id,
+ .signal = sig,
+ },
+#endif /* CONFIG_TRACEPOINTS */
+ });
+ return -EPERM;
+}
+
+static int hook_file_send_sigiotask(struct task_struct *tsk,
+ struct fown_struct *fown, int signum)
+{
+ const struct landlock_cred_security *subject;
+ bool is_scoped = false;
+#ifdef CONFIG_TRACEPOINTS
+ u64 target_domain_id = 0;
+#endif /* CONFIG_TRACEPOINTS */
+
+ /* Lock already held by send_sigio() and send_sigurg(). */
+ lockdep_assert_held(&fown->lock);
+ subject = &landlock_file(fown->file)->fown_subject;
+
+ /*
+ * 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;
+
+ /*
+ * Always allow delivery to the file owner's own process, including a
+ * thread-group leader reached through a process-group owner. This
+ * mirrors hook_task_kill()'s same-process exemption and preserves the
+ * guarantee of commit 18eb75f3af40 ("landlock: Always allow signals
+ * between threads of the same process"), which the registration-time
+ * check cannot honor for a process-group target.
+ */
+ if (task_tgid(tsk) == landlock_file(fown->file)->fown_tg)
+ return 0;
+
+ scoped_guard(rcu) {
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(tsk);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
+ signal_scope.scope);
+#ifdef CONFIG_TRACEPOINTS
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_TRACEPOINTS */
+ }
+
+ if (!is_scoped)
+ 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_SECURITY_LANDLOCK_LOG
+ .layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+#ifdef CONFIG_TRACEPOINTS
+ .trace_signal = &(struct landlock_signal_trace) {
+ .target_domain_id = target_domain_id,
+ .signal = signum ? signum : SIGIO,
+ },
+#endif /* CONFIG_TRACEPOINTS */
+ });
+ return -EPERM;
+}
+
+static struct security_hook_list landlock_hooks[] __ro_after_init = {
+ LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
+ LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
+
+ LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect),
+ LSM_HOOK_INIT(unix_may_send, hook_unix_may_send),
+
+ LSM_HOOK_INIT(task_kill, hook_task_kill),
+ LSM_HOOK_INIT(file_send_sigiotask, hook_file_send_sigiotask),
+};
+
+__init void landlock_add_task_hooks(void)
+{
+ security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks),
+ &landlock_lsmid);
+}