[PATCH v4 29/30] selftests/landlock: Test audit rule with AUDIT_EXE_LANDLOCK_DOM
From: Mickaël Salaün <mic@digikod.net>
Date: 2025-01-08 15:44:32
Also in:
lkml
Subsystem:
kernel selftest framework, landlock security module, the rest · Maintainers:
Shuah Khan, Mickaël Salaün, Linus Torvalds
Add audit_rule.exe_landlock_domain tests to filter Landlock denials according to the binary that created the sandbox. The wait-pipe.c test program is updated to sandbox itself and send a (denied) signal to its parent. Cc: Günther Noack <gnoack@google.com> Cc: Paul Moore <paul@paul-moore.com> Signed-off-by: Mickaël Salaün <mic@digikod.net> Link: https://lore.kernel.org/r/20250108154338.1129069-30-mic@digikod.net (local) --- Changes since v3: - New patch. --- tools/testing/selftests/landlock/audit.h | 1 + tools/testing/selftests/landlock/audit_test.c | 153 ++++++++++++++++++ tools/testing/selftests/landlock/wait-pipe.c | 30 +++- 3 files changed, 183 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index 37979a62478c..fba96123776c 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h@@ -140,6 +140,7 @@ static int audit_filter_exe(const int audit_fd, switch (filter->record_type) { case AUDIT_EXE: + case AUDIT_EXE_LANDLOCK_DENY: break; default: return -EINVAL;
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index d5330e843395..921f316ddbf8 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c@@ -7,7 +7,9 @@ #define _GNU_SOURCE #include <errno.h> +#include <limits.h> #include <linux/landlock.h> +#include <stdlib.h> #include <sys/mount.h> #include <sys/prctl.h> #include <sys/types.h>
@@ -33,6 +35,22 @@ static int matches_log_umount(struct __test_metadata *const _metadata, REGEX_LANDLOCK_PREFIX " blockers=.*"); } +static int matches_log_signal(struct __test_metadata *const _metadata, + int audit_fd, const pid_t opid) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=scope.signal opid=%d ocomm=\"audit_test\"$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = + snprintf(log_match, sizeof(log_match), log_template, opid); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_DENY, log_match); +} + FIXTURE(audit) { struct audit_filter audit_filter;
@@ -155,4 +173,139 @@ TEST_F(audit, fs_deny) _metadata->exit_code = KSFT_FAIL; } +FIXTURE(audit_rule) +{ + struct audit_filter audit_filter_main, audit_filter_test; + int audit_fd; +}; + +FIXTURE_VARIANT(audit_rule) +{ + const bool with_exe_landlock_deny_child; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_rule, exe_landlock_deny_child) { + /* clang-format on */ + .with_exe_landlock_deny_child = true, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_rule, exe_landlock_deny_parent) { + /* clang-format on */ + .with_exe_landlock_deny_child = false, +}; + +FIXTURE_SETUP(audit_rule) +{ + const char *path = NULL; + + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + + if (variant->with_exe_landlock_deny_child) + /* Filter on the sandboxer instead of the current exe. */ + path = bin_wait_pipe; + + self->audit_fd = audit_init(); + EXPECT_LE(0, self->audit_fd) + { + const char *error_msg; + + /* kill "$(auditctl -s | sed -ne 's/^pid \([0-9]\+\)$/\1/p')" */ + if (self->audit_fd == -EEXIST) + error_msg = "socket already in use (e.g. auditd)"; + else + error_msg = strerror(-self->audit_fd); + TH_LOG("Failed to initialize audit: %s", error_msg); + } + + /* Applies main filter for the test task. */ + EXPECT_EQ(0, audit_init_filter_exe(AUDIT_EXE, &self->audit_filter_main, + bin_wait_pipe)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter_main, + AUDIT_ADD_RULE, AUDIT_FILTER_EXCLUDE)); + + /* Applies test filter for the test task or the current task. */ + EXPECT_EQ(0, audit_init_filter_exe(AUDIT_EXE_LANDLOCK_DENY, + &self->audit_filter_test, path)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter_test, + AUDIT_ADD_RULE, AUDIT_FILTER_EXCLUDE)); + + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN(audit_rule) +{ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter_main, + AUDIT_DEL_RULE, AUDIT_FILTER_EXCLUDE)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter_test, + AUDIT_DEL_RULE, AUDIT_FILTER_EXCLUDE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, close(self->audit_fd)); +} + +TEST_F(audit_rule, exe_landlock_deny) +{ + struct audit_records records; + int pipe_child[2], pipe_parent[2]; + char buf_parent; + pid_t child; + int status; + + ASSERT_EQ(0, pipe2(pipe_child, 0)); + ASSERT_EQ(0, pipe2(pipe_parent, 0)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char pipe_child_str[12], pipe_parent_str[12]; + char *const argv[] = { (char *)bin_wait_pipe, pipe_child_str, + pipe_parent_str, NULL }; + + /* Passes the pipe FDs to the executed binary. */ + EXPECT_EQ(0, close(pipe_child[0])); + EXPECT_EQ(0, close(pipe_parent[1])); + snprintf(pipe_child_str, sizeof(pipe_child_str), "%d", + pipe_child[1]); + snprintf(pipe_parent_str, sizeof(pipe_parent_str), "%d", + pipe_parent[0]); + + ASSERT_EQ(0, execve(argv[0], argv, NULL)) + { + TH_LOG("Failed to execute \"%s\": %s", argv[0], + strerror(errno)); + }; + _exit(1); + return; + } + + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + + /* Waits for the child. */ + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that there was no denial until now. */ + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.deny); + + /* Signals the child to terminate. */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* Tests that the audit record only matches the child. */ + if (variant->with_exe_landlock_deny_child) { + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid())); + } else { + audit_count_records(self->audit_fd, &records); + EXPECT_EQ(0, records.deny); + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); +} + TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/wait-pipe.c b/tools/testing/selftests/landlock/wait-pipe.c
index 0dbcd260a0fa..153f8ca93ac6 100644
--- a/tools/testing/selftests/landlock/wait-pipe.c
+++ b/tools/testing/selftests/landlock/wait-pipe.c@@ -2,20 +2,31 @@ /* * Write in a pipe and wait. * - * Used by layout1.umount_sandboxer from fs_test.c + * Used by layout1.umount_sandboxer from fs_test.c and + * audit_rule.exe_landlock_deny from audit_test.c * * Copyright © 2024-2025 Microsoft Corporation */ #define _GNU_SOURCE +#include <linux/landlock.h> +#include <linux/prctl.h> +#include <signal.h> #include <stdio.h> #include <stdlib.h> +#include <sys/prctl.h> #include <unistd.h> +#include "wrappers.h" + int main(int argc, char *argv[]) { + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; int pipe_child, pipe_parent; char buf; + int ruleset_fd; /* The first argument must be the file descriptor number of a pipe. */ if (argc != 3) {
@@ -26,6 +37,20 @@ int main(int argc, char *argv[]) pipe_child = atoi(argv[1]); pipe_parent = atoi(argv[2]); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + return 1; + } + + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + close(ruleset_fd); + /* Signals that we are waiting. */ if (write(pipe_child, ".", 1) != 1) { perror("Failed to write to first argument");
@@ -38,5 +63,8 @@ int main(int argc, char *argv[]) return 1; } + /* Tries to send a signal. */ + kill(getppid(), 0); + return 0; }
--
2.47.1