mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-09-18 23:09:29 +02:00
selftests/landlock: Add filesystem tracepoint tests
Add filesystem-specific trace tests in a dedicated file, following the audit-test pattern of living alongside each subsystem's functional tests. trace_fs_test.c verifies that the add_rule_fs, check_rule_fs, and deny_access_fs events fire with the correct fields on matching rules and denied accesses, that check_rule_fs does not fire for unhandled access types, and that no event fires without a sandbox. A denial covered by a quiet rule still emits a deny_access_fs event but with logged=0, the same suppression verdict audit applies; because that verdict must not depend on CONFIG_AUDIT, the test also runs under a tracepoints-only build. Add trace_layout1 fixture tests in fs_test.c that reuse the layout1 hierarchy to verify the per-layer grants field: field values, multi-rule pathwalk short-circuit, request intersection, the optional truncate right surfacing in the request and grants, and an empty grants set from a rule that grants none of the requested rights. Cc: Günther Noack <gnoack@google.com> Cc: Tingmao Wang <m@maowtm.org> Link: https://patch.msgid.link/20260811094338.288094-16-mic@digikod.net Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
@@ -44,6 +44,9 @@
|
||||
|
||||
#include "audit.h"
|
||||
#include "common.h"
|
||||
#include "trace.h"
|
||||
|
||||
#define TRACE_TASK "fs_test"
|
||||
|
||||
#ifndef renameat2
|
||||
int renameat2(int olddirfd, const char *oldpath, int newdirfd,
|
||||
@@ -10436,4 +10439,484 @@ TEST_F(audit_quiet_rename, quiet_behind_mountpoint_disconnected)
|
||||
ASSERT_EQ(0, records.access);
|
||||
}
|
||||
|
||||
/* clang-format off */
|
||||
FIXTURE(trace_layout1) {
|
||||
/* clang-format on */
|
||||
int tracefs_ok;
|
||||
};
|
||||
|
||||
FIXTURE_SETUP(trace_layout1)
|
||||
{
|
||||
struct stat st;
|
||||
|
||||
/*
|
||||
* Check tracefs availability before creating the layout, following the
|
||||
* layout3_fs pattern: skip before any layout creation to avoid leaving
|
||||
* stale TMP_DIR on skip.
|
||||
*/
|
||||
if (stat(TRACEFS_LANDLOCK_DIR, &st)) {
|
||||
self->tracefs_ok = 0;
|
||||
SKIP(return, "tracefs not available");
|
||||
}
|
||||
self->tracefs_ok = 1;
|
||||
|
||||
/* Isolate tracefs state (PID filter, event enables). */
|
||||
set_cap(_metadata, CAP_SYS_ADMIN);
|
||||
ASSERT_EQ(0, unshare(CLONE_NEWNS));
|
||||
ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
|
||||
clear_cap(_metadata, CAP_SYS_ADMIN);
|
||||
|
||||
prepare_layout(_metadata);
|
||||
create_layout1(_metadata);
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_fixture_setup());
|
||||
ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
}
|
||||
|
||||
FIXTURE_TEARDOWN_PARENT(trace_layout1)
|
||||
{
|
||||
if (!self->tracefs_ok)
|
||||
return;
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
|
||||
tracefs_clear_pid_filter();
|
||||
tracefs_fixture_teardown();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
remove_layout1(_metadata);
|
||||
cleanup_layout(_metadata);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that check_rule_fs events include correct field values: domain, dev,
|
||||
* ino, access_request, and grants. All values are verified against stat() of
|
||||
* the rule path on a deterministic tmpfs layout.
|
||||
*/
|
||||
TEST_F(trace_layout1, check_rule_fs_fields)
|
||||
{
|
||||
struct stat dir_stat;
|
||||
char expected_dev[32];
|
||||
char expected_ino[32];
|
||||
char *buf;
|
||||
char field[64];
|
||||
|
||||
if (!self->tracefs_ok)
|
||||
SKIP(return, "tracefs not available");
|
||||
|
||||
ASSERT_EQ(0, stat(dir_s1d1, &dir_stat));
|
||||
snprintf(expected_dev, sizeof(expected_dev), "%u:%u",
|
||||
major(dir_stat.st_dev), minor(dir_stat.st_dev));
|
||||
snprintf(expected_ino, sizeof(expected_ino), "%lu", dir_stat.st_ino);
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
sandbox_child_fs_access(_metadata, dir_s1d1,
|
||||
LANDLOCK_ACCESS_FS_READ_DIR,
|
||||
LANDLOCK_ACCESS_FS_READ_DIR, dir_s1d1);
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
buf = tracefs_read_trace();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_NE(NULL, buf);
|
||||
|
||||
EXPECT_EQ(1,
|
||||
tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)))
|
||||
{
|
||||
TH_LOG("Expected 1 check_rule_fs event\n%s", buf);
|
||||
}
|
||||
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"dev", field, sizeof(field)));
|
||||
EXPECT_STREQ(expected_dev, field)
|
||||
{
|
||||
TH_LOG("Expected dev=%s, got %s", expected_dev, field);
|
||||
}
|
||||
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"ino", field, sizeof(field)));
|
||||
EXPECT_STREQ(expected_ino, field)
|
||||
{
|
||||
TH_LOG("Expected ino=%s, got %s", expected_ino, field);
|
||||
}
|
||||
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"access_request", field,
|
||||
sizeof(field)));
|
||||
EXPECT_STREQ("read_dir", field)
|
||||
{
|
||||
TH_LOG("Expected access_request=read_dir, got %s", field);
|
||||
}
|
||||
|
||||
/*
|
||||
* The domain handles only READ_DIR, so the rule carries the
|
||||
* unhandled-rights padding; intersecting with the request leaves just
|
||||
* the requested read_dir (no padding, no hex).
|
||||
*/
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"grants", field, sizeof(field)));
|
||||
EXPECT_STREQ("{read_dir}", field)
|
||||
{
|
||||
TH_LOG("Expected grants={read_dir}, got %s", field);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies check_rule_fs behavior with multiple rules. With rules at s1d1 and
|
||||
* s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the
|
||||
* pathwalk short-circuits after the first rule fully unmasks the single layer.
|
||||
*/
|
||||
TEST_F(trace_layout1, check_rule_fs_multiple_rules)
|
||||
{
|
||||
pid_t pid;
|
||||
int status;
|
||||
char *buf;
|
||||
int count;
|
||||
|
||||
if (!self->tracefs_ok)
|
||||
SKIP(return, "tracefs not available");
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
pid = fork();
|
||||
ASSERT_LE(0, pid);
|
||||
|
||||
if (pid == 0) {
|
||||
struct landlock_ruleset_attr 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;
|
||||
|
||||
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
|
||||
if (ruleset_fd < 0)
|
||||
_exit(1);
|
||||
|
||||
path_beneath.parent_fd =
|
||||
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0)
|
||||
_exit(1);
|
||||
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
&path_beneath, 0))
|
||||
_exit(1);
|
||||
close(path_beneath.parent_fd);
|
||||
|
||||
path_beneath.parent_fd =
|
||||
open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0)
|
||||
_exit(1);
|
||||
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
&path_beneath, 0))
|
||||
_exit(1);
|
||||
close(path_beneath.parent_fd);
|
||||
|
||||
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
|
||||
if (landlock_restrict_self(ruleset_fd, 0))
|
||||
_exit(1);
|
||||
close(ruleset_fd);
|
||||
|
||||
fd = open(dir_s1d2, 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));
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
buf = tracefs_read_trace();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_NE(NULL, buf);
|
||||
|
||||
/*
|
||||
* Only 1 check_rule_fs event: the rule on dir_s1d2 fully unmasked the
|
||||
* single layer, so the pathwalk short-circuits before reaching the
|
||||
* dir_s1d1 rule.
|
||||
*/
|
||||
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
||||
EXPECT_EQ(1, count)
|
||||
{
|
||||
TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count,
|
||||
buf);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies the grants array is intersected with the request: a handled,
|
||||
* granted, but unrequested right (execute) is filtered out, leaving only the
|
||||
* requested read_dir.
|
||||
*/
|
||||
TEST_F(trace_layout1, check_rule_fs_request_subset)
|
||||
{
|
||||
char *buf;
|
||||
char field[64];
|
||||
|
||||
if (!self->tracefs_ok)
|
||||
SKIP(return, "tracefs not available");
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
/*
|
||||
* Handle and grant READ_DIR|EXECUTE; the open only requests read_dir.
|
||||
*/
|
||||
sandbox_child_fs_access(
|
||||
_metadata, dir_s1d1,
|
||||
LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
|
||||
LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE,
|
||||
dir_s1d1);
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
buf = tracefs_read_trace();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_NE(NULL, buf);
|
||||
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"access_request", field,
|
||||
sizeof(field)));
|
||||
EXPECT_STREQ("read_dir", field);
|
||||
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"grants", field, sizeof(field)));
|
||||
EXPECT_STREQ("{read_dir}", field);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that the optional TRUNCATE access right, which hook_file_open()
|
||||
* speculatively evaluates on every open, appears in the access_request= and
|
||||
* grants= fields. Opening file1_s1d1 read-only needs only read_file, but the
|
||||
* open hook also evaluates truncate; the domain handles and the rule grants
|
||||
* both, so the event reports access_request=read_file|truncate and
|
||||
* grants={read_file|truncate}, and the open is allowed.
|
||||
*/
|
||||
TEST_F(trace_layout1, check_rule_fs_optional_access)
|
||||
{
|
||||
pid_t pid;
|
||||
int status;
|
||||
char *buf;
|
||||
char field[64];
|
||||
int count;
|
||||
|
||||
if (!self->tracefs_ok)
|
||||
SKIP(return, "tracefs not available");
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
pid = fork();
|
||||
ASSERT_LE(0, pid);
|
||||
|
||||
if (pid == 0) {
|
||||
struct landlock_ruleset_attr attr = {
|
||||
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
LANDLOCK_ACCESS_FS_TRUNCATE,
|
||||
};
|
||||
struct landlock_path_beneath_attr path_beneath = {
|
||||
.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
LANDLOCK_ACCESS_FS_TRUNCATE,
|
||||
};
|
||||
int ruleset_fd, fd;
|
||||
|
||||
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
|
||||
if (ruleset_fd < 0)
|
||||
_exit(1);
|
||||
|
||||
path_beneath.parent_fd =
|
||||
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0)
|
||||
_exit(1);
|
||||
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
&path_beneath, 0))
|
||||
_exit(1);
|
||||
close(path_beneath.parent_fd);
|
||||
|
||||
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
|
||||
if (landlock_restrict_self(ruleset_fd, 0))
|
||||
_exit(1);
|
||||
close(ruleset_fd);
|
||||
|
||||
/* Read-only open needs only read_file; truncate is optional. */
|
||||
fd = open(file1_s1d1, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
_exit(1);
|
||||
close(fd);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
ASSERT_EQ(pid, waitpid(pid, &status, 0));
|
||||
ASSERT_TRUE(WIFEXITED(status));
|
||||
/* The open is allowed: the required read_file is granted. */
|
||||
EXPECT_EQ(0, WEXITSTATUS(status));
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
buf = tracefs_read_trace();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_NE(NULL, buf);
|
||||
|
||||
/* The rule at dir_s1d1 matches when opening file1_s1d1. */
|
||||
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
||||
EXPECT_EQ(1, count)
|
||||
{
|
||||
TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count,
|
||||
buf);
|
||||
}
|
||||
|
||||
/* The open hook adds the optional truncate to the request. */
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"access_request", field,
|
||||
sizeof(field)));
|
||||
EXPECT_STREQ("read_file|truncate", field);
|
||||
|
||||
/* The rule grants both, so truncate appears in the grants array. */
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
|
||||
"grants", field, sizeof(field)));
|
||||
EXPECT_STREQ("{read_file|truncate}", field);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that check_rule_fs fires for a rule that matches the inode even when
|
||||
* it grants none of the requested rights, so the grants set is empty. Landlock
|
||||
* cannot know a rule ignores the request before reading it, so the event is
|
||||
* still emitted (grants={}), which lets a tracer see that the rule matched.
|
||||
* The domain handles READ_DIR|EXECUTE, dir_s1d2 grants only EXECUTE and its
|
||||
* parent dir_s1d1 grants only READ_DIR. Reading dir_s1d2 (requesting read_dir)
|
||||
* first matches the dir_s1d2 rule, which grants nothing requested (grants={});
|
||||
* walking up to dir_s1d1 then grants read_dir (grants={read_dir}) and allows
|
||||
* the access.
|
||||
*/
|
||||
TEST_F(trace_layout1, check_rule_fs_empty_grant)
|
||||
{
|
||||
pid_t pid;
|
||||
int status;
|
||||
char *buf;
|
||||
int count;
|
||||
|
||||
if (!self->tracefs_ok)
|
||||
SKIP(return, "tracefs not available");
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_EQ(0, tracefs_clear());
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
|
||||
pid = fork();
|
||||
ASSERT_LE(0, pid);
|
||||
|
||||
if (pid == 0) {
|
||||
struct landlock_ruleset_attr attr = {
|
||||
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
LANDLOCK_ACCESS_FS_EXECUTE,
|
||||
};
|
||||
struct landlock_path_beneath_attr path_beneath = {};
|
||||
int ruleset_fd, fd;
|
||||
|
||||
ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
|
||||
if (ruleset_fd < 0)
|
||||
_exit(1);
|
||||
|
||||
/* Parent dir_s1d1 grants only READ_DIR. */
|
||||
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR;
|
||||
path_beneath.parent_fd =
|
||||
open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0)
|
||||
_exit(1);
|
||||
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
&path_beneath, 0))
|
||||
_exit(1);
|
||||
close(path_beneath.parent_fd);
|
||||
|
||||
/* Child dir_s1d2 grants only EXECUTE. */
|
||||
path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
|
||||
path_beneath.parent_fd =
|
||||
open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC);
|
||||
if (path_beneath.parent_fd < 0)
|
||||
_exit(1);
|
||||
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
|
||||
&path_beneath, 0))
|
||||
_exit(1);
|
||||
close(path_beneath.parent_fd);
|
||||
|
||||
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
|
||||
if (landlock_restrict_self(ruleset_fd, 0))
|
||||
_exit(1);
|
||||
close(ruleset_fd);
|
||||
|
||||
fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd < 0)
|
||||
_exit(1);
|
||||
close(fd);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
ASSERT_EQ(pid, waitpid(pid, &status, 0));
|
||||
ASSERT_TRUE(WIFEXITED(status));
|
||||
EXPECT_EQ(0, WEXITSTATUS(status));
|
||||
|
||||
set_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
buf = tracefs_read_trace();
|
||||
clear_cap(_metadata, CAP_DAC_OVERRIDE);
|
||||
ASSERT_NE(NULL, buf);
|
||||
|
||||
/*
|
||||
* dir_s1d2 (grants nothing requested) then dir_s1d1 (grants read_dir).
|
||||
*/
|
||||
count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
|
||||
EXPECT_EQ(2, count)
|
||||
{
|
||||
TH_LOG("Expected 2 check_rule_fs events, got %d\n%s", count,
|
||||
buf);
|
||||
}
|
||||
|
||||
/* The dir_s1d2 rule matches the inode but grants none of read_dir. */
|
||||
EXPECT_EQ(
|
||||
1,
|
||||
tracefs_count_matches(
|
||||
buf,
|
||||
TRACE_PREFIX(
|
||||
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
|
||||
"access_request=read_dir "
|
||||
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
|
||||
"grants={}$"))
|
||||
{
|
||||
TH_LOG("Expected a grants={} event\n%s", buf);
|
||||
}
|
||||
|
||||
/* Walking up to dir_s1d1 grants the requested read_dir. */
|
||||
EXPECT_EQ(
|
||||
1,
|
||||
tracefs_count_matches(
|
||||
buf,
|
||||
TRACE_PREFIX(
|
||||
TRACE_TASK) "landlock_check_rule_fs: domain=[0-9a-f]\\+ "
|
||||
"access_request=read_dir "
|
||||
"dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ "
|
||||
"grants={read_dir}$"))
|
||||
{
|
||||
TH_LOG("Expected a grants={read_dir} event\n%s", buf);
|
||||
}
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
TEST_HARNESS_MAIN
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Landlock tests - Filesystem tracepoints
|
||||
*
|
||||
* Copyright © 2026 Cloudflare, Inc.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.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"
|
||||
|
||||
/*
|
||||
* 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_FS_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_FS_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_fs trace
|
||||
* event with the expected path and field values: ruleset ID is non-zero,
|
||||
* access_rights is non-zero, and path matches.
|
||||
*/
|
||||
TEST_F(trace_fs, add_rule_fs)
|
||||
{
|
||||
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_FS(TRACE_TASK));
|
||||
EXPECT_EQ(1, count)
|
||||
{
|
||||
TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
|
||||
}
|
||||
|
||||
/* Ruleset ID should be non-zero. */
|
||||
ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(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_FS(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_FS(TRACE_TASK),
|
||||
"path", field_buf, sizeof(field_buf)));
|
||||
EXPECT_STREQ("/usr", field_buf);
|
||||
|
||||
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)
|
||||
{
|
||||
char *buf;
|
||||
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, REGEX_DENY_ACCESS_FS(TRACE_TASK));
|
||||
EXPECT_LE(1, count);
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user