[PATCH v4 14/19] selftests/landlock: Add trace event test infrastructure and tests

Mickaël Salaün <[email protected]>
Newsgroups org.kernel.vger.linux-security-module,org.kernel.vger.linux-trace-kernel
Message-ID <[email protected]>
Add tracefs test infrastructure in trace.h: helpers for mounting
tracefs, enabling/disabling events, reading the trace buffer, counting
regex matches, and extracting field values, plus per-event regex
patterns.  The patterns are anchored with ^ and $, verify every
TP_printk field, and use no unescaped dot characters; TRACE_PREFIX
matches the ftrace line format with either the expected task name
(truncated to TASK_COMM_LEN - 1) or "<...>" for an evicted comm cache
entry.

Add trace_test.c with the trace fixture (setup enables all available
events with a PID filter, teardown disables and clears) and the
lifecycle, API, denial-field, and log-flag tests.  Extend the existing
true helper to open its working directory before exiting, triggering a
read_dir denial inside a sandbox, so the exec-based tests can verify
same_exec and the logged decision across an exec.  Move regex_escape()
from audit.h to common.h for shared use by the audit and trace tests.

Enable CONFIG_ENABLE_DEFAULT_TRACERS alongside CONFIG_FTRACE in the
selftest config: CONFIG_FTRACE alone only enables the tracer menu
without activating any tracer, while CONFIG_ENABLE_DEFAULT_TRACERS
selects TRACING (and thus TRACEPOINTS and event tracing) without
depending on architecture-specific syscall tracepoints.  When
CONFIG_FTRACE is disabled it cannot be set, so TRACEPOINTS is correctly
disabled too.

Cc: Günther Noack <[email protected]>
Cc: Tingmao Wang <[email protected]>
Signed-off-by: Mickaël Salaün <[email protected]>
---

Changes since v2:
https://patch.msgid.link/[email protected]
- Renamed the restrict_self trace matchers (REGEX_RESTRICT_SELF,
  TRACEFS_RESTRICT_SELF_ENABLE) and the restrict_self tests to
  create_domain.
- Trim the intentionally-elided-coverage comment: drop the
  check_rule_net and ptrace-TRACEME notes (both now have dedicated
  trace tests) and the stale claim that TRACEME routes through
  hook_ptrace_access_check.
- Updated add_rule_net_fields test expected access mask to include the
  new UDP access bits (LANDLOCK_ACCESS_NET_BIND_UDP,
  LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP).
- Assert the symbolic access-right names in the trace field tests
  instead of hex masks.
- Switched the selftest config from CONFIG_FTRACE_SYSCALLS to
  CONFIG_ENABLE_DEFAULT_TRACERS, which selects TRACING without an
  architecture-specific syscall-tracepoint dependency.
- Updated the denial field tests to assert the single logged field
  instead of log_same_exec and log_new_exec; log_flags_subdomains_off
  now checks logged==0 (the case the raw flags could not express).
- Follow the check_rule_fs printk label rename request= to
  access_request= (the trace-only field and its label now share one
  name): define REGEX_CHECK_RULE_FS with the final access_request=
  label, since restrict_self already matches check_rule_fs events
  here and the kernel emits the renamed label.
- Define REGEX_CHECK_RULE_NET with the final access_request= label
  (same check_rule request= to access_request= rename), so the shared
  matcher carries its final form from the patch that introduces trace.h
  rather than being updated in a later test patch.
- Define REGEX_DENY_PTRACE and REGEX_DENY_SCOPE_SIGNAL with the final
  role-prefixed tracee_comm= and target_comm= labels (deny_ptrace and
  deny_scope_signal printk label rename comm= to *_comm=, each label now
  matching its sibling *_pid field), so both matchers carry their final
  form from the patch that introduces trace.h; the per-layer domain
  fields and the tests that consume the labels are added later.
- Reuse a shared scope-based ruleset helper (build_enforce_ruleset) in
  the trace tests: define it here and call it from create_domain_nested
  and create_domain_invalid, which only need a domain.

Changes since v1:
- New patch.
---
 tools/testing/selftests/landlock/audit.h      |   35 -
 tools/testing/selftests/landlock/common.h     |   47 +
 tools/testing/selftests/landlock/config       |    2 +
 tools/testing/selftests/landlock/trace.h      |  634 ++++++++++
 tools/testing/selftests/landlock/trace_test.c | 1101 +++++++++++++++++
 tools/testing/selftests/landlock/true.c       |   10 +
 6 files changed, 1794 insertions(+), 35 deletions(-)
 create mode 100644 tools/testing/selftests/landlock/trace.h
 create mode 100644 tools/testing/selftests/landlock/trace_test.c

diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index f45fdef35681..d428ce802f49 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -214,41 +214,6 @@ static int audit_set_status(int fd, __u32 key, __u32 val)
 	return audit_request(fd, &msg, NULL);
 }
 
