diff options
-rw-r--r-- | include/uapi/linux/audit.h | 1 | ||||
-rw-r--r-- | security/landlock/audit.c | 89 | ||||
-rw-r--r-- | security/landlock/audit.h | 7 | ||||
-rw-r--r-- | security/landlock/domain.c | 101 | ||||
-rw-r--r-- | security/landlock/domain.h | 86 | ||||
-rw-r--r-- | security/landlock/ruleset.c | 3 | ||||
-rw-r--r-- | security/landlock/ruleset.h | 3 |
7 files changed, 286 insertions, 4 deletions
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h index 5dd53f416a4a..9a4ecc9f6dc5 100644 --- a/include/uapi/linux/audit.h +++ b/include/uapi/linux/audit.h @@ -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 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 diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 3a6ec7c8e7c3..40ff230316c4 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -36,12 +36,19 @@ struct landlock_request { #ifdef CONFIG_AUDIT +void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy); + 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_hierarchy *const hierarchy) +{ +} + +static inline void landlock_log_denial(const struct landlock_cred_security *const subject, const struct landlock_request *const request) { diff --git a/security/landlock/domain.c b/security/landlock/domain.c index e6367933303c..3c1715e4b1c9 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -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 <linux/uidgid.h> + #include "domain.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; + ssize_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->pid = get_pid(task_pid(current)); + details->uid = from_kuid(&init_user_ns, current_uid()); + 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 that is enforcing the + * restriction. 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; } diff --git a/security/landlock/domain.h b/security/landlock/domain.h index f8df7103bd3d..2c6f04da3628 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -10,8 +10,62 @@ #ifndef _SECURITY_LANDLOCK_DOMAIN_H #define _SECURITY_LANDLOCK_DOMAIN_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> + +#include "audit.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 { + /** + * @pid: PID of the task that initially restricted itself. It still + * identifies the same task. Keeping a reference to this PID ensures that + * it will not be recycled. + */ + struct pid *pid; + /** + * @uid: UID of the task that initially restricted itself, at creation time. + */ + uid_t uid; + /** + * @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 @@ -30,9 +84,24 @@ struct landlock_hierarchy { #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 */ }; @@ -40,6 +109,16 @@ struct landlock_hierarchy { int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); +static inline void +landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy) +{ + if (WARN_ON_ONCE(!hierarchy || !hierarchy->details)) + return; + + put_pid(hierarchy->details->pid); + kfree(hierarchy->details); +} + #else /* CONFIG_AUDIT */ static inline int @@ -48,6 +127,11 @@ landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) return 0; } +static inline void +landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy) +{ +} + #endif /* CONFIG_AUDIT */ static inline void @@ -62,6 +146,8 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { const struct landlock_hierarchy *const freeme = hierarchy; + landlock_log_drop_domain(hierarchy); + landlock_free_hierarchy_details(hierarchy); hierarchy = hierarchy->parent; kfree(freeme); } diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index f273a40e9780..ce7940efea51 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -521,6 +521,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. */ diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index bbb5996545d2..28af42d66261 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -17,10 +17,11 @@ #include <linux/workqueue.h> #include "access.h" -#include "domain.h" #include "limits.h" #include "object.h" +struct landlock_hierarchy; + /** * struct landlock_layer - Access rights for a given layer */ |