Skip to content

Commit 1d63698

Browse files
committed
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>
1 parent 33e65b0 commit 1d63698

7 files changed

Lines changed: 286 additions & 4 deletions

File tree

include/uapi/linux/audit.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
#define AUDIT_IPE_CONFIG_CHANGE 1421 /* IPE config change */
148148
#define AUDIT_IPE_POLICY_LOAD 1422 /* IPE policy load */
149149
#define AUDIT_LANDLOCK_ACCESS 1423 /* Landlock denial */
150+
#define AUDIT_LANDLOCK_DOMAIN 1424 /* Landlock domain status */
150151

151152
#define AUDIT_FIRST_KERN_ANOM_MSG 1700
152153
#define AUDIT_LAST_KERN_ANOM_MSG 1799

security/landlock/audit.c

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <kunit/test.h>
99
#include <linux/audit.h>
1010
#include <linux/lsm_audit.h>
11+
#include <linux/pid.h>
1112

1213
#include "audit.h"
1314
#include "cred.h"
@@ -32,6 +33,38 @@ static void log_blockers(struct audit_buffer *const ab,
3233
audit_log_format(ab, "%s", get_blocker(type));
3334
}
3435

36+
static void log_domain(struct landlock_hierarchy *const hierarchy)
37+
{
38+
struct audit_buffer *ab;
39+
40+
/* Ignores already logged domains. */
41+
if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED)
42+
return;
43+
44+
/* Uses consistent allocation flags wrt common_lsm_audit(). */
45+
ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
46+
AUDIT_LANDLOCK_DOMAIN);
47+
if (!ab)
48+
return;
49+
50+
WARN_ON_ONCE(hierarchy->id == 0);
51+
audit_log_format(
52+
ab,
53+
"domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=",
54+
hierarchy->id, pid_nr(hierarchy->details->pid),
55+
hierarchy->details->uid);
56+
audit_log_untrustedstring(ab, hierarchy->details->exe_path);
57+
audit_log_format(ab, " comm=");
58+
audit_log_untrustedstring(ab, hierarchy->details->comm);
59+
audit_log_end(ab);
60+
61+
/*
62+
* There may be race condition leading to logging of the same domain
63+
* several times but that is OK.
64+
*/
65+
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
66+
}
67+
3568
static struct landlock_hierarchy *
3669
get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
3770
{
@@ -110,12 +143,20 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
110143
if (!is_valid_request(request))
111144
return;
112145

113-
if (!audit_enabled)
114-
return;
115-
116146
youngest_layer = request->layer_plus_one - 1;
117147
youngest_denied = get_hierarchy(subject->domain, youngest_layer);
118148

149+
/*
150+
* Consistently keeps track of the number of denied access requests
151+
* even if audit is currently disabled, or if audit rules currently
152+
* exclude this record type, or if landlock_restrict_self(2)'s flags
153+
* quiet logs.
154+
*/
155+
atomic64_inc(&youngest_denied->num_denials);
156+
157+
if (!audit_enabled)
158+
return;
159+
119160
/* Ignores denials after an execution. */
120161
if (!(subject->domain_exec & (1 << youngest_layer)))
121162
return;
@@ -130,6 +171,48 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
130171
log_blockers(ab, request->type);
131172
audit_log_lsm_data(ab, &request->audit);
132173
audit_log_end(ab);
174+
175+
/* Logs this domain the first time it shows in log. */
176+
log_domain(youngest_denied);
177+
}
178+
179+
/**
180+
* landlock_log_drop_domain - Create an audit record on domain deallocation
181+
*
182+
* @hierarchy: The domain's hierarchy being deallocated.
183+
*
184+
* Only domains which previously appeared in the audit logs are logged again.
185+
* This is useful to know when a domain will never show again in the audit log.
186+
*
187+
* Called in a work queue scheduled by landlock_put_ruleset_deferred() called
188+
* by hook_cred_free().
189+
*/
190+
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
191+
{
192+
struct audit_buffer *ab;
193+
194+
if (WARN_ON_ONCE(!hierarchy))
195+
return;
196+
197+
if (!audit_enabled)
198+
return;
199+
200+
/* Ignores domains that were not logged. */
201+
if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
202+
return;
203+
204+
/*
205+
* If logging of domain allocation succeeded, warns about failure to log
206+
* domain deallocation to highlight unbalanced domain lifetime logs.
207+
*/
208+
ab = audit_log_start(audit_context(), GFP_KERNEL,
209+
AUDIT_LANDLOCK_DOMAIN);
210+
if (!ab)
211+
return;
212+
213+
audit_log_format(ab, "domain=%llx status=deallocated denials=%llu",
214+
hierarchy->id, atomic64_read(&hierarchy->num_denials));
215+
audit_log_end(ab);
133216
}
134217

