summaryrefslogtreecommitdiff
path: root/security/landlock/audit.c
diff options
context:
space:
mode:
authorMickaël Salaün <mic@digikod.net>2025-03-20 20:06:59 +0100
committerMickaël Salaün <mic@digikod.net>2025-03-26 13:59:38 +0100
commit1d636984e088b17e8587eb5ed9d9d7a80b656c4c (patch)
treeec664e60f664a109261a33772f2166e0a1d89547 /security/landlock/audit.c
parent33e65b0d3add6bdc731e9298995cbbc979349f51 (diff)
landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status
Asynchronously log domain information when it first denies an access. This minimize the amount of generated logs, which makes it possible to always log denials for the current execution since they should not happen. These records are identified with the new AUDIT_LANDLOCK_DOMAIN type. The AUDIT_LANDLOCK_DOMAIN message contains: - the "domain" ID which is described; - the "status" which can either be "allocated" or "deallocated"; - the "mode" which is for now only "enforcing"; - for the "allocated" status, a minimal set of properties to easily identify the task that loaded the domain's policy with landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and command line ("comm"); - for the "deallocated" state, the number of "denials" accounted to this domain, which is at least 1. This requires each domain to save these task properties at creation time in the new struct landlock_details. A reference to the PID is kept for the lifetime of the domain to avoid race conditions when investigating the related task. The executable path is resolved and stored to not keep a reference to the filesystem and block related actions. All these metadata are stored for the lifetime of the related domain and should then be minimal. The required memory is not accounted to the task calling landlock_restrict_self(2) contrary to most other Landlock allocations (see related comment). The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS record for the same domain, which is always followed by AUDIT_SYSCALL and AUDIT_PROCTITLE. This is in line with the audit logic to first record the cause of an event, and then add context with other types of record. Audit event sample for a first denial: type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer" type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Audit event sample for a following denial: type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Log domain deletion with the "deallocated" state when a domain was previously logged. This makes it possible for log parsers to free potential resources when a domain ID will never show again. The number of denied access requests is useful to easily check how many access requests a domain blocked and potentially if some of them are missing in logs because of audit rate limiting, audit rules, or Landlock log configuration flags (see following commit). Audit event sample for a deletion of a domain that denied something: type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2 Cc: Günther Noack <gnoack@google.com> Acked-by: Paul Moore <paul@paul-moore.com> Link: https://lore.kernel.org/r/20250320190717.2287696-11-mic@digikod.net [mic: Update comment and GFP flag for landlock_log_drop_domain()] Signed-off-by: Mickaël Salaün <mic@digikod.net>
Diffstat (limited to 'security/landlock/audit.c')
-rw-r--r--security/landlock/audit.c89
1 files changed, 86 insertions, 3 deletions
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index b0752263012a..39118f7ff990 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -8,6 +8,7 @@
#include <kunit/test.h>
#include <linux/audit.h>
#include <linux/lsm_audit.h>
+#include <linux/pid.h>
#include "audit.h"
#include "cred.h"
@@ -32,6 +33,38 @@ static void log_blockers(struct audit_buffer *const ab,
audit_log_format(ab, "%s", get_blocker(type));
}
+static void log_domain(struct landlock_hierarchy *const hierarchy)
+{
+ struct audit_buffer *ab;
+
+ /* Ignores already logged domains. */
+ if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED)
+ return;
+
+ /* Uses consistent allocation flags wrt common_lsm_audit(). */
+ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ WARN_ON_ONCE(hierarchy->id == 0);
+ audit_log_format(
+ ab,
+ "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
+ hierarchy->id, pid_nr(hierarchy->details->pid),
+ hierarchy->details->uid);
+ audit_log_untrustedstring(ab, hierarchy->details->exe_path);
+ audit_log_format(ab, " comm=");
+ audit_log_untrustedstring(ab, hierarchy->details->comm);
+ audit_log_end(ab);
+
+ /*
+ * There may be race condition leading to logging of the same domain
+ * several times but that is OK.
+ */
+ WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
+}
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
@@ -110,12 +143,20 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
if (!is_valid_request(request))
return;
- if (!audit_enabled)
- return;
-
youngest_layer = request->layer_plus_one - 1;
youngest_denied = get_hierarchy(subject->domain, youngest_layer);
+ /*
+ * Consistently keeps track of the number of denied access requests
+ * even if audit is currently disabled, or if audit rules currently
+ * exclude this record type, or if landlock_restrict_self(2)'s flags
+ * quiet logs.
+ */
+ atomic64_inc(&youngest_denied->num_denials);
+
+ if (!audit_enabled)
+ return;
+
/* Ignores denials after an execution. */
if (!(subject->domain_exec & (1 << youngest_layer)))
return;
@@ -130,6 +171,48 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
log_blockers(ab, request->type);
audit_log_lsm_data(ab, &request->audit);
audit_log_end(ab);
+
+ /* Logs this domain the first time it shows in log. */
+ log_domain(youngest_denied);
+}
+
+/**
+ * landlock_log_drop_domain - Create an audit record on domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Only domains which previously appeared in the audit logs are logged again.
+ * This is useful to know when a domain will never show again in the audit log.
+ *
+ * Called in a work queue scheduled by landlock_put_ruleset_deferred() called
+ * by hook_cred_free().
+ */
+void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!hierarchy))
+ return;
+
+ if (!audit_enabled)
+ return;
+
+ /* Ignores domains that were not logged. */
+ if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
+ return;
+
+ /*
+ * If logging of domain allocation succeeded, warns about failure to log
+ * domain deallocation to highlight unbalanced domain lifetime logs.
+ */
+ ab = audit_log_start(audit_context(), GFP_KERNEL,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
+ hierarchy->id, atomic64_read(&hierarchy->num_denials));
+ audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST