diff options
Diffstat (limited to 'tools/testing/selftests/landlock')
-rw-r--r-- | tools/testing/selftests/landlock/.gitignore | 1 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/Makefile | 6 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/audit.h | 479 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/audit_test.c | 671 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/base_test.c | 130 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/common.h | 20 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/config | 1 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/fs_test.c | 595 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/net_test.c | 132 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/ptrace_test.c | 140 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/scoped_abstract_unix_test.c | 111 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/scoped_signal_test.c | 108 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/wait-pipe-sandbox.c | 131 |
13 files changed, 2507 insertions, 18 deletions
diff --git a/tools/testing/selftests/landlock/.gitignore b/tools/testing/selftests/landlock/.gitignore index 335b2b1a3463..a820329cae0d 100644 --- a/tools/testing/selftests/landlock/.gitignore +++ b/tools/testing/selftests/landlock/.gitignore @@ -2,3 +2,4 @@ /sandbox-and-launch /true /wait-pipe +/wait-pipe-sandbox diff --git a/tools/testing/selftests/landlock/Makefile b/tools/testing/selftests/landlock/Makefile index 5cb0828f0514..a3f449914bf9 100644 --- a/tools/testing/selftests/landlock/Makefile +++ b/tools/testing/selftests/landlock/Makefile @@ -10,7 +10,11 @@ src_test := $(wildcard *_test.c) TEST_GEN_PROGS := $(src_test:.c=) -TEST_GEN_PROGS_EXTENDED := true sandbox-and-launch wait-pipe +TEST_GEN_PROGS_EXTENDED := \ + true \ + sandbox-and-launch \ + wait-pipe \ + wait-pipe-sandbox # Short targets: $(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h new file mode 100644 index 000000000000..18a6014920b5 --- /dev/null +++ b/tools/testing/selftests/landlock/audit.h @@ -0,0 +1,479 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock audit helpers + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <linux/audit.h> +#include <linux/limits.h> +#include <linux/netlink.h> +#include <regex.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <unistd.h> + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#endif + +#ifndef __maybe_unused +#define __maybe_unused __attribute__((__unused__)) +#endif + +#define REGEX_LANDLOCK_PREFIX "^audit([0-9.:]\\+): domain=\\([0-9a-f]\\+\\)" + +struct audit_filter { + __u32 record_type; + size_t exe_len; + char exe[PATH_MAX]; +}; + +struct audit_message { + struct nlmsghdr header; + union { + struct audit_status status; + struct audit_features features; + struct audit_rule_data rule; + struct nlmsgerr err; + char data[PATH_MAX + 200]; + }; +}; + +static const struct timeval audit_tv_dom_drop = { + /* + * Because domain deallocation is tied to asynchronous credential + * freeing, receiving such event may take some time. In practice, + * on a small VM, it should not exceed 100k usec, but let's wait up + * to 1 second to be safe. + */ + .tv_sec = 1, +}; + +static const struct timeval audit_tv_default = { + .tv_usec = 1, +}; + +static int audit_send(const int fd, const struct audit_message *const msg) +{ + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + int ret; + + do { + ret = sendto(fd, msg, msg->header.nlmsg_len, 0, + (struct sockaddr *)&addr, sizeof(addr)); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) + return -errno; + + if (ret != msg->header.nlmsg_len) + return -E2BIG; + + return 0; +} + +static int audit_recv(const int fd, struct audit_message *msg) +{ + struct sockaddr_nl addr; + socklen_t addrlen = sizeof(addr); + struct audit_message msg_tmp; + int err; + + if (!msg) + msg = &msg_tmp; + + do { + err = recvfrom(fd, msg, sizeof(*msg), 0, + (struct sockaddr *)&addr, &addrlen); + } while (err < 0 && errno == EINTR); + + if (err < 0) + return -errno; + + if (addrlen != sizeof(addr) || addr.nl_pid != 0) + return -EINVAL; + + /* Checks Netlink error or end of messages. */ + if (msg->header.nlmsg_type == NLMSG_ERROR) + return msg->err.error; + + return 0; +} + +static int audit_request(const int fd, + const struct audit_message *const request, + struct audit_message *reply) +{ + struct audit_message msg_tmp; + bool first_reply = true; + int err; + + err = audit_send(fd, request); + if (err) + return err; + + if (!reply) + reply = &msg_tmp; + + do { + if (first_reply) + first_reply = false; + else + reply = &msg_tmp; + + err = audit_recv(fd, reply); + if (err) + return err; + } while (reply->header.nlmsg_type != NLMSG_ERROR && + reply->err.msg.nlmsg_type != request->header.nlmsg_type); + + return reply->err.error; +} + +static int audit_filter_exe(const int audit_fd, + const struct audit_filter *const filter, + const __u16 type) +{ + struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)) + + NLMSG_ALIGN(filter->exe_len), + .nlmsg_type = type, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .rule = { + .flags = AUDIT_FILTER_EXCLUDE, + .action = AUDIT_NEVER, + .field_count = 1, + .fields[0] = filter->record_type, + .fieldflags[0] = AUDIT_NOT_EQUAL, + .values[0] = filter->exe_len, + .buflen = filter->exe_len, + } + }; + + if (filter->record_type != AUDIT_EXE) + return -EINVAL; + + memcpy(msg.rule.buf, filter->exe, filter->exe_len); + return audit_request(audit_fd, &msg, NULL); +} + +static int audit_filter_drop(const int audit_fd, const __u16 type) +{ + struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.rule)), + .nlmsg_type = type, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .rule = { + .flags = AUDIT_FILTER_EXCLUDE, + .action = AUDIT_NEVER, + .field_count = 1, + .fields[0] = AUDIT_MSGTYPE, + .fieldflags[0] = AUDIT_NOT_EQUAL, + .values[0] = AUDIT_LANDLOCK_DOMAIN, + } + }; + + return audit_request(audit_fd, &msg, NULL); +} + +static int audit_set_status(int fd, __u32 key, __u32 val) +{ + const struct audit_message msg = { + .header = { + .nlmsg_len = NLMSG_SPACE(sizeof(msg.status)), + .nlmsg_type = AUDIT_SET, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + }, + .status = { + .mask = key, + .enabled = key == AUDIT_STATUS_ENABLED ? val : 0, + .pid = key == AUDIT_STATUS_PID ? val : 0, + } + }; + + return audit_request(fd, &msg, NULL); +} + +/* Returns a pointer to the last filled character of @dst, which is `\0`. */ +static __maybe_unused char *regex_escape(const char *const src, char *dst, + size_t dst_size) +{ + char *d = dst; + + for (const char *s = src; *s; s++) { + switch (*s) { + case '$': + case '*': + case '.': + case '[': + case '\\': + case ']': + case '^': + if (d >= dst + dst_size - 2) + return (char *)-ENOMEM; + + *d++ = '\\'; + *d++ = *s; + break; + default: + if (d >= dst + dst_size - 1) + return (char *)-ENOMEM; + + *d++ = *s; + } + } + if (d >= dst + dst_size - 1) + return (char *)-ENOMEM; + + *d = '\0'; + return d; +} + +/* + * @domain_id: The domain ID extracted from the audit message (if the first part + * of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is + * not found. + */ +static int audit_match_record(int audit_fd, const __u16 type, + const char *const pattern, __u64 *domain_id) +{ + struct audit_message msg; + int ret, err = 0; + bool matches_record = !type; + regmatch_t matches[2]; + regex_t regex; + + ret = regcomp(®ex, pattern, 0); + if (ret) + return -EINVAL; + + do { + memset(&msg, 0, sizeof(msg)); + err = audit_recv(audit_fd, &msg); + if (err) + goto out; + + if (msg.header.nlmsg_type == type) + matches_record = true; + } while (!matches_record); + + ret = regexec(®ex, msg.data, ARRAY_SIZE(matches), matches, 0); + if (ret) { + printf("DATA: %s\n", msg.data); + printf("ERROR: no match for pattern: %s\n", pattern); + err = -ENOENT; + } + + if (domain_id) { + *domain_id = 0; + if (matches[1].rm_so != -1) { + int match_len = matches[1].rm_eo - matches[1].rm_so; + /* The maximal characters of a 2^64 hexadecimal number is 17. */ + char dom_id[18]; + + if (match_len > 0 && match_len < sizeof(dom_id)) { + memcpy(dom_id, msg.data + matches[1].rm_so, + match_len); + dom_id[match_len] = '\0'; + if (domain_id) + *domain_id = strtoull(dom_id, NULL, 16); + } + } + } + +out: + regfree(®ex); + return err; +} + +static int __maybe_unused matches_log_domain_allocated(int audit_fd, pid_t pid, + __u64 *domain_id) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " status=allocated mode=enforcing pid=%d uid=[0-9]\\+" + " exe=\"[^\"]\\+\" comm=\".*_test\"$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = + snprintf(log_match, sizeof(log_match), log_template, pid); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, + domain_id); +} + +static int __maybe_unused matches_log_domain_deallocated( + int audit_fd, unsigned int num_denials, __u64 *domain_id) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " status=deallocated denials=%u$"; + char log_match[sizeof(log_template) + 10]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template, + num_denials); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, + domain_id); +} + +struct audit_records { + size_t access; + size_t domain; +}; + +static int audit_count_records(int audit_fd, struct audit_records *records) +{ + struct audit_message msg; + int err; + + records->access = 0; + records->domain = 0; + + do { + memset(&msg, 0, sizeof(msg)); + err = audit_recv(audit_fd, &msg); + if (err) { + if (err == -EAGAIN) + return 0; + else + return err; + } + + switch (msg.header.nlmsg_type) { + case AUDIT_LANDLOCK_ACCESS: + records->access++; + break; + case AUDIT_LANDLOCK_DOMAIN: + records->domain++; + break; + } + } while (true); + + return 0; +} + +static int audit_init(void) +{ + int fd, err; + + fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_AUDIT); + if (fd < 0) + return -errno; + + err = audit_set_status(fd, AUDIT_STATUS_ENABLED, 1); + if (err) + return err; + + err = audit_set_status(fd, AUDIT_STATUS_PID, getpid()); + if (err) + return err; + + /* Sets a timeout for negative tests. */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)); + if (err) + return -errno; + + return fd; +} + +static int audit_init_filter_exe(struct audit_filter *filter, const char *path) +{ + char *absolute_path = NULL; + + /* It is assume that there is not already filtering rules. */ + filter->record_type = AUDIT_EXE; + if (!path) { + filter->exe_len = readlink("/proc/self/exe", filter->exe, + sizeof(filter->exe) - 1); + if (filter->exe_len < 0) + return -errno; + + return 0; + } + + absolute_path = realpath(path, NULL); + if (!absolute_path) + return -errno; + + /* No need for the terminating NULL byte. */ + filter->exe_len = strlen(absolute_path); + if (filter->exe_len > sizeof(filter->exe)) + return -E2BIG; + + memcpy(filter->exe, absolute_path, filter->exe_len); + free(absolute_path); + return 0; +} + +static int audit_cleanup(int audit_fd, struct audit_filter *filter) +{ + struct audit_filter new_filter; + + if (audit_fd < 0 || !filter) { + int err; + + /* + * Simulates audit_init_with_exe_filter() when called from + * FIXTURE_TEARDOWN_PARENT(). + */ + audit_fd = audit_init(); + if (audit_fd < 0) + return audit_fd; + + filter = &new_filter; + err = audit_init_filter_exe(filter, NULL); + if (err) + return err; + } + + /* Filters might not be in place. */ + audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE); + audit_filter_drop(audit_fd, AUDIT_DEL_RULE); + + /* + * Because audit_cleanup() might not be called by the test auditd + * process, it might not be possible to explicitly set it. Anyway, + * AUDIT_STATUS_ENABLED will implicitly be set to 0 when the auditd + * process will exit. + */ + return close(audit_fd); +} + +static int audit_init_with_exe_filter(struct audit_filter *filter) +{ + int fd, err; + + fd = audit_init(); + if (fd < 0) + return fd; + + err = audit_init_filter_exe(filter, NULL); + if (err) + return err; + + err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE); + if (err) + return err; + + return fd; +} diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c new file mode 100644 index 000000000000..cfc571afd0eb --- /dev/null +++ b/tools/testing/selftests/landlock/audit_test.c @@ -0,0 +1,671 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Audit + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <limits.h> +#include <linux/landlock.h> +#include <pthread.h> +#include <stdlib.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "audit.h" +#include "common.h" + +static int matches_log_signal(struct __test_metadata *const _metadata, + int audit_fd, const pid_t opid, __u64 *domain_id) +{ + 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_ACCESS, log_match, + domain_id); +} + +FIXTURE(audit) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + 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); + } + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN(audit) +{ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +TEST_F(audit, layers) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int status, ruleset_fd, i; + __u64(*domain_stack)[16]; + __u64 prev_dom = 3; + pid_t child; + + domain_stack = mmap(NULL, sizeof(*domain_stack), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, domain_stack); + memset(domain_stack, 0, sizeof(*domain_stack)); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + for (i = 0; i < ARRAY_SIZE(*domain_stack); i++) { + __u64 denial_dom = 1; + __u64 allocated_dom = 2; + + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + + /* Creates a denial to get the domain ID. */ + EXPECT_EQ(-1, kill(getppid(), 0)); + EXPECT_EQ(EPERM, errno); + EXPECT_EQ(0, + matches_log_signal(_metadata, self->audit_fd, + getppid(), &denial_dom)); + EXPECT_EQ(0, matches_log_domain_allocated( + self->audit_fd, getpid(), + &allocated_dom)); + EXPECT_NE(denial_dom, 1); + EXPECT_NE(denial_dom, 0); + EXPECT_EQ(denial_dom, allocated_dom); + + /* Checks that the new domain is younger than the previous one. */ + EXPECT_GT(allocated_dom, prev_dom); + prev_dom = allocated_dom; + (*domain_stack)[i] = allocated_dom; + } + + /* Checks that we reached the maximum number of layers. */ + EXPECT_EQ(-1, landlock_restrict_self(ruleset_fd, 0)); + EXPECT_EQ(E2BIG, errno); + + /* Updates filter rules to match the drop record. */ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE)); + EXPECT_EQ(0, + audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + /* Purges log from deallocated domains. */ + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_dom_drop, sizeof(audit_tv_dom_drop))); + for (i = ARRAY_SIZE(*domain_stack) - 1; i >= 0; i--) { + __u64 deallocated_dom = 2; + + EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1, + &deallocated_dom)); + EXPECT_EQ((*domain_stack)[i], deallocated_dom) + { + TH_LOG("Failed to match domain %llx (#%d)", + (*domain_stack)[i], i); + } + } + EXPECT_EQ(0, munmap(domain_stack, sizeof(*domain_stack))); + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, sizeof(audit_tv_default))); + EXPECT_EQ(0, close(ruleset_fd)); +} + +struct thread_data { + pid_t parent_pid; + int ruleset_fd, pipe_child, pipe_parent; +}; + +static void *thread_audit_test(void *arg) +{ + const struct thread_data *data = (struct thread_data *)arg; + uintptr_t err = 0; + char buffer; + + /* TGID and TID are different for a second thread. */ + if (getpid() == gettid()) { + err = 1; + goto out; + } + + if (landlock_restrict_self(data->ruleset_fd, 0)) { + err = 2; + goto out; + } + + if (close(data->ruleset_fd)) { + err = 3; + goto out; + } + + /* Creates a denial to get the domain ID. */ + if (kill(data->parent_pid, 0) != -1) { + err = 4; + goto out; + } + + if (EPERM != errno) { + err = 5; + goto out; + } + + /* Signals the parent to read denial logs. */ + if (write(data->pipe_child, ".", 1) != 1) { + err = 6; + goto out; + } + + /* Waits for the parent to update audit filters. */ + if (read(data->pipe_parent, &buffer, 1) != 1) { + err = 7; + goto out; + } + +out: + close(data->pipe_child); + close(data->pipe_parent); + return (void *)err; +} + +/* Checks that the PID tied to a domain is not a TID but the TGID. */ +TEST_F(audit, thread) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + __u64 denial_dom = 1; + __u64 allocated_dom = 2; + __u64 deallocated_dom = 3; + pthread_t thread; + int pipe_child[2], pipe_parent[2]; + char buffer; + struct thread_data child_data; + + child_data.parent_pid = getppid(); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + child_data.pipe_child = pipe_child[1]; + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + child_data.pipe_parent = pipe_parent[0]; + child_data.ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, child_data.ruleset_fd); + + /* TGID and TID are the same for the initial thread . */ + EXPECT_EQ(getpid(), gettid()); + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, pthread_create(&thread, NULL, thread_audit_test, + &child_data)); + + /* Waits for the child to generate a denial. */ + ASSERT_EQ(1, read(pipe_child[0], &buffer, 1)); + EXPECT_EQ(0, close(pipe_child[0])); + + /* Matches the signal log to get the domain ID. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + child_data.parent_pid, &denial_dom)); + EXPECT_NE(denial_dom, 1); + EXPECT_NE(denial_dom, 0); + + EXPECT_EQ(0, matches_log_domain_allocated(self->audit_fd, getpid(), + &allocated_dom)); + EXPECT_EQ(denial_dom, allocated_dom); + + /* Updates filter rules to match the drop record. */ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + /* Signals the thread to exit, which will generate a domain deallocation. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(0, pthread_join(thread, NULL)); + + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_dom_drop, sizeof(audit_tv_dom_drop))); + EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 1, + &deallocated_dom)); + EXPECT_EQ(denial_dom, deallocated_dom); + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, sizeof(audit_tv_default))); +} + +FIXTURE(audit_flags) +{ + struct audit_filter audit_filter; + int audit_fd; + __u64 *domain_id; +}; + +FIXTURE_VARIANT(audit_flags) +{ + const int restrict_flags; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, default) { + /* clang-format on */ + .restrict_flags = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +FIXTURE_SETUP(audit_flags) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + 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); + } + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + self->domain_id = mmap(NULL, sizeof(*self->domain_id), + PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, self->domain_id); + /* Domain IDs are greater or equal to 2^32. */ + *self->domain_id = 1; +} + +FIXTURE_TEARDOWN(audit_flags) +{ + EXPECT_EQ(0, munmap(self->domain_id, sizeof(*self->domain_id))); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +TEST_F(audit_flags, signal) +{ + int status; + pid_t child; + struct audit_records records; + __u64 deallocated_dom = 2; + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int ruleset_fd; + + /* Add filesystem restrictions. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, + variant->restrict_flags)); + EXPECT_EQ(0, close(ruleset_fd)); + + /* First signal checks to test log entries. */ + EXPECT_EQ(-1, kill(getppid(), 0)); + EXPECT_EQ(EPERM, errno); + + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(-EAGAIN, matches_log_signal( + _metadata, self->audit_fd, + getppid(), self->domain_id)); + EXPECT_EQ(*self->domain_id, 1); + } else { + __u64 allocated_dom = 3; + + EXPECT_EQ(0, matches_log_signal( + _metadata, self->audit_fd, + getppid(), self->domain_id)); + + /* Checks domain information records. */ + EXPECT_EQ(0, matches_log_domain_allocated( + self->audit_fd, getpid(), + &allocated_dom)); + EXPECT_NE(*self->domain_id, 1); + EXPECT_NE(*self->domain_id, 0); + EXPECT_EQ(*self->domain_id, allocated_dom); + } + + /* Second signal checks to test audit_count_records(). */ + EXPECT_EQ(-1, kill(getppid(), 0)); + EXPECT_EQ(EPERM, errno); + + /* Makes sure there is no superfluous logged records. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(0, records.access); + } else { + EXPECT_EQ(1, records.access); + } + EXPECT_EQ(0, records.domain); + + /* Updates filter rules to match the drop record. */ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_drop(self->audit_fd, AUDIT_ADD_RULE)); + EXPECT_EQ(0, + audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + if (variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) { + EXPECT_EQ(-EAGAIN, + matches_log_domain_deallocated(self->audit_fd, 0, + &deallocated_dom)); + EXPECT_EQ(deallocated_dom, 2); + } else { + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_dom_drop, + sizeof(audit_tv_dom_drop))); + EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2, + &deallocated_dom)); + EXPECT_NE(deallocated_dom, 2); + EXPECT_NE(deallocated_dom, 0); + EXPECT_EQ(deallocated_dom, *self->domain_id); + EXPECT_EQ(0, setsockopt(self->audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, + sizeof(audit_tv_default))); + } +} + +static int matches_log_fs_read_root(int audit_fd) +{ + return audit_match_record( + audit_fd, AUDIT_LANDLOCK_ACCESS, + REGEX_LANDLOCK_PREFIX + " blockers=fs\\.read_dir path=\"/\" dev=\"[^\"]\\+\" ino=[0-9]\\+$", + NULL); +} + +FIXTURE(audit_exec) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_VARIANT(audit_exec) +{ + const int restrict_flags; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, default) { + /* clang-format on */ + .restrict_flags = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, same_exec_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, subdomains_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_exec, subdomains_off_and_cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, +}; + +FIXTURE_SETUP(audit_exec) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + + 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 test filter for the bin_wait_pipe_sandbox program. */ + EXPECT_EQ(0, audit_init_filter_exe(&self->audit_filter, + bin_wait_pipe_sandbox)); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_ADD_RULE)); + + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN(audit_exec) +{ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_filter_exe(self->audit_fd, &self->audit_filter, + AUDIT_DEL_RULE)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, close(self->audit_fd)); +} + +TEST_F(audit_exec, signal_and_open) +{ + 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) { + const struct landlock_ruleset_attr layer1 = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + char pipe_child_str[12], pipe_parent_str[12]; + char *const argv[] = { (char *)bin_wait_pipe_sandbox, + pipe_child_str, pipe_parent_str, NULL }; + int ruleset_fd; + + /* 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]); + + ruleset_fd = + landlock_create_ruleset(&layer1, sizeof(layer1), 0); + if (ruleset_fd < 0) { + perror("Failed to create a ruleset"); + _exit(1); + } + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + if (landlock_restrict_self(ruleset_fd, + variant->restrict_flags)) { + perror("Failed to restrict self"); + _exit(1); + } + close(ruleset_fd); + + 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. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + /* + * Wait for the child to do a first denied action by layer1 and + * sandbox itself with layer2. + */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the audit record only matches the child. */ + if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) { + /* Matches the current domain. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + /* Checks that we didn't miss anything. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + /* + * Wait for the child to do a second denied action by layer1 and + * layer2, and sandbox itself with layer3. + */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the audit record only matches the child. */ + if (variant->restrict_flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) { + /* Matches the current domain. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + if (!(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* Matches the child domain. */ + EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd)); + } + + /* Checks that we didn't miss anything. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + /* Waits for the child to terminate. */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); + + /* Tests that the audit record only matches the child. */ + if (!(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* + * Matches the child domains, which tests that the + * llcred->domain_exec bitmask is correctly updated with a new + * domain. + */ + EXPECT_EQ(0, matches_log_fs_read_root(self->audit_fd)); + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + } + + /* Checks that we didn't miss anything. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 1bc16fde2e8a..7b69002239d7 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -76,7 +76,7 @@ TEST(abi_version) const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, }; - ASSERT_EQ(6, landlock_create_ruleset(NULL, 0, + ASSERT_EQ(7, landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, @@ -98,10 +98,54 @@ TEST(abi_version) ASSERT_EQ(EINVAL, errno); } +/* + * Old source trees might not have the set of Kselftest fixes related to kernel + * UAPI headers. + */ +#ifndef LANDLOCK_CREATE_RULESET_ERRATA +#define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1) +#endif + +TEST(errata) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + int errata; + + errata = landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_ERRATA); + /* The errata bitmask will not be backported to tests. */ + ASSERT_LE(0, errata); + TH_LOG("errata: 0x%x", errata); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, + LANDLOCK_CREATE_RULESET_ERRATA)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr), + LANDLOCK_CREATE_RULESET_ERRATA)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), + LANDLOCK_CREATE_RULESET_ERRATA)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset( + NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION | + LANDLOCK_CREATE_RULESET_ERRATA)); + ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_ERRATA | + 1 << 31)); + ASSERT_EQ(EINVAL, errno); +} + /* Tests ordering of syscall argument checks. */ TEST(create_ruleset_checks_ordering) { - const int last_flag = LANDLOCK_CREATE_RULESET_VERSION; + const int last_flag = LANDLOCK_CREATE_RULESET_ERRATA; const int invalid_flag = last_flag << 1; int ruleset_fd; const struct landlock_ruleset_attr ruleset_attr = { @@ -233,6 +277,88 @@ TEST(restrict_self_checks_ordering) ASSERT_EQ(0, close(ruleset_fd)); } +TEST(restrict_self_fd) +{ + int fd; + + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + + EXPECT_EQ(-1, landlock_restrict_self(fd, 0)); + EXPECT_EQ(EBADFD, errno); +} + +TEST(restrict_self_fd_flags) +{ + int fd; + + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + + /* + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF accepts -1 but not any file + * descriptor. + */ + EXPECT_EQ(-1, landlock_restrict_self( + fd, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADFD, errno); +} + +TEST(restrict_self_flags) +{ + const __u32 last_flag = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF; + + /* Tests invalid flag combinations. */ + + EXPECT_EQ(-1, landlock_restrict_self(-1, last_flag << 1)); + EXPECT_EQ(EINVAL, errno); + + EXPECT_EQ(-1, landlock_restrict_self(-1, -1)); + EXPECT_EQ(EINVAL, errno); + + /* Tests valid flag combinations. */ + + EXPECT_EQ(-1, landlock_restrict_self(-1, 0)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(-1, + landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON)); + EXPECT_EQ(EBADF, errno); + + /* Tests with an invalid ruleset_fd. */ + + EXPECT_EQ(-1, landlock_restrict_self( + -2, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + EXPECT_EQ(EBADF, errno); + + EXPECT_EQ(0, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); +} + TEST(ruleset_fd_io) { struct landlock_ruleset_attr ruleset_attr = { diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h index 6064c9ac0532..88a3c78f5d98 100644 --- a/tools/testing/selftests/landlock/common.h +++ b/tools/testing/selftests/landlock/common.h @@ -11,6 +11,7 @@ #include <errno.h> #include <linux/securebits.h> #include <sys/capability.h> +#include <sys/prctl.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/wait.h> @@ -30,6 +31,7 @@ static const char bin_sandbox_and_launch[] = "./sandbox-and-launch"; static const char bin_wait_pipe[] = "./wait-pipe"; +static const char bin_wait_pipe_sandbox[] = "./wait-pipe-sandbox"; static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) { @@ -37,10 +39,12 @@ static void _init_caps(struct __test_metadata *const _metadata, bool drop_all) /* Only these three capabilities are useful for the tests. */ const cap_value_t caps[] = { /* clang-format off */ + CAP_AUDIT_CONTROL, CAP_DAC_OVERRIDE, CAP_MKNOD, CAP_NET_ADMIN, CAP_NET_BIND_SERVICE, + CAP_SETUID, CAP_SYS_ADMIN, CAP_SYS_CHROOT, /* clang-format on */ @@ -204,6 +208,22 @@ enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd) } } +static void __maybe_unused +drop_access_rights(struct __test_metadata *const _metadata, + const struct landlock_ruleset_attr *const ruleset_attr) +{ + int ruleset_fd; + + ruleset_fd = + landlock_create_ruleset(ruleset_attr, sizeof(*ruleset_attr), 0); + EXPECT_LE(0, ruleset_fd) + { + TH_LOG("Failed to create a ruleset: %s", strerror(errno)); + } + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + struct protocol_variant { int domain; int type; diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config index 425de4c20271..8fe9b461b1fd 100644 --- a/tools/testing/selftests/landlock/config +++ b/tools/testing/selftests/landlock/config @@ -1,4 +1,5 @@ CONFIG_AF_UNIX_OOB=y +CONFIG_AUDIT=y CONFIG_CGROUPS=y CONFIG_CGROUP_SCHED=y CONFIG_INET=y diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index aa6f2c1cbec7..73729382d40f 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -41,6 +41,7 @@ #define _ASM_GENERIC_FCNTL_H #include <linux/fcntl.h> +#include "audit.h" #include "common.h" #ifndef renameat2 @@ -5554,4 +5555,598 @@ TEST_F_FORK(layout3_fs, release_inodes) ASSERT_EQ(EACCES, test_open(TMP_DIR, O_RDONLY)); } +static int matches_log_fs_extra(struct __test_metadata *const _metadata, + int audit_fd, const char *const blockers, + const char *const path, const char *const extra) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=fs\\.%s path=\"%s\" dev=\"[^\"]\\+\" ino=[0-9]\\+$"; + char *absolute_path = NULL; + size_t log_match_remaining = sizeof(log_template) + strlen(blockers) + + PATH_MAX * 2 + + (extra ? strlen(extra) : 0) + 1; + char log_match[log_match_remaining]; + char *log_match_cursor = log_match; + size_t chunk_len; + + chunk_len = snprintf(log_match_cursor, log_match_remaining, + REGEX_LANDLOCK_PREFIX " blockers=%s path=\"", + blockers); + if (chunk_len < 0 || chunk_len >= log_match_remaining) + return -E2BIG; + + /* + * It is assume that absolute_path does not contain control characters nor + * spaces, see audit_string_contains_control(). + */ + absolute_path = realpath(path, NULL); + if (!absolute_path) + return -errno; + + log_match_remaining -= chunk_len; + log_match_cursor += chunk_len; + log_match_cursor = regex_escape(absolute_path, log_match_cursor, + log_match_remaining); + free(absolute_path); + if (log_match_cursor < 0) + return (long long)log_match_cursor; + + log_match_remaining -= log_match_cursor - log_match; + chunk_len = snprintf(log_match_cursor, log_match_remaining, + "\" dev=\"[^\"]\\+\" ino=[0-9]\\+%s$", + extra ?: ""); + if (chunk_len < 0 || chunk_len >= log_match_remaining) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +static int matches_log_fs(struct __test_metadata *const _metadata, int audit_fd, + const char *const blockers, const char *const path) +{ + return matches_log_fs_extra(_metadata, audit_fd, blockers, path, NULL); +} + +FIXTURE(audit_layout1) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_layout1) +{ + prepare_layout(_metadata); + + create_layout1(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + disable_caps(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(audit_layout1) +{ + remove_layout1(_metadata); + + cleanup_layout(_metadata); + + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +TEST_F(audit_layout1, execute_make) +{ + struct audit_records records; + + copy_file(_metadata, bin_true, file1_s1d1); + test_execute(_metadata, 0, file1_s1d1); + test_check_exec(_metadata, 0, file1_s1d1); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_EXECUTE, + }); + + test_execute(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute", + file1_s1d1)); + test_check_exec(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute", + file1_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +/* + * Using a set of handled/denied access rights make it possible to check that + * only the blocked ones are logged. + */ + +/* clang-format off */ +static const __u64 access_fs_16 = + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_MAKE_CHAR | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_MAKE_FIFO | + LANDLOCK_ACCESS_FS_MAKE_BLOCK | + LANDLOCK_ACCESS_FS_MAKE_SYM | + LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV; +/* clang-format on */ + +TEST_F(audit_layout1, execute_read) +{ + struct audit_records records; + + copy_file(_metadata, bin_true, file1_s1d1); + test_execute(_metadata, 0, file1_s1d1); + test_check_exec(_metadata, 0, file1_s1d1); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + /* + * The only difference with the previous audit_layout1.execute_read test is + * the extra ",fs\\.read_file" blocked by the executable file. + */ + test_execute(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.execute,fs\\.read_file", file1_s1d1)); + test_check_exec(_metadata, EACCES, file1_s1d1); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.execute,fs\\.read_file", file1_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, write_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.write_file", file1_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, read_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_file", + file1_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, read_dir) +{ + struct audit_records records; + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_dir", + dir_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, remove_dir) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + EXPECT_EQ(0, unlink(file2_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, rmdir(dir_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_dir", dir_s1d2)); + + EXPECT_EQ(-1, unlinkat(AT_FDCWD, dir_s1d3, AT_REMOVEDIR)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_dir", dir_s1d2)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, remove_file) +{ + struct audit_records records; + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, unlink(file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file", dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_char) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_char", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_dir) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mkdir(file1_s1d3, 0755)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_dir", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_reg) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_reg", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_sock) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sock", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_fifo) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_fifo", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_block) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_block", dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, make_sym) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, symlink("target", file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.make_sym", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, refer_handled) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_REFER, + }); + + EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3)); + EXPECT_EQ(EXDEV, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + EXPECT_EQ(0, + matches_log_domain_allocated(self->audit_fd, getpid(), NULL)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, refer_make) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REFER, + }); + + EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, refer_rename) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(EACCES, test_rename(file1_s1d2, file1_s2d3)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d2)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg,fs\\.refer", + dir_s2d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +TEST_F(audit_layout1, refer_exchange) +{ + struct audit_records records; + + EXPECT_EQ(0, unlink(file1_s1d3)); + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + /* + * The only difference with the previous audit_layout1.refer_rename test is + * the extra ",fs\\.make_reg" blocked by the source directory. + */ + EXPECT_EQ(EACCES, test_exchange(file1_s1d2, file1_s2d3)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg,fs\\.refer", + dir_s1d2)); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg,fs\\.refer", + dir_s2d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + +/* + * This test checks that the audit record is correctly generated when the + * operation is only partially denied. This is the case for rename(2) when the + * source file is allowed to be referenced but the destination directory is not. + * + * This is also a regression test for commit d617f0d72d80 ("landlock: Optimize + * file path walks and prepare for audit support") and commit 058518c20920 + * ("landlock: Align partial refer access checks with final ones"). + */ +TEST_F(audit_layout1, refer_rename_half) +{ + struct audit_records records; + const struct rule layer1[] = { + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + {}, + }; + int ruleset_fd = + create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1); + + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d3)); + ASSERT_EQ(EXDEV, errno); + + /* Only half of the request is denied. */ + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, truncate) +{ + struct audit_records records; + + drop_access_rights(_metadata, &(struct landlock_ruleset_attr){ + .handled_access_fs = access_fs_16, + }); + + EXPECT_EQ(-1, truncate(file1_s1d3, 0)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.truncate", + file1_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, ioctl_dev) +{ + struct audit_records records; + int fd; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + access_fs_16 & + ~LANDLOCK_ACCESS_FS_READ_FILE, + }); + + fd = open("/dev/null", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FIONREAD)); + EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd, + "fs\\.ioctl_dev", "/dev/null", + " ioctlcmd=0x541b")); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + +TEST_F(audit_layout1, mount) +{ + struct audit_records records; + + drop_access_rights(_metadata, + &(struct landlock_ruleset_attr){ + .handled_access_fs = + LANDLOCK_ACCESS_FS_EXECUTE, + }); + + set_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL)); + EXPECT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.change_topology", dir_s3d2)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c index d9de0ee49ebc..2a45208551e6 100644 --- a/tools/testing/selftests/landlock/net_test.c +++ b/tools/testing/selftests/landlock/net_test.c @@ -20,6 +20,7 @@ #include <sys/syscall.h> #include <sys/un.h> +#include "audit.h" #include "common.h" const short sock_port_start = (1 << 10); @@ -1868,4 +1869,135 @@ TEST_F(port_specific, bind_connect_1023) EXPECT_EQ(0, close(bind_fd)); } +static int matches_log_tcp(const int audit_fd, const char *const blockers, + const char *const dir_addr, const char *const addr, + const char *const dir_port) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=%s %s=%s %s=1024$"; + /* + * Max strlen(blockers): 16 + * Max strlen(dir_addr): 5 + * Max strlen(addr): 12 + * Max strlen(dir_port): 4 + */ + char log_match[sizeof(log_template) + 37]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template, + blockers, dir_addr, addr, dir_port); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +FIXTURE(audit) +{ + struct service_fixture srv0; + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_VARIANT(audit) +{ + const char *const addr; + const struct protocol_variant prot; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv4) { + /* clang-format on */ + .addr = "127\\.0\\.0\\.1", + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv6) { + /* clang-format on */ + .addr = "::1", + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +FIXTURE_SETUP(audit) +{ + ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + setup_loopback(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + disable_caps(_metadata); +}; + +FIXTURE_TEARDOWN(audit) +{ + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(self->audit_fd, &self->audit_filter)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +TEST_F(audit, bind) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + struct audit_records records; + int ruleset_fd, sock_fd; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv0); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0)); + EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr", + variant->addr, "src")); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + +TEST_F(audit, connect) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + struct audit_records records; + int ruleset_fd, sock_fd; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv0); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv0)); + EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.connect_tcp", + "daddr", variant->addr, "dest")); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c index 8f31b673ff2d..4e356334ecb7 100644 --- a/tools/testing/selftests/landlock/ptrace_test.c +++ b/tools/testing/selftests/landlock/ptrace_test.c @@ -4,6 +4,7 @@ * * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> * Copyright © 2019-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #define _GNU_SOURCE @@ -17,6 +18,7 @@ #include <sys/wait.h> #include <unistd.h> +#include "audit.h" #include "common.h" /* Copied from security/yama/yama_lsm.c */ @@ -434,4 +436,142 @@ TEST_F(hierarchy, trace) _metadata->exit_code = KSFT_FAIL; } +static int matches_log_ptrace(struct __test_metadata *const _metadata, + int audit_fd, const pid_t opid) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=ptrace opid=%d ocomm=\"ptrace_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_ACCESS, log_match, + NULL); +} + +FIXTURE(audit) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit) +{ + disable_caps(_metadata); + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +FIXTURE_TEARDOWN_PARENT(audit) +{ + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */ +TEST_F(audit, trace) +{ + pid_t child; + int status; + int pipe_child[2], pipe_parent[2]; + int yama_ptrace_scope; + char buf_parent; + struct audit_records records; + + /* Makes sure there is no superfluous logged records. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + yama_ptrace_scope = get_yama_ptrace_scope(); + ASSERT_LE(0, yama_ptrace_scope); + + if (yama_ptrace_scope > YAMA_SCOPE_DISABLED) + TH_LOG("Incomplete tests due to Yama restrictions (scope %d)", + yama_ptrace_scope); + + /* + * Removes all effective and permitted capabilities to not interfere + * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS. + */ + drop_caps(_metadata); + + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char buf_child; + + ASSERT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(0, close(pipe_child[0])); + + /* Waits for the parent to be in a domain, if any. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + + /* Tests child PTRACE_TRACEME. */ + EXPECT_EQ(-1, ptrace(PTRACE_TRACEME)); + EXPECT_EQ(EPERM, errno); + /* We should see the child process. */ + EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, + getpid())); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + /* Checks for a domain creation. */ + EXPECT_EQ(1, records.domain); + + /* + * Signals that the PTRACE_ATTACH test is done and the + * PTRACE_TRACEME test is ongoing. + */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + /* Waits for the parent PTRACE_ATTACH test. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(0, close(pipe_child[1])); + ASSERT_EQ(0, close(pipe_parent[0])); + create_domain(_metadata); + + /* Signals that the parent is in a domain. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* + * Waits for the child to test PTRACE_ATTACH on the parent and start + * testing PTRACE_TRACEME. + */ + ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* The child should not be traced by the parent. */ + EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0)); + EXPECT_EQ(ESRCH, errno); + + /* Tests PTRACE_ATTACH on the child. */ + EXPECT_EQ(-1, ptrace(PTRACE_ATTACH, child, NULL, 0)); + EXPECT_EQ(EPERM, errno); + EXPECT_EQ(0, matches_log_ptrace(_metadata, self->audit_fd, child)); + + /* Signals that the parent PTRACE_ATTACH test is done. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + /* Makes sure there is no superfluous logged records. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c index a6b59d2ab1b4..6825082c079c 100644 --- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -20,6 +20,7 @@ #include <sys/wait.h> #include <unistd.h> +#include "audit.h" #include "common.h" #include "scoped_common.h" @@ -267,6 +268,116 @@ TEST_F(scoped_domains, connect_to_child) _metadata->exit_code = KSFT_FAIL; } +FIXTURE(scoped_audit) +{ + struct service_fixture dgram_address; + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(scoped_audit) +{ + disable_caps(_metadata); + + memset(&self->dgram_address, 0, sizeof(self->dgram_address)); + set_unix_address(&self->dgram_address, 1); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + self->audit_fd = audit_init_with_exe_filter(&self->audit_filter); + EXPECT_LE(0, self->audit_fd); + drop_caps(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(scoped_audit) +{ + EXPECT_EQ(0, audit_cleanup(-1, NULL)); +} + +/* python -c 'print(b"\0selftests-landlock-abstract-unix-".hex().upper())' */ +#define ABSTRACT_SOCKET_PATH_PREFIX \ + "0073656C6674657374732D6C616E646C6F636B2D61627374726163742D756E69782D" + +/* + * Simpler version of scoped_domains.connect_to_child, but with audit tests. + */ +TEST_F(scoped_audit, connect_to_child) +{ + pid_t child; + int err_dgram, status; + int pipe_child[2], pipe_parent[2]; + char buf; + int dgram_client; + struct audit_records records; + + /* Makes sure there is no superfluous logged records. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int dgram_server; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + + /* Waits for the parent to be in a domain. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + + dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server); + ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len)); + + /* Signals to the parent that child is listening. */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + /* Waits to connect. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + EXPECT_EQ(0, close(dgram_server)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + + create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* Signals that the parent is in a domain, if any. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_client); + + /* Waits for the child to listen */ + ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); + err_dgram = connect(dgram_client, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len); + EXPECT_EQ(-1, err_dgram); + EXPECT_EQ(EPERM, errno); + + EXPECT_EQ( + 0, + audit_match_record( + self->audit_fd, AUDIT_LANDLOCK_ACCESS, + REGEX_LANDLOCK_PREFIX + " blockers=scope\\.abstract_unix_socket path=" ABSTRACT_SOCKET_PATH_PREFIX + "[0-9A-F]\\+$", + NULL)); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(dgram_client)); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + FIXTURE(scoped_vs_unscoped) { struct service_fixture parent_stream_address, parent_dgram_address, diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c index 475ee62a832d..d8bf33417619 100644 --- a/tools/testing/selftests/landlock/scoped_signal_test.c +++ b/tools/testing/selftests/landlock/scoped_signal_test.c @@ -249,47 +249,67 @@ TEST_F(scoped_domains, check_access_signal) _metadata->exit_code = KSFT_FAIL; } -static int thread_pipe[2]; - enum thread_return { THREAD_INVALID = 0, THREAD_SUCCESS = 1, THREAD_ERROR = 2, + THREAD_TEST_FAILED = 3, }; -void *thread_func(void *arg) +static void *thread_sync(void *arg) { + const int pipe_read = *(int *)arg; char buf; - if (read(thread_pipe[0], &buf, 1) != 1) + if (read(pipe_read, &buf, 1) != 1) return (void *)THREAD_ERROR; return (void *)THREAD_SUCCESS; } -TEST(signal_scoping_threads) +TEST(signal_scoping_thread_before) { - pthread_t no_sandbox_thread, scoped_thread; + pthread_t no_sandbox_thread; enum thread_return ret = THREAD_INVALID; + int thread_pipe[2]; drop_caps(_metadata); ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC)); - ASSERT_EQ(0, - pthread_create(&no_sandbox_thread, NULL, thread_func, NULL)); + ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_sync, + &thread_pipe[0])); - /* Restricts the domain after creating the first thread. */ + /* Enforces restriction after creating the thread. */ create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); - ASSERT_EQ(EPERM, pthread_kill(no_sandbox_thread, 0)); - ASSERT_EQ(1, write(thread_pipe[1], ".", 1)); - - ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_func, NULL)); - ASSERT_EQ(0, pthread_kill(scoped_thread, 0)); - ASSERT_EQ(1, write(thread_pipe[1], ".", 1)); + EXPECT_EQ(0, pthread_kill(no_sandbox_thread, 0)); + EXPECT_EQ(1, write(thread_pipe[1], ".", 1)); EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret)); EXPECT_EQ(THREAD_SUCCESS, ret); + + EXPECT_EQ(0, close(thread_pipe[0])); + EXPECT_EQ(0, close(thread_pipe[1])); +} + +TEST(signal_scoping_thread_after) +{ + pthread_t scoped_thread; + enum thread_return ret = THREAD_INVALID; + int thread_pipe[2]; + + drop_caps(_metadata); + ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC)); + + /* Enforces restriction before creating the thread. */ + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_sync, + &thread_pipe[0])); + + EXPECT_EQ(0, pthread_kill(scoped_thread, 0)); + EXPECT_EQ(1, write(thread_pipe[1], ".", 1)); + EXPECT_EQ(0, pthread_join(scoped_thread, (void **)&ret)); EXPECT_EQ(THREAD_SUCCESS, ret); @@ -297,6 +317,64 @@ TEST(signal_scoping_threads) EXPECT_EQ(0, close(thread_pipe[1])); } +struct thread_setuid_args { + int pipe_read, new_uid; +}; + +void *thread_setuid(void *ptr) +{ + const struct thread_setuid_args *arg = ptr; + char buf; + + if (read(arg->pipe_read, &buf, 1) != 1) + return (void *)THREAD_ERROR; + + /* libc's setuid() should update all thread's credentials. */ + if (getuid() != arg->new_uid) + return (void *)THREAD_TEST_FAILED; + + return (void *)THREAD_SUCCESS; +} + +TEST(signal_scoping_thread_setuid) +{ + struct thread_setuid_args arg; + pthread_t no_sandbox_thread; + enum thread_return ret = THREAD_INVALID; + int pipe_parent[2]; + int prev_uid; + + disable_caps(_metadata); + + /* This test does not need to be run as root. */ + prev_uid = getuid(); + arg.new_uid = prev_uid + 1; + EXPECT_LT(0, arg.new_uid); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + arg.pipe_read = pipe_parent[0]; + + /* Capabilities must be set before creating a new thread. */ + set_cap(_metadata, CAP_SETUID); + ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_setuid, + &arg)); + + /* Enforces restriction after creating the thread. */ + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + EXPECT_NE(arg.new_uid, getuid()); + EXPECT_EQ(0, setuid(arg.new_uid)); + EXPECT_EQ(arg.new_uid, getuid()); + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + + EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret)); + EXPECT_EQ(THREAD_SUCCESS, ret); + + clear_cap(_metadata, CAP_SETUID); + EXPECT_EQ(0, close(pipe_parent[0])); + EXPECT_EQ(0, close(pipe_parent[1])); +} + const short backlog = 10; static volatile sig_atomic_t signal_received; diff --git a/tools/testing/selftests/landlock/wait-pipe-sandbox.c b/tools/testing/selftests/landlock/wait-pipe-sandbox.c new file mode 100644 index 000000000000..87dbc9164430 --- /dev/null +++ b/tools/testing/selftests/landlock/wait-pipe-sandbox.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Write in a pipe, wait, sandbox itself, test sandboxing, and wait again. + * + * Used by audit_exec.flags from audit_test.c + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <fcntl.h> +#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" + +static int sync_with(int pipe_child, int pipe_parent) +{ + char buf; + + /* Signals that we are waiting. */ + if (write(pipe_child, ".", 1) != 1) { + perror("Failed to write to first argument"); + return 1; + } + + /* Waits for the parent do its test. */ + if (read(pipe_parent, &buf, 1) != 1) { + perror("Failed to write to the second argument"); + return 1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + const struct landlock_ruleset_attr layer2 = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + const struct landlock_ruleset_attr layer3 = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int err, pipe_child, pipe_parent, ruleset_fd; + + /* The first argument must be the file descriptor number of a pipe. */ + if (argc != 3) { + fprintf(stderr, "Wrong number of arguments (not two)\n"); + return 1; + } + + pipe_child = atoi(argv[1]); + pipe_parent = atoi(argv[2]); + /* PR_SET_NO_NEW_PRIVS already set by parent. */ + + /* First step to test parent's layer1. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + /* Tries to send a signal, denied by layer1. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + /* Second step to test parent's layer1 and our layer2. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + ruleset_fd = landlock_create_ruleset(&layer2, sizeof(layer2), 0); + if (ruleset_fd < 0) { + perror("Failed to create the layer2 ruleset"); + return 1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + close(ruleset_fd); + + /* Tries to send a signal, denied by layer1. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + /* Tries to open ., denied by layer2. */ + if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) { + fprintf(stderr, "Successfully opened /"); + return 1; + } + + /* Third step to test our layer2 and layer3. */ + err = sync_with(pipe_child, pipe_parent); + if (err) + return err; + + ruleset_fd = landlock_create_ruleset(&layer3, sizeof(layer3), 0); + if (ruleset_fd < 0) { + perror("Failed to create the layer3 ruleset"); + return 1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + close(ruleset_fd); + + /* Tries to open ., denied by layer2. */ + if (open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC) >= 0) { + fprintf(stderr, "Successfully opened /"); + return 1; + } + + /* Tries to send a signal, denied by layer3. */ + if (!kill(getppid(), 0)) { + fprintf(stderr, "Successfully sent a signal to the parent"); + return 1; + } + + return 0; +} |