Linux Trace Kernel
 help / color / mirror / Atom feed
* [PATCH 3/4] tracing/inject: Validate entry allocation size
From: Li Qiang @ 2026-07-22  6:10 UTC (permalink / raw)
  To: rostedt, mhiramat; +Cc: linux-trace-kernel, linux-kernel, Li Qiang, stable
In-Reply-To: <20260722061040.112747-1-liqiang01@kylinos.cn>

trace_get_entry_size() calculated the allocation from signed field offsets
and sizes without validating their sum. A malformed event could use a
negative range or overflow the calculation, allocate too little memory, and
then write past it while initializing or populating the entry. Events with
no fields also allocated less than a trace_entry.

Start at sizeof(struct trace_entry), validate each field range, and reserve
room for the trailing NUL. Propagate sizing errors to parse_entry() before
it initializes the allocation.

Fixes: 6c3edaf9fd6a ("tracing: Introduce trace event injection")
Cc: stable@vger.kernel.org
Signed-off-by: Li Qiang <liqiang01@kylinos.cn>
---
 kernel/trace/trace_events_inject.c | 31 ++++++++++++++++++++++++------
 1 file changed, 25 insertions(+), 6 deletions(-)

diff --git a/kernel/trace/trace_events_inject.c b/kernel/trace/trace_events_inject.c
index a8f076809db4..b8b141c00d5c 100644
--- a/kernel/trace/trace_events_inject.c
+++ b/kernel/trace/trace_events_inject.c
@@ -135,27 +135,43 @@ parse_field(char *str, struct trace_event_call *call,
 	return -EINVAL;
 }
 