-/* Returns a pointer to the last filled character of @dst, which is `\0`.  */
-static __maybe_unused char *regex_escape(const char *const src, char *dst,
-					 size_t dst_size)
-{
-	char *d = dst;
-
-	for (const char *s = src; *s; s++) {
-		switch (*s) {
-		case '$':
-		case '*':
-		case '.':
-		case '[':
-		case '\\':
-		case ']':
-		case '^':
-			if (d >= dst + dst_size - 2)
-				return (char *)-ENOMEM;
-
-			*d++ = '\\';
-			*d++ = *s;
-			break;
-		default:
-			if (d >= dst + dst_size - 1)
-				return (char *)-ENOMEM;
-
-			*d++ = *s;
-		}
-	}
-	if (d >= dst + dst_size - 1)
-		return (char *)-ENOMEM;
-
-	*d = '\0';
-	return d;
-}
-
 /*
  * @domain_id: The domain ID extracted from the audit message (if the first part
  * of @pattern is REGEX_LANDLOCK_PREFIX).  It is set to 0 if the domain ID is
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 7206d5105d66..c5124de68a51 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -253,3 +253,50 @@ static void __maybe_unused set_unix_address(struct service_fixture *const srv,
 	srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
 	srv->unix_addr.sun_path[0] = '\0';
 }
+
+/**
+ * regex_escape - Escape BRE metacharacters in a string
+ *
+ * @src: Source string to escape.
+ * @dst: Destination buffer for the escaped string.
+ * @dst_size: Size of the destination buffer.
+ *
+ * Escapes characters that have special meaning in POSIX Basic Regular
+ * Expressions: $ * . [ \ ] ^
+ *
+ * Returns a pointer to the NUL terminator in @dst (cursor-style API for
+ * chaining), or (char *)-ENOMEM if the buffer is too small.
+ */
+static __maybe_unused char *regex_escape(const char *const src, char *dst,
+					 size_t dst_size)
+{
+	char *d = dst;
+
+	for (const char *s = src; *s; s++) {
+		switch (*s) {
+		case '$':
+		case '*':
+		case '.':
+		case '[':
+		case '\\':
+		case ']':
+		case '^':
+			if (d >= dst + dst_size - 2)
+				return (char *)-ENOMEM;
+
+			*d++ = '\\';
+			*d++ = *s;
+			break;
+		default:
+			if (d >= dst + dst_size - 1)
+				return (char *)-ENOMEM;
+
+			*d++ = *s;
+		}
+	}
+	if (d >= dst + dst_size - 1)
+		return (char *)-ENOMEM;
+
+	*d = '\0';
+	return d;
+}
diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
index 8fe9b461b1fd..d86321936fd8 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -2,6 +2,8 @@ CONFIG_AF_UNIX_OOB=y
 CONFIG_AUDIT=y
 CONFIG_CGROUPS=y
 CONFIG_CGROUP_SCHED=y
+CONFIG_ENABLE_DEFAULT_TRACERS=y
+CONFIG_FTRACE=y
 CONFIG_INET=y
 CONFIG_IPV6=y
 CONFIG_KEYS=y
diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
new file mode 100644
index 000000000000..aa35d8ad166e
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace.h
@@ -0,0 +1,634 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Landlock trace test helpers
+ *
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+
+#define TRACEFS_ROOT "/sys/kernel/tracing"
+#define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
+#define TRACEFS_CREATE_RULESET_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
+#define TRACEFS_CREATE_DOMAIN_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
+#define TRACEFS_ADD_RULE_FS_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
+#define TRACEFS_ADD_RULE_NET_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
+#define TRACEFS_CHECK_RULE_FS_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
+#define TRACEFS_CHECK_RULE_NET_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
+#define TRACEFS_DENY_ACCESS_FS_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
+#define TRACEFS_DENY_ACCESS_NET_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
+#define TRACEFS_DENY_PTRACE_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
+#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
+#define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
+	TRACEFS_LANDLOCK_DIR                           \
+	"/landlock_deny_scope_abstract_unix_socket/enable"
+#define TRACEFS_FREE_DOMAIN_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
+#define TRACEFS_FREE_RULESET_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
+#define TRACEFS_TRACE TRACEFS_ROOT "/trace"
+#define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
+#define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
+
+#define TRACE_BUFFER_SIZE (64 * 1024)
+
+/*
+ * Trace line prefix: matches the ftrace "trace" file format.  Format: "
+ * <task>-<pid> [<cpu>] <flags> <timestamp>: "
+ *
+ * The task parameter must be a string literal truncated to 15 chars
+ * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm.  The
+ * pattern accepts either the expected task name or "<...>" because the ftrace
+ * comm cache may evict short-lived processes (e.g., forked children that exit
+ * before the trace buffer is read).
+ *
+ * No unescaped '.' in any REGEX macro; literal dots use '\\.'.
+ */
+#define TRACE_PREFIX(task)  \
+	"^ *\\(<\\.\\.\\.>" \
+	"\\|" task "\\)"    \
+	"-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
+
+/*
+ * Task name for events emitted by kworker threads (e.g., free_domain fires from
+ * a work queue, not from the test process).
+ */
+#define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
+
+#define REGEX_ADD_RULE_FS(task)           \
+	TRACE_PREFIX(task)                \
+	"landlock_add_rule_fs: "          \
+	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+	"access_rights=[a-z_|]* "         \
+	"dev=[0-9]\\+:[0-9]\\+ "          \
+	"ino=[0-9]\\+ "                   \
+	"path=[^ ]\\+$"
+
+#define REGEX_ADD_RULE_NET(task)          \
+	TRACE_PREFIX(task)                \
+	"landlock_add_rule_net: "         \
+	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+	"access_rights=[a-z_|]* "         \
+	"port=[0-9]\\+$"
+
+#define REGEX_CREATE_RULESET(task)        \
+	TRACE_PREFIX(task)                \
+	"landlock_create_ruleset: "       \
+	"ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+	"handled_fs=[a-z_|]* "            \
+	"handled_net=[a-z_|]* "           \
+	"scoped=[a-z_|]*$"
+
+#define REGEX_CREATE_DOMAIN(task)  \
+	TRACE_PREFIX(task)         \
+	"landlock_create_domain: " \
+	"domain=[0-9a-f]\\+ "      \
+	"parent=[0-9a-f]\\+ "      \
+	"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
+
+#define REGEX_CHECK_RULE_FS(task)  \
+	TRACE_PREFIX(task)         \
+	"landlock_check_rule_fs: " \
+	"domain=[0-9a-f]\\+ "      \
+	"access_request=[a-z_|]* " \
+	"dev=[0-9]\\+:[0-9]\\+ "   \
+	"ino=[0-9]\\+ "            \
+	"grants={[a-z_|,]*}$"
+
+#define REGEX_CHECK_RULE_NET(task)  \
+	TRACE_PREFIX(task)          \
+	"landlock_check_rule_net: " \
+	"domain=[0-9a-f]\\+ "       \
+	"access_request=[a-z_|]* "  \
+	"port=[0-9]\\+ "            \
+	"grants={[a-z_|,]*}$"
+
+#define REGEX_DENY_ACCESS_FS(task)  \
+	TRACE_PREFIX(task)          \
+	"landlock_deny_access_fs: " \
+	"domain=[0-9a-f]\\+ "       \
+	"same_exec=[01] "           \
+	"logged=[01] "              \
+	"blockers=[a-z_|]* "        \
+	"dev=[0-9]\\+:[0-9]\\+ "    \
+	"ino=[0-9]\\+ "             \
+	"path=[^ ]*$"
+
+#define REGEX_DENY_ACCESS_NET(task)  \
+	TRACE_PREFIX(task)           \
+	"landlock_deny_access_net: " \
+	"domain=[0-9a-f]\\+ "        \
+	"same_exec=[01] "            \
+	"logged=[01] "               \
+	"blockers=[a-z_|]* "         \
+	"sport=[0-9]\\+ "            \
+	"dport=[0-9]\\+$"
+
+#define REGEX_DENY_PTRACE(task)  \
+	TRACE_PREFIX(task)       \
+	"landlock_deny_ptrace: " \
+	"domain=[0-9a-f]\\+ "    \
+	"same_exec=[01] "        \
+	"logged=[01] "           \
+	"tracee_pid=[0-9]\\+ "   \
+	"tracee_comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_SIGNAL(task)  \
+	TRACE_PREFIX(task)             \
+	"landlock_deny_scope_signal: " \
+	"domain=[0-9a-f]\\+ "          \
+	"same_exec=[01] "              \
+	"logged=[01] "                 \
+	"target_pid=[0-9]\\+ "         \
+	"target_comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task)  \
+	TRACE_PREFIX(task)                           \
+	"landlock_deny_scope_abstract_unix_socket: " \
+	"domain=[0-9a-f]\\+ "                        \
+	"same_exec=[01] "                            \
+	"logged=[01] "                               \
+	"peer_pid=[0-9]\\+ "                         \
+	"sun_path=[^ ]*$"
+
+#define REGEX_FREE_DOMAIN(task)  \
+	TRACE_PREFIX(task)       \
+	"landlock_free_domain: " \
+	"domain=[0-9a-f]\\+ "    \
+	"denials=[0-9]\\+$"
+
+#define REGEX_FREE_RULESET(task)  \
+	TRACE_PREFIX(task)        \
+	"landlock_free_ruleset: " \
+	"ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
+
+static int __maybe_unused tracefs_write(const char *path, const char *value)
+{
+	int fd;
+	ssize_t ret;
+	size_t len = strlen(value);
+
+	fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
+	if (fd < 0)
+		return -errno;
+
+	ret = write(fd, value, len);
+	close(fd);
+	if (ret < 0)
+		return -errno;
+	if ((size_t)ret != len)
+		return -EIO;
+
+	return 0;
+}
+
+static int __maybe_unused tracefs_write_int(const char *path, int value)
+{
+	char buf[32];
+
+	snprintf(buf, sizeof(buf), "%d", value);
+	return tracefs_write(path, buf);
+}
+
+static int __maybe_unused tracefs_setup(void)
+{
+	struct stat st;
+
+	/* Mount tracefs if not already mounted. */
+	if (stat(TRACEFS_ROOT, &st) != 0) {
+		int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
+
+		if (ret)
+			return -errno;
+	}
+
+	/* Verify landlock events are available. */
+	if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
+		return -ENOENT;
+
+	return 0;
+}
+
+/*
+ * Set up PID-based event filtering so only events from the current process and
+ * its children are recorded.  This is analogous to audit's AUDIT_EXE filter: it
+ * prevents events from unrelated processes from polluting the trace buffer.
+ */
+static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
+{
+	int ret;
+
+	/* Enable event-fork so children inherit the PID filter. */
+	ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
+	if (ret)
+		return ret;
+
+	return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
+}
+
+/* Clear the PID filter to stop filtering by PID. */
+static int __maybe_unused tracefs_clear_pid_filter(void)
+{
+	return tracefs_write(TRACEFS_SET_EVENT_PID, "");
+}
+
+static int __maybe_unused tracefs_enable_event(const char *enable_path,
+					       bool enable)
+{
+	return tracefs_write(enable_path, enable ? "1" : "0");
+}
+
+static int __maybe_unused tracefs_clear(void)
+{
+	return tracefs_write(TRACEFS_TRACE, "");
+}
+
+/*
+ * Reads the trace buffer content into a newly allocated buffer.  The caller is
+ * responsible for freeing the returned buffer.  Returns NULL on error.
+ */
+static char __maybe_unused *tracefs_read_trace(void)
+{
+	char *buf;
+	int fd;
+	ssize_t total = 0, ret;
+
+	buf = malloc(TRACE_BUFFER_SIZE);
+	if (!buf)
+		return NULL;
+
+	fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
+	if (fd < 0) {
+		free(buf);
+		return NULL;
+	}
+
+	while (total < TRACE_BUFFER_SIZE - 1) {
+		ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
+		if (ret <= 0)
+			break;
+		total += ret;
+	}
+	close(fd);
+	buf[total] = '\0';
+	return buf;
+}
+
+/* Counts the number of lines in @buf matching the basic regex @pattern. */
+static int __maybe_unused tracefs_count_matches(const char *buf,
+						const char *pattern)
+{
+	regex_t regex;
+	int count = 0;
+	const char *line, *end;
+
+	if (regcomp(&regex, pattern, 0) != 0)
+		return -EINVAL;
+
+	line = buf;
+	while (*line) {
+		end = strchr(line, '\n');
+		if (!end)
+			end = line + strlen(line);
+
+		/* Create a temporary NUL-terminated line. */
+		size_t len = end - line;
+		char *tmp = malloc(len + 1);
+
+		if (tmp) {
+			memcpy(tmp, line, len);
+			tmp[len] = '\0';
+			if (regexec(&regex, tmp, 0, NULL, 0) == 0)
+				count++;
+			free(tmp);
+		}
+
+		if (*end == '\n')
+			line = end + 1;
+		else
+			break;
+	}
+
+	regfree(&regex);
+	return count;
+}
+
+/*
+ * Extracts the value of a named field from a trace line in @buf.  Searches for
+ * the first line matching @line_pattern, then extracts the value after
+ * "@field_name=" into @out.  Stops at space or newline.
+ *
+ * Returns 0 on success, -ENOENT if no match.
+ */
+static int __maybe_unused tracefs_extract_field(const char *buf,
+						const char *line_pattern,
+						const char *field_name,
+						char *out, size_t out_size)
+{
+	regex_t regex;
+	const char *line, *end;
+
+	if (regcomp(&regex, line_pattern, 0) != 0)
+		return -EINVAL;
+
+	line = buf;
+	while (*line) {
+		end = strchr(line, '\n');
+		if (!end)
+			end = line + strlen(line);
+
+		size_t len = end - line;
+		char *tmp = malloc(len + 1);
+
+		if (tmp) {
+			const char *field, *val_start;
+			size_t field_len, val_len;
+
+			memcpy(tmp, line, len);
+			tmp[len] = '\0';
+
+			if (regexec(&regex, tmp, 0, NULL, 0) != 0) {
+				free(tmp);
+				goto next;
+			}
+
+			/*
+			 * Find "field_name=" in the line, ensuring a word
+			 * boundary before the field name to avoid substring
+			 * matches (e.g., "port" in "sport").
+			 */
+			field_len = strlen(field_name);
+			field = tmp;
+			while ((field = strstr(field, field_name))) {
+				if (field[field_len] == '=' &&
+				    (field == tmp || field[-1] == ' '))
+					break;
+				field++;
+			}
+			if (!field) {
+				free(tmp);
+				regfree(&regex);
+				return -ENOENT;
+			}
+
+			val_start = field + field_len + 1;
+			val_len = 0;
+			while (val_start[val_len] &&
+			       val_start[val_len] != ' ' &&
+			       val_start[val_len] != '\n')
+				val_len++;
+
+			if (val_len >= out_size)
+				val_len = out_size - 1;
+			memcpy(out, val_start, val_len);
+			out[val_len] = '\0';
+
+			free(tmp);
+			regfree(&regex);
+			return 0;
+		}
+next:
+		if (*end == '\n')
+			line = end + 1;
+		else
+			break;
+	}
+
+	regfree(&regex);
+	return -ENOENT;
+}
+
+/*
+ * Common fixture setup for trace tests.  Mounts tracefs if needed and sets a
+ * PID filter.  The caller must create a mount namespace first
+ * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
+ * mount; the trace buffer, per-event enable flags, and PID filter are global
+ * kernel state, scoped to the test by the PID filter.
+ *
+ * Returns 0 on success, -errno on failure (caller should SKIP).
+ */
+static int __maybe_unused tracefs_fixture_setup(void)
+{
+	int ret;
+
+	ret = tracefs_setup();
+	if (ret)
+		return ret;
+
+	return tracefs_set_pid_filter(getpid());
+}
+
+static void __maybe_unused tracefs_fixture_teardown(void)
+{
+	tracefs_clear_pid_filter();
+}
+
+/*
+ * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
+ * drops the capability.  Returns the value from @func, or -EPERM if the
+ * capability manipulation fails.
+ */
+static int __maybe_unused tracefs_priv_call(int (*func)(void))
+{
+	const cap_value_t admin = CAP_SYS_ADMIN;
+	cap_t cap_p;
+	int ret;
+
+	cap_p = cap_get_proc();
+	if (!cap_p)
+		return -EPERM;
+
+	if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+	    cap_set_proc(cap_p)) {
+		cap_free(cap_p);
+		return -EPERM;
+	}
+
+	ret = func();
+
+	cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+	cap_set_proc(cap_p);
+	cap_free(cap_p);
+	return ret;
+}
+
+/* Read the trace buffer with elevated privileges.  Returns NULL on failure. */
+static char __maybe_unused *tracefs_read_buf(void)
+{
+	/* Cannot use tracefs_priv_call() because the return type is char *. */
+	cap_t cap_p;
+	char *buf;
+	const cap_value_t admin = CAP_SYS_ADMIN;
+
+	cap_p = cap_get_proc();
+	if (!cap_p)
+		return NULL;
+
+	if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+	    cap_set_proc(cap_p)) {
+		cap_free(cap_p);
+		return NULL;
+	}
+
+	buf = tracefs_read_trace();
+
+	cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+	cap_set_proc(cap_p);
+	cap_free(cap_p);
+	return buf;
+}
+
+/* Clear the trace buffer with elevated privileges.  Returns 0 on success. */
+static int __maybe_unused tracefs_clear_buf(void)
+{
+	return tracefs_priv_call(tracefs_clear);
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox and performs an FS access.  The
+ * parent waits for the child, then reads the trace buffer.
+ *
+ * Requires common.h and wrappers.h to be included before trace.h.
+ */
+static void __maybe_unused sandbox_child_fs_access(
+	struct __test_metadata *const _metadata, const char *rule_path,
+	__u64 handled_access, __u64 allowed_access, const char *access_path)
+{
+	pid_t pid;
+	int status;
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr ruleset_attr = {
+			.handled_access_fs = handled_access,
+		};
+		struct landlock_path_beneath_attr path_beneath = {
+			.allowed_access = allowed_access,
+		};
+		int ruleset_fd, fd;
+
+		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+						     sizeof(ruleset_attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		path_beneath.parent_fd =
+			open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd < 0) {
+			close(ruleset_fd);
+			_exit(1);
+		}
+
+		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+				      &path_beneath, 0)) {
+			close(path_beneath.parent_fd);
+			close(ruleset_fd);
+			_exit(1);
+		}
+		close(path_beneath.parent_fd);
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, 0)) {
+			close(ruleset_fd);
+			_exit(1);
+		}
+		close(ruleset_fd);
+
+		fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (fd >= 0)
+			close(fd);
+
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox allowing execute+read_dir for
+ * /usr and execute-only for ".", then execs ./true.  The true binary opens "."
+ * on startup, triggering a read_dir denial with same_exec=0.  The parent waits
+ * for the child to exit.
+ */
+static void __maybe_unused sandbox_child_exec_true(
+	struct __test_metadata *const _metadata, __u32 restrict_flags)
+{
+	pid_t pid;
+	int status;
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
+					     LANDLOCK_ACCESS_FS_EXECUTE,
+		};
+		struct landlock_path_beneath_attr path_beneath = {
+			.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
+					  LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int ruleset_fd;
+
+		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		path_beneath.parent_fd =
+			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd >= 0) {
+			landlock_add_rule(ruleset_fd,
+					  LANDLOCK_RULE_PATH_BENEATH,
+					  &path_beneath, 0);
+			close(path_beneath.parent_fd);
+		}
+
+		path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
+		path_beneath.parent_fd =
+			open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd >= 0) {
+			landlock_add_rule(ruleset_fd,
+					  LANDLOCK_RULE_PATH_BENEATH,
+					  &path_beneath, 0);
+			close(path_beneath.parent_fd);
+		}
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, restrict_flags))
+			_exit(1);
+		close(ruleset_fd);
+
+		execl("./true", "./true", NULL);
+		_exit(1);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+}
diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c
new file mode 100644
index 000000000000..a141f22ad98f
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace_test.c
@@ -0,0 +1,1101 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Tracepoints
+ *
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/landlock.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "trace_test"
+
+/* clang-format off */
+FIXTURE(trace) {
+	/* clang-format on */
+	int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace)
+{
+	int ret;
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, unshare(CLONE_NEWNS));
+	ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+	ret = tracefs_fixture_setup();
+	if (ret) {
+		clear_cap(_metadata, CAP_SYS_ADMIN);
+		self->tracefs_ok = 0;
+		SKIP(return, "tracefs not available");
+	}
+	self->tracefs_ok = 1;
+
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, true));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, true));
+	ASSERT_EQ(0, tracefs_clear());
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace)
+{
+	if (!self->tracefs_ok)
+		return;
+
+	/* Disables landlock events and clears PID filter. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
+	tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
+	tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
+	tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false);
+	tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
+	tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
+	tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
+	tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+	tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false);
+	tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false);
+	tracefs_clear_pid_filter();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/*
+	 * The mount namespace is cleaned up automatically when the test process
+	 * (harness child) exits.
+	 */
+}
+
+/*
+ * Verifies that no trace events are emitted when the tracepoints are disabled.
+ */
+TEST_F(trace, no_trace_when_disabled)
+{
+	char *buf;
+
+	/* Disable all landlock events. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false));
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false));
+	ASSERT_EQ(0,
+		  tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE,
+					  false));
+	ASSERT_EQ(0, tracefs_enable_event(
+			     TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
+			     false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false));
+	ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false));
+	ASSERT_EQ(0, tracefs_clear());
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/*
+	 * Trigger both allowed and denied accesses to verify neither check_rule
+	 * nor check_access events fire when disabled.
+	 */
+	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+				LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+	/* Read trace buffer and verify no landlock events at all. */
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(0, tracefs_count_matches(buf, "landlock_"))
+	{
+		TH_LOG("Expected 0 landlock events when disabled\n%s", buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies that landlock_create_ruleset emits a trace event with the correct
+ * handled access masks.
+ */
+TEST_F(trace, create_ruleset)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+	};
+	int ruleset_fd;
+	char *buf, *dot;
+	char field[64];
+
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_RULESET(TRACE_TASK)))
+	{
+		TH_LOG("Expected 1 create_ruleset event\n%s", buf);
+	}
+
+	/* Verify handled_fs matches what we requested. */
+	EXPECT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+					"handled_fs", field, sizeof(field)));
+	EXPECT_STREQ("read_file", field);
+
+	/* Verify handled_net matches. */
+	EXPECT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+					"handled_net", field, sizeof(field)));
+	EXPECT_STREQ("bind_tcp", field);
+
+	/* Verify version is 0 at creation (no rules added yet). */
+	EXPECT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+					"ruleset", field, sizeof(field)));
+	/* Format is <hex>.<dec>; version is after the dot. */
+	dot = strchr(field, '.');
+	ASSERT_NE(0, !!dot);
+	EXPECT_STREQ("0", dot + 1);
+
+	free(buf);
+}
+
+/*
+ * Verifies that the ruleset version increments with each add_rule call and that
+ * create_domain records the correct version.
+ */
+TEST_F(trace, ruleset_version)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	const char *dot;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr ruleset_attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		struct landlock_path_beneath_attr path_beneath = {
+			.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int ruleset_fd;
+
+		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+						     sizeof(ruleset_attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		/* First rule: version becomes 1. */
+		path_beneath.parent_fd =
+			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd < 0)
+			_exit(1);
+		landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+				  &path_beneath, 0);
+		close(path_beneath.parent_fd);
+
+		/* Second rule: version becomes 2. */
+		path_beneath.parent_fd =
+			open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd < 0)
+			_exit(1);
+		landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+				  &path_beneath, 0);
+		close(path_beneath.parent_fd);
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(1);
+		close(ruleset_fd);
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/* Verify create_ruleset has version=0. */
+	ASSERT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+					"ruleset", field, sizeof(field)));
+	dot = strchr(field, '.');
+	ASSERT_NE(0, !!dot);
+	EXPECT_STREQ("0", dot + 1);
+
+	/* Verify 2 add_rule_fs events were emitted. */
+	EXPECT_EQ(2, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+	{
+		TH_LOG("Expected 2 add_rule_fs events\n%s", buf);
+	}
+
+	/*
+	 * Verify create_domain records version=2 (after 2 add_rule calls).  The
+	 * ruleset field format is <hex_id>.<dec_version>.
+	 */
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "ruleset", field, sizeof(field)));
+	dot = strchr(field, '.');
+	ASSERT_NE(0, !!dot);
+	EXPECT_STREQ("2", dot + 1);
+
+	free(buf);
+}
+
+/*
+ * Verifies that landlock_create_domain emits a trace event linking the ruleset
+ * ID to the new domain ID.
+ */
+TEST_F(trace, create_domain)
+{
+	pid_t pid;
+	int status, check_count;
+	char *buf;
+	char parent_id[64], domain_id[64], check_domain[64];
+
+	/* Clear before the sandboxed child. */
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr ruleset_attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		struct landlock_path_beneath_attr path_beneath = {
+			.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int ruleset_fd, fd;
+
+		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+						     sizeof(ruleset_attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		path_beneath.parent_fd =
+			open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+		if (path_beneath.parent_fd < 0)
+			_exit(1);
+
+		landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+				  &path_beneath, 0);
+		close(path_beneath.parent_fd);
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(1);
+		close(ruleset_fd);
+
+		/* Trigger a check_rule to verify domain_id correlation. */
+		fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (fd >= 0)
+			close(fd);
+
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/* Verify create_domain event exists. */
+	EXPECT_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+	{
+		TH_LOG("Expected 1 create_domain event\n%s", buf);
+	}
+
+	/* Extract the domain ID from create_domain. */
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "domain", domain_id,
+					   sizeof(domain_id)));
+
+	/* Verify domain ID is non-zero. */
+	EXPECT_NE(0, strcmp(domain_id, "0"));
+
+	/* Verify parent=0 (first restriction, no prior domain). */
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "parent", parent_id,
+					   sizeof(parent_id)));
+	EXPECT_STREQ("0", parent_id);
+
+	/*
+	 * Verify the same domain ID appears in the check_rule event, confirming
+	 * end-to-end correlation.
+	 */
+	check_count =
+		tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+	ASSERT_LE(1, check_count)
+	{
+		TH_LOG("Expected check_rule_fs events\n%s", buf);
+	}
+
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+					   "domain", check_domain,
+					   sizeof(check_domain)));
+	EXPECT_STREQ(domain_id, check_domain);
+
+	free(buf);
+}
+
+/* Builds a rule-less scope-based ruleset; returns the fd or -1. */
+static int build_enforce_ruleset(void)
+{
+	const struct landlock_ruleset_attr attr = {
+		.scoped = LANDLOCK_SCOPE_SIGNAL,
+	};
+
+	return landlock_create_ruleset(&attr, sizeof(attr), 0);
+}
+
+/*
+ * Verifies that nested landlock_restrict_self calls produce trace events with
+ * correct parent domain IDs: the second create_domain's parent should be the
+ * first domain's ID.
+ */
+TEST_F(trace, create_domain_nested)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	const char *after_first;
+	char first_domain[64], first_parent[64], second_parent[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		int ruleset_fd;
+
+		/* First restriction. */
+		ruleset_fd = build_enforce_ruleset();
+		if (ruleset_fd < 0)
+			_exit(1);
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(1);
+		close(ruleset_fd);
+
+		/* Second restriction (nested). */
+		ruleset_fd = build_enforce_ruleset();
+		if (ruleset_fd < 0)
+			_exit(1);
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(1);
+		close(ruleset_fd);
+
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/* Should have 2 create_domain events. */
+	EXPECT_EQ(2,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+	{
+		TH_LOG("Expected 2 create_domain events\n%s", buf);
+	}
+
+	/*
+	 * Extract domain and parent from each create_domain event.  The first
+	 * event (parent=0) is the outer domain; the second (parent!=0) is the
+	 * nested domain whose parent should match the first domain's ID.
+	 */
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "domain", first_domain,
+					   sizeof(first_domain)));
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "parent", first_parent,
+					   sizeof(first_parent)));
+	EXPECT_STREQ("0", first_parent);
+
+	/*
+	 * Find the second create_domain by scanning past the first.
+	 * tracefs_extract_field returns the first match, so search in the
+	 * buffer after the first event.
+	 *
+	 * Skip past the first create_domain line. tracefs_extract_field matches
+	 * the first line that matches the regex, so passing the buffer after
+	 * the first matching line gives us the second event.
+	 */
+	after_first = strstr(buf, "landlock_create_domain:");
+	ASSERT_NE(NULL, after_first);
+	after_first = strchr(after_first, '\n');
+	ASSERT_NE(NULL, after_first);
+
+	ASSERT_EQ(0, tracefs_extract_field(
+			     after_first + 1, REGEX_CREATE_DOMAIN(TRACE_TASK),
+			     "parent", second_parent, sizeof(second_parent)));
+
+	/* The second domain's parent should be the first domain's ID. */
+	EXPECT_STREQ(first_domain, second_parent);
+
+	free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule does not emit a trace event when the syscall
+ * fails (e.g., invalid ruleset fd).
+ */
+TEST_F(trace, add_rule_invalid_fd)
+{
+	struct landlock_path_beneath_attr path_beneath = {
+		.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
+	};
+	char *buf;
+
+	path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+	ASSERT_LE(0, path_beneath.parent_fd);
+
+	/* Invalid ruleset fd (-1). */
+	ASSERT_EQ(-1, landlock_add_rule(-1, LANDLOCK_RULE_PATH_BENEATH,
+					&path_beneath, 0));
+	ASSERT_EQ(0, close(path_beneath.parent_fd));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+	{
+		TH_LOG("No add_rule_fs event expected on invalid fd\n%s", buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies that landlock_create_domain does not emit a trace event when the
+ * syscall fails (e.g., invalid ruleset fd or unknown flags).
+ */
+TEST_F(trace, create_domain_invalid)
+{
+	int ruleset_fd;
+	char *buf;
+
+	ruleset_fd = build_enforce_ruleset();
+	ASSERT_LE(0, ruleset_fd);
+
+	/* Clear the trace buffer after create_ruleset event. */
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/* Invalid fd. */
+	ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
+
+	/* Unknown flags. */
+	ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, -1));
+
+	ASSERT_EQ(0, close(ruleset_fd));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(0,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+	{
+		TH_LOG("No create_domain event expected on error\n%s", buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies that trace_landlock_free_domain fires when a domain is deallocated,
+ * with the correct denials count.
+ */
+TEST_F(trace, free_domain)
+{
+	char *buf;
+	int count;
+	char denials_field[32];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/*
+	 * The domain is freed via a work queue (kworker), so the free_domain
+	 * trace event is emitted from a different PID.  Clear the PID filter
+	 * BEFORE the child exits, so the kworker event passes the filter when
+	 * it fires.
+	 */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	tracefs_clear_pid_filter();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+				LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+	/*
+	 * Wait for the deferred deallocation work to run.  The domain is freed
+	 * asynchronously from a kworker; poll until the event appears or a
+	 * timeout is reached.
+	 */
+	for (int retry = 0; retry < 10; retry++) {
+		usleep(100000);
+
+		set_cap(_metadata, CAP_SYS_ADMIN);
+		buf = tracefs_read_trace();
+		clear_cap(_metadata, CAP_SYS_ADMIN);
+		ASSERT_NE(NULL, buf);
+
+		count = tracefs_count_matches(buf,
+					      REGEX_FREE_DOMAIN(KWORKER_TASK));
+		if (count >= 1)
+			break;
+		free(buf);
+		buf = NULL;
+	}
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	ASSERT_NE(NULL, buf);
+	EXPECT_LE(1, count)
+	{
+		TH_LOG("Expected free_domain event, got %d\n%s", count, buf);
+	}
+
+	/* Verify denials count matches the single denial we triggered. */
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+					   "denials", denials_field,
+					   sizeof(denials_field)));
+	EXPECT_STREQ("1", denials_field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that deny_access_fs includes the enriched fields: same_exec and
+ * logged.
+ */
+TEST_F(trace, deny_access_fs_fields)
+{
+	char *buf;
+	char field_buf[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/* Trigger a denial: rule for /usr, access /tmp. */
+	sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+				LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/* Verify the enriched fields are present and have valid values. */
+	ASSERT_EQ(0, tracefs_extract_field(
+			     buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "same_exec",
+			     field_buf, sizeof(field_buf)));
+	/* Child is the same exec that restricted itself. */
+	EXPECT_STREQ("1", field_buf);
+
+	/* Same exec with default flags: audit would log this denial. */
+	ASSERT_EQ(0, tracefs_extract_field(
+			     buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "logged",
+			     field_buf, sizeof(field_buf)));
+	EXPECT_STREQ("1", field_buf);
+
+	free(buf);
+}
+
+/*
+ * Verifies that same_exec is 1 (true) for denials from the same executable that
+ * called landlock_restrict_self().
+ */
+TEST_F(trace, same_exec_before_exec)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int ruleset_fd, dir_fd;
+
+		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		/* No rules: all read_dir access is denied. */
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(1);
+		close(ruleset_fd);
+
+		/* Trigger denial without exec (same executable). */
+		dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (dir_fd >= 0)
+			close(dir_fd);
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/* Should have at least one deny_access_fs denial. */
+	EXPECT_LE(1,
+		  tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+	/* Verify same_exec=1 (same executable, no exec). */
+	ASSERT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+					"same_exec", field, sizeof(field)));
+	EXPECT_STREQ("1", field);
+
+	/* Same exec with default flags: audit would log this denial. */
+	ASSERT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+					"logged", field, sizeof(field)));
+	EXPECT_STREQ("1", field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that same_exec is 0 (false) for denials from a process that has
+ * exec'd a new binary after landlock_restrict_self().  The sandboxed child
+ * exec's true which opens "." and triggers a read_dir denial.  Covers the
+ * "trace-only" visibility condition: with same_exec=0 and the default
+ * log_new_exec=0, audit suppresses the denial (logged=0) but the trace event
+ * still fires.
+ */
+TEST_F(trace, same_exec_after_exec)
+{
+	char *buf;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	sandbox_child_exec_true(_metadata, 0);
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+	/* Verify same_exec=0 (different executable after exec). */
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+					   "same_exec", field, sizeof(field)));
+	EXPECT_STREQ("0", field);
+
+	/*
+	 * same_exec=0 with default log_new_exec=0: audit suppresses (logged=0).
+	 */
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+					   "logged", field, sizeof(field)));
+	EXPECT_STREQ("0", field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF suppresses logging
+ * (logged=0) for a denial from the same executable.
+ */
+TEST_F(trace, log_flags_same_exec_off)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int ruleset_fd, dir_fd;
+
+		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+		if (ruleset_fd < 0)
+			_exit(1);
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(
+			    ruleset_fd,
+			    LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF))
+			_exit(1);
+		close(ruleset_fd);
+
+		dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (dir_fd >= 0)
+			close(dir_fd);
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_LE(1,
+		  tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+	/* Same-exec denial with LOG_SAME_EXEC_OFF: audit suppresses it. */
+	ASSERT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+					"logged", field, sizeof(field)));
+	EXPECT_STREQ("0", field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON causes a post-exec
+ * denial to be logged (logged=1).  The child exec's true so that the denial
+ * comes from a new executable (same_exec=0).
+ */
+TEST_F(trace, log_flags_new_exec_on)
+{
+	char *buf;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	sandbox_child_exec_true(_metadata,
+				LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+					   "same_exec", field, sizeof(field)));
+	EXPECT_STREQ("0", field);
+
+	/* LOG_NEW_EXEC_ON: the post-exec denial (same_exec=0) is logged. */
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+					   "logged", field, sizeof(field)));
+	EXPECT_STREQ("1", field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that denials suppressed by audit log flags are still counted in
+ * num_denials.  The child restricts itself with default flags (log_same_exec=1,
+ * log_new_exec=0), then execs true which attempts to read a denied directory.
+ * After exec, same_exec=0 and log_new_exec=0, so audit suppresses the denial.
+ * But the trace event fires unconditionally and free_domain must report the
+ * correct denials count.
+ */
+TEST_F(trace, non_audit_visible_denial_counting)
+{
+	char *buf = NULL;
+	char denials_field[32];
+	int count;
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, tracefs_clear());
+	tracefs_clear_pid_filter();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	sandbox_child_exec_true(_metadata, 0);
+
+	/* Wait for free_domain event with retry. */
+	for (int retry = 0; retry < 10; retry++) {
+		usleep(100000);
+
+		set_cap(_metadata, CAP_SYS_ADMIN);
+		buf = tracefs_read_trace();
+		clear_cap(_metadata, CAP_SYS_ADMIN);
+		if (!buf)
+			break;
+
+		count = tracefs_count_matches(buf,
+					      REGEX_FREE_DOMAIN(KWORKER_TASK));
+		if (count >= 1)
+			break;
+		free(buf);
+		buf = NULL;
+	}
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/*
+	 * The denial happened after exec (same_exec=0), so audit would suppress
+	 * it.  But num_denials counts all denials regardless.
+	 */
+	ASSERT_NE(NULL, buf)
+	{
+		TH_LOG("free_domain event not found after 10 retries");
+	}
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+					   "denials", denials_field,
+					   sizeof(denials_field)));
+	EXPECT_STREQ("1", denials_field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule_net emits a trace event with the correct port
+ * and allowed access mask fields.
+ */
+TEST_F(trace, add_rule_net_fields)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+	};
+	struct landlock_net_port_attr net_port = {
+		.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+		.port = 8080,
+	};
+	int ruleset_fd;
+	char *buf;
+	char field[64];
+
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+				       &net_port, 0));
+	close(ruleset_fd);
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_ADD_RULE_NET(TRACE_TASK)))
+	{
+		TH_LOG("Expected 1 add_rule_net event\n%s", buf);
+	}
+
+	/*
+	 * Verify the port is in host endianness, matching the UAPI convention
+	 * (landlock_net_port_attr.port).  On little-endian, htons(8080) is
+	 * 36895, so this comparison catches byte-order bugs.
+	 */
+	EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+					   "port", field, sizeof(field)));
+	EXPECT_STREQ("8080", field);
+	/*
+	 * The allowed mask is the absolute value after transformation: the
+	 * user-requested BIND_TCP plus all unhandled access rights (the other
+	 * net access bits are unhandled because the ruleset only handles
+	 * BIND_TCP).
+	 */
+	EXPECT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+					"access_rights", field, sizeof(field)));
+	EXPECT_STREQ("bind_tcp|connect_tcp|bind_udp|connect_send_udp", field);
+
+	free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF suppresses audit
+ * logging for child domains (logged=0) even though the child's own
+ * per-execution flags are the defaults, while the trace event still fires
+ * (tracing is unconditional).  The parent creates a domain with
+ * LOG_SUBDOMAINS_OFF, then the child creates a sub-domain and triggers a
+ * denial.
+ */
+TEST_F(trace, log_flags_subdomains_off)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	char field[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+
+	if (pid == 0) {
+		struct landlock_ruleset_attr attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+		};
+		int parent_fd, child_fd, dir_fd;
+
+		/* Parent domain with LOG_SUBDOMAINS_OFF. */
+		parent_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+		if (parent_fd < 0)
+			_exit(1);
+
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(
+			    parent_fd,
+			    LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
+			_exit(1);
+		close(parent_fd);
+
+		/* Child sub-domain with default flags. */
+		child_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+		if (child_fd < 0)
+			_exit(1);
+
+		if (landlock_restrict_self(child_fd, 0))
+			_exit(1);
+		close(child_fd);
+
+		/* Trigger a denial from the child domain. */
+		dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+		if (dir_fd >= 0)
+			close(dir_fd);
+		_exit(0);
+	}
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	/*
+	 * Trace fires unconditionally even though audit is disabled for the
+	 * child domain (parent had LOG_SUBDOMAINS_OFF).
+	 */
+	EXPECT_LE(1,
+		  tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)))
+	{
+		TH_LOG("Expected deny_access_fs event despite "
+		       "LOG_SUBDOMAINS_OFF\n%s",
+		       buf);
+	}
+
+	/*
+	 * The child's per-execution flags default to logging, but the
+	 * ancestor's LOG_SUBDOMAINS_OFF disables it, so audit suppresses this
+	 * denial (logged=0).  This is exactly the case the single logged field
+	 * captures and the raw per-execution flags could not.
+	 */
+	ASSERT_EQ(0,
+		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+					"logged", field, sizeof(field)));
+	EXPECT_STREQ("0", field);
+
+	free(buf);
+}
+
+/* Verifies that landlock_free_ruleset fires when a ruleset FD is closed. */
+TEST_F(trace, free_ruleset_on_close)
+{
+	struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+	};
+	int ruleset_fd;
+	char *buf;
+
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/* Closing the FD should trigger free_ruleset. */
+	close(ruleset_fd);
+
+	buf = tracefs_read_buf();
+	ASSERT_NE(NULL, buf);
+
+	EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_FREE_RULESET(TRACE_TASK)))
+	{
+		TH_LOG("Expected 1 free_ruleset event\n%s", buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * The following tests are intentionally elided because the underlying kernel
+ * mechanisms are already validated by audit tests:
+ *
+ * - Domain ID monotonicity: validated by audit_test.c:layers.  The same
+ *   landlock_get_id_range() function serves both audit and trace.
+ *
+ * - Domain deallocation order (LIFO): validated by audit_test.c:layers.  Trace
+ *   events fire from the same free_domain_work() code path.
+ *
+ * - Max-layer stacking (16 domains): validated by audit_test.c:layers.
+ *
+ * - IPv6 network tests: IPv6 hook dispatch uses the same
+ *   current_check_access_socket() as IPv4, validated by net_test.c:audit tests.
+ *
+ * - Per-access-right full matrix (all 16 FS rights): hook dispatch is validated
+ *   by fs_test.c:audit tests.  Trace tests verify representative samples to
+ *   ensure bitmask encoding is correct.
+ *
+ * - Combined log flag variants (e.g., LOG_SUBDOMAINS_OFF + LOG_NEW_EXEC_ON):
+ *   individual flag tests above cover each flag's effect on trace fields.  Flag
+ *   combination logic is validated by audit_test.c:audit_flags tests.
+ *
+ * - fs.refer multi-record denials and fs.change_topology (mount):
+ *   trace_denial() uses the same code path for all FS request types.  The
+ *   DENTRY union member is validated by the deny_access_fs_fields
+ *   test.  Audit tests in fs_test.c cover refer and mount denial specifics.
+ */
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/true.c b/tools/testing/selftests/landlock/true.c
index 3f9ccbf52783..1e39b664512d 100644
--- a/tools/testing/selftests/landlock/true.c
+++ b/tools/testing/selftests/landlock/true.c
@@ -1,5 +1,15 @@
 // SPDX-License-Identifier: GPL-2.0
+/*
+ * Minimal helper for Landlock selftests.  Opens its own working directory
+ * before exiting, which may trigger access denials depending on the sandbox
+ * configuration.
+ */
+
+#include <fcntl.h>
+#include <unistd.h>
+
 int main(void)
 {
+	close(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
 	return 0;
 }
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.