mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-30 15:43:08 -04:00
selftests/landlock: Add landlock_enforce_domain trace tests
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> Link: https://patch.msgid.link/20260811094338.288094-19-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user