-static int trace_get_entry_size(struct trace_event_call *call)
+static int trace_get_entry_size(struct trace_event_call *call, int *entry_size)
 {
 	struct ftrace_event_field *field;
 	struct list_head *head;
-	int size = 0;
+	int field_size;
+	int size = sizeof(struct trace_entry);
 
 	head = trace_get_fields(call);
 	list_for_each_entry(field, head, link) {
-		if (field->size + field->offset > size)
-			size = field->size + field->offset;
+		if (field->offset < 0 || field->size < 0 ||
+		    field->size > INT_MAX - field->offset)
+			return -E2BIG;
+
+		field_size = field->size + field->offset;
+		if (field_size > size)
+			size = field_size;
 	}
 
-	return size;
+	/* trace_alloc_entry() reserves an extra NUL byte. */
+	if (size == INT_MAX)
+		return -E2BIG;
+
+	*entry_size = size;
+	return 0;
 }
 
 static void *trace_alloc_entry(struct trace_event_call *call, int *size)
 {
-	int entry_size = trace_get_entry_size(call);
+	int entry_size;
 	struct ftrace_event_field *field;
 	struct list_head *head;
 	void *entry = NULL;
+	int ret;
+
+	ret = trace_get_entry_size(call, &entry_size);
+	if (ret)
+		return ERR_PTR(ret);
 
 	/* We need an extra '\0' at the end. */
 	entry = kzalloc(entry_size + 1, GFP_KERNEL);
@@ -202,6 +218,9 @@ static int parse_entry(char *str, struct trace_event_call *call, void **pentry)
 	int len;
 
 	entry = trace_alloc_entry(call, &entry_size);
+	if (IS_ERR(entry))
+		return PTR_ERR(entry);
+
 	*pentry = entry;
 	if (!entry)
 		return -ENOMEM;
-- 
2.43.0


^ permalink raw reply related

* [PATCH 2/4] tracing/user_events: Validate explicit struct field sizes
From: Li Qiang @ 2026-07-22  6:10 UTC (permalink / raw)
  To: rostedt, mhiramat; +Cc: linux-trace-kernel, linux-kernel, Li Qiang, stable
In-Reply-To: <20260722061040.112747-1-liqiang01@kylinos.cn>

User event declarations permit an explicit size for a struct field. The
parser accumulated that size in an unsigned offset, then assigned the
parsed unsigned value directly to signed field metadata. Oversized
declarations or cumulative offsets could wrap or become invalid signed
values.

Validate an explicit size is representable as int before storing it. Keep
the running offset signed and reject additions exceeding INT_MAX, so
invalid field layouts are rejected during declaration parsing.

Fixes: 7f5a08c79df3 ("user_events: Add minimal support for trace_event into ftrace")
Cc: stable@vger.kernel.org
Signed-off-by: Li Qiang <liqiang01@kylinos.cn>
---
 kernel/trace/trace_events_user.c | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/kernel/trace/trace_events_user.c b/kernel/trace/trace_events_user.c
index 8c82ecb735f4..fd5b3946921c 100644
--- a/kernel/trace/trace_events_user.c
+++ b/kernel/trace/trace_events_user.c
@@ -1197,10 +1197,12 @@ static int user_event_add_field(struct user_event *user, const char *type,
  * Format: type name [size]
  */
 static int user_event_parse_field(char *field, struct user_event *user,
-				  u32 *offset)
+				  int *offset)
 {
 	char *part, *type, *name;
-	u32 depth = 0, saved_offset = *offset;
+	u32 depth = 0;
+	unsigned int field_size;
+	int saved_offset = *offset;
 	int len, size = -EINVAL;
 	bool is_struct = false;
 
@@ -1261,8 +1263,11 @@ static int user_event_parse_field(char *field, struct user_event *user,
 			if (!is_struct)
 				return -EINVAL;
 
-			if (kstrtou32(part, 10, &size))
+			if (kstrtouint(part, 10, &field_size))
 				return -EINVAL;
+			if (field_size > INT_MAX)
+				return -E2BIG;
+			size = field_size;
 			break;
 		default:
 			return -EINVAL;
@@ -1281,6 +1286,9 @@ static int user_event_parse_field(char *field, struct user_event *user,
 	if (size < 0)
 		return size;
 
+	if (size > INT_MAX - saved_offset)
+		return -E2BIG;
+
 	*offset = saved_offset + size;
 
 	return user_event_add_field(user, type, name, saved_offset, size,
@@ -1290,7 +1298,7 @@ static int user_event_parse_field(char *field, struct user_event *user,
 static int user_event_parse_fields(struct user_event *user, char *args)
 {
 	char *field;
-	u32 offset = sizeof(struct trace_entry);
+	int offset = sizeof(struct trace_entry);
 	int ret = -EINVAL;
 
 	if (args == NULL)
-- 
2.43.0


^ permalink raw reply related

* [PATCH 1/4] tracing/hist: Prevent overflow in histogram expression strings
From: Li Qiang @ 2026-07-22  6:10 UTC (permalink / raw)
  To: rostedt, mhiramat; +Cc: linux-trace-kernel, linux-kernel, Li Qiang, stable
In-Reply-To: <20260722061040.112747-1-liqiang01@kylinos.cn>

expr_str() builds a histogram expression in a fixed-size
MAX_FILTER_STR_VAL allocation with unbounded strcat() calls.
Synthetic events permit field names longer than that buffer. Constructing
a trigger expression can therefore write past the allocation.

Build the expression with seq_buf. Propagate construction errors to the
parser and reject strings that overflow the fixed-size buffer with -E2BIG.

Fixes: 100719dcef44 ("tracing: Add simple expression support to hist triggers")
Cc: stable@vger.kernel.org
Signed-off-by: Li Qiang <liqiang01@kylinos.cn>
---
 kernel/trace/trace_events_hist.c | 89 +++++++++++++++++++++-----------
 1 file changed, 59 insertions(+), 30 deletions(-)

diff --git a/kernel/trace/trace_events_hist.c b/kernel/trace/trace_events_hist.c
index 82ce492ab268..2b2547546360 100644
--- a/kernel/trace/trace_events_hist.c
+++ b/kernel/trace/trace_events_hist.c
@@ -1733,86 +1733,99 @@ static const char *get_hist_field_flags(struct hist_field *hist_field)
 	return flags_str;
 }
 
-static void expr_field_str(struct hist_field *field, char *expr)
+static void expr_field_str(struct hist_field *field, struct seq_buf *s)
 {
 	if (field->flags & HIST_FIELD_FL_VAR_REF) {
 		if (!field->system)
-			strcat(expr, "$");
+			seq_buf_putc(s, '$');
 	} else if (field->flags & HIST_FIELD_FL_CONST) {
 		char str[HIST_CONST_DIGITS_MAX];
 
 		snprintf(str, HIST_CONST_DIGITS_MAX, "%llu", field->constant);
-		strcat(expr, str);
+		seq_buf_puts(s, str);
 	}
 
-	strcat(expr, hist_field_name(field, 0));
+	seq_buf_puts(s, hist_field_name(field, 0));
 
 	if (field->flags && !(field->flags & HIST_FIELD_FL_VAR_REF)) {
 		const char *flags_str = get_hist_field_flags(field);
 
 		if (flags_str) {
-			strcat(expr, ".");
-			strcat(expr, flags_str);
+			seq_buf_putc(s, '.');
+			seq_buf_puts(s, flags_str);
 		}
 	}
 }
 
 static char *expr_str(struct hist_field *field, unsigned int level)
 {
+	struct seq_buf s;
 	char *expr;
+	int ret = 0;
 
 	if (level > 1)
-		return NULL;
+		return ERR_PTR(-EINVAL);
 
 	expr = kzalloc(MAX_FILTER_STR_VAL, GFP_KERNEL);
 	if (!expr)
-		return NULL;
+		return ERR_PTR(-ENOMEM);
+
+	seq_buf_init(&s, expr, MAX_FILTER_STR_VAL);
 
 	if (!field->operands[0]) {
-		expr_field_str(field, expr);
-		return expr;
+		expr_field_str(field, &s);
+		goto out;
 	}
 
 	if (field->operator == FIELD_OP_UNARY_MINUS) {
 		char *subexpr;
 
-		strcat(expr, "-(");
+		seq_buf_puts(&s, "-(");
 		subexpr = expr_str(field->operands[0], ++level);
-		if (!subexpr) {
-			kfree(expr);
-			return NULL;
+		if (IS_ERR(subexpr)) {
+			ret = PTR_ERR(subexpr);
+			goto free;
 		}
-		strcat(expr, subexpr);
-		strcat(expr, ")");
+		seq_buf_puts(&s, subexpr);
+		seq_buf_putc(&s, ')');
 
 		kfree(subexpr);
-
-		return expr;
+		goto out;
 	}
 
-	expr_field_str(field->operands[0], expr);
+	expr_field_str(field->operands[0], &s);
 
 	switch (field->operator) {
 	case FIELD_OP_MINUS:
-		strcat(expr, "-");
+		seq_buf_putc(&s, '-');
 		break;
 	case FIELD_OP_PLUS:
-		strcat(expr, "+");
+		seq_buf_putc(&s, '+');
 		break;
 	case FIELD_OP_DIV:
-		strcat(expr, "/");
+		seq_buf_putc(&s, '/');
 		break;
 	case FIELD_OP_MULT:
-		strcat(expr, "*");
+		seq_buf_putc(&s, '*');
 		break;
 	default:
-		kfree(expr);
-		return NULL;
+		ret = -EINVAL;
+		goto free;
 	}
 
-	expr_field_str(field->operands[1], expr);
+	expr_field_str(field->operands[1], &s);
 
+out:
+	seq_buf_str(&s);
+	if (seq_buf_has_overflowed(&s)) {
+		ret = -E2BIG;
+		goto free;
+	}
 	return expr;
+
+free:
+	kfree(expr);
+	return ERR_PTR(ret);
 }
 
 /*
@@ -2556,6 +2569,7 @@ static struct hist_field *parse_unary(struct hist_trigger_data *hist_data,
 				      char *var_name, unsigned int *n_subexprs)
 {
 	struct hist_field *operand1, *expr = NULL;
+	char *expr_name;
 	unsigned long operand_flags;
 	int ret = 0;
 	char *s;
@@ -2625,7 +2639,12 @@ static struct hist_field *parse_unary(struct hist_trigger_data *hist_data,
 	expr->size = operand1->size;
 	expr->is_signed = operand1->is_signed;
 	expr->operator = FIELD_OP_UNARY_MINUS;
-	expr->name = expr_str(expr, 0);
+	expr_name = expr_str(expr, 0);
+	if (IS_ERR(expr_name)) {
+		ret = PTR_ERR(expr_name);
+		goto free;
+	}
+	expr->name = expr_name;
 	expr->type = kstrdup_const(operand1->type, GFP_KERNEL);
 	if (!expr->type) {
 		ret = -ENOMEM;
@@ -2691,7 +2710,7 @@ static struct hist_field *parse_expr(struct hist_trigger_data *hist_data,
 	struct hist_field *var1 = NULL, *var2 = NULL;
 	unsigned long operand_flags, operand2_flags;
 	int field_op, ret = -EINVAL;
-	char *sep, *operand1_str;
+	char *expr_name, *sep, *operand1_str;
 	enum hist_field_fn op_fn;
 	bool combine_consts;
 
@@ -2837,7 +2856,12 @@ static struct hist_field *parse_expr(struct hist_trigger_data *hist_data,
 		destroy_hist_field(operand2, 0);
 		destroy_hist_field(operand1, 0);
 
-		expr->name = expr_str(expr, 0);
+		expr_name = expr_str(expr, 0);
+		if (IS_ERR(expr_name)) {
+			ret = PTR_ERR(expr_name);
+			goto free_expr;
+		}
+		expr->name = expr_name;
 	} else {
 		/* The operand sizes should be the same, so just pick one */
 		expr->size = operand1->size;
@@ -2850,7 +2874,12 @@ static struct hist_field *parse_expr(struct hist_trigger_data *hist_data,
 			goto free_expr;
 		}
 
-		expr->name = expr_str(expr, 0);
+		expr_name = expr_str(expr, 0);
+		if (IS_ERR(expr_name)) {
+			ret = PTR_ERR(expr_name);
+			goto free_expr;
+		}
+		expr->name = expr_name;
 	}
 
 	return expr;
-- 
2.43.0


^ permalink raw reply related

* [PATCH 0/4] tracing: fix histogram and injection buffer overflows
From: Li Qiang @ 2026-07-22  6:10 UTC (permalink / raw)
  To: rostedt, mhiramat; +Cc: linux-trace-kernel, linux-kernel, Li Qiang

The histogram expression and trace injection paths derive allocation sizes
from unbounded strings or field metadata. This series bounds histogram
formatting, validates user-event field declarations and injection allocation
sizes, and protects dynamic string growth.

The first two patches validate metadata at the producer and consumer
boundaries. The last two tighten trace injection allocation checks, including
events with no user fields.

Li Qiang (4):
  tracing/hist: Prevent overflow in histogram expression strings
  tracing/user_events: Validate explicit struct field sizes
  tracing/inject: Validate entry allocation size
  tracing/inject: Prevent overflow growing string fields

 kernel/trace/trace_events_hist.c   | 89 ++++++++++++++++++++----------
 kernel/trace/trace_events_inject.c | 34 ++++++++++--
 kernel/trace/trace_events_user.c   | 16 ++++--
 3 files changed, 99 insertions(+), 40 deletions(-)


base-commit: b95f03f04d475aa6719d15a636ddf32222d55657
-- 
2.43.0

^ permalink raw reply

* Re: [PATCH v2 4/4] rv/rtapp: Add wakeup monitor
From: Nam Cao @ 2026-07-22  7:42 UTC (permalink / raw)
  To: Chao Liu
  Cc: Gabriele Monaco, Steven Rostedt, linux-trace-kernel, linux-doc,
	linux-kernel
In-Reply-To: <amBHR4JTr2InJDk8@ChaodeMacBook-Pro.local>

Chao Liu <chao.liu@processmission.com> writes:
>> +static void handle_sched_waking(void *data, struct task_struct *task)
>> +{
>> +	if (in_task()) {
>> +		if (current->prio > task->prio)
>> +			ltl_atom_pulse(task, LTL_WOKEN_BY_LOWER_PRIO, true);
>
> The implementation also reports wakeups from softirq context through
> WOKEN_BY_SOFTIRQ:
>
>> +	} else if (in_serving_softirq()) {
>> +		ltl_atom_pulse(task, LTL_WOKEN_BY_SOFTIRQ, true);
>> +	}
>
> Should this description mention softirq wakeups as well?

Technically, on RT kernel, softirq is a SCHED_OTHER task. On !RT kernel,
softirq can also be executed as a SCHED_OTHER task, which is the cause
for concern.

But explicitly mentioning softirq does not hurt, I wouldn't oppose that.

Nam

^ permalink raw reply

* Re: [PATCH v2 4/4] rv/rtapp: Add wakeup monitor
From: Chao Liu @ 2026-07-22  9:29 UTC (permalink / raw)
  To: Nam Cao
  Cc: Gabriele Monaco, Steven Rostedt, linux-trace-kernel, linux-doc,
	linux-kernel
In-Reply-To: <87y0f3wjuk.fsf@yellow.woof>

On Wed, Jul 22, 2026 at 09:42:59AM +0800, Nam Cao wrote:
> Chao Liu <chao.liu@processmission.com> writes:
> >> +static void handle_sched_waking(void *data, struct task_struct *task)
> >> +{
> >> +	if (in_task()) {
> >> +		if (current->prio > task->prio)
> >> +			ltl_atom_pulse(task, LTL_WOKEN_BY_LOWER_PRIO, true);
> >
> > The implementation also reports wakeups from softirq context through
> > WOKEN_BY_SOFTIRQ:
> >
> >> +	} else if (in_serving_softirq()) {
> >> +		ltl_atom_pulse(task, LTL_WOKEN_BY_SOFTIRQ, true);
> >> +	}
> >
> > Should this description mention softirq wakeups as well?
> 
> Technically, on RT kernel, softirq is a SCHED_OTHER task. On !RT kernel,
> softirq can also be executed as a SCHED_OTHER task, which is the cause
> for concern.
> 
> But explicitly mentioning softirq does not hurt, I wouldn't oppose that.
Thanks for the clarification. I understand it better now.

Thanks,
Chao
> 
> Nam

^ permalink raw reply

* Re: [PATCH v2 4/4] rv/rtapp: Add wakeup monitor
From: Gabriele Monaco @ 2026-07-22  9:36 UTC (permalink / raw)
  To: Nam Cao, Chao Liu
  Cc: Steven Rostedt, linux-trace-kernel, linux-doc, linux-kernel
In-Reply-To: <87y0f3wjuk.fsf@yellow.woof>

On Wed, 2026-07-22 at 09:42 +0200, Nam Cao wrote:
> Chao Liu <chao.liu@processmission.com> writes:
> > > +static void handle_sched_waking(void *data, struct task_struct *task)
> > > +{
> > > +	if (in_task()) {
> > > +		if (current->prio > task->prio)
> > > +			ltl_atom_pulse(task, LTL_WOKEN_BY_LOWER_PRIO,
> > > true);
> > 
> > The implementation also reports wakeups from softirq context through
> > WOKEN_BY_SOFTIRQ:
> > 
> > > +	} else if (in_serving_softirq()) {
> > > +		ltl_atom_pulse(task, LTL_WOKEN_BY_SOFTIRQ, true);
> > > +	}
> > 
> > Should this description mention softirq wakeups as well?
> 
> Technically, on RT kernel, softirq is a SCHED_OTHER task. On !RT kernel,
> softirq can also be executed as a SCHED_OTHER task, which is the cause
> for concern.
> 
> But explicitly mentioning softirq does not hurt, I wouldn't oppose that.

Sounds good to me. So as far as I understand, this change is going to go through
a separate patch right?
I'm considering this series final.

Thanks,
Gabriele


^ permalink raw reply

* Re: [PATCH v2 4/4] mm/mlock: migrate folios out of CMA when mlocking a range
From: Wandun @ 2026-07-22  9:58 UTC (permalink / raw)
  To: Sebastian Andrzej Siewior, David Hildenbrand (Arm)
  Cc: vbabka, rostedt, mhiramat, Alexander.Krabler, hughd, fvdl,
	linux-mm, linux-kernel, linux-trace-kernel, linux-rt-devel, akpm,
	surenb, mhocko, jackmanb, hannes, ziy, ljs, riel, liam, harry,
	jannh, lance.yang, mathieu.desnoyers, matthew.brost,
	joshua.hahnjy, rakie.kim, byungchul, gourry, ying.huang, apopple,
	pfalcato
In-Reply-To: <20260709131509.iIPP2VBh@linutronix.de>

Hi Sebastian, David,

Sorry for the long delay in getting back to this, and thanks a lot for
the review.


On 7/9/26 21:15, Sebastian Andrzej Siewior wrote:
> On 2026-07-09 12:04:33 [+0200], David Hildenbrand (Arm) wrote:
>> On 7/7/26 14:59, Wandun Chen wrote:
>>> From: Wandun Chen <chenwandun@lixiang.com>
>>>
>>> The region covered by mlock[all] may contain CMA pages. cma_alloc installs
>>
>> What about ZONE_MOVABLE where memory is supposed to be migratable?
> 
> Would it be bad if the pages would not be movable anymore? Does this
> effect just memory-hotplug or something else, too?

ZONE_MOVABLE does not need the same treatment as CMA.
1. On RT compact_unevictable_allowed is 0 by default, pages in ZONE_MOVABLE are
   never migrated by compaction, so no latency spike, no problem.

2. For the scenario of memory offlining within ZONE_MOVABLE, because of offline
   is an explicit administrator action. Any jitter/latency is expected and
   acceptable, the operator chose to offline memory and would not do so
   during an RT-critical phase.

3. cma_alloc() runs at runtime, triggered by drivers as part of normal operation,
   and isolation path hardcodes ISOLATE_UNEVICTABLE, so it migrates the mlocked
   pages and installs migration entries. The fault-and-wait latency hits the RT
   task unpredictably during normal operation, which is not expected, and is
   exactly the spike this patch targets.

> 
>> Also, what about drivers that mmap() CMA memory to user space, and
>> __mm_populate()->populate_vma_page_range() would actually try mlocking them, and
>> they actually must remain on CMA areas?
> 
> It should be safe to skip those. They belong to device and they
> shouldn't be affected by anything including getting swapped out.

Agreed. Only LRU folios are ever isolated for migration here
(isolate_folio_to_list() -> folio_isolate_lru()), and device-owned CMA
buffers are not on the LRU, so they are left in place. I'll double-check
the mmap paths and make sure this holds, and state it explicitly in v3.


Best regards,
Wandun

> 
> Sebastian


^ permalink raw reply

* [GIT PULL] RTLA fixes for v7.2-rc5
From: Tomas Glozar @ 2026-07-22 11:01 UTC (permalink / raw)
  To: Steven Rostedt; +Cc: LKML, linux-trace-kernel, Tomas Glozar

Steven,

One more RTLA fix, this time a one-liner fixing an older bug found just after
the previous pull request.

The following changes since commit 1590cf0329716306e948a8fc29f1d3ee87d3989f:

  Linux 7.2-rc4 (2026-07-19 13:54:41 -0700)

are available in the Git repository at:

  https://git.kernel.org/pub/scm/linux/kernel/git/tglozar/linux.git tags/rtla-fixes-v7.2-rc5

for you to fetch changes up to fafb66e5903c2bcfc7b7e259042a8282f18a6faa:

  rtla/timerlat_top: Fix on-threshold actions firing on signal (2026-07-21 15:42:08 +0200)

----------------------------------------------------------------
RTLA fixes for v7.2-rc5

- Fix timerlat top actions triggering on signal

Fix a bug in RTLA's timerlat top actions feature where on-threshold
actions are triggered on any signal, regardless of whether a latency
spike had actually occured during the measurement.

Signed-off-by: Tomas Glozar <tglozar@redhat.com>

----------------------------------------------------------------
Tomas Glozar (1):
      rtla/timerlat_top: Fix on-threshold actions firing on signal

 tools/tracing/rtla/src/timerlat_top.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


^ permalink raw reply

* Re: [PATCH v4 13/17] rv: Add KUnit stub for current
From: Gabriele Monaco @ 2026-07-22 11:37 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, Nam Cao
  Cc: Steven Rostedt, Masami Hiramatsu, Thomas Weissschuh, Tomas Glozar,
	John Kacur, Wen Yang
In-Reply-To: <20260717154638.220789-14-gmonaco@redhat.com>

On Fri, 2026-07-17 at 17:46 +0200, Gabriele Monaco wrote:
> Some monitors do not only rely on tracepoint arguments but also on the
> currently executing task.
> This makes it more challenging to mock events in KUnit.
> 
> Define wrapper functions around current, the functionality is stubbed
> only during KUnit, however the additional function call is necessary
> whenever the KUnit tests are built in.
> 
> Reviewed-by: Nam Cao <namcao@linutronix.de>
> Signed-off-by: Gabriele Monaco <gmonaco@redhat.com>

As reported by sashiko, using KUnit's standard stubbing can be dangerous. In the
rare case of an NMI causing an RV event (e.g. a pagefault) when an unrelated
KUnit test is running, the stub would end up using locks and causing issues.

Additionally, it isn't ideal to downgrade to a function call directly if the
tests are built, which is for instance the default on the stock Fedora kernel
(CONFIG_KUNIT_ALL_TESTS=m).

I'm soon going to send a V5 addressing both: no stub and function call only if
KUnit is really running.
I'll reset the review since the patch essentially changes.

> ---
>  include/rv/da_monitor.h                       |  1 +
>  include/rv/kunit.h                            | 13 +++++++++-
>  include/rv/ltl_monitor.h                      |  1 +
>  kernel/trace/rv/Kconfig                       |  3 +++
>  .../trace/rv/monitors/pagefault/pagefault.c   |  2 +-
>  kernel/trace/rv/monitors/sleep/sleep.c        | 24 +++++++++----------
>  kernel/trace/rv/rv.c                          |  8 +++++++
>  kernel/trace/rv/rv_monitors_test.c            |  9 +++++++
>  8 files changed, 47 insertions(+), 14 deletions(-)
> 
> diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
> index 773564720ba1..9f7ba443d777 100644
> --- a/include/rv/da_monitor.h
> +++ b/include/rv/da_monitor.h
> @@ -16,6 +16,7 @@
>  
>  #include <rv/automata.h>
>  #include <linux/rv.h>
> +#include <rv/kunit.h>
>  #include <linux/stringify.h>
>  #include <linux/bug.h>
>  #include <linux/sched.h>
> diff --git a/include/rv/kunit.h b/include/rv/kunit.h
> index ff98b5137285..6d16a422a80c 100644
> --- a/include/rv/kunit.h
> +++ b/include/rv/kunit.h
> @@ -2,7 +2,10 @@
>  /*
>   * Copyright (C) 2026-2029 Red Hat, Inc. Gabriele Monaco <gmonaco@redhat.com>
>   *
> - * Declaration of utilities to run KUnit tests.
> + * Declaration of wrappers to allow stubbing core functionality, like
> current,
> + * and other testing utilities.
> + * Necessary only when mocking may be needed. If the RV KUnit test is
> + * enabled, the wrappers incur an additional function call overhead.
>   */
>  
>  #ifndef _RV_KUNIT_H
> @@ -16,6 +19,7 @@
>  
>  int rv_set_testing(struct kunit_suite *suite);
>  void rv_clear_testing(struct kunit_suite *suite);
> +struct task_struct *rv_get_current(void);
>  
>  #define RV_KUNIT_MAX_MOCK_TASKS 8
>  
> @@ -23,6 +27,7 @@ struct rv_kunit_ctx {
>  	int reactions, expected;
>  	int mock_task_count;
>  	struct task_struct *mock_tasks[RV_KUNIT_MAX_MOCK_TASKS];
> +	struct task_struct *curr;
>  };
>  
>  #define RV_KUNIT_EXPECT_REACTION(test, ctx)                             \
> @@ -57,5 +62,11 @@ void prepare_test(struct kunit *test, const struct
> rv_kunit_mon *mon);
>  void teardown_test(void *arg);
>  struct task_struct *rv_kunit_alloc_mock_task(struct kunit *test);
>  
> +#define rv_mock_current(ctx, task) (ctx->curr = task)
> +
> +#else /* !CONFIG_RV_MONITORS_KUNIT_TEST */
> +
> +#define rv_get_current() current
> +
>  #endif /* CONFIG_RV_MONITORS_KUNIT_TEST */
>  #endif /* _RV_KUNIT_H */
> diff --git a/include/rv/ltl_monitor.h b/include/rv/ltl_monitor.h
> index 56e83edcf0c4..d7dc01db4dd9 100644
> --- a/include/rv/ltl_monitor.h
> +++ b/include/rv/ltl_monitor.h
> @@ -9,6 +9,7 @@
>  #include <linux/stringify.h>
>  #include <linux/seq_buf.h>
>  #include <rv/instrumentation.h>
> +#include <rv/kunit.h>
>  #include <trace/events/task.h>
>  #include <trace/events/sched.h>
>  
> diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
> index 34c1feb35a9b..7bae9723cdbf 100644
> --- a/kernel/trace/rv/Kconfig
> +++ b/kernel/trace/rv/Kconfig
> @@ -121,4 +121,7 @@ config RV_MONITORS_KUNIT_TEST
>  	  These tests verify that monitors correctly detect violations by
>  	  triggering fake events and validating the expected reactions.
>  
> +	  Enabling this may slightly increase overhead of some monitors even
> +	  when the KUnit test is not running.
> +
>  	  If unsure, say N.
> diff --git a/kernel/trace/rv/monitors/pagefault/pagefault.c
> b/kernel/trace/rv/monitors/pagefault/pagefault.c
> index 5e1a2a606783..e52500fd2de0 100644
> --- a/kernel/trace/rv/monitors/pagefault/pagefault.c
> +++ b/kernel/trace/rv/monitors/pagefault/pagefault.c
> @@ -38,7 +38,7 @@ static void ltl_atoms_init(struct task_struct *task, struct
> ltl_monitor *mon, bo
>  static void handle_page_fault(void *data, unsigned long address, struct
> pt_regs *regs,
>  			      unsigned long error_code)
>  {
> -	ltl_atom_pulse(current, LTL_PAGEFAULT, true);
> +	ltl_atom_pulse(rv_get_current(), LTL_PAGEFAULT, true);
>  }
>  
>  static int enable_pagefault(void)
> diff --git a/kernel/trace/rv/monitors/sleep/sleep.c
> b/kernel/trace/rv/monitors/sleep/sleep.c
> index 12328ce663f5..71d2005ce520 100644
> --- a/kernel/trace/rv/monitors/sleep/sleep.c
> +++ b/kernel/trace/rv/monitors/sleep/sleep.c
> @@ -102,7 +102,7 @@ static void handle_sched_waking(void *data, struct
> task_struct *task)
>  	if (this_cpu_read(hardirq_context)) {
>  		ltl_atom_pulse(task, LTL_WOKEN_BY_HARDIRQ, true);
>  	} else if (in_task()) {
> -		if (current->prio <= task->prio)
> +		if (rv_get_current()->prio <= task->prio)
>  			ltl_atom_pulse(task,
> LTL_WOKEN_BY_EQUAL_OR_HIGHER_PRIO, true);
>  	} else if (in_nmi()) {
>  		ltl_atom_pulse(task, LTL_WOKEN_BY_NMI, true);
> @@ -112,12 +112,12 @@ static void handle_sched_waking(void *data, struct
> task_struct *task)
>  static void handle_contention_begin(void *data, void *lock, unsigned int
> flags)
>  {
>  	if (flags & LCB_F_RT)
> -		ltl_atom_update(current, LTL_BLOCK_ON_RT_MUTEX, true);
> +		ltl_atom_update(rv_get_current(), LTL_BLOCK_ON_RT_MUTEX,
> true);
>  }
>  
>  static void handle_contention_end(void *data, void *lock, int ret)
>  {
> -	ltl_atom_update(current, LTL_BLOCK_ON_RT_MUTEX, false);
> +	ltl_atom_update(rv_get_current(), LTL_BLOCK_ON_RT_MUTEX, false);
>  }
>  
>  static void handle_sys_enter(void *data, struct pt_regs *regs, long id)
> @@ -126,7 +126,7 @@ static void handle_sys_enter(void *data, struct pt_regs
> *regs, long id)
>  	unsigned long args[6];
>  	int op, cmd;
>  
> -	mon = ltl_get_monitor(current);
> +	mon = ltl_get_monitor(rv_get_current());
>  
>  	switch (id) {
>  #ifdef __NR_clock_nanosleep
> @@ -135,11 +135,11 @@ static void handle_sys_enter(void *data, struct pt_regs
> *regs, long id)
>  #ifdef __NR_clock_nanosleep_time64
>  	case __NR_clock_nanosleep_time64:
>  #endif
> -		syscall_get_arguments(current, regs, args);
> +		syscall_get_arguments(rv_get_current(), regs, args);
>  		ltl_atom_set(mon, LTL_NANOSLEEP_CLOCK_MONOTONIC, args[0] ==
> CLOCK_MONOTONIC);
>  		ltl_atom_set(mon, LTL_NANOSLEEP_CLOCK_TAI, args[0] ==
> CLOCK_TAI);
>  		ltl_atom_set(mon, LTL_NANOSLEEP_TIMER_ABSTIME, args[1] ==
> TIMER_ABSTIME);
> -		ltl_atom_update(current, LTL_CLOCK_NANOSLEEP, true);
> +		ltl_atom_update(rv_get_current(), LTL_CLOCK_NANOSLEEP, true);
>  		break;
>  
>  #ifdef __NR_futex
> @@ -148,25 +148,25 @@ static void handle_sys_enter(void *data, struct pt_regs
> *regs, long id)
>  #ifdef __NR_futex_time64
>  	case __NR_futex_time64:
>  #endif
> -		syscall_get_arguments(current, regs, args);
> +		syscall_get_arguments(rv_get_current(), regs, args);
>  		op = args[1];
>  		cmd = op & FUTEX_CMD_MASK;
>  
>  		switch (cmd) {
>  		case FUTEX_LOCK_PI:
>  		case FUTEX_LOCK_PI2:
> -			ltl_atom_update(current, LTL_FUTEX_LOCK_PI, true);
> +			ltl_atom_update(rv_get_current(), LTL_FUTEX_LOCK_PI,
> true);
>  			break;
>  		case FUTEX_WAIT:
>  		case FUTEX_WAIT_BITSET:
>  		case FUTEX_WAIT_REQUEUE_PI:
> -			ltl_atom_update(current, LTL_FUTEX_WAIT, true);
> +			ltl_atom_update(rv_get_current(), LTL_FUTEX_WAIT,
> true);
>  			break;
>  		}
>  		break;
>  #ifdef __NR_epoll_wait
>  	case __NR_epoll_wait:
> -		ltl_atom_update(current, LTL_EPOLL_WAIT, true);
> +		ltl_atom_update(rv_get_current(), LTL_EPOLL_WAIT, true);
>  		break;
>  #endif
>  	}
> @@ -174,7 +174,7 @@ static void handle_sys_enter(void *data, struct pt_regs
> *regs, long id)
>  
>  static void handle_sys_exit(void *data, struct pt_regs *regs, long ret)
>  {
> -	struct ltl_monitor *mon = ltl_get_monitor(current);
> +	struct ltl_monitor *mon = ltl_get_monitor(rv_get_current());
>  
>  	ltl_atom_set(mon, LTL_FUTEX_LOCK_PI, false);
>  	ltl_atom_set(mon, LTL_FUTEX_WAIT, false);
> @@ -182,7 +182,7 @@ static void handle_sys_exit(void *data, struct pt_regs
> *regs, long ret)
>  	ltl_atom_set(mon, LTL_NANOSLEEP_CLOCK_TAI, false);
>  	ltl_atom_set(mon, LTL_NANOSLEEP_TIMER_ABSTIME, false);
>  	ltl_atom_set(mon, LTL_EPOLL_WAIT, false);
> -	ltl_atom_update(current, LTL_CLOCK_NANOSLEEP, false);
> +	ltl_atom_update(rv_get_current(), LTL_CLOCK_NANOSLEEP, false);
>  }
>  
>  static void handle_kthread_stop(void *data, struct task_struct *task)
> diff --git a/kernel/trace/rv/rv.c b/kernel/trace/rv/rv.c
> index cfe950fef3b4..edb10812f296 100644
> --- a/kernel/trace/rv/rv.c
> +++ b/kernel/trace/rv/rv.c
> @@ -142,6 +142,7 @@
>  #include <linux/module.h>
>  #include <linux/init.h>
>  #include <linux/slab.h>
> +#include <kunit/static_stub.h>
>  
>  #ifdef CONFIG_RV_MON_EVENTS
>  #define CREATE_TRACE_POINTS
> @@ -893,4 +894,11 @@ void rv_clear_testing(struct kunit_suite *suite)
>  	mutex_unlock(&rv_interface_lock);
>  }
>  EXPORT_SYMBOL_IF_KUNIT(rv_clear_testing);
> +
> +struct task_struct *rv_get_current(void)
> +{
> +	KUNIT_STATIC_STUB_REDIRECT(rv_get_current);
> +	return current;
> +}
> +EXPORT_SYMBOL_GPL(rv_get_current);
>  #endif
> diff --git a/kernel/trace/rv/rv_monitors_test.c
> b/kernel/trace/rv/rv_monitors_test.c
> index 2145c85d4c9a..2108973383b2 100644
> --- a/kernel/trace/rv/rv_monitors_test.c
> +++ b/kernel/trace/rv/rv_monitors_test.c
> @@ -106,6 +106,13 @@ struct task_struct *rv_kunit_alloc_mock_task(struct kunit
> *test)
>  	return tsk;
>  }
>  
> +static struct task_struct *stub_rv_get_current(void)
> +{
> +	if (active_ctx && active_ctx->curr)
> +		return active_ctx->curr;
> +	return current;
> +}
> +
>  static int rv_mon_test_init(struct kunit *test)
>  {
>  	struct rv_kunit_ctx *ctx;
> @@ -115,6 +122,8 @@ static int rv_mon_test_init(struct kunit *test)
>  
>  	test->priv = ctx;
>  
> +	kunit_activate_static_stub(test, rv_get_current,
> stub_rv_get_current);
> +
>  	return 0;
>  }
>  


^ permalink raw reply

* Re: [PATCH v4 8/8] selftests/verification: add tlob selftests
From: Gabriele Monaco @ 2026-07-22 13:34 UTC (permalink / raw)
  To: wen.yang; +Cc: Nam Cao, linux-trace-kernel, linux-kernel
In-Reply-To: <4eb9a676efe90de8dfc1a9188d6ea81336e65e63.1783524627.git.wen.yang@linux.dev>

On Wed, 2026-07-08 at 23:38 +0800, wen.yang@linux.dev wrote:
> From: Wen Yang <wen.yang@linux.dev>
> 
> Add seven ftrace-style test scripts for the tlob RV monitor under
> tools/testing/selftests/verification/test.d/tlob/.  The tests cover
> uprobe binding management, budget violation detection, and per-state
> time accounting.
> 
> Helper binaries tlob_target and tlob_sym are included in the same
> directory so the suite is self-contained.  tlob_sym resolves ELF
> symbol offsets for uprobe registration; tlob_target provides busy-spin,
> sleep, and preempt workloads.
> 
> ftracetest is updated to walk up the directory tree when searching for
> test.d/functions, so monitor subdirectories can be passed as the test
> directory without placing a dummy functions shim in each new directory.

I believe this last part deserves its own patch, so ftrace folks won't lose
track of it (and it's probably a good idea for them to Ack it, make sure they're
Cc'd).

> 
> Signed-off-by: Wen Yang <wen.yang@linux.dev>
> ---
>  tools/testing/selftests/ftrace/ftracetest     |  18 +-
>  .../testing/selftests/verification/.gitignore |   2 +
>  tools/testing/selftests/verification/Makefile |  19 +-
>  .../verification/test.d/tlob/Makefile         |  28 +++
>  .../test.d/tlob/run_tlob_tests.sh             |  90 ++++++++
>  .../verification/test.d/tlob/tlob_sym.c       | 209 ++++++++++++++++++
>  .../verification/test.d/tlob/tlob_target.c    | 138 ++++++++++++
>  .../verification/test.d/tlob/uprobe_bind.tc   |  37 ++++
>  .../test.d/tlob/uprobe_detail_running.tc      |  51 +++++
>  .../test.d/tlob/uprobe_detail_sleeping.tc     |  50 +++++
>  .../test.d/tlob/uprobe_detail_waiting.tc      |  66 ++++++
>  .../verification/test.d/tlob/uprobe_multi.tc  |  64 ++++++
>  .../test.d/tlob/uprobe_no_event.tc            |  19 ++
>  .../test.d/tlob/uprobe_violation.tc           |  67 ++++++
>  14 files changed, 854 insertions(+), 4 deletions(-)
>  create mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
>  create mode 100755
> tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/tlob_target.c
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_no_event.tc
>  create mode 100644
> tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
> 
> diff --git a/tools/testing/selftests/ftrace/ftracetest
> b/tools/testing/selftests/ftrace/ftracetest
> index 0a56bf209f6c..91c007b0a74a 100755
> --- a/tools/testing/selftests/ftrace/ftracetest
> +++ b/tools/testing/selftests/ftrace/ftracetest
> @@ -159,9 +159,21 @@ parse_opts() { # opts
>    if [ -n "$OPT_TEST_CASES" ]; then
>      TEST_CASES=$OPT_TEST_CASES
>    fi
> -  if [ -n "$OPT_TEST_DIR" -a -f "$OPT_TEST_DIR"/test.d/functions ]; then
> -    TOP_DIR=$OPT_TEST_DIR
> -    TEST_DIR=$TOP_DIR/test.d
> +  if [ -n "$OPT_TEST_DIR" ]; then
> +    # Walk up from OPT_TEST_DIR to find the nearest ancestor containing
> +    # test.d/functions, allowing monitor subdirectories to be passed
> directly.
> +    dir=$OPT_TEST_DIR
> +    while [ "$dir" != "/" ]; do
> +      if [ -f "$dir/test.d/functions" ]; then
> +        TOP_DIR=$dir
> +        TEST_DIR=$TOP_DIR/test.d
> +        break
> +      fi
> +      dir=$(dirname "$dir")
> +    done
> +    if [ -z "$TOP_DIR" ]; then
> +      errexit "no test.d/functions found above $OPT_TEST_DIR"
> +    fi

This won't happen, before calling parse_opts, the script initialises
TOP_DIR to `absdir $0` (ftracetest's folder). You can leave it as it is,
no need to catch an error since it was gracefully continuing anyway.

>    fi
>  }
>  
> diff --git a/tools/testing/selftests/verification/.gitignore
> b/tools/testing/selftests/verification/.gitignore
> index 2659417cb2c7..cbbd03ee16c7 100644
> --- a/tools/testing/selftests/verification/.gitignore
> +++ b/tools/testing/selftests/verification/.gitignore
> @@ -1,2 +1,4 @@
>  # SPDX-License-Identifier: GPL-2.0-only
>  logs
> +test.d/tlob/tlob_sym
> +test.d/tlob/tlob_target
> diff --git a/tools/testing/selftests/verification/Makefile
> b/tools/testing/selftests/verification/Makefile
> index aa8790c22a71..0b32bdfdb8db 100644
> --- a/tools/testing/selftests/verification/Makefile
> +++ b/tools/testing/selftests/verification/Makefile
> @@ -1,8 +1,25 @@
>  # SPDX-License-Identifier: GPL-2.0
> -all:
>  
>  TEST_PROGS := verificationtest-ktap
>  TEST_FILES := test.d settings
>  EXTRA_CLEAN := $(OUTPUT)/logs/*
>  
> +# Subdirectories that provide binaries used by the test runner.
> +# Each entry must contain a Makefile that accepts OUTDIR= and
> +# deposits its binaries there.

Does the entry's Makefile support OUTDIR ? It doesn't look like it.

By the way, reimplementing things like this is probably going to break a
few things like installation (e.g. for distros to package the
kselftests) and clean target. Perhaps we could refactor it to use
standard kselftests methods. See my mockup at the end.

> +BUILD_SUBDIRS := test.d/tlob
> +
>  include ../lib.mk
> +
> +all: $(patsubst %,_build_%,$(BUILD_SUBDIRS))
> +
> +clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
> +
> +.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \
> +        $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
> +
> +$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%:
> +	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
> +
> +$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%:
> +	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
> diff --git a/tools/testing/selftests/verification/test.d/tlob/Makefile
> b/tools/testing/selftests/verification/test.d/tlob/Makefile
> new file mode 100644
> index 000000000000..05a2d2599c4e
> --- /dev/null
> +++ b/tools/testing/selftests/verification/test.d/tlob/Makefile
> @@ -0,0 +1,28 @@
> +# SPDX-License-Identifier: GPL-2.0
> +# Builds tlob selftest helper binaries in the directory of this Makefile.
> +#
> +# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and
> +# tlob_target alongside the .tc scripts so they are self-contained.
> +
> +CFLAGS += $(TOOLS_INCLUDES)
> +
> +# For standalone execution via vng
> +FTRACETEST := ../../../ftrace/ftracetest
> +LOGDIR ?= ../../logs

Are those needed? They aren't used in the Makefile nor exported to the
executed scripts, I'd just drop them.

> +
> +.PHONY: all
> +all: tlob_sym tlob_target
> +
> +tlob_sym: tlob_sym.c
> +	$(CC) $(CFLAGS) -o $@ $<
> +
> +tlob_target: tlob_target.c
> +	$(CC) $(CFLAGS) -o $@ $<
> +
> +.PHONY: run_tests
> +run_tests: all
> +	@./run_tlob_tests.sh
> +
> +.PHONY: clean
> +clean:
> +	$(RM) tlob_sym tlob_target
> diff --git
> a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
> b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
> new file mode 100755
> index 000000000000..cd949756e713
> --- /dev/null
> +++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
> @@ -0,0 +1,90 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Standalone runner for tlob selftests
> +# Usage: ./run_tlob_tests.sh [options]
> +#
> +# Options:
> +#   -v, --verbose    Verbose output
> +#   -k, --keep       Keep test logs
> +#   -l, --logdir DIR Log directory (default: ../../logs)
> +#   -h, --help       Show this help

I get you want a way to run tlob tests alone, but can we reduce the
amount of code to maintain? Why do we need to parse and forward
arguments? Cannot we just pass "$@" to ftracetest?

See that in my mockup later.

> +
> +set -e
> +
> +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
> +FTRACETEST="$SCRIPT_DIR/../../../ftrace/ftracetest"
> +LOGDIR="$SCRIPT_DIR/../../logs"
> +VERBOSE=""
> +KEEP=""
> +EXTRA_ARGS=""
> +
> +# Parse arguments
> +while [[ $# -gt 0 ]]; do
> +    case $1 in
> +        -v|--verbose)
> +            VERBOSE="-v"
> +            shift
> +            ;;
> +        -k|--keep)
> +            KEEP="-k"
> +            shift
> +            ;;
> +        -l|--logdir)
> +            LOGDIR="$2"
> +            shift 2
> +            ;;
> +        -h|--help)
> +            echo "Usage: $0 [options]"
> +            echo ""
> +            echo "Options:"
> +            echo "  -v, --verbose    Verbose output"
> +            echo "  -k, --keep       Keep test logs"
> +            echo "  -l, --logdir DIR Log directory (default: ../../logs)"
> +            echo "  -h, --help       Show this help"
> +            echo ""
> +            echo "Examples:"
> +            echo "  $0                           # Run all tlob tests"
> +            echo "  $0 -v                        # Run with verbose output"
> +            echo "  $0 -v -l /tmp/tlob-logs      # Custom log directory"
> +            echo ""
> +            echo "With vng:"
> +            echo "  vng -v --rwdir $LOGDIR -- $0"
> +            exit 0
> +            ;;
> +        *)
> +            EXTRA_ARGS="$EXTRA_ARGS $1"
> +            shift
> +            ;;
> +    esac
> +done
> +
> +# Build test helpers
> +echo "Building tlob test helpers..."
> +make -C "$SCRIPT_DIR" all
> +
> +# Check ftracetest exists
> +if [ ! -x "$FTRACETEST" ]; then
> +    echo "Error: $FTRACETEST not found or not executable"
> +    echo "Make sure you're running from the correct directory"
> +    exit 1
> +fi
> +
> +# Create log directory
> +mkdir -p "$LOGDIR"
> +
> +# Run tests
> +echo "Running tlob selftests..."
> +echo "Log directory: $LOGDIR"
> +echo ""
> +
> +# Export RV_BINDIR so test scripts can find tlob_target and tlob_sym
> +export RV_BINDIR="$SCRIPT_DIR"
> +
> +# Pass the test directory, not individual .tc files
> +# ftracetest will discover all .tc files in the directory
> +"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \
> +    "$SCRIPT_DIR" $EXTRA_ARGS
> +
> +echo ""
> +echo "Tests completed. Logs saved to: $LOGDIR"

I tried to refactor it to follow more standard selftest building,
avoiding to maintain things ourselves. The only drawback is that you'd
have to move the tlob_*.c files to selftests/verification , then we can
still use RV_BINDIR but support it only via Makefile and
run_tlob_tests.sh (so let's drop defining it in all tests and save
inconvenience if things ever change).

By the way, this isn't necessarily bad, RV_BINDIR is a general term that
any other selftest can end up using (and shouldn't point to tlob's
directory).

I didn't do it to avoid confusion, but it may be more appropriate to
change the name (e.g. RVTEST_ROOT or VERIFICATIONTEST_ROOT)?

Now you won't need separate Makefiles and everything should work
seamlessly.

(I tested this in vng with both your script and the Makefile, but this
is far from a deep testing, it should apply cleanly on your tree)

From 84eff70a20d79d0900714338903c0f5d568a45d9 Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <gmonaco@redhat.com>
Date: Wed, 22 Jul 2026 15:29:21 +0200
Subject: [PATCH] selftests/verification: Simplify tlob tests

Squash this with the other patch should you accept it!
Drop nested Makefile in favour of lib.mk and simplify run_tlob_tests

---
 .../testing/selftests/verification/.gitignore |  4 +-
 tools/testing/selftests/verification/Makefile | 20 +----
 .../verification/test.d/tlob/Makefile         | 28 -------
 .../test.d/tlob/run_tlob_tests.sh             | 79 +------------------
 .../verification/test.d/tlob/uprobe_bind.tc   |  1 -
 .../test.d/tlob/uprobe_detail_running.tc      |  1 -
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  1 -
 .../test.d/tlob/uprobe_detail_waiting.tc      |  1 -
 .../verification/test.d/tlob/uprobe_multi.tc  |  1 -
 .../test.d/tlob/uprobe_violation.tc           |  1 -
 .../verification/{test.d/tlob => }/tlob_sym.c |  0
 .../{test.d/tlob => }/tlob_target.c           |  0
 12 files changed, 9 insertions(+), 128 deletions(-)
 delete mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_sym.c (100%)
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_target.c (100%)

diff --git a/tools/testing/selftests/verification/.gitignore b/tools/testing/selftests/verification/.gitignore
index cbbd03ee16c7..d2f231f1bacb 100644
--- a/tools/testing/selftests/verification/.gitignore
+++ b/tools/testing/selftests/verification/.gitignore
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 logs
-test.d/tlob/tlob_sym
-test.d/tlob/tlob_target
+tlob_sym
+tlob_target
diff --git a/tools/testing/selftests/verification/Makefile b/tools/testing/selftests/verification/Makefile
index 0b32bdfdb8db..17442e6bb87f 100644
--- a/tools/testing/selftests/verification/Makefile
+++ b/tools/testing/selftests/verification/Makefile
@@ -4,22 +4,8 @@ TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*
 
-# Subdirectories that provide binaries used by the test runner.
-# Each entry must contain a Makefile that accepts OUTDIR= and
-# deposits its binaries there.
-BUILD_SUBDIRS := test.d/tlob
+TEST_GEN_FILES := tlob_sym tlob_target
 
-include ../lib.mk
-
-all: $(patsubst %,_build_%,$(BUILD_SUBDIRS))
-
-clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
+export RV_BINDIR := $(OUTPUT)
 
-.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \
-        $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
-
-$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%:
-	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
-
-$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%:
-	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
+include ../lib.mk
diff --git a/tools/testing/selftests/verification/test.d/tlob/Makefile b/tools/testing/selftests/verification/test.d/tlob/Makefile
deleted file mode 100644
index 05a2d2599c4e..000000000000
--- a/tools/testing/selftests/verification/test.d/tlob/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0
-# Builds tlob selftest helper binaries in the directory of this Makefile.
-#
-# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and
-# tlob_target alongside the .tc scripts so they are self-contained.
-
-CFLAGS += $(TOOLS_INCLUDES)
-
-# For standalone execution via vng
-FTRACETEST := ../../../ftrace/ftracetest
-LOGDIR ?= ../../logs
-
-.PHONY: all
-all: tlob_sym tlob_target
-
-tlob_sym: tlob_sym.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-tlob_target: tlob_target.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-.PHONY: run_tests
-run_tests: all
-	@./run_tlob_tests.sh
-
-.PHONY: clean
-clean:
-	$(RM) tlob_sym tlob_target
diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
index cd949756e713..6bedb1813891 100755
--- a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
+++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
@@ -2,89 +2,18 @@
 # SPDX-License-Identifier: GPL-2.0
 #
 # Standalone runner for tlob selftests
-# Usage: ./run_tlob_tests.sh [options]
-#
-# Options:
-#   -v, --verbose    Verbose output
-#   -k, --keep       Keep test logs
-#   -l, --logdir DIR Log directory (default: ../../logs)
-#   -h, --help       Show this help
 
 set -e
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 FTRACETEST="$SCRIPT_DIR/../../../ftrace/ftracetest"
-LOGDIR="$SCRIPT_DIR/../../logs"
-VERBOSE=""
-KEEP=""
-EXTRA_ARGS=""
-
-# Parse arguments
-while [[ $# -gt 0 ]]; do
-    case $1 in
-        -v|--verbose)
-            VERBOSE="-v"
-            shift
-            ;;
-        -k|--keep)
-            KEEP="-k"
-            shift
-            ;;
-        -l|--logdir)
-            LOGDIR="$2"
-            shift 2
-            ;;
-        -h|--help)
-            echo "Usage: $0 [options]"
-            echo ""
-            echo "Options:"
-            echo "  -v, --verbose    Verbose output"
-            echo "  -k, --keep       Keep test logs"
-            echo "  -l, --logdir DIR Log directory (default: ../../logs)"
-            echo "  -h, --help       Show this help"
-            echo ""
-            echo "Examples:"
-            echo "  $0                           # Run all tlob tests"
-            echo "  $0 -v                        # Run with verbose output"
-            echo "  $0 -v -l /tmp/tlob-logs      # Custom log directory"
-            echo ""
-            echo "With vng:"
-            echo "  vng -v --rwdir $LOGDIR -- $0"
-            exit 0
-            ;;
-        *)
-            EXTRA_ARGS="$EXTRA_ARGS $1"
-            shift
-            ;;
-    esac
-done
 
 # Build test helpers
 echo "Building tlob test helpers..."
-make -C "$SCRIPT_DIR" all
-
-# Check ftracetest exists
-if [ ! -x "$FTRACETEST" ]; then
-    echo "Error: $FTRACETEST not found or not executable"
-    echo "Make sure you're running from the correct directory"
-    exit 1
-fi
-
-# Create log directory
-mkdir -p "$LOGDIR"
-
-# Run tests
-echo "Running tlob selftests..."
-echo "Log directory: $LOGDIR"
-echo ""
+make -C "$SCRIPT_DIR/../.." all
 
 # Export RV_BINDIR so test scripts can find tlob_target and tlob_sym
-export RV_BINDIR="$SCRIPT_DIR"
-
-# Pass the test directory, not individual .tc files
-# ftracetest will discover all .tc files in the directory
-"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \
-    "$SCRIPT_DIR" $EXTRA_ARGS
+export RV_BINDIR="$(realpath "$SCRIPT_DIR/../..")"
 
-echo ""
-echo "Tests completed. Logs saved to: $LOGDIR"
+# Run ftracetest, forwarding all options and passing the test directory
+exec "$FTRACETEST" -K --rv "$SCRIPT_DIR" "$@"
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
index 4a1c18c7485a..be2f3555c30d 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor uprobe binding (visible in monitor file, removable, duplicate rejected)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
index afca157b5ea4..46c98ea03872 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail running (running_ns dominates when task busy-spins between probes)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
index 0a6470b4cadb..7e82c7c7f98b 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail sleeping (sleeping_ns dominates when task blocks between probes)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
index ef22fce700fc..43a33357f5ef 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor detail waiting (waiting_ns dominates when task is preempted between probes)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
index a798f3e9b3fa..3c606b354ad2 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
index 8a94bd679b88..ff8b736932ca 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor budget violation (error_env_tlob and detail_env_tlob fire with correct fields)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c b/tools/testing/selftests/verification/tlob_sym.c
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
rename to tools/testing/selftests/verification/tlob_sym.c
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_target.c b/tools/testing/selftests/verification/tlob_target.c
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_target.c
rename to tools/testing/selftests/verification/tlob_target.c
-- 
2.55.0


^ permalink raw reply related

* Re: [PATCH v4 8/8] selftests/verification: add tlob selftests
From: Gabriele Monaco @ 2026-07-22 13:59 UTC (permalink / raw)
  To: wen.yang; +Cc: Nam Cao, linux-trace-kernel, linux-kernel
In-Reply-To: <1aeb814fd0cf348a8cb30cf6c411036aeacc35d9.camel@redhat.com>

On Wed, 2026-07-22 at 15:34 +0200, Gabriele Monaco wrote:
> I tried to refactor it to follow more standard selftest building,
> avoiding to maintain things ourselves. The only drawback is that you'd
> have to move the tlob_*.c files to selftests/verification , then we can
> still use RV_BINDIR but support it only via Makefile and
> run_tlob_tests.sh (so let's drop defining it in all tests and save
> inconvenience if things ever change).

I made a mistake in the Makefile (OUTPUT isn't populated before lib.mk),
please find the fixed version attached.

And really, stop skipping the test result if there is a mistake in the
build system, that should be LOUD. I removed all exit_unsupported for
you (as I already mentioned, you can use chrt:program taskset:program
for those instead, all other things are real errors).

From 088d371276d4bdb67d23941e178a351b6afa309a Mon Sep 17 00:00:00 2001
From: Gabriele Monaco <gmonaco@redhat.com>
Date: Wed, 22 Jul 2026 15:29:21 +0200
Subject: [PATCH] selftests/verification: Simplify tlob tests

Squash this with the other patch should you accept it!
Drop nested Makefile in favour of lib.mk and simplify run_tlob_tests

---
 .../testing/selftests/verification/.gitignore |  4 +-
 tools/testing/selftests/verification/Makefile | 18 +----
 .../verification/test.d/tlob/Makefile         | 28 -------
 .../test.d/tlob/run_tlob_tests.sh             | 79 +------------------
 .../verification/test.d/tlob/uprobe_bind.tc   |  5 --
 .../test.d/tlob/uprobe_detail_running.tc      |  5 --
 .../test.d/tlob/uprobe_detail_sleeping.tc     |  5 --
 .../test.d/tlob/uprobe_detail_waiting.tc      | 10 +--
 .../verification/test.d/tlob/uprobe_multi.tc  |  1 -
 .../test.d/tlob/uprobe_violation.tc           |  1 -
 .../verification/{test.d/tlob => }/tlob_sym.c |  0
 .../{test.d/tlob => }/tlob_target.c           |  0
 12 files changed, 9 insertions(+), 147 deletions(-)
 delete mode 100644 tools/testing/selftests/verification/test.d/tlob/Makefile
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_sym.c (100%)
 rename tools/testing/selftests/verification/{test.d/tlob => }/tlob_target.c (100%)

diff --git a/tools/testing/selftests/verification/.gitignore b/tools/testing/selftests/verification/.gitignore
index cbbd03ee16c7..d2f231f1bacb 100644
--- a/tools/testing/selftests/verification/.gitignore
+++ b/tools/testing/selftests/verification/.gitignore
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 logs
-test.d/tlob/tlob_sym
-test.d/tlob/tlob_target
+tlob_sym
+tlob_target
diff --git a/tools/testing/selftests/verification/Makefile b/tools/testing/selftests/verification/Makefile
index 0b32bdfdb8db..aa17478076b1 100644
--- a/tools/testing/selftests/verification/Makefile
+++ b/tools/testing/selftests/verification/Makefile
@@ -4,22 +4,8 @@ TEST_PROGS := verificationtest-ktap
 TEST_FILES := test.d settings
 EXTRA_CLEAN := $(OUTPUT)/logs/*
 
-# Subdirectories that provide binaries used by the test runner.
-# Each entry must contain a Makefile that accepts OUTDIR= and
-# deposits its binaries there.
-BUILD_SUBDIRS := test.d/tlob
+TEST_GEN_FILES := tlob_sym tlob_target
 
 include ../lib.mk
 
-all: $(patsubst %,_build_%,$(BUILD_SUBDIRS))
-
-clean: $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
-
-.PHONY: $(patsubst %,_build_%,$(BUILD_SUBDIRS)) \
-        $(patsubst %,_clean_%,$(BUILD_SUBDIRS))
-
-$(patsubst %,_build_%,$(BUILD_SUBDIRS)): _build_%:
-	$(MAKE) -C $* OUTDIR="$(OUTPUT)" TOOLS_INCLUDES="$(TOOLS_INCLUDES)"
-
-$(patsubst %,_clean_%,$(BUILD_SUBDIRS)): _clean_%:
-	$(MAKE) -C $* OUTDIR="$(OUTPUT)" clean
+export RV_BINDIR := $(OUTPUT)
diff --git a/tools/testing/selftests/verification/test.d/tlob/Makefile b/tools/testing/selftests/verification/test.d/tlob/Makefile
deleted file mode 100644
index 05a2d2599c4e..000000000000
--- a/tools/testing/selftests/verification/test.d/tlob/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0
-# Builds tlob selftest helper binaries in the directory of this Makefile.
-#
-# Invoked by ../../Makefile via BUILD_SUBDIRS; outputs tlob_sym and
-# tlob_target alongside the .tc scripts so they are self-contained.
-
-CFLAGS += $(TOOLS_INCLUDES)
-
-# For standalone execution via vng
-FTRACETEST := ../../../ftrace/ftracetest
-LOGDIR ?= ../../logs
-
-.PHONY: all
-all: tlob_sym tlob_target
-
-tlob_sym: tlob_sym.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-tlob_target: tlob_target.c
-	$(CC) $(CFLAGS) -o $@ $<
-
-.PHONY: run_tests
-run_tests: all
-	@./run_tlob_tests.sh
-
-.PHONY: clean
-clean:
-	$(RM) tlob_sym tlob_target
diff --git a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
index cd949756e713..6bedb1813891 100755
--- a/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
+++ b/tools/testing/selftests/verification/test.d/tlob/run_tlob_tests.sh
@@ -2,89 +2,18 @@
 # SPDX-License-Identifier: GPL-2.0
 #
 # Standalone runner for tlob selftests
-# Usage: ./run_tlob_tests.sh [options]
-#
-# Options:
-#   -v, --verbose    Verbose output
-#   -k, --keep       Keep test logs
-#   -l, --logdir DIR Log directory (default: ../../logs)
-#   -h, --help       Show this help
 
 set -e
 
 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 FTRACETEST="$SCRIPT_DIR/../../../ftrace/ftracetest"
-LOGDIR="$SCRIPT_DIR/../../logs"
-VERBOSE=""
-KEEP=""
-EXTRA_ARGS=""
-
-# Parse arguments
-while [[ $# -gt 0 ]]; do
-    case $1 in
-        -v|--verbose)
-            VERBOSE="-v"
-            shift
-            ;;
-        -k|--keep)
-            KEEP="-k"
-            shift
-            ;;
-        -l|--logdir)
-            LOGDIR="$2"
-            shift 2
-            ;;
-        -h|--help)
-            echo "Usage: $0 [options]"
-            echo ""
-            echo "Options:"
-            echo "  -v, --verbose    Verbose output"
-            echo "  -k, --keep       Keep test logs"
-            echo "  -l, --logdir DIR Log directory (default: ../../logs)"
-            echo "  -h, --help       Show this help"
-            echo ""
-            echo "Examples:"
-            echo "  $0                           # Run all tlob tests"
-            echo "  $0 -v                        # Run with verbose output"
-            echo "  $0 -v -l /tmp/tlob-logs      # Custom log directory"
-            echo ""
-            echo "With vng:"
-            echo "  vng -v --rwdir $LOGDIR -- $0"
-            exit 0
-            ;;
-        *)
-            EXTRA_ARGS="$EXTRA_ARGS $1"
-            shift
-            ;;
-    esac
-done
 
 # Build test helpers
 echo "Building tlob test helpers..."
-make -C "$SCRIPT_DIR" all
-
-# Check ftracetest exists
-if [ ! -x "$FTRACETEST" ]; then
-    echo "Error: $FTRACETEST not found or not executable"
-    echo "Make sure you're running from the correct directory"
-    exit 1
-fi
-
-# Create log directory
-mkdir -p "$LOGDIR"
-
-# Run tests
-echo "Running tlob selftests..."
-echo "Log directory: $LOGDIR"
-echo ""
+make -C "$SCRIPT_DIR/../.." all
 
 # Export RV_BINDIR so test scripts can find tlob_target and tlob_sym
-export RV_BINDIR="$SCRIPT_DIR"
-
-# Pass the test directory, not individual .tc files
-# ftracetest will discover all .tc files in the directory
-"$FTRACETEST" -K $VERBOSE $KEEP --rv --logdir "$LOGDIR" \
-    "$SCRIPT_DIR" $EXTRA_ARGS
+export RV_BINDIR="$(realpath "$SCRIPT_DIR/../..")"
 
-echo ""
-echo "Tests completed. Logs saved to: $LOGDIR"
+# Run ftracetest, forwarding all options and passing the test directory
+exec "$FTRACETEST" -K --rv "$SCRIPT_DIR" "$@"
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
index 4a1c18c7485a..efa4ae47ce72 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_bind.tc
@@ -3,17 +3,12 @@
 # description: Test tlob monitor uprobe binding (visible in monitor file, removable, duplicate rejected)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
-[ -x "$UPROBE_TARGET" ] || exit_unsupported
-[ -x "$TLOB_SYM" ]      || exit_unsupported
 TLOB_MONITOR=monitors/tlob/monitor
 
 busy_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
-[ -n "$busy_offset" ] || exit_unsupported
-[ -n "$stop_offset" ] || exit_unsupported
 
 "$UPROBE_TARGET" 30000 &
 busy_pid=$!
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
index afca157b5ea4..ef53b1b70104 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_running.tc
@@ -3,17 +3,12 @@
 # description: Test tlob monitor detail running (running_ns dominates when task busy-spins between probes)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
-[ -x "$UPROBE_TARGET" ] || exit_unsupported
-[ -x "$TLOB_SYM" ]      || exit_unsupported
 TLOB_MONITOR=monitors/tlob/monitor
 
 start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_busy_work_done 2>/dev/null)
-[ -n "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ] || exit_unsupported
 
 "$UPROBE_TARGET" 5000 &
 busy_pid=$!
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
index 0a6470b4cadb..f97e18059e15 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_sleeping.tc
@@ -3,17 +3,12 @@
 # description: Test tlob monitor detail sleeping (sleeping_ns dominates when task blocks between probes)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
-[ -x "$UPROBE_TARGET" ] || exit_unsupported
-[ -x "$TLOB_SYM" ]      || exit_unsupported
 TLOB_MONITOR=monitors/tlob/monitor
 
 start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_sleep_work_done 2>/dev/null)
-[ -n "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ] || exit_unsupported
 
 "$UPROBE_TARGET" 5000 sleep &
 busy_pid=$!
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
index ef22fce700fc..0a5c41e57617 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_detail_waiting.tc
@@ -1,22 +1,14 @@
 #!/bin/sh
 # SPDX-License-Identifier: GPL-2.0-or-later
 # description: Test tlob monitor detail waiting (waiting_ns dominates when task is preempted between probes)
-# requires: tlob:monitor
+# requires: tlob:monitor chrt:program taskset:program
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
-[ -x "$UPROBE_TARGET" ] || exit_unsupported
-[ -x "$TLOB_SYM" ]      || exit_unsupported
 TLOB_MONITOR=monitors/tlob/monitor
 
-command -v chrt    > /dev/null || exit_unsupported
-command -v taskset > /dev/null || exit_unsupported
-
 start_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work 2>/dev/null)
 stop_offset=$("$TLOB_SYM" sym_offset "$UPROBE_TARGET" tlob_preempt_work_done 2>/dev/null)
-[ -n "$start_offset" ] || exit_unsupported
-[ -n "$stop_offset" ]  || exit_unsupported
 
 cpu=0
 
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
index a798f3e9b3fa..3c606b354ad2 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_multi.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor multiple uprobe bindings (different offsets fire independently)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
index 8a94bd679b88..ff8b736932ca 100644
--- a/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
+++ b/tools/testing/selftests/verification/test.d/tlob/uprobe_violation.tc
@@ -3,7 +3,6 @@
 # description: Test tlob monitor budget violation (error_env_tlob and detail_env_tlob fire with correct fields)
 # requires: tlob:monitor
 
-RV_BINDIR="${RV_BINDIR:-$(realpath "$(dirname "${1:-$0}")")}"
 UPROBE_TARGET="${RV_BINDIR}/tlob_target"
 TLOB_SYM="${RV_BINDIR}/tlob_sym"
 [ -x "$UPROBE_TARGET" ] || exit_unsupported
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_sym.c b/tools/testing/selftests/verification/tlob_sym.c
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_sym.c
rename to tools/testing/selftests/verification/tlob_sym.c
diff --git a/tools/testing/selftests/verification/test.d/tlob/tlob_target.c b/tools/testing/selftests/verification/tlob_target.c
similarity index 100%
rename from tools/testing/selftests/verification/test.d/tlob/tlob_target.c
rename to tools/testing/selftests/verification/tlob_target.c

-- 
2.55.0


^ permalink raw reply related

* Re: [PATCH v4 7/8] rv/tlob: add KUnit tests for the tlob monitor
From: Gabriele Monaco @ 2026-07-22 14:42 UTC (permalink / raw)
  To: wen.yang; +Cc: Nam Cao, linux-trace-kernel, linux-kernel
In-Reply-To: <18ab97e8a248e55dfeef5daa49daa407bf58dc56.1783524627.git.wen.yang@linux.dev>

On Wed, 2026-07-08 at 23:38 +0800, wen.yang@linux.dev wrote:
> From: Wen Yang <wen.yang@linux.dev>
>

...

> +++ b/kernel/trace/rv/monitors/tlob/.kunitconfig
> @@ -0,0 +1,8 @@
> +CONFIG_FTRACE=y
> +CONFIG_HIGH_RES_TIMERS=y
> +CONFIG_KUNIT=y
> +CONFIG_MODULES=y
> +CONFIG_RV=y
> +CONFIG_RV_MON_TLOB=y
> +CONFIG_TLOB_KUNIT_TEST=y
> +CONFIG_UPROBES=y

Let's perhaps drop here what isn't a (mostly) direct dependency and make
it slimmer? Others are requirements of those anyway (and if they stop
being so, they aren't required for the test either).

  CONFIG_KUNIT=y
  CONFIG_UPROBES=y
  CONFIG_RV=y
  CONFIG_RV_MON_TLOB=y
  CONFIG_TLOB_KUNIT_TEST=y

> diff --git a/kernel/trace/rv/monitors/tlob/Kconfig
> b/kernel/trace/rv/monitors/tlob/Kconfig
> index aa43382073d2..402ef2e5c076 100644
> --- a/kernel/trace/rv/monitors/tlob/Kconfig
> +++ b/kernel/trace/rv/monitors/tlob/Kconfig
> @@ -10,3 +10,10 @@ config RV_MON_TLOB
>  	  monitor.  tlob tracks per-task elapsed wall-clock time across a
>  	  user-delimited code section and emits error_env_tlob when the
>  	  elapsed time exceeds a configurable per-invocation budget.
> +
> +config TLOB_KUNIT_TEST
> +	tristate "KUnit tests for tlob monitor" if !KUNIT_ALL_TESTS
> +	depends on RV_MON_TLOB && KUNIT
> +	default KUNIT_ALL_TESTS
> +	help
> +	  Enable KUnit unit tests for the tlob RV monitor.
> diff --git a/kernel/trace/rv/monitors/tlob/tlob.c
> b/kernel/trace/rv/monitors/tlob/tlob.c
> index b45e84195131..a6f9c371646c 100644
> --- a/kernel/trace/rv/monitors/tlob/tlob.c
> +++ b/kernel/trace/rv/monitors/tlob/tlob.c
> @@ -708,7 +708,7 @@ static ssize_t tlob_monitor_read(struct file *file,
>   * PATH may contain ':'; the last ':' separates path from offset.
>   * Returns 0, -EINVAL, or -ERANGE.
>   */
> -static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
> +VISIBLE_IF_KUNIT int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
>  				  char **path_out,
>  				  loff_t *start_out, loff_t *stop_out)
>  {
> @@ -782,6 +782,7 @@ static int tlob_parse_uprobe_line(char *buf, u64 *thr_out,
>  	*stop_out  = (loff_t)stop_val;
>  	return 0;
>  }
> +EXPORT_SYMBOL_IF_KUNIT(tlob_parse_uprobe_line);
>  
>  /*
>   * Parse "-PATH:OFFSET_START" (ftrace uprobe_events removal convention).
> @@ -810,8 +811,9 @@ VISIBLE_IF_KUNIT int tlob_parse_remove_line(char *buf,
> char **path_out,
>  	*start_out = (loff_t)off;
>  	return 0;
>  }
> +EXPORT_SYMBOL_IF_KUNIT(tlob_parse_remove_line);
>  
> -VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char *buf)
> +static int tlob_create_or_delete_uprobe(char *buf)

That's a nit, but probably all the VISIBLE_IF_KUNIT /
EXPORT_SYMBOL_IF_KUNIT should be added by this patch alone.

And definitely tlob_create_or_delete_uprobe shouldn't have it in the
earlier patch just for it to be removed here.

>  {
>  	loff_t offset_start, offset_stop;
>  	u64 threshold_ns;
> @@ -836,7 +838,6 @@ VISIBLE_IF_KUNIT int tlob_create_or_delete_uprobe(char
> *buf)
>  	mutex_unlock(&tlob_uprobe_mutex);
>  	return ret;
>  }
> -EXPORT_SYMBOL_IF_KUNIT(tlob_create_or_delete_uprobe);
>  
>  static ssize_t tlob_monitor_write(struct file *file,
>  				  const char __user *ubuf,
> diff --git a/kernel/trace/rv/monitors/tlob/tlob.h
> b/kernel/trace/rv/monitors/tlob/tlob.h
> index fceeba748c85..15bdf3b7fade 100644
> --- a/kernel/trace/rv/monitors/tlob/tlob.h
> +++ b/kernel/trace/rv/monitors/tlob/tlob.h
> @@ -145,7 +145,9 @@ int tlob_start_task(struct task_struct *task, u64
> threshold_ns);
>  int tlob_stop_task(struct task_struct *task);
>  
>  #if IS_ENABLED(CONFIG_KUNIT)

Any reason not to use IS_ENABLED(CONFIG_TLOB_KUNIT_TEST) ?

> -int tlob_create_or_delete_uprobe(char *buf);
> +int tlob_parse_uprobe_line(char *buf, u64 *thr_out, char **path_out,
> +			   loff_t *start_out, loff_t *stop_out);
> +int tlob_parse_remove_line(char *buf, char **path_out, loff_t *start_out);

Same here, those can all be exported in this patch, no need to mention
any KUNIT stuff earlier.

>  #endif /* CONFIG_KUNIT */
>  
>  #endif /* _RV_TLOB_H */
> diff --git a/kernel/trace/rv/monitors/tlob/tlob_kunit.c
> b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
> new file mode 100644
> index 000000000000..7448f3fab959
> --- /dev/null
> +++ b/kernel/trace/rv/monitors/tlob/tlob_kunit.c
> @@ -0,0 +1,139 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * KUnit tests for the tlob RV monitor.
> + *
> + */

Besides the minor nits above, tests look good.

Thanks,
Gabriele

> +#include <kunit/test.h>
> +
> +#include "tlob.h"
> +
> +MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
> +
> +/* Valid "p PATH:START STOP threshold=NS" lines. */
> +static const char * const tlob_parse_valid[] = {
> +	"p /usr/bin/myapp:4768 4848 threshold=5000000",
> +	"p /usr/bin/myapp:0x12a0 0x12f0 threshold=10000000",
> +	"p /opt/my:app/bin:0x100 0x200 threshold=1000000",
> +};
> +
> +/* Malformed "p ..." lines that must be rejected with -EINVAL. */
> +static const char * const tlob_parse_invalid[] = {
> +	"p :0x100 0x200 threshold=5000",
> +	"p /usr/bin/myapp:0x100 threshold=5000",
> +	"p /usr/bin/myapp:-1 0x200 threshold=5000",
> +	"p /usr/bin/myapp:0x100 -1 threshold=5000000",	/* negative stop
> offset */
> +	"p /usr/bin/myapp:0x100 0x200",
> +	"p /usr/bin/myapp:0x100 0x100 threshold=5000",
> +};
> +
> +/* threshold_ns out of valid range => -ERANGE. */
> +static const char * const tlob_parse_out_of_range[] = {
> +	"p /usr/bin/myapp:0x100 0x200 threshold=0",
> +	"p /usr/bin/myapp:0x100 0x200 threshold=999",
> +	"p /usr/bin/myapp:0x100 0x200 threshold=3600000000001",
> +};
> +
> +/* Valid "-PATH:OFFSET_START" remove lines. */
> +static const char * const tlob_remove_valid[] = {
> +	"-/usr/bin/myapp:0x100",
> +	"-/opt/my:app/bin:0x200",
> +};
> +
> +/* Malformed remove lines that must be rejected with -EINVAL. */
> +static const char * const tlob_remove_invalid[] = {
> +	"-usr/bin/myapp:0x100",
> +	"-/usr/bin/myapp",
> +	"-/:0x100",
> +	"-/usr/bin/myapp:-1",	/* negative offset */
> +	"-/usr/bin/myapp:abc",
> +};
> +
> +static void tlob_parse_valid_accepted(struct kunit *test)
> +{
> +	u64 thr;
> +	char *path;
> +	loff_t start, stop;
> +	char buf[128];
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(tlob_parse_valid); i++) {
> +		strscpy(buf, tlob_parse_valid[i], sizeof(buf));
> +		KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr,
> &path,
> +							     &start, &stop),
> 0);
> +	}
> +}
> +
> +static void tlob_parse_invalid_rejected(struct kunit *test)
> +{
> +	u64 thr;
> +	char *path;
> +	loff_t start, stop;
> +	char buf[128];
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(tlob_parse_invalid); i++) {
> +		strscpy(buf, tlob_parse_invalid[i], sizeof(buf));
> +		KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr,
> &path,
> +							     &start, &stop),
> -EINVAL);
> +	}
> +}
> +
> +static void tlob_parse_out_of_range_rejected(struct kunit *test)
> +{
> +	u64 thr;
> +	char *path;
> +	loff_t start, stop;
> +	char buf[128];
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(tlob_parse_out_of_range); i++) {
> +		strscpy(buf, tlob_parse_out_of_range[i], sizeof(buf));
> +		KUNIT_EXPECT_EQ(test, tlob_parse_uprobe_line(buf, &thr,
> &path,
> +							     &start, &stop),
> -ERANGE);
> +	}
> +}
> +
> +static void tlob_remove_valid_accepted(struct kunit *test)
> +{
> +	char *path;
> +	loff_t start;
> +	char buf[128];
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(tlob_remove_valid); i++) {
> +		strscpy(buf, tlob_remove_valid[i], sizeof(buf));
> +		KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path,
> &start), 0);
> +	}
> +}
> +
> +static void tlob_remove_invalid_rejected(struct kunit *test)
> +{
> +	char *path;
> +	loff_t start;
> +	char buf[128];
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(tlob_remove_invalid); i++) {
> +		strscpy(buf, tlob_remove_invalid[i], sizeof(buf));
> +		KUNIT_EXPECT_EQ(test, tlob_parse_remove_line(buf, &path,
> &start), -EINVAL);
> +	}
> +}
> +
> +static struct kunit_case tlob_parse_cases[] = {
> +	KUNIT_CASE(tlob_parse_valid_accepted),
> +	KUNIT_CASE(tlob_parse_invalid_rejected),
> +	KUNIT_CASE(tlob_parse_out_of_range_rejected),
> +	KUNIT_CASE(tlob_remove_valid_accepted),
> +	KUNIT_CASE(tlob_remove_invalid_rejected),
> +	{}
> +};
> +
> +static struct kunit_suite tlob_parse_suite = {
> +	.name       = "tlob_parse",
> +	.test_cases = tlob_parse_cases,
> +};
> +
> +kunit_test_suite(tlob_parse_suite);
> +
> +MODULE_DESCRIPTION("KUnit tests for the tlob RV monitor");
> +MODULE_LICENSE("GPL");


