mirror of
https://github.com/torvalds/linux.git
synced 2026-09-25 17:42:03 +02:00
Filesystem denial traces identify the policy change needed to allow a request, so require exact blocker values rather than merely nonempty output. Pin a READ_DIR denial to exactly one event with blockers=read_dir. Pin a REFER-only mount denial to EPERM and exactly one event with blockers=change_topology. The mount child retains CAP_SYS_ADMIN so Landlock is the only expected source of EPERM. This prevents a later capability failure from masking a Landlock regression; the trace-collecting parent remains unsandboxed. Cc: Günther Noack <gnoack@google.com> Cc: Steven Rostedt <rostedt@goodmis.org> Link: https://patch.msgid.link/20260918185036.608651-8-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
733 lines
20 KiB
C
733 lines
20 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Landlock tests - Filesystem tracepoints
|
|
*
|
|
* Copyright © 2026 Cloudflare, Inc.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <linux/landlock.h>
|
|
#include <sched.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/mount.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "common.h"
|
|
#include "trace.h"
|
|
|
|
#define TRACE_TASK "trace_fs_test"
|
|
|
|
/* Mirrors TRACE_SEQ_SIZE, conservatively larger than the usable buffer. */
|
|
#define TRACE_SEQUENCE_SIZE 8192
|
|
#define OCTAL_ESCAPE_LEN 4
|
|
#define LONG_PATH_COMPONENT_COUNT 11
|
|
#define LONG_PATH_COMPONENT_LEN 240
|
|
#define LONG_PATH_LEN \
|
|
(LONG_PATH_COMPONENT_COUNT * (LONG_PATH_COMPONENT_LEN + 1) + \
|
|
sizeof("/tmp"))
|
|
#define LONG_ESCAPED_PATH_LEN \
|
|
(LONG_PATH_COMPONENT_COUNT * LONG_PATH_COMPONENT_LEN * OCTAL_ESCAPE_LEN)
|
|
|
|
static_assert(LONG_ESCAPED_PATH_LEN > TRACE_SEQUENCE_SIZE,
|
|
"escaped path must exceed the trace sequence");
|
|
static_assert(LONG_PATH_LEN < PATH_MAX, "path must fit in PATH_MAX");
|
|
|
|
static void create_long_path(struct __test_metadata *const _metadata,
|
|
char *path)
|
|
{
|
|
size_t path_len;
|
|
|
|
strcpy(path, "/tmp");
|
|
path_len = strlen(path);
|
|
|
|
set_cap(_metadata, CAP_SYS_ADMIN);
|
|
ASSERT_EQ(0, mount("tmpfs", "/tmp", "tmpfs", 0, NULL));
|
|
clear_cap(_metadata, CAP_SYS_ADMIN);
|
|
|
|
for (int i = 0; i < LONG_PATH_COMPONENT_COUNT; i++) {
|
|
path[path_len++] = '/';
|
|
memset(path + path_len, ' ', LONG_PATH_COMPONENT_LEN);
|
|
path_len += LONG_PATH_COMPONENT_LEN;
|
|
path[path_len] = '\0';
|
|
ASSERT_EQ(0, mkdir(path, 0700));
|
|
}
|
|
}
|
|
|
|
static void expect_truncated_path(struct __test_metadata *const _metadata,
|
|
const char *const trace,
|
|
const char *const event_regex)
|
|
{
|
|
static const char marker[] = "\xe2\x80\xa6";
|
|
char *path;
|
|
size_t path_len;
|
|
|
|
path = malloc(TRACE_SEQUENCE_SIZE);
|
|
ASSERT_NE(NULL, path);
|
|
ASSERT_EQ(0, tracefs_extract_field(trace, event_regex, "path", path,
|
|
TRACE_SEQUENCE_SIZE));
|
|
EXPECT_EQ(path, strstr(path, "/tmp/"));
|
|
EXPECT_NE(NULL, strstr(path, "\\040"));
|
|
|
|
path_len = strlen(path);
|
|
ASSERT_LE(sizeof(marker) - 1, path_len);
|
|
EXPECT_STREQ(marker, path + path_len - (sizeof(marker) - 1));
|
|
free(path);
|
|
}
|
|
|
|
/*
|
|
* Like REGEX_DENY_ACCESS_FS(), but pins the logged field to a specific value
|
|
* ("0" or "1") so a test can tell a suppressed (quiet) denial from a logged
|
|
* one. The tracepoint fires for every denial; logged carries the audit
|
|
* verdict.
|
|
*/
|
|
#define REGEX_DENY_ACCESS_FS_LOGGED(task, log) \
|
|
TRACE_PREFIX(task) \
|
|
"landlock_deny_access_fs: " \
|
|
"domain=[0-9a-f]\\+ " \
|
|
"same_exec=[01] " \
|
|
"logged=" log " " \
|
|
"blockers=[a-z_|]* " \
|
|
"dev=[0-9]\\+:[0-9]\\+ " \
|
|
"ino=[0-9]\\+ " \
|
|
"path=[^ ]*$"
|
|
|
|
/* clang-format off */
|
|
FIXTURE(trace_fs) {
|
|
/* clang-format on */
|
|
int tracefs_ok;
|
|
};
|
|
|
|
FIXTURE_SETUP(trace_fs)
|
|
{
|
|
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_ADD_RULE_PATH_BENEATH_ENABLE,
|
|
true));
|
|
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
|
|
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
|
|
ASSERT_EQ(0, tracefs_clear());
|
|
clear_cap(_metadata, CAP_SYS_ADMIN);
|
|
}
|
|
|
|
FIXTURE_TEARDOWN(trace_fs)
|
|
{
|
|
if (!self->tracefs_ok)
|
|
return;
|
|
|
|
set_cap(_metadata, CAP_SYS_ADMIN);
|
|
tracefs_enable_event(TRACEFS_ADD_RULE_PATH_BENEATH_ENABLE, false);
|
|
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
|
|
tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
|
|
tracefs_fixture_teardown();
|
|
clear_cap(_metadata, CAP_SYS_ADMIN);
|
|
}
|
|
|
|
/*
|
|
* Baseline: verifies that without Landlock, the operation succeeds and no
|
|
* check_rule or deny_access trace events fire.
|
|
*/
|
|
TEST_F(trace_fs, unsandboxed)
|
|
{
|
|
char *buf;
|
|
int count, status, fd;
|
|
pid_t pid;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
pid = fork();
|
|
ASSERT_LE(0, pid);
|
|
|
|
if (pid == 0) {
|
|
/*
|
|
* No sandbox: verify that a normal FS access does not produce
|
|
* Landlock trace events.
|
|
*/
|
|
fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0)
|
|
close(fd);
|
|
_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);
|
|
|
|
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
|
EXPECT_EQ(0, count);
|
|
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
|
EXPECT_EQ(0, count);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that adding a filesystem rule emits a landlock_add_rule_path_beneath
|
|
* event with the expected path and field values: the ruleset ID and
|
|
* access_rights are non-zero, and the path matches.
|
|
*/
|
|
TEST_F(trace_fs, add_rule_path_beneath)
|
|
{
|
|
struct landlock_ruleset_attr ruleset_attr = {
|
|
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
|
|
LANDLOCK_ACCESS_FS_WRITE_FILE |
|
|
LANDLOCK_ACCESS_FS_READ_DIR,
|
|
};
|
|
struct landlock_path_beneath_attr path_beneath = {
|
|
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
|
|
};
|
|
char *buf, field_buf[64];
|
|
int ruleset_fd, count;
|
|
|
|
ruleset_fd =
|
|
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
|
|
ASSERT_LE(0, ruleset_fd);
|
|
|
|
path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
|
|
ASSERT_LE(0, path_beneath.parent_fd);
|
|
|
|
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
|
&path_beneath, 0));
|
|
ASSERT_EQ(0, close(path_beneath.parent_fd));
|
|
ASSERT_EQ(0, close(ruleset_fd));
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
count = tracefs_count_matches(buf,
|
|
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
|
|
EXPECT_EQ(1, count)
|
|
{
|
|
TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s",
|
|
count, buf);
|
|
}
|
|
|
|
/* Ruleset ID should be non-zero. */
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
|
|
"ruleset", field_buf, sizeof(field_buf)));
|
|
EXPECT_STRNE("0", field_buf);
|
|
|
|
/* Access rights should be non-zero. */
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
|
|
"access_rights", field_buf, sizeof(field_buf)));
|
|
EXPECT_STRNE("", field_buf);
|
|
|
|
/* Path should be /usr. */
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
|
|
"path", field_buf, sizeof(field_buf)));
|
|
EXPECT_STREQ("/usr", field_buf);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that a path whose escaping exceeds the trace scratch sequence does
|
|
* not corrupt a sibling symbolic field.
|
|
*/
|
|
TEST_F(trace_fs, add_rule_path_beneath_escaped_path_overflow)
|
|
{
|
|
static const char access_prefix[] = "execute|write_file|read_file|";
|
|
static const char access_suffix[] = "|ioctl_dev|resolve_unix";
|
|
struct landlock_ruleset_attr ruleset_attr = {
|
|
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
|
|
};
|
|
struct landlock_path_beneath_attr path_beneath = {
|
|
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
|
|
};
|
|
char path[PATH_MAX];
|
|
char *buf, field_buf[256];
|
|
size_t field_len;
|
|
int ruleset_fd, count;
|
|
|
|
create_long_path(_metadata, path);
|
|
|
|
ruleset_fd =
|
|
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
|
|
ASSERT_LE(0, ruleset_fd);
|
|
path_beneath.parent_fd = open(path, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
|
ASSERT_LE(0, path_beneath.parent_fd);
|
|
|
|
ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
|
&path_beneath, 0));
|
|
ASSERT_EQ(0, close(path_beneath.parent_fd));
|
|
ASSERT_EQ(0, close(ruleset_fd));
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
count = tracefs_count_matches(buf,
|
|
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
|
|
EXPECT_EQ(1, count)
|
|
{
|
|
TH_LOG("Expected 1 add_rule_path_beneath event, got %d\n%s",
|
|
count, buf);
|
|
}
|
|
|
|
/*
|
|
* The marker catches a full revert with any compiler. The symbolic
|
|
* field also catches scratch-sequence poisoning when the compiler
|
|
* evaluates the overflowing path first, as GCC currently does.
|
|
*/
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK),
|
|
"access_rights", field_buf, sizeof(field_buf)));
|
|
EXPECT_EQ(0,
|
|
strncmp(field_buf, access_prefix, sizeof(access_prefix) - 1));
|
|
EXPECT_EQ(NULL, strstr(field_buf, "|refer|"));
|
|
field_len = strlen(field_buf);
|
|
ASSERT_LE(sizeof(access_suffix) - 1, field_len);
|
|
EXPECT_STREQ(access_suffix,
|
|
field_buf + field_len - (sizeof(access_suffix) - 1));
|
|
expect_truncated_path(_metadata, buf,
|
|
REGEX_ADD_RULE_PATH_BENEATH(TRACE_TASK));
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that an overflowing denied path does not corrupt its sibling
|
|
* symbolic blockers field.
|
|
*/
|
|
TEST_F(trace_fs, deny_access_fs_escaped_path_overflow)
|
|
{
|
|
char path[PATH_MAX];
|
|
char *buf, field_buf[64];
|
|
int count, err;
|
|
|
|
create_long_path(_metadata, path);
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
|
|
LANDLOCK_ACCESS_FS_READ_DIR, path);
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
|
EXPECT_EQ(1, count)
|
|
{
|
|
TH_LOG("Expected 1 deny_access_fs event, got %d\n%s", count,
|
|
buf);
|
|
}
|
|
|
|
/*
|
|
* The marker catches a full revert with any compiler. The symbolic
|
|
* field also catches scratch-sequence poisoning when the compiler
|
|
* evaluates the overflowing path first, as GCC currently does.
|
|
*/
|
|
err = tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
|
|
"blockers", field_buf, sizeof(field_buf));
|
|
ASSERT_EQ(0, err);
|
|
EXPECT_STREQ("read_dir", field_buf);
|
|
expect_truncated_path(_metadata, buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that an allowed access emits check_rule events (rule matched during
|
|
* pathwalk) but does NOT emit deny_access events (no denial).
|
|
*/
|
|
TEST_F(trace_fs, allowed_access)
|
|
{
|
|
char *buf, field_buf[64];
|
|
int count;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
/* Rule allows READ_DIR for /usr, access /usr which is allowed. */
|
|
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
|
|
LANDLOCK_ACCESS_FS_READ_DIR, "/usr");
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
|
EXPECT_LE(1, count);
|
|
|
|
/* Single-layer grants array, intersected with the request. */
|
|
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
|
"grants", field_buf,
|
|
sizeof(field_buf)));
|
|
EXPECT_STREQ("{read_dir}", field_buf);
|
|
|
|
count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
|
EXPECT_EQ(0, count);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that accessing a path whose access type is not in the handled set
|
|
* does not emit landlock_check_rule events. The ruleset handles READ_FILE, but
|
|
* the directory open checks READ_DIR which is unhandled; Landlock has no
|
|
* opinion and no rule evaluation occurs.
|
|
*/
|
|
TEST_F(trace_fs, check_rule_unhandled)
|
|
{
|
|
char *buf;
|
|
int count;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
/* Handles READ_FILE only; READ_DIR is unhandled. */
|
|
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE,
|
|
LANDLOCK_ACCESS_FS_READ_FILE, "/tmp");
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
/* No check_rule events because READ_DIR is not in the handled set. */
|
|
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
|
EXPECT_EQ(0, count);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that nested domains (child sandboxed under a parent domain) emit
|
|
* check_rule events from both layers and produce a deny_access event when the
|
|
* inner domain's rule does not cover the access.
|
|
*/
|
|
TEST_F(trace_fs, check_rule_nested)
|
|
{
|
|
char *buf, field_buf[64], *comma;
|
|
size_t first_len, second_len;
|
|
int count_rule, count_access, status;
|
|
pid_t pid;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
pid = fork();
|
|
ASSERT_LE(0, pid);
|
|
|
|
if (pid == 0) {
|
|
struct landlock_ruleset_attr ruleset_attr = {
|
|
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
|
|
};
|
|
struct landlock_path_beneath_attr path_beneath = {
|
|
.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
|
|
};
|
|
int ruleset_fd, fd;
|
|
|
|
/* First layer: allow /usr. */
|
|
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
|
|
sizeof(ruleset_attr), 0);
|
|
if (ruleset_fd < 0)
|
|
_exit(1);
|
|
|
|
path_beneath.parent_fd =
|
|
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
|
|
if (path_beneath.parent_fd < 0) {
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
|
|
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
|
&path_beneath, 0)) {
|
|
close(path_beneath.parent_fd);
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
close(path_beneath.parent_fd);
|
|
|
|
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);
|
|
|
|
/* Second layer: also allow /usr. */
|
|
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
|
|
sizeof(ruleset_attr), 0);
|
|
if (ruleset_fd < 0)
|
|
_exit(1);
|
|
|
|
path_beneath.parent_fd =
|
|
open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
|
|
if (path_beneath.parent_fd < 0) {
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
|
|
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
|
&path_beneath, 0)) {
|
|
close(path_beneath.parent_fd);
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
close(path_beneath.parent_fd);
|
|
|
|
if (landlock_restrict_self(ruleset_fd, 0)) {
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
close(ruleset_fd);
|
|
|
|
/* Access /usr which is allowed by both layers. */
|
|
fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0)
|
|
close(fd);
|
|
|
|
/* Access /tmp which has no rule in either layer. */
|
|
fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0)
|
|
close(fd);
|
|
|
|
_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);
|
|
|
|
count_rule =
|
|
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
|
EXPECT_LE(1, count_rule);
|
|
|
|
/*
|
|
* Both layers have the same rule, so the grants array must have two
|
|
* identical symbolic entries, e.g. {read_dir,read_dir}.
|
|
*/
|
|
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
|
"grants", field_buf,
|
|
sizeof(field_buf)));
|
|
comma = strchr(field_buf, ',');
|
|
EXPECT_NE(0, !!comma);
|
|
if (comma) {
|
|
/*
|
|
* Verify both entries are identical: compare the substring
|
|
* before the comma with the substring after it (stripping the
|
|
* braces).
|
|
*/
|
|
first_len = comma - field_buf - 1;
|
|
second_len = strlen(comma + 1) - 1;
|
|
EXPECT_EQ(first_len, second_len);
|
|
EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len));
|
|
}
|
|
|
|
count_access =
|
|
tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
|
EXPECT_LE(1, count_access);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that a denied FS access emits a landlock_deny_access_fs trace event
|
|
* with the blocked access and path.
|
|
*/
|
|
TEST_F(trace_fs, deny_access_fs_denied)
|
|
{
|
|
const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK);
|
|
char *buf, blockers[64];
|
|
int count;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
/*
|
|
* Rule allows READ_DIR for /usr, but access /tmp which has no rule.
|
|
* READ_DIR access to /tmp is denied by absence and should emit a
|
|
* deny_access_fs event.
|
|
*/
|
|
sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
|
|
LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
|
|
count = tracefs_count_matches(buf, event_regex);
|
|
EXPECT_EQ(1, count)
|
|
{
|
|
TH_LOG("Expected 1 access denial, got %d\n%s", count, buf);
|
|
}
|
|
ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers",
|
|
blockers, sizeof(blockers)));
|
|
EXPECT_STREQ("read_dir", blockers);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* Verifies that a denied mount reports the singleton topology blocker rather
|
|
* than an empty access mask.
|
|
*/
|
|
TEST_F(trace_fs, deny_change_topology)
|
|
{
|
|
const char *const event_regex = REGEX_DENY_ACCESS_FS(TRACE_TASK);
|
|
const struct landlock_ruleset_attr ruleset_attr = {
|
|
.handled_access_fs = LANDLOCK_ACCESS_FS_REFER,
|
|
};
|
|
char *buf, blockers[64];
|
|
int count, ruleset_fd, status;
|
|
pid_t pid;
|
|
|
|
ruleset_fd =
|
|
landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
|
|
ASSERT_LE(0, ruleset_fd);
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
/* Ensure that Landlock is the only expected mount denial. */
|
|
set_cap(_metadata, CAP_SYS_ADMIN);
|
|
pid = fork();
|
|
ASSERT_LE(0, pid);
|
|
if (pid == 0) {
|
|
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
if (landlock_restrict_self(ruleset_fd, 0)) {
|
|
close(ruleset_fd);
|
|
_exit(2);
|
|
}
|
|
close(ruleset_fd);
|
|
|
|
if (mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) != -1)
|
|
_exit(3);
|
|
|
|
if (errno != EPERM)
|
|
_exit(4);
|
|
|
|
_exit(0);
|
|
}
|
|
close(ruleset_fd);
|
|
clear_cap(_metadata, CAP_SYS_ADMIN);
|
|
|
|
ASSERT_EQ(pid, waitpid(pid, &status, 0));
|
|
ASSERT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
|
|
buf = tracefs_read_buf();
|
|
ASSERT_NE(NULL, buf);
|
|
count = tracefs_count_matches(buf, event_regex);
|
|
EXPECT_EQ(1, count)
|
|
{
|
|
TH_LOG("Expected 1 topology denial, got %d\n%s", count, buf);
|
|
}
|
|
ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "blockers",
|
|
blockers, sizeof(blockers)));
|
|
EXPECT_STREQ("change_topology", blockers);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/*
|
|
* A denied FS access covered by a quiet rule (LANDLOCK_ADD_RULE_QUIET with the
|
|
* access listed in quiet_access_fs) still emits a landlock_deny_access_fs
|
|
* event, but with logged=0, the same audit-logging verdict audit would apply to
|
|
* suppress the record.
|
|
*/
|
|
TEST_F(trace_fs, deny_access_fs_quiet)
|
|
{
|
|
char *buf, field[64];
|
|
pid_t pid;
|
|
int status;
|
|
|
|
ASSERT_EQ(0, tracefs_clear_buf());
|
|
|
|
pid = fork();
|
|
ASSERT_LE(0, pid);
|
|
if (pid == 0) {
|
|
struct landlock_ruleset_attr ruleset_attr = {
|
|
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
|
|
.quiet_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
|
|
};
|
|
struct landlock_path_beneath_attr path_beneath = {
|
|
.allowed_access = 0,
|
|
};
|
|
int ruleset_fd, fd;
|
|
|
|
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
|
|
sizeof(ruleset_attr), 0);
|
|
if (ruleset_fd < 0)
|
|
_exit(1);
|
|
|
|
/* Marks /tmp quiet without granting any access. */
|
|
path_beneath.parent_fd =
|
|
open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
|
|
if (path_beneath.parent_fd < 0) {
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
|
&path_beneath, LANDLOCK_ADD_RULE_QUIET)) {
|
|
close(path_beneath.parent_fd);
|
|
close(ruleset_fd);
|
|
_exit(1);
|
|
}
|
|
close(path_beneath.parent_fd);
|
|
|
|
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);
|
|
|
|
/* Denied READ_DIR on the quiet /tmp: suppressed, logged=0. */
|
|
fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd >= 0)
|
|
close(fd);
|
|
_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);
|
|
|
|
/* The event fires with the suppressed verdict. */
|
|
EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
|
|
TRACE_TASK, "0")));
|
|
/* The quiet rule must not leave the denial logged. */
|
|
EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS_LOGGED(
|
|
TRACE_TASK, "1")));
|
|
|
|
/*
|
|
* Quiet suppresses only the logged verdict: the rest of the denial
|
|
* event stays populated (non-zero domain, non-empty blockers).
|
|
*/
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
|
|
"domain", field, sizeof(field)));
|
|
EXPECT_STRNE("0", field);
|
|
ASSERT_EQ(0, tracefs_extract_field(
|
|
buf, REGEX_DENY_ACCESS_FS_LOGGED(TRACE_TASK, "0"),
|
|
"blockers", field, sizeof(field)));
|
|
EXPECT_STRNE("", field);
|
|
|
|
free(buf);
|
|
}
|
|
|
|
TEST_HARNESS_MAIN
|