diff options
Diffstat (limited to 'security/landlock/task.c')
| -rw-r--r-- | security/landlock/task.c | 540 |
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); +} |