^ permalink raw reply

* Re: [PATCH 1/4] tracing/hist: Prevent overflow in histogram expression strings
From: Steven Rostedt @ 2026-07-22 16:32 UTC (permalink / raw)
  To: Li Qiang; +Cc: mhiramat, linux-trace-kernel, linux-kernel, stable
In-Reply-To: <20260722061040.112747-2-liqiang01@kylinos.cn>

On Wed, 22 Jul 2026 14:10:37 +0800
Li Qiang <liqiang01@kylinos.cn> wrote:

> expr_str() builds a histogram expression in a fixed-size
> MAX_FILTER_STR_VAL allocation with unbounded strcat() calls.
> Synthetic events permit field names longer than that buffer. Constructing
> a trigger expression can therefore write past the allocation.
> 
> Build the expression with seq_buf. Propagate construction errors to the
> parser and reject strings that overflow the fixed-size buffer with -E2BIG.
> 
> Fixes: 100719dcef44 ("tracing: Add simple expression support to hist triggers")
> Cc: stable@vger.kernel.org
> Signed-off-by: Li Qiang <liqiang01@kylinos.cn>

This has been fixed by this:

   https://patch.msgid.link/20260611055945.22348-4-pengpeng@iscas.ac.cn

Since it can only be updated by root, I didn't not add a fixes nor a stable
tag and will be sending it in the next merge window (I haven't updated my
linux-next branch, but will soon)

-- Steve

^ permalink raw reply

* Re: [PATCH 2/4] tracing/user_events: Validate explicit struct field sizes
From: Steven Rostedt @ 2026-07-22 16:33 UTC (permalink / raw)
  To: Li Qiang, Beau Belgrave
  Cc: mhiramat, linux-trace-kernel, linux-kernel, stable
In-Reply-To: <20260722061040.112747-3-liqiang01@kylinos.cn>

Beau,

Can you review this?

Thanks,

-- Steve


On Wed, 22 Jul 2026 14:10:38 +0800
Li Qiang <liqiang01@kylinos.cn> wrote:

