selftests/landlock: Add scope and ptrace tracepoint tests

Add trace tests for the landlock_deny_ptrace,
landlock_deny_scope_signal, and landlock_deny_scope_abstract_unix_socket
tracepoints, each placed alongside the functional tests for its
subsystem, mirroring the audit test layout.

Each tracepoint is exercised by a fixture with three variants that pin
both branches of the other-party domain field: denied against an
unsandboxed other party (other-party domain ID 0), denied against a
sandboxed other party (non-zero ID), and an allowed baseline that
records no event.  A second fixture per type exercises an alternate LSM
hook that reaches the same tracepoint with the same other-party domain
ID, since each denial type can be reached through more than one hook.

The datagram abstract-unix variant does not assert peer_pid, which is 0
for a datagram peer (no SO_PEERCRED); sun_path is the reliable peer
identifier.  The ptrace fixtures install a plain domain-creating ruleset
rather than a dedicated flag, since ptrace denial relies on domain
ancestry, not on a specific scoped flag.  The fixtures unshare the mount
namespace and remount / as MS_PRIVATE before mounting tracefs so the
helper instance is visible only to the test process.

Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20260811094338.288094-18-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
Mickaël Salaün 2026-08-11 11:43:31 +02:00
parent 6e2df0117b
commit aef2dd32dd
No known key found for this signature in database
GPG Key ID: E5E3D0E88C82F6D2
4 changed files with 1080 additions and 7 deletions

View File

