summaryrefslogtreecommitdiff
path: root/security/landlock/task.c
diff options
context:
space:
mode:
authorMickaël Salaün <mic@digikod.net>2025-03-20 20:06:58 +0100
committerMickaël Salaün <mic@digikod.net>2025-03-26 13:59:38 +0100
commit33e65b0d3add6bdc731e9298995cbbc979349f51 (patch)
treef669e2b465ef9e386288bdc31154e3e080306937 /security/landlock/task.c
parent14f6c14e9fbde4a802c10c7e770b5ba63853ebc6 (diff)
landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials
Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates that something unexpected happened. For now, only denied access are logged, which means that any AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with "success=no". However, log parsers should check this syscall property because this is the only sign that a request was denied. Indeed, we could have "success=yes" if Landlock would support a "permissive" mode. We could also add a new field to AUDIT_LANDLOCK_DOMAIN for this mode (see following commit). By default, the only logged access requests are those coming from the same executed program that enforced the Landlock restriction on itself. In other words, no audit record are created for a task after it called execve(2). This is required to avoid log spam because programs may only be aware of their own restrictions, but not the inherited ones. Following commits will allow to conditionally generate AUDIT_LANDLOCK_ACCESS records according to dedicated landlock_restrict_self(2)'s flags. The AUDIT_LANDLOCK_ACCESS message contains: - the "domain" ID restricting the action on an object, - the "blockers" that are missing to allow the requested access, - a set of fields identifying the related object (e.g. task identified with "opid" and "ocomm"). The blockers are implicit restrictions (e.g. ptrace), or explicit access rights (e.g. filesystem), or explicit scopes (e.g. signal). This field contains a list of at least one element, each separated with a comma. The initial blocker is "ptrace", which describe all implicit Landlock restrictions related to ptrace (e.g. deny tracing of tasks outside a sandbox). Add audit support to ptrace_access_check and ptrace_traceme hooks. For the ptrace_access_check case, we log the current/parent domain and the child task. For the ptrace_traceme case, we log the parent domain and the current/child task. Indeed, the requester and the target are the current task, but the action would be performed by the parent task. Audit event sample: type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 A following commit adds user documentation. Add KUnit tests to check reading of domain ID relative to layer level. The quick return for non-landlocked tasks is moved from task_ptrace() to each LSM hooks. It is not useful to inline the audit_enabled check because other computation are performed by landlock_log_denial(). Use scoped guards for RCU read-side critical sections. Cc: Günther Noack <gnoack@google.com> Acked-by: Paul Moore <paul@paul-moore.com> Link: https://lore.kernel.org/r/20250320190717.2287696-10-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
Diffstat (limited to 'security/landlock/task.c')
-rw-r--r--security/landlock/task.c96
1 files changed, 73 insertions, 23 deletions
diff --git a/security/landlock/task.c b/security/landlock/task.c
index b89ab9a904eb..30ac4340c62e 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -12,6 +12,7 @@
#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>
@@ -19,6 +20,7 @@
#include <net/af_unix.h>
#include <net/sock.h>
+#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
@@ -41,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;
}
@@ -95,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;
}
/**
@@ -112,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;
}
/**
@@ -131,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))