> User event declarations permit an explicit size for a struct field. The
> parser accumulated that size in an unsigned offset, then assigned the
> parsed unsigned value directly to signed field metadata. Oversized
> declarations or cumulative offsets could wrap or become invalid signed
> values.
> 
> Validate an explicit size is representable as int before storing it. Keep
> the running offset signed and reject additions exceeding INT_MAX, so
> invalid field layouts are rejected during declaration parsing.
> 
> Fixes: 7f5a08c79df3 ("user_events: Add minimal support for trace_event into ftrace")
> Cc: stable@vger.kernel.org
> Signed-off-by: Li Qiang <liqiang01@kylinos.cn>
> ---
>  kernel/trace/trace_events_user.c | 16 ++++++++++++----
>  1 file changed, 12 insertions(+), 4 deletions(-)
> 
> diff --git a/kernel/trace/trace_events_user.c b/kernel/trace/trace_events_user.c
> index 8c82ecb735f4..fd5b3946921c 100644
> --- a/kernel/trace/trace_events_user.c
> +++ b/kernel/trace/trace_events_user.c
> @@ -1197,10 +1197,12 @@ static int user_event_add_field(struct user_event *user, const char *type,
>   * Format: type name [size]
>   */
>  static int user_event_parse_field(char *field, struct user_event *user,
> -				  u32 *offset)
> +				  int *offset)
>  {
>  	char *part, *type, *name;
> -	u32 depth = 0, saved_offset = *offset;
> +	u32 depth = 0;
> +	unsigned int field_size;
> +	int saved_offset = *offset;
>  	int len, size = -EINVAL;
>  	bool is_struct = false;
>  
> @@ -1261,8 +1263,11 @@ static int user_event_parse_field(char *field, struct user_event *user,
>  			if (!is_struct)
>  				return -EINVAL;
>  
> -			if (kstrtou32(part, 10, &size))
> +			if (kstrtouint(part, 10, &field_size))
>  				return -EINVAL;
> +			if (field_size > INT_MAX)
> +				return -E2BIG;
> +			size = field_size;
>  			break;
>  		default:
>  			return -EINVAL;
> @@ -1281,6 +1286,9 @@ static int user_event_parse_field(char *field, struct user_event *user,
>  	if (size < 0)
>  		return size;
>  
> +	if (size > INT_MAX - saved_offset)
> +		return -E2BIG;
> +
>  	*offset = saved_offset + size;
>  
>  	return user_event_add_field(user, type, name, saved_offset, size,
> @@ -1290,7 +1298,7 @@ static int user_event_parse_field(char *field, struct user_event *user,
>  static int user_event_parse_fields(struct user_event *user, char *args)
>  {
>  	char *field;
> -	u32 offset = sizeof(struct trace_entry);
> +	int offset = sizeof(struct trace_entry);
>  	int ret = -EINVAL;
>  
>  	if (args == NULL)


^ permalink raw reply

* [PATCH v3 01/20] landlock: Prepare ruleset and domain type split
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Rulesets and domains serve fundamentally different purposes: a ruleset
is mutable and user-facing, created by landlock_create_ruleset(), while
a domain is immutable after construction and enforced on tasks via
landlock_restrict_self().  Today both are represented by struct
landlock_ruleset, which conflates mutable and immutable state in a
single type: the lock field is unused by domains, the hierarchy field is
unused by rulesets, and lifecycle functions must handle both cases.

Prepare for a clean type split by introducing two new structures:

- struct landlock_rules: the red-black tree roots and rule count, shared
  by both rulesets and domains.  Decoupling rule storage from the domain
  API lets the backing data structure change independently (e.g. to a
  hash table, cf. [1]).
- struct landlock_domain: the immutable domain enforced on tasks, with
  no lock field because its rules and access masks are fixed once
  construction completes.  The name reflects the role, not the internal
  data structure.

Add the domain lifecycle helpers (landlock_get_domain(),
landlock_put_domain(), landlock_put_domain_deferred()) and move domain.o
from landlock-$(CONFIG_AUDIT) to landlock-y, because these are needed
unconditionally, not just for audit logging.

No behavioral change.  The new types and lifecycle functions are not yet
used by any caller.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20250523165741.693976-1-mic@digikod.net [1]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-2-mic@digikod.net
- Adapt rule insertion to the base's quiet flag (landlock_insert_rule()
  gains a flags argument).
- Drop Tingmao Wang's Reviewed-by; the rebase reworked the patch.

Changes since v1:
- New patch.
---
 security/landlock/Makefile  |  6 +--
 security/landlock/domain.c  | 35 ++++++++++++++++
 security/landlock/domain.h  | 69 ++++++++++++++++++++++++++++++++
 security/landlock/ruleset.c | 71 ++++++++++++++++-----------------
 security/landlock/ruleset.h | 79 +++++++++++++++++++++++++++----------
 5 files changed, 200 insertions(+), 60 deletions(-)

diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index ffa7646d99f3..23e13644916f 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -8,11 +8,11 @@ landlock-y := \
 	cred.o \
 	task.o \
 	fs.o \
-	tsync.o
+	tsync.o \
+	domain.o
 
 landlock-$(CONFIG_INET) += net.o
 
 landlock-$(CONFIG_AUDIT) += \
 	id.o \
-	audit.o \
-	domain.o
+	audit.o
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 9a8355fccd26..c5efb7635a7b 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -15,14 +15,49 @@
 #include <linux/mm.h>
 #include <linux/path.h>
 #include <linux/pid.h>
+#include <linux/refcount.h>
 #include <linux/sched.h>
 #include <linux/signal.h>
+#include <linux/slab.h>
 #include <linux/uidgid.h>
+#include <linux/workqueue.h>
 
 #include "access.h"
 #include "common.h"
 #include "domain.h"
 #include "id.h"
+#include "ruleset.h"
+
+static void free_domain(struct landlock_domain *const domain)
+{
+	might_sleep();
+	landlock_free_rules(&domain->rules);
+	landlock_put_hierarchy(domain->hierarchy);
+	kfree(domain);
+}
+
+void landlock_put_domain(struct landlock_domain *const domain)
+{
+	might_sleep();
+	if (domain && refcount_dec_and_test(&domain->usage))
+		free_domain(domain);
+}
+
+static void free_domain_work(struct work_struct *const work)
+{
+	struct landlock_domain *domain;
+
+	domain = container_of(work, struct landlock_domain, work_free);
+	free_domain(domain);
+}
+
+void landlock_put_domain_deferred(struct landlock_domain *const domain)
+{
+	if (domain && refcount_dec_and_test(&domain->usage)) {
+		INIT_WORK(&domain->work_free, free_domain_work);
+		schedule_work(&domain->work_free);
+	}
+}
 
 #ifdef CONFIG_AUDIT
 
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 2a1660e3dea7..bcfd0452e260 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -10,6 +10,7 @@
 #ifndef _SECURITY_LANDLOCK_DOMAIN_H
 #define _SECURITY_LANDLOCK_DOMAIN_H
 
+#include <linux/cleanup.h>
 #include <linux/limits.h>
 #include <linux/mm.h>
 #include <linux/path.h>
@@ -17,9 +18,11 @@
 #include <linux/refcount.h>
 #include <linux/sched.h>
 #include <linux/slab.h>
+#include <linux/workqueue.h>
 
 #include "access.h"
 #include "audit.h"
+#include "ruleset.h"
 
 enum landlock_log_status {
 	LANDLOCK_LOG_PENDING = 0,
@@ -176,4 +179,70 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
 	}
 }
 
+/**
+ * struct landlock_domain - Immutable Landlock domain
+ *
+ * A domain is created from a ruleset by landlock_merge_ruleset() and enforced
+ * on a task.  Once created, its rules and access masks are immutable.  Unlike
+ * &struct landlock_ruleset, a domain has no lock field.
+ */
+struct landlock_domain {
+	/**
+	 * @rules: Red-black tree storage for rules.
+	 */
+	struct landlock_rules rules;
+	/**
+	 * @hierarchy: Enables hierarchy identification even when a parent
+	 * domain vanishes.  This is needed for the ptrace and scope
+	 * restrictions.
+	 */
+	struct landlock_hierarchy *hierarchy;
+	union {
+		/**
+		 * @work_free: Enables to free a domain within a lockless
+		 * section.  This is only used by landlock_put_domain_deferred()
+		 * when @usage reaches zero.  The fields @usage, @num_layers and
+		 * @access_masks are then unused.
+		 */
+		struct work_struct work_free;
+		struct {
+			/**
+			 * @usage: Number of credentials referencing this
+			 * domain.
+			 */
+			refcount_t usage;
+			/**
+			 * @num_layers: Number of layers that are used in this
+			 * domain.  This enables to check that all the layers
+			 * allow an access request.
+			 */
+			u32 num_layers;
+			/**
+			 * @access_masks: Contains the subset of filesystem and
+			 * network actions that are restricted by a domain.  A
+			 * domain saves all layers of merged rulesets in a stack
+			 * (FAM), starting from the first layer to the last one.
+			 * These layers are used when merging rulesets, for user
+			 * space backward compatibility (i.e. future-proof), and
+			 * to properly handle merged rulesets without
+			 * overlapping access rights.  These layers are set once
+			 * and never changed for the lifetime of the domain.
+			 */
+			struct access_masks access_masks[];
+		};
+	};
+};
+
+void landlock_put_domain(struct landlock_domain *const domain);
+void landlock_put_domain_deferred(struct landlock_domain *const domain);
+
+DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
+	    if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+
+static inline void landlock_get_domain(struct landlock_domain *const domain)
+{
+	if (domain)
+		refcount_inc(&domain->usage);
+}
+
 #endif /* _SECURITY_LANDLOCK_DOMAIN_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 4dd09ea22c84..9e560aed64b6 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -39,16 +39,16 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
 		return ERR_PTR(-ENOMEM);
 	refcount_set(&new_ruleset->usage, 1);
 	mutex_init(&new_ruleset->lock);
-	new_ruleset->root_inode = RB_ROOT;
+	new_ruleset->rules.root_inode = RB_ROOT;
 
 #if IS_ENABLED(CONFIG_INET)
-	new_ruleset->root_net_port = RB_ROOT;
+	new_ruleset->rules.root_net_port = RB_ROOT;
 #endif /* IS_ENABLED(CONFIG_INET) */
 
 	new_ruleset->num_layers = num_layers;
 	/*
 	 * hierarchy = NULL
-	 * num_rules = 0
+	 * rules.num_rules = 0
 	 * access_masks[] = 0
 	 */
 	return new_ruleset;
@@ -148,19 +148,7 @@ create_rule(const struct landlock_id id,
 static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
 				const enum landlock_key_type key_type)
 {
-	switch (key_type) {
-	case LANDLOCK_KEY_INODE:
-		return &ruleset->root_inode;
-
-#if IS_ENABLED(CONFIG_INET)
-	case LANDLOCK_KEY_NET_PORT:
-		return &ruleset->root_net_port;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
-	default:
-		WARN_ON_ONCE(1);
-		return ERR_PTR(-EINVAL);
-	}
+	return landlock_get_rule_root(&ruleset->rules, key_type);
 }
 
 static void free_rule(struct landlock_rule *const rule,
@@ -176,19 +164,24 @@ static void free_rule(struct landlock_rule *const rule,
 
 static void build_check_ruleset(void)
 {
-	const struct landlock_ruleset ruleset = {
+	const struct landlock_rules rules = {
 		.num_rules = ~0,
+	};
+	const struct landlock_ruleset ruleset = {
 		.num_layers = ~0,
 	};
 
-	BUILD_BUG_ON(ruleset.num_rules < LANDLOCK_MAX_NUM_RULES);
+	BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
 	BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
 }
 
 /**
- * insert_rule - Create and insert a rule in a ruleset
+ * insert_rule - Create and insert a rule into the rule storage
  *
- * @ruleset: The ruleset to be updated.
+ * @rules: The rule storage to be updated.  The caller is responsible for
+ *         any required locking.  For rulesets, this means holding
+ *         &landlock_ruleset.lock.  For domains under construction, no lock is
+ *         needed because the domain is not yet visible to other tasks.
  * @id: The ID to build the new rule with.  The underlying kernel object, if
  *      any, must be held by the caller.
  * @layers: One or multiple layers to be copied into the new rule.
@@ -196,16 +189,16 @@ static void build_check_ruleset(void)
  *
  * When user space requests to add a new rule to a ruleset, @layers only
  * contains one entry and this entry is not assigned to any level.  In this
- * case, the new rule will extend @ruleset, similarly to a boolean OR between
+ * case, the new rule will extend @rules, similarly to a boolean OR between
  * access rights.
  *
  * When merging a ruleset in a domain, or copying a domain, @layers will be
- * added to @ruleset as new constraints, similarly to a boolean AND between
- * access rights.
+ * added to @rules as new constraints, similarly to a boolean AND between access
+ * rights.
  *
  * Return: 0 on success, -errno on failure.
  */
-static int insert_rule(struct landlock_ruleset *const ruleset,
+static int insert_rule(struct landlock_rules *const rules,
 		       const struct landlock_id id,
 		       const struct landlock_layer (*layers)[],
 		       const size_t num_layers)
@@ -216,14 +209,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
 	struct rb_root *root;
 
 	might_sleep();
-	lockdep_assert_held(&ruleset->lock);
 	if (WARN_ON_ONCE(!layers))
 		return -ENOENT;
 
 	if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
 		return -ENOENT;
 
-	root = get_root(ruleset, id.type);
+	root = landlock_get_rule_root(rules, id.type);
 	if (IS_ERR(root))
 		return PTR_ERR(root);
 
@@ -249,7 +241,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
 		if ((*layers)[0].level == 0) {
 			/*
 			 * Extends access rights when the request comes from
-			 * landlock_add_rule(2), i.e. @ruleset is not a domain.
+			 * landlock_add_rule(2), i.e. contained by a ruleset.
 			 */
 			if (WARN_ON_ONCE(this->num_layers != 1))
 				return -EINVAL;
@@ -278,14 +270,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
 
 	/* There is no match for @id. */
 	build_check_ruleset();
-	if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
+	if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES)
 		return -E2BIG;
 	new_rule = create_rule(id, layers, num_layers, NULL);
 	if (IS_ERR(new_rule))
 		return PTR_ERR(new_rule);
 	rb_link_node(&new_rule->node, parent_node, walker_node);
 	rb_insert_color(&new_rule->node, root);
-	ruleset->num_rules++;
+	rules->num_rules++;
 	return 0;
 }
 
@@ -319,7 +311,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
 	} };
 
 	build_check_layer();
-	return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
+	lockdep_assert_held(&ruleset->lock);
+	return insert_rule(&ruleset->rules, id, &layers, ARRAY_SIZE(layers));
 }
 
 static int merge_tree(struct landlock_ruleset *const dst,
@@ -358,7 +351,7 @@ static int merge_tree(struct landlock_ruleset *const dst,
 		layers[0].access = walker_rule->layers[0].access;
 		layers[0].flags = walker_rule->layers[0].flags;
 
-		err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
+		err = insert_rule(&dst->rules, id, &layers, ARRAY_SIZE(layers));
 		if (err)
 			return err;
 	}
@@ -432,7 +425,7 @@ static int inherit_tree(struct landlock_ruleset *const parent,
 			.type = key_type,
 		};
 
-		err = insert_rule(child, id, &walker_rule->layers,
+		err = insert_rule(&child->rules, id, &walker_rule->layers,
 				  walker_rule->num_layers);
 		if (err)
 			return err;
@@ -486,21 +479,26 @@ static int inherit_ruleset(struct landlock_ruleset *const parent,
 	return err;
 }
 
-static void free_ruleset(struct landlock_ruleset *const ruleset)
+void landlock_free_rules(struct landlock_rules *const rules)
 {
 	struct landlock_rule *freeme, *next;
 
 	might_sleep();
-	rbtree_postorder_for_each_entry_safe(freeme, next, &ruleset->root_inode,
+	rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode,
 					     node)
 		free_rule(freeme, LANDLOCK_KEY_INODE);
 
 #if IS_ENABLED(CONFIG_INET)
 	rbtree_postorder_for_each_entry_safe(freeme, next,
-					     &ruleset->root_net_port, node)
+					     &rules->root_net_port, node)
 		free_rule(freeme, LANDLOCK_KEY_NET_PORT);
 #endif /* IS_ENABLED(CONFIG_INET) */
+}
 
+static void free_ruleset(struct landlock_ruleset *const ruleset)
+{
+	might_sleep();
+	landlock_free_rules(&ruleset->rules);
 	landlock_put_hierarchy(ruleset->hierarchy);
 	kfree(ruleset);
 }
@@ -604,7 +602,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
 	const struct rb_root *root;
 	const struct rb_node *node;
 
-	root = get_root((struct landlock_ruleset *)ruleset, id.type);
+	root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+				      id.type);
 	if (IS_ERR(root))
 		return NULL;
 	node = root->rb_node;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 0437adf17428..c8c83efff4c5 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -68,13 +68,12 @@ union landlock_key {
  */
 enum landlock_key_type {
 	/**
-	 * @LANDLOCK_KEY_INODE: Type of &landlock_ruleset.root_inode's node
-	 * keys.
+	 * @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys.
 	 */
 	LANDLOCK_KEY_INODE = 1,
 	/**
-	 * @LANDLOCK_KEY_NET_PORT: Type of &landlock_ruleset.root_net_port's
-	 * node keys.
+	 * @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node
+	 * keys.
 	 */
 	LANDLOCK_KEY_NET_PORT,
 };
@@ -122,30 +121,44 @@ struct landlock_rule {
 };
 
 /**
- * struct landlock_ruleset - Landlock ruleset
+ * struct landlock_rules - Red-black tree storage for Landlock rules
  *
- * This data structure must contain unique entries, be updatable, and quick to
- * match an object.
+ * This structure holds the rule trees shared by both rulesets and domains.
  */
-struct landlock_ruleset {
+struct landlock_rules {
 	/**
 	 * @root_inode: Root of a red-black tree containing &struct
-	 * landlock_rule nodes with inode object.  Once a ruleset is tied to a
-	 * process (i.e. as a domain), this tree is immutable until @usage
-	 * reaches zero.
+	 * landlock_rule nodes with inode object.  Immutable for domains.
 	 */
 	struct rb_root root_inode;
 
 #if IS_ENABLED(CONFIG_INET)
 	/**
 	 * @root_net_port: Root of a red-black tree containing &struct
-	 * landlock_rule nodes with network port. Once a ruleset is tied to a
-	 * process (i.e. as a domain), this tree is immutable until @usage
-	 * reaches zero.
+	 * landlock_rule nodes with network port.  Immutable for domains.
 	 */
 	struct rb_root root_net_port;
 #endif /* IS_ENABLED(CONFIG_INET) */
 
+	/**
+	 * @num_rules: Number of non-overlapping (i.e. not for the same object)
+	 * rules in this tree storage.
+	 */
+	u32 num_rules;
+};
+
+/**
+ * struct landlock_ruleset - Landlock ruleset
+ *
+ * This data structure must contain unique entries, be updatable, and quick to
+ * match an object.
+ */
+struct landlock_ruleset {
+	/**
+	 * @rules: Red-black tree storage for rules.
+	 */
+	struct landlock_rules rules;
+
 	/**
 	 * @hierarchy: Enables hierarchy identification even when a parent
 	 * domain vanishes.  This is needed for the ptrace protection.
@@ -156,8 +169,8 @@ struct landlock_ruleset {
 		 * @work_free: Enables to free a ruleset within a lockless
 		 * section.  This is only used by
 		 * landlock_put_ruleset_deferred() when @usage reaches zero.
-		 * The fields @lock, @usage, @num_rules, @num_layers,
-		 * @quiet_masks and @access_masks are then unused.
+		 * The fields @lock, @usage, @num_layers, @quiet_masks and
+		 * @access_masks are then unused.
 		 */
 		struct work_struct work_free;
 		struct {
@@ -171,11 +184,6 @@ struct landlock_ruleset {
 			 * descriptors referencing this ruleset.
 			 */
 			refcount_t usage;
-			/**
-			 * @num_rules: Number of non-overlapping (i.e. not for
-			 * the same object) rules in this ruleset.
-			 */
-			u32 num_rules;
 			/**
 			 * @num_layers: Number of layers that are used in this
 			 * ruleset.  This enables to check that all the layers
@@ -221,6 +229,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
 			 const struct landlock_id id,
 			 const access_mask_t access, const u32 flags);
 
+void landlock_free_rules(struct landlock_rules *const rules);
+
 struct landlock_ruleset *
 landlock_merge_ruleset(struct landlock_ruleset *const parent,
 		       struct landlock_ruleset *const ruleset);
@@ -229,6 +239,33 @@ const struct landlock_rule *
 landlock_find_rule(const struct landlock_ruleset *const ruleset,
 		   const struct landlock_id id);
 
+/**
+ * landlock_get_rule_root - Get the root of a rule tree by key type
+ *
+ * @rules: The rules storage to look up.
+ * @key_type: The type of key to select the tree for.
+ *
+ * Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type.
+ */
+static inline struct rb_root *
+landlock_get_rule_root(struct landlock_rules *const rules,
+		       const enum landlock_key_type key_type)
+{
+	switch (key_type) {
+	case LANDLOCK_KEY_INODE:
+		return &rules->root_inode;
+
+#if IS_ENABLED(CONFIG_INET)
+	case LANDLOCK_KEY_NET_PORT:
+		return &rules->root_net_port;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+	default:
+		WARN_ON_ONCE(1);
+		return ERR_PTR(-EINVAL);
+	}
+}
+
 static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
 {
 	if (ruleset)
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 02/20] landlock: Move domain query functions to domain.c
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Grouping domain-specific code in one compilation unit reduces coupling
between domain and ruleset implementations.

Move the access-check functions that only operate on a domain (rule
lookup, layer unmasking, layer-mask init, access-mask union) from
ruleset.[ch] to domain.[ch].  They evaluate whether a domain grants a
requested access during the pathwalk and network checks and do not
modify the domain.

The merge and inherit chain stays in ruleset.c for now because it calls
the static create_ruleset() allocator; a following commit moves it once
the domain type switch eliminates that dependency.

No behavioral change.  The functions move with unchanged signatures and
bodies.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-3-mic@digikod.net
- Adapt the moved access-check helpers to the base's struct layer_masks
  (renamed from struct layer_access_masks), including the per-layer
  quiet flag.

Changes since v1:
- New patch.
---
 security/landlock/domain.c  | 162 ++++++++++++++++++++++++++++++++++++
 security/landlock/domain.h  |  38 +++++++++
 security/landlock/net.c     |   1 +
 security/landlock/ruleset.c | 150 ---------------------------------
 security/landlock/ruleset.h |  38 ---------
 5 files changed, 201 insertions(+), 188 deletions(-)

diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index c5efb7635a7b..8fb74107c2ef 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -10,11 +10,15 @@
 #include <kunit/test.h>
 #include <linux/bitops.h>
 #include <linux/bits.h>
+#include <linux/cleanup.h>
 #include <linux/cred.h>
+#include <linux/err.h>
 #include <linux/file.h>
 #include <linux/mm.h>
+#include <linux/overflow.h>
 #include <linux/path.h>
 #include <linux/pid.h>
+#include <linux/rbtree.h>
 #include <linux/refcount.h>
 #include <linux/sched.h>
 #include <linux/signal.h>
@@ -26,6 +30,7 @@
 #include "common.h"
 #include "domain.h"
 #include "id.h"
+#include "limits.h"
 #include "ruleset.h"
 
 static void free_domain(struct landlock_domain *const domain)
@@ -59,6 +64,163 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
 	}
 }
 
+/* The returned access has the same lifetime as the domain. */
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+		   const struct landlock_id id)
+{
+	const struct rb_root *root;
+	const struct rb_node *node;
+
+	root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+				      id.type);
+	if (IS_ERR(root))
+		return NULL;
+	node = root->rb_node;
+
+	while (node) {
+		struct landlock_rule *this =
+			rb_entry(node, struct landlock_rule, node);
+
+		if (this->key.data == id.key.data)
+			return this;
+		if (this->key.data < id.key.data)
+			node = node->rb_right;
+		else
+			node = node->rb_left;
+	}
+	return NULL;
+}
+
+/**
+ * landlock_unmask_layers - Remove the access rights in @masks which are
+ *                          granted in @rule
+ *
+ * Updates the set of (per-layer) unfulfilled access rights @masks so that all
+ * the access rights granted in @rule are removed from it (because they are now
+ * fulfilled).
+ *
+ * @rule: A rule that grants a set of access rights for each layer.
+ * @masks: A matrix of unfulfilled access rights for each layer.
+ *
+ * Return: True if the request is allowed (i.e. the access rights granted all
+ * remaining unfulfilled access rights and masks has no leftover set bits).
+ */
+bool landlock_unmask_layers(const struct landlock_rule *const rule,
+			    struct layer_masks *masks)
+{
+	if (!masks)
+		return true;
+	if (!rule)
+		return false;
+
+	/*
+	 * An access is granted if, for each policy layer, at least one rule
+	 * encountered on the pathwalk grants the requested access, regardless
+	 * of its position in the layer stack.  We must then check the remaining
+	 * layers for each inode, from the first added layer to the last one.
+	 * When there are multiple requested accesses, for each policy layer,
+	 * the full set of requested accesses may not be granted by only one
+	 * rule, but by the union (binary OR) of multiple rules.  For example,
+	 * /a/b <execute> + /a <read> grants /a/b <execute + read>.
+	 *
+	 * This function is called once per matching rule during the pathwalk,
+	 * progressively clearing bits in @masks.  The overall access decision
+	 * is per-layer: access is granted iff masks->layers[l].access == 0 for
+	 * all layers l.  When two independent mechanisms can each grant access
+	 * within a layer (e.g. a path rule OR a scope exception), the
+	 * composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not
+	 * (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when
+	 * different layers grant via different mechanisms.
+	 */
+	for (size_t i = 0; i < rule->num_layers; i++) {
+		const struct landlock_layer *const layer = &rule->layers[i];
+
+		/* Clear the bits where the layer in the rule grants access. */
+		masks->layers[layer->level - 1].access &= ~layer->access;
+
+#ifdef CONFIG_AUDIT
+		/* Collect rule flags for each layer. */
+		if (layer->flags.quiet)
+			masks->layers[layer->level - 1].quiet = true;
+#endif /* CONFIG_AUDIT */
+	}
+
+	for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+		if (masks->layers[i].access)
+			return false;
+	}
+	return true;
+}
+
+typedef access_mask_t
+get_access_mask_t(const struct landlock_ruleset *const ruleset,
+		  const u16 layer_level);
+
+/**
+ * landlock_init_layer_masks - Initialize layer masks from an access request
+ *
+ * Populates @masks such that for each access right in @access_request, the bits
+ * for all the layers are set where this access right is handled.  Rule flags
+ * are also zeroed.
+ *
+ * @domain: The domain that defines the current restrictions.
+ * @access_request: The requested access rights to check.
+ * @masks: Layer access masks to populate.
+ * @key_type: The key type to switch between access masks of different types.
+ *
+ * Return: An access mask where each access right bit is set which is handled in
+ * any of the active layers in @domain.
+ */
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+			  const access_mask_t access_request,
+			  struct layer_masks *const masks,
+			  const enum landlock_key_type key_type)
+{
+	access_mask_t handled_accesses = 0;
+	get_access_mask_t *get_access_mask;
+
+	switch (key_type) {
+	case LANDLOCK_KEY_INODE:
+		get_access_mask = landlock_get_fs_access_mask;
+		break;
+
+#if IS_ENABLED(CONFIG_INET)
+	case LANDLOCK_KEY_NET_PORT:
+		get_access_mask = landlock_get_net_access_mask;
+		break;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+	default:
+		WARN_ON_ONCE(1);
+		return 0;
+	}
+
+	/* An empty access request can happen because of O_WRONLY | O_RDWR. */
+	if (!access_request)
+		return 0;
+
+	for (size_t i = 0; i < domain->num_layers; i++) {
+		const access_mask_t handled = get_access_mask(domain, i);
+
+		masks->layers[i].access = access_request & handled;
+		handled_accesses |= masks->layers[i].access;
+#ifdef CONFIG_AUDIT
+		masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
+	}
+	for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
+	     i++) {
+		masks->layers[i].access = 0;
+#ifdef CONFIG_AUDIT
+		masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
+	}
+
+	return handled_accesses;
+}
+
 #ifdef CONFIG_AUDIT
 
 /**
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index bcfd0452e260..9f9b892ab17d 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -233,12 +233,50 @@ struct landlock_domain {
 	};
 };
 
+/**
+ * landlock_union_access_masks - Return all access rights handled in the
+ *				 domain
+ *
+ * @domain: Landlock ruleset (used as a domain)
+ *
+ * Return: An access_masks result of the OR of all the domain's access masks.
+ */
+static inline struct access_masks
+landlock_union_access_masks(const struct landlock_ruleset *const domain)
+{
+	union access_masks_all matches = {};
+	size_t layer_level;
+
+	for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
+		union access_masks_all layer = {
+			.masks = domain->access_masks[layer_level],
+		};
+
+		matches.all |= layer.all;
+	}
+
+	return matches.masks;
+}
+
 void landlock_put_domain(struct landlock_domain *const domain);
 void landlock_put_domain_deferred(struct landlock_domain *const domain);
 
 DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
 	    if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
 
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+		   const struct landlock_id id);
+
+bool landlock_unmask_layers(const struct landlock_rule *const rule,
+			    struct layer_masks *masks);
+
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+			  const access_mask_t access_request,
+			  struct layer_masks *masks,
+			  const enum landlock_key_type key_type);
+
 static inline void landlock_get_domain(struct landlock_domain *const domain)
 {
 	if (domain)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 46c17116fcf4..feecc8c77ab4 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -15,6 +15,7 @@
 #include "audit.h"
 #include "common.h"
 #include "cred.h"
+#include "domain.h"
 #include "limits.h"
 #include "net.h"
 #include "ruleset.h"
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 9e560aed64b6..96e65804d2ff 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -591,153 +591,3 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
 
 	return no_free_ptr(new_dom);
 }
-
-/*
- * The returned access has the same lifetime as @ruleset.
- */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
-		   const struct landlock_id id)
-{
-	const struct rb_root *root;
-	const struct rb_node *node;
-
-	root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
-				      id.type);
-	if (IS_ERR(root))
-		return NULL;
-	node = root->rb_node;
-
-	while (node) {
-		struct landlock_rule *this =
-			rb_entry(node, struct landlock_rule, node);
-
-		if (this->key.data == id.key.data)
-			return this;
-		if (this->key.data < id.key.data)
-			node = node->rb_right;
-		else
-			node = node->rb_left;
-	}
-	return NULL;
-}
-
-/**
- * landlock_unmask_layers - Remove the access rights in @masks
- *                          which are granted in @rule
- *
- * Updates the set of (per-layer) unfulfilled access rights @masks
- * so that all the access rights granted in @rule are removed from it
- * (because they are now fulfilled).
- *
- * @rule: A rule that grants a set of access rights for each layer
- * @masks: A matrix of unfulfilled access rights for each layer
- *
- * Return: True if the request is allowed (i.e. the access rights granted all
- * remaining unfulfilled access rights and masks has no leftover set bits).
- */
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_masks *masks)
-{
-	if (!masks)
-		return true;
-	if (!rule)
-		return false;
-
-	/*
-	 * An access is granted if, for each policy layer, at least one rule
-	 * encountered on the pathwalk grants the requested access,
-	 * regardless of its position in the layer stack.  We must then check
-	 * the remaining layers for each inode, from the first added layer to
-	 * the last one.  When there is multiple requested accesses, for each
-	 * policy layer, the full set of requested accesses may not be granted
-	 * by only one rule, but by the union (binary OR) of multiple rules.
-	 * E.g. /a/b <execute> + /a <read> => /a/b <execute + read>
-	 */
-	for (size_t i = 0; i < rule->num_layers; i++) {
-		const struct landlock_layer *const layer = &rule->layers[i];
-
-		/* Clear the bits where the layer in the rule grants access. */
-		masks->layers[layer->level - 1].access &= ~layer->access;
-
-#ifdef CONFIG_AUDIT
-		/* Collect rule flags for each layer. */
-		if (layer->flags.quiet)
-			masks->layers[layer->level - 1].quiet = true;
-#endif /* CONFIG_AUDIT */
-	}
-
-	for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
-		if (masks->layers[i].access)
-			return false;
-	}
-	return true;
-}
-
-typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
-		  const u16 layer_level);
-
-/**
- * landlock_init_layer_masks - Initialize layer masks from an access request
- *
- * Populates @masks such that for each access right in @access_request, the bits
- * for all the layers are set where this access right is handled.  Rule flags
- * are also zeroed.
- *
- * @domain: The domain that defines the current restrictions.
- * @access_request: The requested access rights to check.
- * @masks: Layer access masks to populate.
- * @key_type: The key type to switch between access masks of different types.
- *
- * Return: An access mask where each access right bit is set which is handled
- * in any of the active layers in @domain.
- */
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
-			  const access_mask_t access_request,
-			  struct layer_masks *const masks,
-			  const enum landlock_key_type key_type)
-{
-	access_mask_t handled_accesses = 0;
-	get_access_mask_t *get_access_mask;
-
-	switch (key_type) {
-	case LANDLOCK_KEY_INODE:
-		get_access_mask = landlock_get_fs_access_mask;
-		break;
-
-#if IS_ENABLED(CONFIG_INET)
-	case LANDLOCK_KEY_NET_PORT:
-		get_access_mask = landlock_get_net_access_mask;
-		break;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
-	default:
-		WARN_ON_ONCE(1);
-		return 0;
-	}
-
-	/* An empty access request can happen because of O_WRONLY | O_RDWR. */
-	if (!access_request)
-		return 0;
-
-	for (size_t i = 0; i < domain->num_layers; i++) {
-		const access_mask_t handled = get_access_mask(domain, i);
-
-		masks->layers[i].access = access_request & handled;
-		handled_accesses |= masks->layers[i].access;
-#ifdef CONFIG_AUDIT
-		masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
-	}
-	for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
-	     i++) {
-		masks->layers[i].access = 0;
-#ifdef CONFIG_AUDIT
-		masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
-	}
-
-	return handled_accesses;
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index c8c83efff4c5..f9486c06b7ec 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -235,10 +235,6 @@ struct landlock_ruleset *
 landlock_merge_ruleset(struct landlock_ruleset *const parent,
 		       struct landlock_ruleset *const ruleset);
 
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
-		   const struct landlock_id id);
-
 /**
  * landlock_get_rule_root - Get the root of a rule tree by key type
  *
@@ -272,31 +268,6 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
 		refcount_inc(&ruleset->usage);
 }
 
-/**
- * landlock_union_access_masks - Return all access rights handled in the
- *				 domain
- *
- * @domain: Landlock ruleset (used as a domain)
- *
- * Return: An access_masks result of the OR of all the domain's access masks.
- */
-static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
-{
-	union access_masks_all matches = {};
-	size_t layer_level;
-
-	for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
-		union access_masks_all layer = {
-			.masks = domain->access_masks[layer_level],
-		};
-
-		matches.all |= layer.all;
-	}
-
-	return matches.masks;
-}
-
 static inline void
 landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
 			    const access_mask_t fs_access_mask,
@@ -355,13 +326,4 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
 	return ruleset->access_masks[layer_level].scope;
 }
 
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_masks *masks);
-
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
-			  const access_mask_t access_request,
-			  struct layer_masks *masks,
-			  const enum landlock_key_type key_type);
-
 #endif /* _SECURITY_LANDLOCK_RULESET_H */
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 05/20] landlock: Decouple the per-denial logging decision from CONFIG_AUDIT
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Until now, whether a denial is logged was decided inside
landlock_audit_denial(): a per-execution flag check (log_same_exec or
log_new_exec, selected by the credential's domain_exec bitmask),
preceded by a LANDLOCK_LOG_DISABLED early return in
landlock_log_denial() for domains an ancestor fully quieted.

Factor that decision into a single is_denial_logged() helper called once
by landlock_log_denial(), and pass its result to landlock_audit_denial()
as a "logged" boolean.  A following commit passes the same boolean to
the deny tracepoints, so audit and tracing share one decision that stays
correct as new log state is added, and a tracepoints-only build
(CONFIG_AUDIT=n) computes it identically.  Computing the logged verdict
once in the shared helper makes audit and tracing apply identical
filtering, so they cannot report different logged= values for the same
denial as log controls grow.

Move the LANDLOCK_LOG_DISABLED gate out of landlock_log_denial() into
the decision so num_denials counts every denial, including those a
domain quiets.  This was previously masked: the only reader of
num_denials is the audit "domain deallocated" record, emitted only for
domains that reached LANDLOCK_LOG_RECORDED; a fully quieted domain never
records, so its undercount was never observable.  A following commit
adds a free_domain tracepoint that reports num_denials, which needs the
full count.

This is not a functional change for audit: the logged decision and the
audit_enabled gate are preserved, so the emitted records are identical.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
- New patch.
---
 security/landlock/audit.c | 90 ++++-----------------------------------
 security/landlock/audit.h | 14 ++----
 security/landlock/log.c   | 88 ++++++++++++++++++++++++++++++++++++--
 3 files changed, 97 insertions(+), 95 deletions(-)

diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index dfa1e5a64aac..32260e7cbfe9 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -136,104 +136,32 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
 	WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
 }
 
-static access_mask_t
-pick_access_mask_for_request_type(const enum landlock_request_type type,
-				  const struct access_masks access_masks)
-{
-	switch (type) {
-	case LANDLOCK_REQUEST_FS_ACCESS:
-		return access_masks.fs;
-	case LANDLOCK_REQUEST_NET_ACCESS:
-		return access_masks.net;
-	default:
-		WARN_ONCE(1, "Invalid request type %d passed to %s", type,
-			  __func__);
-		return 0;
-	}
-}
-
 /**
  * landlock_audit_denial - Create an audit record for a denied access request
  *
- * @subject: The Landlock subject's credential denying an action.
  * @request: Detail of the user space request.
  * @youngest_denied: The youngest hierarchy node that denied the access.
- * @youngest_layer: The layer index of @youngest_denied.
  * @missing: The set of denied access rights.
- * @object_quiet_flag: Whether the object denied by @youngest_denied is
- *                     covered by a quiet rule in that layer.
+ * @logged: Whether the denial is selected for logging, as computed by
+ *          landlock_log_denial() (domain policy and quiet rules).
  *
- * Called from landlock_log_denial() with the same arguments.
+ * Emits the record when audit is enabled and the denial is selected for
+ * logging.
  */
-void landlock_audit_denial(const struct landlock_cred_security *const subject,
-			   const struct landlock_request *const request,
+void landlock_audit_denial(const struct landlock_request *const request,
 			   struct landlock_hierarchy *const youngest_denied,
-			   const size_t youngest_layer,
-			   const access_mask_t missing,
-			   const bool object_quiet_flag)
+			   const access_mask_t missing, const bool logged)
 {
 	struct audit_buffer *ab;
-	bool quiet_applicable_to_access = false;
 
 	if (!audit_enabled)
 		return;
 
-	/* Checks if the current exec was restricting itself. */
-	if (subject->domain_exec & BIT(youngest_layer)) {
-		/* Ignores denials for the same execution. */
-		if (!youngest_denied->log_same_exec)
-			return;
-	} else {
-		/* Ignores denials after a new execution. */
-		if (!youngest_denied->log_new_exec)
-			return;
-	}
-
 	/*
-	 * Checks if the object is marked quiet by the layer that denied the
-	 * request.  If it's a different layer that marked it as quiet, but that
-	 * layer is not the one that denied the request, we should still audit
-	 * log the denial.
+	 * Skips denials the domain's policy or a quiet rule excludes from
+	 * logging (folded into @logged by landlock_log_denial()).
 	 */
-	if (object_quiet_flag) {
-		/*
-		 * We now check if the denied requests are all covered by the
-		 * layer's quiet access bits.
-		 */
-		const access_mask_t quiet_mask =
-			pick_access_mask_for_request_type(
-				request->type, youngest_denied->quiet_masks);
-
-		quiet_applicable_to_access = (quiet_mask & missing) == missing;
-	} else {
-		/*
-		 * Either the object is not quiet, or this is a scope request.
-		 * We check request->type to distinguish between the two cases.
-		 */
-		const access_mask_t quiet_mask =
-			youngest_denied->quiet_masks.scope;
-
-		switch (request->type) {
-		case LANDLOCK_REQUEST_SCOPE_SIGNAL:
-			quiet_applicable_to_access =
-				!!(quiet_mask & LANDLOCK_SCOPE_SIGNAL);
-			break;
-		case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
-			quiet_applicable_to_access =
-				!!(quiet_mask &
-				   LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
-			break;
-		/*
-		 * Leave LANDLOCK_REQUEST_PTRACE and
-		 * LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they
-		 * are never quiet.
-		 */
-		default:
-			break;
-		}
-	}
-
-	if (quiet_applicable_to_access)
+	if (!logged)
 		return;
 
 	/* Uses consistent allocation flags wrt common_lsm_audit(). */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 1b36bb3c6cfd..14a514065e08 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -12,18 +12,14 @@
 
 #include "access.h"
 
-struct landlock_cred_security;
 struct landlock_hierarchy;
 struct landlock_request;
 
 #ifdef CONFIG_AUDIT
 
-void landlock_audit_denial(const struct landlock_cred_security *const subject,
-			   const struct landlock_request *const request,
+void landlock_audit_denial(const struct landlock_request *const request,
 			   struct landlock_hierarchy *const youngest_denied,
-			   const size_t youngest_layer,
-			   const access_mask_t missing,
-			   const bool object_quiet_flag);
+			   const access_mask_t missing, const bool logged);
 
 void landlock_audit_free_domain(
 	const struct landlock_hierarchy *const hierarchy);
@@ -31,11 +27,9 @@ void landlock_audit_free_domain(
 #else /* CONFIG_AUDIT */
 
 static inline void
-landlock_audit_denial(const struct landlock_cred_security *const subject,
-		      const struct landlock_request *const request,
+landlock_audit_denial(const struct landlock_request *const request,
 		      struct landlock_hierarchy *const youngest_denied,
-		      const size_t youngest_layer, const access_mask_t missing,
-		      const bool object_quiet_flag)
+		      const access_mask_t missing, const bool logged)
 {
 }
 
diff --git a/security/landlock/log.c b/security/landlock/log.c
index 6dd3373c4ba4..f42e6dc4de5c 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -405,6 +405,86 @@ static bool is_valid_request(const struct landlock_request *const request)
 	return true;
 }
 
+static access_mask_t
+pick_access_mask_for_request_type(const enum landlock_request_type type,
+				  const struct access_masks access_masks)
+{
+	switch (type) {
+	case LANDLOCK_REQUEST_FS_ACCESS:
+		return access_masks.fs;
+	case LANDLOCK_REQUEST_NET_ACCESS:
+		return access_masks.net;
+	default:
+		WARN_ONCE(1, "Invalid request type %d passed to %s", type,
+			  __func__);
+		return 0;
+	}
+}
+
+/*
+ * Whether a quiet rule silences the denial: the rule must cover the whole
+ * denied access in the layer that denied it (a quiet rule in a non-denying
+ * layer does not suppress the denial).
+ */
+static bool
+is_denial_quieted(const struct landlock_request *const request,
+		  const struct landlock_hierarchy *const youngest_denied,
+		  const access_mask_t missing, const bool object_quiet_flag)
+{
+	if (object_quiet_flag) {
+		const access_mask_t quiet_mask =
+			pick_access_mask_for_request_type(
+				request->type, youngest_denied->quiet_masks);
+
+		return (quiet_mask & missing) == missing;
+	}
+
+	/*
+	 * Either the object is not quiet, or this is a scope request.  We check
+	 * request->type to distinguish between the two cases.
+	 */
+	switch (request->type) {
+	case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+		return !!(youngest_denied->quiet_masks.scope &
+			  LANDLOCK_SCOPE_SIGNAL);
+	case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+		return !!(youngest_denied->quiet_masks.scope &
+			  LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+	/*
+	 * Leave LANDLOCK_REQUEST_PTRACE and LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY
+	 * unhandled for now - they are never quiet.
+	 */
+	default:
+		return false;
+	}
+}
+
+/*
+ * Computes whether a denial from youngest_denied is selected for logging by the
+ * domain's policy: its logging must not be disabled (by both per-execution
+ * flags being off, or by an ancestor's
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF), the per-execution flag matching
+ * same_exec must be set, and no quiet rule may cover the denied access.
+ * landlock_log_denial() computes this once and passes it to
+ * landlock_audit_denial(), which additionally requires audit_enabled.
+ */
+static bool
+is_denial_logged(const struct landlock_request *const request,
+		 const struct landlock_hierarchy *const youngest_denied,
+		 const access_mask_t missing, const bool same_exec,
+		 const bool object_quiet_flag)
+{
+	if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
+		return false;
+
+	if (!(same_exec ? youngest_denied->log_same_exec :
+			  youngest_denied->log_new_exec))
+		return false;
+
+	return !is_denial_quieted(request, youngest_denied, missing,
+				  object_quiet_flag);
+}
+
 /**
  * landlock_log_denial - Log a denied access
  *
@@ -451,8 +531,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 			get_hierarchy(subject->domain, youngest_layer);
 	}
 
-	if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
-		return;
+	const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer));
+	const bool logged = is_denial_logged(request, youngest_denied, missing,
+					     same_exec, object_quiet_flag);
 
 	/*
 	 * Consistently keeps track of the number of denied access requests even
@@ -461,8 +542,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 	 */
 	atomic64_inc(&youngest_denied->num_denials);
 
-	landlock_audit_denial(subject, request, youngest_denied, youngest_layer,
-			      missing, object_quiet_flag);
+	landlock_audit_denial(request, youngest_denied, missing, logged);
 }
 
 /**
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 04/20] landlock: Split denial logging from audit into common framework
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Tracepoint emission requires the denial framework (layer identification,
request validation) without depending on CONFIG_AUDIT.  Separate the
denial logging infrastructure from the audit-specific code by
introducing a common log framework.

Create CONFIG_SECURITY_LANDLOCK_LOG, enabled by default when
CONFIG_AUDIT is set; a following commit extends it to CONFIG_TRACEPOINTS
when the first tracepoint consumer is added.  Move the common framework
(the request types, the layer identification and request validation, and
the landlock_log_denial() and landlock_log_free_domain() entry points)
into log.c and log.h, and keep the audit-specific record formatting in
audit.c. log.o is built for CONFIG_SECURITY_LANDLOCK_LOG and audit.o for
CONFIG_AUDIT, so the common framework is available to a tracepoints-only
build.  The entry points dispatch to no-op static inline audit stubs
without CONFIG_AUDIT, so the call sites stay unconditional.  Rename the
former landlock_log_drop_domain() to landlock_log_free_domain() to match
the landlock_free_domain tracepoint added in a following commit.

landlock_log_denial() counts denials even without audit, so its
declaration and no-op stub are guarded by CONFIG_SECURITY_LANDLOCK_LOG,
not CONFIG_AUDIT; a CONFIG_AUDIT guard would expose the stub and clash
with log.c's definition in a tracepoints-only build.

Widen the ID allocation (id.o and the landlock_init_id() /
landlock_get_id_range() declarations) and the log-state representation
(the domain_exec and log_subdomains_off credential fields, the
landlock_hierarchy log fields, and the code that maintains them) from
CONFIG_AUDIT to CONFIG_SECURITY_LANDLOCK_LOG, so each field and its
writer share one guard and are available to tracing without audit
support.

Widen the denial-path state that feeds the per-denial logging decision
the same way, so the "logged" verdict is computed identically whether or
not CONFIG_AUDIT is set.  Widening fown_layer is what keeps the
file-owner-signal path valid without audit: otherwise
hook_file_send_sigiotask() would leave layer_plus_one at zero, tripping
the is_valid_request() canary and dropping the LANDLOCK_SCOPE_SIGNAL
denial from tracing.

The ruleset-level quiet_masks stays on no CONFIG guard: it is builder
state validated and stored from user input, kept available so
LANDLOCK_ADD_RULE_QUIET flags are accepted and ignored, not rejected,
when CONFIG_SECURITY_LANDLOCK_LOG is disabled.

Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-5-mic@digikod.net
- Split the denial logging into a common framework
  (landlock_log_denial() and landlock_log_free_domain(), layer
  identification, request validation) and the audit-specific record
  formatting (landlock_audit_denial(), landlock_audit_free_domain())
  built as separate translation units, each gated by its own CONFIG,
  instead of renaming audit.c to log.c.  The dispatchers reach the audit
  helpers through no-op stubs so the call sites stay unconditional.
- Gate CONFIG_SECURITY_LANDLOCK_LOG on CONFIG_AUDIT only; a following
  commit extends it to CONFIG_TRACEPOINTS with the first tracepoint.
- Move the log_subdomains_off credential field and the log-state writers
  (hook_bprm_creds_for_exec(), the landlock_restrict_self() log
  assignments) to CONFIG_SECURITY_LANDLOCK_LOG alongside domain_exec and
  the hierarchy log fields, so each field and its writer share one
  guard.
- Thread the base's quiet flag through the split (object_quiet
  parameter to landlock_audit_denial()).
- Widen the quiet consumer state and the file-owner-signal layer
  identification (fown_layer) from CONFIG_AUDIT to
  CONFIG_SECURITY_LANDLOCK_LOG so the per-denial logged verdict is
  computed identically whether CONFIG_AUDIT is set; the ruleset-level
  quiet_masks stays ungated to accept and ignore quiet flags when the
  log framework is disabled.

Changes since v1:
- New patch.
---
 security/landlock/Kconfig    |   5 +
 security/landlock/Makefile   |   6 +-
 security/landlock/access.h   |   4 +-
 security/landlock/audit.c    | 485 ++-------------------------------
 security/landlock/audit.h    |  61 ++---
 security/landlock/cred.c     |   8 +-
 security/landlock/cred.h     |   8 +-
 security/landlock/domain.c   |  24 +-
 security/landlock/domain.h   |  14 +-
 security/landlock/fs.c       |  27 +-
 security/landlock/fs.h       |   8 +-
 security/landlock/id.h       |   6 +-
 security/landlock/log.c      | 502 +++++++++++++++++++++++++++++++++++
 security/landlock/log.h      |  77 ++++++
 security/landlock/net.c      |   2 +-
 security/landlock/syscalls.c |  12 +-
 security/landlock/task.c     |   6 +-
 17 files changed, 685 insertions(+), 570 deletions(-)
 create mode 100644 security/landlock/log.c
 create mode 100644 security/landlock/log.h

diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig
index 3f1493402052..b4e7f0e9ba9b 100644
--- a/security/landlock/Kconfig
+++ b/security/landlock/Kconfig
@@ -21,6 +21,11 @@ config SECURITY_LANDLOCK
 	  you should also prepend "landlock," to the content of CONFIG_LSM to
 	  enable Landlock at boot time.
 
+config SECURITY_LANDLOCK_LOG
+	bool
+	depends on SECURITY_LANDLOCK
+	default y if AUDIT
+
 config SECURITY_LANDLOCK_KUNIT_TEST
 	bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
 	depends on KUNIT=y
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 23e13644916f..2462e6c5921e 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -13,6 +13,8 @@ landlock-y := \
 
 landlock-$(CONFIG_INET) += net.o
 
-landlock-$(CONFIG_AUDIT) += \
+landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
 	id.o \
-	audit.o
+	log.o
+
+landlock-$(CONFIG_AUDIT) += audit.o
diff --git a/security/landlock/access.h b/security/landlock/access.h
index 1a8227a709d9..bbbb41f41147 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -74,13 +74,13 @@ struct layer_mask {
 	 * @access: The unfulfilled access rights for this layer.
 	 */
 	access_mask_t access : LANDLOCK_NUM_ACCESS_MAX;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	/**
 	 * @quiet: Whether we have encountered a rule with the quiet flag for
 	 * this layer.  Used to control logging.
 	 */
 	access_mask_t quiet : 1;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 } __packed __aligned(sizeof(access_mask_t));
 
 /*
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 7cb00619a741..dfa1e5a64aac 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -5,7 +5,6 @@
  * Copyright © 2023-2025 Microsoft Corporation
  */
 
-#include <kunit/test.h>
 #include <linux/audit.h>
 #include <linux/bitops.h>
 #include <linux/lsm_audit.h>
@@ -18,7 +17,7 @@
 #include "cred.h"
 #include "domain.h"
 #include "limits.h"
-#include "ruleset.h"
+#include "log.h"
 
 static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
@@ -137,393 +136,6 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
 	WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
 }
 
-static struct landlock_hierarchy *
-get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
-{
-	struct landlock_hierarchy *hierarchy = domain->hierarchy;
-	ssize_t i;
-
-	if (WARN_ON_ONCE(layer >= domain->num_layers))
-		return hierarchy;
-
-	for (i = domain->num_layers - 1; i > layer; i--) {
-		if (WARN_ON_ONCE(!hierarchy->parent))
-			break;
-
-		hierarchy = hierarchy->parent;
-	}
-
-	return hierarchy;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_hierarchy(struct kunit *const test)
-{
-	struct landlock_hierarchy dom0_hierarchy = {
-		.id = 10,
-	};
-	struct landlock_hierarchy dom1_hierarchy = {
-		.parent = &dom0_hierarchy,
-		.id = 20,
-	};
-	struct landlock_hierarchy dom2_hierarchy = {
-		.parent = &dom1_hierarchy,
-		.id = 30,
-	};
-	struct landlock_domain dom2 = {
-		.hierarchy = &dom2_hierarchy,
-		.num_layers = 3,
-	};
-
-	KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
-	KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
-	KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
-	/* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-/* Get the youngest layer that denied the access_request. */
-static size_t get_denied_layer(const struct landlock_domain *const domain,
-			       access_mask_t *const access_request,
-			       const struct layer_masks *masks)
-{
-	for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
-		if (masks->layers[i].access & *access_request) {
-			*access_request &= masks->layers[i].access;
-			return i;
-		}
-	}
-
-	/* Not found - fall back to default values */
-	*access_request = 0;
-	return domain->num_layers - 1;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_denied_layer(struct kunit *const test)
-{
-	const struct landlock_domain dom = {
-		.num_layers = 5,
-	};
-	const struct layer_masks masks = {
-		.layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
-				    LANDLOCK_ACCESS_FS_READ_DIR,
-		.layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
-				    LANDLOCK_ACCESS_FS_READ_DIR,
-		.layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
-	};
-	access_mask_t access;
-
-	access = LANDLOCK_ACCESS_FS_EXECUTE;
-	KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
-
-	access = LANDLOCK_ACCESS_FS_READ_FILE;
-	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
-
-	access = LANDLOCK_ACCESS_FS_READ_DIR;
-	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
-
-	access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
-	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access,
-			LANDLOCK_ACCESS_FS_READ_FILE |
-				LANDLOCK_ACCESS_FS_READ_DIR);
-
-	access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
-	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
-
-	access = LANDLOCK_ACCESS_FS_WRITE_FILE;
-	KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
-	KUNIT_EXPECT_EQ(test, access, 0);
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-static size_t
-get_layer_from_deny_masks(access_mask_t *const access_request,
-			  const access_mask_t all_existing_optional_access,
-			  const deny_masks_t deny_masks,
-			  optional_access_t quiet_optional_accesses,
-			  bool *quiet)
-{
-	const unsigned long access_opt = all_existing_optional_access;
-	const unsigned long access_req = *access_request;
-	access_mask_t missing = 0;
-	size_t youngest_layer = 0;
-	size_t access_index = 0;
-	unsigned long access_bit;
-	bool should_quiet = false;
-
-	/* This will require change with new object types. */
-	WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
-
-	for_each_set_bit(access_bit, &access_opt,
-			 BITS_PER_TYPE(access_mask_t)) {
-		if (access_req & BIT(access_bit)) {
-			const size_t layer =
-				(deny_masks >>
-				 (access_index *
-				  HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
-				(LANDLOCK_MAX_NUM_LAYERS - 1);
-			const bool layer_has_quiet =
-				!!(quiet_optional_accesses & BIT(access_index));
-
-			if (layer > youngest_layer) {
-				youngest_layer = layer;
-				missing = BIT(access_bit);
-				should_quiet = layer_has_quiet;
-			} else if (layer == youngest_layer) {
-				missing |= BIT(access_bit);
-				/*
-				 * Whether the layer has rules with quiet flag
-				 * covering the file accessed does not depend on
-				 * the access, and so the following
-				 * WARN_ON_ONCE() should not fail.
-				 */
-				WARN_ON_ONCE(should_quiet && !layer_has_quiet);
-				should_quiet = layer_has_quiet;
-			}
-		}
-		access_index++;
-	}
-
-	*access_request = missing;
-	*quiet = should_quiet;
-	return youngest_layer;
-}
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static void test_get_layer_from_deny_masks(struct kunit *const test)
-{
-	deny_masks_t deny_mask;
-	access_mask_t access;
-	optional_access_t quiet_optional_accesses;
-	bool quiet;
-
-	/* truncate:0 ioctl_dev:2 */
-	deny_mask = 0x20;
-	quiet_optional_accesses = 0;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	/* layer denying truncate: quiet, ioctl: not quiet */
-	quiet_optional_accesses = 0b01;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-
-	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	/* Reverse order - truncate:2 ioctl_dev:0 */
-	deny_mask = 0x02;
-	quiet_optional_accesses = 0;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	/* layer denying truncate: quiet, ioctl: not quiet */
-	quiet_optional_accesses = 0b01;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-
-	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-
-	/* layer denying truncate: not quiet, ioctl: quiet */
-	quiet_optional_accesses = 0b10;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 0,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 2,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	/* truncate:15 ioctl_dev:15 */
-	deny_mask = 0xff;
-	quiet_optional_accesses = 0;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access,
-			LANDLOCK_ACCESS_FS_TRUNCATE |
-				LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, false);
-
-	/* Both quiet (same layer so quietness must be the same) */
-	quiet_optional_accesses = 0b11;
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE;
-	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-
-	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
-	KUNIT_EXPECT_EQ(test, 15,
-			get_layer_from_deny_masks(
-				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				deny_mask, quiet_optional_accesses, &quiet));
-	KUNIT_EXPECT_EQ(test, access,
-			LANDLOCK_ACCESS_FS_TRUNCATE |
-				LANDLOCK_ACCESS_FS_IOCTL_DEV);
-	KUNIT_EXPECT_EQ(test, quiet, true);
-}
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
-
-static bool is_valid_request(const struct landlock_request *const request)
-{
-	if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
-		return false;
-
-	if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
-		return false;
-
-	if (request->access) {
-		if (WARN_ON_ONCE(!(!!request->layer_masks ^
-				   !!request->all_existing_optional_access)))
-			return false;
-	} else {
-		if (WARN_ON_ONCE(request->layer_masks ||
-				 request->all_existing_optional_access))
-			return false;
-	}
-
-	if (request->deny_masks) {
-		if (WARN_ON_ONCE(!request->all_existing_optional_access))
-			return false;
-		static_assert(sizeof(request->all_existing_optional_access) ==
-			      sizeof(u32));
-		if (WARN_ON_ONCE(
-			    request->quiet_optional_accesses >=
-			    BIT(hweight32(
-				    request->all_existing_optional_access))))
-			return false;
-	}
-
-	return true;
-}
-
 static access_mask_t
 pick_access_mask_for_request_type(const enum landlock_request_type type,
 				  const struct access_masks access_masks)
@@ -541,62 +153,27 @@ pick_access_mask_for_request_type(const enum landlock_request_type type,
 }
 
 /**
- * landlock_log_denial - Create audit records related to a denial
+ * landlock_audit_denial - Create an audit record for a denied access request
  *
  * @subject: The Landlock subject's credential denying an action.
  * @request: Detail of the user space request.
+ * @youngest_denied: The youngest hierarchy node that denied the access.
+ * @youngest_layer: The layer index of @youngest_denied.
+ * @missing: The set of denied access rights.
+ * @object_quiet_flag: Whether the object denied by @youngest_denied is
+ *                     covered by a quiet rule in that layer.
+ *
+ * Called from landlock_log_denial() with the same arguments.
  */
-void landlock_log_denial(const struct landlock_cred_security *const subject,
-			 const struct landlock_request *const request)
+void landlock_audit_denial(const struct landlock_cred_security *const subject,
+			   const struct landlock_request *const request,
+			   struct landlock_hierarchy *const youngest_denied,
+			   const size_t youngest_layer,
+			   const access_mask_t missing,
+			   const bool object_quiet_flag)
 {
 	struct audit_buffer *ab;
-	struct landlock_hierarchy *youngest_denied;
-	size_t youngest_layer;
-	access_mask_t missing;
-	bool object_quiet_flag = false, quiet_applicable_to_access = false;
-
-	if (WARN_ON_ONCE(!subject || !subject->domain ||
-			 !subject->domain->hierarchy || !request))
-		return;
-
-	if (!is_valid_request(request))
-		return;
-
-	missing = request->access;
-	if (missing) {
-		/* Gets the nearest domain that denies the request. */
-		if (request->layer_masks) {
-			youngest_layer = get_denied_layer(subject->domain,
-							  &missing,
-							  request->layer_masks);
-			object_quiet_flag =
-				request->layer_masks->layers[youngest_layer]
-					.quiet;
-		} else {
-			youngest_layer = get_layer_from_deny_masks(
-				&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
-				request->deny_masks,
-				request->quiet_optional_accesses,
-				&object_quiet_flag);
-		}
-		youngest_denied =
-			get_hierarchy(subject->domain, youngest_layer);
-	} else {
-		youngest_layer = request->layer_plus_one - 1;
-		youngest_denied =
-			get_hierarchy(subject->domain, youngest_layer);
-	}
-
-	if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
-		return;
-
-	/*
-	 * 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);
+	bool quiet_applicable_to_access = false;
 
 	if (!audit_enabled)
 		return;
@@ -675,23 +252,19 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
 }
 
 /**
- * landlock_log_drop_domain - Create an audit record on domain deallocation
+ * landlock_audit_free_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_domain_deferred() called by
- * hook_cred_free().
+ * Called from landlock_log_free_domain().
  */
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+void landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
 {
 	struct audit_buffer *ab;
 
-	if (WARN_ON_ONCE(!hierarchy))
-		return;
-
 	if (!audit_enabled)
 		return;
 
@@ -712,23 +285,3 @@ void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
 			 hierarchy->id, atomic64_read(&hierarchy->num_denials));
 	audit_log_end(ab);
 }
-
-#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
-
-static struct kunit_case test_cases[] = {
-	/* clang-format off */
-	KUNIT_CASE(test_get_hierarchy),
-	KUNIT_CASE(test_get_denied_layer),
-	KUNIT_CASE(test_get_layer_from_deny_masks),
-	{}
-	/* clang-format on */
-};
-
-static struct kunit_suite test_suite = {
-	.name = "landlock_audit",
-	.test_cases = test_cases,
-};
-
-kunit_test_suite(test_suite);
-
-#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 4617a855712c..1b36bb3c6cfd 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -8,68 +8,39 @@
 #ifndef _SECURITY_LANDLOCK_AUDIT_H
 #define _SECURITY_LANDLOCK_AUDIT_H
 
-#include <linux/audit.h>
-#include <linux/lsm_audit.h>
+#include <linux/types.h>
 
 #include "access.h"
 
 struct landlock_cred_security;
 struct landlock_hierarchy;
-
-enum landlock_request_type {
-	LANDLOCK_REQUEST_PTRACE = 1,
-	LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
-	LANDLOCK_REQUEST_FS_ACCESS,
-	LANDLOCK_REQUEST_NET_ACCESS,
-	LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
-	LANDLOCK_REQUEST_SCOPE_SIGNAL,
-};
-
-/*
- * We should be careful to only use a variable of this type for
- * landlock_log_denial().  This way, the compiler can remove it entirely if
- * CONFIG_AUDIT is not set.
- */
-struct landlock_request {
-	/* Mandatory fields. */
-	enum landlock_request_type type;
-	struct common_audit_data audit;
-
-	/**
-	 * layer_plus_one: First layer level that denies the request + 1.  The
-	 * extra one is useful to detect uninitialized field.
-	 */
-	size_t layer_plus_one;
-
-	/* Required field for configurable access control. */
-	access_mask_t access;
-
-	/* Required fields for requests with layer masks. */
-	const struct layer_masks *layer_masks;
-
-	/* Required fields for requests with deny masks. */
-	const access_mask_t all_existing_optional_access;
-	deny_masks_t deny_masks;
-	optional_access_t quiet_optional_accesses;
-};
+struct landlock_request;
 
 #ifdef CONFIG_AUDIT
 
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
+void landlock_audit_denial(const struct landlock_cred_security *const subject,
+			   const struct landlock_request *const request,
+			   struct landlock_hierarchy *const youngest_denied,
+			   const size_t youngest_layer,
+			   const access_mask_t missing,
+			   const bool object_quiet_flag);
 
-void landlock_log_denial(const struct landlock_cred_security *const subject,
-			 const struct landlock_request *const request);
+void landlock_audit_free_domain(
+	const struct landlock_hierarchy *const hierarchy);
 
 #else /* CONFIG_AUDIT */
 
 static inline void
-landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+landlock_audit_denial(const struct landlock_cred_security *const subject,
+		      const struct landlock_request *const request,
+		      struct landlock_hierarchy *const youngest_denied,
+		      const size_t youngest_layer, const access_mask_t missing,
+		      const bool object_quiet_flag)
 {
 }
 
 static inline void
-landlock_log_denial(const struct landlock_cred_security *const subject,
-		    const struct landlock_request *const request)
+landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy)
 {
 }
 
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index 58b544993db4..03449c26247e 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -41,7 +41,7 @@ static void hook_cred_free(struct cred *const cred)
 		landlock_put_domain_deferred(dom);
 }
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
 {
@@ -50,16 +50,16 @@ static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm)
 	return 0;
 }
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 static struct security_hook_list landlock_hooks[] __ro_after_init = {
 	LSM_HOOK_INIT(cred_prepare, hook_cred_prepare),
 	LSM_HOOK_INIT(cred_transfer, hook_cred_transfer),
 	LSM_HOOK_INIT(cred_free, hook_cred_free),
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec),
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 };
 
 __init void landlock_add_cred_hooks(void)
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index e7830cfed041..a5ff9957949a 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -36,7 +36,7 @@ struct landlock_cred_security {
 	 */
 	struct landlock_domain *domain;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	/**
 	 * @domain_exec: Bitmask identifying the domain layers that were enforced by
 	 * the current task's executed file (i.e. no new execve(2) since
@@ -50,17 +50,17 @@ struct landlock_cred_security {
 	 * not require a current domain.
 	 */
 	u8 log_subdomains_off : 1;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 } __packed;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 /* Makes sure all layer executions can be stored. */
 static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security,
 					  domain_exec)) >=
 	      LANDLOCK_MAX_NUM_LAYERS);
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 static inline struct landlock_cred_security *
 landlock_cred(const struct cred *cred)
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 2e0d75175c2a..6444cf27bda2 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -171,11 +171,11 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
 		/* Clear the bits where the layer in the rule grants access. */
 		masks->layers[layer->level - 1].access &= ~layer->access;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		/* Collect rule flags for each layer. */
 		if (layer->flags.quiet)
 			masks->layers[layer->level - 1].quiet = true;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	}
 
 	for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
@@ -238,16 +238,16 @@ landlock_init_layer_masks(const struct landlock_domain *const domain,
 
 		masks->layers[i].access = access_request & handled;
 		handled_accesses |= masks->layers[i].access;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	}
 	for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
 	     i++) {
 		masks->layers[i].access = 0;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	}
 
 	return handled_accesses;
@@ -464,14 +464,14 @@ landlock_merge_ruleset(struct landlock_domain *const parent,
 	if (err)
 		return ERR_PTR(err);
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	return no_free_ptr(new_dom);
 }
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 /**
  * get_current_exe - Get the current's executable path, if any
@@ -582,6 +582,10 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
 	return 0;
 }
 
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+
 static deny_masks_t
 get_layer_deny_mask(const access_mask_t all_existing_optional_access,
 		    const unsigned long access_bit, const size_t layer)
@@ -753,4 +757,4 @@ kunit_test_suite(test_suite);
 
 #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index f0925297dcba..1237e3e25240 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -21,7 +21,7 @@
 #include <linux/workqueue.h>
 
 #include "access.h"
-#include "audit.h"
+#include "log.h"
 #include "ruleset.h"
 
 enum landlock_log_status {
@@ -84,7 +84,7 @@ struct landlock_hierarchy {
 	 */
 	refcount_t usage;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	/**
 	 * @log_status: Whether this domain should be logged or not.  Because
 	 * concurrent log entries may be created at the same time, it is still
@@ -119,10 +119,10 @@ struct landlock_hierarchy {
 	 * logged) if the related object is marked as quiet.
 	 */
 	struct access_masks quiet_masks;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 };
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 deny_masks_t
 landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
@@ -145,7 +145,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
 	kfree(hierarchy->details);
 }
 
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 static inline int
 landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
@@ -158,7 +158,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
 {
 }
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 static inline void
 landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy)
@@ -172,7 +172,7 @@ 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_log_free_domain(hierarchy);
 		landlock_free_hierarchy_details(hierarchy);
 		hierarchy = hierarchy->parent;
 		kfree(freeme);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 0b77d310ffd4..d39da6e9fa8c 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -42,12 +42,12 @@
 #include <uapi/linux/landlock.h>
 
 #include "access.h"
-#include "audit.h"
 #include "common.h"
 #include "cred.h"
 #include "domain.h"
 #include "fs.h"
 #include "limits.h"
+#include "log.h"
 #include "object.h"
 #include "ruleset.h"
 #include "setup.h"
@@ -932,10 +932,11 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 	path_put(&walker_path);
 
 	/*
-	 * Check CONFIG_AUDIT to enable elision of log_request_parent* and
-	 * associated caller's stack variables thanks to dead code elimination.
+	 * Check CONFIG_SECURITY_LANDLOCK_LOG to enable elision of
+	 * log_request_parent* and associated caller's stack variables thanks to
+	 * dead code elimination.
 	 */
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	if (!allowed_parent1 && log_request_parent1) {
 		log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS;
 		log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH;
@@ -951,7 +952,7 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 		log_request_parent2->access = access_masked_parent2;
 		log_request_parent2->layer_masks = layer_masks_parent2;
 	}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	return allowed_parent1 && allowed_parent2;
 }
@@ -1801,14 +1802,14 @@ static int hook_file_open(struct file *const file)
 	 * file access rights in the opened struct file.
 	 */
 	landlock_file(file)->allowed_access = allowed_access;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	landlock_file(file)->deny_masks = landlock_get_deny_masks(
 		_LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks);
 	landlock_file(file)->quiet_optional_accesses =
 		landlock_get_quiet_optional_accesses(
 			_LANDLOCK_ACCESS_FS_OPTIONAL,
 			landlock_file(file)->deny_masks, &layer_masks);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	if (access_mask_subset(open_access_request, allowed_access))
 		return 0;
@@ -1842,10 +1843,10 @@ static int hook_file_truncate(struct file *const file)
 		},
 		.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
 		.access = LANDLOCK_ACCESS_FS_TRUNCATE,
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		.deny_masks = landlock_file(file)->deny_masks,
 		.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	});
 	return -EACCES;
 }
@@ -1882,10 +1883,10 @@ static int hook_file_ioctl_common(const struct file *const file,
 		},
 		.all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL,
 		.access = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		.deny_masks = landlock_file(file)->deny_masks,
 		.quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	});
 	return -EACCES;
 }
@@ -1961,9 +1962,9 @@ static void hook_file_set_fowner(struct file *file)
 	prev_tg = landlock_file(file)->fown_tg;
 	landlock_file(file)->fown_subject = fown_subject;
 	landlock_file(file)->fown_tg = fown_tg;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	landlock_file(file)->fown_layer = fown_layer;