@ -11,7 +11,9 @@
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <sched.h>
#include <signal.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/types.h>
@ -20,6 +22,7 @@
#include "audit.h"
#include "common.h"
#include "trace.h"
/* Copied from security/yama/yama_lsm.c */
#define YAMA_SCOPE_DISABLED 0
@ -430,4 +433,403 @@ TEST_F(audit, trace)
EXPECT_EQ(0, records.domain);
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_ptrace) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_ptrace)
{
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_DENY_PTRACE_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_ptrace)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_ptrace)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/* Denied: sandboxed child ptraces unsandboxed parent (tracee_domain=0). */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: sandboxed child ptraces a sandboxed parent, so the tracee is in a
* domain and tracee_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child uses PTRACE_TRACEME. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_ptrace, deny_ptrace)
{
char *buf, field[64], expected_pid[16];
int count, status;
pid_t child, parent;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
parent = getpid();
/*
* Set a known comm so the denied variant can verify both the trace line
* task name and the tracee_comm= field.
*/
prctl(PR_SET_NAME, "ll_trace_test");
/*
* For the non-zero tracee_domain case, sandbox the parent (the tracee)
* before forking. The child inherits that domain and adds its own
* layer, so the child (tracer) is not an ancestor of the tracee and the
* ptrace is still denied, with tracee_domain= naming the parent's
* domain.
*/
if (variant->sandbox_target)
create_domain(_metadata);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int ruleset_fd;
/*
* Any scope creates a domain. Ptrace denial checks
* domain ancestry, not specific flags.
*/
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
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);
/* PTRACE_ATTACH on unsandboxed parent: denied. */
if (ptrace(PTRACE_ATTACH, parent, NULL, NULL) == 0) {
ptrace(PTRACE_DETACH, parent, NULL, NULL);
_exit(2);
}
if (errno != EPERM)
_exit(3);
} else {
/* No sandbox: ptrace should succeed. */
if (ptrace(PTRACE_TRACEME) != 0)
_exit(1);
}
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test"));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_ptrace event, got %d\n%s", count,
buf);
}
/* Verify tracee_pid is the parent's TGID. */
snprintf(expected_pid, sizeof(expected_pid), "%d", parent);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/* Verify tracee_comm matches prctl(PR_SET_NAME). */
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_comm", field, sizeof(field)));
EXPECT_STREQ("ll_trace_test", field);
/*
* Verify tracee_domain: 0 when the tracee is unsandboxed,
* non-zero when the tracee is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected tracee_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_ptrace events, got %d\n%s",
count, buf);
}
}
free(buf);
}
/* clang-format off */
FIXTURE(trace_ptrace_traceme) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_ptrace_traceme)
{
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_DENY_PTRACE_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_ptrace_traceme)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_ptrace_traceme)
{
/* clang-format on */
bool sandbox_tracer;
bool sandbox_tracee;
int expect_denied;
};
/*
* Denied: a sandboxed tracer cannot trace the unsandboxed child that asked to
* be traced with PTRACE_TRACEME (tracee_domain=0).
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied) {
/* clang-format on */
.sandbox_tracer = true,
.sandbox_tracee = false,
.expect_denied = 1,
};
/*
* Denied: a sandboxed child in its own domain asks to be traced by a tracer in
* an unrelated domain, so the tracee is in a domain and tracee_domain= is
* non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, denied_scoped_tracee) {
/* clang-format on */
.sandbox_tracer = true,
.sandbox_tracee = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child uses PTRACE_TRACEME with an unsandboxed tracer. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_ptrace_traceme, allowed) {
/* clang-format on */
.sandbox_tracer = false,
.sandbox_tracee = false,
.expect_denied = 0,
};
TEST_F(trace_ptrace_traceme, deny_ptrace)
{
char *buf, field[64], expected_pid[16];
int count, status, sync_pipe[2];
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* Set a known comm so the denied variant can verify both the trace line
* task name and the tracee_comm= field. The tracee is the current
* (child) task for PTRACE_TRACEME, so the child inherits this name.
*/
prctl(PR_SET_NAME, "ll_trace_test");
ASSERT_EQ(0, pipe2(sync_pipe, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
char c;
close(sync_pipe[1]);
/*
* The tracee is the current task; for the non-zero
* tracee_domain case it sandboxes itself in its own domain,
* unrelated to the tracer's domain, so PTRACE_TRACEME is still
* denied and tracee_domain= names the child's own domain.
*/
if (variant->sandbox_tracee)
create_domain(_metadata);
/* Waits for the tracer (parent) to enter its domain, if any. */
if (read(sync_pipe[0], &c, 1) != 1)
_exit(1);
close(sync_pipe[0]);
if (variant->expect_denied) {
if (ptrace(PTRACE_TRACEME) == 0)
_exit(2);
if (errno != EPERM)
_exit(3);
} else {
if (ptrace(PTRACE_TRACEME) != 0)
_exit(4);
/* Lets the tracer reap the trace-stop and detach. */
raise(SIGSTOP);
}
_exit(0);
}
close(sync_pipe[0]);
/*
* For a denial, the proposed tracer must be in a domain that is not an
* ancestor of the tracee's domain. Sandboxing the parent after the
* fork gives it a domain unrelated to the child.
*/
if (variant->sandbox_tracer)
create_domain(_metadata);
/* Signals the child that the tracer is in its domain, if any. */
ASSERT_EQ(1, write(sync_pipe[1], ".", 1));
close(sync_pipe[1]);
if (!variant->expect_denied) {
/* PTRACE_TRACEME succeeded: reap the SIGSTOP and detach. */
ASSERT_EQ(child, waitpid(child, &status, WUNTRACED));
ASSERT_TRUE(WIFSTOPPED(status));
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test"));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_ptrace event, got %d\n%s", count,
buf);
}
/* Verify tracee_pid is the child's TGID (the traced task). */
snprintf(expected_pid, sizeof(expected_pid), "%d", child);
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/*
* Verify tracee_domain: 0 when the tracee is unsandboxed,
* non-zero when the tracee is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_PTRACE("ll_trace_test"),
"tracee_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_tracee, strcmp("0", field) != 0)
{
TH_LOG("Unexpected tracee_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_ptrace events, got %d\n%s",
count, buf);
}
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@ -12,6 +12,7 @@
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
@ -23,6 +24,9 @@
#include "audit.h"
#include "common.h"
#include "scoped_common.h"
#include "trace.h"
#define TRACE_TASK "scoped_abstract"
/* Number of pending connections queue to be hold. */
const short backlog = 10;
@ -1205,4 +1209,264 @@ TEST(self_connect)
_metadata->exit_code = KSFT_FAIL;
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_unix) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_unix)
{
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_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_unix)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_unix) {
/* clang-format on */
int sock_type; /* SOCK_STREAM (connect) or SOCK_DGRAM (sendto). */
bool sandbox;
bool sandbox_target; /* Peer owned by a domain: peer_domain != 0. */
int expect_denied;
};
/* clang-format off */
/* Stream: sandboxed client connect() to an unsandboxed peer (peer_domain=0). */
FIXTURE_VARIANT_ADD(trace_unix, stream_denied) {
.sock_type = SOCK_STREAM, .sandbox = true,
.sandbox_target = false, .expect_denied = 1,
};
/* Stream: peer socket owned by a domain, so peer_domain != 0. */
FIXTURE_VARIANT_ADD(trace_unix, stream_denied_scoped_peer) {
.sock_type = SOCK_STREAM, .sandbox = true,
.sandbox_target = true, .expect_denied = 1,
};
/* Stream: unsandboxed client, connect() succeeds, no event. */
FIXTURE_VARIANT_ADD(trace_unix, stream_allowed) {
.sock_type = SOCK_STREAM, .sandbox = false,
.sandbox_target = false, .expect_denied = 0,
};
/* Datagram: sandboxed client sendto() an unsandboxed peer (peer_domain=0). */
FIXTURE_VARIANT_ADD(trace_unix, dgram_denied) {
.sock_type = SOCK_DGRAM, .sandbox = true,
.sandbox_target = false, .expect_denied = 1,
};
/* Datagram: peer socket owned by a domain, so peer_domain != 0. */
FIXTURE_VARIANT_ADD(trace_unix, dgram_denied_scoped_peer) {
.sock_type = SOCK_DGRAM, .sandbox = true,
.sandbox_target = true, .expect_denied = 1,
};
/* Datagram: unsandboxed client, sendto() succeeds, no event. */
FIXTURE_VARIANT_ADD(trace_unix, dgram_allowed) {
.sock_type = SOCK_DGRAM, .sandbox = false,
.sandbox_target = false, .expect_denied = 0,
};
/* clang-format on */
/*
* A sandboxed thread reaching an abstract unix socket peer through connect(2)
* (stream) or sendto(2) (datagram) is denied and emits
* landlock_deny_scope_abstract_unix_socket. The abstract name is crafted with
* a space and an embedded NUL followed by an "END" marker to check the
* tracepoint escaping and its length handling (a raw space would break the
* sun_path field regex; strlen() would truncate at the NUL and drop "END").
* peer_pid is only meaningful for a stream peer (a datagram peer has no
* SO_PEERCRED), so it is asserted only there.
*/
TEST_F(trace_unix, deny_scope_unix)
{
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
};
char *buf, field[128], expected_pid[16];
int server_fd, count, status, name_len, addr_len;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* For the non-zero peer_domain case, sandbox the parent before it
* creates the server socket, so the socket carries the parent's domain
* and peer_domain= is non-zero.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
server_fd = socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0);
ASSERT_LE(0, server_fd);
addr.sun_path[0] = '\0';
name_len = snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1,
"landlock_trace_test_%d ", getpid());
addr.sun_path[1 + name_len] = '\0';
memcpy(addr.sun_path + 1 + name_len + 1, "END", 3);
addr_len =
offsetof(struct sockaddr_un, sun_path) + 1 + name_len + 1 + 3;
ASSERT_EQ(0, bind(server_fd, (struct sockaddr *)&addr, addr_len));
if (variant->sock_type == SOCK_STREAM)
ASSERT_EQ(0, listen(server_fd, 1));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_fd, ret;
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
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);
}
client_fd =
socket(AF_UNIX, variant->sock_type | SOCK_CLOEXEC, 0);
if (client_fd < 0)
_exit(1);
if (variant->sock_type == SOCK_STREAM)
ret = connect(client_fd, (struct sockaddr *)&addr,
addr_len);
else
ret = sendto(client_fd, ".", 1, 0,
(struct sockaddr *)&addr, addr_len);
if (variant->sandbox) {
/* Reaching the peer should be denied. */
if (ret != -1 || errno != EPERM) {
close(client_fd);
_exit(2);
}
} else {
/* No sandbox: stream connect() == 0, sendto() == 1. */
int ok = variant->sock_type == SOCK_STREAM ? 0 : 1;
if (ret != ok) {
close(client_fd);
_exit(2);
}
}
close(client_fd);
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
close(server_fd);
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(
buf, REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK));
if (!variant->expect_denied) {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope events, got %d\n%s",
count, buf);
}
free(buf);
return;
}
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_abstract_unix_socket event, "
"got %d\n%s",
count, buf);
}
/*
* sun_path is escaped: a raw space would break this field's [^ ]*$
* regex, so a successful extract proves the space was escaped, and its
* full length is honored: the "END" marker after the embedded NUL must
* survive (strlen() would truncate it at the NUL).
*/
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK),
"sun_path", field, sizeof(field)));
EXPECT_NE(NULL, strstr(field, "END"))
{
TH_LOG("sun_path truncated or unescaped: %s", field);
}
/* peer_pid is the parent's PID for a stream peer (0 for datagram). */
if (variant->sock_type == SOCK_STREAM) {
snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(
TRACE_TASK),
"peer_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
}
/* peer_domain: 0 when the peer is unsandboxed, non-zero otherwise. */
ASSERT_EQ(0, tracefs_extract_field(
buf,
REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK),
"peer_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected peer_domain=%s", field);
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@ -10,7 +10,9 @@
#include <fcntl.h>
#include <linux/landlock.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
@ -18,6 +20,9 @@
#include "common.h"
#include "scoped_common.h"
#include "trace.h"
#define TRACE_TASK "scoped_signal_t"
/* This variable is used for handling several signals. */
static volatile sig_atomic_t is_signaled;
@ -762,4 +767,403 @@ TEST(sigio_to_pgid_self)
EXPECT_EQ(0, close(trigger[1]));
}
/* Trace tests */
/* clang-format off */
FIXTURE(trace_signal) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_signal)
{
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_DENY_SCOPE_SIGNAL_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_signal)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_signal)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/* Denied: sandboxed child signals unsandboxed parent (target_domain=0). */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: sandboxed child signals a sandboxed parent, so the target is in a
* domain and target_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: unsandboxed child signals unsandboxed parent. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_signal, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_signal, deny_scope_signal)
{
char *buf, field[64], expected_pid[16];
int count, status;
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
/*
* For the non-zero target_domain case, sandbox the parent (the signal
* target) before forking. The child inherits that domain and adds its
* own scoped layer, so the signal is still denied and target_domain=
* names the parent's domain.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
if (variant->sandbox) {
struct landlock_ruleset_attr ruleset_attr = {
.scoped = LANDLOCK_SCOPE_SIGNAL,
};
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
_exit(1);
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);
}
if (variant->sandbox) {
/* Signal to unsandboxed parent should be denied. */
if (kill(getppid(), 0) == 0)
_exit(2);
if (errno != EPERM)
_exit(3);
} else {
/* No sandbox: kill should succeed. */
if (kill(getppid(), 0) != 0)
_exit(1);
}
_exit(0);
}
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_signal event, got %d\n%s",
count, buf);
}
/* Verify target_pid is the parent's PID. */
snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_pid", field, sizeof(field)));
EXPECT_STREQ(expected_pid, field);
/*
* Verify target_domain: 0 when the target is unsandboxed,
* non-zero when the target is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected target_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope_signal events, "
"got %d\n%s",
count, buf);
}
}
free(buf);
}
/*
* Trace test for the asynchronous SIGIO/SIGURG delivery path
* (hook_file_send_sigiotask), which reaches the same landlock_deny_scope_signal
* tracepoint as a synchronous kill(2) but through fcntl(F_SETOWN).
*/
/* clang-format off */
FIXTURE(trace_fown) {
/* clang-format on */
int tracefs_ok;
};
FIXTURE_SETUP(trace_fown)
{
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_DENY_SCOPE_SIGNAL_ENABLE, true));
ASSERT_EQ(0, tracefs_clear());
clear_cap(_metadata, CAP_SYS_ADMIN);
}
FIXTURE_TEARDOWN(trace_fown)
{
if (!self->tracefs_ok)
return;
set_cap(_metadata, CAP_SYS_ADMIN);
tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false);
tracefs_fixture_teardown();
clear_cap(_metadata, CAP_SYS_ADMIN);
}
/* clang-format off */
FIXTURE_VARIANT(trace_fown)
{
/* clang-format on */
bool sandbox;
bool sandbox_target;
int expect_denied;
};
/*
* Denied: a sandboxed file owner's SIGURG is delivered to an unsandboxed target
* process (target_domain=0).
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, denied) {
/* clang-format on */
.sandbox = true,
.sandbox_target = false,
.expect_denied = 1,
};
/*
* Denied: the SIGURG target sandboxes itself in its own domain, so the target
* is in a domain and target_domain= is non-zero.
*/
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, denied_scoped_target) {
/* clang-format on */
.sandbox = true,
.sandbox_target = true,
.expect_denied = 1,
};
/* Allowed: an unsandboxed file owner delivers SIGURG. */
/* clang-format off */
FIXTURE_VARIANT_ADD(trace_fown, allowed) {
/* clang-format on */
.sandbox = false,
.sandbox_target = false,
.expect_denied = 0,
};
TEST_F(trace_fown, deny_scope_fown)
{
int server_socket, recv_socket;
struct service_fixture server_address;
char buffer_parent, field[64], *buf;
int status, count;
int pipe_parent[2], pipe_child[2];
pid_t child;
if (!self->tracefs_ok)
SKIP(return, "tracefs not available");
memset(&server_address, 0, sizeof(server_address));
set_unix_address(&server_address, 0);
ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
child = fork();
ASSERT_LE(0, child);
if (child == 0) {
int client_socket;
char buffer_child;
EXPECT_EQ(0, close(pipe_parent[1]));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(0, setup_signal_handler(SIGURG));
client_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, client_socket);
/*
* The SIGURG target is this child; for the non-zero
* target_domain case it sandboxes itself in its own domain,
* unrelated to the file owner's domain.
*/
if (variant->sandbox_target)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/* Waits for the parent to listen. */
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr,
server_address.unix_addr_len));
/*
* Waits for the parent to accept the connection, sandbox
* itself, and call fcntl(F_SETOWN).
*/
ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1));
/* Triggers the asynchronous SIGURG to this file owner. */
ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB));
EXPECT_EQ(0, close(client_socket));
ASSERT_EQ(1, write(pipe_child[1], ".", 1));
EXPECT_EQ(0, close(pipe_child[1]));
_exit(0);
return;
}
EXPECT_EQ(0, close(pipe_parent[0]));
EXPECT_EQ(0, close(pipe_child[1]));
server_socket = socket(AF_UNIX, SOCK_STREAM, 0);
ASSERT_LE(0, server_socket);
ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr,
server_address.unix_addr_len));
ASSERT_EQ(0, listen(server_socket, backlog));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
recv_socket = accept(server_socket, NULL, NULL);
ASSERT_LE(0, recv_socket);
/*
* The file owner is the denying subject; its domain is captured at
* fcntl(F_SETOWN) time, so sandbox it before setting the owner.
*/
if (variant->sandbox)
create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
/*
* Sets the child to receive SIGURG for MSG_OOB. This uncommon use is a
* valid attack scenario which also simplifies this test.
*/
ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child));
ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
/* Waits for the child to send MSG_OOB. */
ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1));
EXPECT_EQ(0, close(pipe_child[0]));
ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB));
EXPECT_EQ(0, close(recv_socket));
EXPECT_EQ(0, close(server_socket));
ASSERT_EQ(child, waitpid(child, &status, 0));
ASSERT_TRUE(WIFEXITED(status));
EXPECT_EQ(0, WEXITSTATUS(status));
buf = tracefs_read_buf();
ASSERT_NE(NULL, buf);
count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK));
if (variant->expect_denied) {
EXPECT_EQ(variant->expect_denied, count)
{
TH_LOG("Expected deny_scope_signal event, got %d\n%s",
count, buf);
}
/*
* Verify target_domain: 0 when the target is unsandboxed,
* non-zero when the target is in a domain.
*/
ASSERT_EQ(0, tracefs_extract_field(
buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
"target_domain", field, sizeof(field)));
EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0)
{
TH_LOG("Unexpected target_domain=%s", field);
}
} else {
EXPECT_EQ(0, count)
{
TH_LOG("Expected 0 deny_scope_signal events, "
"got %d\n%s",
count, buf);
}
}
free(buf);
}
TEST_HARNESS_MAIN

View File

@ -146,13 +146,14 @@
"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]\\+ " \
#define REGEX_DENY_PTRACE(task) \
TRACE_PREFIX(task) \
"landlock_deny_ptrace: " \
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"tracee_domain=[0-9a-f]\\+ " \
"tracee_pid=[0-9]\\+ " \
"tracee_comm=[^ ]*$"
#define REGEX_DENY_SCOPE_SIGNAL(task) \
@ -161,6 +162,7 @@
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"target_domain=[0-9a-f]\\+ " \
"target_pid=[0-9]\\+ " \
"target_comm=[^ ]*$"
@ -170,6 +172,7 @@
"domain=[0-9a-f]\\+ " \
"same_exec=[01] " \
"logged=[01] " \
"peer_domain=[0-9a-f]\\+ " \
"peer_pid=[0-9]\\+ " \
"sun_path=[^ ]*$"