@@ -147,6 +147,7 @@
#define AUDIT_IPE_CONFIG_CHANGE 1421 /* IPE config change */
#define AUDIT_IPE_POLICY_LOAD 1422 /* IPE policy load */
#define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */
+#define AUDIT_LANDLOCK_DOMAIN 1424 /* Landlock domain status */
#define AUDIT_FIRST_KERN_ANOM_MSG 1700
#define AUDIT_LAST_KERN_ANOM_MSG 1799
@@ -8,6 +8,8 @@
#include <kunit/test.h>
#include <linux/audit.h>
#include <linux/lsm_audit.h>
+#include <linux/pid.h>
+#include <linux/uidgid.h>
#include "audit.h"
#include "cred.h"
@@ -31,6 +33,40 @@ static void log_blockers(struct audit_buffer *const ab,
audit_log_format(ab, "%s", get_blocker(type));
}
+static void log_node(struct landlock_hierarchy *const node)
+{
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!node))
+ return;
+
+ /* Ignores already logged domains. */
+ if (READ_ONCE(node->log_status) == LANDLOCK_LOG_RECORDED)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ WARN_ON_ONCE(node->id == 0);
+ audit_log_format(
+ ab,
+ "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
+ node->id, pid_nr(node->details->pid),
+ from_kuid(&init_user_ns, node->details->cred->uid));
+ audit_log_untrustedstring(ab, node->details->exe_path);
+ audit_log_format(ab, " comm=");
+ audit_log_untrustedstring(ab, node->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(node->log_status, LANDLOCK_LOG_RECORDED);
+}
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
{
@@ -106,12 +142,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;
@@ -125,6 +169,46 @@ 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 if it is the first time. */
+ log_node(youngest_denied);
+}
+
+/**
+ * landlock_log_drop_domain - Create an audit record when a domain is deleted
+ *
+ * @domain: The domain being deleted.
+ *
+ * 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.
+ *
+ * This record is not directly tied to a syscall entry.
+ *
+ * Called by the cred_free() hook, in an uninterruptible context.
+ */
+void landlock_log_drop_domain(const struct landlock_ruleset *const domain)
+{
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!domain->hierarchy))
+ return;
+
+ if (!audit_enabled)
+ return;
+
+ /* Ignores domains that were not logged. */
+ if (READ_ONCE(domain->hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
+ domain->hierarchy->id,
+ atomic64_read(&domain->hierarchy->num_denials));
+ audit_log_end(ab);
}
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
@@ -37,11 +37,18 @@ struct landlock_request {
#ifdef CONFIG_AUDIT
+void landlock_log_drop_domain(const struct landlock_ruleset *const domain);
+
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
#else /* CONFIG_AUDIT */
+static inline void
+landlock_log_drop_domain(const struct landlock_ruleset *const domain)
+{
+}
+
static inline void
landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
@@ -7,21 +7,122 @@
* Copyright © 2024-2025 Microsoft Corporation
*/
+#include <linux/cred.h>
+#include <linux/file.h>
+#include <linux/mm.h>
+#include <linux/path.h>
+#include <linux/pid.h>
+#include <linux/sched.h>
+
#include "domain.h"
+#include "fs.h"
#include "id.h"
#ifdef CONFIG_AUDIT
+/**
+ * get_current_exe - Get the current's executable path, if any
+ *
+ * @exe_str: Returned pointer to a path string with a lifetime tied to the
+ * returned buffer, if any.
+ * @exe_size: Returned size of @exe_str (including the trailing null
+ * character), if any.
+ *
+ * Returns: A pointer to an allocated buffer where @exe_str point to, %NULL if
+ * there is no executable path, or an error otherwise.
+ */
+static const void *get_current_exe(const char **const exe_str,
+ size_t *const exe_size)
+{
+ const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE;
+ struct mm_struct *mm = current->mm;
+ struct file *file __free(fput) = NULL;
+ char *buffer __free(kfree) = NULL;
+ const char *exe;
+ size_t size;
+
+ if (!mm)
+ return NULL;
+
+ file = get_mm_exe_file(mm);
+ if (!file)
+ return NULL;
+
+ buffer = kmalloc(buffer_size, GFP_KERNEL);
+ if (!buffer)
+ return ERR_PTR(-ENOMEM);
+
+ exe = d_path(&file->f_path, buffer, buffer_size);
+ if (WARN_ON_ONCE(IS_ERR(exe)))
+ /* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */
+ return ERR_CAST(exe);
+
+ size = buffer + buffer_size - exe;
+ if (WARN_ON_ONCE(size <= 0))
+ return ERR_PTR(-ENAMETOOLONG);
+
+ *exe_size = size;
+ *exe_str = exe;
+ return no_free_ptr(buffer);
+}
+
+/*
+ * Returns: A newly allocated object describing a domain, or an error
+ * otherwise.
+ */
+static struct landlock_details *get_current_details(void)
+{
+ /* Cf. audit_log_d_path_exe() */
+ static const char null_path[] = "(null)";
+ const char *path_str = null_path;
+ size_t path_size = sizeof(null_path);
+ const void *buffer __free(kfree) = NULL;
+ struct landlock_details *details;
+
+ buffer = get_current_exe(&path_str, &path_size);
+ if (IS_ERR(buffer))
+ return ERR_CAST(buffer);
+
+ /*
+ * Create the new details according to the path's length. Do not
+ * allocate with GFP_KERNEL_ACCOUNT because it is independent from the
+ * caller.
+ */
+ details =
+ kzalloc(struct_size(details, exe_path, path_size), GFP_KERNEL);
+ if (!details)
+ return ERR_PTR(-ENOMEM);
+
+ memcpy(details->exe_path, path_str, path_size);
+ WARN_ON_ONCE(current_cred() != current_real_cred());
+ details->cred = get_current_cred();
+ details->pid = get_pid(task_pid(current));
+ get_task_comm(details->comm, current);
+ return details;
+}
+
/**
* landlock_init_hierarchy_log - Partially initialize landlock_hierarchy
*
* @hierarchy: The hierarchy to initialize.
*
+ * The current task is referenced as the domain restrictor. The subjective
+ * credentials must not be in an overridden state.
+ *
* @hierarchy->parent and @hierarchy->usage should already be set.
*/
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
{
+ struct landlock_details *details;
+
+ details = get_current_details();
+ if (IS_ERR(details))
+ return PTR_ERR(details);
+
+ hierarchy->details = details;
hierarchy->id = landlock_get_id_range(1);
+ hierarchy->log_status = LANDLOCK_LOG_PENDING;
+ atomic64_set(&hierarchy->num_denials, 0);
return 0;
}
@@ -10,8 +10,61 @@
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
+#include <linux/cred.h>
+#include <linux/limits.h>
#include <linux/mm.h>
+#include <linux/path.h>
+#include <linux/pid.h>
#include <linux/refcount.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+
+enum landlock_log_status {
+ LANDLOCK_LOG_PENDING = 0,
+ LANDLOCK_LOG_RECORDED,
+};
+
+/**
+ * struct landlock_details - Domain's creation information
+ *
+ * Rarely accessed, mainly when logging the first domain's denial.
+ *
+ * The contained pointers are initialized at the domain creation time and never
+ * changed again. Contrary to most other Landlock object types, this one is
+ * not allocated with GFP_KERNEL_ACCOUNT because its size may not be under the
+ * caller's control (e.g. unknown exe_path) and the data is not explicitly
+ * requested nor used by tasks.
+ */
+struct landlock_details {
+ /**
+ * @cred: Credential of the task that initially restricted itself, at
+ * creation time.
+ */
+ const struct cred *cred;
+ /**
+ * @pid: PID of the task that initially restricted itself. It still
+ * identifies the same task.
+ */
+ struct pid *pid;
+ /**
+ * @comm: Command line of the task that initially restricted itself, at
+ * creation time. Always NULL terminated.
+ */
+ char comm[TASK_COMM_LEN];
+ /**
+ * @exe_path: Executable path of the task that initially restricted
+ * itself, at creation time. Always NULL terminated, and never greater
+ * than LANDLOCK_PATH_MAX_SIZE.
+ */
+ char exe_path[];
+};
+
+/* Adds 11 extra characters for the potential " (deleted)" suffix. */
+#define LANDLOCK_PATH_MAX_SIZE (PATH_MAX + 11)
+
+/* Makes sure the greatest landlock_details can be allocated. */
+static_assert(struct_size_t(struct landlock_details, exe_path,
+ LANDLOCK_PATH_MAX_SIZE) <= KMALLOC_MAX_SIZE);
/**
* struct landlock_hierarchy - Node in a domain hierarchy
@@ -29,10 +82,25 @@ struct landlock_hierarchy {
refcount_t usage;
#ifdef CONFIG_AUDIT
+ /**
+ * @log_status: Whether this domain should be logged or not. Because
+ * concurrent log entries may be created at the same time, it is still
+ * possible to have several domain records of the same domain.
+ */
+ enum landlock_log_status log_status;
+ /**
+ * @num_denials: Number of access requests denied by this domain.
+ * Masked (i.e. never logged) denials are still counted.
+ */
+ atomic64_t num_denials;
/**
* @id: Landlock domain ID, sets once at domain creation time.
*/
u64 id;
+ /**
+ * @details: Information about the related domain.
+ */
+ const struct landlock_details *details;
#endif /* CONFIG_AUDIT */
};
@@ -510,6 +510,9 @@ static void free_ruleset_work(struct work_struct *const work)
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
{
if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
+ /* Logs with the current context. */
+ landlock_log_drop_domain(ruleset);
+
INIT_WORK(&ruleset->work_free, free_ruleset_work);
schedule_work(&ruleset->work_free);
}
@@ -521,6 +524,9 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
* @parent: Parent domain.
* @ruleset: New ruleset to be merged.
*
+ * The current task is requesting to be restricted. The subjective credentials
+ * must not be in an overridden state. cf. landlock_init_hierarchy_log().
+ *
* Returns the intersection of @parent and @ruleset, or returns @parent if
* @ruleset is empty, or returns a duplicate of @ruleset if @parent is empty.
*/