-#endif /* CONFIG_AUDIT*/
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	/* May be called in an RCU read-side critical section. */
 	landlock_put_domain_deferred(prev_dom);
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index b4421d9df68f..c16f24e30bd5 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -57,7 +57,7 @@ struct landlock_file_security {
 	 */
 	access_mask_t allowed_access;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	/**
 	 * @deny_masks: Domain layer levels that deny an optional access (see
 	 * _LANDLOCK_ACCESS_FS_OPTIONAL).
@@ -75,7 +75,7 @@ struct landlock_file_security {
 	 * LANDLOCK_SCOPE_SIGNAL.
 	 */
 	u8 fown_layer;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	/**
 	 * @fown_subject: Landlock credential of the task that set the PID that
@@ -97,7 +97,7 @@ struct landlock_file_security {
 	struct pid *fown_tg;
 };
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 /* Makes sure all layers can be identified. */
 /* clang-format off */
@@ -113,7 +113,7 @@ static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security,
 					  quiet_optional_accesses)) >=
 	      HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL));
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 /**
  * struct landlock_superblock_security - Superblock security blob
diff --git a/security/landlock/id.h b/security/landlock/id.h
index 45dcfb9e9a8b..2a43c2b523a8 100644
--- a/security/landlock/id.h
+++ b/security/landlock/id.h
@@ -8,18 +8,18 @@
 #ifndef _SECURITY_LANDLOCK_ID_H
 #define _SECURITY_LANDLOCK_ID_H
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 
 void __init landlock_init_id(void);
 
 u64 landlock_get_id_range(size_t number_of_ids);
 
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 static inline void __init landlock_init_id(void)
 {
 }
 
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 #endif /* _SECURITY_LANDLOCK_ID_H */
diff --git a/security/landlock/log.c b/security/landlock/log.c
new file mode 100644
index 000000000000..6dd3373c4ba4
--- /dev/null
+++ b/security/landlock/log.c
@@ -0,0 +1,502 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Log helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ */
+
+#include <kunit/test.h>
+#include <linux/bitops.h>
+#include <uapi/linux/landlock.h>
+
+#include "access.h"
+#include "audit.h"
+#include "common.h"
+#include "cred.h"
+#include "domain.h"
+#include "limits.h"
+#include "log.h"
+#include "ruleset.h"
+
+static struct landlock_hierarchy *
+get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
+{
+	struct landlock_hierarchy *hierarchy = domain->hierarchy;
+	ssize_t i;
+
+	if (WARN_ON_ONCE(layer >= domain->num_layers))
+		return hierarchy;
+
+	for (i = domain->num_layers - 1; i > layer; i--) {
+		if (WARN_ON_ONCE(!hierarchy->parent))
+			break;
+
+		hierarchy = hierarchy->parent;
+	}
+
+	return hierarchy;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_hierarchy(struct kunit *const test)
+{
+	struct landlock_hierarchy dom0_hierarchy = {
+		.id = 10,
+	};
+	struct landlock_hierarchy dom1_hierarchy = {
+		.parent = &dom0_hierarchy,
+		.id = 20,
+	};
+	struct landlock_hierarchy dom2_hierarchy = {
+		.parent = &dom1_hierarchy,
+		.id = 30,
+	};
+	struct landlock_domain dom2 = {
+		.hierarchy = &dom2_hierarchy,
+		.num_layers = 3,
+	};
+
+	KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id);
+	KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id);
+	KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id);
+	/* KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); */
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+/* Get the youngest layer that denied the access_request. */
+static size_t get_denied_layer(const struct landlock_domain *const domain,
+			       access_mask_t *const access_request,
+			       const struct layer_masks *masks)
+{
+	for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) {
+		if (masks->layers[i].access & *access_request) {
+			*access_request &= masks->layers[i].access;
+			return i;
+		}
+	}
+
+	/* Not found - fall back to default values */
+	*access_request = 0;
+	return domain->num_layers - 1;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_denied_layer(struct kunit *const test)
+{
+	const struct landlock_domain dom = {
+		.num_layers = 5,
+	};
+	const struct layer_masks masks = {
+		.layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE |
+				    LANDLOCK_ACCESS_FS_READ_DIR,
+		.layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE |
+				    LANDLOCK_ACCESS_FS_READ_DIR,
+		.layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR,
+	};
+	access_mask_t access;
+
+	access = LANDLOCK_ACCESS_FS_EXECUTE;
+	KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE);
+
+	access = LANDLOCK_ACCESS_FS_READ_FILE;
+	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE);
+
+	access = LANDLOCK_ACCESS_FS_READ_DIR;
+	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
+
+	access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR;
+	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access,
+			LANDLOCK_ACCESS_FS_READ_FILE |
+				LANDLOCK_ACCESS_FS_READ_DIR);
+
+	access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR;
+	KUNIT_EXPECT_EQ(test, 1, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR);
+
+	access = LANDLOCK_ACCESS_FS_WRITE_FILE;
+	KUNIT_EXPECT_EQ(test, 4, get_denied_layer(&dom, &access, &masks));
+	KUNIT_EXPECT_EQ(test, access, 0);
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+static size_t
+get_layer_from_deny_masks(access_mask_t *const access_request,
+			  const access_mask_t all_existing_optional_access,
+			  const deny_masks_t deny_masks,
+			  optional_access_t quiet_optional_accesses,
+			  bool *quiet)
+{
+	const unsigned long access_opt = all_existing_optional_access;
+	const unsigned long access_req = *access_request;
+	access_mask_t missing = 0;
+	size_t youngest_layer = 0;
+	size_t access_index = 0;
+	unsigned long access_bit;
+	bool should_quiet = false;
+
+	/* This will require change with new object types. */
+	WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL);
+
+	for_each_set_bit(access_bit, &access_opt,
+			 BITS_PER_TYPE(access_mask_t)) {
+		if (access_req & BIT(access_bit)) {
+			const size_t layer =
+				(deny_masks >>
+				 (access_index *
+				  HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) &
+				(LANDLOCK_MAX_NUM_LAYERS - 1);
+			const bool layer_has_quiet =
+				!!(quiet_optional_accesses & BIT(access_index));
+
+			if (layer > youngest_layer) {
+				youngest_layer = layer;
+				missing = BIT(access_bit);
+				should_quiet = layer_has_quiet;
+			} else if (layer == youngest_layer) {
+				missing |= BIT(access_bit);
+				/*
+				 * Whether the layer has rules with quiet flag
+				 * covering the file accessed does not depend on
+				 * the access, and so the following
+				 * WARN_ON_ONCE() should not fail.
+				 */
+				WARN_ON_ONCE(should_quiet && !layer_has_quiet);
+				should_quiet = layer_has_quiet;
+			}
+		}
+		access_index++;
+	}
+
+	*access_request = missing;
+	*quiet = should_quiet;
+	return youngest_layer;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_layer_from_deny_masks(struct kunit *const test)
+{
+	deny_masks_t deny_mask;
+	access_mask_t access;
+	optional_access_t quiet_optional_accesses;
+	bool quiet;
+
+	/* truncate:0 ioctl_dev:2 */
+	deny_mask = 0x20;
+	quiet_optional_accesses = 0;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* layer denying truncate: quiet, ioctl: not quiet */
+	quiet_optional_accesses = 0b01;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* Reverse order - truncate:2 ioctl_dev:0 */
+	deny_mask = 0x02;
+	quiet_optional_accesses = 0;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* layer denying truncate: quiet, ioctl: not quiet */
+	quiet_optional_accesses = 0b01;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	/* layer denying truncate: not quiet, ioctl: quiet */
+	quiet_optional_accesses = 0b10;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 0,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 2,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* truncate:15 ioctl_dev:15 */
+	deny_mask = 0xff;
+	quiet_optional_accesses = 0;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access,
+			LANDLOCK_ACCESS_FS_TRUNCATE |
+				LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, false);
+
+	/* Both quiet (same layer so quietness must be the same) */
+	quiet_optional_accesses = 0b11;
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+
+	access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV;
+	KUNIT_EXPECT_EQ(test, 15,
+			get_layer_from_deny_masks(
+				&access, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				deny_mask, quiet_optional_accesses, &quiet));
+	KUNIT_EXPECT_EQ(test, access,
+			LANDLOCK_ACCESS_FS_TRUNCATE |
+				LANDLOCK_ACCESS_FS_IOCTL_DEV);
+	KUNIT_EXPECT_EQ(test, quiet, true);
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+static bool is_valid_request(const struct landlock_request *const request)
+{
+	if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS))
+		return false;
+
+	if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access)))
+		return false;
+
+	if (request->access) {
+		if (WARN_ON_ONCE(!(!!request->layer_masks ^
+				   !!request->all_existing_optional_access)))
+			return false;
+	} else {
+		if (WARN_ON_ONCE(request->layer_masks ||
+				 request->all_existing_optional_access))
+			return false;
+	}
+
+	if (request->deny_masks) {
+		if (WARN_ON_ONCE(!request->all_existing_optional_access))
+			return false;
+		static_assert(sizeof(request->all_existing_optional_access) ==
+			      sizeof(u32));
+		if (WARN_ON_ONCE(
+			    request->quiet_optional_accesses >=
+			    BIT(hweight32(
+				    request->all_existing_optional_access))))
+			return false;
+	}
+
+	return true;
+}
+
+/**
+ * landlock_log_denial - Log a denied access
+ *
+ * @subject: The Landlock subject's credential denying an action.
+ * @request: Detail of the user space request.
+ */
+void landlock_log_denial(const struct landlock_cred_security *const subject,
+			 const struct landlock_request *const request)
+{
+	struct landlock_hierarchy *youngest_denied;
+	size_t youngest_layer;
+	access_mask_t missing;
+	bool object_quiet_flag = false;
+
+	if (WARN_ON_ONCE(!subject || !subject->domain ||
+			 !subject->domain->hierarchy || !request))
+		return;
+
+	if (!is_valid_request(request))
+		return;
+
+	missing = request->access;
+	if (missing) {
+		/* Gets the nearest domain that denies the request. */
+		if (request->layer_masks) {
+			youngest_layer = get_denied_layer(subject->domain,
+							  &missing,
+							  request->layer_masks);
+			object_quiet_flag =
+				request->layer_masks->layers[youngest_layer]
+					.quiet;
+		} else {
+			youngest_layer = get_layer_from_deny_masks(
+				&missing, _LANDLOCK_ACCESS_FS_OPTIONAL,
+				request->deny_masks,
+				request->quiet_optional_accesses,
+				&object_quiet_flag);
+		}
+		youngest_denied =
+			get_hierarchy(subject->domain, youngest_layer);
+	} else {
+		youngest_layer = request->layer_plus_one - 1;
+		youngest_denied =
+			get_hierarchy(subject->domain, youngest_layer);
+	}
+
+	if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
+		return;
+
+	/*
+	 * 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);
+
+	landlock_audit_denial(subject, request, youngest_denied, youngest_layer,
+			      missing, object_quiet_flag);
+}
+
+/**
+ * landlock_log_free_domain - Log domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Called in a work queue scheduled by landlock_put_domain_deferred() called by
+ * hook_cred_free().
+ */
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+	if (WARN_ON_ONCE(!hierarchy))
+		return;
+
+	landlock_audit_free_domain(hierarchy);
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static struct kunit_case test_cases[] = {
+	/* clang-format off */
+	KUNIT_CASE(test_get_hierarchy),
+	KUNIT_CASE(test_get_denied_layer),
+	KUNIT_CASE(test_get_layer_from_deny_masks),
+	{}
+	/* clang-format on */
+};
+
+static struct kunit_suite test_suite = {
+	.name = "landlock_log",
+	.test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
diff --git a/security/landlock/log.h b/security/landlock/log.h
new file mode 100644
index 000000000000..25afc17cf055
--- /dev/null
+++ b/security/landlock/log.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Log helpers
+ *
+ * Copyright © 2023-2025 Microsoft Corporation
+ */
+
+#ifndef _SECURITY_LANDLOCK_LOG_H
+#define _SECURITY_LANDLOCK_LOG_H
+
+#include <linux/lsm_audit.h>
+
+#include "access.h"
+
+struct landlock_cred_security;
+struct landlock_hierarchy;
+
+enum landlock_request_type {
+	LANDLOCK_REQUEST_PTRACE = 1,
+	LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
+	LANDLOCK_REQUEST_FS_ACCESS,
+	LANDLOCK_REQUEST_NET_ACCESS,
+	LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
+	LANDLOCK_REQUEST_SCOPE_SIGNAL,
+};
+
+/*
+ * We should be careful to only use a variable of this type for
+ * landlock_log_denial().  This way, the compiler can remove it entirely if
+ * CONFIG_SECURITY_LANDLOCK_LOG is not set.
+ */
+struct landlock_request {
+	/* Mandatory fields. */
+	enum landlock_request_type type;
+	struct common_audit_data audit;
+
+	/**
+	 * layer_plus_one: First layer level that denies the request + 1.  The
+	 * extra one is useful to detect uninitialized field.
+	 */
+	size_t layer_plus_one;
+
+	/* Required field for configurable access control. */
+	access_mask_t access;
+
+	/* Required fields for requests with layer masks. */
+	const struct layer_masks *layer_masks;
+
+	/* Required fields for requests with deny masks. */
+	const access_mask_t all_existing_optional_access;
+	deny_masks_t deny_masks;
+	optional_access_t quiet_optional_accesses;
+};
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+
+void landlock_log_free_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_SECURITY_LANDLOCK_LOG */
+
+static inline void
+landlock_log_free_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)
+{
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+#endif /* _SECURITY_LANDLOCK_LOG_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 7447738ca2e6..e27b3ba15664 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -12,11 +12,11 @@
 #include <linux/socket.h>
 #include <net/ipv6.h>
 
-#include "audit.h"
 #include "common.h"
 #include "cred.h"
 #include "domain.h"
 #include "limits.h"
+#include "log.h"
 #include "net.h"
 #include "ruleset.h"
 
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index fe8505dc0ba5..0ad20a6f9564 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -574,11 +574,11 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
 	new_llcred = landlock_cred(new_cred);
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 	prev_log_subdomains = !new_llcred->log_subdomains_off;
 	new_llcred->log_subdomains_off = !prev_log_subdomains ||
 					 !log_subdomains;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 	/*
 	 * The only case when a ruleset may not be set is if
@@ -600,20 +600,20 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 			return PTR_ERR(new_dom);
 		}
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		new_dom->hierarchy->log_same_exec = log_same_exec;
 		new_dom->hierarchy->log_new_exec = log_new_exec;
 		if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
 			new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 		/* Replaces the old (prepared) domain. */
 		landlock_put_domain(new_llcred->domain);
 		new_llcred->domain = new_dom;
 
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		new_llcred->domain_exec |= BIT(new_dom->num_layers - 1);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	}
 
 	if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) {
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 40e6bfa4bc75..c0da22736ea0 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -20,11 +20,11 @@
 #include <net/af_unix.h>
 #include <net/sock.h>
 
-#include "audit.h"
 #include "common.h"
 #include "cred.h"
 #include "domain.h"
 #include "fs.h"
+#include "log.h"
 #include "ruleset.h"
 #include "setup.h"
 #include "task.h"
@@ -435,9 +435,9 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
 			.type = LSM_AUDIT_DATA_TASK,
 			.u.tsk = tsk,
 		},
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 	});
 	return -EPERM;
 }
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 08/20] landlock: Add create_ruleset and free_ruleset tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Add the first Landlock tracepoints, for ruleset lifecycle:
landlock_create_ruleset fires from the landlock_create_ruleset() syscall
handler, and landlock_free_ruleset fires in free_ruleset() before the
ruleset is freed.

These tracepoints, and the ones added by the following commits, share a
common design.  Rather than one polymorphic event distinguished by a
status field (as audit uses a shared record type with a "status="
field), each lifecycle transition and denial type gets its own event
with a type-safe TP_PROTO, giving precise ftrace filtering by event name
and type-safe eBPF access.  TP_PROTO passes the object pointer and the
fields are read from it in TP_fast_assign, so an eBPF program reads the
full object state (rules, access masks, hierarchy) via BTF from a single
pointer rather than from the flattened TP_STRUCT__entry fields.  The
whole cost is paid only when a tracer is attached; the static branch is
not taken otherwise.  Trace fields carry the bare access-right and scope
names (read_file), reusing the audit name tables; audit prepends the
category (fs.read_file), which the trace event name already conveys.
The trace header's DOC comment documents the consistency and locking
guarantees these events share.

create_ruleset needs no lock because the ruleset is not yet shared (its
file descriptor is not yet installed).  The deallocation events use the
"free_" prefix, not "drop_", because they fire when the object is
actually freed.

Add trace.c, built for CONFIG_TRACEPOINTS, which defines
CREATE_TRACE_POINTS, and extend CONFIG_SECURITY_LANDLOCK_LOG to also be
selected by CONFIG_TRACEPOINTS so the common log framework is available
to a tracepoints-only build.

Add an id field to struct landlock_ruleset, gated on CONFIG_TRACEPOINTS
and assigned from landlock_get_id_range() at creation.  Only the
tracepoints consume it (audit identifies domains, not rulesets), so it
does not exist in an audit-only build.  The Landlock ID is a stable u64
that names the ruleset across the trace stream and uses the same scheme
as audit, so a ruleset can be correlated between trace and audit
records.

Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-7-mic@digikod.net
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
  convention (requested by Steven Rostedt).
- Render the handled access masks as symbolic names with
  __print_flags(), shared with the audit blocker names.
- Move the id.h declaration guard widening (landlock_init_id,
  landlock_get_id_range) to the earlier commit that builds id.o under
  CONFIG_SECURITY_LANDLOCK_LOG, so intermediate patches build without
  CONFIG_AUDIT.
- Refine the trace-event consistency-guarantee DOC comment (observable
  from user space, balanced creation and deallocation events) and
  document why create_ruleset emits before the file descriptor is
  installed.
- Define CREATE_TRACE_POINTS in a dedicated trace.c built for
  CONFIG_TRACEPOINTS, and extend CONFIG_SECURITY_LANDLOCK_LOG to be
  selected by CONFIG_TRACEPOINTS here (the first tracepoint consumer),
  rather than placing CREATE_TRACE_POINTS in the audit and log
  translation unit.
- Gate the struct landlock_ruleset id field on CONFIG_TRACEPOINTS (only
  tracing uses it) instead of CONFIG_SECURITY_LANDLOCK_LOG.

Changes since v1:
- New patch (split from the v1 add_rule_fs tracepoint patch).
---
 MAINTAINERS                     |   1 +
 include/linux/landlock.h        |   1 +
 include/trace/events/landlock.h | 139 ++++++++++++++++++++++++++++++++
 security/landlock/Kconfig       |   2 +-
 security/landlock/Makefile      |   2 +
 security/landlock/ruleset.c     |   8 ++
 security/landlock/ruleset.h     |   9 +++
 security/landlock/syscalls.c    |  11 +++
 security/landlock/trace.c       |  17 ++++
 9 files changed, 189 insertions(+), 1 deletion(-)
 create mode 100644 include/trace/events/landlock.h
 create mode 100644 security/landlock/trace.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 91de7e3c2836..ca41aec9993c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14599,6 +14599,7 @@ F:	Documentation/security/landlock.rst
 F:	Documentation/userspace-api/landlock.rst
 F:	fs/ioctl.c
 F:	include/linux/landlock.h
+F:	include/trace/events/landlock.h
 F:	include/uapi/linux/landlock.h
 F:	samples/landlock/
 F:	security/landlock/
diff --git a/include/linux/landlock.h b/include/linux/landlock.h
index cabe6c784833..004cbd0b9298 100644
--- a/include/linux/landlock.h
+++ b/include/linux/landlock.h
@@ -9,6 +9,7 @@
 #ifndef _LINUX_LANDLOCK_H
 #define _LINUX_LANDLOCK_H
 
+#include <linux/types.h>
 #include <uapi/linux/landlock.h>
 
 /*
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
new file mode 100644
index 000000000000..2fb717055cc8
--- /dev/null
+++ b/include/trace/events/landlock.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM landlock
+
+#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_LANDLOCK_H
+
+#include <linux/landlock.h>
+#include <linux/tracepoint.h>
+
+struct landlock_ruleset;
+
+/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
+#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
+
+/**
+ * DOC: Landlock trace events
+ *
+ * These guarantees and constraints hold for every Landlock tracepoint.
+ * A new tracepoint must uphold them, and an eBPF consumer can rely on
+ * them.
+ *
+ * Lifecycle consistency
+ * ~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Lifecycle events are balanced: a creation event always has a matching
+ * deallocation event and vice versa, so an eBPF program can model object
+ * lifetimes from the trace stream without reconciliation logic.  A creation
+ * event fires while the object is still private to the calling thread
+ * (landlock_create_ruleset fires before the ruleset's file descriptor is
+ * installed, so it cannot race a concurrent :manpage:`close(2)`); if fd
+ * installation later fails and the ruleset is freed, free_ruleset still
+ * fires, keeping the pair balanced.  The domain pair (create_domain and
+ * free_domain) is balanced the same way: create_domain fires when the
+ * domain is created (under the ruleset lock, before thread-sync), and
+ * free_domain fires when it is freed.  A rare thread-sync failure aborts
+ * the just-created domain, which then emits both events (its creation, then
+ * an immediate free).  Denial events fire only for denials that actually
+ * happen.
+ *
+ * Pointer access
+ * ~~~~~~~~~~~~~~
+ *
+ * All pointer arguments in TP_PROTO are guaranteed non-NULL by the
+ * caller, but pointers reached through them may still be NULL (e.g.,
+ * hierarchy->parent at a root domain) and must be checked.  eBPF programs
+ * read these pointers via BTF for richer introspection than the
+ * TP_STRUCT__entry fields, which serve TP_printk display only.
+ *
+ * Mutable object pointers are passed while the caller holds the object's
+ * lock, so TP_fast_assign and a BTF reader see the exact object the event
+ * reports, a snapshot no concurrent writer can change: add_rule holds the
+ * modified ruleset's lock, and create_domain holds the ruleset lock across
+ * the emission (before the thread-sync wait) so the inspected ruleset is
+ * the one merged into the domain.  Objects immutable at the emission site
+ * (a domain after creation, a hierarchy at its last reference) need no
+ * lock.  A few values that no held lock protects are a best-effort
+ * lockless snapshot instead: a task's comm, and the deny_access_net struct
+ * sock (whose network hook holds no socket lock), matching how the sched
+ * and signal trace events sample comm.
+ */
+
+/**
+ * landlock_create_ruleset - New ruleset created
+ *
+ * @ruleset: Newly created ruleset (never NULL); not yet shared via an fd,
+ *           so no lock is needed.
+ *
+ * Emitted by sys_landlock_create_ruleset() while the new ruleset is still
+ * private to the calling thread, before its file descriptor is installed,
+ * so it cannot race a concurrent :manpage:`close(2)`.  Balanced by a
+ * matching landlock_free_ruleset event.
+ */
+TRACE_EVENT(landlock_create_ruleset,
+
+	TP_PROTO(const struct landlock_ruleset *ruleset),
+
+	TP_ARGS(ruleset),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		ruleset_id	)
+		__field(	access_mask_t,	handled_fs	)
+		__field(	access_mask_t,	handled_net	)
+		__field(	access_mask_t,	scoped		)
+	),
+
+	TP_fast_assign(
+		__entry->ruleset_id	= ruleset->id;
+		__entry->handled_fs	= ruleset->handled_masks.fs;
+		__entry->handled_net	= ruleset->handled_masks.net;
+		__entry->scoped		= ruleset->handled_masks.scope;
+	),
+
+	TP_printk("ruleset=%llx handled_fs=%s handled_net=%s scoped=%s",
+		__entry->ruleset_id,
+		__print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES),
+		__print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES),
+		__print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES))
+);
+
+/**
+ * landlock_free_ruleset - Ruleset freed
+ *
+ * @ruleset: Ruleset being freed (never NULL); at its last reference, so no
+ *           lock is needed.
+ *
+ * Emitted when a ruleset's last reference is dropped (typically when
+ * the creating process closes the ruleset file descriptor).  Fires even
+ * when file-descriptor installation failed after creation, keeping the
+ * create/free pair balanced.
+ */
+TRACE_EVENT(landlock_free_ruleset,
+
+	TP_PROTO(const struct landlock_ruleset *ruleset),
+
+	TP_ARGS(ruleset),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		ruleset_id	)
+	),
+
+	TP_fast_assign(
+		__entry->ruleset_id	= ruleset->id;
+	),
+
+	TP_printk("ruleset=%llx", __entry->ruleset_id)
+);
+
+#undef _LANDLOCK_NAME_ENTRY
+
+#endif /* _TRACE_LANDLOCK_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig
index b4e7f0e9ba9b..7aeac29160e8 100644
--- a/security/landlock/Kconfig
+++ b/security/landlock/Kconfig
@@ -24,7 +24,7 @@ config SECURITY_LANDLOCK
 config SECURITY_LANDLOCK_LOG
 	bool
 	depends on SECURITY_LANDLOCK
-	default y if AUDIT
+	default y if AUDIT || TRACEPOINTS
 
 config SECURITY_LANDLOCK_KUNIT_TEST
 	bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 2462e6c5921e..2711f4876939 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -18,3 +18,5 @@ landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
 	log.o
 
 landlock-$(CONFIG_AUDIT) += audit.o
+
+landlock-$(CONFIG_TRACEPOINTS) += trace.o
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 13edb77f07a0..3bfee53177d8 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -23,10 +23,13 @@
 #include <uapi/linux/landlock.h>
 
 #include "access.h"
+#include "id.h"
 #include "limits.h"
 #include "object.h"
 #include "ruleset.h"
 
+#include <trace/events/landlock.h>
+
 struct landlock_ruleset *
 landlock_create_ruleset(const access_mask_t fs_access_mask,
 			const access_mask_t net_access_mask,
@@ -50,6 +53,10 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
 	new_ruleset->rules.root_net_port = RB_ROOT;
 #endif /* IS_ENABLED(CONFIG_INET) */
 
+#ifdef CONFIG_TRACEPOINTS
+	new_ruleset->id = landlock_get_id_range(1);
+#endif /* CONFIG_TRACEPOINTS */
+
 	/* Should already be checked in landlock_create_ruleset(). */
 	if (fs_access_mask) {
 		const access_mask_t mask = fs_access_mask &
@@ -325,6 +332,7 @@ void landlock_free_rules(struct landlock_rules *const rules)
 static void free_ruleset(struct landlock_ruleset *const ruleset)
 {
 	might_sleep();
+	trace_landlock_free_ruleset(ruleset);
 	landlock_free_rules(&ruleset->rules);
 	kfree(ruleset);
 }
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index da6a12a8e066..ca1fd5f4c417 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -4,6 +4,7 @@
  *
  * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
  * Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
  */
 
 #ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -164,6 +165,14 @@ struct landlock_ruleset {
 	 * @usage: Number of file descriptors referencing this ruleset.
 	 */
 	refcount_t usage;
+
+#ifdef CONFIG_TRACEPOINTS
+	/**
+	 * @id: Unique identifier for this ruleset, used for tracing.
+	 */
+	u64 id;
+#endif /* CONFIG_TRACEPOINTS */
+
 	/**
 	 * @quiet_masks: Stores the quiet flags for an unmerged ruleset.  For a
 	 * merged domain, this is stored in each layer's struct
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 0ad20a6f9564..dbc4facf00b6 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -38,6 +38,8 @@
 #include "setup.h"
 #include "tsync.h"
 
+#include <trace/events/landlock.h>
+
 static bool is_initialized(void)
 {
 	if (likely(landlock_initialized))
@@ -281,6 +283,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
 	ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
 	ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
 
+	/*
+	 * Emits before anon_inode_getfd() installs the file descriptor, while
+	 * the ruleset is still private to this thread: no lock is needed, and
+	 * the event cannot race a concurrent close() freeing the ruleset under
+	 * the tracepoint's BTF read.  This is the last point at which the
+	 * ruleset is guaranteed alive and unshared.
+	 */
+	trace_landlock_create_ruleset(ruleset);
+
 	/* Creates anonymous FD referring to the ruleset. */
 	ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
 				      ruleset, O_RDWR | O_CLOEXEC);
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
new file mode 100644
index 000000000000..aeb6eeebe42a
--- /dev/null
+++ b/security/landlock/trace.c
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#include "ruleset.h"
+
+/*
+ * Generates the tracepoint definitions in this translation unit.  The trace
+ * event header dereferences the traced objects in TP_fast_assign, so the full
+ * struct definitions (e.g. ruleset.h) must be included before it.
+ */
+#define CREATE_TRACE_POINTS
+#include <trace/events/landlock.h>
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 09/20] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Add tracepoints for Landlock rule addition, landlock_add_rule_fs for
filesystem rules and landlock_add_rule_net for network rules, so trace
consumers can correlate filesystem objects and network ports with their
rulesets.  Both are emitted under the ruleset lock (asserted in
TP_fast_assign) so an eBPF program reads the ruleset, including the rule
just inserted, in a consistent snapshot.

Add a version field to struct landlock_ruleset, gated on
CONFIG_TRACEPOINTS like the id field and incremented under the ruleset
lock on each successful landlock_add_rule(2), including when it only
extends an existing rule's access rights.  It fills the existing 4-byte
hole after usage, so the struct does not grow.  Pairing the ruleset ID
with the version lets a later restrict_self event record the exact
ruleset revision merged into a domain.

Resolve the filesystem rule's absolute path with d_absolute_path()
rather than the d_path() audit uses: d_absolute_path() produces
namespace-independent paths that do not depend on the tracer's chroot
state, making trace output deterministic regardless of mount namespace
configuration.  Distinguish the error cases as "<too_long>"
(-ENAMETOOLONG) and "<unreachable>" (anonymous files or detached
mounts).

Cc: Christian Brauner <brauner@kernel.org>
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-8-mic@digikod.net
- Order the add_rule_net tracepoint arguments access_rights before
  port, matching add_rule_fs.
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
  convention (requested by Steven Rostedt).
- Render access_rights as symbolic names with __print_flags(), shared
  with the audit blocker names, instead of raw hex.
- Drop the tautological version static assertion (the counter tracks
  add-rule operations, not rule count) and clarify that @version is
  incremented on access-right extensions too.
- Gate the version field and its writer on CONFIG_TRACEPOINTS (only
  tracing uses it) instead of CONFIG_SECURITY_LANDLOCK_LOG, matching the
  id field.
- Adapt rule insertion to the base's quiet flag (landlock_insert_rule()
  flags argument).

Changes since v1:
https://patch.msgid.link/20250523165741.693976-5-mic@digikod.net
- Added landlock_add_rule_net tracepoint for network rules.
- Dropped key=inode:0x%lx from add_rule_fs printk, using dev/ino
  instead.
- Used ruleset Landlock ID instead of kernel pointer in printk.
- Differentiated d_absolute_path() error cases (suggested by
  Tingmao Wang).
- Moved DEFINE_FREE(__putname) to include/linux/fs.h (noticed by
  Tingmao Wang).
- Added version field to struct landlock_ruleset.
- Added version to add_rule trace events (format:
  ruleset=<id>.<version>).
- Added d_absolute_path() vs d_path() rationale to commit message.
---
 include/linux/fs.h              |   1 +
 include/trace/events/landlock.h | 115 +++++++++++++++++++++++++++++++-
 security/landlock/fs.c          |  19 ++++++
 security/landlock/fs.h          |  30 +++++++++
 security/landlock/net.c         |  11 +++
 security/landlock/ruleset.c     |  13 +++-
 security/landlock/ruleset.h     |   7 ++
 7 files changed, 191 insertions(+), 5 deletions(-)

diff --git a/include/linux/fs.h b/include/linux/fs.h
index 50ce731a2b78..925517c672f3 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2587,6 +2587,7 @@ extern void __init vfs_caches_init(void);
 
 #define __getname()		kmalloc(PATH_MAX, GFP_KERNEL)
 #define __putname(name)		kfree(name)
+DEFINE_FREE(__putname, char *, if (_T) __putname(_T))
 
 void emergency_thaw_all(void);
 extern int sync_filesystem(struct super_block *);
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 2fb717055cc8..69a75cf47f65 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -14,6 +14,7 @@
 #include <linux/tracepoint.h>
 
 struct landlock_ruleset;