135218
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST

security/landlock/audit.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ struct landlock_request {
3636

3737
#ifdef CONFIG_AUDIT
3838

39+
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
40+
3941
void landlock_log_denial(const struct landlock_cred_security *const subject,
4042
const struct landlock_request *const request);
4143

4244
#else /* CONFIG_AUDIT */
4345

46+
static inline void
47+
landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
48+
{
49+
}
50+
4451
static inline void
4552
landlock_log_denial(const struct landlock_cred_security *const subject,
4653
const struct landlock_request *const request)

security/landlock/domain.c

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,122 @@
77
* Copyright © 2024-2025 Microsoft Corporation
88
*/
99

10+
#include <linux/cred.h>
11+
#include <linux/file.h>
12+
#include <linux/mm.h>
13+
#include <linux/path.h>
14+
#include <linux/pid.h>
15+
#include <linux/sched.h>
16+
#include <linux/uidgid.h>
17+
1018
#include "domain.h"
1119
#include "id.h"
1220

1321
#ifdef CONFIG_AUDIT
1422

23+
/**
24+
* get_current_exe - Get the current's executable path, if any
25+
*
26+
* @exe_str: Returned pointer to a path string with a lifetime tied to the
27+
* returned buffer, if any.
28+
* @exe_size: Returned size of @exe_str (including the trailing null
29+
* character), if any.
30+
*
31+
* Returns: A pointer to an allocated buffer where @exe_str point to, %NULL if
32+
* there is no executable path, or an error otherwise.
33+
*/
34+
static const void *get_current_exe(const char **const exe_str,
35+
size_t *const exe_size)
36+
{
37+
const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE;
38+
struct mm_struct *mm = current->mm;
39+
struct file *file __free(fput) = NULL;
40+
char *buffer __free(kfree) = NULL;
41+
const char *exe;
42+
ssize_t size;
43+
44+
if (!mm)
45+
return NULL;
46+
47+
file = get_mm_exe_file(mm);
48+
if (!file)
49+
return NULL;
50+
51+
buffer = kmalloc(buffer_size, GFP_KERNEL);
52+
if (!buffer)
53+
return ERR_PTR(-ENOMEM);
54+
55+
exe = d_path(&file->f_path, buffer, buffer_size);
56+
if (WARN_ON_ONCE(IS_ERR(exe)))
57+
/* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */
58+
return ERR_CAST(exe);
59+
60+
size = buffer + buffer_size - exe;
61+
if (WARN_ON_ONCE(size <= 0))
62+
return ERR_PTR(-ENAMETOOLONG);
63+
64+
*exe_size = size;
65+
*exe_str = exe;
66+
return no_free_ptr(buffer);
67+
}
68+
69+
/*
70+
* Returns: A newly allocated object describing a domain, or an error
71+
* otherwise.
72+
*/
73+
static struct landlock_details *get_current_details(void)
74+
{
75+
/* Cf. audit_log_d_path_exe() */
76+
static const char null_path[] = "(null)";
77+
const char *path_str = null_path;
78+
size_t path_size = sizeof(null_path);
79+
const void *buffer __free(kfree) = NULL;
80+
struct landlock_details *details;
81+
82+
buffer = get_current_exe(&path_str, &path_size);
83+
if (IS_ERR(buffer))
84+
return ERR_CAST(buffer);
85+
86+
/*
87+
* Create the new details according to the path's length. Do not
88+
* allocate with GFP_KERNEL_ACCOUNT because it is independent from the
89+
* caller.
90+
*/
91+
details =
92+
kzalloc(struct_size(details, exe_path, path_size), GFP_KERNEL);
93+
if (!details)
94+
return ERR_PTR(-ENOMEM);
95+
96+
memcpy(details->exe_path, path_str, path_size);
97+
WARN_ON_ONCE(current_cred() != current_real_cred());
98+
details->pid = get_pid(task_pid(current));
99+
details->uid = from_kuid(&init_user_ns, current_uid());
100+
get_task_comm(details->comm, current);
101+
return details;
102+
}
103+
15104
/**
16105
* landlock_init_hierarchy_log - Partially initialize landlock_hierarchy
17106
*
18107
* @hierarchy: The hierarchy to initialize.
19108
*
109+
* The current task is referenced as the domain that is enforcing the
110+
* restriction. The subjective credentials must not be in an overridden state.
111+
*
20112
* @hierarchy->parent and @hierarchy->usage should already be set.
21113
*/
22114
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
23115
{
116+
struct landlock_details *details;
117+
118+
details = get_current_details();
119+
if (IS_ERR(details))
120+
return PTR_ERR(details);
121+
122+
hierarchy->details = details;
24123
hierarchy->id = landlock_get_id_range(1);
124+
hierarchy->log_status = LANDLOCK_LOG_PENDING;
125+
atomic64_set(&hierarchy->num_denials, 0);
25126
return 0;
26127
}
27128

security/landlock/domain.h

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,62 @@
1010
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
1111
#define _SECURITY_LANDLOCK_DOMAIN_H
1212

13+
#include <linux/limits.h>
1314
#include <linux/mm.h>
15+
#include <linux/path.h>
16+
#include <linux/pid.h>
1417
#include <linux/refcount.h>
18+
#include <linux/sched.h>
19+
#include <linux/slab.h>
20+
21+
#include "audit.h"
22+
23+
enum landlock_log_status {
24+
LANDLOCK_LOG_PENDING = 0,
25+
LANDLOCK_LOG_RECORDED,
26+
};
27+
28+
/**
29+
* struct landlock_details - Domain's creation information
30+
*
31+
* Rarely accessed, mainly when logging the first domain's denial.
32+
*
33+
* The contained pointers are initialized at the domain creation time and never
34+
* changed again. Contrary to most other Landlock object types, this one is
35+
* not allocated with GFP_KERNEL_ACCOUNT because its size may not be under the
36+
* caller's control (e.g. unknown exe_path) and the data is not explicitly
37+
* requested nor used by tasks.
38+
*/
39+
struct landlock_details {
40+
/**
41+
* @pid: PID of the task that initially restricted itself. It still
42+
* identifies the same task. Keeping a reference to this PID ensures that
43+
* it will not be recycled.
44+
*/
45+
struct pid *pid;
46+
/**
47+
* @uid: UID of the task that initially restricted itself, at creation time.
48+
*/
49+
uid_t uid;
50+
/**
51+
* @comm: Command line of the task that initially restricted itself, at
52+
* creation time. Always NULL terminated.
53+
*/
54+
char comm[TASK_COMM_LEN];
55+
/**
56+
* @exe_path: Executable path of the task that initially restricted
57+
* itself, at creation time. Always NULL terminated, and never greater
58+
* than LANDLOCK_PATH_MAX_SIZE.
59+
*/
60+
char exe_path[];
61+
};
62+
63+
/* Adds 11 extra characters for the potential " (deleted)" suffix. */
64+
#define LANDLOCK_PATH_MAX_SIZE (PATH_MAX + 11)
65+
66+
/* Makes sure the greatest landlock_details can be allocated. */
67+
static_assert(struct_size_t(struct landlock_details, exe_path,
68+
LANDLOCK_PATH_MAX_SIZE) <= KMALLOC_MAX_SIZE);
1569

1670
/**
1771
* struct landlock_hierarchy - Node in a domain hierarchy
@@ -29,17 +83,42 @@ struct landlock_hierarchy {
2983
refcount_t usage;
3084

3185
#ifdef CONFIG_AUDIT
86+
/**
87+
* @log_status: Whether this domain should be logged or not. Because
88+
* concurrent log entries may be created at the same time, it is still
89+
* possible to have several domain records of the same domain.
90+
*/
91+
enum landlock_log_status log_status;
92+
/**
93+
* @num_denials: Number of access requests denied by this domain.
94+
* Masked (i.e. never logged) denials are still counted.
95+
*/
96+
atomic64_t num_denials;
3297
/**
3398
* @id: Landlock domain ID, sets once at domain creation time.
3499
*/
35100
u64 id;
101+
/**
102+
* @details: Information about the related domain.
103+
*/
104+
const struct landlock_details *details;
36105
#endif /* CONFIG_AUDIT */
37106
};
38107

39108
#ifdef CONFIG_AUDIT
40109

41110
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
42111

112+
static inline void
113+
landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
114+
{
115+
if (WARN_ON_ONCE(!hierarchy || !hierarchy->details))
116+
return;
117+
118+
put_pid(hierarchy->details->pid);
119+
kfree(hierarchy->details);
120+
}
121+
43122
#else /* CONFIG_AUDIT */
44123

45124
static inline int
@@ -48,6 +127,11 @@ landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
48127
return 0;
49128
}
50129

130+
static inline void
131+
landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
132+
{
133+
}
134+
51135
#endif /* CONFIG_AUDIT */
52136

53137
static inline void
@@ -62,6 +146,8 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
62146
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
63147
const struct landlock_hierarchy *const freeme = hierarchy;
64148

149+
landlock_log_drop_domain(hierarchy);
150+
landlock_free_hierarchy_details(hierarchy);
65151
hierarchy = hierarchy->parent;
66152
kfree(freeme);
67153
}

0 commit comments

Comments
 (0)