[PATCH v4 18/19] selftests/landlock: Add landlock_enforce_domain trace 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 trace tests for the landlock_enforce_domain event in trace_test.c,
asserting field counts after the syscall returns rather than line
ordering across per-CPU buffers.  They cover single-threaded and TSYNC
enforcement (complete and process_wide set), a multi-threaded non-TSYNC
process (process_wide clear), the single-threaded non-leader edge case,
the flags-only path that creates no domain, and a thread-sync abort that
emits create_domain and free_domain but no enforce_domain.

landlock_enforce_domain is added to the fixture enable path and every
disable list so its zero-events assertions cannot be tripped by a stray
enforcement event.

Test coverage for security/landlock is 91.6% of 2571 lines according to
LLVM 22.

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

Changes since v3:
https://patch.msgid.link/[email protected]
- Rework the enforce_domain field tests into a trace_enforce
  FIXTURE_VARIANT table (variants single, no_new_privs, tsync_single,
  tsync_multithread, tsync_no_new_privs, multithread_non_tsync), each
  checking the total/complete/process_wide/no_new_privs event counts
  for a (nthreads, flags) input.  This covers the new no_new_privs
  field and both ways it is set: the NO_NEW_PRIVS flag (which makes
  child_enforce() skip prctl()) or a prior prctl(), each propagated to
  the siblings by TSYNC; count_enforce_matches() gains a no_new_privs
  regex field.  enforce_flags_only, enforce_single_non_leader, and
  enforce_abort stay standalone (distinct scaffolding).
- Repurpose enforce_single_non_leader: an un-reaped zombie group
  leader keeps get_nr_threads() at 2, so a single-live-thread group
  is unreachable and the test always skipped; assert the reachable
  behavior instead, a non-leader enforcing while the zombie leader
  is still counted reports process_wide=0.  Drop the now-unused
  read_thread_count()/ENFORCE_SKIP_EXIT helpers.
- Raise enforce_abort's sibling count to 200 (matching tsync_test's
  NUM_IDLE_THREADS) so the thread-sync abort race has a window to
  fire on multi-core hosts instead of almost never landing with a
  handful of threads; the test still skips gracefully where the race
  does not occur (e.g. single-CPU UML).
- Drop the unused, stale REGEX_ENFORCE_DOMAIN macro, superseded by
  count_enforce_matches() (it also lacked the no_new_privs field).

Changes since v2:
- New patch.
---
 tools/testing/selftests/landlock/trace.h      |   2 +
 tools/testing/selftests/landlock/trace_test.c | 519 ++++++++++++++++++
 2 files changed, 521 insertions(+)

diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
index 4c82ecffc235..ba0c5e92001f 100644
--- a/tools/testing/selftests/landlock/trace.h
+++ b/tools/testing/selftests/landlock/trace.h
@@ -25,6 +25,8 @@
 	TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
 #define TRACEFS_CREATE_DOMAIN_ENABLE \
 	TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
+#define TRACEFS_ENFORCE_DOMAIN_ENABLE \
+	TRACEFS_LANDLOCK_DIR "/landlock_enforce_domain/enable"
 #define TRACEFS_ADD_RULE_FS_ENABLE \
 	TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
 #define TRACEFS_ADD_RULE_NET_ENABLE \
diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c
index a141f22ad98f..afdaf8511b3a 100644
--- a/tools/testing/selftests/landlock/trace_test.c
+++ b/tools/testing/selftests/landlock/trace_test.c
@@ -9,6 +9,7 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/landlock.h>
+#include <pthread.h>
 #include <sched.h>
 #include <stdio.h>
 #include <string.h>
@@ -47,6 +48,7 @@ FIXTURE_SETUP(trace)
 
 	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_ENFORCE_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));
@@ -69,6 +71,7 @@ FIXTURE_TEARDOWN(trace)
 	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_ENFORCE_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);
@@ -98,6 +101,8 @@ TEST_F(trace, no_trace_when_disabled)
 	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_ENFORCE_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));
@@ -1069,6 +1074,520 @@ TEST_F(trace, free_ruleset_on_close)
 	free(buf);
 }
 
+/*
+ * Counts landlock_enforce_domain lines, filtered by @domain (NULL matches any),
+ * @complete and @process_wide (a negative value matches any).  Builds the
+ * anchored regex dynamically so a single helper covers every field assertion.
+ */
+static int count_enforce_matches(const char *buf, const char *domain,
+				 int complete, int process_wide,
+				 int no_new_privs)
+{
+	char pattern[512], dom[80], comp[8], pw[8], nnp[8];
+
+	if (domain)
+		snprintf(dom, sizeof(dom), "%s", domain);
+	else
+		snprintf(dom, sizeof(dom), "[0-9a-f]\\+");
+	if (complete < 0)
+		snprintf(comp, sizeof(comp), "[01]");
+	else
+		snprintf(comp, sizeof(comp), "%d", complete);
+	if (process_wide < 0)
+		snprintf(pw, sizeof(pw), "[01]");
+	else
+		snprintf(pw, sizeof(pw), "%d", process_wide);
+	if (no_new_privs < 0)
+		snprintf(nnp, sizeof(nnp), "[01]");
+	else
+		snprintf(nnp, sizeof(nnp), "%d", no_new_privs);
+
+	snprintf(pattern, sizeof(pattern),
+		 TRACE_PREFIX(TRACE_TASK) "landlock_enforce_domain: "
+					  "domain=%s "
+					  "complete=%s process_wide=%s "
+					  "no_new_privs=%s$",
+		 dom, comp, pw, nnp);
+	return tracefs_count_matches(buf, pattern);
+}
+
+/* Idle sibling: waits on the barrier so it is a live thread, then sleeps. */
+static void *enforce_idle(void *arg)
+{
+	pthread_barrier_t *barrier = arg;
+
+	pthread_barrier_wait(barrier);
+	while (true)
+		sleep(1);
+	return NULL;
+}
+
+/*
+ * Child body: spawns @nthreads idle siblings (barrier-synchronized so they are
+ * live when the syscall runs), then enforces a domain with @flags.  Returns 0
+ * on success; the process exits afterwards, reaping the siblings.
+ */
+static int child_enforce(int nthreads, __u32 flags)
+{
+	pthread_t threads[8];
+	pthread_barrier_t barrier;
+	int ruleset_fd, i;
+
+	if (nthreads > 0) {
+		if (pthread_barrier_init(&barrier, NULL, nthreads + 1))
+			return 1;
+		for (i = 0; i < nthreads; i++)
+			if (pthread_create(&threads[i], NULL, enforce_idle,
+					   &barrier))
+				return 1;
+		pthread_barrier_wait(&barrier);
+	}
+
+	ruleset_fd = build_enforce_ruleset();
+	if (ruleset_fd < 0)
+		return 1;
+
+	/*
+	 * LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS sets no_new_privs itself, so skip
+	 * the prctl() to exercise that path; otherwise Landlock requires
+	 * no_new_privs up front.
+	 */
+	if (!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS))
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	if (landlock_restrict_self(ruleset_fd, flags))
+		return 1;
+	close(ruleset_fd);
+	return 0;
+}
+
+/*
+ * Runs in a spawned thread after the group leader called pthread_exit().  The
+ * leader lingers as an un-reaped zombie, so get_nr_threads() still counts it
+ * and this non-leader is not the only thread; enforcing here therefore reports
+ * process_wide=0.
+ */
+static void *enforce_nonleader(void *arg)
+{
+	int ruleset_fd;
+
+	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);
+	_exit(0);
+}
+
+/*
+ * Collapses the enforce_domain field cases into one parametrized test.  Each
+ * variant runs child_enforce(nthreads, flags) and checks the resulting
+ * enforce_domain events.  The flags column also selects how no_new_privs is
+ * set: with LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS child_enforce() skips the
+ * prctl() so the flag sets it (and, with TSYNC, propagates to the siblings);
+ * otherwise a prior prctl() sets it on the caller (and TSYNC propagates that).
+ */
+
+/* clang-format off */
+FIXTURE(trace_enforce) {
+	/* clang-format on */
+	int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_enforce)
+{
+	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_ENFORCE_DOMAIN_ENABLE, true));
+	ASSERT_EQ(0, tracefs_clear());
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_enforce)
+{
+	if (!self->tracefs_ok)
+		return;
+
+	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_ENFORCE_DOMAIN_ENABLE, false);
+	tracefs_fixture_teardown();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/* clang-format off */
+FIXTURE_VARIANT(trace_enforce) {
+	/* clang-format on */
+	/* Inputs to child_enforce(). */
+	int nthreads;
+	__u32 flags;
+	/* Expected enforce_domain event counts. */
+	int total;
+	int complete;
+	int process_wide;
+	int no_new_privs;
+};
+
+/* clang-format off */
+
+/* Single thread, no flags: prctl-backed no_new_privs. */
+FIXTURE_VARIANT_ADD(trace_enforce, single) {
+	.nthreads = 0, .flags = 0,
+	.total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
+};
+
+/* Single thread: the NO_NEW_PRIVS flag sets no_new_privs (no prctl). */
+FIXTURE_VARIANT_ADD(trace_enforce, no_new_privs) {
+	.nthreads = 0, .flags = LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
+	.total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
+};
+
+/* TSYNC on a lone thread still concludes, process-wide. */
+FIXTURE_VARIANT_ADD(trace_enforce, tsync_single) {
+	.nthreads = 0, .flags = LANDLOCK_RESTRICT_SELF_TSYNC,
+	.total = 1, .complete = 1, .process_wide = 1, .no_new_privs = 1,
+};
+
+/* TSYNC sweeps N siblings; the caller's prctl-backed nnp propagates to all. */
+FIXTURE_VARIANT_ADD(trace_enforce, tsync_multithread) {
+	.nthreads = 3, .flags = LANDLOCK_RESTRICT_SELF_TSYNC,
+	.total = 4, .complete = 1, .process_wide = 4, .no_new_privs = 4,
+};
+
+/* TSYNC + NO_NEW_PRIVS flag sets nnp on the caller and every swept sibling. */
+FIXTURE_VARIANT_ADD(trace_enforce, tsync_no_new_privs) {
+	.nthreads = 3,
+	.flags = LANDLOCK_RESTRICT_SELF_TSYNC | LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS,
+	.total = 4, .complete = 1, .process_wide = 4, .no_new_privs = 4,
+};
+
+/* Non-TSYNC on a multi-threaded process enforces only the caller. */
+FIXTURE_VARIANT_ADD(trace_enforce, multithread_non_tsync) {
+	.nthreads = 3, .flags = 0,
+	.total = 1, .complete = 1, .process_wide = 0, .no_new_privs = 1,
+};
+
+/* clang-format on */
+
+/*
+ * One create_domain and variant->total enforce_domain events sharing that
+ * domain ID; complete=1 marks the single concluding event, and the process_wide
+ * / no_new_privs counts match the variant.  Counts are order-independent,
+ * evaluated after the syscall returns.
+ */
+TEST_F(trace_enforce, enforce)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+	char domain[64];
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		_exit(child_enforce(variant->nthreads, variant->flags));
+
+	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_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(variant->total, count_enforce_matches(buf, NULL, -1, -1, -1))
+	{
+		TH_LOG("Expected %d enforce_domain events\n%s", variant->total,
+		       buf);
+	}
+	EXPECT_EQ(variant->complete,
+		  count_enforce_matches(buf, NULL, 1, -1, -1));
+	EXPECT_EQ(variant->total - variant->complete,
+		  count_enforce_matches(buf, NULL, 0, -1, -1));
+	EXPECT_EQ(variant->process_wide,
+		  count_enforce_matches(buf, NULL, -1, 1, -1));
+	EXPECT_EQ(variant->total - variant->process_wide,
+		  count_enforce_matches(buf, NULL, -1, 0, -1));
+	EXPECT_EQ(variant->no_new_privs,
+		  count_enforce_matches(buf, NULL, -1, -1, 1));
+
+	ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+					   "domain", domain, sizeof(domain)));
+	EXPECT_EQ(variant->total,
+		  count_enforce_matches(buf, domain, -1, -1, -1));
+
+	free(buf);
+}
+
+/*
+ * A non-leader thread enforcing a domain while the group leader lingers as an
+ * un-reaped zombie reports process_wide=0: get_nr_threads() counts the zombie
+ * leader, so the group is not single-threaded.  This is the reachable half of
+ * the caveat that process_wide==0 never proves the process is multi-threaded
+ * (get_nr_threads(), unlike the leader-relative thread_group_empty(), counts
+ * the zombie leader).
+ */
+TEST_F(trace, enforce_single_non_leader)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0) {
+		pthread_t worker;
+
+		if (pthread_create(&worker, NULL, enforce_nonleader, NULL))
+			_exit(1);
+		/* Leader leaves; the worker enforces as a non-leader. */
+		pthread_exit(NULL);
+	}
+
+	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_EQ(1,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(1, count_enforce_matches(buf, NULL, 1, 0, -1))
+	{
+		TH_LOG("Expected complete=1 process_wide=0 for non-leader\n%s",
+		       buf);
+	}
+
+	free(buf);
+}
+
+/*
+ * Verifies the flags-only path (ruleset_fd == -1) creates no domain and emits
+ * neither create_domain nor enforce_domain, with and without TSYNC.
+ */
+TEST_F(trace, enforce_flags_only)
+{
+	pid_t pid;
+	int status;
+	char *buf;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0) {
+		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+		if (landlock_restrict_self(
+			    -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
+			_exit(1);
+		if (landlock_restrict_self(
+			    -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+					LANDLOCK_RESTRICT_SELF_TSYNC))
+			_exit(1);
+		_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_EQ(0,
+		  tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)));
+	EXPECT_EQ(0, count_enforce_matches(buf, NULL, -1, -1, -1))
+	{
+		TH_LOG("No enforce_domain expected on flags-only path\n%s",
+		       buf);
+	}
+
+	free(buf);
+}
+
+static void enforce_nop_handler(int sig)
+{
+}
+
+struct abort_signaler_data {
+	pthread_t target;
+	volatile bool stop;
+};
+
+/*
+ * Hammers the target thread with SIGUSR1 to interrupt the TSYNC prepare wait.
+ */
+static void *abort_signaler(void *arg)
+{
+	struct abort_signaler_data *data = arg;
+
+	while (!data->stop)
+		pthread_kill(data->target, SIGUSR1);
+	return NULL;
+}
+
+/*
+ * Child body for the abort test: with idle siblings and a signaler interrupting
+ * it, repeatedly enforces under TSYNC.  An interrupted attempt aborts its
+ * just-created domain (create_domain + free_domain, zero enforce_domain) while
+ * -ERESTARTNOINTR transparently restarts the syscall, so a successful retry may
+ * add its own full lifecycle.
+ */
+static int child_abort(int nsiblings, int attempts)
+{
+	pthread_t threads[200];
+	pthread_t signaler;
+	pthread_barrier_t barrier;
+	struct abort_signaler_data data = {};
+	struct sigaction sa = {};
+	int i;
+
+	sa.sa_handler = enforce_nop_handler;
+	if (sigaction(SIGUSR1, &sa, NULL))
+		return 1;
+
+	if (pthread_barrier_init(&barrier, NULL, nsiblings + 1))
+		return 1;
+	for (i = 0; i < nsiblings; i++)
+		if (pthread_create(&threads[i], NULL, enforce_idle, &barrier))
+			return 1;
+	pthread_barrier_wait(&barrier);
+
+	data.target = pthread_self();
+	if (pthread_create(&signaler, NULL, abort_signaler, &data))
+		return 1;
+
+	prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+	for (i = 0; i < attempts; i++) {
+		int ruleset_fd = build_enforce_ruleset();
+
+		if (ruleset_fd < 0)
+			break;
+		/*
+		 * Ignore the result: an abort returns an error, that is fine.
+		 */
+		landlock_restrict_self(ruleset_fd,
+				       LANDLOCK_RESTRICT_SELF_TSYNC);
+		close(ruleset_fd);
+	}
+
+	data.stop = true;
+	pthread_join(signaler, NULL);
+	return 0;
+}
+
+/*
+ * Verifies the abort contract: a domain aborted by a thread-sync failure emits
+ * create_domain and free_domain but zero enforce_domain.  The signal race is
+ * probabilistic and -ERESTARTNOINTR may add a successful retry's lifecycle, so
+ * events are grouped by domain ID and the test SKIPs if no abort occurred.
+ */
+TEST_F(trace, enforce_abort)
+{
+	pid_t pid;
+	int status, retry;
+	char *buf = NULL;
+	const char *cursor;
+	char domain[64];
+	bool abort_found = false;
+
+	ASSERT_EQ(0, tracefs_clear_buf());
+
+	/* free_domain fires from a kworker, so widen the filter first. */
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	tracefs_clear_pid_filter();
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	pid = fork();
+	ASSERT_LE(0, pid);
+	if (pid == 0)
+		/*
+		 * Match tsync_test's NUM_IDLE_THREADS: enough siblings that
+		 * credential preparation runs in several serialized waves,
+		 * giving the signaler a window to interrupt the thread-sync
+		 * wait and abort the operation.  A handful of threads finishes
+		 * in a single wave, leaving no window (the abort never fires).
+		 */
+		_exit(child_abort(200, 8));
+
+	ASSERT_EQ(pid, waitpid(pid, &status, 0));
+	ASSERT_TRUE(WIFEXITED(status));
+	EXPECT_EQ(0, WEXITSTATUS(status));
+
+	/* Poll for the asynchronous free_domain events. */
+	for (retry = 0; retry < 10; retry++) {
+		usleep(100000);
+		set_cap(_metadata, CAP_SYS_ADMIN);
+		free(buf);
+		buf = tracefs_read_trace();
+		clear_cap(_metadata, CAP_SYS_ADMIN);
+		ASSERT_NE(NULL, buf);
+	}
+
+	set_cap(_metadata, CAP_SYS_ADMIN);
+	ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+	clear_cap(_metadata, CAP_SYS_ADMIN);
+
+	/*
+	 * Walk every create_domain and look for one whose domain ID has zero
+	 * enforce_domain events but a matching free_domain: that is an aborted
+	 * domain (created, never enforced, freed).
+	 */
+	cursor = buf;
+	while (tracefs_extract_field(cursor, REGEX_CREATE_DOMAIN(TRACE_TASK),
+				     "domain", domain, sizeof(domain)) == 0) {
+		const char *cd, *nl;
+		char free_pattern[256];
+
+		if (count_enforce_matches(buf, domain, -1, -1, -1) == 0) {
+			snprintf(
+				free_pattern, sizeof(free_pattern),
+				TRACE_PREFIX(
+					KWORKER_TASK) "landlock_free_domain: "
+						      "domain=%s denials=[0-9]\\+$",
+				domain);
+			if (tracefs_count_matches(buf, free_pattern) >= 1)
+				abort_found = true;
+		}
+
+		cd = strstr(cursor, "landlock_create_domain:");
+		if (!cd)
+			break;
+		nl = strchr(cd, '\n');
+		if (!nl)
+			break;
+		cursor = nl + 1;
+	}
+
+	if (!abort_found) {
+		free(buf);
+		SKIP(return, "signal race did not produce a thread-sync abort");
+	}
+
+	free(buf);
+}
+
 /*
  * The following tests are intentionally elided because the underlying kernel
  * mechanisms are already validated by audit tests:
-- 
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.