+struct path;
 
 /* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
 #define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
@@ -63,6 +64,14 @@ struct landlock_ruleset;
  * lockless snapshot instead: a task's comm, and the deny_access_net struct
  * sock (whose network hook holds no socket lock), matching how the sched
  * and signal trace events sample comm.
+ *
+ * Field encoding
+ * ~~~~~~~~~~~~~~
+ *
+ * Fields that mirror the Landlock UAPI use the same C types and endianness
+ * (e.g. network ports are __u64 in host endianness, like
+ * landlock_net_port_attr.port).  Per-event details, such as where a value
+ * is byte-swapped, live in the field's own kdoc.
  */
 
 /**
@@ -84,6 +93,7 @@ TRACE_EVENT(landlock_create_ruleset,
 
 	TP_STRUCT__entry(
 		__field(	__u64,		ruleset_id	)
+		__field(	__u32,		ruleset_version	)
 		__field(	access_mask_t,	handled_fs	)
 		__field(	access_mask_t,	handled_net	)
 		__field(	access_mask_t,	scoped		)
@@ -91,13 +101,14 @@ TRACE_EVENT(landlock_create_ruleset,
 
 	TP_fast_assign(
 		__entry->ruleset_id	= ruleset->id;
+		__entry->ruleset_version = ruleset->version;
 		__entry->handled_fs	= ruleset->handled_masks.fs;
 		__entry->handled_net	= ruleset->handled_masks.net;
 		__entry->scoped		= ruleset->handled_masks.scope;
 	),
 
-	TP_printk("ruleset=%llx handled_fs=%s handled_net=%s scoped=%s",
-		__entry->ruleset_id,
+	TP_printk("ruleset=%llx.%u handled_fs=%s handled_net=%s scoped=%s",
+		__entry->ruleset_id, __entry->ruleset_version,
 		__print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES),
 		__print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES),
 		__print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES))
@@ -122,13 +133,111 @@ TRACE_EVENT(landlock_free_ruleset,
 
 	TP_STRUCT__entry(
 		__field(	__u64,		ruleset_id	)
+		__field(	__u32,		ruleset_version	)
+	),
+
+	TP_fast_assign(
+		__entry->ruleset_id	= ruleset->id;
+		__entry->ruleset_version = ruleset->version;
+	),
+
+	TP_printk("ruleset=%llx.%u",
+		__entry->ruleset_id, __entry->ruleset_version)
+);
+
+/**
+ * landlock_add_rule_fs - Filesystem rule added to a ruleset
+ *
+ * @ruleset: Source ruleset (never NULL).
+ * @access_rights: Effective access mask stored in the rule, not the raw
+ *                 sys_landlock_add_rule() argument (unhandled rights
+ *                 added).
+ * @path: Filesystem path for the rule (never NULL).
+ * @pathname: Resolved absolute path string (never NULL; error placeholder
+ *            on resolution failure).
+ *
+ * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
+ * the reported ruleset is a stable snapshot that no concurrent writer can
+ * change.
+ */
+TRACE_EVENT(landlock_add_rule_fs,
+
+	TP_PROTO(const struct landlock_ruleset *ruleset,
+		 access_mask_t access_rights, const struct path *path,
+		 const char *pathname),
+
+	TP_ARGS(ruleset, access_rights, path, pathname),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		ruleset_id	)
+		__field(	__u32,		ruleset_version	)
+		__field(	access_mask_t,	access_rights	)
+		__field(	dev_t,		dev		)
+		__field(	ino_t,		ino		)
+		__string(	pathname,	pathname	)
+	),
+
+	TP_fast_assign(
+		lockdep_assert_held(&ruleset->lock);
+		__entry->ruleset_id	= ruleset->id;
+		__entry->ruleset_version = ruleset->version;
+		__entry->access_rights	= access_rights;
+		__entry->dev		= path->dentry->d_sb->s_dev;
+		/*
+		 * The inode number may not be the user-visible one,
+		 * but it will be the same used by audit.
+		 */
+		__entry->ino		= d_backing_inode(path->dentry)->i_ino;
+		__assign_str(pathname);
+	),
+
+	TP_printk("ruleset=%llx.%u access_rights=%s dev=%u:%u ino=%lu path=%s",
+		__entry->ruleset_id, __entry->ruleset_version,
+		__print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_FS_NAMES),
+		MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+		__print_untrusted_str(pathname))
+);
+
+/**
+ * landlock_add_rule_net - Network port rule added to a ruleset
+ *
+ * @ruleset: Source ruleset (never NULL).
+ * @access_rights: Effective access mask stored in the rule, not the raw
+ *                 sys_landlock_add_rule() argument (unhandled rights
+ *                 added).
+ * @port: Network port, the landlock_net_port_attr.port UAPI value
+ *        forwarded directly.
+ *
+ * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
+ * the reported ruleset is a stable snapshot that no concurrent writer can
+ * change.
+ */
+TRACE_EVENT(landlock_add_rule_net,
+
+	TP_PROTO(const struct landlock_ruleset *ruleset,
+		 access_mask_t access_rights, __u64 port),
+
+	TP_ARGS(ruleset, access_rights, port),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		ruleset_id	)
+		__field(	__u32,		ruleset_version	)
+		__field(	access_mask_t,	access_rights	)
+		__field(	__u64,		port		)
 	),
 
 	TP_fast_assign(
+		lockdep_assert_held(&ruleset->lock);
 		__entry->ruleset_id	= ruleset->id;
+		__entry->ruleset_version = ruleset->version;
+		__entry->access_rights	= access_rights;
+		__entry->port		= port;
 	),
 
-	TP_printk("ruleset=%llx", __entry->ruleset_id)
+	TP_printk("ruleset=%llx.%u access_rights=%s port=%llu",
+		__entry->ruleset_id, __entry->ruleset_version,
+		__print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_NET_NAMES),
+		__entry->port)
 );
 
 #undef _LANDLOCK_NAME_ENTRY
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index d39da6e9fa8c..48744a21d0a3 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -52,6 +52,8 @@
 #include "ruleset.h"
 #include "setup.h"
 
+#include <trace/events/landlock.h>
+
 /* Underlying object management */
 
 static void release_inode(struct landlock_object *const object)
@@ -346,7 +348,24 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 		return PTR_ERR(id.key.object);
 	mutex_lock(&ruleset->lock);
 	err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+	/*
+	 * Emit after the rule insertion succeeds, so every event corresponds to
+	 * a rule that is actually in the ruleset.  The ruleset lock is still
+	 * held for BTF consistency (enforced by lockdep_assert_held in
+	 * TP_fast_assign).
+	 */
+	if (!err && trace_landlock_add_rule_fs_enabled()) {
+		char *buffer __free(__putname) = __getname();
+		const char *pathname =
+			buffer ? resolve_path_for_trace(path, buffer) :
+				 "<no_mem>";
+
+		trace_landlock_add_rule_fs(ruleset, access_rights, path,
+					   pathname);
+	}
 	mutex_unlock(&ruleset->lock);
+
 	/*
 	 * No need to check for an error because landlock_insert_rule()
 	 * increments the refcount for the new object if needed.
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index c16f24e30bd5..4e3d7cbd7e1e 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -11,6 +11,7 @@
 #define _SECURITY_LANDLOCK_FS_H
 
 #include <linux/build_bug.h>
+#include <linux/cleanup.h>
 #include <linux/fs.h>
 #include <linux/init.h>
 #include <linux/rcupdate.h>
@@ -153,4 +154,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 			    const struct path *const path,
 			    access_mask_t access_hierarchy, const u32 flags);
 
+/**
+ * resolve_path_for_trace - Resolve a path for tracepoint display
+ *
+ * @path: The path to resolve.
+ * @buf: A buffer of at least PATH_MAX bytes for the resolved path.
+ *
+ * Uses d_absolute_path() to produce a namespace-independent absolute path,
+ * unlike d_path() which resolves relative to the process's chroot.  This
+ * ensures trace output is deterministic regardless of the tracer's mount
+ * namespace.
+ *
+ * Return: A pointer into @buf with the resolved path, or an error string
+ * ("<too_long>", "<unreachable>").
+ */
+static inline const char *resolve_path_for_trace(const struct path *path,
+						 char *buf)
+{
+	const char *p;
+
+	p = d_absolute_path(path, buf, PATH_MAX);
+	if (!IS_ERR_OR_NULL(p))
+		return p;
+
+	if (PTR_ERR(p) == -ENAMETOOLONG)
+		return "<too_long>";
+
+	return "<unreachable>";
+}
+
 #endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index e27b3ba15664..ead97fcfdcff 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -20,6 +20,8 @@
 #include "net.h"
 #include "ruleset.h"
 
+#include <trace/events/landlock.h>
+
 int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
 			     const u16 port, access_mask_t access_rights,
 			     const u32 flags)
@@ -37,6 +39,15 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
 
 	mutex_lock(&ruleset->lock);
 	err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+	/*
+	 * Emit after the rule insertion succeeds, so every event corresponds to
+	 * a rule that is actually in the ruleset.  The ruleset lock is still
+	 * held for BTF consistency (enforced by lockdep_assert_held in
+	 * TP_fast_assign).
+	 */
+	if (!err)
+		trace_landlock_add_rule_net(ruleset, access_rights, port);
 	mutex_unlock(&ruleset->lock);
 
 	return err;
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 3bfee53177d8..b78714047ddf 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -4,6 +4,7 @@
  *
  * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
  * Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
  */
 
 #include <linux/bits.h>
@@ -306,11 +307,19 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
 			.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
 		},
 	} };
+	int err;
 
 	build_check_layer();
 	lockdep_assert_held(&ruleset->lock);
-	return landlock_rule_insert(&ruleset->rules, id, &layers,
-				    ARRAY_SIZE(layers));
+	err = landlock_rule_insert(&ruleset->rules, id, &layers,
+				   ARRAY_SIZE(layers));
+
+#ifdef CONFIG_TRACEPOINTS
+	if (!err)
+		ruleset->version++;
+#endif /* CONFIG_TRACEPOINTS */
+
+	return err;
 }
 
 void landlock_free_rules(struct landlock_rules *const rules)
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index ca1fd5f4c417..799d9b3cc205 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -167,6 +167,13 @@ struct landlock_ruleset {
 	refcount_t usage;
 
 #ifdef CONFIG_TRACEPOINTS
+	/**
+	 * @version: Counter incremented on each successful
+	 * landlock_add_rule(2), including when it only extends an existing
+	 * rule's access rights.  Used by tracepoints to correlate a domain with
+	 * the exact ruleset state it was created from.  Protected by @lock.
+	 */
+	u32 version;
 	/**
 	 * @id: Unique identifier for this ruleset, used for tracing.
 	 */
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 06/20] landlock: Consolidate access-right and scope names in a shared header
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Audit formats denial records with per-right name strings.  A following
commit adds trace events that print the same access and scope masks with
__print_flags() and need the same names, but a trace event header cannot
include Landlock-internal headers, so the names cannot be shared from
the logging unit.

Define the filesystem, network, and scope names once, as the
_LANDLOCK_ACCESS_FS_NAMES, _LANDLOCK_ACCESS_NET_NAMES, and
_LANDLOCK_SCOPE_NAMES lists in the public Landlock header.  Each entry
is a _LANDLOCK_NAME_ENTRY() the consumer expands: audit maps it to a
"[bit] = name" array slot for an O(1) lookup, the trace events map it to
a __print_flags() { mask, name } pair.  The bit value comes only from
the LANDLOCK_* UAPI constant each entry references, so every bit-to-name
mapping has a single source and does not depend on entry order.

The shared names are unprefixed; blocker_prefix() prepends the
fs./net./scope. category for audit records, so the scope names move from
inline literals to the shared table too.  Audit records are unchanged.

No functional change.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
- New patch.
---
 MAINTAINERS               |  1 +
 include/linux/landlock.h  | 55 +++++++++++++++++++++++++
 security/landlock/audit.c | 84 ++++++++++++++++++++++++---------------
 3 files changed, 109 insertions(+), 31 deletions(-)
 create mode 100644 include/linux/landlock.h

diff --git a/MAINTAINERS b/MAINTAINERS
index a674e36529f7..91de7e3c2836 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14598,6 +14598,7 @@ F:	Documentation/admin-guide/LSM/landlock.rst
 F:	Documentation/security/landlock.rst
 F:	Documentation/userspace-api/landlock.rst
 F:	fs/ioctl.c
+F:	include/linux/landlock.h
 F:	include/uapi/linux/landlock.h
 F:	samples/landlock/
 F:	security/landlock/
diff --git a/include/linux/landlock.h b/include/linux/landlock.h
new file mode 100644
index 000000000000..cabe6c784833
--- /dev/null
+++ b/include/linux/landlock.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Public types and definitions
+ *
+ * Copyright © 2016-2026 Mickaël Salaün <mic@digikod.net>
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _LINUX_LANDLOCK_H
+#define _LINUX_LANDLOCK_H
+
+#include <uapi/linux/landlock.h>
+
+/*
+ * Access-right and scope names, shared between the audit records (get_blocker()
+ * in security/landlock/audit.c) and the trace events
+ * (include/trace/events/landlock.h).  A consumer defines
+ * _LANDLOCK_NAME_ENTRY(mask, name) before expanding a list and undefines it
+ * afterwards: audit maps each entry to a "[bit] = name" slot for O(1) lookup,
+ * the trace events map it to a __print_flags() { mask, name } pair.  The bit
+ * value lives only in the LANDLOCK_* UAPI constant each entry references.
+ * Names are unprefixed; audit prepends the "fs."/"net."/"scope." category.
+ */
+#define _LANDLOCK_ACCESS_FS_NAMES \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_EXECUTE, "execute"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_WRITE_FILE, "write_file"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_FILE, "read_file"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_DIR, "read_dir"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_DIR, "remove_dir"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_FILE, "remove_file"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_CHAR, "make_char"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_DIR, "make_dir"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_REG, "make_reg"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SOCK, "make_sock"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_FIFO, "make_fifo"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_BLOCK, "make_block"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SYM, "make_sym"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REFER, "refer"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_TRUNCATE, "truncate"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_IOCTL_DEV, "ioctl_dev"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_RESOLVE_UNIX, "resolve_unix")
+
+#define _LANDLOCK_ACCESS_NET_NAMES \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_TCP, "bind_tcp"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_TCP, "connect_tcp"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_UDP, "bind_udp"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, \
+			     "connect_send_udp")
+
+#define _LANDLOCK_SCOPE_NAMES \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, \
+			     "abstract_unix_socket"), \
+	_LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_SIGNAL, "signal")
+
+#endif /* _LINUX_LANDLOCK_H */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 32260e7cbfe9..e02963834e48 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -7,6 +7,7 @@
 
 #include <linux/audit.h>
 #include <linux/bitops.h>
+#include <linux/landlock.h>
 #include <linux/lsm_audit.h>
 #include <linux/pid.h>
 #include <uapi/linux/landlock.h>
@@ -19,38 +20,28 @@
 #include "limits.h"
 #include "log.h"
 
-static const char *const fs_access_strings[] = {
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
-	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
-};
+/*
+ * Access-right and scope names are built from the lists shared with the trace
+ * events (see <linux/landlock.h>).  The designated initializer places each name
+ * at its bit index, so the lookup stays O(1) and does not depend on the entry
+ * order.  log_blockers() adds the "fs."/"net."/"scope." category prefix.
+ */
+#define _LANDLOCK_NAME_ENTRY(mask, name) [BIT_INDEX(mask)] = name
+
+static const char *const fs_access_strings[] = { _LANDLOCK_ACCESS_FS_NAMES };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
 
-static const char *const net_access_strings[] = {
-	[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
-	[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
-	[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_udp",
-	[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)] =
-		"net.connect_send_udp",
-};
+static const char *const net_access_strings[] = { _LANDLOCK_ACCESS_NET_NAMES };
 
 static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
 
+static const char *const scope_strings[] = { _LANDLOCK_SCOPE_NAMES };
+
+static_assert(ARRAY_SIZE(scope_strings) == LANDLOCK_NUM_SCOPE);
+
+#undef _LANDLOCK_NAME_ENTRY
+
 static __attribute_const__ const char *
 get_blocker(const enum landlock_request_type type,
 	    const unsigned long access_bit)
@@ -62,7 +53,7 @@ get_blocker(const enum landlock_request_type type,
 
 	case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
 		WARN_ON_ONCE(access_bit != -1);
-		return "fs.change_topology";
+		return "change_topology";
 
 	case LANDLOCK_REQUEST_FS_ACCESS:
 		if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
@@ -76,32 +67,63 @@ get_blocker(const enum landlock_request_type type,
 
 	case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
 		WARN_ON_ONCE(access_bit != -1);
-		return "scope.abstract_unix_socket";
+		return scope_strings[BIT_INDEX(
+			LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)];
 
 	case LANDLOCK_REQUEST_SCOPE_SIGNAL:
 		WARN_ON_ONCE(access_bit != -1);
-		return "scope.signal";
+		return scope_strings[BIT_INDEX(LANDLOCK_SCOPE_SIGNAL)];
 	}
 
 	WARN_ON_ONCE(1);
 	return "unknown";
 }
 
+/*
+ * Returns the audit category prefix prepended to the unprefixed blocker name
+ * returned by get_blocker() (filesystem and network access rights,
+ * change_topology, and scopes).  The ptrace blocker is standalone and carries
+ * its full name in get_blocker(), so it uses no prefix.
+ */
+static __attribute_const__ const char *
+blocker_prefix(const enum landlock_request_type type)
+{
+	switch (type) {
+	case LANDLOCK_REQUEST_PTRACE:
+		return "";
+
+	case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+	case LANDLOCK_REQUEST_FS_ACCESS:
+		return "fs.";
+
+	case LANDLOCK_REQUEST_NET_ACCESS:
+		return "net.";
+
+	case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+	case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+		return "scope.";
+	}
+
+	WARN_ON_ONCE(1);
+	return "";
+}
+
 static void log_blockers(struct audit_buffer *const ab,
 			 const enum landlock_request_type type,
 			 const access_mask_t access)
 {
 	const unsigned long access_mask = access;
+	const char *const prefix = blocker_prefix(type);
 	unsigned long access_bit;
 	bool is_first = true;
 
 	for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
-		audit_log_format(ab, "%s%s", is_first ? "" : ",",
+		audit_log_format(ab, "%s%s%s", is_first ? "" : ",", prefix,
 				 get_blocker(type, access_bit));
 		is_first = false;
 	}
 	if (is_first)
-		audit_log_format(ab, "%s", get_blocker(type, -1));
+		audit_log_format(ab, "%s%s", prefix, get_blocker(type, -1));
 }
 
 static void log_domain(struct landlock_hierarchy *const hierarchy)
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 07/20] tracing: Add __print_untrusted_str()
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Landlock tracepoints expose filesystem paths and process names that may
contain spaces, equal signs, or other characters that break ftrace field
parsing.

Add a __print_untrusted_str() TP_printk helper that escapes separators
(space, equal sign), quotes, backslashes, and non-printable bytes via
string_escape_mem(), so an untrusted string cannot inject field
separators or control characters into the ftrace output and can be
unambiguously recovered.

This guarantee applies to the in-kernel ftrace text output (trace,
trace_pipe).  Userspace libtraceevent (perf, trace-cmd) does not yet
parse this helper, so a companion libtraceevent change is needed for
those consumers to pretty-print the field.

Cc: Günther Noack <gnoack@google.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-6-mic@digikod.net
- Add a Return: kdoc for trace_print_untrusted_str_seq() and rename its
  declaration parameter to src.

Changes since v1:
https://patch.msgid.link/20250523165741.693976-4-mic@digikod.net
- Remove WARN_ON() (pointed out by Steven Rostedt).
---
 include/linux/trace_events.h               |  2 +
 include/trace/stages/stage3_trace_output.h |  4 ++
 include/trace/stages/stage7_class_define.h |  1 +
 kernel/trace/trace_output.c                | 44 ++++++++++++++++++++++
 4 files changed, 51 insertions(+)

diff --git a/include/linux/trace_events.h b/include/linux/trace_events.h
index 308c76b57d13..4ece9b8d9d6b 100644
--- a/include/linux/trace_events.h
+++ b/include/linux/trace_events.h
@@ -60,6 +60,8 @@ trace_print_hex_dump_seq(struct trace_seq *p, const char *prefix_str,
 			 int prefix_type, int rowsize, int groupsize,
 			 const void *buf, size_t len, bool ascii);
 
+const char *trace_print_untrusted_str_seq(struct trace_seq *s, const char *src);
+
 int trace_raw_output_prep(struct trace_iterator *iter,
 			  struct trace_event *event);
 extern __printf(2, 3)
diff --git a/include/trace/stages/stage3_trace_output.h b/include/trace/stages/stage3_trace_output.h
index 181b81335781..0235dbd11b52 100644
--- a/include/trace/stages/stage3_trace_output.h
+++ b/include/trace/stages/stage3_trace_output.h
@@ -133,6 +133,10 @@
 	trace_print_hex_dump_seq(p, prefix_str, prefix_type,		\
 				 rowsize, groupsize, buf, len, ascii)
 
+#undef __print_untrusted_str
+#define __print_untrusted_str(str)					\
+		trace_print_untrusted_str_seq(p, __get_str(str))
+
 #undef __print_ns_to_secs
 #define __print_ns_to_secs(value)			\
 	({						\
diff --git a/include/trace/stages/stage7_class_define.h b/include/trace/stages/stage7_class_define.h
index 47008897a795..f29a82e7a4c4 100644
--- a/include/trace/stages/stage7_class_define.h
+++ b/include/trace/stages/stage7_class_define.h
@@ -24,6 +24,7 @@
 #undef __print_array
 #undef __print_dynamic_array
 #undef __print_hex_dump
+#undef __print_untrusted_str
 #undef __get_buf
 
 #undef __event_in_hardirq
diff --git a/kernel/trace/trace_output.c b/kernel/trace/trace_output.c
index a5ad76175d10..f0a9b29b3e36 100644
--- a/kernel/trace/trace_output.c
+++ b/kernel/trace/trace_output.c
@@ -16,6 +16,7 @@
 #include <linux/btf.h>
 #include <linux/bpf.h>
 #include <linux/hashtable.h>
+#include <linux/string_helpers.h>
 
 #include "trace_output.h"
 #include "trace_btf.h"
@@ -325,6 +326,49 @@ trace_print_hex_dump_seq(struct trace_seq *p, const char *prefix_str,
 }
 EXPORT_SYMBOL(trace_print_hex_dump_seq);
 
+/**
+ * trace_print_untrusted_str_seq - print a string after escaping characters
+ * @s: trace seq struct to write to
+ * @src: The string to print
+ *
+ * Prints a string to a trace seq after escaping all special characters,
+ * including common separators (space, equal sign), quotes, and backslashes.
+ * This transforms a string from an untrusted source (e.g. user space) to make
+ * it:
+ * - safe to parse,
+ * - easy to read (for simple strings),
+ * - easy to get back the original.
+ *
+ * Return: A pointer to the escaped string within the trace seq buffer, or
+ * %NULL if @src is %NULL or the buffer is exhausted.
+ */
+const char *trace_print_untrusted_str_seq(struct trace_seq *s,
+					   const char *src)
+{
+	int escaped_size;
+	char *buf;
+	size_t buf_size = seq_buf_get_buf(&s->seq, &buf);
+	const char *ret = trace_seq_buffer_ptr(s);
+
+	/* Buffer exhaustion is normal when the trace buffer is full. */
+	if (!src || buf_size == 0)
+		return NULL;
+
+	escaped_size = string_escape_mem(src, strlen(src), buf, buf_size,
+		ESCAPE_SPACE | ESCAPE_SPECIAL | ESCAPE_NAP | ESCAPE_APPEND |
+		ESCAPE_OCTAL, " ='\"\\");
+	if (unlikely(escaped_size >= buf_size)) {
+		/* We need some room for the final '\0'. */
+		seq_buf_set_overflow(&s->seq);
+		s->full = 1;
+		return NULL;
+	}
+	seq_buf_commit(&s->seq, escaped_size);
+	trace_seq_putc(s, 0);
+	return ret;
+}
+EXPORT_SYMBOL(trace_print_untrusted_str_seq);
+
 int trace_raw_output_prep(struct trace_iterator *iter,
 			  struct trace_event *trace_event)
 {
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 12/20] landlock: Add tracepoints for rule checking
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Merge landlock_find_rule() into landlock_unmask_layers() so rule
pointers stay inside the domain implementation while unmask checking
gets the matched rule it needs for the check_rule tracepoint.
landlock_unmask_layers() now takes a landlock_id and the domain instead
of a rule pointer.  A rename or link evaluates the same dentry against
both renamed parents, so this path now looks the rule up once per
parent; collapsing that back to a single lookup is left to a follow-up.

Emit, via the per-type wrappers unmask_layers_fs() and
unmask_layers_net(), the rights each matching rule grants at every
domain layer.  The events carry this as a dynamic per-layer array (up to
LANDLOCK_MAX_NUM_LAYERS entries) reserved from the trace ring buffer,
not the caller's stack, and rendered symbolically per layer.  A
WARN_ON_ONCE() in __trace_landlock_fill_layers() flags a rule whose
layer levels fall outside the domain range or are unsorted, a
cannot-happen case; the zero-filled slots keep the rendered output and
the array bounds safe regardless.

Setting allowed_parent2 to true for non-dom-check requests when
get_inode_id() returns false preserves the pre-refactoring behavior: a
negative dentry (no backing inode) has no matching rule, so the access
is allowed at this path component.  Before the refactoring,
landlock_unmask_layers() with a NULL rule produced this result as a side
effect; now the caller must set it explicitly.

Name the trace-only check_rule fields so each printk label equals its
ring-buffer field name and works directly as an ftrace filter: the
request field is labelled access_request= and the per-layer array is
named grants.  Values audit also logs keep audit's label (domain=,
ruleset=) so a single filter works across trace and audit.

Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-10-mic@digikod.net
- Order the check_rule tracepoint arguments with the common part
  (domain, rule) before the event-specific access_request and object.
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
  convention (requested by Steven Rostedt).
- Render the request access right as symbolic names with
  __print_flags(), shared with the audit blocker names.
- Rename the per-layer field label allowed= to grants= and render it
  symbolically via __print_landlock_layers(), intersected with the
  request to show the granted requested rights per layer (was a raw hex
  value).
- Rename struct layer_access_masks to the base's struct layer_masks.
- Rename the check_rule printk label request= to access_request= and the
  per-layer ring-buffer field layers to grants, so each trace-only field
  and its printk label share one name (usable directly as an ftrace
  filter); audit-shared labels (domain=, ruleset=) are unchanged.

Changes since v1:
https://patch.msgid.link/20250523165741.693976-6-mic@digikod.net
- Merged find-rule consolidation (v1 2/5) into this patch.
- Added check_rule_net tracepoint for network rules.
- Added get_inode_id() helper with rcu_access_pointer().
- Added allowed_parent2 behavioral fix.
---
 include/trace/events/landlock.h | 203 ++++++++++++++++++++++++++++++++
 security/landlock/domain.c      |  32 +++--
 security/landlock/domain.h      |  10 +-
 security/landlock/fs.c          | 149 +++++++++++++++--------
 security/landlock/net.c         |  21 +++-
 5 files changed, 350 insertions(+), 65 deletions(-)

diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 7f221d8fff38..c693248afe23 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -11,10 +11,13 @@
 #define _TRACE_LANDLOCK_H
 
 #include <linux/landlock.h>
+#include <linux/trace_seq.h>
 #include <linux/tracepoint.h>
 
+struct dentry;
 struct landlock_domain;
 struct landlock_hierarchy;
+struct landlock_rule;
 struct landlock_ruleset;
 struct path;
 
@@ -74,7 +77,110 @@ struct path;
  * (e.g. network ports are __u64 in host endianness, like
  * landlock_net_port_attr.port).  Per-event details, such as where a value
  * is byte-swapped, live in the field's own kdoc.
+ *
+ * Rule-check fields
+ * ~~~~~~~~~~~~~~~~~
+ *
+ * The check_rule events fire during an access check, once per matching
+ * rule, before the final allow-or-deny verdict.  They share domain (the
+ * enforcing domain being evaluated), access_request (the access mask being
+ * checked), and rule (the matching rule, with per-layer access masks).
+ */
+
+#ifdef CREATE_TRACE_POINTS
+
+/*
+ * Fills the dense per-domain-layer array layers (one access mask per layer,
+ * indexed by level - 1) from rule's sparse layer stack, keeping only the
+ * requested rights (access_request).  Layers with no matching rule entry get
+ * a zero mask.  Shared by the check_rule_fs and check_rule_net events.
+ *
+ * rule->layers is sorted by ascending level, with levels in the domain's
+ * [1, num_layers] range (see landlock_merge_ruleset()), so every entry maps
+ * to a slot.  A leftover entry would be a malformed rule; the zero-filled
+ * slots keep the output and the array bounds safe regardless.
  */
+static inline void
+__trace_landlock_fill_layers(access_mask_t *const layers,
+			     const size_t num_layers,
+			     const struct landlock_rule *const rule,
+			     const access_mask_t access_request)
+{
+	size_t i = 0;
+
+	for (size_t level = 1; level <= num_layers; level++) {
+		access_mask_t grants = 0;
+
+		if (i < rule->num_layers && level == rule->layers[i].level) {
+			grants = rule->layers[i].access & access_request;
+			i++;
+		}
+		layers[level - 1] = grants;
+	}
+
+	/* A leftover entry means an out-of-range or unsorted rule level. */
+	WARN_ON_ONCE(i < rule->num_layers);
+}
+
+/*
+ * Renders the dense per-domain-layer access array as symbolic flag names for
+ * the grants field: layers wrapped in "{}", flags within a layer joined by
+ * "|", layers separated by ",", an empty layer rendered as nothing.
+ * Open-codes the flag walk because trace_print_flags_seq() NUL-terminates per
+ * call and so cannot be chained into a single field.  The shared names table
+ * covers every access right, so masked bits are always named.  Returns the
+ * trace_seq position like __print_flags().
+ */
+static inline const char *
+__trace_landlock_print_layers(struct trace_seq *p,
+			      const access_mask_t *const layers,
+			      const size_t num_layers,
+			      const struct trace_print_flags *const names,
+			      const size_t names_size)
+{
+	const char *const ret = trace_seq_buffer_ptr(p);
+
+	trace_seq_putc(p, '{');
+	for (size_t i = 0; i < num_layers; i++) {
+		access_mask_t mask = layers[i];
+		bool first = true;
+
+		if (i)
+			trace_seq_putc(p, ',');
+		for (size_t j = 0; mask && j < names_size; j++) {
+			if ((mask & names[j].mask) != names[j].mask)
+				continue;
+			if (!first)
+				trace_seq_putc(p, '|');
+			trace_seq_puts(p, names[j].name);
+			mask &= ~names[j].mask;
+			first = false;
+		}
+	}
+	trace_seq_putc(p, '}');
+	trace_seq_putc(p, 0);
+	return ret;
+}
+
+#endif /* CREATE_TRACE_POINTS */
+
+/*
+ * Prints a per-layer access mask array (the dynamic array @array) as symbolic
+ * flag names using the shared @flag_names list (a _LANDLOCK_*_NAMES macro).
+ * Stays outside CREATE_TRACE_POINTS: TP_printk is expanded in the print-output
+ * pass where that macro is undefined.
+ */
+#define __print_landlock_layers(array, flag_names...)			\
+	({								\
+		static const struct trace_print_flags __layer_names[] = { \
+			flag_names					\
+		};							\
+		__trace_landlock_print_layers(				\
+			p, __get_dynamic_array(array),			\
+			__get_dynamic_array_len(array) /		\
+				sizeof(access_mask_t),			\
+			__layer_names, ARRAY_SIZE(__layer_names));	\
+	})
 
 /**
  * landlock_create_ruleset - New ruleset created
@@ -374,6 +480,103 @@ TRACE_EVENT(landlock_free_domain,
 		__entry->domain_id, __entry->denials)
 );
 
+/**
+ * landlock_check_rule_fs - Filesystem rule evaluated during access check
+ *
+ * @domain: Enforcing domain (never NULL).
+ * @rule: Matching rule with per-layer access masks (never NULL).
+ * @access_request: Access mask evaluated against the rule (the domain's
+ *                   handled mask during rename/link double-checks).
+ * @dentry: Filesystem dentry being checked (never NULL).
+ *
+ * Emitted for each rule that matches during a filesystem access check.
+ * The grants array shows the requested rights the rule grants at each
+ * domain layer.  See Documentation/trace/events-landlock.rst for how to
+ * interpret it.
+ */
+TRACE_EVENT(landlock_check_rule_fs,
+
+	TP_PROTO(const struct landlock_domain *domain,
+		 const struct landlock_rule *rule,
+		 access_mask_t access_request, const struct dentry *dentry),
+
+	TP_ARGS(domain, rule, access_request, dentry),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		domain_id	)
+		__field(	access_mask_t,	access_request	)
+		__field(	dev_t,		dev		)
+		__field(	ino_t,		ino		)
+		__dynamic_array(access_mask_t,	grants,
+				domain->num_layers)
+	),
+
+	TP_fast_assign(
+		__entry->domain_id	= domain->hierarchy->id;
+		__entry->access_request	= access_request;
+		__entry->dev		= dentry->d_sb->s_dev;
+		__entry->ino		= d_backing_inode(dentry)->i_ino;
+
+		__trace_landlock_fill_layers(__get_dynamic_array(grants),
+					     __get_dynamic_array_len(grants) /
+						     sizeof(access_mask_t),
+					     rule, access_request);
+	),
+
+	TP_printk("domain=%llx access_request=%s dev=%u:%u ino=%lu grants=%s",
+		__entry->domain_id,
+		__print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_FS_NAMES),
+		MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+		__print_landlock_layers(grants, _LANDLOCK_ACCESS_FS_NAMES))
+);
+
+/**
+ * landlock_check_rule_net - Network port rule evaluated during access check
+ *
+ * @domain: Enforcing domain (never NULL).
+ * @rule: Matching rule with per-layer access masks (never NULL).
+ * @access_request: Access mask being requested.
+ * @port: Network port being checked (host endianness).
+ *
+ * Emitted for each rule that matches during a network access check.  The
+ * grants array shows the requested rights the rule grants at each domain
+ * layer.  See Documentation/trace/events-landlock.rst for how to
+ * interpret it.
+ */
+TRACE_EVENT(landlock_check_rule_net,
+
+	TP_PROTO(const struct landlock_domain *domain,
+		 const struct landlock_rule *rule,
+		 access_mask_t access_request, __u64 port),
+
+	TP_ARGS(domain, rule, access_request, port),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		domain_id	)
+		__field(	access_mask_t,	access_request	)
+		__field(	__u64,		port		)
+		__dynamic_array(access_mask_t,	grants,
+				domain->num_layers)
+	),
+
+	TP_fast_assign(
+		__entry->domain_id	= domain->hierarchy->id;
+		__entry->access_request	= access_request;
+		__entry->port		= port;
+
+		__trace_landlock_fill_layers(__get_dynamic_array(grants),
+					     __get_dynamic_array_len(grants) /
+						     sizeof(access_mask_t),
+					     rule, access_request);
+	),
+
+	TP_printk("domain=%llx access_request=%s port=%llu grants=%s",
+		__entry->domain_id,
+		__print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_NET_NAMES),
+		__entry->port,
+		__print_landlock_layers(grants, _LANDLOCK_ACCESS_NET_NAMES))
+);
+
 #undef _LANDLOCK_NAME_ENTRY
 
 #endif /* _TRACE_LANDLOCK_H */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 082c4da68536..c20020024de4 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -97,9 +97,9 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
 }
 
 /* The returned access has the same lifetime as the domain. */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
