From: "Mickaël Salaün" <mic@digikod.net>
To: "Günther Noack" <gnoack@google.com>,
"Steven Rostedt" <rostedt@goodmis.org>
Cc: "Mickaël Salaün" <mic@digikod.net>,
"Christian Brauner" <brauner@kernel.org>,
"Jann Horn" <jannh@google.com>, "Jeff Xu" <jeffxu@google.com>,
"Justin Suess" <utilityemal77@gmail.com>,
"Kees Cook" <kees@kernel.org>,
"Masami Hiramatsu" <mhiramat@kernel.org>,
"Mathieu Desnoyers" <mathieu.desnoyers@efficios.com>,
"Matthieu Buffet" <matthieu@buffet.re>,
"Mikhail Ivanov" <ivanov.mikhail1@huawei-partners.com>,
"Tingmao Wang" <m@maowtm.org>,
kernel-team@cloudflare.com,
linux-security-module@vger.kernel.org,
linux-trace-kernel@vger.kernel.org
Subject: [PATCH v4 18/19] selftests/landlock: Add landlock_enforce_domain trace tests
Date: Tue, 11 Aug 2026 11:43:32 +0200 [thread overview]
Message-ID: <20260811094338.288094-19-mic@digikod.net> (raw)
In-Reply-To: <20260811094338.288094-1-mic@digikod.net>
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 <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v3:
https://patch.msgid.link/20260722171159.2776765-20-mic@digikod.net
- 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
next prev parent reply other threads:[~2026-08-11 10:00 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-11 9:43 [PATCH v4 00/19] Landlock tracepoints Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 01/19] landlock: Prepare ruleset and domain type split Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 02/19] landlock: Move domain query functions to domain.c Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 03/19] landlock: Split struct landlock_domain from struct landlock_ruleset Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 04/19] landlock: Split denial logging from audit into common framework Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 05/19] landlock: Decouple the per-denial logging decision from CONFIG_AUDIT Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 06/19] landlock: Consolidate access-right and scope names in a shared header Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 07/19] landlock: Add create_ruleset and free_ruleset tracepoints Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 08/19] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 09/19] landlock: Add create_domain and free_domain tracepoints Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 10/19] landlock: Add landlock_enforce_domain tracepoint Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 11/19] landlock: Add tracepoints for rule checking Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 12/19] landlock: Add landlock_deny_access_fs and landlock_deny_access_net Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 13/19] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 14/19] selftests/landlock: Add trace event test infrastructure and tests Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 15/19] selftests/landlock: Add filesystem tracepoint tests Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 16/19] selftests/landlock: Add network " Mickaël Salaün
2026-08-11 9:43 ` [PATCH v4 17/19] selftests/landlock: Add scope and ptrace " Mickaël Salaün
2026-08-11 9:43 ` Mickaël Salaün [this message]
2026-08-11 9:43 ` [PATCH v4 19/19] landlock: Document tracepoints Mickaël Salaün
2026-08-11 13:51 ` [PATCH v4 00/19] Landlock tracepoints Steven Rostedt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260811094338.288094-19-mic@digikod.net \
--to=mic@digikod.net \
--cc=brauner@kernel.org \
--cc=gnoack@google.com \
--cc=ivanov.mikhail1@huawei-partners.com \
--cc=jannh@google.com \
--cc=jeffxu@google.com \
--cc=kees@kernel.org \
--cc=kernel-team@cloudflare.com \
--cc=linux-security-module@vger.kernel.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=m@maowtm.org \
--cc=mathieu.desnoyers@efficios.com \
--cc=matthieu@buffet.re \
--cc=mhiramat@kernel.org \
--cc=rostedt@goodmis.org \
--cc=utilityemal77@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox