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 9:44 UTC|newest]
Thread overview: 25+ 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-16 17:56 ` Tingmao Wang
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-20 10:45 ` Günther Noack
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-16 22:39 ` Tingmao Wang
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-20 10:36 ` Günther Noack
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.