-		   const struct landlock_id id)
+static const struct landlock_rule *
+find_rule(const struct landlock_domain *const domain,
+	  const struct landlock_id id)
 {
 	const struct rb_root *root;
 	const struct rb_node *node;
@@ -126,26 +126,38 @@ landlock_find_rule(const struct landlock_domain *const domain,
 
 /**
  * landlock_unmask_layers - Remove the access rights in @masks which are
- *                          granted in @rule
+ *                          granted by a matching rule
  *
- * Updates the set of (per-layer) unfulfilled access rights @masks so that all
- * the access rights granted in @rule are removed from it (because they are now
- * fulfilled).
+ * Looks up the rule matching @id in @domain, then updates the set of
+ * (per-layer) unfulfilled access rights @masks so that all the access rights
+ * granted by that rule are removed (because they are now fulfilled).
  *
- * @rule: A rule that grants a set of access rights for each layer.
+ * @domain: The Landlock domain to search for a matching rule.
+ * @id: Identifier for the rule target (e.g. inode, port).
  * @masks: A matrix of unfulfilled access rights for each layer.
+ * @matched_rule: Optional output for the matched rule (for tracing); set to
+ *                the matching rule when non-NULL, unchanged otherwise.
  *
  * Return: True if the request is allowed (i.e. the access rights granted all
  * remaining unfulfilled access rights and masks has no leftover set bits).
  */
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_masks *masks)
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+			    const struct landlock_id id,
+			    struct layer_masks *masks,
+			    const struct landlock_rule **matched_rule)
 {
+	const struct landlock_rule *rule;
+
 	if (!masks)
 		return true;
+
+	rule = find_rule(domain, id);
 	if (!rule)
 		return false;
 
+	if (matched_rule)
+		*matched_rule = rule;
+
 	/*
 	 * An access is granted if, for each policy layer, at least one rule
 	 * encountered on the pathwalk grants the requested access, regardless
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 8351e22016fe..5baa4a73b446 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -305,12 +305,10 @@ struct landlock_domain *
 landlock_merge_ruleset(struct landlock_domain *const parent,
 		       struct landlock_ruleset *const ruleset);
 
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
-		   const struct landlock_id id);
-
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
-			    struct layer_masks *masks);
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+			    const struct landlock_id id,
+			    struct layer_masks *masks,
+			    const struct landlock_rule **matched_rule);
 
 access_mask_t
 landlock_init_layer_masks(const struct landlock_domain *const domain,
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 48744a21d0a3..fe028aac26ae 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -376,31 +376,55 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 
 /* Access-control management */
 
-/*
- * The lifetime of the returned rule is tied to @domain.
+/**
+ * get_inode_id - Look up the Landlock object for a dentry
+ * @dentry: The dentry to look up.
+ * @id: Filled with the inode's Landlock object pointer on success.
+ *
+ * Extracts the Landlock object pointer from @dentry's inode security blob and
+ * stores it in @id for use as a rule-tree lookup key.
  *
- * Returns NULL if no rule is found or if @dentry is negative.
+ * When this returns false (negative dentry or no Landlock object), no rule can
+ * match this inode, so landlock_unmask_layers() need not be called.  Callers
+ * that gate landlock_unmask_layers() on this function must handle the NULL
+ * masks case independently, since the !masks-returns-true early-return in
+ * landlock_unmask_layers() will not be reached.  See the allowed_parent2
+ * initialization in is_access_to_paths_allowed().
+ *
+ * Return: True if a Landlock object exists for @dentry, false otherwise.
  */
-static const struct landlock_rule *
-find_rule(const struct landlock_domain *const domain,
-	  const struct dentry *const dentry)
+static bool get_inode_id(const struct dentry *const dentry,
+			 struct landlock_id *id)
 {
-	const struct landlock_rule *rule;
-	const struct inode *inode;
-	struct landlock_id id = {
-		.type = LANDLOCK_KEY_INODE,
-	};
-
 	/* Ignores nonexistent leafs. */
 	if (d_is_negative(dentry))
-		return NULL;
+		return false;
 
-	inode = d_backing_inode(dentry);
-	rcu_read_lock();
-	id.key.object = rcu_dereference(landlock_inode(inode)->object);
-	rule = landlock_find_rule(domain, id);
-	rcu_read_unlock();
-	return rule;
+	/*
+	 * rcu_access_pointer() is sufficient: the pointer is used only as a
+	 * numeric comparison key for rule lookup, not dereferenced.  The object
+	 * cannot be freed while the domain exists because the domain's rule
+	 * tree holds its own reference to it.
+	 */
+	id->key.object = rcu_access_pointer(
+		landlock_inode(d_backing_inode(dentry))->object);
+	return !!id->key.object;
+}
+
+static bool unmask_layers_fs(const struct landlock_domain *const domain,
+			     const struct landlock_id id,
+			     const access_mask_t access_request,
+			     struct layer_masks *masks,
+			     const struct dentry *const dentry)
+{
+	const struct landlock_rule *rule = NULL;
+	bool ret;
+
+	ret = landlock_unmask_layers(domain, id, masks, &rule);
+	if (rule)
+		trace_landlock_check_rule_fs(domain, rule, access_request,
+					     dentry);
+	return ret;
 }
 
 /*
@@ -781,6 +805,9 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 	bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
 	     child1_is_directory = true, child2_is_directory = true;
 	struct path walker_path;
+	struct landlock_id id = {
+		.type = LANDLOCK_KEY_INODE,
+	};
 	access_mask_t access_masked_parent1, access_masked_parent2;
 	struct layer_masks _layer_masks_child1, _layer_masks_child2;
 	struct layer_masks *layer_masks_child1 = NULL,
@@ -820,28 +847,46 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 		/* For a simple request, only check for requested accesses. */
 		access_masked_parent1 = access_request_parent1;
 		access_masked_parent2 = access_request_parent2;
+		/*
+		 * Simple requests have no parent2 to check, so parent2 is
+		 * trivially allowed.  This must be set explicitly because the
+		 * get_inode_id() gate in the pathwalk loop may prevent
+		 * landlock_unmask_layers() from being called (which would
+		 * otherwise return true for NULL masks as a side effect).
+		 */
+		allowed_parent2 = true;
 		is_dom_check = false;
 	}
 
 	if (unlikely(dentry_child1)) {
-		/*
-		 * Get the layer masks for the child dentries for use by domain
-		 * check later.
-		 */
-		if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-					      &_layer_masks_child1,
-					      LANDLOCK_KEY_INODE))
-			landlock_unmask_layers(find_rule(domain, dentry_child1),
-					       &_layer_masks_child1);
+		struct landlock_id id = {
+			.type = LANDLOCK_KEY_INODE,
+		};
+		access_mask_t handled;
+
+		handled = landlock_init_layer_masks(domain,
+						    LANDLOCK_MASK_ACCESS_FS,
+						    &_layer_masks_child1,
+						    LANDLOCK_KEY_INODE);
+		if (handled && get_inode_id(dentry_child1, &id))
+			unmask_layers_fs(domain, id, handled,
+					 &_layer_masks_child1, dentry_child1);
 		layer_masks_child1 = &_layer_masks_child1;
 		child1_is_directory = d_is_dir(dentry_child1);
 	}
 	if (unlikely(dentry_child2)) {
-		if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-					      &_layer_masks_child2,
-					      LANDLOCK_KEY_INODE))
-			landlock_unmask_layers(find_rule(domain, dentry_child2),
-					       &_layer_masks_child2);
+		struct landlock_id id = {
+			.type = LANDLOCK_KEY_INODE,
+		};
+		access_mask_t handled;
+
+		handled = landlock_init_layer_masks(domain,
+						    LANDLOCK_MASK_ACCESS_FS,
+						    &_layer_masks_child2,
+						    LANDLOCK_KEY_INODE);
+		if (handled && get_inode_id(dentry_child2, &id))
+			unmask_layers_fs(domain, id, handled,
+					 &_layer_masks_child2, dentry_child2);
 		layer_masks_child2 = &_layer_masks_child2;
 		child2_is_directory = d_is_dir(dentry_child2);
 	}
@@ -853,8 +898,6 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 	 * restriction.
 	 */
 	while (true) {
-		const struct landlock_rule *rule;
-
 		/*
 		 * If at least all accesses allowed on the destination are
 		 * already allowed on the source, respectively if there is at
@@ -895,13 +938,20 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
 				break;
 		}
 
-		rule = find_rule(domain, walker_path.dentry);
-		allowed_parent1 =
-			allowed_parent1 ||
-			landlock_unmask_layers(rule, layer_masks_parent1);
-		allowed_parent2 =
-			allowed_parent2 ||
-			landlock_unmask_layers(rule, layer_masks_parent2);
+		if (get_inode_id(walker_path.dentry, &id)) {
+			allowed_parent1 =
+				allowed_parent1 ||
+				unmask_layers_fs(domain, id,
+						 access_masked_parent1,
+						 layer_masks_parent1,
+						 walker_path.dentry);
+			allowed_parent2 =
+				allowed_parent2 ||
+				unmask_layers_fs(domain, id,
+						 access_masked_parent2,
+						 layer_masks_parent2,
+						 walker_path.dentry);
+		}
 
 		/* Stops when a rule from each layer grants access. */
 		if (allowed_parent1 && allowed_parent2)
@@ -1064,23 +1114,30 @@ static bool collect_domain_accesses(const struct landlock_domain *const domain,
 				    struct layer_masks *layer_masks_dom)
 {
 	bool ret = false;
+	access_mask_t access_masked_dom;
 
 	if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
 		return true;
 	if (is_nouser_or_private(dir))
 		return true;
 
-	if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
-				       layer_masks_dom, LANDLOCK_KEY_INODE))
+	access_masked_dom =
+		landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
+					  layer_masks_dom, LANDLOCK_KEY_INODE);
+	if (!access_masked_dom)
 		return true;
 
 	dget(dir);
 	while (true) {
 		struct dentry *parent_dentry;
+		struct landlock_id id = {
+			.type = LANDLOCK_KEY_INODE,
+		};
 
 		/* Gets all layers allowing all domain accesses. */
-		if (landlock_unmask_layers(find_rule(domain, dir),
-					   layer_masks_dom)) {
+		if (get_inode_id(dir, &id) &&
+		    unmask_layers_fs(domain, id, access_masked_dom,
+				     layer_masks_dom, dir)) {
 			/*
 			 * Stops when all handled accesses are allowed by at
 			 * least one rule in each layer.
diff --git a/security/landlock/net.c b/security/landlock/net.c
index ead97fcfdcff..8f2aaac54b33 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -53,6 +53,22 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
 	return err;
 }
 
+static bool unmask_layers_net(const struct landlock_domain *const domain,
+			      const struct landlock_id id,
+			      struct layer_masks *masks,
+			      access_mask_t access_request)
+{
+	const struct landlock_rule *rule = NULL;
+	bool ret;
+
+	ret = landlock_unmask_layers(domain, id, masks, &rule);
+	if (rule)
+		trace_landlock_check_rule_net(
+			domain, rule, access_request,
+			ntohs((__force __be16)id.key.data));
+	return ret;
+}
+
 static int current_check_access_socket(struct socket *const sock,
 				       struct sockaddr *const address,
 				       const int addrlen,
@@ -62,7 +78,6 @@ static int current_check_access_socket(struct socket *const sock,
 	unsigned short sock_family;
 	__be16 port;
 	struct layer_masks layer_masks = {};
-	const struct landlock_rule *rule;
 	struct landlock_id id = {
 		.type = LANDLOCK_KEY_NET_PORT,
 	};
@@ -248,14 +263,14 @@ static int current_check_access_socket(struct socket *const sock,
 	id.key.data = (__force uintptr_t)port;
 	BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
 
-	rule = landlock_find_rule(subject->domain, id);
 	access_request = landlock_init_layer_masks(subject->domain,
 						   access_request, &layer_masks,
 						   LANDLOCK_KEY_NET_PORT);
 	if (!access_request)
 		return 0;
 
-	if (landlock_unmask_layers(rule, &layer_masks))
+	if (unmask_layers_net(subject->domain, id, &layer_masks,
+			      access_request))
 		return 0;
 
 	audit_net.family = address->sa_family;
-- 
2.54.0


^ permalink raw reply related

* [PATCH v3 10/20] landlock: Add create_domain and free_domain tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
  To: Günther Noack, Steven Rostedt
  Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
	Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
	Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
	linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>

Add a landlock_create_domain tracepoint emitted from
landlock_restrict_self() after the new domain is created, so a consumer
can correlate the source ruleset with the resulting domain.  The
flags-only path (ruleset_fd == -1) creates no domain and emits no event.

Move the ruleset lock acquisition from landlock_merge_ruleset() to the
caller so the lock is held across both the merge and the tracepoint
emission, giving an eBPF program a consistent ruleset snapshot.  Release
it before the thread-sync: holding ruleset->lock across
landlock_restrict_sibling_threads() would deadlock a sibling blocked on
the same lock.  The event therefore fires before the (rare) thread-sync
failure path; when that path aborts the just-created domain, the
matching free_domain event fires so the create/free pair stays balanced.

Add a landlock_free_domain tracepoint that fires when a domain's
hierarchy node is freed.  The hierarchy node is the lifecycle boundary
because it represents the domain's identity and outlives the domain's
access masks, which may still be active in descendant domains.

A domain freed without ever being committed to a credential was never
visible to user space, so free_domain is suppressed for it.  This is
tracked by a new landlock_log_status value, LANDLOCK_LOG_UNCOMMITTED,
which is also the zero value so a hierarchy whose initialization failed
defaults to not observable.  A hierarchy is born UNCOMMITTED and is
promoted to LANDLOCK_LOG_PENDING (or LANDLOCK_LOG_DISABLED when logging
is off) right after its create_domain event fires; a thread-sync failure
does not reset it, so an aborted domain that already emitted
create_domain still emits the matching free_domain.  Promoting right
after the event, rather than at commit_creds() time, avoids a race: on a
successful thread-sync the sibling threads commit the new domain in
lockstep before landlock_restrict_self() returns, so the shared domain
may already have moved to LANDLOCK_LOG_RECORDED through a plain store,
and a late promotion would race that store and could unbalance the
domain allocation and deallocation audit records.

Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v2:
https://patch.msgid.link/20260406143717.1815792-9-mic@digikod.net
- Renamed the landlock_restrict_self tracepoint to
  landlock_create_domain and reordered its arguments to (domain,
  ruleset) for domain-first consistency with the other
  domain-lifecycle events.
- Reformat the TRACE_EVENT body (TP_PROTO, TP_STRUCT__entry,
  TP_fast_assign, TP_printk) to the kernel tracing convention with one
  field per line and tab-aligned columns (requested by Steven Rostedt).
- Emit the create_domain creation event under the ruleset lock, just
  after the merge and before the thread-sync wait, so an observer reads
  the exact ruleset frozen into the domain and the lock is not held
  across thread-sync (where a sibling blocked in landlock_add_rule() on
  the same lock would stall it).
- Gate free_domain on a new LANDLOCK_LOG_UNCOMMITTED status so a domain
  freed before its creation event fired emits no free_domain;
  create_domain marks the domain committed right after emitting the
  event, so a later thread-sync failure still emits the matching
  free_domain and the create/free pair stays balanced.
- Emit free_domain from landlock_trace_free_domain() in a dedicated
  tracing translation unit, and move the decoupling of the log state and
  landlock_log_free_domain() from CONFIG_AUDIT to the commit that
  separates the common log framework from audit.
- Clarify in the create_domain emission comment that the event fires
  before the infallible commit_creds().
- Clarify the free_domain kdoc: the event fires when the hierarchy
  node's refcount reaches zero, after all child domains release their
  parent reference (the earlier "all descendants freed" phrasing could
  be misread).

Changes since v1:
- New patch.
---
 include/trace/events/landlock.h | 82 +++++++++++++++++++++++++++++++++
 security/landlock/domain.c      | 28 ++++++-----
 security/landlock/domain.h      | 16 ++++++-
 security/landlock/log.c         |  6 ++-
 security/landlock/syscalls.c    | 45 +++++++++++++++++-
 security/landlock/trace.c       | 31 ++++++++++++-
 security/landlock/trace.h       | 28 +++++++++++
 7 files changed, 219 insertions(+), 17 deletions(-)
 create mode 100644 security/landlock/trace.h

diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 69a75cf47f65..cb0e21a2fa1f 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -13,6 +13,8 @@
 #include <linux/landlock.h>
 #include <linux/tracepoint.h>
 
+struct landlock_domain;
+struct landlock_hierarchy;
 struct landlock_ruleset;
 struct path;
 
@@ -240,6 +242,86 @@ TRACE_EVENT(landlock_add_rule_net,
 		__entry->port)
 );
 
+/**
+ * landlock_create_domain - New domain created
+ *
+ * @domain: Newly created domain (never NULL, immutable after creation).
+ *          @domain->hierarchy->id is its unique ID, shared with the
+ *          landlock_enforce_domain and landlock_free_domain events;
+ *          @domain->hierarchy->details holds the requesting process.
+ * @ruleset: Source ruleset frozen into the domain (never NULL).  The
+ *           ruleset lock is held across the emission, so a BPF program
+ *           reading it via BTF sees the exact merged ruleset;
+ *           @ruleset->id / @ruleset->version identify it.
+ *
+ * Emitted by sys_landlock_restrict_self() once, in the requesting
+ * thread's context, right after the merge and before thread-sync.  The
+ * flags-only path (ruleset_fd == -1) creates no domain and does not
+ * emit this event.  Paired with the per-thread landlock_enforce_domain
+ * (join on @domain->hierarchy->id) and balanced by a matching
+ * landlock_free_domain event.
+ */
+TRACE_EVENT(landlock_create_domain,
+
+	TP_PROTO(const struct landlock_domain *domain,
+		 const struct landlock_ruleset *ruleset),
+
+	TP_ARGS(domain, ruleset),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		domain_id	)
+		__field(	__u64,		parent_id	)
+		__field(	__u64,		ruleset_id	)
+		__field(	__u32,		ruleset_version	)
+	),
+
+	TP_fast_assign(
+		lockdep_assert_held(&ruleset->lock);
+		__entry->domain_id	= domain->hierarchy->id;
+		__entry->parent_id	= domain->hierarchy->parent ?
+					  domain->hierarchy->parent->id : 0;
+		__entry->ruleset_id	= ruleset->id;
+		__entry->ruleset_version = ruleset->version;
+	),
+
+	TP_printk("domain=%llx parent=%llx ruleset=%llx.%u",
+		__entry->domain_id, __entry->parent_id,
+		__entry->ruleset_id, __entry->ruleset_version)
+);
+
+/**
+ * landlock_free_domain - Domain freed
+ *
+ * @hierarchy: Hierarchy node being freed (never NULL).
+ *
+ * Emitted when the hierarchy node's last reference is dropped: its
+ * refcount reaches zero after all child domains have released their
+ * parent reference.  A committed domain is
+ * freed from a kworker via landlock_put_domain_deferred() (the credential
+ * free path runs in RCU context, where sleeping is forbidden), so the
+ * current task is not the sandboxed task that triggered the free.  Balanced
+ * by a matching landlock_create_domain event.
+ */
+TRACE_EVENT(landlock_free_domain,
+
+	TP_PROTO(const struct landlock_hierarchy *hierarchy),
+
+	TP_ARGS(hierarchy),
+
+	TP_STRUCT__entry(
+		__field(	__u64,		domain_id	)
+		__field(	__u64,		denials		)
+	),
+
+	TP_fast_assign(
+		__entry->domain_id	= hierarchy->id;
+		__entry->denials	= atomic64_read(&hierarchy->num_denials);
+	),
+
+	TP_printk("domain=%llx denials=%llu",
+		__entry->domain_id, __entry->denials)
+);
+
 #undef _LANDLOCK_NAME_ENTRY
 
 #endif /* _TRACE_LANDLOCK_H */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 6444cf27bda2..082c4da68536 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -309,31 +309,28 @@ static int merge_ruleset(struct landlock_domain *const dst,
 	if (WARN_ON_ONCE(!dst || !dst->hierarchy))
 		return -EINVAL;
 
-	mutex_lock(&src->lock);
+	lockdep_assert_held(&src->lock);
 
 	/* Stacks the new layer. */
-	if (WARN_ON_ONCE(dst->num_layers < 1)) {
-		err = -EINVAL;
-		goto out_unlock;
-	}
+	if (WARN_ON_ONCE(dst->num_layers < 1))
+		return -EINVAL;
+
 	dst->handled_masks[dst->num_layers - 1] =
 		landlock_upgrade_handled_access_masks(src->handled_masks);
 
 	/* Merges the @src inode tree. */
 	err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
 	if (err)
-		goto out_unlock;
+		return err;
 
 #if IS_ENABLED(CONFIG_INET)
 	/* Merges the @src network port tree. */
 	err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
 	if (err)
-		goto out_unlock;
+		return err;
 #endif /* IS_ENABLED(CONFIG_INET) */
 
-out_unlock:
-	mutex_unlock(&src->lock);
-	return err;
+	return 0;
 }
 
 static int inherit_tree(struct landlock_domain *const parent,
@@ -415,6 +412,8 @@ static int inherit_ruleset(struct landlock_domain *const parent,
  * The current task is requesting to be restricted.  The subjective credentials
  * must not be in an overridden state. cf. landlock_init_hierarchy_log().
  *
+ * The caller must hold @ruleset->lock.
+ *
  * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
  * failure.  If @parent is NULL, the new domain duplicates @ruleset.
  */
@@ -427,6 +426,7 @@ landlock_merge_ruleset(struct landlock_domain *const parent,
 	int err;
 
 	might_sleep();
+	lockdep_assert_held(&ruleset->lock);
 	if (WARN_ON_ONCE(!ruleset))
 		return ERR_PTR(-EINVAL);
 
@@ -575,7 +575,13 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
 
 	hierarchy->details = details;
 	hierarchy->id = landlock_get_id_range(1);
-	hierarchy->log_status = LANDLOCK_LOG_PENDING;
+	/*
+	 * The hierarchy is born unobservable: landlock_restrict_self() moves it
+	 * out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation
+	 * event, so the matching free_domain event fires for it and not for a
+	 * hierarchy whose creation was never observed.
+	 */
+	hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED;
 	hierarchy->log_same_exec = true;
 	hierarchy->log_new_exec = false;
 	atomic64_set(&hierarchy->num_denials, 0);
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 1237e3e25240..8351e22016fe 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -25,7 +25,21 @@
 #include "ruleset.h"
 
 enum landlock_log_status {
-	LANDLOCK_LOG_PENDING = 0,
+	/*
+	 * Hierarchy whose creation event has not been emitted, so it is not yet
+	 * observable from user space.  A hierarchy is born in this state (the
+	 * zero value, so a partially initialized hierarchy defaults to "not
+	 * observable") and leaves it when landlock_restrict_self() emits its
+	 * creation event, right after the merge and before the thread-sync
+	 * wait.  No trace free_domain event (and no audit deallocation record)
+	 * fires while a hierarchy is in this state, so a hierarchy that never
+	 * became observable (e.g. its initialization failed) is freed silently.
+	 * A domain aborted by a thread-sync failure already emitted its
+	 * creation event, so it is no longer UNCOMMITTED and does fire
+	 * free_domain.
+	 */
+	LANDLOCK_LOG_UNCOMMITTED = 0,
+	LANDLOCK_LOG_PENDING,
 	LANDLOCK_LOG_RECORDED,
 	LANDLOCK_LOG_DISABLED,
 };
diff --git a/security/landlock/log.c b/security/landlock/log.c
index f42e6dc4de5c..13033808bdbd 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -17,6 +17,7 @@
 #include "limits.h"
 #include "log.h"
 #include "ruleset.h"
+#include "trace.h"
 
 static struct landlock_hierarchy *
 get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
@@ -550,14 +551,15 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
  *
  * @hierarchy: The domain's hierarchy being deallocated.
  *
- * Called in a work queue scheduled by landlock_put_domain_deferred() called by
- * hook_cred_free().
+ * Called from landlock_put_domain_deferred() (via a work queue scheduled by
+ * hook_cred_free()) or directly from landlock_put_domain().
  */
 void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
 {
 	if (WARN_ON_ONCE(!hierarchy))
 		return;
 
+	landlock_trace_free_domain(hierarchy);
 	landlock_audit_free_domain(hierarchy);
 }
 
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index dbc4facf00b6..7a1ec140cf14 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -536,6 +536,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 		flags)
 {
 	struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
+	struct landlock_domain *new_dom = NULL;
 	struct cred *new_cred;
 	struct landlock_cred_security *new_llcred;
 	bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
@@ -604,18 +605,50 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 		 * manipulating the current credentials because they are
 		 * dedicated per thread.
 		 */
-		struct landlock_domain *const new_dom =
-			landlock_merge_ruleset(new_llcred->domain, ruleset);
+		mutex_lock(&ruleset->lock);
+		new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
 		if (IS_ERR(new_dom)) {
+			mutex_unlock(&ruleset->lock);
 			abort_creds(new_cred);
 			return PTR_ERR(new_dom);
 		}
+		/*
+		 * Emits the domain-creation event while @ruleset->lock is still
+		 * held, right after the merge, so an eBPF program attached to
+		 * the tracepoint reads the exact ruleset that was merged into
+		 * the domain: a consistent snapshot that a concurrent
+		 * landlock_add_rule() (which holds the same lock) cannot
+		 * modify.
+		 *
+		 * This must come before the thread-sync wait below.  Holding
+		 * @ruleset->lock across landlock_restrict_sibling_threads()
+		 * would hang: a sibling thread blocked in landlock_add_rule()
+		 * on the same @ruleset->lock cannot run the task_work that
+		 * thread-sync waits for (the lock wait is uninterruptible).
+		 * Emitting here keeps the lock off the thread-sync path.
+		 *
+		 * The trade-off is that the event fires for a domain that a
+		 * later (rare) thread-sync failure aborts.  That path emits the
+		 * matching free_domain event so the create/free pair stays
+		 * balanced (see the thread-sync error path below).
+		 */
+		trace_landlock_create_domain(new_dom, ruleset);
+		mutex_unlock(&ruleset->lock);
 
 #ifdef CONFIG_SECURITY_LANDLOCK_LOG
 		new_dom->hierarchy->log_same_exec = log_same_exec;
 		new_dom->hierarchy->log_new_exec = log_new_exec;
+		/*
+		 * The creation event fired above, so move the domain out of
+		 * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
+		 * too, even if a thread-sync failure aborts it below.  Audit
+		 * logging may still be disabled (DISABLED); tracing observes it
+		 * anyway.
+		 */
 		if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
 			new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
+		else
+			new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
 #endif /* CONFIG_SECURITY_LANDLOCK_LOG */
 
 		/* Replaces the old (prepared) domain. */
@@ -631,6 +664,14 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 		const int err = landlock_restrict_sibling_threads(
 			current_cred(), new_cred);
 		if (err) {
+			/*
+			 * Thread-sync failed (rare), so the new domain is
+			 * aborted instead of committed.  Its creation event
+			 * already fired above, so the imminent free must emit
+			 * the matching free_domain event to keep the
+			 * create/free pair balanced; no special log_status is
+			 * set here.
+			 */
 			abort_creds(new_cred);
 			return err;
 		}
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
index aeb6eeebe42a..5e6df313c7d2 100644
--- a/security/landlock/trace.c
+++ b/security/landlock/trace.c
@@ -6,12 +6,41 @@
  * Copyright © 2026 Cloudflare, Inc.
  */
 
+#include "trace.h"
+#include "domain.h"
 #include "ruleset.h"
 
 /*
  * Generates the tracepoint definitions in this translation unit.  The trace
  * event header dereferences the traced objects in TP_fast_assign, so the full
- * struct definitions (e.g. ruleset.h) must be included before it.
+ * struct definitions (e.g. ruleset.h, domain.h) must be included before it.
  */
 #define CREATE_TRACE_POINTS
 #include <trace/events/landlock.h>
+
+/**
+ * landlock_trace_free_domain - Emit a tracepoint on domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Fires only for a hierarchy whose creation event was emitted, i.e. one that
+ * left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self().  This keeps the
+ * create/free pair balanced: a hierarchy that never became observable is freed
+ * silently, while a domain that landlock_restrict_self() created and a
+ * thread-sync failure then aborted still fires free_domain, because its
+ * creation event already fired.
+ *
+ * Called from landlock_log_free_domain().
+ */
+void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+	/*
+	 * The log_status read is a correctness guard (keep the create/free pair
+	 * balanced), not a cost guard, so this cold path needs no
+	 * trace_..._enabled() check: the tracepoint is a static-branch no-op
+	 * when disabled.  The denial path guards trace_..._enabled() instead
+	 * because it does expensive __getname()/path work before emitting.
+	 */
+	if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
+		trace_landlock_free_domain(hierarchy);
+}
diff --git a/security/landlock/trace.h b/security/landlock/trace.h
new file mode 100644
index 000000000000..59c8ea348625
--- /dev/null
+++ b/security/landlock/trace.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _SECURITY_LANDLOCK_TRACE_H
+#define _SECURITY_LANDLOCK_TRACE_H
+
+struct landlock_hierarchy;
+
+#ifdef CONFIG_TRACEPOINTS
+
+void landlock_trace_free_domain(
+	const struct landlock_hierarchy *const hierarchy);
+
+#else /* CONFIG_TRACEPOINTS */
+
+static inline void
+landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
+#endif /* CONFIG_TRACEPOINTS */
+
+#endif /* _SECURITY_LANDLOCK_TRACE_H */
-- 
2.54.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox