diff options
Diffstat (limited to 'tools/testing/selftests/landlock')
24 files changed, 19549 insertions, 0 deletions
diff --git a/tools/testing/selftests/landlock/.gitignore b/tools/testing/selftests/landlock/.gitignore new file mode 100644 index 000000000..1974e17a2 --- /dev/null +++ b/tools/testing/selftests/landlock/.gitignore @@ -0,0 +1,6 @@ +/*_test +/fs_bench +/sandbox-and-launch +/true +/wait-pipe +/wait-pipe-sandbox diff --git a/tools/testing/selftests/landlock/Makefile b/tools/testing/selftests/landlock/Makefile new file mode 100644 index 000000000..fc43225d3 --- /dev/null +++ b/tools/testing/selftests/landlock/Makefile @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# First run: make -C ../../../.. headers_install + +CFLAGS += -Wall -O2 $(KHDR_INCLUDES) + +LOCAL_HDRS += $(wildcard *.h) + +src_test := $(wildcard *_test.c) + +TEST_GEN_PROGS := $(src_test:.c=) +TEST_GEN_PROGS += fs_bench + +TEST_GEN_PROGS_EXTENDED := \ + true \ + sandbox-and-launch \ + wait-pipe \ + wait-pipe-sandbox + +# Short targets: +$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread +$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static + +include ../lib.mk + +# Targets with $(OUTPUT)/ prefix: +$(TEST_GEN_PROGS): LDLIBS += -lcap -lpthread +$(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h new file mode 100644 index 000000000..f45fdef35 --- /dev/null +++ b/tools/testing/selftests/landlock/audit.h @@ -0,0 +1,605 @@ +/* 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> + +#include "kselftest.h" + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#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_default = { + /* + * Default socket timeout for audit_match_record() callers that expect a + * record to arrive. Asynchronous kauditd delivery can exceed 1 usec + * under heavy debug configs (KASAN, lockdep), where kauditd_thread + * scheduling between audit_log_end() and netlink_unicast() takes longer + * than the previous 1 usec timeout. 1 second is a generous ceiling: on + * the happy path, kauditd delivers within dozens of usec. + */ + .tv_sec = 1, +}; + +static const struct timeval audit_tv_fast = { + /* + * Fast timeout for paths that expect no record (audit_init() drain, + * audit_count_records(), probes). Causes audit_recv() to return + * -EAGAIN once the socket buffer is empty, naturally terminating the + * read loop. + */ + .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, last_mismatch = {}; + int ret, err = 0; + int num_type_match = 0; + regmatch_t matches[2]; + regex_t regex; + + ret = regcomp(®ex, pattern, 0); + if (ret) + return -EINVAL; + + /* + * Reads records until one matches both the expected type and the + * pattern. Type-matching records with non-matching content are + * silently consumed, which handles stale domain deallocation records + * from a previous test emitted asynchronously by kworker threads. + */ + while (true) { + memset(&msg, 0, sizeof(msg)); + err = audit_recv(audit_fd, &msg); + if (err) { + if (num_type_match) { + printf("DATA: %s\n", last_mismatch.data); + printf("ERROR: %d record(s) matched type %u" + " but not pattern: %s\n", + num_type_match, type, pattern); + } + goto out; + } + + if (type && msg.header.nlmsg_type != type) + continue; + + ret = regexec(®ex, msg.data, ARRAY_SIZE(matches), matches, + 0); + if (!ret) + break; + + num_type_match++; + last_mismatch = msg; + } + + 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); +} + +/* + * Matches a domain deallocation record. When expected_domain_id is non-zero, + * the pattern includes the specific domain ID so that stale deallocation + * records from a previous test (with a different domain ID) are skipped by + * audit_match_record(), waiting for the asynchronous kworker deallocation with + * the default patient timeout. + * + * When expected_domain_id is zero, the caller is probing for any dealloc record + * that may or may not arrive. Temporarily lowers the socket timeout to + * audit_tv_fast for this probe so it returns promptly when no record is + * pending; restores audit_tv_default after. + */ +static int __maybe_unused +matches_log_domain_deallocated(int audit_fd, unsigned int num_denials, + __u64 expected_domain_id, __u64 *domain_id) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " status=deallocated denials=%u$"; + static const char log_template_with_id[] = + "^audit([0-9.:]\\+): domain=\\(%llx\\)" + " status=deallocated denials=%u$"; + char log_match[sizeof(log_template_with_id) + 32]; + int log_match_len, err; + + if (expected_domain_id) + log_match_len = snprintf(log_match, sizeof(log_match), + log_template_with_id, + (unsigned long long)expected_domain_id, + num_denials); + else + log_match_len = snprintf(log_match, sizeof(log_match), + log_template, num_denials); + + if (log_match_len >= sizeof(log_match)) + return -E2BIG; + + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_fast, sizeof(audit_tv_fast))) + return -errno; + } + + err = audit_match_record(audit_fd, AUDIT_LANDLOCK_DOMAIN, log_match, + domain_id); + + if (!expected_domain_id) { + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, + &audit_tv_default, sizeof(audit_tv_default)) && + !err) + err = -errno; + } + + return err; +} + +struct audit_records { + size_t access; + size_t domain; +}; + +/* + * Counts remaining audit records by type, skipping domain deallocation records. + * Deallocation records are emitted asynchronously from kworker threads after a + * previous test's child has exited, so they can arrive after the drain in + * audit_init() and after the preceding audit_match_record() call. Allocation + * records are emitted synchronously during landlock_log_denial() in the current + * test's syscall context, so only those are counted in records->domain. + * + * Temporarily lowers SO_RCVTIMEO to audit_tv_fast for the read loop: this is a + * "no record expected" path that should terminate on the first -EAGAIN. The + * default patient timeout is restored on exit for subsequent + * audit_match_record() callers. + */ +static int audit_count_records(int audit_fd, struct audit_records *records) +{ + static const char dealloc_pattern[] = REGEX_LANDLOCK_PREFIX + " status=deallocated "; + struct audit_message msg; + regex_t dealloc_re; + int ret, err = 0; + + ret = regcomp(&dealloc_re, dealloc_pattern, 0); + if (ret) + return -ENOMEM; + + records->access = 0; + records->domain = 0; + + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast))) { + err = -errno; + goto out; + } + + do { + memset(&msg, 0, sizeof(msg)); + err = audit_recv(audit_fd, &msg); + if (err) { + if (err == -EAGAIN) + err = 0; + break; + } + + switch (msg.header.nlmsg_type) { + case AUDIT_LANDLOCK_ACCESS: + records->access++; + break; + case AUDIT_LANDLOCK_DOMAIN: + ret = regexec(&dealloc_re, msg.data, 0, NULL, 0); + if (ret == REG_NOMATCH) { + records->domain++; + } else if (ret != 0) { + err = -EIO; + goto out; + } + break; + } + } while (true); + +out: + if (setsockopt(audit_fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)) && + !err) + err = -errno; + regfree(&dealloc_re); + return err; +} + +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) + goto err_close; + + err = audit_set_status(fd, AUDIT_STATUS_PID, getpid()); + if (err) + goto err_close; + + /* Uses the fast timeout to drain stale records below. */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_fast, + sizeof(audit_tv_fast)); + if (err) { + err = -errno; + goto err_close; + } + + /* + * Drains stale audit records that accumulated in the kernel backlog + * while no audit daemon socket was open. This happens when non-audit + * Landlock tests generate records while audit_enabled is non-zero (e.g. + * from boot configuration), or when domain deallocation records arrive + * asynchronously after a previous test's socket was closed. + */ + while (audit_recv(fd, NULL) == 0) + ; + + /* + * Restores the default timeout for audit_match_record() callers that + * expect a record to arrive. Paths that expect no record restore the + * fast timeout locally (audit_count_records(), the expected_domain_id + * == 0 probe in matches_log_domain_deallocated()). + */ + err = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &audit_tv_default, + sizeof(audit_tv_default)); + if (err) { + err = -errno; + goto err_close; + } + + return fd; + +err_close: + close(fd); + return err; +} + +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) { + int ret = readlink("/proc/self/exe", filter->exe, + sizeof(filter->exe) - 1); + if (ret < 0) + return -errno; + + filter->exe_len = ret; + 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; + int err = 0; + + if (audit_fd < 0 || !filter) { + /* + * 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) + goto err_close; + } + + /* Filters might not be in place. */ + audit_filter_exe(audit_fd, filter, AUDIT_DEL_RULE); + audit_filter_drop(audit_fd, AUDIT_DEL_RULE); + + err = audit_set_status(audit_fd, AUDIT_STATUS_ENABLED, 0); + +err_close: + close(audit_fd); + return err; +} + +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) + goto err_close; + + err = audit_filter_exe(fd, filter, AUDIT_ADD_RULE); + if (err) + goto err_close; + + return fd; + +err_close: + close(fd); + return err; +} diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c new file mode 100644 index 000000000..72b561237 --- /dev/null +++ b/tools/testing/selftests/landlock/audit_test.c @@ -0,0 +1,1000 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Audit + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.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)[LANDLOCK_MAX_NUM_LAYERS]; + __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. Records arrive in LIFO order + * (innermost domain first) because landlock_put_hierarchy() walks the + * chain sequentially in a single kworker context. + */ + 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, + (*domain_stack)[i], + &deallocated_dom)); + EXPECT_EQ((*domain_stack)[i], deallocated_dom) + { + TH_LOG("Failed to match domain %llx (#%d)", + (unsigned long long)(*domain_stack)[i], i); + } + } + EXPECT_EQ(0, munmap(domain_stack, sizeof(*domain_stack))); + EXPECT_EQ(0, close(ruleset_fd)); +} + +struct thread_data { + pid_t parent_pid; + int ruleset_fd, pipe_child, pipe_parent; + bool mute_subdomains; +}; + +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, matches_log_domain_deallocated( + self->audit_fd, 1, denial_dom, &deallocated_dom)); + EXPECT_EQ(denial_dom, deallocated_dom); +} + +/* + * Verifies that log_subdomains_off set via the ruleset_fd=-1 path (without + * creating a domain) is inherited by children across fork(). This exercises + * the hook_cred_transfer() fix: the Landlock credential blob must be copied + * even when the source credential has no domain. + * + * Phase 1 (baseline): a child without muting creates a domain and triggers a + * denial that IS logged. + * + * Phase 2 (after muting): the parent mutes subdomain logs, forks another child + * who creates a domain and triggers a denial that is NOT logged. + */ +TEST_F(audit, log_subdomains_off_fork) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + struct audit_records records; + int ruleset_fd, status; + pid_t child; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + /* + * Phase 1: forks a child that creates a domain and triggers a denial + * before any muting. This proves the audit path works. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + ASSERT_EQ(-1, kill(getppid(), 0)); + ASSERT_EQ(EPERM, errno); + _exit(0); + return; + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(true, WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); + + /* The denial must be logged (baseline). */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, getpid(), + NULL)); + + /* Drains any remaining records (e.g. domain allocation). */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + + /* + * Mutes subdomain logs without creating a domain. The parent's + * credential has domain=NULL and log_subdomains_off=1. + */ + ASSERT_EQ(0, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)); + + /* + * Phase 2: forks a child that creates a domain and triggers a denial. + * Because log_subdomains_off was inherited via fork(), the child's + * domain has log_status=LANDLOCK_LOG_DISABLED. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + ASSERT_EQ(-1, kill(getppid(), 0)); + ASSERT_EQ(EPERM, errno); + _exit(0); + return; + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(true, WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); + + /* No denial record should appear. */ + EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd, + getpid(), NULL)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(ruleset_fd)); +} + +/* + * Thread function: runs two rounds of (create domain, trigger denial, signal + * back), waiting for the main thread before each round. When mute_subdomains + * is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating + * the domain. The ruleset_fd is kept open across both rounds so each + * restrict_self call stacks a new domain layer. + */ +static void *thread_sandbox_deny_twice(void *arg) +{ + const struct thread_data *data = (struct thread_data *)arg; + uintptr_t err = 0; + char buffer; + + /* Phase 1: optionally mutes, creates a domain, and triggers a denial. */ + if (read(data->pipe_parent, &buffer, 1) != 1) { + err = 1; + goto out; + } + + if (data->mute_subdomains && + landlock_restrict_self(-1, + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + err = 2; + goto out; + } + + if (landlock_restrict_self(data->ruleset_fd, 0)) { + err = 3; + goto out; + } + + if (kill(data->parent_pid, 0) != -1 || errno != EPERM) { + err = 4; + goto out; + } + + if (write(data->pipe_child, ".", 1) != 1) { + err = 5; + goto out; + } + + /* Phase 2: stacks another domain and triggers a denial. */ + if (read(data->pipe_parent, &buffer, 1) != 1) { + err = 6; + goto out; + } + + if (landlock_restrict_self(data->ruleset_fd, 0)) { + err = 7; + goto out; + } + + if (kill(data->parent_pid, 0) != -1 || errno != EPERM) { + err = 8; + goto out; + } + + if (write(data->pipe_child, ".", 1) != 1) { + err = 9; + goto out; + } + +out: + close(data->ruleset_fd); + close(data->pipe_child); + close(data->pipe_parent); + return (void *)err; +} + +/* + * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with + * LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off + * to a sibling thread, suppressing audit logging on domains it subsequently + * creates. + * + * Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a + * domain and triggers a denial that IS logged. + * + * Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain + * and triggers a denial that is NOT logged. + */ +TEST_F(audit, log_subdomains_off_tsync) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + struct audit_records records; + struct thread_data child_data = {}; + int pipe_child[2], pipe_parent[2]; + char buffer; + pthread_t thread; + void *thread_ret; + + 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); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + /* Creates the sibling thread. */ + ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice, + &child_data)); + + /* + * Phase 1: the sibling creates a domain and triggers a denial before + * any log muting. This proves the audit path works. + */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(1, read(pipe_child[0], &buffer, 1)); + + /* The denial must be logged. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + child_data.parent_pid, NULL)); + + /* Drains any remaining records (e.g. domain allocation). */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + + /* + * Mutes subdomain logs and propagates to the sibling thread via TSYNC, + * without creating a domain. + */ + ASSERT_EQ(0, landlock_restrict_self( + -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_TSYNC)); + + /* + * Phase 2: the sibling stacks another domain and triggers a denial. + * Because log_subdomains_off was propagated via TSYNC, the new domain + * has log_status=LANDLOCK_LOG_DISABLED. + */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(1, read(pipe_child[0], &buffer, 1)); + + /* No denial record should appear. */ + EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd, + child_data.parent_pid, NULL)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(pipe_child[0])); + EXPECT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(0, pthread_join(thread, &thread_ret)); + EXPECT_EQ(NULL, thread_ret); +} + +/* + * Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's + * log_subdomains_off, re-enabling audit logging on domains the sibling + * subsequently creates. + * + * Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and + * triggers a denial that is NOT logged. + * + * Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another + * domain and triggers a denial that IS logged, proving the muting was + * overridden. + */ +TEST_F(audit, tsync_override_log_subdomains_off) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + struct audit_records records; + struct thread_data child_data = {}; + int pipe_child[2], pipe_parent[2]; + char buffer; + pthread_t thread; + void *thread_ret; + + 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); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + child_data.mute_subdomains = true; + + /* Creates the sibling thread. */ + ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice, + &child_data)); + + /* + * Phase 1: the sibling mutes subdomain logs, creates a domain, and + * triggers a denial. The denial must not be logged. + */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(1, read(pipe_child[0], &buffer, 1)); + + EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd, + child_data.parent_pid, NULL)); + + /* Drains any remaining records. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + /* + * Overrides the sibling's log_subdomains_off by calling TSYNC without + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF. + */ + ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC)); + + /* + * Phase 2: the sibling stacks another domain and triggers a denial. + * Because TSYNC replaced its log_subdomains_off with 0, the new domain + * has log_status=LANDLOCK_LOG_PENDING. + */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(1, read(pipe_child[0], &buffer, 1)); + + /* The denial must be logged. */ + EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd, + child_data.parent_pid, NULL)); + + EXPECT_EQ(0, close(pipe_child[0])); + EXPECT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(0, pthread_join(thread, &thread_ret)); + EXPECT_EQ(NULL, thread_ret); +} + +FIXTURE(audit_flags) +{ + struct audit_filter audit_filter; + int audit_fd; + __u64 *domain_id; +}; + +FIXTURE_VARIANT(audit_flags) +{ + const int restrict_flags; + const __u64 quiet_scoped; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, default) { + /* clang-format on */ + .restrict_flags = 0, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, same_exec_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, subdomains_off) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, cross_exec_on) { + /* clang-format on */ + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit_flags, signal_quieted) { + /* clang-format on */ + .restrict_flags = 0, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, +}; + +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; + bool expect_audit = !(variant->restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF) && + !(variant->quiet_scoped & LANDLOCK_SCOPE_SIGNAL); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = variant->quiet_scoped, + }; + 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 (!expect_audit) { + 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 (!expect_audit) { + 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 (!expect_audit) { + /* + * No deallocation record: denials=0 never matches a real + * record. + */ + EXPECT_EQ(-EAGAIN, + matches_log_domain_deallocated(self->audit_fd, 0, 0, + &deallocated_dom)); + EXPECT_EQ(deallocated_dom, 2); + } else { + EXPECT_EQ(0, matches_log_domain_deallocated(self->audit_fd, 2, + *self->domain_id, + &deallocated_dom)); + EXPECT_NE(deallocated_dom, 2); + EXPECT_NE(deallocated_dom, 0); + EXPECT_EQ(deallocated_dom, *self->domain_id); + } +} + +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_cleanup(self->audit_fd, &self->audit_filter)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +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 new file mode 100644 index 000000000..cbd3c1669 --- /dev/null +++ b/tools/testing/selftests/landlock/base_test.c @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Common user space base + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/keyctl.h> +#include <linux/landlock.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/socket.h> +#include <sys/types.h> + +#include "common.h" + +#ifndef O_PATH +#define O_PATH 010000000 +#endif + +TEST(inconsistent_attr) +{ + const long page_size = sysconf(_SC_PAGESIZE); + char *const buf = malloc(page_size + 1); + struct landlock_ruleset_attr *const ruleset_attr = (void *)buf; + + ASSERT_NE(NULL, buf); + + /* Checks copy_from_user(). */ + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 0, 0)); + /* The size if less than sizeof(struct landlock_attr_enforce). */ + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 1, 0)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 7, 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, 1, 0)); + /* The size if less than sizeof(struct landlock_attr_enforce). */ + ASSERT_EQ(EFAULT, errno); + + ASSERT_EQ(-1, landlock_create_ruleset( + NULL, sizeof(struct landlock_ruleset_attr), 0)); + ASSERT_EQ(EFAULT, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0)); + ASSERT_EQ(E2BIG, errno); + + /* Checks minimal valid attribute size. */ + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, 8, 0)); + ASSERT_EQ(ENOMSG, errno); + ASSERT_EQ(-1, landlock_create_ruleset( + ruleset_attr, + sizeof(struct landlock_ruleset_attr), 0)); + ASSERT_EQ(ENOMSG, errno); + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0)); + ASSERT_EQ(ENOMSG, errno); + + /* Checks non-zero value. */ + buf[page_size - 2] = '.'; + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size, 0)); + ASSERT_EQ(E2BIG, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(ruleset_attr, page_size + 1, 0)); + ASSERT_EQ(E2BIG, errno); + + free(buf); +} + +TEST(abi_version) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + ASSERT_EQ(10, landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION)); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, + LANDLOCK_CREATE_RULESET_VERSION)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr), + LANDLOCK_CREATE_RULESET_VERSION)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), + LANDLOCK_CREATE_RULESET_VERSION)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION | + 1 << 31)); + 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_ERRATA; + const int invalid_flag = last_flag << 1; + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + + /* Checks priority for invalid flags. */ + ASSERT_EQ(-1, landlock_create_ruleset(NULL, 0, invalid_flag)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, invalid_flag)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(NULL, sizeof(ruleset_attr), + invalid_flag)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), + invalid_flag)); + ASSERT_EQ(EINVAL, errno); + + /* Checks too big ruleset_attr size. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, -1, 0)); + ASSERT_EQ(E2BIG, errno); + + /* Checks too small ruleset_attr size. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0, 0)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 1, 0)); + ASSERT_EQ(EINVAL, errno); + + /* Checks valid call. */ + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); +} + +/* Tests ordering of syscall argument checks. */ +TEST(add_rule_checks_ordering) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE, + .parent_fd = -1, + }; + const int ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + + /* Checks invalid flags. */ + ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100)); + ASSERT_EQ(EINVAL, errno); + + /* Checks invalid ruleset FD. */ + ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 0)); + ASSERT_EQ(EBADF, errno); + + /* Checks invalid rule type. */ + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, 0, NULL, 0)); + ASSERT_EQ(EINVAL, errno); + + /* Checks invalid rule attr. */ + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + NULL, 0)); + ASSERT_EQ(EFAULT, errno); + + /* Checks invalid path_beneath.parent_fd. */ + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, 0)); + ASSERT_EQ(EBADF, errno); + + /* Checks valid call. */ + path_beneath_attr.parent_fd = + open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath_attr.parent_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, 0)); + ASSERT_EQ(0, close(path_beneath_attr.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); +} + +/* Tests ordering of syscall argument and permission checks. */ +TEST(restrict_self_checks_ordering) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE, + .parent_fd = -1, + }; + const int ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + path_beneath_attr.parent_fd = + open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath_attr.parent_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, 0)); + ASSERT_EQ(0, close(path_beneath_attr.parent_fd)); + + /* Checks unprivileged enforcement without no_new_privs. */ + drop_caps(_metadata); + ASSERT_EQ(-1, landlock_restrict_self(-1, -1)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, landlock_restrict_self(-1, 0)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0)); + ASSERT_EQ(EPERM, errno); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + /* Checks invalid flags. */ + ASSERT_EQ(-1, landlock_restrict_self(-1, -1)); + ASSERT_EQ(EINVAL, errno); + + /* Checks invalid ruleset FD. */ + ASSERT_EQ(-1, landlock_restrict_self(-1, 0)); + ASSERT_EQ(EBADF, errno); + + /* Checks valid call. */ + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + 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_logging_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_logging_flags) +{ + const __u32 last_flag = LANDLOCK_RESTRICT_SELF_TSYNC; + + /* 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 = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, + }; + int ruleset_fd; + char buf; + + drop_caps(_metadata); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(-1, write(ruleset_fd, ".", 1)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(-1, read(ruleset_fd, &buf, 1)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +/* Tests enforcement of a ruleset FD transferred through a UNIX socket. */ +TEST(ruleset_fd_transfer) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, + }; + int ruleset_fd_tx, dir_fd; + int socket_fds[2]; + pid_t child; + int status; + + drop_caps(_metadata); + + /* Creates a test ruleset with a simple rule. */ + ruleset_fd_tx = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd_tx); + path_beneath_attr.parent_fd = + open("/tmp", O_PATH | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath_attr.parent_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd_tx, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, 0)); + ASSERT_EQ(0, close(path_beneath_attr.parent_fd)); + + /* Sends the ruleset FD over a socketpair and then close it. */ + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, + socket_fds)); + ASSERT_EQ(0, send_fd(socket_fds[0], ruleset_fd_tx)); + ASSERT_EQ(0, close(socket_fds[0])); + ASSERT_EQ(0, close(ruleset_fd_tx)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const int ruleset_fd_rx = recv_fd(socket_fds[1]); + + ASSERT_LE(0, ruleset_fd_rx); + ASSERT_EQ(0, close(socket_fds[1])); + + /* Enforces the received ruleset on the child. */ + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd_rx, 0)); + ASSERT_EQ(0, close(ruleset_fd_rx)); + + /* Checks that the ruleset enforcement. */ + ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + ASSERT_EQ(EACCES, errno); + dir_fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, dir_fd); + ASSERT_EQ(0, close(dir_fd)); + _exit(_metadata->exit_code); + return; + } + + ASSERT_EQ(0, close(socket_fds[1])); + + /* Checks that the parent is unrestricted. */ + dir_fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, dir_fd); + ASSERT_EQ(0, close(dir_fd)); + dir_fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, dir_fd); + ASSERT_EQ(0, close(dir_fd)); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + +TEST(cred_transfer) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + int ruleset_fd, dir_fd; + pid_t child; + int status; + + drop_caps(_metadata); + + dir_fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + EXPECT_LE(0, dir_fd); + EXPECT_EQ(0, close(dir_fd)); + + /* Denies opening directories. */ + 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, 0)); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Checks ruleset enforcement. */ + EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + EXPECT_EQ(EACCES, errno); + + /* Needed for KEYCTL_SESSION_TO_PARENT permission checks */ + EXPECT_NE(-1, syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL, 0, + 0, 0)) + { + TH_LOG("Failed to join session keyring: %s", strerror(errno)); + } + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + /* Checks ruleset enforcement. */ + EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + EXPECT_EQ(EACCES, errno); + + /* + * KEYCTL_SESSION_TO_PARENT is a no-op unless we have a + * different session keyring in the child, so make that happen. + */ + EXPECT_NE(-1, syscall(__NR_keyctl, KEYCTL_JOIN_SESSION_KEYRING, + NULL, 0, 0, 0)); + + /* + * KEYCTL_SESSION_TO_PARENT installs credentials on the parent + * that never go through the cred_prepare hook, this path uses + * cred_transfer instead. + */ + EXPECT_EQ(0, syscall(__NR_keyctl, KEYCTL_SESSION_TO_PARENT, 0, + 0, 0, 0)); + + /* Re-checks ruleset enforcement. */ + EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + EXPECT_EQ(EACCES, errno); + + _exit(_metadata->exit_code); + return; + } + + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* Re-checks ruleset enforcement. */ + EXPECT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + EXPECT_EQ(EACCES, errno); +} + +TEST(useless_quiet_rule_fs) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = 0, + }; + struct landlock_path_beneath_attr path_beneath_attr = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, + }; + int ruleset_fd, root_fd; + + drop_caps(_metadata); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + root_fd = open("/", O_PATH | O_CLOEXEC); + ASSERT_LE(0, root_fd); + path_beneath_attr.parent_fd = root_fd; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_attr, + LANDLOCK_ADD_RULE_QUIET)); + ASSERT_EQ(EINVAL, errno); + + /* Check that the rule had not been added. */ + ASSERT_EQ(0, close(root_fd)); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(-1, open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)); + ASSERT_EQ(EACCES, errno); +} + +TEST(useless_quiet_rule_net) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = 0, + }; + struct landlock_net_port_attr net_port_attr = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = 1024, + }; + int ruleset_fd; + + drop_caps(_metadata); + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(-1, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &net_port_attr, LANDLOCK_ADD_RULE_QUIET)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST(invalid_quiet_bits_1) +{ + const struct landlock_ruleset_attr ruleset_attr_fs = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + const struct landlock_ruleset_attr ruleset_attr_net = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + const struct landlock_ruleset_attr ruleset_attr_scoped = { + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, + }; + + /* Quiet bit set but not part of the handled mask. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs, + sizeof(ruleset_attr_fs), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net, + sizeof(ruleset_attr_net), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped, + sizeof(ruleset_attr_scoped), 0)); + ASSERT_EQ(EINVAL, errno); +} + +TEST(invalid_quiet_bits_2) +{ + const struct landlock_ruleset_attr ruleset_attr_fs = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .quiet_access_fs = 1ULL << 63, + }; + const struct landlock_ruleset_attr ruleset_attr_net = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + .quiet_access_net = 1ULL << 63, + }; + const struct landlock_ruleset_attr ruleset_attr_scoped = { + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = 1ULL << 63, + }; + + /* Quiet bit outside of the valid access range. */ + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_fs, + sizeof(ruleset_attr_fs), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_net, + sizeof(ruleset_attr_net), 0)); + ASSERT_EQ(EINVAL, errno); + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr_scoped, + sizeof(ruleset_attr_scoped), 0)); + ASSERT_EQ(EINVAL, errno); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h new file mode 100644 index 000000000..7206d5105 --- /dev/null +++ b/tools/testing/selftests/landlock/common.h @@ -0,0 +1,255 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock test helpers + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2021 Microsoft Corporation + */ + +#include <arpa/inet.h> +#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> +#include <unistd.h> + +#include "kselftest_harness.h" +#include "wrappers.h" + +#define TMP_DIR "tmp" + +/* TEST_F_FORK() should not be used for new tests. */ +#define TEST_F_FORK(fixture_name, test_name) TEST_F(fixture_name, test_name) + +#define LANDLOCK_MAX_NUM_LAYERS 16 + +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) +{ + cap_t cap_p; + /* 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 */ + }; + const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED; + + if ((cap_get_secbits() & noroot) != noroot) + EXPECT_EQ(0, cap_set_secbits(noroot)); + + cap_p = cap_get_proc(); + EXPECT_NE(NULL, cap_p); + EXPECT_NE(-1, cap_clear(cap_p)); + if (!drop_all) { + EXPECT_NE(-1, cap_set_flag(cap_p, CAP_PERMITTED, + ARRAY_SIZE(caps), caps, CAP_SET)); + } + + /* Automatically resets ambient capabilities. */ + EXPECT_NE(-1, cap_set_proc(cap_p)) + { + TH_LOG("Failed to set capabilities: %s", strerror(errno)); + } + EXPECT_NE(-1, cap_free(cap_p)); + + /* Quickly checks that ambient capabilities are cleared. */ + EXPECT_NE(-1, cap_get_ambient(caps[0])); +} + +/* We cannot put such helpers in a library because of kselftest_harness.h . */ +static void __maybe_unused disable_caps(struct __test_metadata *const _metadata) +{ + _init_caps(_metadata, false); +} + +static void __maybe_unused drop_caps(struct __test_metadata *const _metadata) +{ + _init_caps(_metadata, true); +} + +static void _change_cap(struct __test_metadata *const _metadata, + const cap_flag_t flag, const cap_value_t cap, + const cap_flag_value_t value) +{ + cap_t cap_p; + + cap_p = cap_get_proc(); + EXPECT_NE(NULL, cap_p); + EXPECT_NE(-1, cap_set_flag(cap_p, flag, 1, &cap, value)); + EXPECT_NE(-1, cap_set_proc(cap_p)) + { + TH_LOG("Failed to set capability %d: %s", cap, strerror(errno)); + } + EXPECT_NE(-1, cap_free(cap_p)); +} + +static void __maybe_unused set_cap(struct __test_metadata *const _metadata, + const cap_value_t cap) +{ + _change_cap(_metadata, CAP_EFFECTIVE, cap, CAP_SET); +} + +static void __maybe_unused clear_cap(struct __test_metadata *const _metadata, + const cap_value_t cap) +{ + _change_cap(_metadata, CAP_EFFECTIVE, cap, CAP_CLEAR); +} + +static void __maybe_unused +set_ambient_cap(struct __test_metadata *const _metadata, const cap_value_t cap) +{ + _change_cap(_metadata, CAP_INHERITABLE, cap, CAP_SET); + + EXPECT_NE(-1, cap_set_ambient(cap, CAP_SET)) + { + TH_LOG("Failed to set ambient capability %d: %s", cap, + strerror(errno)); + } +} + +static void __maybe_unused clear_ambient_cap( + struct __test_metadata *const _metadata, const cap_value_t cap) +{ + EXPECT_EQ(1, cap_get_ambient(cap)); + _change_cap(_metadata, CAP_INHERITABLE, cap, CAP_CLEAR); + EXPECT_EQ(0, cap_get_ambient(cap)); +} + +/* Receives an FD from a UNIX socket. Returns the received FD, or -errno. */ +static int __maybe_unused recv_fd(int usock) +{ + int fd_rx; + union { + /* Aligned ancillary data buffer. */ + char buf[CMSG_SPACE(sizeof(fd_rx))]; + struct cmsghdr _align; + } cmsg_rx = {}; + char data = '\0'; + struct iovec io = { + .iov_base = &data, + .iov_len = sizeof(data), + }; + struct msghdr msg = { + .msg_iov = &io, + .msg_iovlen = 1, + .msg_control = &cmsg_rx.buf, + .msg_controllen = sizeof(cmsg_rx.buf), + }; + struct cmsghdr *cmsg; + int res; + + res = recvmsg(usock, &msg, MSG_CMSG_CLOEXEC); + if (res < 0) + return -errno; + + cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg->cmsg_len != CMSG_LEN(sizeof(fd_rx))) + return -EIO; + + memcpy(&fd_rx, CMSG_DATA(cmsg), sizeof(fd_rx)); + return fd_rx; +} + +/* Sends an FD on a UNIX socket. Returns 0 on success or -errno. */ +static int __maybe_unused send_fd(int usock, int fd_tx) +{ + union { + /* Aligned ancillary data buffer. */ + char buf[CMSG_SPACE(sizeof(fd_tx))]; + struct cmsghdr _align; + } cmsg_tx = {}; + char data_tx = '.'; + struct iovec io = { + .iov_base = &data_tx, + .iov_len = sizeof(data_tx), + }; + struct msghdr msg = { + .msg_iov = &io, + .msg_iovlen = 1, + .msg_control = &cmsg_tx.buf, + .msg_controllen = sizeof(cmsg_tx.buf), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + + cmsg->cmsg_len = CMSG_LEN(sizeof(fd_tx)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + memcpy(CMSG_DATA(cmsg), &fd_tx, sizeof(fd_tx)); + + if (sendmsg(usock, &msg, 0) < 0) + return -errno; + return 0; +} + +static void __maybe_unused +enforce_ruleset(struct __test_metadata *const _metadata, const int ruleset_fd) +{ + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0)) + { + TH_LOG("Failed to enforce ruleset: %s", strerror(errno)); + } +} + +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; + int protocol; +}; + +struct service_fixture { + struct protocol_variant protocol; + /* port is also stored in ipv4_addr.sin_port or ipv6_addr.sin6_port */ + unsigned short port; + union { + struct sockaddr_in ipv4_addr; + struct sockaddr_in6 ipv6_addr; + struct { + struct sockaddr_un unix_addr; + socklen_t unix_addr_len; + }; + struct sockaddr_storage _largest; + }; +}; + +static void __maybe_unused set_unix_address(struct service_fixture *const srv, + const unsigned short index) +{ + srv->unix_addr.sun_family = AF_UNIX; + sprintf(srv->unix_addr.sun_path, + "_selftests-landlock-abstract-unix-tid%d-index%d", sys_gettid(), + index); + srv->unix_addr_len = SUN_LEN(&srv->unix_addr); + srv->unix_addr.sun_path[0] = '\0'; +} diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config new file mode 100644 index 000000000..8fe9b461b --- /dev/null +++ b/tools/testing/selftests/landlock/config @@ -0,0 +1,19 @@ +CONFIG_AF_UNIX_OOB=y +CONFIG_AUDIT=y +CONFIG_CGROUPS=y +CONFIG_CGROUP_SCHED=y +CONFIG_INET=y +CONFIG_IPV6=y +CONFIG_KEYS=y +CONFIG_MPTCP=y +CONFIG_MPTCP_IPV6=y +CONFIG_NET=y +CONFIG_NET_NS=y +CONFIG_OVERLAY_FS=y +CONFIG_PROC_FS=y +CONFIG_SECURITY=y +CONFIG_SECURITY_LANDLOCK=y +CONFIG_SHMEM=y +CONFIG_SYSFS=y +CONFIG_TMPFS=y +CONFIG_TMPFS_XATTR=y diff --git a/tools/testing/selftests/landlock/config.um b/tools/testing/selftests/landlock/config.um new file mode 100644 index 000000000..40937c039 --- /dev/null +++ b/tools/testing/selftests/landlock/config.um @@ -0,0 +1 @@ +CONFIG_HOSTFS=y diff --git a/tools/testing/selftests/landlock/fs_bench.c b/tools/testing/selftests/landlock/fs_bench.c new file mode 100644 index 000000000..d13a88dcd --- /dev/null +++ b/tools/testing/selftests/landlock/fs_bench.c @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock filesystem benchmark + * + * This program benchmarks the time required for file access checks. We use a + * large number (-d flag) of nested directories where each directory inode has + * an associated Landlock rule, and we repeatedly (-n flag) exercise a file + * access for which Landlock has to walk the path all the way up to the root. + * + * With an increasing number of nested subdirectories, Landlock's portion of the + * overall system call time increases, which makes the effects of Landlock + * refactorings more measurable. + * + * This benchmark does *not* measure the building of the Landlock ruleset. The + * time required to add all these rules is not large enough to be easily + * measurable. A separate benchmark tool would be better to test that, and that + * tool could then also use a simpler file system layout. + * + * Copyright © 2026 Google LLC + */ + +#define _GNU_SOURCE +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <linux/landlock.h> +#include <linux/prctl.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/times.h> +#include <time.h> +#include <unistd.h> + +#include "wrappers.h" + +static void usage(const char *const argv0) +{ + printf("Usage:\n"); + printf(" %s [OPTIONS]\n", argv0); + printf("\n"); + printf(" Benchmark expensive Landlock checks for D nested dirs\n"); + printf("\n"); + printf("Options:\n"); + printf(" -h help\n"); + printf(" -L disable Landlock (as a baseline)\n"); + printf(" -d D set directory depth to D\n"); + printf(" -n N set number of benchmark iterations to N\n"); +} + +/* + * Build a deep directory, enforce Landlock and return the FD to the + * deepest dir. On any failure, exit the process with an error. + */ +static int build_directory(size_t depth, const bool use_landlock) +{ + const char *path = "d"; /* directory name */ + int abi, ruleset_fd, curr, prev; + + if (use_landlock) { + abi = landlock_create_ruleset(NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 7) + err(1, "Landlock ABI too low: got %d, wanted 7+", abi); + } + + ruleset_fd = -1; + if (use_landlock) { + struct landlock_ruleset_attr attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + }; + ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0U); + if (ruleset_fd < 0) + err(1, "landlock_create_ruleset"); + } + + curr = open(".", O_PATH); + if (curr < 0) + err(1, "open(.)"); + + while (depth--) { + if (use_landlock) { + struct landlock_path_beneath_attr attr = { + .allowed_access = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .parent_fd = curr, + }; + if (landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_PATH_BENEATH, &attr, + 0) < 0) + err(1, "landlock_add_rule"); + } + + if (mkdirat(curr, path, 0700) < 0) + err(1, "mkdirat(%s)", path); + + prev = curr; + curr = openat(curr, path, O_PATH); + if (curr < 0) + err(1, "openat(%s)", path); + + close(prev); + } + + if (use_landlock) { + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) + err(1, "prctl"); + + if (landlock_restrict_self(ruleset_fd, 0) < 0) + err(1, "landlock_restrict_self"); + } + + close(ruleset_fd); + return curr; +} + +static void remove_recursively(const size_t depth) +{ + const char *path = "d"; /* directory name */ + + int fd = openat(AT_FDCWD, ".", O_PATH); + + if (fd < 0) + err(1, "openat(.)"); + + for (size_t i = 0; i < depth - 1; i++) { + int oldfd = fd; + + fd = openat(fd, path, O_PATH); + if (fd < 0) + err(1, "openat(%s)", path); + close(oldfd); + } + + for (size_t i = 0; i < depth; i++) { + if (unlinkat(fd, path, AT_REMOVEDIR) < 0) + err(1, "unlinkat(%s)", path); + int newfd = openat(fd, "..", O_PATH); + + close(fd); + fd = newfd; + } + close(fd); +} + +int main(int argc, char *argv[]) +{ + bool use_landlock = true; + size_t num_iterations = 100000; + size_t num_subdirs = 10000; + int c, curr, fd; + struct tms start_time, end_time; + + setbuf(stdout, NULL); + while ((c = getopt(argc, argv, "hLd:n:")) != -1) { + switch (c) { + case 'h': + usage(argv[0]); + return EXIT_SUCCESS; + case 'L': + use_landlock = false; + break; + case 'd': + num_subdirs = atoi(optarg); + break; + case 'n': + num_iterations = atoi(optarg); + break; + default: + usage(argv[0]); + return EXIT_FAILURE; + } + } + + printf("*** Benchmark ***\n"); + printf("%zu dirs, %zu iterations, %s Landlock\n", num_subdirs, + num_iterations, use_landlock ? "with" : "without"); + + if (times(&start_time) == -1) + err(1, "times"); + + curr = build_directory(num_subdirs, use_landlock); + + for (int i = 0; i < num_iterations; i++) { + fd = openat(curr, "file.txt", O_CREAT | O_TRUNC | O_WRONLY, + 0600); + if (use_landlock) { + if (fd == 0) + errx(1, "openat succeeded, expected EACCES"); + if (errno != EACCES) + err(1, "openat expected EACCES, but got"); + } + if (fd != -1) + close(fd); + } + + if (times(&end_time) == -1) + err(1, "times"); + + printf("*** Benchmark concluded ***\n"); + printf("System: %ld clocks\n", + end_time.tms_stime - start_time.tms_stime); + printf("User : %ld clocks\n", + end_time.tms_utime - start_time.tms_utime); + printf("Clocks per second: %ld\n", CLOCKS_PER_SEC); + + close(curr); + + remove_recursively(num_subdirs); +} diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c new file mode 100644 index 000000000..86e08aa6e --- /dev/null +++ b/tools/testing/selftests/landlock/fs_test.c @@ -0,0 +1,10192 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Filesystem + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2020 ANSSI + * Copyright © 2020-2022 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <asm/termbits.h> +#include <fcntl.h> +#include <libgen.h> +#include <linux/fiemap.h> +#include <linux/landlock.h> +#include <linux/magic.h> +#include <sched.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <sys/capability.h> +#include <sys/ioctl.h> +#include <sys/mount.h> +#include <sys/prctl.h> +#include <sys/resource.h> +#include <sys/sendfile.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysmacros.h> +#include <sys/un.h> +#include <sys/vfs.h> +#include <unistd.h> + +/* + * Intentionally included last to work around header conflict. + * See https://sourceware.org/glibc/wiki/Synchronizing_Headers. + */ +#include <linux/fs.h> +#include <linux/mount.h> + +/* Defines AT_EXECVE_CHECK without type conflicts. */ +#define _ASM_GENERIC_FCNTL_H +#include <linux/fcntl.h> + +#include "audit.h" +#include "common.h" + +#ifndef renameat2 +int renameat2(int olddirfd, const char *oldpath, int newdirfd, + const char *newpath, unsigned int flags) +{ + return syscall(__NR_renameat2, olddirfd, oldpath, newdirfd, newpath, + flags); +} +#endif + +#ifndef open_tree +int open_tree(int dfd, const char *filename, unsigned int flags) +{ + return syscall(__NR_open_tree, dfd, filename, flags); +} +#endif + +static int sys_execveat(int dirfd, const char *pathname, char *const argv[], + char *const envp[], int flags) +{ + return syscall(__NR_execveat, dirfd, pathname, argv, envp, flags); +} + +#ifndef RENAME_EXCHANGE +#define RENAME_EXCHANGE (1 << 1) +#endif + +static const char bin_true[] = "./true"; + +/* Paths (sibling number and depth) */ +static const char dir_s1d1[] = TMP_DIR "/s1d1"; +static const char file1_s1d1[] = TMP_DIR "/s1d1/f1"; +static const char file2_s1d1[] = TMP_DIR "/s1d1/f2"; +static const char dir_s1d2[] = TMP_DIR "/s1d1/s1d2"; +static const char file1_s1d2[] = TMP_DIR "/s1d1/s1d2/f1"; +static const char file2_s1d2[] = TMP_DIR "/s1d1/s1d2/f2"; +static const char dir_s1d3[] = TMP_DIR "/s1d1/s1d2/s1d3"; +static const char file1_s1d3[] = TMP_DIR "/s1d1/s1d2/s1d3/f1"; +static const char file2_s1d3[] = TMP_DIR "/s1d1/s1d2/s1d3/f2"; + +static const char dir_s2d1[] = TMP_DIR "/s2d1"; +static const char file1_s2d1[] = TMP_DIR "/s2d1/f1"; +static const char dir_s2d2[] = TMP_DIR "/s2d1/s2d2"; +static const char file1_s2d2[] = TMP_DIR "/s2d1/s2d2/f1"; +static const char dir_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3"; +static const char file1_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f1"; +static const char file2_s2d3[] = TMP_DIR "/s2d1/s2d2/s2d3/f2"; + +static const char dir_s3d1[] = TMP_DIR "/s3d1"; +static const char file1_s3d1[] = TMP_DIR "/s3d1/f1"; +/* dir_s3d2 is a mount point. */ +static const char dir_s3d2[] = TMP_DIR "/s3d1/s3d2"; +static const char dir_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3"; +static const char file1_s3d3[] = TMP_DIR "/s3d1/s3d2/s3d3/f1"; +static const char dir_s3d4[] = TMP_DIR "/s3d1/s3d2/s3d4"; +static const char file1_s3d4[] = TMP_DIR "/s3d1/s3d2/s3d4/f1"; + +/* + * layout1 hierarchy: + * + * tmp + * ├── s1d1 + * │ ├── f1 + * │ ├── f2 + * │ └── s1d2 + * │ ├── f1 + * │ ├── f2 + * │ └── s1d3 + * │ ├── f1 + * │ └── f2 + * ├── s2d1 + * │ ├── f1 + * │ └── s2d2 + * │ ├── f1 + * │ └── s2d3 + * │ ├── f1 + * │ └── f2 + * └── s3d1 + * ├── f1 + * └── s3d2 [mount point] + * ├── s3d3 + * │ └── f1 + * └── s3d4 + * └── f1 + */ + +static bool fgrep(FILE *const inf, const char *const str) +{ + char line[32]; + const int slen = strlen(str); + + while (!feof(inf)) { + if (!fgets(line, sizeof(line), inf)) + break; + if (strncmp(line, str, slen)) + continue; + + return true; + } + + return false; +} + +static bool supports_filesystem(const char *const filesystem) +{ + char str[32]; + int len; + bool res = true; + FILE *const inf = fopen("/proc/filesystems", "r"); + + /* + * Consider that the filesystem is supported if we cannot get the + * supported ones. + */ + if (!inf) + return true; + + /* filesystem can be null for bind mounts. */ + if (!filesystem) + goto out; + + len = snprintf(str, sizeof(str), "nodev\t%s\n", filesystem); + if (len >= sizeof(str)) + /* Ignores too-long filesystem names. */ + goto out; + + res = fgrep(inf, str); + +out: + fclose(inf); + return res; +} + +static bool cwd_matches_fs(unsigned int fs_magic) +{ + struct statfs statfs_buf; + + if (!fs_magic) + return true; + + if (statfs(".", &statfs_buf)) + return true; + + return statfs_buf.f_type == fs_magic; +} + +static void mkdir_parents(struct __test_metadata *const _metadata, + const char *const path) +{ + char *walker; + const char *parent; + int i, err; + + ASSERT_NE(path[0], '\0'); + walker = strdup(path); + ASSERT_NE(NULL, walker); + parent = walker; + for (i = 1; walker[i]; i++) { + if (walker[i] != '/') + continue; + walker[i] = '\0'; + err = mkdir(parent, 0700); + ASSERT_FALSE(err && errno != EEXIST) + { + TH_LOG("Failed to create directory \"%s\": %s", parent, + strerror(errno)); + } + walker[i] = '/'; + } + free(walker); +} + +static void create_directory(struct __test_metadata *const _metadata, + const char *const path) +{ + mkdir_parents(_metadata, path); + ASSERT_EQ(0, mkdir(path, 0700)) + { + TH_LOG("Failed to create directory \"%s\": %s", path, + strerror(errno)); + } +} + +static void create_file(struct __test_metadata *const _metadata, + const char *const path) +{ + mkdir_parents(_metadata, path); + ASSERT_EQ(0, mknod(path, S_IFREG | 0700, 0)) + { + TH_LOG("Failed to create file \"%s\": %s", path, + strerror(errno)); + } +} + +static int remove_path(const char *const path) +{ + char *walker; + int i, ret, err = 0; + + walker = strdup(path); + if (!walker) { + err = ENOMEM; + goto out; + } + if (unlink(path) && rmdir(path)) { + if (errno != ENOENT && errno != ENOTDIR) + err = errno; + goto out; + } + for (i = strlen(walker); i > 0; i--) { + if (walker[i] != '/') + continue; + walker[i] = '\0'; + ret = rmdir(walker); + if (ret) { + if (errno != ENOTEMPTY && errno != EBUSY) + err = errno; + goto out; + } + if (strcmp(walker, TMP_DIR) == 0) + goto out; + } + +out: + free(walker); + return err; +} + +struct mnt_opt { + const char *const source; + const char *const type; + const unsigned long flags; + const char *const data; +}; + +#define MNT_TMP_DATA "size=4m,mode=700" + +static const struct mnt_opt mnt_tmp = { + .type = "tmpfs", + .data = MNT_TMP_DATA, +}; + +static int mount_opt(const struct mnt_opt *const mnt, const char *const target) +{ + return mount(mnt->source ?: mnt->type, target, mnt->type, mnt->flags, + mnt->data); +} + +static void prepare_layout_opt(struct __test_metadata *const _metadata, + const struct mnt_opt *const mnt) +{ + disable_caps(_metadata); + umask(0077); + create_directory(_metadata, TMP_DIR); + + /* + * Do not pollute the rest of the system: creates a private mount point + * for tests relying on pivot_root(2) and move_mount(2). + */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNS | CLONE_NEWCGROUP)); + ASSERT_EQ(0, mount_opt(mnt, TMP_DIR)) + { + TH_LOG("Failed to mount the %s filesystem: %s", mnt->type, + strerror(errno)); + /* + * FIXTURE_TEARDOWN() is not called when FIXTURE_SETUP() + * failed, so we need to explicitly do a minimal cleanup to + * avoid cascading errors with other tests that don't depend on + * the same filesystem. + */ + remove_path(TMP_DIR); + } + ASSERT_EQ(0, mount(NULL, TMP_DIR, NULL, MS_PRIVATE | MS_REC, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +static void prepare_layout(struct __test_metadata *const _metadata) +{ + prepare_layout_opt(_metadata, &mnt_tmp); +} + +static void cleanup_layout(struct __test_metadata *const _metadata) +{ + set_cap(_metadata, CAP_SYS_ADMIN); + if (umount(TMP_DIR)) { + /* + * According to the test environment, the mount point of the + * current directory may be shared or not, which changes the + * visibility of the nested TMP_DIR mount point for the test's + * parent process doing this cleanup. + */ + ASSERT_EQ(EINVAL, errno); + } + clear_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(0, remove_path(TMP_DIR)); +} + +/* clang-format off */ +FIXTURE(layout0) {}; +/* clang-format on */ + +FIXTURE_SETUP(layout0) +{ + prepare_layout(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(layout0) +{ + cleanup_layout(_metadata); +} + +static void create_layout1(struct __test_metadata *const _metadata) +{ + create_file(_metadata, file1_s1d1); + create_file(_metadata, file1_s1d2); + create_file(_metadata, file1_s1d3); + create_file(_metadata, file2_s1d1); + create_file(_metadata, file2_s1d2); + create_file(_metadata, file2_s1d3); + + create_file(_metadata, file1_s2d1); + create_file(_metadata, file1_s2d2); + create_file(_metadata, file1_s2d3); + create_file(_metadata, file2_s2d3); + + create_file(_metadata, file1_s3d1); + create_directory(_metadata, dir_s3d2); + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount_opt(&mnt_tmp, dir_s3d2)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + create_file(_metadata, file1_s3d3); + create_file(_metadata, file1_s3d4); +} + +static void remove_layout1(struct __test_metadata *const _metadata) +{ + EXPECT_EQ(0, remove_path(file2_s1d3)); + EXPECT_EQ(0, remove_path(file2_s1d2)); + EXPECT_EQ(0, remove_path(file2_s1d1)); + EXPECT_EQ(0, remove_path(file1_s1d3)); + EXPECT_EQ(0, remove_path(file1_s1d2)); + EXPECT_EQ(0, remove_path(file1_s1d1)); + EXPECT_EQ(0, remove_path(dir_s1d3)); + + EXPECT_EQ(0, remove_path(file2_s2d3)); + EXPECT_EQ(0, remove_path(file1_s2d3)); + EXPECT_EQ(0, remove_path(file1_s2d2)); + EXPECT_EQ(0, remove_path(file1_s2d1)); + EXPECT_EQ(0, remove_path(dir_s2d2)); + + EXPECT_EQ(0, remove_path(file1_s3d1)); + EXPECT_EQ(0, remove_path(file1_s3d3)); + EXPECT_EQ(0, remove_path(file1_s3d4)); + set_cap(_metadata, CAP_SYS_ADMIN); + umount(dir_s3d2); + clear_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(0, remove_path(dir_s3d2)); +} + +/* clang-format off */ +FIXTURE(layout1) {}; +/* clang-format on */ + +FIXTURE_SETUP(layout1) +{ + prepare_layout(_metadata); + + create_layout1(_metadata); +} + +FIXTURE_TEARDOWN_PARENT(layout1) +{ + remove_layout1(_metadata); + + cleanup_layout(_metadata); +} + +/* + * This helper enables to use the ASSERT_* macros and print the line number + * pointing to the test caller. + */ +static int test_open_rel(const int dirfd, const char *const path, + const int flags) +{ + int fd; + + /* Works with file and directories. */ + fd = openat(dirfd, path, flags | O_CLOEXEC); + if (fd < 0) + return errno; + /* + * Mixing error codes from close(2) and open(2) should not lead to any + * (access type) confusion for this test. + */ + if (close(fd) != 0) + return errno; + return 0; +} + +static int test_open(const char *const path, const int flags) +{ + return test_open_rel(AT_FDCWD, path, flags); +} + +TEST_F_FORK(layout1, no_restriction) +{ + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +TEST_F_FORK(layout1, inval) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + .parent_fd = -1, + }; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + int ruleset_fd; + + path_beneath.parent_fd = + open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + ruleset_fd = open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + /* Returns EBADF because ruleset_fd is not a landlock-ruleset FD. */ + ASSERT_EQ(EBADF, errno); + ASSERT_EQ(0, close(ruleset_fd)); + + ruleset_fd = open(dir_s1d1, O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + /* Returns EBADFD because ruleset_fd is not a valid ruleset. */ + ASSERT_EQ(EBADFD, errno); + ASSERT_EQ(0, close(ruleset_fd)); + + /* Gets a real ruleset. */ + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(0, close(path_beneath.parent_fd)); + + /* Tests without O_PATH. */ + path_beneath.parent_fd = open(dir_s1d2, 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)); + + /* Tests with a ruleset FD. */ + path_beneath.parent_fd = ruleset_fd; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EBADFD, errno); + + /* Checks unhandled allowed_access. */ + path_beneath.parent_fd = + open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + /* Test with legitimate values. */ + path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_EXECUTE; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EINVAL, errno); + path_beneath.allowed_access &= ~LANDLOCK_ACCESS_FS_EXECUTE; + + /* Tests with denied-by-default access right. */ + path_beneath.allowed_access |= LANDLOCK_ACCESS_FS_REFER; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EINVAL, errno); + path_beneath.allowed_access &= ~LANDLOCK_ACCESS_FS_REFER; + + /* Test with unknown (64-bits) value. */ + path_beneath.allowed_access |= (1ULL << 60); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EINVAL, errno); + path_beneath.allowed_access &= ~(1ULL << 60); + + /* Test with no access. */ + path_beneath.allowed_access = 0; + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(ENOMSG, errno); + path_beneath.allowed_access &= ~(1ULL << 60); + + ASSERT_EQ(0, close(path_beneath.parent_fd)); + + /* Enforces the ruleset. */ + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + + ASSERT_EQ(0, close(ruleset_fd)); +} + +/* clang-format off */ + +#define ACCESS_FILE ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_TRUNCATE | \ + LANDLOCK_ACCESS_FS_IOCTL_DEV | \ + LANDLOCK_ACCESS_FS_RESOLVE_UNIX) + +#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX + +#define ACCESS_ALL ( \ + ACCESS_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) + +/* clang-format on */ + +TEST_F_FORK(layout1, file_and_dir_access_rights) +{ + __u64 access; + int err; + struct landlock_path_beneath_attr path_beneath_file = {}, + path_beneath_dir = {}; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = ACCESS_ALL, + }; + const int ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + + /* Tests access rights for files. */ + path_beneath_file.parent_fd = open(file1_s1d2, O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath_file.parent_fd); + + /* Tests access rights for directories. */ + path_beneath_dir.parent_fd = + open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath_dir.parent_fd); + + for (access = 1; access <= ACCESS_LAST; access <<= 1) { + path_beneath_dir.allowed_access = access; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_dir, 0)); + + path_beneath_file.allowed_access = access; + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath_file, 0); + if (access & ACCESS_FILE) { + ASSERT_EQ(0, err); + } else { + ASSERT_EQ(-1, err); + ASSERT_EQ(EINVAL, errno); + } + } + ASSERT_EQ(0, close(path_beneath_file.parent_fd)); + ASSERT_EQ(0, close(path_beneath_dir.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout0, ruleset_with_unknown_access) +{ + __u64 access_mask; + + for (access_mask = 1ULL << 63; access_mask != ACCESS_LAST; + access_mask >>= 1) { + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = access_mask, + }; + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0)); + ASSERT_EQ(EINVAL, errno); + } +} + +TEST_F_FORK(layout0, rule_with_unknown_access) +{ + __u64 access; + struct landlock_path_beneath_attr path_beneath = {}; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = ACCESS_ALL, + }; + const int ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd); + + path_beneath.parent_fd = + open(TMP_DIR, O_PATH | O_DIRECTORY | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + for (access = 1ULL << 63; access != ACCESS_LAST; access >>= 1) { + path_beneath.allowed_access = access; + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + EXPECT_EQ(EINVAL, errno); + } + ASSERT_EQ(0, close(path_beneath.parent_fd)); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout1, rule_with_unhandled_access) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE, + }; + struct landlock_path_beneath_attr path_beneath = {}; + int ruleset_fd; + __u64 access; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + path_beneath.parent_fd = open(file1_s1d2, O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + + for (access = 1; access > 0; access <<= 1) { + int err; + + path_beneath.allowed_access = access; + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0); + if (access == ruleset_attr.handled_access_fs) { + EXPECT_EQ(0, err); + } else { + EXPECT_EQ(-1, err); + EXPECT_EQ(EINVAL, errno); + } + } + + EXPECT_EQ(0, close(path_beneath.parent_fd)); + EXPECT_EQ(0, close(ruleset_fd)); +} + +static void add_path_beneath(struct __test_metadata *const _metadata, + const int ruleset_fd, const __u64 allowed_access, + const char *const path, __u32 flags) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = allowed_access, + }; + + path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd) + { + TH_LOG("Failed to open directory \"%s\": %s", path, + strerror(errno)); + } + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, flags)) + { + TH_LOG("Failed to update the ruleset with \"%s\": %s", path, + strerror(errno)); + } + ASSERT_EQ(0, close(path_beneath.parent_fd)); +} + +struct rule { + const char *path; + __u64 access; +}; + +/* clang-format off */ + +#define ACCESS_RO ( \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_READ_DIR) + +#define ACCESS_RW ( \ + ACCESS_RO | \ + LANDLOCK_ACCESS_FS_WRITE_FILE) + +/* clang-format on */ + +static int create_ruleset(struct __test_metadata *const _metadata, + const __u64 handled_access_fs, + const struct rule rules[]) +{ + int ruleset_fd, i; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = handled_access_fs, + }; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd) + { + TH_LOG("Failed to create a ruleset: %s", strerror(errno)); + } + + if (rules) + for (i = 0; rules[i].path; i++) { + if (!rules[i].access) + continue; + + add_path_beneath(_metadata, ruleset_fd, rules[i].access, + rules[i].path, 0); + } + return ruleset_fd; +} + +static void enforce_fs(struct __test_metadata *const _metadata, + const __u64 access_fs, const struct rule rules[]) +{ + const int ruleset_fd = create_ruleset(_metadata, access_fs, rules); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout0, proc_nsfs) +{ + const struct rule rules[] = { + { + .path = "/dev/null", + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + struct landlock_path_beneath_attr path_beneath; + const int ruleset_fd = create_ruleset( + _metadata, rules[0].access | LANDLOCK_ACCESS_FS_READ_DIR, + rules); + + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, test_open("/proc/self/ns/mnt", O_RDONLY)); + + enforce_ruleset(_metadata, ruleset_fd); + + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/dev", O_RDONLY)); + ASSERT_EQ(0, test_open("/dev/null", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/dev/full", O_RDONLY)); + + ASSERT_EQ(EACCES, test_open("/proc", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/proc/self", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/proc/self/ns", O_RDONLY)); + /* + * Because nsfs is an internal filesystem, /proc/self/ns/mnt is a + * disconnected path. Such path cannot be identified and must then be + * allowed. + */ + ASSERT_EQ(0, test_open("/proc/self/ns/mnt", O_RDONLY)); + + /* + * Checks that it is not possible to add nsfs-like filesystem + * references to a ruleset. + */ + path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + path_beneath.parent_fd = open("/proc/self/ns/mnt", O_PATH | O_CLOEXEC); + ASSERT_LE(0, path_beneath.parent_fd); + ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &path_beneath, 0)); + ASSERT_EQ(EBADFD, errno); + ASSERT_EQ(0, close(path_beneath.parent_fd)); +} + +TEST_F_FORK(layout0, unpriv) +{ + const struct rule rules[] = { + { + .path = TMP_DIR, + .access = ACCESS_RO, + }, + {}, + }; + int ruleset_fd; + + drop_caps(_metadata); + + ruleset_fd = create_ruleset(_metadata, ACCESS_RO, rules); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, 0)); + ASSERT_EQ(EPERM, errno); + + /* enforce_ruleset() calls prctl(no_new_privs). */ + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout1, effective_access) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + { + .path = file1_s2d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + char buf; + int reg_fd; + + enforce_fs(_metadata, ACCESS_RW, rules); + + /* Tests on a directory (with or without O_PATH). */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(0, test_open("/", O_RDONLY | O_PATH)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY | O_PATH)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY | O_PATH)); + + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + /* Tests on a file (with or without O_PATH). */ + ASSERT_EQ(EACCES, test_open(dir_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY | O_PATH)); + + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + + /* Checks effective read and write actions. */ + reg_fd = open(file1_s2d2, O_RDWR | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(1, write(reg_fd, ".", 1)); + ASSERT_LE(0, lseek(reg_fd, 0, SEEK_SET)); + ASSERT_EQ(1, read(reg_fd, &buf, 1)); + ASSERT_EQ('.', buf); + ASSERT_EQ(0, close(reg_fd)); + + /* Just in case, double-checks effective actions. */ + reg_fd = open(file1_s2d2, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(-1, write(reg_fd, &buf, 1)); + ASSERT_EQ(EBADF, errno); + ASSERT_EQ(0, close(reg_fd)); +} + +TEST_F_FORK(layout1, unhandled_access) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + {}, + }; + + /* Here, we only handle read accesses, not write accesses. */ + enforce_fs(_metadata, ACCESS_RO, rules); + + /* + * Because the policy does not handle LANDLOCK_ACCESS_FS_WRITE_FILE, + * opening for write-only should be allowed, but not read-write. + */ + ASSERT_EQ(0, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + + ASSERT_EQ(0, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); +} + +TEST_F_FORK(layout1, ruleset_overlap) +{ + const struct rule rules[] = { + /* These rules should be ORed among them. */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + /* Checks s1d1 hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d2 hierarchy. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d3 hierarchy. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); +} + +TEST_F_FORK(layout1, layer_rule_unions) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + /* dir_s1d3 should allow READ_FILE and WRITE_FILE (O_RDWR). */ + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + const struct rule layer2[] = { + /* Doesn't change anything from layer1. */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + const struct rule layer3[] = { + /* Only allows write (but not read) to dir_s1d3. */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, layer1); + + /* Checks s1d1 hierarchy with layer1. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d2 hierarchy with layer1. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d3 hierarchy with layer1. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_WRONLY)); + /* dir_s1d3 should allow READ_FILE and WRITE_FILE (O_RDWR). */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Doesn't change anything from layer1. */ + enforce_fs(_metadata, ACCESS_RW, layer2); + + /* Checks s1d1 hierarchy with layer2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d2 hierarchy with layer2. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d3 hierarchy with layer2. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_WRONLY)); + /* dir_s1d3 should allow READ_FILE and WRITE_FILE (O_RDWR). */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Only allows write (but not read) to dir_s1d3. */ + enforce_fs(_metadata, ACCESS_RW, layer3); + + /* Checks s1d1 hierarchy with layer3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d2 hierarchy with layer3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Checks s1d3 hierarchy with layer3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_WRONLY)); + /* dir_s1d3 should now deny READ_FILE and WRITE_FILE (O_RDWR). */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); +} + +TEST_F_FORK(layout1, non_overlapping_accesses) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {}, + }; + const struct rule layer2[] = { + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, layer1); + + ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, mknod(file1_s1d2, S_IFREG | 0700, 0)); + ASSERT_EQ(0, unlink(file1_s1d2)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REMOVE_FILE, layer2); + + /* Unchanged accesses for file creation. */ + ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, mknod(file1_s1d2, S_IFREG | 0700, 0)); + + /* Checks file removing. */ + ASSERT_EQ(-1, unlink(file1_s1d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d3)); +} + +TEST_F_FORK(layout1, interleaved_masked_accesses) +{ + /* + * Checks overly restrictive rules: + * layer 1: allows R s1d1/s1d2/s1d3/file1 + * layer 2: allows RW s1d1/s1d2/s1d3 + * allows W s1d1/s1d2 + * denies R s1d1/s1d2 + * layer 3: allows R s1d1 + * layer 4: allows R s1d1/s1d2 + * denies W s1d1/s1d2 + * layer 5: allows R s1d1/s1d2 + * layer 6: allows X ---- + * layer 7: allows W s1d1/s1d2 + * denies R s1d1/s1d2 + */ + const struct rule layer1_read[] = { + /* Allows read access to file1_s1d3 with the first layer. */ + { + .path = file1_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + /* First rule with write restrictions. */ + const struct rule layer2_read_write[] = { + /* Start by granting read-write access via its parent directory... */ + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + /* ...but also denies read access via its grandparent directory. */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + const struct rule layer3_read[] = { + /* Allows read access via its great-grandparent directory. */ + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + const struct rule layer4_read_write[] = { + /* + * Try to confuse the deny access by denying write (but not + * read) access via its grandparent directory. + */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + const struct rule layer5_read[] = { + /* + * Try to override layer2's deny read access by explicitly + * allowing read access via file1_s1d3's grandparent. + */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + const struct rule layer6_execute[] = { + /* + * Restricts an unrelated file hierarchy with a new access + * (non-overlapping) type. + */ + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {}, + }; + const struct rule layer7_read_write[] = { + /* + * Finally, denies read access to file1_s1d3 via its + * grandparent. + */ + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer1_read); + + /* Checks that read access is granted for file1_s1d3 with layer 1. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, + layer2_read_write); + + /* Checks that previous access rights are unchanged with layer 2. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer3_read); + + /* Checks that previous access rights are unchanged with layer 3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY)); + + /* This time, denies write access for the file hierarchy. */ + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, + layer4_read_write); + + /* + * Checks that the only change with layer 4 is that write access is + * denied. + */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer5_read); + + /* Checks that previous access rights are unchanged with layer 5. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, layer6_execute); + + /* Checks that previous access rights are unchanged with layer 6. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, + layer7_read_write); + + /* Checks read access is now denied with layer 7. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY)); +} + +TEST_F_FORK(layout1, inherit_subset) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + }, + {}, + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + enforce_ruleset(_metadata, ruleset_fd); + + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* Write access is forbidden. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Write access is forbidden. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Tests shared rule extension: the following rules should not grant + * any new access, only remove some. Once enforced, these rules are + * ANDed with the previous ones. + */ + add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, + dir_s1d2, 0); + /* + * According to ruleset_fd, dir_s1d2 should now have the + * LANDLOCK_ACCESS_FS_READ_FILE and LANDLOCK_ACCESS_FS_WRITE_FILE + * access rights (even if this directory is opened a second time). + * However, when enforcing this updated ruleset, the ruleset tied to + * the current process (i.e. its domain) will still only have the + * dir_s1d2 with LANDLOCK_ACCESS_FS_READ_FILE and + * LANDLOCK_ACCESS_FS_READ_DIR accesses, but + * LANDLOCK_ACCESS_FS_WRITE_FILE must not be allowed because it would + * be a privilege escalation. + */ + enforce_ruleset(_metadata, ruleset_fd); + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Try to get more privileges by adding new access rights to the parent + * directory: dir_s1d1. + */ + add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, dir_s1d1, 0); + enforce_ruleset(_metadata, ruleset_fd); + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* + * Now, dir_s1d3 get a new rule tied to it, only allowing + * LANDLOCK_ACCESS_FS_WRITE_FILE. The (kernel internal) difference is + * that there was no rule tied to it before. + */ + add_path_beneath(_metadata, ruleset_fd, LANDLOCK_ACCESS_FS_WRITE_FILE, + dir_s1d3, 0); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + /* + * Same tests and results as above, except for open(dir_s1d3) which is + * now denied because the new rule mask the rule previously inherited + * from dir_s1d2. + */ + + /* Same tests and results as above. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d2. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + /* Readdir access is still allowed. */ + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* It is still forbidden to write in file1_s1d3. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + /* + * Readdir of dir_s1d3 is still allowed because of the OR policy inside + * the same layer. + */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); +} + +TEST_F_FORK(layout1, inherit_superset) +{ + const struct rule rules[] = { + { + .path = dir_s1d3, + .access = ACCESS_RO, + }, + {}, + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + enforce_ruleset(_metadata, ruleset_fd); + + /* Readdir access is denied for dir_s1d2. */ + ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + /* Readdir access is allowed for dir_s1d3. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + /* File access is allowed for file1_s1d3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + /* Now dir_s1d2, parent of dir_s1d3, gets a new rule tied to it. */ + add_path_beneath(_metadata, ruleset_fd, + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + dir_s1d2, 0); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Readdir access is still denied for dir_s1d2. */ + ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + /* Readdir access is still allowed for dir_s1d3. */ + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + /* File access is still allowed for file1_s1d3. */ + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); +} + +TEST_F_FORK(layout0, max_layers) +{ + int i, err; + const struct rule rules[] = { + { + .path = TMP_DIR, + .access = ACCESS_RO, + }, + {}, + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + for (i = 0; i < LANDLOCK_MAX_NUM_LAYERS; i++) + enforce_ruleset(_metadata, ruleset_fd); + + for (i = 0; i < 2; i++) { + err = landlock_restrict_self(ruleset_fd, 0); + ASSERT_EQ(-1, err); + ASSERT_EQ(E2BIG, errno); + } + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout1, empty_or_same_ruleset) +{ + struct landlock_ruleset_attr ruleset_attr = {}; + int ruleset_fd; + + /* Tests empty handled_access_fs. */ + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(-1, ruleset_fd); + ASSERT_EQ(ENOMSG, errno); + + /* Enforces policy which denies read access to all files. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL); + + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + /* Nests a policy which denies read access to all directories. */ + ruleset_fd = + create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, NULL); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); + + /* Enforces a second time with the same ruleset. */ + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout1, rule_on_mountpoint) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + /* dir_s3d2 is a mount point. */ + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +TEST_F_FORK(layout1, rule_over_mountpoint) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + /* dir_s3d2 is a mount point. */ + .path = dir_s3d1, + .access = ACCESS_RO, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s3d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s3d3, O_RDONLY)); +} + +/* + * This test verifies that we can apply a landlock rule on the root directory + * (which might require special handling). + */ +TEST_F_FORK(layout1, rule_over_root_allow_then_deny) +{ + struct rule rules[] = { + { + .path = "/", + .access = ACCESS_RO, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + /* Checks allowed access. */ + ASSERT_EQ(0, test_open("/", O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + + rules[0].access = LANDLOCK_ACCESS_FS_READ_FILE; + enforce_fs(_metadata, ACCESS_RW, rules); + + /* Checks denied access (on a directory). */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); +} + +TEST_F_FORK(layout1, rule_over_root_deny) +{ + const struct rule rules[] = { + { + .path = "/", + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + /* Checks denied access (on a directory). */ + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY)); +} + +TEST_F_FORK(layout1, rule_inside_mount_ns) +{ + const struct rule rules[] = { + { + .path = "s3d3", + .access = ACCESS_RO, + }, + {}, + }; + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3)) + { + TH_LOG("Failed to pivot root: %s", strerror(errno)); + }; + ASSERT_EQ(0, chdir("/")); + clear_cap(_metadata, CAP_SYS_ADMIN); + + enforce_fs(_metadata, ACCESS_RW, rules); + + ASSERT_EQ(0, test_open("s3d3", O_RDONLY)); + ASSERT_EQ(EACCES, test_open("/", O_RDONLY)); +} + +TEST_F_FORK(layout1, mount_and_pivot) +{ + const struct rule rules[] = { + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {}, + }; + + enforce_fs(_metadata, ACCESS_RW, rules); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3)); + ASSERT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +TEST_F_FORK(layout1, move_mount) +{ + const struct rule rules[] = { + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + {}, + }; + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD, + dir_s1d2, 0)) + { + TH_LOG("Failed to move mount: %s", strerror(errno)); + } + + ASSERT_EQ(0, syscall(__NR_move_mount, AT_FDCWD, dir_s1d2, AT_FDCWD, + dir_s3d2, 0)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + enforce_fs(_metadata, ACCESS_RW, rules); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(-1, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD, + dir_s1d2, 0)); + ASSERT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +TEST_F_FORK(layout1, topology_changes_with_net_only) +{ + const struct landlock_ruleset_attr ruleset_net = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + + /* Add network restrictions. */ + drop_access_rights(_metadata, &ruleset_net); + + /* Mount, remount, move_mount, umount, and pivot_root checks. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount_opt(&mnt_tmp, dir_s1d2)); + ASSERT_EQ(0, mount(NULL, dir_s1d2, NULL, MS_PRIVATE | MS_REC, NULL)); + ASSERT_EQ(0, syscall(__NR_move_mount, AT_FDCWD, dir_s1d2, AT_FDCWD, + dir_s2d2, 0)); + ASSERT_EQ(0, umount(dir_s2d2)); + ASSERT_EQ(0, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3)); + ASSERT_EQ(0, chdir("/")); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +TEST_F_FORK(layout1, topology_changes_with_net_and_fs) +{ + const struct landlock_ruleset_attr ruleset_net_fs = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + .handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE, + }; + + /* Add network and filesystem restrictions. */ + drop_access_rights(_metadata, &ruleset_net_fs); + + /* Mount, remount, move_mount, umount, and pivot_root checks. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(-1, mount_opt(&mnt_tmp, dir_s1d2)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_PRIVATE | MS_REC, NULL)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD, + dir_s2d2, 0)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, umount(dir_s3d2)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(-1, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3)); + ASSERT_EQ(EPERM, errno); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +TEST_F_FORK(layout1, release_inodes) +{ + const struct rule rules[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + .path = dir_s3d2, + .access = ACCESS_RO, + }, + { + .path = dir_s3d3, + .access = ACCESS_RO, + }, + {}, + }; + const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules); + + /* Unmount a file hierarchy while it is being used by a ruleset. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, umount(dir_s3d2)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(dir_s3d2, O_RDONLY)); + /* This dir_s3d3 would not be allowed and does not exist anyway. */ + ASSERT_EQ(ENOENT, test_open(dir_s3d3, O_RDONLY)); +} + +/* + * This test checks that a rule on a directory used as a mount point does not + * grant access to the mount covering it. It is a generalization of the bind + * mount case in layout3_fs.hostfs.release_inodes that tests hidden mount points. + */ +TEST_F_FORK(layout1, covered_rule) +{ + const struct rule layer1[] = { + { + .path = dir_s3d2, + .access = LANDLOCK_ACCESS_FS_READ_DIR, + }, + {}, + }; + int ruleset_fd; + + /* Unmount to simplify FIXTURE_TEARDOWN. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, umount(dir_s3d2)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + /* Creates a ruleset with the future hidden directory. */ + ruleset_fd = + create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1); + + /* Covers with a new mount point. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount_opt(&mnt_tmp, dir_s3d2)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + ASSERT_EQ(0, test_open(dir_s3d2, O_RDONLY)); + + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + /* Checks that access to the new mount point is denied. */ + ASSERT_EQ(EACCES, test_open(dir_s3d2, O_RDONLY)); +} + +enum relative_access { + REL_OPEN, + REL_CHDIR, + REL_CHROOT_ONLY, + REL_CHROOT_CHDIR, +}; + +static void test_relative_path(struct __test_metadata *const _metadata, + const enum relative_access rel) +{ + /* + * Common layer to check that chroot doesn't ignore it (i.e. a chroot + * is not a disconnected root directory). + */ + const struct rule layer1_base[] = { + { + .path = TMP_DIR, + .access = ACCESS_RO, + }, + {}, + }; + const struct rule layer2_subs[] = { + { + .path = dir_s1d2, + .access = ACCESS_RO, + }, + { + .path = dir_s2d2, + .access = ACCESS_RO, + }, + {}, + }; + int dirfd, ruleset_fd; + + enforce_fs(_metadata, ACCESS_RW, layer1_base); + + ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_subs); + + ASSERT_LE(0, ruleset_fd); + switch (rel) { + case REL_OPEN: + case REL_CHDIR: + break; + case REL_CHROOT_ONLY: + ASSERT_EQ(0, chdir(dir_s2d2)); + break; + case REL_CHROOT_CHDIR: + ASSERT_EQ(0, chdir(dir_s1d2)); + break; + default: + ASSERT_TRUE(false); + return; + } + + set_cap(_metadata, CAP_SYS_CHROOT); + enforce_ruleset(_metadata, ruleset_fd); + + switch (rel) { + case REL_OPEN: + dirfd = open(dir_s1d2, O_DIRECTORY); + ASSERT_LE(0, dirfd); + break; + case REL_CHDIR: + ASSERT_EQ(0, chdir(dir_s1d2)); + dirfd = AT_FDCWD; + break; + case REL_CHROOT_ONLY: + /* Do chroot into dir_s1d2 (relative to dir_s2d2). */ + ASSERT_EQ(0, chroot("../../s1d1/s1d2")) + { + TH_LOG("Failed to chroot: %s", strerror(errno)); + } + dirfd = AT_FDCWD; + break; + case REL_CHROOT_CHDIR: + /* Do chroot into dir_s1d2. */ + ASSERT_EQ(0, chroot(".")) + { + TH_LOG("Failed to chroot: %s", strerror(errno)); + } + dirfd = AT_FDCWD; + break; + } + + ASSERT_EQ((rel == REL_CHROOT_CHDIR) ? 0 : EACCES, + test_open_rel(dirfd, "..", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, ".", O_RDONLY)); + + if (rel == REL_CHROOT_ONLY) { + /* The current directory is dir_s2d2. */ + ASSERT_EQ(0, test_open_rel(dirfd, "./s2d3", O_RDONLY)); + } else { + /* The current directory is dir_s1d2. */ + ASSERT_EQ(0, test_open_rel(dirfd, "./s1d3", O_RDONLY)); + } + + if (rel == REL_CHROOT_ONLY || rel == REL_CHROOT_CHDIR) { + /* Checks the root dir_s1d2. */ + ASSERT_EQ(0, test_open_rel(dirfd, "/..", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "/", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "/f1", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "/s1d3", O_RDONLY)); + } + + if (rel != REL_CHROOT_CHDIR) { + ASSERT_EQ(EACCES, test_open_rel(dirfd, "../../s1d1", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s1d1/s1d2", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s1d1/s1d2/s1d3", + O_RDONLY)); + + ASSERT_EQ(EACCES, test_open_rel(dirfd, "../../s2d1", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s2d1/s2d2", O_RDONLY)); + ASSERT_EQ(0, test_open_rel(dirfd, "../../s2d1/s2d2/s2d3", + O_RDONLY)); + } + + if (rel == REL_OPEN) + ASSERT_EQ(0, close(dirfd)); + ASSERT_EQ(0, close(ruleset_fd)); +} + +TEST_F_FORK(layout1, relative_open) +{ + test_relative_path(_metadata, REL_OPEN); +} + +TEST_F_FORK(layout1, relative_chdir) +{ + test_relative_path(_metadata, REL_CHDIR); +} + +TEST_F_FORK(layout1, relative_chroot_only) +{ + test_relative_path(_metadata, REL_CHROOT_ONLY); +} + +TEST_F_FORK(layout1, relative_chroot_chdir) +{ + test_relative_path(_metadata, REL_CHROOT_CHDIR); +} + +static void copy_file(struct __test_metadata *const _metadata, + const char *const src_path, const char *const dst_path) +{ + int dst_fd, src_fd; + struct stat statbuf; + + dst_fd = open(dst_path, O_WRONLY | O_TRUNC | O_CLOEXEC); + ASSERT_LE(0, dst_fd) + { + TH_LOG("Failed to open \"%s\": %s", dst_path, strerror(errno)); + } + src_fd = open(src_path, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, src_fd) + { + TH_LOG("Failed to open \"%s\": %s", src_path, strerror(errno)); + } + ASSERT_EQ(0, fstat(src_fd, &statbuf)); + ASSERT_EQ(statbuf.st_size, + sendfile(dst_fd, src_fd, 0, statbuf.st_size)); + ASSERT_EQ(0, close(src_fd)); + ASSERT_EQ(0, close(dst_fd)); +} + +static void test_execute(struct __test_metadata *const _metadata, const int err, + const char *const path) +{ + int status; + char *const argv[] = { (char *)path, NULL }; + const pid_t child = fork(); + + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_EQ(err ? -1 : 0, execve(path, argv, NULL)) + { + TH_LOG("Failed to execute \"%s\": %s", path, + strerror(errno)); + }; + ASSERT_EQ(err, errno); + _exit(__test_passed(_metadata) ? 2 : 1); + return; + } + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(err ? 2 : 0, WEXITSTATUS(status)) + { + TH_LOG("Unexpected return code for \"%s\"", path); + }; +} + +static void test_check_exec(struct __test_metadata *const _metadata, + const int err, const char *const path) +{ + int ret; + char *const argv[] = { (char *)path, NULL }; + + ret = sys_execveat(AT_FDCWD, path, argv, NULL, + AT_EMPTY_PATH | AT_EXECVE_CHECK); + if (err) { + EXPECT_EQ(-1, ret); + EXPECT_EQ(errno, err); + } else { + EXPECT_EQ(0, ret); + } +} + +TEST_F_FORK(layout1, execute) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {}, + }; + + copy_file(_metadata, bin_true, file1_s1d1); + copy_file(_metadata, bin_true, file1_s1d2); + copy_file(_metadata, bin_true, file1_s1d3); + + /* Checks before file1_s1d1 being denied. */ + test_execute(_metadata, 0, file1_s1d1); + test_check_exec(_metadata, 0, file1_s1d1); + + enforce_fs(_metadata, rules[0].access, rules); + + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + test_execute(_metadata, EACCES, file1_s1d1); + test_check_exec(_metadata, EACCES, file1_s1d1); + + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + test_execute(_metadata, 0, file1_s1d2); + test_check_exec(_metadata, 0, file1_s1d2); + + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + test_execute(_metadata, 0, file1_s1d3); + test_check_exec(_metadata, 0, file1_s1d3); +} + +TEST_F_FORK(layout1, umount_sandboxer) +{ + int pipe_child[2], pipe_parent[2]; + char buf_parent; + pid_t child; + int status; + + copy_file(_metadata, bin_sandbox_and_launch, file1_s3d3); + 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 *)file1_s3d3, + (char *)bin_wait_pipe, pipe_child_str, + pipe_parent_str, NULL }; + + /* Passes the pipe FDs to the executed binary and its child. */ + 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]); + + /* + * We need bin_sandbox_and_launch (copied inside the mount as + * file1_s3d3) to execute bin_wait_pipe (outside the mount) to + * make sure the mount point will not be EBUSY because of + * file1_s3d3 being in use. This avoids a potential race + * condition between the following read() and umount() calls. + */ + 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 to sandbox itself. */ + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the sandboxer is tied to its mount point. */ + set_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(-1, umount(dir_s3d2)); + EXPECT_EQ(EBUSY, errno); + clear_cap(_metadata, CAP_SYS_ADMIN); + + /* Signals the child to launch a grandchild. */ + EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* Waits for the grandchild. */ + EXPECT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + + /* Tests that the domain's sandboxer is not tied to its mount point. */ + set_cap(_metadata, CAP_SYS_ADMIN); + EXPECT_EQ(0, umount(dir_s3d2)) + { + TH_LOG("Failed to umount \"%s\": %s", dir_s3d2, + strerror(errno)); + }; + clear_cap(_metadata, CAP_SYS_ADMIN); + + /* Signals the grandchild 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)); +} + +TEST_F_FORK(layout1, link) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {}, + }; + const struct rule layer2[] = { + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + enforce_fs(_metadata, layer1[0].access, layer1); + + ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + + /* Denies linking because of reparenting. */ + ASSERT_EQ(-1, link(file1_s2d1, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, link(file2_s1d2, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, link(file2_s1d3, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, link(file2_s1d2, file1_s1d2)); + ASSERT_EQ(0, link(file2_s1d3, file1_s1d3)); + + /* Prepares for next unlinks. */ + ASSERT_EQ(0, unlink(file2_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_fs(_metadata, layer2[0].access, layer2); + + /* Checks that linkind doesn't require the ability to delete a file. */ + ASSERT_EQ(0, link(file1_s1d2, file2_s1d2)); + ASSERT_EQ(0, link(file1_s1d3, file2_s1d3)); +} + +static int test_rename(const char *const oldpath, const char *const newpath) +{ + if (rename(oldpath, newpath)) + return errno; + return 0; +} + +static int test_exchange(const char *const oldpath, const char *const newpath) +{ + if (renameat2(AT_FDCWD, oldpath, AT_FDCWD, newpath, RENAME_EXCHANGE)) + return errno; + return 0; +} + +static int test_renameat(int olddirfd, const char *oldpath, int newdirfd, + const char *newpath) +{ + if (renameat2(olddirfd, oldpath, newdirfd, newpath, 0)) + return errno; + return 0; +} + +static int test_exchangeat(int olddirfd, const char *oldpath, int newdirfd, + const char *newpath) +{ + if (renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_EXCHANGE)) + return errno; + return 0; +} + +TEST_F_FORK(layout1, rename_file) +{ + const struct rule rules[] = { + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d2)); + + enforce_fs(_metadata, rules[0].access, rules); + + /* + * Tries to replace a file, from a directory that allows file removal, + * but to a different directory (which also allows file removal). + */ + ASSERT_EQ(-1, rename(file1_s2d3, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d3, AT_FDCWD, file1_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d3, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + + /* + * Tries to replace a file, from a directory that denies file removal, + * to a different directory (which allows file removal). + */ + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d1, AT_FDCWD, file1_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d2, AT_FDCWD, file1_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + + /* Exchanges files and directories that partially allow removal. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d2, AT_FDCWD, file1_s2d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + /* Checks that file1_s2d1 cannot be removed (instead of ENOTDIR). */ + ASSERT_EQ(-1, rename(dir_s2d2, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d1, AT_FDCWD, dir_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + /* Checks that file1_s1d1 cannot be removed (instead of EISDIR). */ + ASSERT_EQ(-1, rename(file1_s1d1, dir_s1d2)); + ASSERT_EQ(EACCES, errno); + + /* Renames files with different parents. */ + ASSERT_EQ(-1, rename(file1_s2d2, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EACCES, errno); + + /* Exchanges and renames files with same parent. */ + ASSERT_EQ(0, renameat2(AT_FDCWD, file2_s2d3, AT_FDCWD, file1_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(0, rename(file2_s2d3, file1_s2d3)); + + /* Exchanges files and directories with same parent, twice. */ + ASSERT_EQ(0, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(0, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); +} + +TEST_F_FORK(layout1, rename_dir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + {}, + }; + + /* Empties dir_s1d3 to allow renaming. */ + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_fs(_metadata, rules[0].access, rules); + + /* Exchanges and renames directory to a different parent. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(dir_s2d3, dir_s1d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + + /* + * Exchanges directory to the same parent, which doesn't allow + * directory removal. + */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s1d1, AT_FDCWD, dir_s2d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + /* Checks that dir_s1d2 cannot be removed (instead of ENOTDIR). */ + ASSERT_EQ(-1, rename(dir_s1d2, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, dir_s1d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + /* Checks that dir_s1d2 cannot be removed (instead of EISDIR). */ + ASSERT_EQ(-1, rename(file1_s1d1, dir_s1d2)); + ASSERT_EQ(EACCES, errno); + + /* + * Exchanges and renames directory to the same parent, which allows + * directory removal. + */ + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_s1d3, AT_FDCWD, file1_s1d2, + RENAME_EXCHANGE)); + ASSERT_EQ(0, unlink(dir_s1d3)); + ASSERT_EQ(0, mkdir(dir_s1d3, 0700)); + ASSERT_EQ(0, rename(file1_s1d2, dir_s1d3)); + ASSERT_EQ(0, rmdir(dir_s1d3)); +} + +TEST_F_FORK(layout1, reparent_refer) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + {}, + }; + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1); + + ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d1)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d2)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d3)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(-1, rename(dir_s1d3, dir_s2d1)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(dir_s1d3, dir_s2d2)); + ASSERT_EQ(EXDEV, errno); + /* + * Moving should only be allowed when the source and the destination + * parent directory have REFER. + */ + ASSERT_EQ(-1, rename(dir_s1d3, dir_s2d3)); + ASSERT_EQ(ENOTEMPTY, errno); + ASSERT_EQ(0, unlink(file1_s2d3)); + ASSERT_EQ(0, unlink(file2_s2d3)); + ASSERT_EQ(0, rename(dir_s1d3, dir_s2d3)); +} + +/* Checks renames beneath dir_s1d1. */ +static void refer_denied_by_default(struct __test_metadata *const _metadata, + const struct rule layer1[], + const int layer1_err, + const struct rule layer2[]) +{ + ASSERT_EQ(0, unlink(file1_s1d2)); + + enforce_fs(_metadata, layer1[0].access, layer1); + + /* + * If the first layer handles LANDLOCK_ACCESS_FS_REFER (according to + * layer1_err), then it allows some different-parent renames and links. + */ + ASSERT_EQ(layer1_err, test_rename(file1_s1d1, file1_s1d2)); + if (layer1_err == 0) + ASSERT_EQ(layer1_err, test_rename(file1_s1d2, file1_s1d1)); + ASSERT_EQ(layer1_err, test_exchange(file2_s1d1, file2_s1d2)); + ASSERT_EQ(layer1_err, test_exchange(file2_s1d2, file2_s1d1)); + + enforce_fs(_metadata, layer2[0].access, layer2); + + /* + * Now, either the first or the second layer does not handle + * LANDLOCK_ACCESS_FS_REFER, which means that any different-parent + * renames and links are denied, thus making the layer handling + * LANDLOCK_ACCESS_FS_REFER null and void. + */ + ASSERT_EQ(EXDEV, test_rename(file1_s1d1, file1_s1d2)); + ASSERT_EQ(EXDEV, test_exchange(file2_s1d1, file2_s1d2)); + ASSERT_EQ(EXDEV, test_exchange(file2_s1d2, file2_s1d1)); +} + +const struct rule layer_dir_s1d1_refer[] = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + {}, +}; + +const struct rule layer_dir_s1d1_execute[] = { + { + /* Matches a parent directory. */ + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {}, +}; + +const struct rule layer_dir_s2d1_execute[] = { + { + /* Does not match a parent directory. */ + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {}, +}; + +/* + * Tests precedence over renames: denied by default for different parent + * directories, *with* a rule matching a parent directory, but not directly + * denying access (with MAKE_REG nor REMOVE). + */ +TEST_F_FORK(layout1, refer_denied_by_default1) +{ + refer_denied_by_default(_metadata, layer_dir_s1d1_refer, 0, + layer_dir_s1d1_execute); +} + +/* + * Same test but this time turning around the ABI version order: the first + * layer does not handle LANDLOCK_ACCESS_FS_REFER. + */ +TEST_F_FORK(layout1, refer_denied_by_default2) +{ + refer_denied_by_default(_metadata, layer_dir_s1d1_execute, EXDEV, + layer_dir_s1d1_refer); +} + +/* + * Tests precedence over renames: denied by default for different parent + * directories, *without* a rule matching a parent directory, but not directly + * denying access (with MAKE_REG nor REMOVE). + */ +TEST_F_FORK(layout1, refer_denied_by_default3) +{ + refer_denied_by_default(_metadata, layer_dir_s1d1_refer, 0, + layer_dir_s2d1_execute); +} + +/* + * Same test but this time turning around the ABI version order: the first + * layer does not handle LANDLOCK_ACCESS_FS_REFER. + */ +TEST_F_FORK(layout1, refer_denied_by_default4) +{ + refer_denied_by_default(_metadata, layer_dir_s2d1_execute, EXDEV, + layer_dir_s1d1_refer); +} + +/* + * Tests walking through a denied root mount. + */ +TEST_F_FORK(layout1, refer_mount_root_deny) +{ + int root_fd; + + /* Creates a mount object from a non-mount point. */ + set_cap(_metadata, CAP_SYS_ADMIN); + root_fd = + open_tree(AT_FDCWD, dir_s1d1, + AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + clear_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_LE(0, root_fd); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, NULL); + + /* Link denied by Landlock: EACCES. */ + EXPECT_EQ(-1, linkat(root_fd, ".", root_fd, "does_not_exist", 0)); + EXPECT_EQ(EACCES, errno); + + /* renameat2() always returns EBUSY. */ + EXPECT_EQ(-1, renameat2(root_fd, ".", root_fd, "does_not_exist", 0)); + EXPECT_EQ(EBUSY, errno); + + EXPECT_EQ(0, close(root_fd)); +} + +TEST_F_FORK(layout1, refer_part_mount_tree_is_allowed) +{ + const struct rule layer1[] = { + { + /* Parent mount point. */ + .path = dir_s3d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_REG, + }, + { + /* + * Removing the source file is allowed because its + * access rights are already a superset of the + * destination. + */ + .path = dir_s3d4, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s3d3)); + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + layer1); + + ASSERT_EQ(0, rename(file1_s3d4, file1_s3d3)); +} + +TEST_F_FORK(layout1, reparent_link) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d3, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {}, + }; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, + layer1); + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + /* Denies linking because of missing MAKE_REG. */ + ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + /* Denies linking because of missing source and destination REFER. */ + ASSERT_EQ(-1, link(file1_s2d1, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + /* Denies linking because of missing source REFER. */ + ASSERT_EQ(-1, link(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + + /* Denies linking because of missing MAKE_REG. */ + ASSERT_EQ(-1, link(file1_s2d2, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + /* Denies linking because of missing destination REFER. */ + ASSERT_EQ(-1, link(file1_s2d2, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + /* Allows linking because of REFER and MAKE_REG. */ + ASSERT_EQ(0, link(file1_s2d2, file1_s1d3)); + ASSERT_EQ(0, unlink(file1_s2d2)); + /* Reverse linking denied because of missing MAKE_REG. */ + ASSERT_EQ(-1, link(file1_s1d3, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s2d3)); + /* Checks reverse linking. */ + ASSERT_EQ(0, link(file1_s1d3, file1_s2d3)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + /* + * This is OK for a file link, but it should not be allowed for a + * directory rename (because of the superset of access rights. + */ + ASSERT_EQ(0, link(file1_s2d3, file1_s1d3)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + ASSERT_EQ(-1, link(file2_s1d2, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, link(file2_s1d3, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, link(file2_s1d2, file1_s1d2)); + ASSERT_EQ(0, link(file2_s1d3, file1_s1d3)); +} + +TEST_F_FORK(layout1, reparent_rename) +{ + /* Same rules as for reparent_link. */ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d3, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {}, + }; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, + layer1); + + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + /* Denies renaming because of missing MAKE_REG. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file2_s1d1, AT_FDCWD, file1_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(-1, rename(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + /* Even denies same file exchange. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file2_s1d1, AT_FDCWD, file2_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Denies renaming because of missing source and destination REFER. */ + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + /* + * Denies renaming because of missing MAKE_REG, source and destination + * REFER. + */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d1, AT_FDCWD, file2_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file2_s1d1, AT_FDCWD, file1_s2d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Denies renaming because of missing source REFER. */ + ASSERT_EQ(-1, rename(file1_s2d1, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + /* Denies renaming because of missing MAKE_REG. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d1, AT_FDCWD, file2_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Denies renaming because of missing MAKE_REG. */ + ASSERT_EQ(-1, rename(file1_s2d2, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + /* Denies renaming because of missing destination REFER*/ + ASSERT_EQ(-1, rename(file1_s2d2, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + /* Denies exchange because of one missing MAKE_REG. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, file2_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + /* Allows renaming because of REFER and MAKE_REG. */ + ASSERT_EQ(0, rename(file1_s2d2, file1_s1d3)); + + /* Reverse renaming denied because of missing MAKE_REG. */ + ASSERT_EQ(-1, rename(file1_s1d3, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s2d3)); + ASSERT_EQ(0, rename(file1_s1d3, file1_s2d3)); + + /* Tests reverse renaming. */ + ASSERT_EQ(0, rename(file1_s2d3, file1_s1d3)); + ASSERT_EQ(0, renameat2(AT_FDCWD, file2_s2d3, AT_FDCWD, file1_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(0, rename(file1_s1d3, file1_s2d3)); + + /* + * This is OK for a file rename, but it should not be allowed for a + * directory rename (because of the superset of access rights). + */ + ASSERT_EQ(0, rename(file1_s2d3, file1_s1d3)); + ASSERT_EQ(0, rename(file1_s1d3, file1_s2d3)); + + /* + * Tests superset restrictions applied to directories. Not only the + * dir_s2d3's parent (dir_s2d2) should be taken into account but also + * access rights tied to dir_s2d3. dir_s2d2 is missing one access right + * compared to dir_s1d3/file1_s1d3 (MAKE_REG) but it is provided + * directly by the moved dir_s2d3. + */ + ASSERT_EQ(0, rename(dir_s2d3, file1_s1d3)); + ASSERT_EQ(0, rename(file1_s1d3, dir_s2d3)); + /* + * The first rename is allowed but not the exchange because dir_s1d3's + * parent (dir_s1d2) doesn't have REFER. + */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d3, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s1d3, AT_FDCWD, file1_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(file1_s2d3, dir_s1d3)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(-1, rename(file2_s1d2, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(file2_s1d3, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + /* Renaming in the same directory is always allowed. */ + ASSERT_EQ(0, rename(file2_s1d2, file1_s1d2)); + ASSERT_EQ(0, rename(file2_s1d3, file1_s1d3)); + + ASSERT_EQ(0, unlink(file1_s1d2)); + /* Denies because of missing source MAKE_REG and destination REFER. */ + ASSERT_EQ(-1, rename(dir_s2d3, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, unlink(file1_s1d3)); + /* Denies because of missing source MAKE_REG and REFER. */ + ASSERT_EQ(-1, rename(dir_s2d2, file1_s1d3)); + ASSERT_EQ(EXDEV, errno); +} + +static void +reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + /* Interesting for the layer2 tests. */ + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = dir_s2d3, + .access = LANDLOCK_ACCESS_FS_MAKE_REG, + }, + {}, + }; + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, + layer1); +} + +static void +reparent_exdev_layers_enforce2(struct __test_metadata *const _metadata) +{ + const struct rule layer2[] = { + { + .path = dir_s2d3, + .access = LANDLOCK_ACCESS_FS_MAKE_DIR, + }, + {}, + }; + /* + * Same checks as before but with a second layer and a new MAKE_DIR + * rule (and no explicit handling of REFER). + */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, layer2); +} + +TEST_F_FORK(layout1, reparent_exdev_layers_rename1) +{ + ASSERT_EQ(0, unlink(file1_s2d2)); + ASSERT_EQ(0, unlink(file1_s2d3)); + + reparent_exdev_layers_enforce1(_metadata); + + /* + * Moving the dir_s1d3 directory below dir_s2d2 is allowed by Landlock + * because it doesn't inherit new access rights. + */ + ASSERT_EQ(0, rename(dir_s1d3, file1_s2d2)); + ASSERT_EQ(0, rename(file1_s2d2, dir_s1d3)); + + /* + * Moving the dir_s1d3 directory below dir_s2d3 is allowed, even if it + * gets a new inherited access rights (MAKE_REG), because MAKE_REG is + * already allowed for dir_s1d3. + */ + ASSERT_EQ(0, rename(dir_s1d3, file1_s2d3)); + ASSERT_EQ(0, rename(file1_s2d3, dir_s1d3)); + + /* + * However, moving the file1_s1d3 file below dir_s2d3 is allowed + * because it cannot inherit MAKE_REG right (which is dedicated to + * directories). + */ + ASSERT_EQ(0, rename(file1_s1d3, file1_s2d3)); + + reparent_exdev_layers_enforce2(_metadata); + + /* + * Moving the dir_s1d3 directory below dir_s2d2 is now denied because + * MAKE_DIR is not tied to dir_s2d2. + */ + ASSERT_EQ(-1, rename(dir_s1d3, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + + /* + * Moving the dir_s1d3 directory below dir_s2d3 is forbidden because it + * would grants MAKE_REG and MAKE_DIR rights to it. + */ + ASSERT_EQ(-1, rename(dir_s1d3, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + + /* + * Moving the file2_s1d3 file below dir_s2d3 is denied because the + * second layer does not handle REFER, which is always denied by + * default. + */ + ASSERT_EQ(-1, rename(file2_s1d3, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); +} + +TEST_F_FORK(layout1, reparent_exdev_layers_rename2) +{ + reparent_exdev_layers_enforce1(_metadata); + + /* Checks EACCES predominance over EXDEV. */ + ASSERT_EQ(-1, rename(file1_s1d1, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file1_s1d2, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file1_s1d1, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + /* Modify layout! */ + ASSERT_EQ(0, rename(file1_s1d2, file1_s2d3)); + + /* Without REFER source. */ + ASSERT_EQ(-1, rename(dir_s1d1, file1_s2d2)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, rename(dir_s1d2, file1_s2d2)); + ASSERT_EQ(EXDEV, errno); + + reparent_exdev_layers_enforce2(_metadata); + + /* Checks EACCES predominance over EXDEV. */ + ASSERT_EQ(-1, rename(file1_s1d1, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + /* Checks with actual file2_s1d2. */ + ASSERT_EQ(-1, rename(file2_s1d2, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file1_s1d1, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + /* + * Modifying the layout is now denied because the second layer does not + * handle REFER, which is always denied by default. + */ + ASSERT_EQ(-1, rename(file2_s1d2, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + + /* Without REFER source, EACCES wins over EXDEV. */ + ASSERT_EQ(-1, rename(dir_s1d1, file1_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(dir_s1d2, file1_s2d2)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F_FORK(layout1, reparent_exdev_layers_exchange1) +{ + const char *const dir_file1_s1d2 = file1_s1d2, *const dir_file2_s2d3 = + file2_s2d3; + + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, mkdir(file1_s1d2, 0700)); + ASSERT_EQ(0, unlink(file2_s2d3)); + ASSERT_EQ(0, mkdir(file2_s2d3, 0700)); + + reparent_exdev_layers_enforce1(_metadata); + + /* Error predominance with file exchange: returns EXDEV and EACCES. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d3, AT_FDCWD, file1_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* + * Checks with directories which creation could be allowed, but denied + * because of access rights that would be inherited. + */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file1_s1d2, AT_FDCWD, + dir_file2_s2d3, RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, + dir_file1_s1d2, RENAME_EXCHANGE)); + ASSERT_EQ(EXDEV, errno); + + /* Checks with same access rights. */ + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + + /* Checks with different (child-only) access rights. */ + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_file1_s1d2, + RENAME_EXCHANGE)); + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_file1_s1d2, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + + /* + * Checks that exchange between file and directory are consistent. + * + * Moving a file (file1_s2d2) to a directory which only grants more + * directory-related access rights is allowed, and at the same time + * moving a directory (dir_file2_s2d3) to another directory which + * grants less access rights is allowed too. + * + * See layout1.reparent_exdev_layers_exchange3 for inverted arguments. + */ + ASSERT_EQ(0, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_file2_s2d3, + RENAME_EXCHANGE)); + /* + * However, moving back the directory is denied because it would get + * more access rights than the current state and because file creation + * is forbidden (in dir_s2d2). + */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, file1_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_file2_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + reparent_exdev_layers_enforce2(_metadata); + + /* Error predominance with file exchange: returns EXDEV and EACCES. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d3, AT_FDCWD, file1_s1d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Checks with directories which creation is now denied. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file1_s1d2, AT_FDCWD, + dir_file2_s2d3, RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, + dir_file1_s1d2, RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Checks with different (child-only) access rights. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + /* Denied because of MAKE_DIR. */ + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Checks with different (child-only) access rights. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_file1_s1d2, + RENAME_EXCHANGE)); + /* Denied because of MAKE_DIR. */ + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file1_s1d2, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* See layout1.reparent_exdev_layers_exchange2 for complement. */ +} + +TEST_F_FORK(layout1, reparent_exdev_layers_exchange2) +{ + const char *const dir_file2_s2d3 = file2_s2d3; + + ASSERT_EQ(0, unlink(file2_s2d3)); + ASSERT_EQ(0, mkdir(file2_s2d3, 0700)); + + reparent_exdev_layers_enforce1(_metadata); + reparent_exdev_layers_enforce2(_metadata); + + /* Checks that exchange between file and directory are consistent. */ + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_file2_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, file1_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F_FORK(layout1, reparent_exdev_layers_exchange3) +{ + const char *const dir_file2_s2d3 = file2_s2d3; + + ASSERT_EQ(0, unlink(file2_s2d3)); + ASSERT_EQ(0, mkdir(file2_s2d3, 0700)); + + reparent_exdev_layers_enforce1(_metadata); + + /* + * Checks that exchange between file and directory are consistent, + * including with inverted arguments (see + * layout1.reparent_exdev_layers_exchange1). + */ + ASSERT_EQ(0, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, file1_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_file2_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_file2_s2d3, AT_FDCWD, file1_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F_FORK(layout1, reparent_remove) +{ + const struct rule layer1[] = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + layer1); + + /* Access denied because of wrong/swapped remove file/dir. */ + ASSERT_EQ(-1, rename(file1_s1d1, dir_s2d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(dir_s2d2, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, dir_s2d2, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, dir_s2d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + /* Access allowed thanks to the matching rights. */ + ASSERT_EQ(-1, rename(file1_s2d1, dir_s1d2)); + ASSERT_EQ(EISDIR, errno); + ASSERT_EQ(-1, rename(dir_s1d2, file1_s2d1)); + ASSERT_EQ(ENOTDIR, errno); + ASSERT_EQ(-1, rename(dir_s1d3, file1_s2d1)); + ASSERT_EQ(ENOTDIR, errno); + ASSERT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, rename(dir_s1d3, file1_s2d1)); + + /* Effectively removes a file and a directory by exchanging them. */ + ASSERT_EQ(0, mkdir(dir_s1d3, 0700)); + ASSERT_EQ(0, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s2d2, AT_FDCWD, dir_s1d3, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F_FORK(layout1, reparent_dom_superset) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = file1_s1d2, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_EXECUTE, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_SOCK, + }, + { + .path = dir_s2d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_FIFO, + }, + {}, + }; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_FIFO, + layer1); + + ASSERT_EQ(-1, rename(file1_s1d2, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + /* + * Moving file1_s1d2 beneath dir_s2d3 would grant it the READ_FILE + * access right. + */ + ASSERT_EQ(-1, rename(file1_s1d2, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + /* + * Moving file1_s1d2 should be allowed even if dir_s2d2 grants a + * superset of access rights compared to dir_s1d2, because file1_s1d2 + * already has these access rights anyway. + */ + ASSERT_EQ(0, rename(file1_s1d2, file1_s2d2)); + ASSERT_EQ(0, rename(file1_s2d2, file1_s1d2)); + + ASSERT_EQ(-1, rename(dir_s1d3, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + /* + * Moving dir_s1d3 beneath dir_s2d3 would grant it the MAKE_FIFO access + * right. + */ + ASSERT_EQ(-1, rename(dir_s1d3, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + /* + * Moving dir_s1d3 should be allowed even if dir_s2d2 grants a superset + * of access rights compared to dir_s1d2, because dir_s1d3 already has + * these access rights anyway. + */ + ASSERT_EQ(0, rename(dir_s1d3, file1_s2d2)); + ASSERT_EQ(0, rename(file1_s2d2, dir_s1d3)); + + /* + * Moving file1_s2d3 beneath dir_s1d2 is allowed, but moving it back + * will be denied because the new inherited access rights from dir_s1d2 + * will be less than the destination (original) dir_s2d3. This is a + * sinkhole scenario where we cannot move back files or directories. + */ + ASSERT_EQ(0, rename(file1_s2d3, file2_s1d2)); + ASSERT_EQ(-1, rename(file2_s1d2, file1_s2d3)); + ASSERT_EQ(EXDEV, errno); + ASSERT_EQ(0, unlink(file2_s1d2)); + ASSERT_EQ(0, unlink(file2_s2d3)); + /* + * Checks similar directory one-way move: dir_s2d3 loses EXECUTE and + * MAKE_SOCK which were inherited from dir_s1d3. + */ + ASSERT_EQ(0, rename(dir_s2d3, file2_s1d2)); + ASSERT_EQ(-1, rename(file2_s1d2, dir_s2d3)); + ASSERT_EQ(EXDEV, errno); +} + +TEST_F_FORK(layout1, remove_dir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_fs(_metadata, rules[0].access, rules); + + ASSERT_EQ(0, rmdir(dir_s1d3)); + ASSERT_EQ(0, mkdir(dir_s1d3, 0700)); + ASSERT_EQ(0, unlinkat(AT_FDCWD, dir_s1d3, AT_REMOVEDIR)); + + /* dir_s1d2 itself cannot be removed. */ + ASSERT_EQ(-1, rmdir(dir_s1d2)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, unlinkat(AT_FDCWD, dir_s1d2, AT_REMOVEDIR)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rmdir(dir_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, unlinkat(AT_FDCWD, dir_s1d1, AT_REMOVEDIR)); + ASSERT_EQ(EACCES, errno); +} + +TEST_F_FORK(layout1, remove_file) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {}, + }; + + enforce_fs(_metadata, rules[0].access, rules); + + ASSERT_EQ(-1, unlink(file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, unlinkat(AT_FDCWD, file1_s1d1, 0)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlinkat(AT_FDCWD, file1_s1d3, 0)); +} + +static void test_make_file(struct __test_metadata *const _metadata, + const __u64 access, const mode_t mode, + const dev_t dev) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = access, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file2_s1d1)); + ASSERT_EQ(0, mknod(file2_s1d1, mode | 0400, dev)) + { + TH_LOG("Failed to make file \"%s\": %s", file2_s1d1, + strerror(errno)); + }; + + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d2)); + + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_fs(_metadata, access, rules); + + ASSERT_EQ(-1, mknod(file1_s1d1, mode | 0400, dev)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, mknod(file1_s1d2, mode | 0400, dev)) + { + TH_LOG("Failed to make file \"%s\": %s", file1_s1d2, + strerror(errno)); + }; + ASSERT_EQ(0, link(file1_s1d2, file2_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d2)); + ASSERT_EQ(0, rename(file1_s1d2, file2_s1d2)); + + ASSERT_EQ(0, mknod(file1_s1d3, mode | 0400, dev)); + ASSERT_EQ(0, link(file1_s1d3, file2_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, rename(file1_s1d3, file2_s1d3)); +} + +TEST_F_FORK(layout1, make_char) +{ + /* Creates a /dev/null device. */ + set_cap(_metadata, CAP_MKNOD); + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_CHAR, S_IFCHR, + makedev(1, 3)); +} + +TEST_F_FORK(layout1, make_block) +{ + /* Creates a /dev/loop0 device. */ + set_cap(_metadata, CAP_MKNOD); + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_BLOCK, S_IFBLK, + makedev(7, 0)); +} + +TEST_F_FORK(layout1, make_reg_1) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, S_IFREG, 0); +} + +TEST_F_FORK(layout1, make_reg_2) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, 0, 0); +} + +TEST_F_FORK(layout1, make_sock) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_SOCK, S_IFSOCK, 0); +} + +TEST_F_FORK(layout1, make_fifo) +{ + test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_FIFO, S_IFIFO, 0); +} + +TEST_F_FORK(layout1, make_sym) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_SYM, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file2_s1d1)); + ASSERT_EQ(0, symlink("none", file2_s1d1)); + + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d2)); + + ASSERT_EQ(0, unlink(file1_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + enforce_fs(_metadata, rules[0].access, rules); + + ASSERT_EQ(-1, symlink("none", file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(-1, rename(file2_s1d1, file1_s1d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, symlink("none", file1_s1d2)); + ASSERT_EQ(0, link(file1_s1d2, file2_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d2)); + ASSERT_EQ(0, rename(file1_s1d2, file2_s1d2)); + + ASSERT_EQ(0, symlink("none", file1_s1d3)); + ASSERT_EQ(0, link(file1_s1d3, file2_s1d3)); + ASSERT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, rename(file1_s1d3, file2_s1d3)); +} + +TEST_F_FORK(layout1, make_dir) +{ + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_MAKE_DIR, + }, + {}, + }; + + ASSERT_EQ(0, unlink(file1_s1d1)); + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file1_s1d3)); + + enforce_fs(_metadata, rules[0].access, rules); + + /* Uses file_* as directory names. */ + ASSERT_EQ(-1, mkdir(file1_s1d1, 0700)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, mkdir(file1_s1d2, 0700)); + ASSERT_EQ(0, mkdir(file1_s1d3, 0700)); +} + +static int open_proc_fd(struct __test_metadata *const _metadata, const int fd, + const int open_flags) +{ + static const char path_template[] = "/proc/self/fd/%d"; + char procfd_path[sizeof(path_template) + 10]; + const int procfd_path_size = + snprintf(procfd_path, sizeof(procfd_path), path_template, fd); + + ASSERT_LT(procfd_path_size, sizeof(procfd_path)); + return open(procfd_path, open_flags); +} + +TEST_F_FORK(layout1, proc_unlinked_file) +{ + const struct rule rules[] = { + { + .path = file1_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + int reg_fd, proc_fd; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, + rules); + + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + reg_fd = open(file1_s1d2, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, reg_fd); + ASSERT_EQ(0, unlink(file1_s1d2)); + + proc_fd = open_proc_fd(_metadata, reg_fd, O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + ASSERT_EQ(0, close(proc_fd)); + + proc_fd = open_proc_fd(_metadata, reg_fd, O_RDWR | O_CLOEXEC); + ASSERT_EQ(-1, proc_fd) + { + TH_LOG("Successfully opened /proc/self/fd/%d: %s", reg_fd, + strerror(errno)); + } + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, close(reg_fd)); +} + +TEST_F_FORK(layout1, proc_pipe) +{ + int proc_fd; + int pipe_fds[2]; + char buf = '\0'; + const struct rule rules[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + + /* Limits read and write access to files tied to the filesystem. */ + enforce_fs(_metadata, rules[0].access, rules); + + /* Checks enforcement for normal files. */ + ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDWR)); + + /* Checks access to pipes through FD. */ + ASSERT_EQ(0, pipe2(pipe_fds, O_CLOEXEC)); + ASSERT_EQ(1, write(pipe_fds[1], ".", 1)) + { + TH_LOG("Failed to write in pipe: %s", strerror(errno)); + } + ASSERT_EQ(1, read(pipe_fds[0], &buf, 1)); + ASSERT_EQ('.', buf); + + /* Checks write access to pipe through /proc/self/fd . */ + proc_fd = open_proc_fd(_metadata, pipe_fds[1], O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + ASSERT_EQ(1, write(proc_fd, ".", 1)) + { + TH_LOG("Failed to write through /proc/self/fd/%d: %s", + pipe_fds[1], strerror(errno)); + } + ASSERT_EQ(0, close(proc_fd)); + + /* Checks read access to pipe through /proc/self/fd . */ + proc_fd = open_proc_fd(_metadata, pipe_fds[0], O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, proc_fd); + buf = '\0'; + ASSERT_EQ(1, read(proc_fd, &buf, 1)) + { + TH_LOG("Failed to read through /proc/self/fd/%d: %s", + pipe_fds[1], strerror(errno)); + } + ASSERT_EQ(0, close(proc_fd)); + + ASSERT_EQ(0, close(pipe_fds[0])); + ASSERT_EQ(0, close(pipe_fds[1])); +} + +/* Invokes truncate(2) and returns its errno or 0. */ +static int test_truncate(const char *const path) +{ + if (truncate(path, 10) < 0) + return errno; + return 0; +} + +/* + * Invokes creat(2) and returns its errno or 0. + * Closes the opened file descriptor on success. + */ +static int test_creat(const char *const path) +{ + int fd = creat(path, 0600); + + if (fd < 0) + return errno; + + /* + * Mixing error codes from close(2) and creat(2) should not lead to any + * (access type) confusion for this test. + */ + if (close(fd) < 0) + return errno; + return 0; +} + +/* + * Exercises file truncation when it's not restricted, + * as it was the case before LANDLOCK_ACCESS_FS_TRUNCATE existed. + */ +TEST_F_FORK(layout1, truncate_unhandled) +{ + const char *const file_r = file1_s1d1; + const char *const file_w = file2_s1d1; + const char *const file_none = file1_s1d2; + const struct rule rules[] = { + { + .path = file_r, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = file_w, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + /* Implicitly: No rights for file_none. */ + {}, + }; + + /* Enables Landlock. */ + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE, + rules); + + /* + * Checks read right: truncate and open with O_TRUNC work, unless the + * file is attempted to be opened for writing. + */ + EXPECT_EQ(0, test_truncate(file_r)); + EXPECT_EQ(0, test_open(file_r, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_r, O_WRONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_creat(file_r)); + + /* + * Checks write right: truncate and open with O_TRUNC work, unless the + * file is attempted to be opened for reading. + */ + EXPECT_EQ(0, test_truncate(file_w)); + EXPECT_EQ(EACCES, test_open(file_w, O_RDONLY | O_TRUNC)); + EXPECT_EQ(0, test_open(file_w, O_WRONLY | O_TRUNC)); + EXPECT_EQ(0, test_creat(file_w)); + + /* + * Checks "no rights" case: truncate works but all open attempts fail, + * including creat. + */ + EXPECT_EQ(0, test_truncate(file_none)); + EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_creat(file_none)); +} + +TEST_F_FORK(layout1, truncate) +{ + const char *const file_rwt = file1_s1d1; + const char *const file_rw = file2_s1d1; + const char *const file_rt = file1_s1d2; + const char *const file_t = file2_s1d2; + const char *const file_none = file1_s1d3; + const char *const dir_t = dir_s2d1; + const char *const file_in_dir_t = file1_s2d1; + const char *const dir_w = dir_s3d1; + const char *const file_in_dir_w = file1_s3d1; + const struct rule rules[] = { + { + .path = file_rwt, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_TRUNCATE, + }, + { + .path = file_rw, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = file_rt, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_TRUNCATE, + }, + { + .path = file_t, + .access = LANDLOCK_ACCESS_FS_TRUNCATE, + }, + /* Implicitly: No access rights for file_none. */ + { + .path = dir_t, + .access = LANDLOCK_ACCESS_FS_TRUNCATE, + }, + { + .path = dir_w, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + + /* Enables Landlock. */ + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_TRUNCATE, + rules); + + /* Checks read, write and truncate rights: truncation works. */ + EXPECT_EQ(0, test_truncate(file_rwt)); + EXPECT_EQ(0, test_open(file_rwt, O_RDONLY | O_TRUNC)); + EXPECT_EQ(0, test_open(file_rwt, O_WRONLY | O_TRUNC)); + + /* Checks read and write rights: no truncate variant works. */ + EXPECT_EQ(EACCES, test_truncate(file_rw)); + EXPECT_EQ(EACCES, test_open(file_rw, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_rw, O_WRONLY | O_TRUNC)); + + /* + * Checks read and truncate rights: truncation works. + * + * Note: Files can get truncated using open() even with O_RDONLY. + */ + EXPECT_EQ(0, test_truncate(file_rt)); + EXPECT_EQ(0, test_open(file_rt, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_rt, O_WRONLY | O_TRUNC)); + + /* Checks truncate right: truncate works, but can't open file. */ + EXPECT_EQ(0, test_truncate(file_t)); + EXPECT_EQ(EACCES, test_open(file_t, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_t, O_WRONLY | O_TRUNC)); + + /* Checks "no rights" case: No form of truncation works. */ + EXPECT_EQ(EACCES, test_truncate(file_none)); + EXPECT_EQ(EACCES, test_open(file_none, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_none, O_WRONLY | O_TRUNC)); + + /* + * Checks truncate right on directory: truncate works on contained + * files. + */ + EXPECT_EQ(0, test_truncate(file_in_dir_t)); + EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_RDONLY | O_TRUNC)); + EXPECT_EQ(EACCES, test_open(file_in_dir_t, O_WRONLY | O_TRUNC)); + + /* + * Checks creat in dir_w: This requires the truncate right when + * overwriting an existing file, but does not require it when the file + * is new. + */ + EXPECT_EQ(EACCES, test_creat(file_in_dir_w)); + + ASSERT_EQ(0, unlink(file_in_dir_w)); + EXPECT_EQ(0, test_creat(file_in_dir_w)); +} + +/* Invokes ftruncate(2) and returns its errno or 0. */ +static int test_ftruncate(int fd) +{ + if (ftruncate(fd, 10) < 0) + return errno; + return 0; +} + +TEST_F_FORK(layout1, ftruncate) +{ + /* + * This test opens a new file descriptor at different stages of + * Landlock restriction: + * + * without restriction: ftruncate works + * something else but truncate restricted: ftruncate works + * truncate restricted and permitted: ftruncate works + * truncate restricted and not permitted: ftruncate fails + * + * Whether this works or not is expected to depend on the time when the + * FD was opened, not to depend on the time when ftruncate() was + * called. + */ + const char *const path = file1_s1d1; + const __u64 handled1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE; + const struct rule layer1[] = { + { + .path = path, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + const __u64 handled2 = LANDLOCK_ACCESS_FS_TRUNCATE; + const struct rule layer2[] = { + { + .path = path, + .access = LANDLOCK_ACCESS_FS_TRUNCATE, + }, + {}, + }; + const __u64 handled3 = LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_WRITE_FILE; + const struct rule layer3[] = { + { + .path = path, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + int fd_layer0, fd_layer1, fd_layer2, fd_layer3; + + fd_layer0 = open(path, O_WRONLY); + EXPECT_EQ(0, test_ftruncate(fd_layer0)); + + enforce_fs(_metadata, handled1, layer1); + + fd_layer1 = open(path, O_WRONLY); + EXPECT_EQ(0, test_ftruncate(fd_layer0)); + EXPECT_EQ(0, test_ftruncate(fd_layer1)); + + enforce_fs(_metadata, handled2, layer2); + + fd_layer2 = open(path, O_WRONLY); + EXPECT_EQ(0, test_ftruncate(fd_layer0)); + EXPECT_EQ(0, test_ftruncate(fd_layer1)); + EXPECT_EQ(0, test_ftruncate(fd_layer2)); + + enforce_fs(_metadata, handled3, layer3); + + fd_layer3 = open(path, O_WRONLY); + EXPECT_EQ(0, test_ftruncate(fd_layer0)); + EXPECT_EQ(0, test_ftruncate(fd_layer1)); + EXPECT_EQ(0, test_ftruncate(fd_layer2)); + EXPECT_EQ(EACCES, test_ftruncate(fd_layer3)); + + ASSERT_EQ(0, close(fd_layer0)); + ASSERT_EQ(0, close(fd_layer1)); + ASSERT_EQ(0, close(fd_layer2)); + ASSERT_EQ(0, close(fd_layer3)); +} + +/* clang-format off */ +FIXTURE(ftruncate) {}; +/* clang-format on */ + +FIXTURE_SETUP(ftruncate) +{ + prepare_layout(_metadata); + create_file(_metadata, file1_s1d1); +} + +FIXTURE_TEARDOWN_PARENT(ftruncate) +{ + EXPECT_EQ(0, remove_path(file1_s1d1)); + cleanup_layout(_metadata); +} + +FIXTURE_VARIANT(ftruncate) +{ + const __u64 handled; + const __u64 allowed; + const int expected_open_result; + const int expected_ftruncate_result; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ftruncate, w_w) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_WRITE_FILE, + .allowed = LANDLOCK_ACCESS_FS_WRITE_FILE, + .expected_open_result = 0, + .expected_ftruncate_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ftruncate, t_t) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_TRUNCATE, + .allowed = LANDLOCK_ACCESS_FS_TRUNCATE, + .expected_open_result = 0, + .expected_ftruncate_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ftruncate, wt_w) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE, + .allowed = LANDLOCK_ACCESS_FS_WRITE_FILE, + .expected_open_result = 0, + .expected_ftruncate_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ftruncate, wt_wt) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE, + .allowed = LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE, + .expected_open_result = 0, + .expected_ftruncate_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ftruncate, wt_t) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_TRUNCATE, + .allowed = LANDLOCK_ACCESS_FS_TRUNCATE, + .expected_open_result = EACCES, +}; + +TEST_F_FORK(ftruncate, open_and_ftruncate) +{ + const char *const path = file1_s1d1; + const struct rule rules[] = { + { + .path = path, + .access = variant->allowed, + }, + {}, + }; + int fd; + + /* Enables Landlock. */ + enforce_fs(_metadata, variant->handled, rules); + + fd = open(path, O_WRONLY); + EXPECT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0)); + if (fd >= 0) { + EXPECT_EQ(variant->expected_ftruncate_result, + test_ftruncate(fd)); + ASSERT_EQ(0, close(fd)); + } +} + +TEST_F_FORK(ftruncate, open_and_ftruncate_in_different_processes) +{ + int child, fd, status; + int socket_fds[2]; + + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, + socket_fds)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + /* + * Enables Landlock in the child process, open a file descriptor + * where truncation is forbidden and send it to the + * non-landlocked parent process. + */ + const char *const path = file1_s1d1; + const struct rule rules[] = { + { + .path = path, + .access = variant->allowed, + }, + {}, + }; + int fd; + + enforce_fs(_metadata, variant->handled, rules); + + fd = open(path, O_WRONLY); + ASSERT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0)); + + if (fd >= 0) { + ASSERT_EQ(0, send_fd(socket_fds[0], fd)); + ASSERT_EQ(0, close(fd)); + } + + ASSERT_EQ(0, close(socket_fds[0])); + + _exit(_metadata->exit_code); + return; + } + + if (variant->expected_open_result == 0) { + fd = recv_fd(socket_fds[1]); + ASSERT_LE(0, fd); + + EXPECT_EQ(variant->expected_ftruncate_result, + test_ftruncate(fd)); + ASSERT_EQ(0, close(fd)); + } + + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + ASSERT_EQ(0, close(socket_fds[0])); + ASSERT_EQ(0, close(socket_fds[1])); +} + +/* Invokes the FS_IOC_GETFLAGS IOCTL and returns its errno or 0. */ +static int test_fs_ioc_getflags_ioctl(int fd) +{ + uint32_t flags; + + if (ioctl(fd, FS_IOC_GETFLAGS, &flags) < 0) + return errno; + return 0; +} + +TEST(memfd_ftruncate_and_ioctl) +{ + int fd, i; + + /* + * We exercise the same test both with and without Landlock enabled, to + * ensure that it behaves the same in both cases. + */ + for (i = 0; i < 2; i++) { + /* Creates a new memfd. */ + fd = memfd_create("name", MFD_CLOEXEC); + ASSERT_LE(0, fd); + + /* + * Checks that operations associated with the opened file + * (ftruncate, ioctl) are permitted on file descriptors that are + * created in ways other than open(2). + */ + EXPECT_EQ(0, test_ftruncate(fd)); + EXPECT_EQ(0, test_fs_ioc_getflags_ioctl(fd)); + + ASSERT_EQ(0, close(fd)); + + /* Enables Landlock. */ + enforce_fs(_metadata, ACCESS_ALL, NULL); + } +} + +static int test_fionread_ioctl(int fd) +{ + size_t sz = 0; + + if (ioctl(fd, FIONREAD, &sz) < 0 && errno == EACCES) + return errno; + return 0; +} + +TEST_F_FORK(layout1, o_path_ftruncate_and_ioctl) +{ + int fd; + + /* + * Checks that for files opened with O_PATH, both ioctl(2) and + * ftruncate(2) yield EBADF, as it is documented in open(2) for the + * O_PATH flag. + */ + fd = open(dir_s1d1, O_PATH | O_CLOEXEC); + ASSERT_LE(0, fd); + + EXPECT_EQ(EBADF, test_ftruncate(fd)); + EXPECT_EQ(EBADF, test_fs_ioc_getflags_ioctl(fd)); + + ASSERT_EQ(0, close(fd)); + + /* Enables Landlock. */ + enforce_fs(_metadata, ACCESS_ALL, NULL); + + /* + * Checks that after enabling Landlock, + * - the file can still be opened with O_PATH + * - both ioctl and truncate still yield EBADF (not EACCES). + */ + fd = open(dir_s1d1, O_PATH | O_CLOEXEC); + ASSERT_LE(0, fd); + + EXPECT_EQ(EBADF, test_ftruncate(fd)); + EXPECT_EQ(EBADF, test_fs_ioc_getflags_ioctl(fd)); + + ASSERT_EQ(0, close(fd)); +} + +/* + * ioctl_error - generically call the given ioctl with a pointer to a + * sufficiently large zeroed-out memory region. + * + * Returns the IOCTLs error, or 0. + */ +static int ioctl_error(struct __test_metadata *const _metadata, int fd, + unsigned int cmd) +{ + char buf[128]; /* sufficiently large */ + int res, stdinbak_fd, err; + + /* + * Depending on the IOCTL command, parts of the zeroed-out buffer might + * be interpreted as file descriptor numbers. We do not want to + * accidentally operate on file descriptor 0 (stdin), so we temporarily + * move stdin to a different FD and close FD 0 for the IOCTL call. + */ + stdinbak_fd = dup(0); + ASSERT_LT(0, stdinbak_fd); + ASSERT_EQ(0, close(0)); + + /* Invokes the IOCTL with a zeroed-out buffer. */ + bzero(&buf, sizeof(buf)); + res = ioctl(fd, cmd, &buf); + err = errno; + + /* Restores the old FD 0 and closes the backup FD. */ + ASSERT_EQ(0, dup2(stdinbak_fd, 0)); + ASSERT_EQ(0, close(stdinbak_fd)); + + if (res < 0) + return err; + + return 0; +} + +/* Define some linux/falloc.h IOCTL commands which are not available in uapi headers. */ +struct space_resv { + __s16 l_type; + __s16 l_whence; + __s64 l_start; + __s64 l_len; /* len == 0 means until end of file */ + __s32 l_sysid; + __u32 l_pid; + __s32 l_pad[4]; /* reserved area */ +}; + +#define FS_IOC_RESVSP _IOW('X', 40, struct space_resv) +#define FS_IOC_UNRESVSP _IOW('X', 41, struct space_resv) +#define FS_IOC_RESVSP64 _IOW('X', 42, struct space_resv) +#define FS_IOC_UNRESVSP64 _IOW('X', 43, struct space_resv) +#define FS_IOC_ZERO_RANGE _IOW('X', 57, struct space_resv) + +/* + * Tests a series of blanket-permitted and denied IOCTLs. + */ +TEST_F_FORK(layout1, blanket_permitted_ioctls) +{ + int fd; + + /* Enables Landlock. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL); + + fd = open("/dev/null", O_RDWR | O_CLOEXEC); + ASSERT_LE(0, fd); + + /* + * Checks permitted commands. + * These ones may return errors, but should not be blocked by Landlock. + */ + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIOCLEX)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIONCLEX)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIONBIO)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIOASYNC)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIOQSIZE)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIFREEZE)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FITHAW)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FS_IOC_FIEMAP)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIGETBSZ)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FICLONE)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FICLONERANGE)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FIDEDUPERANGE)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FS_IOC_GETFSUUID)); + EXPECT_NE(EACCES, ioctl_error(_metadata, fd, FS_IOC_GETFSSYSFSPATH)); + + /* + * Checks blocked commands. + * A call to a blocked IOCTL command always returns EACCES. + */ + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FIONREAD)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_GETFLAGS)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_SETFLAGS)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_FSGETXATTR)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_FSSETXATTR)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FIBMAP)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_RESVSP)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_RESVSP64)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_UNRESVSP)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_UNRESVSP64)); + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, FS_IOC_ZERO_RANGE)); + + /* Default case is also blocked. */ + EXPECT_EQ(EACCES, ioctl_error(_metadata, fd, 0xc00ffeee)); + + ASSERT_EQ(0, close(fd)); +} + +/* + * Named pipes are not governed by the LANDLOCK_ACCESS_FS_IOCTL_DEV right, + * because they are not character or block devices. + */ +TEST_F_FORK(layout1, named_pipe_ioctl) +{ + pid_t child_pid; + int fd; + const char *const path = file1_s1d1; + + ASSERT_EQ(0, unlink(path)); + ASSERT_EQ(0, mkfifo(path, 0600)); + + /* Enables Landlock. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL); + + /* The child process opens the pipe for writing. */ + child_pid = fork(); + ASSERT_NE(-1, child_pid); + if (child_pid == 0) { + fd = open(path, O_WRONLY); + close(fd); + exit(0); + } + + fd = open(path, O_RDONLY); + ASSERT_LE(0, fd); + + /* FIONREAD is implemented by pipefifo_fops. */ + EXPECT_EQ(0, test_fionread_ioctl(fd)); + + ASSERT_EQ(0, close(fd)); + ASSERT_EQ(0, unlink(path)); + + ASSERT_EQ(child_pid, waitpid(child_pid, NULL, 0)); +} + +/* + * set_up_named_unix_server - Create a pathname unix socket + * + * If the socket type is not SOCK_DGRAM, also invoke listen(2). + * + * Return: The listening FD - it is the caller responsibility to close it. + */ +static int set_up_named_unix_server(struct __test_metadata *const _metadata, + int type, const char *const path) +{ + int fd; + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + }; + + fd = socket(AF_UNIX, type, 0); + ASSERT_LE(0, fd); + + ASSERT_LT(strlen(path), sizeof(addr.sun_path)); + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + ASSERT_EQ(0, bind(fd, (struct sockaddr *)&addr, sizeof(addr))); + + if (type != SOCK_DGRAM) + ASSERT_EQ(0, listen(fd, 10 /* qlen */)); + return fd; +} + +/* + * test_connect_named_unix - connect to the given named UNIX socket + * + * Return: The errno from connect(), or 0 + */ +static int test_connect_named_unix(struct __test_metadata *const _metadata, + int fd, const char *const path) +{ + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + }; + + ASSERT_LT(strlen(path), sizeof(addr.sun_path)); + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) + return errno; + return 0; +} + +/* For named UNIX domain sockets, no IOCTL restrictions apply. */ +TEST_F_FORK(layout1, named_unix_domain_socket_ioctl) +{ + const char *const path = file1_s1d1; + int srv_fd, cli_fd; + + /* Sets up a server */ + ASSERT_EQ(0, unlink(path)); + srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path); + + /* Enables Landlock. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL); + + /* Sets up a client connection to it */ + cli_fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, cli_fd); + + ASSERT_EQ(0, test_connect_named_unix(_metadata, cli_fd, path)); + + /* FIONREAD and other IOCTLs should not be forbidden. */ + EXPECT_EQ(0, test_fionread_ioctl(cli_fd)); + + EXPECT_EQ(0, close(cli_fd)); + EXPECT_EQ(0, close(srv_fd)); +} + +/* clang-format off */ +FIXTURE(ioctl) {}; + +FIXTURE_SETUP(ioctl) {}; + +FIXTURE_TEARDOWN(ioctl) {}; +/* clang-format on */ + +FIXTURE_VARIANT(ioctl) +{ + const __u64 handled; + const __u64 allowed; + const mode_t open_mode; + /* + * FIONREAD is used as a characteristic device-specific IOCTL command. + * It is implemented in fs/ioctl.c for regular files, + * but we do not blanket-permit it for devices. + */ + const int expected_fionread_result; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ioctl, handled_i_allowed_none) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .allowed = 0, + .open_mode = O_RDWR, + .expected_fionread_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ioctl, handled_i_allowed_i) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .allowed = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .open_mode = O_RDWR, + .expected_fionread_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ioctl, unhandled) { + /* clang-format on */ + .handled = LANDLOCK_ACCESS_FS_EXECUTE, + .allowed = LANDLOCK_ACCESS_FS_EXECUTE, + .open_mode = O_RDWR, + .expected_fionread_result = 0, +}; + +TEST_F_FORK(ioctl, handle_dir_access_file) +{ + const int flag = 0; + const struct rule rules[] = { + { + .path = "/dev", + .access = variant->allowed, + }, + {}, + }; + int fd; + + /* Enables Landlock. */ + enforce_fs(_metadata, variant->handled, rules); + + fd = open("/dev/zero", variant->open_mode); + ASSERT_LE(0, fd); + + /* Checks that IOCTL commands return the expected errors. */ + EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd)); + + /* Checks that unrestrictable commands are unrestricted. */ + EXPECT_EQ(0, ioctl(fd, FIOCLEX)); + EXPECT_EQ(0, ioctl(fd, FIONCLEX)); + EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag)); + EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag)); + EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag)); + + ASSERT_EQ(0, close(fd)); +} + +TEST_F_FORK(ioctl, handle_dir_access_dir) +{ + const int flag = 0; + const struct rule rules[] = { + { + .path = "/dev", + .access = variant->allowed, + }, + {}, + }; + int dir_fd; + + /* Enables Landlock. */ + enforce_fs(_metadata, variant->handled, rules); + + /* + * Ignore variant->open_mode for this test, as we intend to open a + * directory. If the directory can not be opened, the variant is + * infeasible to test with an opened directory. + */ + dir_fd = open("/dev", O_RDONLY); + if (dir_fd < 0) + return; + + /* + * Checks that IOCTL commands return the expected errors. + * We do not use the expected values from the fixture here. + * + * When using IOCTL on a directory, no Landlock restrictions apply. + */ + EXPECT_EQ(0, test_fionread_ioctl(dir_fd)); + + /* Checks that unrestrictable commands are unrestricted. */ + EXPECT_EQ(0, ioctl(dir_fd, FIOCLEX)); + EXPECT_EQ(0, ioctl(dir_fd, FIONCLEX)); + EXPECT_EQ(0, ioctl(dir_fd, FIONBIO, &flag)); + EXPECT_EQ(0, ioctl(dir_fd, FIOASYNC, &flag)); + EXPECT_EQ(0, ioctl(dir_fd, FIGETBSZ, &flag)); + + ASSERT_EQ(0, close(dir_fd)); +} + +TEST_F_FORK(ioctl, handle_file_access_file) +{ + const int flag = 0; + const struct rule rules[] = { + { + .path = "/dev/zero", + .access = variant->allowed, + }, + {}, + }; + int fd; + + /* Enables Landlock. */ + enforce_fs(_metadata, variant->handled, rules); + + fd = open("/dev/zero", variant->open_mode); + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open /dev/zero: %s", strerror(errno)); + } + + /* Checks that IOCTL commands return the expected errors. */ + EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd)); + + /* Checks that unrestrictable commands are unrestricted. */ + EXPECT_EQ(0, ioctl(fd, FIOCLEX)); + EXPECT_EQ(0, ioctl(fd, FIONCLEX)); + EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag)); + EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag)); + EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag)); + + ASSERT_EQ(0, close(fd)); +} + +/* + * test_sendto_named_unix - sendto to the given named UNIX socket + * + * sendto() is equivalent to sendmsg() in this respect. + * + * Return: The errno from sendto(), or 0 + */ +static int test_sendto_named_unix(struct __test_metadata *const _metadata, + int fd, const char *const path) +{ + static const char buf[] = "dummy"; + struct sockaddr_un addr = { + .sun_family = AF_UNIX, + }; + + ASSERT_LT(strlen(path), sizeof(addr.sun_path)); + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&addr, + sizeof(addr)) == -1) + return errno; + return 0; +} + +/* clang-format off */ +FIXTURE(scoped_domains) {}; +/* clang-format on */ + +#include "scoped_base_variants.h" + +FIXTURE_SETUP(scoped_domains) +{ + drop_caps(_metadata); +}; + +FIXTURE_TEARDOWN(scoped_domains) +{ +} + +/* + * Flags for test_connect_to_parent and test_connect_to_child: + * + * USE_SENDTO: Use sendto() instead of connect() (for SOCK_DGRAM only) + * ENFORCE_ALL: Enforce a Landlock domain even when the variant says + * we shouldn't. We enforce a domain where the path is allow-listed, + * and expect the behavior to be the same as if none was used. + */ +#define USE_SENDTO (1 << 0) +#define ENFORCE_ALL (1 << 1) + +static void test_connect_to_parent(struct __test_metadata *const _metadata, + const FIXTURE_VARIANT(scoped_domains) * + variant, + int sock_type, int flags) +{ + const char *const path = "sock"; + const struct rule rules[] = { + { + .path = ".", + .access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + }, + {}, + }; + int cli_fd, srv_fd, res, status; + pid_t child_pid; + int readiness_pipe[2]; + char buf[1]; + + if (variant->domain_both) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules); + + unlink(path); + ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC)); + + child_pid = fork(); + ASSERT_LE(0, child_pid); + + if (child_pid == 0) { + if (variant->domain_child) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + rules); + + /* Wait for server to be available. */ + EXPECT_EQ(0, close(readiness_pipe[1])); + EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1)); + EXPECT_EQ(0, close(readiness_pipe[0])); + + /* Talk to server. */ + cli_fd = socket(AF_UNIX, sock_type, 0); + ASSERT_LE(0, cli_fd); + + if (flags & USE_SENDTO) + res = test_sendto_named_unix(_metadata, cli_fd, path); + else + res = test_connect_named_unix(_metadata, cli_fd, path); + + EXPECT_EQ(variant->domain_child ? EACCES : 0, res); + + /* Clean up. */ + EXPECT_EQ(0, close(cli_fd)); + + _exit(_metadata->exit_code); + return; + } + + if (variant->domain_parent) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules); + + srv_fd = set_up_named_unix_server(_metadata, sock_type, path); + + /* Tell the child that it can connect. */ + EXPECT_EQ(0, close(readiness_pipe[0])); + EXPECT_EQ(sizeof(buf), write(readiness_pipe[1], buf, sizeof(buf))); + EXPECT_EQ(0, close(readiness_pipe[1])); + + /* Wait for child. */ + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* Clean up. */ + EXPECT_EQ(0, close(srv_fd)); + EXPECT_EQ(0, unlink(path)); +} + +static void test_connect_to_child(struct __test_metadata *const _metadata, + const FIXTURE_VARIANT(scoped_domains) * + variant, + int sock_type, int flags) +{ + const char *const path = "sock"; + const struct rule rules[] = { + { + .path = ".", + .access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + }, + {}, + }; + int readiness_pipe[2]; + int shutdown_pipe[2]; + int cli_fd, srv_fd, res, status; + pid_t child_pid; + char buf[1]; + + if (variant->domain_both) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules); + + unlink(path); + ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(shutdown_pipe, O_CLOEXEC)); + + child_pid = fork(); + ASSERT_LE(0, child_pid); + + if (child_pid == 0) { + if (variant->domain_child) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + rules); + + srv_fd = set_up_named_unix_server(_metadata, sock_type, path); + + /* Tell the parent that it can connect. */ + EXPECT_EQ(0, close(readiness_pipe[0])); + EXPECT_EQ(sizeof(buf), + write(readiness_pipe[1], buf, sizeof(buf))); + EXPECT_EQ(0, close(readiness_pipe[1])); + + /* Wait until it is time to shut down. */ + EXPECT_EQ(0, close(shutdown_pipe[1])); + EXPECT_EQ(1, read(shutdown_pipe[0], &buf, 1)); + EXPECT_EQ(0, close(shutdown_pipe[0])); + + /* Cleanup */ + EXPECT_EQ(0, close(srv_fd)); + EXPECT_EQ(0, unlink(path)); + + _exit(_metadata->exit_code); + return; + } + + if (variant->domain_parent) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL); + else if (flags & ENFORCE_ALL) + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules); + + /* Wait for server to be available. */ + EXPECT_EQ(0, close(readiness_pipe[1])); + EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1)); + EXPECT_EQ(0, close(readiness_pipe[0])); + + /* Talk to server. */ + cli_fd = socket(AF_UNIX, sock_type, 0); + ASSERT_LE(0, cli_fd); + + if (flags & USE_SENDTO) + res = test_sendto_named_unix(_metadata, cli_fd, path); + else + res = test_connect_named_unix(_metadata, cli_fd, path); + + EXPECT_EQ(variant->domain_parent ? EACCES : 0, res); + + /* Clean up. */ + EXPECT_EQ(0, close(cli_fd)); + + /* Tell the server to shut down. */ + EXPECT_EQ(0, close(shutdown_pipe[0])); + EXPECT_EQ(sizeof(buf), write(shutdown_pipe[1], buf, sizeof(buf))); + EXPECT_EQ(0, close(shutdown_pipe[1])); + + /* Wait for child. */ + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + +TEST_F(scoped_domains, unix_stream_connect_to_parent) +{ + test_connect_to_parent(_metadata, variant, SOCK_STREAM, 0); +} + +TEST_F(scoped_domains, unix_dgram_connect_to_parent) +{ + test_connect_to_parent(_metadata, variant, SOCK_DGRAM, 0); +} + +TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent) +{ + test_connect_to_parent(_metadata, variant, SOCK_DGRAM, USE_SENDTO); +} + +TEST_F(scoped_domains, unix_seqpacket_connect_to_parent) +{ + test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, 0); +} + +TEST_F(scoped_domains, unix_stream_connect_to_parent_full) +{ + test_connect_to_parent(_metadata, variant, SOCK_STREAM, ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_dgram_connect_to_parent_full) +{ + test_connect_to_parent(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent_full) +{ + test_connect_to_parent(_metadata, variant, SOCK_DGRAM, + USE_SENDTO | ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_seqpacket_connect_to_parent_full) +{ + test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_stream_connect_to_child) +{ + test_connect_to_child(_metadata, variant, SOCK_STREAM, 0); +} + +TEST_F(scoped_domains, unix_dgram_connect_to_child) +{ + test_connect_to_child(_metadata, variant, SOCK_DGRAM, 0); +} + +TEST_F(scoped_domains, unix_dgram_sendmsg_to_child) +{ + test_connect_to_child(_metadata, variant, SOCK_DGRAM, USE_SENDTO); +} + +TEST_F(scoped_domains, unix_seqpacket_connect_to_child) +{ + test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, 0); +} + +TEST_F(scoped_domains, unix_stream_connect_to_child_full) +{ + test_connect_to_child(_metadata, variant, SOCK_STREAM, ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_dgram_connect_to_child_full) +{ + test_connect_to_child(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_dgram_sendmsg_to_child_full) +{ + test_connect_to_child(_metadata, variant, SOCK_DGRAM, + USE_SENDTO | ENFORCE_ALL); +} + +TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full) +{ + test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL); +} + +#undef USE_SENDTO +#undef ENFORCE_ALL + +static void read_core_pattern(struct __test_metadata *const _metadata, + char *buf, size_t buf_size) +{ + int fd; + ssize_t ret; + + fd = open("/proc/sys/kernel/core_pattern", O_RDONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + + ret = read(fd, buf, buf_size - 1); + ASSERT_LE(0, ret); + EXPECT_EQ(0, close(fd)); + + buf[ret] = '\0'; +} + +static void set_core_pattern(struct __test_metadata *const _metadata, + const char *pattern) +{ + int fd; + size_t len = strlen(pattern); + + /* + * Writing to /proc/sys/kernel/core_pattern requires EUID 0 because + * sysctl_perm() checks that, ignoring capabilities like + * CAP_SYS_ADMIN or CAP_DAC_OVERRIDE. + * + * Switching EUID clears the dumpable flag, which must be restored + * afterwards to allow coredumps. + */ + set_cap(_metadata, CAP_SETUID); + ASSERT_EQ(0, seteuid(0)); + clear_cap(_metadata, CAP_SETUID); + + fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open core_pattern for writing: %s", + strerror(errno)); + } + + ASSERT_EQ(len, write(fd, pattern, len)); + EXPECT_EQ(0, close(fd)); + + set_cap(_metadata, CAP_SETUID); + ASSERT_EQ(0, seteuid(getuid())); + clear_cap(_metadata, CAP_SETUID); + + /* Restore dumpable flag cleared by seteuid(). */ + ASSERT_EQ(0, prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)); +} + +FIXTURE(coredump) +{ + char original_core_pattern[256]; +}; + +FIXTURE_SETUP(coredump) +{ + disable_caps(_metadata); + read_core_pattern(_metadata, self->original_core_pattern, + sizeof(self->original_core_pattern)); +} + +FIXTURE_TEARDOWN_PARENT(coredump) +{ + set_core_pattern(_metadata, self->original_core_pattern); +} + +/* + * Test that even when a process is restricted with + * LANDLOCK_ACCESS_FS_RESOLVE_UNIX, the kernel can still initiate a connection + * to the coredump socket on the processes' behalf. + */ +TEST_F_FORK(coredump, socket_not_restricted) +{ + static const char core_pattern[] = "@/tmp/landlock_coredump_test.sock"; + const char *const sock_path = core_pattern + 1; + int srv_fd, conn_fd, status; + pid_t child_pid; + struct ucred cred; + socklen_t cred_len = sizeof(cred); + char buf[4096]; + + /* Set up the coredump server socket. */ + unlink(sock_path); + srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, sock_path); + + /* Point coredumps at our socket. */ + set_core_pattern(_metadata, core_pattern); + + /* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL); + + /* Fork a child that crashes. */ + child_pid = fork(); + ASSERT_LE(0, child_pid); + if (child_pid == 0) { + struct rlimit rl = { + .rlim_cur = RLIM_INFINITY, + .rlim_max = RLIM_INFINITY, + }; + + ASSERT_EQ(0, setrlimit(RLIMIT_CORE, &rl)); + + /* Crash on purpose. */ + kill(getpid(), SIGSEGV); + _exit(1); + } + + /* + * Accept the coredump connection. If Landlock incorrectly denies the + * kernel's coredump connect, accept() will block forever, so the test + * would time out. + */ + conn_fd = accept(srv_fd, NULL, NULL); + ASSERT_LE(0, conn_fd); + + /* Check that the connection came from the crashing child. */ + ASSERT_EQ(0, getsockopt(conn_fd, SOL_SOCKET, SO_PEERCRED, &cred, + &cred_len)); + EXPECT_EQ(child_pid, cred.pid); + + /* Drain the coredump data so the kernel can finish. */ + while (read(conn_fd, buf, sizeof(buf)) > 0) + ; + + EXPECT_EQ(0, close(conn_fd)); + + /* Wait for the child and verify it coredumped. */ + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + ASSERT_TRUE(WIFSIGNALED(status)); + ASSERT_TRUE(WCOREDUMP(status)); + + EXPECT_EQ(0, close(srv_fd)); + EXPECT_EQ(0, unlink(sock_path)); +} + +/* clang-format off */ +FIXTURE(layout1_bind) {}; +/* clang-format on */ + +static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; +static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; +static const char bind_file2_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f2"; + +/* Move targets for disconnected path tests. */ +static const char dir_s4d1[] = TMP_DIR "/s4d1"; +static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; +static const char file2_s4d1[] = TMP_DIR "/s4d1/f2"; +static const char dir_s4d2[] = TMP_DIR "/s4d1/s4d2"; +static const char file1_s4d2[] = TMP_DIR "/s4d1/s4d2/f1"; +static const char file1_name[] = "f1"; +static const char file2_name[] = "f2"; + +FIXTURE_SETUP(layout1_bind) +{ + prepare_layout(_metadata); + + create_layout1(_metadata); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(dir_s1d2, dir_s2d2, NULL, MS_BIND, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN_PARENT(layout1_bind) +{ + /* umount(dir_s2d2)) is handled by namespace lifetime. */ + + remove_path(file1_s4d1); + remove_path(file2_s4d1); + + remove_layout1(_metadata); + + cleanup_layout(_metadata); +} + +/* + * layout1_bind hierarchy: + * + * tmp + * ├── s1d1 + * │ ├── f1 + * │ ├── f2 + * │ └── s1d2 + * │ ├── f1 + * │ ├── f2 + * │ └── s1d3 [disconnected by path_disconnected] + * │ ├── f1 + * │ └── f2 + * ├── s2d1 + * │ ├── f1 + * │ └── s2d2 [bind mount from s1d2] + * │ ├── f1 + * │ ├── f2 + * │ └── s1d3 + * │ ├── f1 + * │ └── f2 + * ├── s3d1 + * │ └── s3d2 + * │ └── s3d3 + * └── s4d1 [renamed from s1d3 by path_disconnected] + * ├── f1 + * ├── f2 + * └── s4d2 + * └── f1 + */ + +TEST_F_FORK(layout1_bind, no_restriction) +{ + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d1, O_RDONLY)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + ASSERT_EQ(ENOENT, test_open(dir_s2d3, O_RDONLY)); + ASSERT_EQ(ENOENT, test_open(file1_s2d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(bind_dir_s1d3, O_RDONLY)); + ASSERT_EQ(0, test_open(bind_file1_s1d3, O_RDONLY)); + + ASSERT_EQ(0, test_open(dir_s3d1, O_RDONLY)); +} + +TEST_F_FORK(layout1_bind, same_content_same_file) +{ + /* + * Sets access right on parent directories of both source and + * destination mount points. + */ + const struct rule layer1_parent[] = { + { + .path = dir_s1d1, + .access = ACCESS_RO, + }, + { + .path = dir_s2d1, + .access = ACCESS_RW, + }, + {}, + }; + /* + * Sets access rights on the same bind-mounted directories. The result + * should be ACCESS_RW for both directories, but not both hierarchies + * because of the first layer. + */ + const struct rule layer2_mount_point[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = dir_s2d2, + .access = ACCESS_RW, + }, + {}, + }; + /* Only allow read-access to the s1d3 hierarchies. */ + const struct rule layer3_source[] = { + { + .path = dir_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + /* Removes all access rights. */ + const struct rule layer4_destination[] = { + { + .path = bind_file1_s1d3, + .access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + + /* Sets rules for the parent directories. */ + enforce_fs(_metadata, ACCESS_RW, layer1_parent); + + /* Checks source hierarchy. */ + ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Checks destination hierarchy. */ + ASSERT_EQ(0, test_open(file1_s2d1, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s2d1, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(file1_s2d2, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY)); + + /* Sets rules for the mount points. */ + enforce_fs(_metadata, ACCESS_RW, layer2_mount_point); + + /* Checks source hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + /* Checks destination hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s2d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s2d1, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(file1_s2d2, O_RDWR)); + ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY)); + ASSERT_EQ(0, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* Sets a (shared) rule only on the source. */ + enforce_fs(_metadata, ACCESS_RW, layer3_source); + + /* Checks source hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d2, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* Checks destination hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s2d2, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s2d2, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY)); + + ASSERT_EQ(0, test_open(bind_file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(bind_file1_s1d3, O_WRONLY)); + ASSERT_EQ(EACCES, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY)); + + /* Sets a (shared) rule only on the destination. */ + enforce_fs(_metadata, ACCESS_RW, layer4_destination); + + /* Checks source hierarchy. */ + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s1d3, O_WRONLY)); + + /* Checks destination hierarchy. */ + ASSERT_EQ(EACCES, test_open(bind_file1_s1d3, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(bind_file1_s1d3, O_WRONLY)); +} + +TEST_F_FORK(layout1_bind, reparent_cross_mount) +{ + const struct rule layer1[] = { + { + /* dir_s2d1 is beneath the dir_s2d2 mount point. */ + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REFER, + }, + { + .path = bind_dir_s1d3, + .access = LANDLOCK_ACCESS_FS_EXECUTE, + }, + {}, + }; + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE, + layer1); + + /* Checks basic denied move. */ + ASSERT_EQ(-1, rename(file1_s1d1, file1_s1d2)); + ASSERT_EQ(EXDEV, errno); + + /* Checks real cross-mount move (Landlock is not involved). */ + ASSERT_EQ(-1, rename(file1_s2d1, file1_s2d2)); + ASSERT_EQ(EXDEV, errno); + + /* Checks move that will give more accesses. */ + ASSERT_EQ(-1, rename(file1_s2d2, bind_file1_s1d3)); + ASSERT_EQ(EXDEV, errno); + + /* Checks legitimate downgrade move. */ + ASSERT_EQ(0, rename(bind_file1_s1d3, file1_s2d2)); +} + +/* + * Make sure access to file through a disconnected path works as expected. + * This test moves s1d3 to s4d1. + */ +TEST_F_FORK(layout1_bind, path_disconnected) +{ + const struct rule layer1_allow_all[] = { + { + .path = TMP_DIR, + .access = ACCESS_ALL, + }, + {}, + }; + const struct rule layer2_allow_just_f1[] = { + { + .path = file1_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + const struct rule layer3_only_s1d2[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + + /* Landlock should not deny access just because it is disconnected. */ + int ruleset_fd_l1 = + create_ruleset(_metadata, ACCESS_ALL, layer1_allow_all); + + /* Creates the new ruleset now before we move the dir containing the file. */ + int ruleset_fd_l2 = + create_ruleset(_metadata, ACCESS_RW, layer2_allow_just_f1); + int ruleset_fd_l3 = + create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2); + int bind_s1d3_fd; + + enforce_ruleset(_metadata, ruleset_fd_l1); + EXPECT_EQ(0, close(ruleset_fd_l1)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + ASSERT_LE(0, bind_s1d3_fd); + + /* Tests access is possible before we move. */ + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); + + /* Makes it disconnected. */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, + strerror(errno)); + } + + /* Tests that access is still possible. */ + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + + /* + * Tests that ".." is not possible (not because of Landlock, but just + * because it's disconnected). + */ + EXPECT_EQ(ENOENT, + test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); + + /* This should still work with a narrower rule. */ + enforce_ruleset(_metadata, ruleset_fd_l2); + EXPECT_EQ(0, close(ruleset_fd_l2)); + + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)); + /* + * Accessing a file through a disconnected file descriptor can still be + * allowed by a rule tied to this file, even if it is no longer visible in + * its mount point. + */ + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + + enforce_ruleset(_metadata, ruleset_fd_l3); + EXPECT_EQ(0, close(ruleset_fd_l3)); + + EXPECT_EQ(EACCES, test_open(file1_s4d1, O_RDONLY)); + /* + * Accessing a file through a disconnected file descriptor can still be + * allowed by a rule tied to the original mount point, even if it is no + * longer visible in its mount point. + */ + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); +} + +/* + * Test that renameat with disconnected paths works under Landlock. This test + * moves s1d3 to s4d2, so that we can have a rule allowing refers on the move + * target's immediate parent. + */ +TEST_F_FORK(layout1_bind, path_disconnected_rename) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = dir_s4d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + + /* This layer only handles LANDLOCK_ACCESS_FS_READ_FILE. */ + const struct rule layer2_only_s1d2[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + int ruleset_fd_l1, ruleset_fd_l2; + pid_t child_pid; + int bind_s1d3_fd, status; + + ASSERT_EQ(0, mkdir(dir_s4d1, 0755)) + { + TH_LOG("Failed to create %s: %s", dir_s4d1, strerror(errno)); + } + ruleset_fd_l1 = create_ruleset(_metadata, ACCESS_ALL, layer1); + ruleset_fd_l2 = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer2_only_s1d2); + + enforce_ruleset(_metadata, ruleset_fd_l1); + EXPECT_EQ(0, close(ruleset_fd_l1)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + ASSERT_LE(0, bind_s1d3_fd); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + + /* Tests ENOENT priority over EACCES for disconnected directory. */ + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, + strerror(errno)); + } + EXPECT_EQ(ENOENT, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); + + /* + * The file is no longer under s1d2 but we should still be able to access it + * with layer 2 because its mount point is evaluated as the first valid + * directory because it was initially a parent. Do a fork to test this so + * we don't prevent ourselves from renaming it back later. + */ + child_pid = fork(); + ASSERT_LE(0, child_pid); + if (child_pid == 0) { + enforce_ruleset(_metadata, ruleset_fd_l2); + EXPECT_EQ(0, close(ruleset_fd_l2)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(EACCES, test_open(file1_s4d2, O_RDONLY)); + + /* + * Tests that access widening checks indeed prevents us from renaming it + * back. + */ + EXPECT_EQ(-1, rename(dir_s4d2, dir_s1d3)); + EXPECT_EQ(EXDEV, errno); + + /* + * Including through the now disconnected fd (but it should return + * EXDEV). + */ + EXPECT_EQ(-1, renameat(bind_s1d3_fd, file1_name, AT_FDCWD, + file1_s2d2)); + EXPECT_EQ(EXDEV, errno); + _exit(_metadata->exit_code); + return; + } + + EXPECT_EQ(child_pid, waitpid(child_pid, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) + { + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d1, dir_s1d3, + strerror(errno)); + } + + /* Now checks that we can access it under l2. */ + child_pid = fork(); + ASSERT_LE(0, child_pid); + if (child_pid == 0) { + enforce_ruleset(_metadata, ruleset_fd_l2); + EXPECT_EQ(0, close(ruleset_fd_l2)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + _exit(_metadata->exit_code); + return; + } + + EXPECT_EQ(child_pid, waitpid(child_pid, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Also test that we can rename via a disconnected path. We move the + * dir back to the disconnected place first, then we rename file1 to + * file2 through our dir fd. + */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, + strerror(errno)); + } + ASSERT_EQ(0, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)) + { + TH_LOG("Failed to rename %s to %s within disconnected %s: %s", + file1_name, file2_name, bind_dir_s1d3, strerror(errno)); + } + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) + { + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + file2_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); + } + EXPECT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + EXPECT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + + /* Move it back using the disconnected path as the target. */ + ASSERT_EQ(0, renameat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file1_name)) + { + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + file1_s1d2, file1_name, bind_dir_s1d3, strerror(errno)); + } + + /* Now make it connected again. */ + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) + { + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d2, dir_s1d3, + strerror(errno)); + } + + /* Checks again that we can access it under l2. */ + enforce_ruleset(_metadata, ruleset_fd_l2); + EXPECT_EQ(0, close(ruleset_fd_l2)); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + EXPECT_EQ(0, test_open(file1_s1d3, O_RDONLY)); +} + +/* + * Test that linkat(2) with disconnected paths works under Landlock. This + * test moves s1d3 to s4d1. + */ +TEST_F_FORK(layout1_bind, path_disconnected_link) +{ + /* Ruleset to be applied after renaming s1d3 to s4d1. */ + const struct rule layer1[] = { + { + .path = dir_s4d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {} + }; + int bind_s1d3_fd; + + /* Removes unneeded files created by layout1, otherwise it will EEXIST. */ + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + ASSERT_LE(0, bind_s1d3_fd); + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + + /* Disconnects bind_s1d3_fd. */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, + strerror(errno)); + } + + /* Need this later to test different parent link. */ + ASSERT_EQ(0, mkdir(dir_s4d2, 0755)) + { + TH_LOG("Failed to create %s: %s", dir_s4d2, strerror(errno)); + } + + enforce_fs(_metadata, ACCESS_ALL, layer1); + + /* From disconnected to connected. */ + ASSERT_EQ(0, linkat(bind_s1d3_fd, file1_name, AT_FDCWD, file1_s2d2, 0)) + { + TH_LOG("Failed to link %s to %s via disconnected %s: %s", + file1_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); + } + + /* Tests that we can access via the new link... */ + EXPECT_EQ(0, test_open(file1_s2d2, O_RDONLY)) + { + TH_LOG("Failed to open newly linked %s: %s", file1_s2d2, + strerror(errno)); + } + + /* ...as well as the old one. */ + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)) + { + TH_LOG("Failed to open original %s: %s", file1_s4d1, + strerror(errno)); + } + + /* From connected to disconnected. */ + ASSERT_EQ(0, unlink(file1_s4d1)); + ASSERT_EQ(0, linkat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file2_name, 0)) + { + TH_LOG("Failed to link %s to %s via disconnected %s: %s", + file1_s2d2, file2_name, bind_dir_s1d3, strerror(errno)); + } + EXPECT_EQ(0, test_open(file2_s4d1, O_RDONLY)); + ASSERT_EQ(0, unlink(file1_s2d2)); + + /* From disconnected to disconnected (same parent). */ + ASSERT_EQ(0, + linkat(bind_s1d3_fd, file2_name, bind_s1d3_fd, file1_name, 0)) + { + TH_LOG("Failed to link %s to %s within disconnected %s: %s", + file2_name, file1_name, bind_dir_s1d3, strerror(errno)); + } + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)) + { + TH_LOG("Failed to open newly linked %s: %s", file1_s4d1, + strerror(errno)); + } + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)) + { + TH_LOG("Failed to open %s through newly created link under disconnected path: %s", + file1_name, strerror(errno)); + } + ASSERT_EQ(0, unlink(file2_s4d1)); + + /* From disconnected to disconnected (different parent). */ + ASSERT_EQ(0, + linkat(bind_s1d3_fd, file1_name, bind_s1d3_fd, "s4d2/f1", 0)) + { + TH_LOG("Failed to link %s to %s within disconnected %s: %s", + file1_name, "s4d2/f1", bind_dir_s1d3, strerror(errno)); + } + EXPECT_EQ(0, test_open(file1_s4d2, O_RDONLY)) + { + TH_LOG("Failed to open %s after link: %s", file1_s4d2, + strerror(errno)); + } + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, "s4d2/f1", O_RDONLY)) + { + TH_LOG("Failed to open %s through disconnected path after link: %s", + "s4d2/f1", strerror(errno)); + } +} + +/* + * layout4_disconnected_leafs with bind mount and renames: + * + * tmp + * ├── s1d1 + * │ └── s1d2 [source of the bind mount] + * │ ├── s1d31 + * │ │ └── s1d41 [now renamed beneath s3d1] + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d32 + * │ └── s1d42 [now renamed beneath s4d1] + * │ ├── f3 + * │ └── f4 + * ├── s2d1 + * │ └── s2d2 [bind mount of s1d2] + * │ ├── s1d31 + * │ │ └── s1d41 [opened FD, now renamed beneath s3d1] + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d32 + * │ └── s1d42 [opened FD, now renamed beneath s4d1] + * │ ├── f3 + * │ └── f4 + * ├── s3d1 + * │ └── s1d41 [renamed here] + * │ ├── f1 + * │ └── f2 + * └── s4d1 + * └── s1d42 [renamed here] + * ├── f3 + * └── f4 + */ +/* clang-format off */ +FIXTURE(layout4_disconnected_leafs) { + int s2d2_fd; +}; +/* clang-format on */ + +FIXTURE_SETUP(layout4_disconnected_leafs) +{ + prepare_layout(_metadata); + + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f1"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f2"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f3"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f4"); + create_directory(_metadata, TMP_DIR "/s2d1/s2d2"); + create_directory(_metadata, TMP_DIR "/s3d1"); + create_directory(_metadata, TMP_DIR "/s4d1"); + + self->s2d2_fd = + open(TMP_DIR "/s2d1/s2d2", O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, self->s2d2_fd); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(TMP_DIR "/s1d1/s1d2", TMP_DIR "/s2d1/s2d2", NULL, + MS_BIND, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN_PARENT(layout4_disconnected_leafs) +{ + /* umount(TMP_DIR "/s2d1") is handled by namespace lifetime. */ + + /* Removes files after renames. */ + remove_path(TMP_DIR "/s3d1/s1d41/f1"); + remove_path(TMP_DIR "/s3d1/s1d41/f2"); + remove_path(TMP_DIR "/s4d1/s1d42/f1"); + remove_path(TMP_DIR "/s4d1/s1d42/f3"); + remove_path(TMP_DIR "/s4d1/s1d42/f4"); + remove_path(TMP_DIR "/s4d1/s1d42/f5"); + + cleanup_layout(_metadata); +} + +FIXTURE_VARIANT(layout4_disconnected_leafs) +{ + /* + * Parent of the bind mount source. It should always be ignored when + * testing against files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_s1d1; + /* + * Source of bind mount (to s2d2). It should always be enforced when + * testing against files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_s1d2; + /* + * Original parent of s1d41. It should always be ignored when testing + * against files under the s1d41 disconnected directory. + */ + const __u64 allowed_s1d31; + /* + * Original parent of s1d42. It should always be ignored when testing + * against files under the s1d42 disconnected directory. + */ + const __u64 allowed_s1d32; + /* + * Opened and disconnected source directory. It should always be enforced + * when testing against files under the s1d41 disconnected directory. + */ + const __u64 allowed_s1d41; + /* + * Opened and disconnected source directory. It should always be enforced + * when testing against files under the s1d42 disconnected directory. + */ + const __u64 allowed_s1d42; + /* + * File in the s1d41 disconnected directory. It should always be enforced + * when testing against itself under the s1d41 disconnected directory. + */ + const __u64 allowed_f1; + /* + * File in the s1d41 disconnected directory. It should always be enforced + * when testing against itself under the s1d41 disconnected directory. + */ + const __u64 allowed_f2; + /* + * File in the s1d42 disconnected directory. It should always be enforced + * when testing against itself under the s1d42 disconnected directory. + */ + const __u64 allowed_f3; + /* + * Parent of the bind mount destination. It should always be enforced when + * testing against files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_s2d1; + /* + * Directory covered by the bind mount. It should always be ignored when + * testing against files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_s2d2; + /* + * New parent of the renamed s1d41. It should always be ignored when + * testing against files under the s1d41 disconnected directory. + */ + const __u64 allowed_s3d1; + /* + * New parent of the renamed s1d42. It should always be ignored when + * testing against files under the s1d42 disconnected directory. + */ + const __u64 allowed_s4d1; + + /* Expected result of the call to open([fd:s1d41]/f1, O_RDONLY). */ + const int expected_read_result; + /* Expected result of the call to renameat([fd:s1d41]/f1, [fd:s1d42]/f1). */ + const int expected_rename_result; + /* + * Expected result of the call to renameat([fd:s1d41]/f2, [fd:s1d42]/f3, + * RENAME_EXCHANGE). + */ + const int expected_exchange_result; + /* Expected result of the call to renameat([fd:s1d42]/f4, [fd:s1d42]/f5). */ + const int expected_same_dir_rename_result; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d1_mount_src_parent) { + /* clang-format on */ + .allowed_s1d1 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_refer) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_create) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_rename) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d31_s1d32_old_parent) { + /* clang-format on */ + .allowed_s1d31 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s1d32 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_refer) { + /* clang-format on */ + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE, + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_create) { + /* clang-format on */ + .allowed_s1d41 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s1d42 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_even) { + /* clang-format on */ + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* The destination directory has more access right. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_more) { + /* clang-format on */ + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_EXECUTE, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + /* Access denied. */ + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* The destination directory has less access right. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_less) { + /* clang-format on */ + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_EXECUTE, + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + /* Access allowed. */ + .expected_rename_result = 0, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_create) { + /* clang-format on */ + .allowed_s2d1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_refer) { + /* clang-format on */ + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_mini) { + /* clang-format on */ + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d2_covered_by_mount) { + /* clang-format on */ + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* Tests collect_domain_accesses(). */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_new_parent_refer) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_new_parent_create) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s4d1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, + s3d1_s4d1_disconnected_rename_even){ + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* The destination directory has more access right. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_disconnected_rename_more) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_EXECUTE, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + /* Access denied. */ + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* The destination directory has less access right. */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_disconnected_rename_less) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_EXECUTE, + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + /* Access allowed. */ + .expected_rename_result = 0, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, f1_f2_f3) { + /* clang-format on */ + .allowed_f1 = LANDLOCK_ACCESS_FS_READ_FILE, + .allowed_f2 = LANDLOCK_ACCESS_FS_READ_FILE, + .allowed_f3 = LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange) +{ + const __u64 handled_access = + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_REG; + const struct rule rules[] = { + { + .path = TMP_DIR "/s1d1", + .access = variant->allowed_s1d1, + }, + { + .path = TMP_DIR "/s1d1/s1d2", + .access = variant->allowed_s1d2, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d31", + .access = variant->allowed_s1d31, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d32", + .access = variant->allowed_s1d32, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41", + .access = variant->allowed_s1d41, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d32/s1d42", + .access = variant->allowed_s1d42, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f1", + .access = variant->allowed_f1, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f2", + .access = variant->allowed_f2, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f3", + .access = variant->allowed_f3, + }, + { + .path = TMP_DIR "/s2d1", + .access = variant->allowed_s2d1, + }, + /* s2d2_fd */ + { + .path = TMP_DIR "/s3d1", + .access = variant->allowed_s3d1, + }, + { + .path = TMP_DIR "/s4d1", + .access = variant->allowed_s4d1, + }, + {}, + }; + int ruleset_fd, s1d41_bind_fd, s1d42_bind_fd; + + ruleset_fd = create_ruleset(_metadata, handled_access, rules); + + /* Adds rule for the covered directory. */ + if (variant->allowed_s2d2) { + ASSERT_EQ(0, landlock_add_rule( + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &(struct landlock_path_beneath_attr){ + .parent_fd = self->s2d2_fd, + .allowed_access = + variant->allowed_s2d2, + }, + 0)); + } + EXPECT_EQ(0, close(self->s2d2_fd)); + + s1d41_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d31/s1d41", + O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, s1d41_bind_fd); + s1d42_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d32/s1d42", + O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, s1d42_bind_fd); + + /* Disconnects and checks source and destination directories. */ + EXPECT_EQ(0, test_open_rel(s1d41_bind_fd, "..", O_DIRECTORY)); + EXPECT_EQ(0, test_open_rel(s1d42_bind_fd, "..", O_DIRECTORY)); + /* Renames to make it accessible through s3d1/s1d41 */ + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d31/s1d41", + AT_FDCWD, TMP_DIR "/s3d1/s1d41")); + /* Renames to make it accessible through s4d1/s1d42 */ + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d32/s1d42", + AT_FDCWD, TMP_DIR "/s4d1/s1d42")); + EXPECT_EQ(ENOENT, test_open_rel(s1d41_bind_fd, "..", O_DIRECTORY)); + EXPECT_EQ(ENOENT, test_open_rel(s1d42_bind_fd, "..", O_DIRECTORY)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + EXPECT_EQ(variant->expected_read_result, + test_open_rel(s1d41_bind_fd, "f1", O_RDONLY)); + + EXPECT_EQ(variant->expected_rename_result, + test_renameat(s1d41_bind_fd, "f1", s1d42_bind_fd, "f1")); + EXPECT_EQ(variant->expected_exchange_result, + test_exchangeat(s1d41_bind_fd, "f2", s1d42_bind_fd, "f3")); + + EXPECT_EQ(variant->expected_same_dir_rename_result, + test_renameat(s1d42_bind_fd, "f4", s1d42_bind_fd, "f5")); +} + +/* + * layout5_disconnected_branch before rename: + * + * tmp + * ├── s1d1 + * │ └── s1d2 [source of the first bind mount] + * │ └── s1d3 + * │ ├── s1d41 + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d42 + * │ ├── f3 + * │ └── f4 + * ├── s2d1 + * │ └── s2d2 [source of the second bind mount] + * │ └── s2d3 + * │ └── s2d4 [first s1d2 bind mount] + * │ └── s1d3 + * │ ├── s1d41 + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d42 + * │ ├── f3 + * │ └── f4 + * ├── s3d1 + * │ └── s3d2 [second s2d2 bind mount] + * │ └── s2d3 + * │ └── s2d4 [first s1d2 bind mount] + * │ └── s1d3 + * │ ├── s1d41 + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d42 + * │ ├── f3 + * │ └── f4 + * └── s4d1 + * + * After rename: + * + * tmp + * ├── s1d1 + * │ └── s1d2 [source of the first bind mount] + * │ └── s1d3 + * │ ├── s1d41 + * │ │ ├── f1 + * │ │ └── f2 + * │ └── s1d42 + * │ ├── f3 + * │ └── f4 + * ├── s2d1 + * │ └── s2d2 [source of the second bind mount] + * ├── s3d1 + * │ └── s3d2 [second s2d2 bind mount] + * └── s4d1 + * └── s2d3 [renamed here] + * └── s2d4 [first s1d2 bind mount] + * └── s1d3 + * ├── s1d41 + * │ ├── f1 + * │ └── f2 + * └── s1d42 + * ├── f3 + * └── f4 + * + * Decision path for access from the s3d1/s3d2/s2d3/s2d4/s1d3 file descriptor: + * 1. first bind mount: s1d3 -> s1d2 + * 2. second bind mount: s2d3 + * 3. tmp mount: s4d1 -> tmp [disconnected branch] + * 4. second bind mount: s2d2 + * 5. tmp mount: s3d1 -> tmp + * 6. parent mounts: [...] -> / + * + * The s4d1 directory is evaluated even if it is not in the s2d2 mount. + */ + +/* clang-format off */ +FIXTURE(layout5_disconnected_branch) { + int s2d4_fd, s3d2_fd; +}; +/* clang-format on */ + +FIXTURE_SETUP(layout5_disconnected_branch) +{ + prepare_layout(_metadata); + + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f1"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f2"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f3"); + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f4"); + create_directory(_metadata, TMP_DIR "/s2d1/s2d2/s2d3/s2d4"); + create_directory(_metadata, TMP_DIR "/s3d1/s3d2"); + create_directory(_metadata, TMP_DIR "/s4d1"); + + self->s2d4_fd = open(TMP_DIR "/s2d1/s2d2/s2d3/s2d4", + O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, self->s2d4_fd); + + self->s3d2_fd = + open(TMP_DIR "/s3d1/s3d2", O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, self->s3d2_fd); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(TMP_DIR "/s1d1/s1d2", TMP_DIR "/s2d1/s2d2/s2d3/s2d4", + NULL, MS_BIND, NULL)); + ASSERT_EQ(0, mount(TMP_DIR "/s2d1/s2d2", TMP_DIR "/s3d1/s3d2", NULL, + MS_BIND | MS_REC, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN_PARENT(layout5_disconnected_branch) +{ + /* Bind mounts are handled by namespace lifetime. */ + + /* Removes files after renames. */ + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f1"); + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f2"); + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f1"); + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f3"); + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f4"); + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f5"); + + cleanup_layout(_metadata); +} + +FIXTURE_VARIANT(layout5_disconnected_branch) +{ + /* + * Parent of all files. It should always be enforced when testing against + * files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_base; + /* + * Parent of the first bind mount source. It should always be ignored when + * testing against files under the s1d41 or s1d42 disconnected directories. + */ + const __u64 allowed_s1d1; + const __u64 allowed_s1d2; + const __u64 allowed_s1d3; + const __u64 allowed_s2d1; + const __u64 allowed_s2d2; + const __u64 allowed_s2d3; + const __u64 allowed_s2d4; + const __u64 allowed_s3d1; + const __u64 allowed_s3d2; + const __u64 allowed_s4d1; + + /* Expected result of the call to open([fd:s1d3]/s1d41/f1, O_RDONLY). */ + const int expected_read_result; + /* + * Expected result of the call to renameat([fd:s1d3]/s1d41/f1, + * [fd:s1d3]/s1d42/f1). + */ + const int expected_rename_result; + /* + * Expected result of the call to renameat([fd:s1d3]/s1d41/f2, + * [fd:s1d3]/s1d42/f3, RENAME_EXCHANGE). + */ + const int expected_exchange_result; + /* + * Expected result of the call to renameat([fd:s1d3]/s1d42/f4, + * [fd:s1d3]/s1d42/f5). + */ + const int expected_same_dir_rename_result; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d1_mount1_src_parent) { + /* clang-format on */ + .allowed_s1d1 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_refer) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_create) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_rename) { + /* clang-format on */ + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_refer) { + /* clang-format on */ + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_create) { + /* clang-format on */ + .allowed_s1d3 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_rename) { + /* clang-format on */ + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_full) { + /* clang-format on */ + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d1_mount2_src_parent) { + /* clang-format on */ + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_refer) { + /* clang-format on */ + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_create) { + /* clang-format on */ + .allowed_s2d2 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_rename) { + /* clang-format on */ + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_refer) { + /* clang-format on */ + .allowed_s2d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_create) { + /* clang-format on */ + .allowed_s2d3 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_rename) { + /* clang-format on */ + .allowed_s2d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d4_mount1_dst) { + /* clang-format on */ + .allowed_s2d4 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_refer) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_create) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_rename) { + /* clang-format on */ + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d2_mount1_dst) { + /* clang-format on */ + .allowed_s3d2 = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_refer) { + /* clang-format on */ + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, + .expected_read_result = 0, + .expected_same_dir_rename_result = EACCES, + .expected_rename_result = EACCES, + .expected_exchange_result = EACCES, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_create) { + /* clang-format on */ + .allowed_s4d1 = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = 0, + .expected_same_dir_rename_result = 0, + .expected_rename_result = EXDEV, + .expected_exchange_result = EXDEV, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_rename) { + /* clang-format on */ + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, + .expected_read_result = EACCES, + .expected_same_dir_rename_result = 0, + .expected_rename_result = 0, + .expected_exchange_result = 0, +}; + +TEST_F_FORK(layout5_disconnected_branch, read_rename_exchange) +{ + const __u64 handled_access = + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_REG; + const struct rule rules[] = { + { + .path = TMP_DIR "/s1d1", + .access = variant->allowed_s1d1, + }, + { + .path = TMP_DIR "/s1d1/s1d2", + .access = variant->allowed_s1d2, + }, + { + .path = TMP_DIR "/s1d1/s1d2/s1d3", + .access = variant->allowed_s1d3, + }, + { + .path = TMP_DIR "/s2d1", + .access = variant->allowed_s2d1, + }, + { + .path = TMP_DIR "/s2d1/s2d2", + .access = variant->allowed_s2d2, + }, + { + .path = TMP_DIR "/s2d1/s2d2/s2d3", + .access = variant->allowed_s2d3, + }, + /* s2d4_fd */ + { + .path = TMP_DIR "/s3d1", + .access = variant->allowed_s3d1, + }, + /* s3d2_fd */ + { + .path = TMP_DIR "/s4d1", + .access = variant->allowed_s4d1, + }, + {}, + }; + int ruleset_fd, s1d3_bind_fd; + + ruleset_fd = create_ruleset(_metadata, handled_access, rules); + ASSERT_LE(0, ruleset_fd); + + /* Adds rules for the covered directories. */ + if (variant->allowed_s2d4) { + ASSERT_EQ(0, landlock_add_rule( + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &(struct landlock_path_beneath_attr){ + .parent_fd = self->s2d4_fd, + .allowed_access = + variant->allowed_s2d4, + }, + 0)); + } + EXPECT_EQ(0, close(self->s2d4_fd)); + + if (variant->allowed_s3d2) { + ASSERT_EQ(0, landlock_add_rule( + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, + &(struct landlock_path_beneath_attr){ + .parent_fd = self->s3d2_fd, + .allowed_access = + variant->allowed_s3d2, + }, + 0)); + } + EXPECT_EQ(0, close(self->s3d2_fd)); + + s1d3_bind_fd = open(TMP_DIR "/s3d1/s3d2/s2d3/s2d4/s1d3", + O_DIRECTORY | O_PATH | O_CLOEXEC); + ASSERT_LE(0, s1d3_bind_fd); + + /* Disconnects and checks source and destination directories. */ + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "..", O_DIRECTORY)); + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "../..", O_DIRECTORY)); + /* Renames to make it accessible through s3d1/s1d41 */ + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s2d1/s2d2/s2d3", + AT_FDCWD, TMP_DIR "/s4d1/s2d3")); + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "..", O_DIRECTORY)); + EXPECT_EQ(ENOENT, test_open_rel(s1d3_bind_fd, "../..", O_DIRECTORY)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + EXPECT_EQ(variant->expected_read_result, + test_open_rel(s1d3_bind_fd, "s1d41/f1", O_RDONLY)); + + EXPECT_EQ(variant->expected_rename_result, + test_renameat(s1d3_bind_fd, "s1d41/f1", s1d3_bind_fd, + "s1d42/f1")); + EXPECT_EQ(variant->expected_exchange_result, + test_exchangeat(s1d3_bind_fd, "s1d41/f2", s1d3_bind_fd, + "s1d42/f3")); + + EXPECT_EQ(variant->expected_same_dir_rename_result, + test_renameat(s1d3_bind_fd, "s1d42/f4", s1d3_bind_fd, + "s1d42/f5")); +} + +#define LOWER_BASE TMP_DIR "/lower" +#define LOWER_DATA LOWER_BASE "/data" +static const char lower_fl1[] = LOWER_DATA "/fl1"; +static const char lower_dl1[] = LOWER_DATA "/dl1"; +static const char lower_dl1_fl2[] = LOWER_DATA "/dl1/fl2"; +static const char lower_fo1[] = LOWER_DATA "/fo1"; +static const char lower_do1[] = LOWER_DATA "/do1"; +static const char lower_do1_fo2[] = LOWER_DATA "/do1/fo2"; +static const char lower_do1_fl3[] = LOWER_DATA "/do1/fl3"; + +static const char (*lower_base_files[])[] = { + &lower_fl1, + &lower_fo1, + NULL, +}; +static const char (*lower_base_directories[])[] = { + &lower_dl1, + &lower_do1, + NULL, +}; +static const char (*lower_sub_files[])[] = { + &lower_dl1_fl2, + &lower_do1_fo2, + &lower_do1_fl3, + NULL, +}; + +#define UPPER_BASE TMP_DIR "/upper" +#define UPPER_DATA UPPER_BASE "/data" +#define UPPER_WORK UPPER_BASE "/work" +static const char upper_fu1[] = UPPER_DATA "/fu1"; +static const char upper_du1[] = UPPER_DATA "/du1"; +static const char upper_du1_fu2[] = UPPER_DATA "/du1/fu2"; +static const char upper_fo1[] = UPPER_DATA "/fo1"; +static const char upper_do1[] = UPPER_DATA "/do1"; +static const char upper_do1_fo2[] = UPPER_DATA "/do1/fo2"; +static const char upper_do1_fu3[] = UPPER_DATA "/do1/fu3"; + +static const char (*upper_base_files[])[] = { + &upper_fu1, + &upper_fo1, + NULL, +}; +static const char (*upper_base_directories[])[] = { + &upper_du1, + &upper_do1, + NULL, +}; +static const char (*upper_sub_files[])[] = { + &upper_du1_fu2, + &upper_do1_fo2, + &upper_do1_fu3, + NULL, +}; + +#define MERGE_BASE TMP_DIR "/merge" +#define MERGE_DATA MERGE_BASE "/data" +static const char merge_fl1[] = MERGE_DATA "/fl1"; +static const char merge_dl1[] = MERGE_DATA "/dl1"; +static const char merge_dl1_fl2[] = MERGE_DATA "/dl1/fl2"; +static const char merge_fu1[] = MERGE_DATA "/fu1"; +static const char merge_du1[] = MERGE_DATA "/du1"; +static const char merge_du1_fu2[] = MERGE_DATA "/du1/fu2"; +static const char merge_fo1[] = MERGE_DATA "/fo1"; +static const char merge_do1[] = MERGE_DATA "/do1"; +static const char merge_do1_fo2[] = MERGE_DATA "/do1/fo2"; +static const char merge_do1_fl3[] = MERGE_DATA "/do1/fl3"; +static const char merge_do1_fu3[] = MERGE_DATA "/do1/fu3"; + +static const char (*merge_base_files[])[] = { + &merge_fl1, + &merge_fu1, + &merge_fo1, + NULL, +}; +static const char (*merge_base_directories[])[] = { + &merge_dl1, + &merge_du1, + &merge_do1, + NULL, +}; +static const char (*merge_sub_files[])[] = { + &merge_dl1_fl2, &merge_du1_fu2, &merge_do1_fo2, + &merge_do1_fl3, &merge_do1_fu3, NULL, +}; + +/* + * layout2_overlay hierarchy: + * + * tmp + * ├── lower + * │ └── data + * │ ├── dl1 + * │ │ └── fl2 + * │ ├── do1 + * │ │ ├── fl3 + * │ │ └── fo2 + * │ ├── fl1 + * │ └── fo1 + * ├── merge + * │ └── data + * │ ├── dl1 + * │ │ └── fl2 + * │ ├── do1 + * │ │ ├── fl3 + * │ │ ├── fo2 + * │ │ └── fu3 + * │ ├── du1 + * │ │ └── fu2 + * │ ├── fl1 + * │ ├── fo1 + * │ └── fu1 + * └── upper + * ├── data + * │ ├── do1 + * │ │ ├── fo2 + * │ │ └── fu3 + * │ ├── du1 + * │ │ └── fu2 + * │ ├── fo1 + * │ └── fu1 + * └── work + * └── work + */ + +FIXTURE(layout2_overlay) +{ + bool skip_test; +}; + +FIXTURE_SETUP(layout2_overlay) +{ + if (!supports_filesystem("overlay")) { + self->skip_test = true; + SKIP(return, "overlayfs is not supported (setup)"); + } + + prepare_layout(_metadata); + + create_directory(_metadata, LOWER_BASE); + set_cap(_metadata, CAP_SYS_ADMIN); + /* Creates tmpfs mount points to get deterministic overlayfs. */ + ASSERT_EQ(0, mount_opt(&mnt_tmp, LOWER_BASE)); + clear_cap(_metadata, CAP_SYS_ADMIN); + create_file(_metadata, lower_fl1); + create_file(_metadata, lower_dl1_fl2); + create_file(_metadata, lower_fo1); + create_file(_metadata, lower_do1_fo2); + create_file(_metadata, lower_do1_fl3); + + create_directory(_metadata, UPPER_BASE); + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount_opt(&mnt_tmp, UPPER_BASE)); + clear_cap(_metadata, CAP_SYS_ADMIN); + create_file(_metadata, upper_fu1); + create_file(_metadata, upper_du1_fu2); + create_file(_metadata, upper_fo1); + create_file(_metadata, upper_do1_fo2); + create_file(_metadata, upper_do1_fu3); + ASSERT_EQ(0, mkdir(UPPER_WORK, 0700)); + + create_directory(_metadata, MERGE_DATA); + set_cap(_metadata, CAP_SYS_ADMIN); + set_cap(_metadata, CAP_DAC_OVERRIDE); + ASSERT_EQ(0, mount("overlay", MERGE_DATA, "overlay", 0, + "lowerdir=" LOWER_DATA ",upperdir=" UPPER_DATA + ",workdir=" UPPER_WORK)); + clear_cap(_metadata, CAP_DAC_OVERRIDE); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN_PARENT(layout2_overlay) +{ + if (self->skip_test) + SKIP(return, "overlayfs is not supported (teardown)"); + + EXPECT_EQ(0, remove_path(lower_do1_fl3)); + EXPECT_EQ(0, remove_path(lower_dl1_fl2)); + EXPECT_EQ(0, remove_path(lower_fl1)); + EXPECT_EQ(0, remove_path(lower_do1_fo2)); + EXPECT_EQ(0, remove_path(lower_fo1)); + + /* umount(LOWER_BASE)) is handled by namespace lifetime. */ + EXPECT_EQ(0, remove_path(LOWER_BASE)); + + EXPECT_EQ(0, remove_path(upper_do1_fu3)); + EXPECT_EQ(0, remove_path(upper_du1_fu2)); + EXPECT_EQ(0, remove_path(upper_fu1)); + EXPECT_EQ(0, remove_path(upper_do1_fo2)); + EXPECT_EQ(0, remove_path(upper_fo1)); + EXPECT_EQ(0, remove_path(UPPER_WORK "/work")); + + /* umount(UPPER_BASE)) is handled by namespace lifetime. */ + EXPECT_EQ(0, remove_path(UPPER_BASE)); + + /* umount(MERGE_DATA)) is handled by namespace lifetime. */ + EXPECT_EQ(0, remove_path(MERGE_DATA)); + + cleanup_layout(_metadata); +} + +TEST_F_FORK(layout2_overlay, no_restriction) +{ + if (self->skip_test) + SKIP(return, "overlayfs is not supported (test)"); + + ASSERT_EQ(0, test_open(lower_fl1, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_dl1, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_dl1_fl2, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_fo1, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_do1, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_do1_fo2, O_RDONLY)); + ASSERT_EQ(0, test_open(lower_do1_fl3, O_RDONLY)); + + ASSERT_EQ(0, test_open(upper_fu1, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_du1, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_du1_fu2, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_fo1, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_do1, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_do1_fo2, O_RDONLY)); + ASSERT_EQ(0, test_open(upper_do1_fu3, O_RDONLY)); + + ASSERT_EQ(0, test_open(merge_fl1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_dl1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_dl1_fl2, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_fu1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_du1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_du1_fu2, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_fo1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_do1, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_do1_fo2, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_do1_fl3, O_RDONLY)); + ASSERT_EQ(0, test_open(merge_do1_fu3, O_RDONLY)); +} + +#define for_each_path(path_list, path_entry, i) \ + for (i = 0, path_entry = *path_list[i]; path_list[i]; \ + path_entry = *path_list[++i]) + +TEST_F_FORK(layout2_overlay, same_content_different_file) +{ + /* Sets access right on parent directories of both layers. */ + const struct rule layer1_base[] = { + { + .path = LOWER_BASE, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = UPPER_BASE, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = MERGE_BASE, + .access = ACCESS_RW, + }, + {}, + }; + const struct rule layer2_data[] = { + { + .path = LOWER_DATA, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = UPPER_DATA, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = MERGE_DATA, + .access = ACCESS_RW, + }, + {}, + }; + /* Sets access right on directories inside both layers. */ + const struct rule layer3_subdirs[] = { + { + .path = lower_dl1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = lower_do1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = upper_du1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = upper_do1, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = merge_dl1, + .access = ACCESS_RW, + }, + { + .path = merge_du1, + .access = ACCESS_RW, + }, + { + .path = merge_do1, + .access = ACCESS_RW, + }, + {}, + }; + /* Tighten access rights to the files. */ + const struct rule layer4_files[] = { + { + .path = lower_dl1_fl2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = lower_do1_fo2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = lower_do1_fl3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = upper_du1_fu2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = upper_do1_fo2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = upper_do1_fu3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = merge_dl1_fl2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = merge_du1_fu2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = merge_do1_fo2, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = merge_do1_fl3, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + { + .path = merge_do1_fu3, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + const struct rule layer5_merge_only[] = { + { + .path = MERGE_DATA, + .access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_WRITE_FILE, + }, + {}, + }; + size_t i; + const char *path_entry; + + if (self->skip_test) + SKIP(return, "overlayfs is not supported (test)"); + + /* Sets rules on base directories (i.e. outside overlay scope). */ + enforce_fs(_metadata, ACCESS_RW, layer1_base); + + /* Checks lower layer. */ + for_each_path(lower_base_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + for_each_path(lower_base_directories, path_entry, i) { + ASSERT_EQ(EACCES, + test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(lower_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + /* Checks upper layer. */ + for_each_path(upper_base_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + for_each_path(upper_base_directories, path_entry, i) { + ASSERT_EQ(EACCES, + test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(upper_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + /* + * Checks that access rights are independent from the lower and upper + * layers: write access to upper files viewed through the merge point + * is still allowed, and write access to lower file viewed (and copied) + * through the merge point is still allowed. + */ + for_each_path(merge_base_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + for_each_path(merge_base_directories, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(merge_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + + /* Sets rules on data directories (i.e. inside overlay scope). */ + enforce_fs(_metadata, ACCESS_RW, layer2_data); + + /* Checks merge. */ + for_each_path(merge_base_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + for_each_path(merge_base_directories, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(merge_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + + /* Same checks with tighter rules. */ + enforce_fs(_metadata, ACCESS_RW, layer3_subdirs); + + /* Checks changes for lower layer. */ + for_each_path(lower_base_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDONLY)); + } + /* Checks changes for upper layer. */ + for_each_path(upper_base_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDONLY)); + } + /* Checks all merge accesses. */ + for_each_path(merge_base_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDWR)); + } + for_each_path(merge_base_directories, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(merge_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + + /* Sets rules directly on overlayed files. */ + enforce_fs(_metadata, ACCESS_RW, layer4_files); + + /* Checks unchanged accesses on lower layer. */ + for_each_path(lower_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + /* Checks unchanged accesses on upper layer. */ + for_each_path(upper_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(path_entry, O_WRONLY)); + } + /* Checks all merge accesses. */ + for_each_path(merge_base_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDWR)); + } + for_each_path(merge_base_directories, path_entry, i) { + ASSERT_EQ(EACCES, + test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(merge_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } + + /* Only allowes access to the merge hierarchy. */ + enforce_fs(_metadata, ACCESS_RW, layer5_merge_only); + + /* Checks new accesses on lower layer. */ + for_each_path(lower_sub_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDONLY)); + } + /* Checks new accesses on upper layer. */ + for_each_path(upper_sub_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDONLY)); + } + /* Checks all merge accesses. */ + for_each_path(merge_base_files, path_entry, i) { + ASSERT_EQ(EACCES, test_open(path_entry, O_RDWR)); + } + for_each_path(merge_base_directories, path_entry, i) { + ASSERT_EQ(EACCES, + test_open(path_entry, O_RDONLY | O_DIRECTORY)); + } + for_each_path(merge_sub_files, path_entry, i) { + ASSERT_EQ(0, test_open(path_entry, O_RDWR)); + } +} + +FIXTURE(layout3_fs) +{ + bool has_created_dir; + bool has_created_file; + bool skip_test; +}; + +FIXTURE_VARIANT(layout3_fs) +{ + const struct mnt_opt mnt; + const char *const file_path; + unsigned int cwd_fs_magic; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(layout3_fs, tmpfs) { + /* clang-format on */ + .mnt = { + .type = "tmpfs", + .data = MNT_TMP_DATA, + }, + .file_path = file1_s1d1, +}; + +FIXTURE_VARIANT_ADD(layout3_fs, ramfs) { + .mnt = { + .type = "ramfs", + .data = "mode=700", + }, + .file_path = TMP_DIR "/dir/file", +}; + +FIXTURE_VARIANT_ADD(layout3_fs, cgroup2) { + .mnt = { + .type = "cgroup2", + }, + .file_path = TMP_DIR "/test/cgroup.procs", +}; + +FIXTURE_VARIANT_ADD(layout3_fs, proc) { + .mnt = { + .type = "proc", + }, + .file_path = TMP_DIR "/self/status", +}; + +FIXTURE_VARIANT_ADD(layout3_fs, sysfs) { + .mnt = { + .type = "sysfs", + }, + .file_path = TMP_DIR "/kernel/notes", +}; + +FIXTURE_VARIANT_ADD(layout3_fs, hostfs) { + .mnt = { + .source = TMP_DIR, + .flags = MS_BIND, + }, + .file_path = TMP_DIR "/dir/file", + .cwd_fs_magic = HOSTFS_SUPER_MAGIC, +}; + +static char *dirname_alloc(const char *path) +{ + char *dup; + + if (!path) + return NULL; + + dup = strdup(path); + if (!dup) + return NULL; + + return dirname(dup); +} + +FIXTURE_SETUP(layout3_fs) +{ + struct stat statbuf; + char *dir_path = dirname_alloc(variant->file_path); + + if (!supports_filesystem(variant->mnt.type) || + !cwd_matches_fs(variant->cwd_fs_magic)) { + self->skip_test = true; + SKIP(return, "this filesystem is not supported (setup)"); + } + + prepare_layout_opt(_metadata, &variant->mnt); + + /* Creates directory when required. */ + if (stat(dir_path, &statbuf)) { + set_cap(_metadata, CAP_DAC_OVERRIDE); + EXPECT_EQ(0, mkdir(dir_path, 0700)) + { + TH_LOG("Failed to create directory \"%s\": %s", + dir_path, strerror(errno)); + } + self->has_created_dir = true; + clear_cap(_metadata, CAP_DAC_OVERRIDE); + } + + /* Creates file when required. */ + if (stat(variant->file_path, &statbuf)) { + int fd; + + set_cap(_metadata, CAP_DAC_OVERRIDE); + fd = creat(variant->file_path, 0600); + EXPECT_LE(0, fd) + { + TH_LOG("Failed to create file \"%s\": %s", + variant->file_path, strerror(errno)); + } + EXPECT_EQ(0, close(fd)); + self->has_created_file = true; + clear_cap(_metadata, CAP_DAC_OVERRIDE); + } + + free(dir_path); +} + +FIXTURE_TEARDOWN_PARENT(layout3_fs) +{ + if (self->skip_test) + SKIP(return, "this filesystem is not supported (teardown)"); + + if (self->has_created_file) { + set_cap(_metadata, CAP_DAC_OVERRIDE); + /* + * Don't check for error because the file might already + * have been removed (cf. release_inode test). + */ + unlink(variant->file_path); + clear_cap(_metadata, CAP_DAC_OVERRIDE); + } + + if (self->has_created_dir) { + char *dir_path = dirname_alloc(variant->file_path); + + set_cap(_metadata, CAP_DAC_OVERRIDE); + /* + * Don't check for error because the directory might already + * have been removed (cf. release_inode test). + */ + rmdir(dir_path); + clear_cap(_metadata, CAP_DAC_OVERRIDE); + free(dir_path); + } + + cleanup_layout(_metadata); +} + +static void layer3_fs_tag_inode(struct __test_metadata *const _metadata, + FIXTURE_DATA(layout3_fs) * self, + const FIXTURE_VARIANT(layout3_fs) * variant, + const char *const rule_path) +{ + const struct rule layer1_allow_read_file[] = { + { + .path = rule_path, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + const char *const dev_null_path = "/dev/null"; + + if (self->skip_test) + SKIP(return, "this filesystem is not supported (test)"); + + /* Checks without Landlock. */ + EXPECT_EQ(0, test_open(dev_null_path, O_RDONLY | O_CLOEXEC)); + EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, + layer1_allow_read_file); + + EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC)); + EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC)); + + /* Forbids directory reading. */ + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL); + + /* Checks with Landlock and forbidden access. */ + EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC)); + EXPECT_EQ(EACCES, test_open(variant->file_path, O_RDONLY | O_CLOEXEC)); +} + +/* Matrix of tests to check file hierarchy evaluation. */ + +TEST_F_FORK(layout3_fs, tag_inode_dir_parent) +{ + /* The current directory must not be the root for this test. */ + layer3_fs_tag_inode(_metadata, self, variant, "."); +} + +TEST_F_FORK(layout3_fs, tag_inode_dir_mnt) +{ + layer3_fs_tag_inode(_metadata, self, variant, TMP_DIR); +} + +TEST_F_FORK(layout3_fs, tag_inode_dir_child) +{ + char *dir_path = dirname_alloc(variant->file_path); + + layer3_fs_tag_inode(_metadata, self, variant, dir_path); + free(dir_path); +} + +TEST_F_FORK(layout3_fs, tag_inode_file) +{ + layer3_fs_tag_inode(_metadata, self, variant, variant->file_path); +} + +/* Light version of layout1.release_inodes */ +TEST_F_FORK(layout3_fs, release_inodes) +{ + const struct rule layer1[] = { + { + .path = TMP_DIR, + .access = LANDLOCK_ACCESS_FS_READ_DIR, + }, + {}, + }; + int ruleset_fd; + + if (self->skip_test) + SKIP(return, "this filesystem is not supported (test)"); + + /* Clean up for the teardown to not fail. */ + if (self->has_created_file) + EXPECT_EQ(0, remove_path(variant->file_path)); + + if (self->has_created_dir) { + char *dir_path = dirname_alloc(variant->file_path); + + /* Don't check for error because of cgroup specificities. */ + remove_path(dir_path); + free(dir_path); + } + + ruleset_fd = + create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1); + + /* Unmount the filesystem while it is being used by a ruleset. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, umount(TMP_DIR)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + /* Replaces with a new mount point to simplify FIXTURE_TEARDOWN. */ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount_opt(&mnt_tmp, TMP_DIR)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + /* Checks that access to the new mount point is denied. */ + 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 assumed 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); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL); + + 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. + */ + +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); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + /* + * 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; + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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; + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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; + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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; + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, NULL); + + 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)); + + enforce_fs(_metadata, + LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, + NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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)); + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + /* + * 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, + }, + {}, + }; + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1); + + 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; + + enforce_fs(_metadata, ACCESS_ALL, NULL); + + 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; + + enforce_fs(_metadata, ACCESS_ALL & ~LANDLOCK_ACCESS_FS_READ_FILE, NULL); + + 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, resolve_unix) +{ + struct audit_records records; + const char *const path = "sock"; + int srv_fd, cli_fd, status; + pid_t child_pid; + + srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path); + + child_pid = fork(); + ASSERT_LE(0, child_pid); + if (!child_pid) { + enforce_fs(_metadata, ACCESS_ALL, NULL); + + cli_fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, cli_fd); + EXPECT_EQ(EACCES, + test_connect_named_unix(_metadata, cli_fd, path)); + + EXPECT_EQ(0, close(cli_fd)); + _exit(_metadata->exit_code); + } + + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd, + "fs\\.resolve_unix", path, NULL)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + EXPECT_EQ(0, close(srv_fd)); +} + +TEST_F(audit_layout1, mount) +{ + struct audit_records records; + + enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL); + + 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); +} + +static bool debug_quiet_tests; + +FIXTURE(audit_quiet_layout1) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_quiet_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); + clear_cap(_metadata, CAP_AUDIT_CONTROL); + + if (getenv("DEBUG_QUIET_TESTS")) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT(audit_quiet_layout1) +{ + remove_layout1(_metadata); + cleanup_layout(_metadata); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(-1, NULL)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +struct a_rule { + const char *path; + __u64 access; + bool quiet; +}; + +struct a_layer { + __u64 handled_access_fs; + __u64 quiet_access_fs; + struct a_rule rules[6]; + __u64 restrict_flags; +}; + +struct a_target { + /* File/dir to try open. */ + const char *target; + /* Open mode (one of O_RDONLY, O_WRONLY, or O_RDWR). */ + int open_mode; + /* Should open succeed? */ + bool expect_open_success; + /* If open fails, whether to expect an audit log for read. */ + bool audit_read_blocked; + /* If open fails, whether to expect an audit log for write. */ + bool audit_write_blocked; + /* If ftruncate() is expected to be allowed. */ + bool expect_truncate_success; + /* If ftruncate fails, whether to expect an audit log. */ + bool audit_truncate; + /* + * If ioctl() is expected to be allowed (ioctl not attempted if neither + * this nor expect_ioctl_denied is set). + */ + bool expect_ioctl_allowed; + /* If ioctl() is expected to be denied. */ + bool expect_ioctl_denied; + /* If ioctl fails, whether to expect an audit log. */ + bool audit_ioctl; +}; + +#define AUDIT_QUIET_MAX_TARGETS 10 + +FIXTURE_VARIANT(audit_quiet_layout1) +{ + struct a_layer layers[3]; + struct a_target targets[AUDIT_QUIET_MAX_TARGETS]; +}; + +#define FS_R LANDLOCK_ACCESS_FS_READ_FILE +#define FS_W LANDLOCK_ACCESS_FS_WRITE_FILE +#define FS_TRUNC LANDLOCK_ACCESS_FS_TRUNCATE +#define FS_IOCTL LANDLOCK_ACCESS_FS_IOCTL_DEV + +static int sprint_access_bits(char *buf, size_t buflen, __u64 access) +{ + size_t offset = 0; + + if (buflen < strlen("rwti make_reg remove_file refer") + 1) + abort(); + + buf[0] = '\0'; + if (access & FS_R) + offset += snprintf(buf + offset, buflen - offset, "r"); + if (access & FS_W) + offset += snprintf(buf + offset, buflen - offset, "w"); + if (access & FS_TRUNC) + offset += snprintf(buf + offset, buflen - offset, "t"); + if (access & FS_IOCTL) + offset += snprintf(buf + offset, buflen - offset, "i"); + if (access & LANDLOCK_ACCESS_FS_MAKE_REG) + offset += snprintf(buf + offset, buflen - offset, ",make_reg"); + if (access & LANDLOCK_ACCESS_FS_REMOVE_FILE) + offset += + snprintf(buf + offset, buflen - offset, ",remove_file"); + if (access & LANDLOCK_ACCESS_FS_REFER) + offset += snprintf(buf + offset, buflen - offset, ",refer"); + + if (buf[0] == ',') { + offset--; + memmove(buf, buf + 1, offset); + buf[offset] = '\0'; + } + + return offset; +} + +static int apply_a_layer(struct __test_metadata *const _metadata, + const struct a_layer *l) +{ + struct landlock_ruleset_attr rs_attr = { + .handled_access_fs = l->handled_access_fs, + .quiet_access_fs = l->quiet_access_fs, + }; + int rs_fd; + int i; + const struct a_rule *r; + char handled_access_s[33], quiet_access_s[33], rule_access_s[33]; + + if (!l->handled_access_fs) + return 0; + + rs_fd = landlock_create_ruleset(&rs_attr, sizeof(rs_attr), 0); + ASSERT_LE(0, rs_fd); + + for (i = 0; i < ARRAY_SIZE(l->rules); i++) { + r = &l->rules[i]; + if (!r->path) + continue; + + add_path_beneath(_metadata, rs_fd, r->access, r->path, + r->quiet ? LANDLOCK_ADD_RULE_QUIET : 0); + } + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(rs_fd, l->restrict_flags)) + { + TH_LOG("Failed to enforce ruleset: %s", strerror(errno)); + } + ASSERT_EQ(0, close(rs_fd)); + + if (debug_quiet_tests) { + sprint_access_bits(handled_access_s, sizeof(handled_access_s), + l->handled_access_fs); + sprint_access_bits(quiet_access_s, sizeof(quiet_access_s), + l->quiet_access_fs); + TH_LOG("applied layer: handled=%s quiet=%s restrict_flags=0x%llx", + handled_access_s, quiet_access_s, + (unsigned long long)l->restrict_flags); + for (i = 0; i < ARRAY_SIZE(l->rules); i++) { + r = &l->rules[i]; + if (!r->path) + continue; + + sprint_access_bits(rule_access_s, sizeof(rule_access_s), + r->access); + TH_LOG(" rule[%d]: path=%s access=%s quiet=%d", i, + r->path, rule_access_s, r->quiet); + } + } + return 0; +} + +void audit_quiet_layout1_test_body(struct __test_metadata *const _metadata, + FIXTURE_DATA(audit_quiet_layout1) * self, + const struct a_target *targets) +{ + struct audit_records records = {}; + int i; + const struct a_target *target; + int fd = -1; + int open_mode; + int ret; + bool expect_audit; + const char *blocker; + + for (i = 0; i < AUDIT_QUIET_MAX_TARGETS; i++) { + target = &targets[i]; + if (!target->target) + continue; + + open_mode = target->open_mode & (O_RDONLY | O_WRONLY | O_RDWR); + + EXPECT_TRUE(open_mode == O_RDONLY || open_mode == O_WRONLY || + open_mode == O_RDWR); + + if (target->expect_open_success) { + EXPECT_FALSE(target->audit_read_blocked); + EXPECT_FALSE(target->audit_write_blocked); + } + if (target->expect_truncate_success) + EXPECT_TRUE(target->expect_open_success && + !target->audit_truncate); + + if (debug_quiet_tests) + TH_LOG("Try open \"%s\" with %s%s", target->target, + open_mode != O_WRONLY ? "r" : "", + open_mode != O_RDONLY ? "w" : ""); + + fd = openat(AT_FDCWD, target->target, open_mode | O_CLOEXEC); + if (target->expect_open_success) { + ASSERT_LE(0, fd) + { + TH_LOG("Failed to open \"%s\": %s", + target->target, strerror(errno)); + }; + } else { + ASSERT_EQ(-1, fd); + ASSERT_EQ(EACCES, errno); + } + + expect_audit = true; + + if (target->audit_read_blocked && target->audit_write_blocked) + blocker = "fs\\.write_file,fs\\.read_file"; + else if (target->audit_read_blocked) + blocker = "fs\\.read_file"; + else if (target->audit_write_blocked) + blocker = "fs\\.write_file"; + else + expect_audit = false; + + if (expect_audit) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + blocker, target->target)); + + /* Check that we see no (other) logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); + + if (target->expect_open_success && fd >= 0) { + if (debug_quiet_tests) + TH_LOG("Try ftruncate \"%s\"", target->target); + + ret = ftruncate(fd, 0); + if (target->expect_truncate_success) { + ASSERT_EQ(0, ret); + } else { + ASSERT_EQ(-1, ret); + if (open_mode != O_RDONLY) + ASSERT_EQ(EACCES, errno); + } + + if (target->audit_truncate) + ASSERT_EQ(0, matches_log_fs(_metadata, + self->audit_fd, + "fs\\.truncate", + target->target)); + + if (target->expect_ioctl_allowed || + target->expect_ioctl_denied) { + if (debug_quiet_tests) + TH_LOG("Try ioctl FIONREAD on \"%s\"", + target->target); + + ret = ioctl_error(_metadata, fd, FIONREAD); + if (target->expect_ioctl_allowed) { + ASSERT_NE(EACCES, ret); + } else { + ASSERT_EQ(EACCES, ret); + } + } + + if (target->audit_ioctl) + ASSERT_EQ(0, matches_log_fs_extra( + _metadata, self->audit_fd, + "fs\\.ioctl_dev", + target->target, + " ioctlcmd=0x541b\\+")); + + /* Check that we see no other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, + &records)); + ASSERT_EQ(0, records.access); + ASSERT_EQ(0, close(fd)); + } + } +} + +TEST_F(audit_quiet_layout1, base) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_simple) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* + * Quiet flag only takes effect if all blocked access bits are + * quieted, otherwise audit log emitted as normal (with all + * blockers) + */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_read) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_W, + .rules = { + { .path = dir_s1d1, .access = FS_R, .quiet = true }, + /* Quiet flags inherit down and are not overridden */ + { .path = file1_s1d1, .access = FS_R, .quiet = false }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read ok */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .expect_open_success = true, + }, + /* Write quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + /* Read allowed, write quieted so no audit */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d2, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_WRONLY, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Some access not quieted */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_allow_write) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + }, + }, + }, + .targets = { + /* Read quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Truncate not quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Write allowed, read quieted so no audit */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_write_quiet_trunc) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_W, .quiet = false }, + }, + }, + }, + .targets = { + /* Read not allowed and not quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Truncate quieted */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + /* Not covered by quiet (truncate) */ + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet (read/write) */ + { + .target = file1_s3d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_rw_quiet_trunc) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_TRUNC, + .rules = { + { .path = dir_s1d1, .access = FS_R | FS_W, .quiet = true }, + { .path = dir_s2d1, .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .audit_truncate = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_all) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d1, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = 0, .quiet = true }, + { .path = dir_s3d1, .access = FS_W, .quiet = false }, + { .path = "/dev/zero", .access = FS_R, .quiet = false }, + { .path = "/dev/null", .access = FS_R, .quiet = true }, + }, + }, + }, + .targets = { + /* No logs */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Truncate quieted - no log */ + { + .target = file1_s2d1, + .open_mode = O_RDWR, + .expect_open_success = true, + }, + /* Truncate not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + /* Not covered by quiet */ + { + .target = file1_s3d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + /* Single file quiet */ + { + .target = file1_s2d3, + .open_mode = O_RDWR, + }, + /* Wrong file */ + { + .target = file2_s2d3, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Ioctl quieted */ + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + /* Ioctl not quieted */ + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_across_mountpoint) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s3d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + { + .target = file1_s3d3, + .open_mode = O_RDONLY, + }, + /* Not covered by quiet */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .audit_read_blocked = true, + .audit_write_blocked = true, + }, + /* Access not quieted */ + { + .target = file1_s3d3, + .open_mode = O_WRONLY, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, allow_all_quiet) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_allowed = true, + }, + }, +}; + +/* + * With LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, it doesn't matter what the + * quiet flags below the layer say. + */ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, subdomains_off) { + .layers = { + { + .handled_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF, + .rules = { + { .path = "/", .access = FS_R, .quiet = false }, + } + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +/* + * With LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, it doesn't matter what the + * quiet flags on the layer say. + */ +FIXTURE_VARIANT_ADD(audit_quiet_layout1, same_exec_off) { + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R, + .restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF, + .rules = { + { .path = dir_s1d1, .access = 0, .quiet = true }, + { .path = file1_s2d2, .access = FS_R | FS_W, .quiet = true }, + { .path = file1_s2d3, .access = FS_R | FS_W, .quiet = false }, + { .path = "/dev/null", .access = FS_R | FS_W, .quiet = true }, + { .path = "/dev/zero", .access = FS_R | FS_W, .quiet = false }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s2d2, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = file1_s2d3, + .open_mode = O_RDWR, + .expect_open_success = true, + /* No audit_truncate */ + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + /* No audit_ioctl */ + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_1) { + /* Here, rules that deny access are always quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = true, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_2) { + /* Here, rules that deny access are never quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = false + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = true + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = false + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = true + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = false + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_3) { + /* This time only the second layer quiets things. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + .audit_ioctl = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_quiet_access) { + /* Here, rules that deny access are always quiet. */ + .layers = { + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = "/dev/null", + .access = FS_R, + .quiet = true, + }, + { + .path = "/dev/zero", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_IOCTL, + .rules = { + { + .path = dir_s1d1, + .access = FS_R | FS_W | FS_TRUNC, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = FS_W, + .quiet = true, + }, + { + .path = "/dev/null", + .access = FS_R | FS_W | FS_IOCTL, + .quiet = false, + }, + { + .path = "/dev/zero", + .access = FS_R, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + }, + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + { + .target = file1_s2d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .audit_truncate = true, + }, + { + .target = "/dev/null", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + { + .target = "/dev/zero", + .open_mode = O_RDONLY, + .expect_open_success = true, + .expect_ioctl_denied = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_1) { + /* Quiet from layer 1 */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = FS_R, + .quiet = true, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = false, + }, + /* Nothing for file2_s1d1 */ + { + .path = file1_s1d2, + .access = FS_W, + .quiet = false, + }, + /* Nothing for file2_s1d2 */ + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Missing both, youngest layer denies write, not quiet */ + { + .target = file2_s1d1, + .open_mode = O_RDWR, + .audit_write_blocked = true, + }, + /* Missing read, denied and quieted by layer 1 */ + { + .target = file1_s1d2, + .open_mode = O_RDWR, + }, + /* Missing write, denied and not quieted by layer 2 */ + { + .target = file2_s1d2, + .open_mode = O_RDWR, + .audit_write_blocked = true, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_2) { + /* Quiet from layer 2 */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = false, + }, + /* Nothing for file2_s1d1 and file1_s1d2 */ + { + .path = file2_s1d2, + .access = FS_R, + .quiet = false, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = 0, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Missing both, youngest layer denies write, quiet */ + { + .target = file2_s1d1, + .open_mode = O_RDWR, + }, + /* Missing read, denied and not quieted by layer 1 */ + { + .target = file1_s1d2, + .open_mode = O_RDWR, + .audit_read_blocked = true, + }, + /* Missing write, denied and quieted by layer 2 */ + { + .target = file2_s1d2, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, quiet_two_layers_different_handled_3) { + /* Quiet from both layers */ + .layers = { + { + .handled_access_fs = FS_R, + .quiet_access_fs = FS_R, + .rules = { + { + .path = file1_s1d1, + .access = FS_R, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = FS_R, + .quiet = true, + }, + }, + }, + { + .handled_access_fs = FS_W, + .quiet_access_fs = FS_W, + .rules = { + { + .path = file1_s1d1, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s1d2, + .access = FS_W, + .quiet = true, + }, + { + .path = file2_s1d2, + .access = 0, + .quiet = true, + }, + }, + }, + }, + .targets = { + { + .target = file1_s1d1, + .open_mode = O_RDWR, + .expect_open_success = true, + .expect_truncate_success = true, + }, + { + .target = file2_s1d1, + .open_mode = O_RDWR, + }, + { + .target = file1_s1d2, + .open_mode = O_RDWR, + }, + { + .target = file2_s1d2, + .open_mode = O_RDWR, + }, + }, +}; + +FIXTURE_VARIANT_ADD(audit_quiet_layout1, without_quiet_then_with_quiet) { + .layers = { + { + .handled_access_fs = FS_R | FS_W, + .quiet_access_fs = FS_R, + .rules = { + { .path = dir_s1d1, .access = FS_W, .quiet = false }, + { .path = dir_s1d1, .access = 0, .quiet = true }, + }, + }, + }, + .targets = { + /* Read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDONLY, + }, + /* Write ok */ + { + .target = file1_s1d1, + .open_mode = O_WRONLY, + .expect_open_success = true, + .expect_truncate_success = true, + }, + /* Write ok, read denied and quieted */ + { + .target = file1_s1d1, + .open_mode = O_RDWR, + }, + /* Not covered by quiet */ + { + .target = file1_s2d1, + .open_mode = O_RDONLY, + .audit_read_blocked = true, + }, + }, +}; + +/* + * The following TEST_F extend the above test cases to test more layers, with + * the inserted layers having varying configurations. + */ + +/* Extra allow all layers, quiet or not, does not change any behaviour. */ +TEST_F(audit_quiet_layout1, allow_all_layer) +{ + struct a_layer allow_all_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = 0, + .rules = { + { + .path = "/", + .access = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet = false, + }, + }, + }; + int i; + + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); + + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); + + /* + * SELF_LOG flags or quiet bits from inner allowing layers should not + * affect behaviour. + */ + allow_all_layer.quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL; + allow_all_layer.rules[0].quiet = true; + /* + * Note: this only works because we're not checking counts of domain + * alloc/dealloc logs + */ + allow_all_layer.restrict_flags = + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF; + ASSERT_EQ(0, apply_a_layer(_metadata, &allow_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +/* + * Add useless outer layers until we reach the layer limit. Should not change + * anything. + */ +TEST_F(audit_quiet_layout1, many_outer_layers) +{ + struct a_layer useless_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC, + .rules = { + { .path = "/", .access = FS_R | FS_W | FS_TRUNC, .quiet = true }, + }, + }; + int i; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) { + if (variant->layers[i].handled_access_fs == 0) + break; + } + + for (; i < LANDLOCK_MAX_NUM_LAYERS; i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &useless_layer)); + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + + audit_quiet_layout1_test_body(_metadata, self, variant->targets); +} + +/* An inner layer that denies and quiets everything should result in no logs. */ +TEST_F(audit_quiet_layout1, deny_all_quiet_layer) +{ + struct a_layer deny_all_layer = { + .handled_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .quiet_access_fs = FS_R | FS_W | FS_TRUNC | FS_IOCTL, + .rules = { + { .path = "/", .access = 0, .quiet = true }, + }, + }; + int i; + FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {}; + + /* Any open should fail with no logs. */ + for (i = 0; i < ARRAY_SIZE(variant->targets); i++) { + const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target){ + .target = target->target, + .open_mode = target->open_mode, + /* We denied everything, open should always fail. */ + .expect_open_success = false, + }; + } + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant_2.targets); +} + +/* + * An inner layer that denies everything without quiet should produce logs for + * all access. + */ +TEST_F(audit_quiet_layout1, deny_all_layer) +{ + struct a_layer deny_all_layer = { + .handled_access_fs = FS_R | FS_W, + .quiet_access_fs = FS_R | FS_W, + }; + int i; + FIXTURE_VARIANT(audit_quiet_layout1) variant_2 = {}; + bool test_has_subdomains_off = false; + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) { + if (variant->layers[i].restrict_flags & + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF) { + test_has_subdomains_off = true; + break; + } + } + + for (i = 0; i < ARRAY_SIZE(variant->targets); i++) { + const struct a_target *target = &variant->targets[i]; + + variant_2.targets[i] = (struct a_target){ + .target = target->target, + .open_mode = target->open_mode, + + /* We denied everything, open should always fail. */ + .expect_open_success = false, + /* Audit should always happen as long as open request contains read. */ + .audit_read_blocked = !test_has_subdomains_off && + target->open_mode != O_WRONLY, + /* Audit should always happen as long as open request contains write. */ + .audit_write_blocked = !test_has_subdomains_off && + target->open_mode != O_RDONLY, + }; + } + + for (i = 0; i < ARRAY_SIZE(variant->layers); i++) + ASSERT_EQ(0, apply_a_layer(_metadata, &variant->layers[i])); + ASSERT_EQ(0, apply_a_layer(_metadata, &deny_all_layer)); + + audit_quiet_layout1_test_body(_metadata, self, variant_2.targets); +} + +/* Uses layout1_bind hierarchy */ +FIXTURE(audit_quiet_rename) +{ + struct audit_filter audit_filter; + int audit_fd; +}; + +FIXTURE_SETUP(audit_quiet_rename) +{ + prepare_layout(_metadata); + create_layout1(_metadata); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, mount(dir_s1d2, dir_s2d2, NULL, MS_BIND, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + 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); + + if (getenv("DEBUG_QUIET_TESTS")) + debug_quiet_tests = true; +} + +FIXTURE_TEARDOWN_PARENT(audit_quiet_rename) +{ + remove_layout1(_metadata); + cleanup_layout(_metadata); + + /* umount(dir_s2d2)) is handled by namespace lifetime. */ + + remove_path(file1_s4d1); + remove_path(file2_s4d1); + + set_cap(_metadata, CAP_AUDIT_CONTROL); + EXPECT_EQ(0, audit_cleanup(-1, NULL)); + clear_cap(_metadata, CAP_AUDIT_CONTROL); +} + +static void simple_quiet_rename(struct __test_metadata *const _metadata, + FIXTURE_DATA(audit_quiet_rename) *const self, + __u64 handled_access, __u64 quiet_access, + bool source_allow, bool dest_allow, + bool source_quiet, bool dest_quiet, + const char *source_blockers, + const char *dest_blockers) +{ + /* We will move file1_s1d1 to file1_s2d1 */ + struct a_layer layer = { + .handled_access_fs = handled_access, + .quiet_access_fs = quiet_access, + .rules = { + { + .path = dir_s1d1, + .access = source_allow ? handled_access : 0, + .quiet = source_quiet, + }, + { + .path = dir_s2d1, + .access = dest_allow ? handled_access : 0, + .quiet = dest_quiet, + }, + }, + }; + struct audit_records records = {}; + int ret, err; + + /* Skip landlock_add_rule for useless rules. */ + if (!source_allow && !source_quiet) + layer.rules[0].path = NULL; + if (!dest_allow && !dest_quiet) + layer.rules[1].path = NULL; + + EXPECT_EQ(0, unlink(file1_s2d1)); + EXPECT_EQ(0, apply_a_layer(_metadata, &layer)); + + if (debug_quiet_tests) + TH_LOG("Try renameat \"%s\" to \"%s\"", file1_s1d1, file1_s2d1); + ret = renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1); + err = errno; + if (ret != 0 && debug_quiet_tests) { + TH_LOG("renameat error: %s", err == EXDEV ? "EXDEV" : + err == EACCES ? "EACCES" : + strerror(err)); + } + if (source_allow && dest_allow) { + ASSERT_EQ(0, ret); + } else { + ASSERT_EQ(-1, ret); + if (handled_access & (LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE)) { + ASSERT_EQ(EACCES, err); + } else { + ASSERT_EQ(EXDEV, err); + } + + if (source_blockers) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + source_blockers, dir_s1d1)); + if (dest_blockers) + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + dest_blockers, dir_s2d1)); + } + /* + * No other logs. records.domain not checked per reasoning in + * audit_quiet_layout1_test_body. + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, rename_ok) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, true, true, false, + false, NULL, NULL); +} + +TEST_F(audit_quiet_rename, no_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, + false, false, "fs\\.remove_file,fs\\.refer", + "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, true, + true, NULL, NULL); +} + +TEST_F(audit_quiet_rename, source_no_quiet_dest_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, + false, true, "fs\\.remove_file,fs\\.refer", NULL); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_no_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, false, true, + false, NULL, "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, only_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, LANDLOCK_ACCESS_FS_REFER, + false, false, true, true, + "fs\\.remove_file,fs\\.refer", + "fs\\.make_reg,fs\\.refer"); +} + +TEST_F(audit_quiet_rename, source_allow_dest_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, true, false, false, + true, NULL, NULL); +} + +TEST_F(audit_quiet_rename, source_quiet_dest_allow) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + + simple_quiet_rename(_metadata, self, access, access, false, true, true, + false, NULL, NULL); +} + +TEST_F(audit_quiet_rename, handle_all_deny_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + /* No logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_not_quiet_refer) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = 0, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s1d1)); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, handle_all_deny_refer_quiet_source_not_quiet_dest) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EXDEV, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_same_dir) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file1_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + /* We didn't unlink destination file */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg,fs\\.refer", + dir_s2d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_flag_on_file_ignored_same_dir) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = file1_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = file2_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file2_s1d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, + matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg", dir_s1d1)); + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet1) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2. Refer is quieted but we are + * also missing remove_file on source. + */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet2) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = LANDLOCK_ACCESS_FS_REFER, + .quiet_access_fs = LANDLOCK_ACCESS_FS_REFER, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_REFER, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2, but refer is quieted (and that + * layer does not handle any other accesses). + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, two_layers_different_quiet3) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = access, + .quiet = false, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = access, + .quiet = false, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* + * The youngest denial will be layer 2, in which everything is quieted. + */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, + first_layer_quiet_deny_all_second_layer_not_quiet_deny_all) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = {}, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.refer", dir_s1d1)); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s2d1)); + /* No other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, + first_layer_quiet_deny_all_second_layer_dest_not_quiet) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer1 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct a_layer layer2 = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file1_s2d1)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer1)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer2)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1)); + ASSERT_EQ(EACCES, errno); + + /* Source is quieted but destination is not. */ + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.make_reg,fs\\.refer", dir_s2d1)); + /* No other logs. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, rename_xchg) +{ + struct a_layer layer = { + .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet_access_fs = LANDLOCK_ACCESS_FS_MAKE_REG, + .rules = { { + .path = dir_s1d1, + .access = LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet = true, + }, + { + .path = dir_s2d1, + .access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER, + .quiet = false, + } }, + }; + struct audit_records records = {}; + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat2(AT_FDCWD, file1_s1d1, AT_FDCWD, file1_s2d1, + RENAME_EXCHANGE)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD, + bind_file2_s1d3)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_ignored) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s1d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + + EXPECT_EQ(0, unlink(file2_s1d3)); + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, renameat(AT_FDCWD, bind_file1_s1d3, AT_FDCWD, + bind_file2_s1d3)); + ASSERT_EQ(EACCES, errno); + ASSERT_EQ(0, matches_log_fs(_metadata, self->audit_fd, + "fs\\.remove_file,fs\\.make_reg", + bind_dir_s1d3)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_on_parent_mount_disconnected) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s2d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + int bind_s1d3_fd; + + EXPECT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY); + ASSERT_GE(bind_s1d3_fd, 0); + + /* Make s1d3 disconnected. */ + create_directory(_metadata, dir_s4d1); + ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_F(audit_quiet_rename, quiet_behind_mountpoint_disconnected) +{ + __u64 access = LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_REFER; + struct a_layer layer = { + .handled_access_fs = access, + .quiet_access_fs = access, + .rules = { + { + .path = dir_s4d1, + .access = 0, + .quiet = true, + }, + }, + }; + struct audit_records records = {}; + int bind_s1d3_fd; + + EXPECT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_DIRECTORY); + ASSERT_GE(bind_s1d3_fd, 0); + + /* Make s1d3 disconnected. */ + create_directory(_metadata, dir_s4d1); + ASSERT_EQ(0, renameat(AT_FDCWD, dir_s1d3, AT_FDCWD, dir_s4d2)); + + ASSERT_EQ(0, apply_a_layer(_metadata, &layer)); + + ASSERT_EQ(-1, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)); + ASSERT_EQ(EACCES, errno); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + ASSERT_EQ(0, records.access); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c new file mode 100644 index 000000000..2ed1f76b7 --- /dev/null +++ b/tools/testing/selftests/landlock/net_test.c @@ -0,0 +1,3191 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock tests - Network + * + * Copyright © 2022-2023 Huawei Tech. Co., Ltd. + * Copyright © 2023 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <linux/landlock.h> +#include <linux/in.h> +#include <sched.h> +#include <stdint.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/socket.h> +#include <sys/syscall.h> +#include <sys/un.h> + +#include "audit.h" +#include "common.h" + +const short sock_port_start = (1 << 10); + +static const char loopback_ipv4[] = "127.0.0.1"; +static const char loopback_ipv6[] = "::1"; + +/* Number pending connections queue to be hold. */ +const short backlog = 10; + +enum sandbox_type { + NO_SANDBOX, + /* This may be used to test rules that allow *and* deny accesses. */ + TCP_SANDBOX, + UDP_SANDBOX, +}; + +static int set_service(struct service_fixture *const srv, + const struct protocol_variant prot, + const unsigned short index) +{ + memset(srv, 0, sizeof(*srv)); + + /* + * Copies all protocol properties in case of the variant only contains + * a subset of them. + */ + srv->protocol = prot; + + /* Checks for port overflow. */ + if (index > 2) + return 1; + srv->port = sock_port_start << (2 * index); + + switch (prot.domain) { + case AF_UNSPEC: + case AF_INET: + srv->ipv4_addr.sin_family = prot.domain; + srv->ipv4_addr.sin_port = htons(srv->port); + srv->ipv4_addr.sin_addr.s_addr = inet_addr(loopback_ipv4); + return 0; + + case AF_INET6: + srv->ipv6_addr.sin6_family = prot.domain; + srv->ipv6_addr.sin6_port = htons(srv->port); + inet_pton(AF_INET6, loopback_ipv6, &srv->ipv6_addr.sin6_addr); + return 0; + + case AF_UNIX: + set_unix_address(srv, index); + return 0; + } + return 1; +} + +static void setup_loopback(struct __test_metadata *const _metadata) +{ + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, unshare(CLONE_NEWNET)); + clear_cap(_metadata, CAP_SYS_ADMIN); + + set_ambient_cap(_metadata, CAP_NET_ADMIN); + ASSERT_EQ(0, system("ip link set dev lo up")); + clear_ambient_cap(_metadata, CAP_NET_ADMIN); +} + +static bool prot_is_tcp(const struct protocol_variant *const prot) +{ + return (prot->domain == AF_INET || prot->domain == AF_INET6) && + prot->type == SOCK_STREAM && + (prot->protocol == IPPROTO_TCP || prot->protocol == IPPROTO_IP); +} + +static bool prot_is_udp(const struct protocol_variant *const prot) +{ + return (prot->domain == AF_INET || prot->domain == AF_INET6) && + prot->type == SOCK_DGRAM && + (prot->protocol == IPPROTO_UDP || prot->protocol == IPPROTO_IP); +} + +static bool is_restricted(const struct protocol_variant *const prot, + const enum sandbox_type sandbox) +{ + if (sandbox == TCP_SANDBOX) + return prot_is_tcp(prot); + else if (sandbox == UDP_SANDBOX) + return prot_is_udp(prot); + return false; +} + +static int socket_variant(const struct service_fixture *const srv) +{ + /* Arbitrary value just to not block other tests indefinitely. */ + const struct timeval timeout = { + .tv_sec = 0, + .tv_usec = 100000, + }; + int sockfd; + int ret; + + sockfd = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC, + srv->protocol.protocol); + if (sockfd < 0) + return -errno; + + ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, + sizeof(timeout)); + if (ret != 0) { + ret = -errno; + close(sockfd); + return ret; + } + ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, + sizeof(timeout)); + if (ret != 0) { + ret = -errno; + close(sockfd); + return ret; + } + return sockfd; +} + +#ifndef SIN6_LEN_RFC2133 +#define SIN6_LEN_RFC2133 24 +#endif + +static socklen_t get_addrlen(const struct service_fixture *const srv, + const bool minimal) +{ + switch (srv->protocol.domain) { + case AF_UNSPEC: + if (minimal) + return sizeof(sa_family_t); + return sizeof(struct sockaddr_storage); + + case AF_INET: + return sizeof(srv->ipv4_addr); + + case AF_INET6: + if (minimal) + return SIN6_LEN_RFC2133; + return sizeof(srv->ipv6_addr); + + case AF_UNIX: + if (minimal) + return sizeof(srv->unix_addr) - + sizeof(srv->unix_addr.sun_path); + return srv->unix_addr_len; + + default: + return 0; + } +} + +static void set_port(struct service_fixture *const srv, uint16_t port) +{ + switch (srv->protocol.domain) { + case AF_UNSPEC: + case AF_INET: + srv->ipv4_addr.sin_port = htons(port); + return; + + case AF_INET6: + srv->ipv6_addr.sin6_port = htons(port); + return; + + default: + return; + } +} + +static uint16_t get_binded_port(int socket_fd, + const struct protocol_variant *const prot) +{ + struct sockaddr_in ipv4_addr; + struct sockaddr_in6 ipv6_addr; + socklen_t ipv4_addr_len, ipv6_addr_len; + + /* Gets binded port. */ + switch (prot->domain) { + case AF_UNSPEC: + case AF_INET: + ipv4_addr_len = sizeof(ipv4_addr); + getsockname(socket_fd, &ipv4_addr, &ipv4_addr_len); + return ntohs(ipv4_addr.sin_port); + + case AF_INET6: + ipv6_addr_len = sizeof(ipv6_addr); + getsockname(socket_fd, &ipv6_addr, &ipv6_addr_len); + return ntohs(ipv6_addr.sin6_port); + + default: + return 0; + } +} + +static int bind_variant_addrlen(const int sock_fd, + const struct service_fixture *const srv, + const socklen_t addrlen) +{ + int ret; + + switch (srv->protocol.domain) { + case AF_UNSPEC: + case AF_INET: + ret = bind(sock_fd, &srv->ipv4_addr, addrlen); + break; + + case AF_INET6: + ret = bind(sock_fd, &srv->ipv6_addr, addrlen); + break; + + case AF_UNIX: + ret = bind(sock_fd, &srv->unix_addr, addrlen); + break; + + default: + errno = EAFNOSUPPORT; + return -errno; + } + + if (ret < 0) + return -errno; + return ret; +} + +static int bind_variant(const int sock_fd, + const struct service_fixture *const srv) +{ + return bind_variant_addrlen(sock_fd, srv, get_addrlen(srv, false)); +} + +static int connect_variant_addrlen(const int sock_fd, + const struct service_fixture *const srv, + const socklen_t addrlen) +{ + int ret; + + switch (srv->protocol.domain) { + case AF_UNSPEC: + case AF_INET: + ret = connect(sock_fd, &srv->ipv4_addr, addrlen); + break; + + case AF_INET6: + ret = connect(sock_fd, &srv->ipv6_addr, addrlen); + break; + + case AF_UNIX: + ret = connect(sock_fd, &srv->unix_addr, addrlen); + break; + + default: + errno = -EAFNOSUPPORT; + return -errno; + } + + if (ret < 0) + return -errno; + return ret; +} + +static int connect_variant(const int sock_fd, + const struct service_fixture *const srv) +{ + return connect_variant_addrlen(sock_fd, srv, get_addrlen(srv, false)); +} + +static int sendto_variant_addrlen(const int sock_fd, + const struct service_fixture *const srv, + const socklen_t addrlen, void *buf, + size_t len, size_t flags) +{ + const struct sockaddr *dst = NULL; + ssize_t ret; + + /* + * We never want our processes to be killed by SIGPIPE: we check return + * codes and errno, so that we have actual error messages. + */ + flags |= MSG_NOSIGNAL; + + if (srv != NULL) { + switch (srv->protocol.domain) { + case AF_UNSPEC: + case AF_INET: + dst = (const struct sockaddr *)&srv->ipv4_addr; + break; + + case AF_INET6: + dst = (const struct sockaddr *)&srv->ipv6_addr; + break; + + case AF_UNIX: + dst = (const struct sockaddr *)&srv->unix_addr; + break; + + default: + errno = EAFNOSUPPORT; + return -errno; + } + } + + ret = sendto(sock_fd, buf, len, flags, dst, addrlen); + if (ret < 0) + return -errno; + + /* errno is not set in cases of partial writes. */ + if (ret != len) + return -EINTR; + + return 0; +} + +static int sendto_variant(const int sock_fd, + const struct service_fixture *const srv, void *buf, + size_t len, size_t flags) +{ + socklen_t addrlen = 0; + + if (srv != NULL) + addrlen = get_addrlen(srv, false); + + return sendto_variant_addrlen(sock_fd, srv, addrlen, buf, len, flags); +} + +static int test_sendmsg(struct __test_metadata *const _metadata, + const struct protocol_variant *prot, int client_fd, + int server_fd, const struct service_fixture *srv, + bool bind_denied, bool send_denied) +{ + int ret; + socklen_t opt_len; + int sock_type; + int addr_family; + struct sockaddr_storage peer_addr = { 0 }; + bool has_remote_port; + bool needs_autobind; + char read_buf[1] = { 0 }; + + /* + * Prepare the test by inspecting the socket type and whether it has a + * local/remote address set (all of which determine the expected + * outcomes). + */ + opt_len = sizeof(sock_type); + ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_TYPE, &sock_type, + &opt_len)); + opt_len = sizeof(addr_family); + ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_DOMAIN, &addr_family, + &opt_len)); + opt_len = sizeof(peer_addr); + has_remote_port = (getpeername(client_fd, (struct sockaddr *)&peer_addr, + &opt_len) == 0); + needs_autobind = (addr_family == AF_INET || addr_family == AF_INET6) && + get_binded_port(client_fd, prot) == 0; + + /* First, check error code with truncated explicit address. */ + if (srv != NULL) { + ret = sendto_variant_addrlen( + client_fd, srv, get_addrlen(srv, true) - 1, "A", 1, 0); + if (sock_type == SOCK_STREAM && !has_remote_port) { + EXPECT_EQ(-EPIPE, ret) + { + return -1; + } + } else if (bind_denied && needs_autobind) { + EXPECT_EQ(-EACCES, ret) + { + return -1; + } + } else { + EXPECT_EQ(-EINVAL, ret) + { + return -1; + } + } + } + + /* With or without explicit destination address (srv can be NULL). */ + ret = sendto_variant(client_fd, srv, "B", 1, 0); + if (sock_type == SOCK_STREAM && !has_remote_port) { + EXPECT_EQ(-EPIPE, ret) + { + return -1; + } + } else if ((send_denied && srv != NULL) || + (bind_denied && needs_autobind)) { + ASSERT_EQ(-EACCES, ret) + { + return -1; + } + } else if (srv == NULL && !has_remote_port) { + if (addr_family == AF_UNIX) { + ASSERT_EQ(-ENOTCONN, ret) + { + return -1; + } + } else if (sock_type == SOCK_STREAM) { + ASSERT_EQ(-EPIPE, ret) + { + return -1; + } + } else { + ASSERT_EQ(-EDESTADDRREQ, ret) + { + return -1; + } + } + } else { + ASSERT_EQ(0, ret); + ASSERT_EQ(1, recv(server_fd, read_buf, 1, 0)); + ASSERT_EQ(read_buf[0], 'B') + { + return -1; + } + } + + return 0; +} + +FIXTURE(protocol) +{ + struct service_fixture srv0, srv1, srv2; + struct service_fixture unspec_any0, unspec_srv0, unspec_srv1; +}; + +FIXTURE_VARIANT(protocol) +{ + const enum sandbox_type sandbox; + const struct protocol_variant prot; +}; + +FIXTURE_SETUP(protocol) +{ + struct protocol_variant prot_unspec = variant->prot; + + prot_unspec.domain = AF_UNSPEC; + + disable_caps(_metadata); + + ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1)); + ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2)); + + ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0)); + ASSERT_EQ(0, set_service(&self->unspec_srv1, prot_unspec, 1)); + + ASSERT_EQ(0, set_service(&self->unspec_any0, prot_unspec, 0)); + self->unspec_any0.ipv4_addr.sin_addr.s_addr = htonl(INADDR_ANY); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(protocol) +{ +} + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv4_tcp1) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv4_tcp2) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + .protocol = IPPROTO_TCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv4_mptcp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + .protocol = IPPROTO_MPTCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv6_tcp1) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv6_tcp2) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + .protocol = IPPROTO_TCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv6_mptcp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + .protocol = IPPROTO_MPTCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv4_udp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_ipv6_udp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_unix_stream) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, no_sandbox_with_unix_datagram) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv4_tcp1) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv4_tcp2) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + .protocol = IPPROTO_TCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv4_mptcp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + .protocol = IPPROTO_MPTCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv6_tcp1) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv6_tcp2) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + .protocol = IPPROTO_TCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv6_mptcp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + .protocol = IPPROTO_MPTCP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv4_udp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_ipv6_udp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_unix_stream) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_unix_datagram) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp1) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + .protocol = IPPROTO_UDP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp2) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp1) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + .protocol = IPPROTO_UDP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp2) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + /* IPPROTO_IP == 0 */ + .protocol = IPPROTO_IP, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_stream) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_datagram) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_UNIX, + .type = SOCK_DGRAM, + }, +}; + +static void test_bind_and_connect(struct __test_metadata *const _metadata, + const struct service_fixture *const srv, + const bool deny_bind, const bool deny_connect) +{ + char buf = '\0'; + int inval_fd, bind_fd, client_fd, status, ret; + pid_t child; + + /* Starts invalid addrlen tests with bind. */ + inval_fd = socket_variant(srv); + ASSERT_LE(0, inval_fd) + { + TH_LOG("Failed to create socket: %s", strerror(errno)); + } + + /* Tries to bind with zero as addrlen. */ + EXPECT_EQ(-EINVAL, bind_variant_addrlen(inval_fd, srv, 0)); + + /* Tries to bind with too small addrlen. */ + EXPECT_EQ(-EINVAL, bind_variant_addrlen(inval_fd, srv, + get_addrlen(srv, true) - 1)); + + /* Tries to bind with minimal addrlen. */ + ret = bind_variant_addrlen(inval_fd, srv, get_addrlen(srv, true)); + if (deny_bind) { + EXPECT_EQ(-EACCES, ret); + } else { + EXPECT_EQ(0, ret) + { + TH_LOG("Failed to bind to socket: %s", strerror(errno)); + } + } + EXPECT_EQ(0, close(inval_fd)); + + /* Starts invalid addrlen tests with connect. */ + inval_fd = socket_variant(srv); + ASSERT_LE(0, inval_fd); + + /* Tries to connect with zero as addrlen. */ + EXPECT_EQ(-EINVAL, connect_variant_addrlen(inval_fd, srv, 0)); + + /* Tries to connect with too small addrlen. */ + EXPECT_EQ(-EINVAL, connect_variant_addrlen(inval_fd, srv, + get_addrlen(srv, true) - 1)); + + /* Tries to connect with minimal addrlen. */ + ret = connect_variant_addrlen(inval_fd, srv, get_addrlen(srv, true)); + if (srv->protocol.domain == AF_UNIX) { + EXPECT_EQ(-EINVAL, ret); + } else if (deny_connect) { + EXPECT_EQ(-EACCES, ret); + } else if (srv->protocol.type == SOCK_STREAM) { + /* No listening server, whatever the value of deny_bind. */ + EXPECT_EQ(-ECONNREFUSED, ret); + } else { + EXPECT_EQ(0, ret) + { + TH_LOG("Failed to connect to socket: %s", + strerror(errno)); + } + } + EXPECT_EQ(0, close(inval_fd)); + + /* Starts connection tests. */ + bind_fd = socket_variant(srv); + ASSERT_LE(0, bind_fd); + + ret = bind_variant(bind_fd, srv); + if (deny_bind) { + EXPECT_EQ(-EACCES, ret); + } else { + EXPECT_EQ(0, ret); + + /* Creates a listening socket. */ + if (srv->protocol.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + } + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int connect_fd, ret; + + /* Closes listening socket for the child. */ + EXPECT_EQ(0, close(bind_fd)); + + /* Starts connection tests. */ + connect_fd = socket_variant(srv); + ASSERT_LE(0, connect_fd); + ret = connect_variant(connect_fd, srv); + if (deny_connect) { + EXPECT_EQ(-EACCES, ret); + } else if (deny_bind && srv->protocol.type == SOCK_STREAM) { + /* No listening server. */ + EXPECT_EQ(-ECONNREFUSED, ret); + } else { + EXPECT_EQ(0, ret); + EXPECT_EQ(1, write(connect_fd, ".", 1)); + } + + EXPECT_EQ(0, close(connect_fd)); + _exit(_metadata->exit_code); + return; + } + + /* Accepts connection from the child. */ + client_fd = bind_fd; + if (!deny_bind && !deny_connect) { + if (srv->protocol.type == SOCK_STREAM) { + client_fd = accept(bind_fd, NULL, 0); + ASSERT_LE(0, client_fd); + } + + EXPECT_EQ(1, read(client_fd, &buf, 1)); + EXPECT_EQ('.', buf); + } + + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* Closes connection, if any. */ + if (client_fd != bind_fd) + EXPECT_LE(0, close(client_fd)); + + /* Closes listening socket. */ + EXPECT_EQ(0, close(bind_fd)); +} + +TEST_F(protocol, bind) +{ + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = bind_access | conn_access, + }; + const struct landlock_net_port_attr bind_connect_p0 = { + .allowed_access = bind_access | conn_access, + .port = self->srv0.port, + }; + const struct landlock_net_port_attr connect_p1 = { + .allowed_access = conn_access, + .port = self->srv1.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows connect and bind for the first port. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect_p0, 0)); + + /* Allows connect and denies bind for the second port. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &connect_p1, 0)); + + /* + * For UDP sockets, allows binding to ephemeral ports (required + * to connect or send a first datagram) + */ + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_net_port_attr bind_ephemeral = { + .allowed_access = bind_access, + .port = 0, + }; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &bind_ephemeral, 0)); + } + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* Binds a socket to the first port. */ + test_bind_and_connect(_metadata, &self->srv0, false, false); + + /* Binds a socket to the second port. */ + test_bind_and_connect(_metadata, &self->srv1, + is_restricted(&variant->prot, variant->sandbox), + false); + + /* Binds a socket to the third port. */ + test_bind_and_connect(_metadata, &self->srv2, + is_restricted(&variant->prot, variant->sandbox), + is_restricted(&variant->prot, variant->sandbox)); +} + +TEST_F(protocol, connect) +{ + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_access = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = bind_access | conn_access, + }; + const struct landlock_net_port_attr bind_connect_p0 = { + .allowed_access = bind_access | conn_access, + .port = self->srv0.port, + }; + const struct landlock_net_port_attr bind_p1 = { + .allowed_access = bind_access, + .port = self->srv1.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows connect and bind for the first port. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect_p0, 0)); + + /* Allows bind and denies connect for the second port. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_p1, 0)); + + /* + * For UDP sockets, allows binding to ephemeral ports (required + * to connect or send a first datagram) + */ + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_net_port_attr bind_ephemeral = { + .allowed_access = bind_access, + .port = 0, + }; + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &bind_ephemeral, 0)); + } + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + test_bind_and_connect(_metadata, &self->srv0, false, false); + + test_bind_and_connect(_metadata, &self->srv1, false, + is_restricted(&variant->prot, variant->sandbox)); + + test_bind_and_connect(_metadata, &self->srv2, + is_restricted(&variant->prot, variant->sandbox), + is_restricted(&variant->prot, variant->sandbox)); +} + +TEST_F(protocol, bind_unspec) +{ + const __u64 bind_access = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = bind_access, + }; + const struct landlock_net_port_attr rule_bind = { + .allowed_access = bind_access, + .port = self->srv0.port, + }; + int bind_fd, ret; + + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows bind. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule_bind, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + + /* Tries to bind with too small addrlen. */ + EXPECT_EQ(-EINVAL, bind_variant_addrlen( + bind_fd, &self->unspec_any0, + get_addrlen(&self->unspec_any0, true) - 1)); + + /* Allowed bind on AF_UNSPEC/INADDR_ANY. */ + ret = bind_variant(bind_fd, &self->unspec_any0); + if (variant->prot.domain == AF_INET) { + EXPECT_EQ(0, ret) + { + TH_LOG("Failed to bind to unspec/any socket: %s", + strerror(errno)); + } + } else if (variant->prot.domain == AF_INET6) { + EXPECT_EQ(-EAFNOSUPPORT, ret); + } else { + EXPECT_EQ(-EINVAL, ret); + } + EXPECT_EQ(0, close(bind_fd)); + + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Denies bind. */ + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + + /* Denied bind on AF_UNSPEC/INADDR_ANY. */ + ret = bind_variant(bind_fd, &self->unspec_any0); + if (variant->prot.domain == AF_INET) { + if (is_restricted(&variant->prot, variant->sandbox)) { + EXPECT_EQ(-EACCES, ret); + } else { + EXPECT_EQ(0, ret); + } + } else if (variant->prot.domain == AF_INET6) { + EXPECT_EQ(-EAFNOSUPPORT, ret); + } else { + EXPECT_EQ(-EINVAL, ret); + } + EXPECT_EQ(0, close(bind_fd)); + + /* Checks bind with AF_UNSPEC and the loopback address. */ + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + ret = bind_variant(bind_fd, &self->unspec_srv0); + if (variant->prot.domain == AF_INET || + variant->prot.domain == AF_INET6) { + EXPECT_EQ(-EAFNOSUPPORT, ret); + } else { + EXPECT_EQ(-EINVAL, ret) + { + TH_LOG("Wrong bind error: %s", strerror(errno)); + } + } + EXPECT_EQ(0, close(bind_fd)); +} + +TEST_F(protocol, connect_unspec) +{ + const __u64 connect_right = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const struct landlock_ruleset_attr ruleset_conn = { + .handled_access_net = connect_right, + }; + const struct landlock_ruleset_attr ruleset_conn_bind = { + .handled_access_net = connect_right | bind_right, + }; + const struct landlock_net_port_attr rule_connect = { + .allowed_access = connect_right, + .port = self->srv0.port, + }; + int bind_fd, client_fd, status; + pid_t child; + + /* Specific connection tests. */ + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + EXPECT_EQ(0, bind_variant(bind_fd, &self->srv0)); + if (self->srv0.protocol.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int connect_fd, ret; + + /* Closes listening socket for the child. */ + EXPECT_EQ(0, close(bind_fd)); + + connect_fd = socket_variant(&self->srv0); + ASSERT_LE(0, connect_fd); + EXPECT_EQ(0, connect_variant(connect_fd, &self->srv0)); + + /* Tries to connect again, or set peer. */ + ret = connect_variant(connect_fd, &self->srv0); + if (self->srv0.protocol.type == SOCK_STREAM) { + EXPECT_EQ(-EISCONN, ret); + } else { + EXPECT_EQ(0, ret); + } + + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const int ruleset_fd = landlock_create_ruleset( + &ruleset_conn, sizeof(ruleset_conn), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows connect. */ + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &rule_connect, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* Disconnects already connected socket, or set peer. */ + ret = connect_variant(connect_fd, &self->unspec_any0); + if (self->srv0.protocol.domain == AF_UNIX && + self->srv0.protocol.type == SOCK_STREAM) { + EXPECT_EQ(-EINVAL, ret); + } else { + EXPECT_EQ(0, ret); + } + + /* Tries to reconnect, or set peer. */ + ret = connect_variant(connect_fd, &self->srv0); + if (self->srv0.protocol.domain == AF_UNIX && + self->srv0.protocol.type == SOCK_STREAM) { + EXPECT_EQ(-EISCONN, ret); + } else { + EXPECT_EQ(0, ret); + } + + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const int ruleset_fd = landlock_create_ruleset( + &ruleset_conn_bind, sizeof(ruleset_conn_bind), + 0); + ASSERT_LE(0, ruleset_fd); + + /* Denies connect and bind. */ + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* Try to re-disconnect with a truncated address struct. */ + EXPECT_EQ(-EINVAL, + connect_variant_addrlen( + connect_fd, &self->unspec_any0, + get_addrlen(&self->unspec_any0, true) - 1)); + + /* + * Re-disconnect, with a minimal sockaddr struct (just a + * bare af_family=AF_UNSPEC field). + */ + ret = connect_variant_addrlen(connect_fd, &self->unspec_any0, + get_addrlen(&self->unspec_any0, + true)); + if (self->srv0.protocol.domain == AF_UNIX && + self->srv0.protocol.type == SOCK_STREAM) { + EXPECT_EQ(-EINVAL, ret); + } else { + /* Always allowed to disconnect. */ + EXPECT_EQ(0, ret); + } + + EXPECT_EQ(0, close(connect_fd)); + _exit(_metadata->exit_code); + return; + } + + client_fd = bind_fd; + if (self->srv0.protocol.type == SOCK_STREAM) { + client_fd = accept(bind_fd, NULL, 0); + ASSERT_LE(0, client_fd); + } + + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* Closes connection, if any. */ + if (client_fd != bind_fd) + EXPECT_LE(0, close(client_fd)); + + /* Closes listening socket. */ + EXPECT_EQ(0, close(bind_fd)); +} + +TEST_F(protocol, sendmsg_stream) +{ + int srv0_fd, tmp_fd, client_fd, res; + char read_buf[1] = { 0 }; + + /* + * Simple test for stream sockets: just deny all connect()/ + * send(explicit addr)/bind(), and make sure we don't interfere with any + * operation. + */ + if (variant->prot.type != SOCK_STREAM) + return; + + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const int 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)); + } + + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + ASSERT_EQ(0, listen(srv0_fd, backlog)); + + /* Send on a non-connected socket. */ + res = sendto_variant(client_fd, NULL, "A", 1, 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-ENOTCONN, res); + } else { + EXPECT_EQ(-EPIPE, res); + } + + /* Send to a truncated (invalid) address on a non-connected socket. */ + res = sendto_variant_addrlen(client_fd, &self->srv0, + get_addrlen(&self->srv0, true) - 1, "B", 1, + 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EOPNOTSUPP, res); + } else { + EXPECT_EQ(-EPIPE, res); + } + + /* Connect. */ + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + tmp_fd = accept(srv0_fd, NULL, 0); + ASSERT_LE(0, tmp_fd); + EXPECT_EQ(0, close(srv0_fd)); + srv0_fd = tmp_fd; + + /* Send without an explicit address. */ + EXPECT_EQ(0, sendto_variant(client_fd, NULL, "C", 1, 0)); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'C'); + + /* Send to a truncated (invalid) address. */ + res = sendto_variant_addrlen(client_fd, &self->srv0, + get_addrlen(&self->srv0, true) - 1, "D", 1, + 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EISCONN, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'D'); + } + + /* Send to a valid but different address. */ + res = sendto_variant(client_fd, &self->srv1, "E", 1, 0); + if (variant->prot.domain == AF_UNIX) { + EXPECT_EQ(-EISCONN, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0)) + { + TH_LOG("recv() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'E'); + } + + EXPECT_EQ(0, close(client_fd)); +} + +TEST_F(protocol, sendmsg_dgram) +{ + const bool restricted = is_restricted(&variant->prot, variant->sandbox); + int srv0_fd, srv1_fd, client_fd, child, status, res; + + if (variant->prot.type != SOCK_DGRAM) + return; + + /* Prepare server on port #0 to be allowed. */ + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + + /* And another server on port #1 to be denied. */ + ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1)); + ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1)); + + /* + * Check that sockets connected before restrictions are not impacted in + * any way. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + if (variant->sandbox == UDP_SANDBOX) { + /* Deny all connect()/send(explicit addr)/bind(). */ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const int 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)); + } + EXPECT_EQ(0, + test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, NULL, restricted, restricted)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + restricted)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv1_fd, &self->srv1, restricted, + restricted)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Restrict connect/send, but not bind(). Then try sending with no + * destination (and no remote peer set), an allowed destination, then a + * denied destination. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const struct landlock_net_port_attr send_p0 = { + .allowed_access = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv0.port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &send_p0, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + -1, NULL, false, false)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, false, false)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv1_fd, &self->srv1, false, + restricted)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Rest of this test is just for autobind enforcement, which only exists + * in IP sockets. + */ + if (variant->prot.domain != AF_INET && variant->prot.domain != AF_INET6) + return; + + /* Restrict bind() to explicit calls with an arbitrary (non-0) port. */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + const uint16_t allowed_src_port = 42424; + struct service_fixture allowed_src; + + allowed_src = self->srv0; + set_port(&allowed_src, allowed_src_port); + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .port = allowed_src_port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + + /* Check that implicit bind(0) in sendmsg() is denied. */ + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + false)); + + /* Same thing for autobind in connect(). */ + res = connect_variant(client_fd, &self->srv0); + if (restricted) { + EXPECT_EQ(-EACCES, res); + } else { + EXPECT_EQ(0, res); + } + EXPECT_EQ(0, close(client_fd)); + + /* Make sendmsg() work by explicitly binding to the only allowed port. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, bind_variant(client_fd, &allowed_src)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, restricted, + false)); + EXPECT_EQ(0, close(client_fd)); + + /* Make connect() work by explicitly binding to the only allowed port. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, bind_variant(client_fd, &allowed_src)); + EXPECT_EQ(0, connect_variant(client_fd, &self->srv0)); + EXPECT_EQ(0, close(client_fd)); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Check that %LANDLOCK_ACCESS_NET_BIND_UDP on port 0 allows implicit + * autobinds. + */ + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP, + .port = 0, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd, + srv0_fd, &self->srv0, false, false)); + EXPECT_EQ(0, close(client_fd)); + _exit(_metadata->exit_code); + } + EXPECT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(1, WIFEXITED(status)); + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); +} + +TEST_F(protocol, sendmsg_unspec) +{ + const bool restricted = is_restricted(&variant->prot, variant->sandbox); + int client_fd, srv0_fd, srv1_fd, res; + char read_buf[1] = { 0 }; + + /* + * We already test for the absence of influence on sendmsg for other + * socket types and other address families, there's no point in adapting + * this test for stream sockets too. + */ + if (variant->prot.type != SOCK_DGRAM) + return; + + /* Prepare client of the right family. */ + ASSERT_LE(0, client_fd = socket_variant(&self->srv0)); + + /* Prepare server on port #0 to be allowed. */ + ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0)); + ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0)); + + /* And another server on port #1 to be denied. */ + ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1)); + ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1)); + + if (variant->sandbox == UDP_SANDBOX) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv0.port, + }; + const int ruleset_fd = landlock_create_ruleset( + &ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* Explicit AF_UNSPEC address but truncated. */ + EXPECT_EQ(-EINVAL, sendto_variant_addrlen( + client_fd, &self->unspec_srv0, + get_addrlen(&self->unspec_srv0, true) - 1, + "A", 1, 0)); + + /* + * Explicit AF_UNSPEC address, should be treated as AF_INET by IPv4 + * sockets (and thus map to srv0, allowed), but be denied by IPv6 + * sockets. + */ + res = sendto_variant(client_fd, &self->unspec_srv0, "B", 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* Always denied on IPv6 socket. */ + EXPECT_EQ(-EACCES, res); + } else { + /* IPv6 sockets treat AF_UNSPEC as a NULL address. */ + EXPECT_EQ(-EDESTADDRREQ, res); + } + } else if (variant->prot.domain == AF_INET) { + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv0_fd, read_buf, 1)) + { + TH_LOG("read() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'B'); + } else { + /* Unix sockets don't accept AF_UNSPEC. */ + EXPECT_EQ(-EINVAL, res); + } + + /* + * Explicit AF_UNSPEC address, should be treated as AF_INET on IPv4 + * sockets (and thus map to srv1, denied), and be denied on IPv6 sockets + * as always. + */ + res = sendto_variant(client_fd, &self->unspec_srv1, "C", 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* Always denied on IPv6 socket. */ + EXPECT_EQ(-EACCES, res); + } else { + /* IPv6 sockets treat AF_UNSPEC as a NULL address. */ + EXPECT_EQ(-EDESTADDRREQ, res); + } + } else if (variant->prot.domain == AF_INET) { + if (restricted) { + /* Sending to srv1 is not allowed, only srv0. */ + EXPECT_EQ(-EACCES, res); + } else { + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv1_fd, read_buf, 1)) + { + TH_LOG("read() failed: %s", strerror(errno)); + } + EXPECT_EQ(read_buf[0], 'C'); + } + } else { + /* Unix sockets don't accept AF_UNSPEC. */ + EXPECT_EQ(-EINVAL, res); + } + + ASSERT_EQ(0, connect_variant(client_fd, &self->srv0)); + + /* Minimal explicit AF_UNSPEC address (just the sa_family_t field) */ + res = sendto_variant_addrlen(client_fd, &self->unspec_srv0, + get_addrlen(&self->unspec_srv0, true), "D", + 1, 0); + if (variant->prot.domain == AF_INET6) { + if (restricted) { + /* AF_UNSPEC is always denied in IPv6. */ + EXPECT_EQ(-EACCES, res); + } else { + /* + * IPv6 sockets treat AF_UNSPEC as a NULL address, + * falling back to the connected address. + */ + ASSERT_EQ(0, res); + EXPECT_EQ(1, read(srv0_fd, read_buf, 1)); + EXPECT_EQ(read_buf[0], 'D'); + } + } else { + /* + * IPv4 socket will expect a struct sockaddr_in, our address is + * considered truncated. And Unix sockets don't accept + * AF_UNSPEC at all. + */ + EXPECT_EQ(-EINVAL, res); + } +} + +FIXTURE(ipv4) +{ + struct service_fixture srv0, srv1; +}; + +FIXTURE_VARIANT(ipv4) +{ + const enum sandbox_type sandbox; + const int type; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, no_sandbox_with_tcp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .type = SOCK_STREAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_tcp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .type = SOCK_STREAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_tcp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .type = SOCK_STREAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, no_sandbox_with_udp) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .type = SOCK_DGRAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_udp) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .type = SOCK_DGRAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_udp) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .type = SOCK_DGRAM, +}; + +FIXTURE_SETUP(ipv4) +{ + const struct protocol_variant prot = { + .domain = AF_INET, + .type = variant->type, + }; + + disable_caps(_metadata); + + set_service(&self->srv0, prot, 0); + set_service(&self->srv1, prot, 1); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(ipv4) +{ +} + +TEST_F(ipv4, from_unix_to_inet) +{ + int unix_stream_fd, unix_dgram_fd; + + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + }; + const struct landlock_net_port_attr tcp_bind_connect_p0 = { + .allowed_access = access_rights, + .port = self->srv0.port, + }; + int ruleset_fd; + + /* Denies connect and bind to check errno value. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows connect and bind for srv0. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_connect_p0, 0)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + unix_stream_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_LE(0, unix_stream_fd); + + unix_dgram_fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); + ASSERT_LE(0, unix_dgram_fd); + + /* Checks unix stream bind and connect for srv0. */ + EXPECT_EQ(-EINVAL, bind_variant(unix_stream_fd, &self->srv0)); + EXPECT_EQ(-EINVAL, connect_variant(unix_stream_fd, &self->srv0)); + + /* Checks unix stream bind and connect for srv1. */ + EXPECT_EQ(-EINVAL, bind_variant(unix_stream_fd, &self->srv1)) + { + TH_LOG("Wrong bind error: %s", strerror(errno)); + } + EXPECT_EQ(-EINVAL, connect_variant(unix_stream_fd, &self->srv1)); + + /* Checks unix datagram bind and connect for srv0. */ + EXPECT_EQ(-EINVAL, bind_variant(unix_dgram_fd, &self->srv0)); + EXPECT_EQ(-EINVAL, connect_variant(unix_dgram_fd, &self->srv0)); + + /* Checks unix datagram bind and connect for srv1. */ + EXPECT_EQ(-EINVAL, bind_variant(unix_dgram_fd, &self->srv1)); + EXPECT_EQ(-EINVAL, connect_variant(unix_dgram_fd, &self->srv1)); +} + +FIXTURE(tcp_layers) +{ + struct service_fixture srv0, srv1; +}; + +FIXTURE_VARIANT(tcp_layers) +{ + const size_t num_layers; + const int domain; +}; + +FIXTURE_SETUP(tcp_layers) +{ + const struct protocol_variant prot = { + .domain = variant->domain, + .type = SOCK_STREAM, + }; + + disable_caps(_metadata); + + ASSERT_EQ(0, set_service(&self->srv0, prot, 0)); + ASSERT_EQ(0, set_service(&self->srv1, prot, 1)); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(tcp_layers) +{ +} + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, no_sandbox_with_ipv4) { + /* clang-format on */ + .domain = AF_INET, + .num_layers = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, one_sandbox_with_ipv4) { + /* clang-format on */ + .domain = AF_INET, + .num_layers = 1, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, two_sandboxes_with_ipv4) { + /* clang-format on */ + .domain = AF_INET, + .num_layers = 2, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, three_sandboxes_with_ipv4) { + /* clang-format on */ + .domain = AF_INET, + .num_layers = 3, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, no_sandbox_with_ipv6) { + /* clang-format on */ + .domain = AF_INET6, + .num_layers = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, one_sandbox_with_ipv6) { + /* clang-format on */ + .domain = AF_INET6, + .num_layers = 1, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, two_sandboxes_with_ipv6) { + /* clang-format on */ + .domain = AF_INET6, + .num_layers = 2, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tcp_layers, three_sandboxes_with_ipv6) { + /* clang-format on */ + .domain = AF_INET6, + .num_layers = 3, +}; + +TEST_F(tcp_layers, ruleset_overlap) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + const struct landlock_net_port_attr tcp_bind = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = self->srv0.port, + }; + const struct landlock_net_port_attr tcp_bind_connect = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + .port = self->srv0.port, + }; + + if (variant->num_layers >= 1) { + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Allows bind. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind, 0)); + /* Also allows bind, but allows connect too. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_connect, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + if (variant->num_layers >= 2) { + int ruleset_fd; + + /* Creates another ruleset layer. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Only allows bind. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + if (variant->num_layers >= 3) { + int ruleset_fd; + + /* Creates another ruleset layer. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Try to allow bind and connect. */ + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_connect, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + /* + * Forbids to connect to the socket because only one ruleset layer + * allows connect. + */ + test_bind_and_connect(_metadata, &self->srv0, false, + variant->num_layers >= 2); +} + +TEST_F(tcp_layers, ruleset_expand) +{ + if (variant->num_layers >= 1) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + }; + /* Allows bind for srv0. */ + const struct landlock_net_port_attr bind_srv0 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = self->srv0.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_srv0, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + if (variant->num_layers >= 2) { + /* Expands network mask with connect action. */ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + /* Allows bind for srv0 and connect to srv0. */ + const struct landlock_net_port_attr tcp_bind_connect_p0 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + .port = self->srv0.port, + }; + /* Try to allow bind for srv1. */ + const struct landlock_net_port_attr tcp_bind_p1 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = self->srv1.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_connect_p0, 0)); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_p1, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + if (variant->num_layers >= 3) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + /* Allows connect to srv0, without bind rule. */ + const struct landlock_net_port_attr tcp_bind_p0 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = self->srv0.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_p0, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + test_bind_and_connect(_metadata, &self->srv0, false, + variant->num_layers >= 3); + + test_bind_and_connect(_metadata, &self->srv1, variant->num_layers >= 1, + variant->num_layers >= 2); +} + +/* clang-format off */ +FIXTURE(mini) {}; +/* clang-format on */ + +FIXTURE_SETUP(mini) +{ + disable_caps(_metadata); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(mini) +{ +} + +/* clang-format off */ + +#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP + +#define ACCESS_ALL ( \ + LANDLOCK_ACCESS_NET_BIND_TCP | \ + LANDLOCK_ACCESS_NET_CONNECT_TCP | \ + LANDLOCK_ACCESS_NET_BIND_UDP | \ + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) + +/* clang-format on */ + +TEST_F(mini, network_access_rights) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = ACCESS_ALL, + }; + struct landlock_net_port_attr net_port = { + .port = sock_port_start, + }; + int ruleset_fd; + __u64 access; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + for (access = 1; access <= ACCESS_LAST; access <<= 1) { + net_port.allowed_access = access; + EXPECT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &net_port, 0)) + { + TH_LOG("Failed to add rule with access 0x%llx: %s", + (unsigned long long)access, strerror(errno)); + } + } + EXPECT_EQ(0, close(ruleset_fd)); +} + +/* Checks invalid attribute, out of landlock network access range. */ +TEST_F(mini, ruleset_with_unknown_access) +{ + __u64 access_mask; + + for (access_mask = 1ULL << 63; access_mask != ACCESS_LAST; + access_mask >>= 1) { + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_mask, + }; + + EXPECT_EQ(-1, landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0)); + EXPECT_EQ(EINVAL, errno); + } +} + +TEST_F(mini, rule_with_unknown_access) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = ACCESS_ALL, + }; + struct landlock_net_port_attr net_port = { + .port = sock_port_start, + }; + int ruleset_fd; + __u64 access; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + for (access = 1ULL << 63; access != ACCESS_LAST; access >>= 1) { + net_port.allowed_access = access; + EXPECT_EQ(-1, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &net_port, 0)); + EXPECT_EQ(EINVAL, errno); + } + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(mini, rule_with_unhandled_access) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + }; + struct landlock_net_port_attr net_port = { + .port = sock_port_start, + }; + int ruleset_fd; + __u64 access; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + for (access = 1; access > 0; access <<= 1) { + int err; + + net_port.allowed_access = access; + err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &net_port, 0); + if (access == ruleset_attr.handled_access_net) { + EXPECT_EQ(0, err); + } else { + EXPECT_EQ(-1, err); + EXPECT_EQ(EINVAL, errno); + } + } + + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST_F(mini, inval) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP + }; + const struct landlock_net_port_attr tcp_bind_connect = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + .port = sock_port_start, + }; + const struct landlock_net_port_attr tcp_denied = { + .allowed_access = 0, + .port = sock_port_start, + }; + const struct landlock_net_port_attr tcp_bind = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = sock_port_start, + }; + int ruleset_fd; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Checks unhandled allowed_access. */ + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind_connect, 0)); + EXPECT_EQ(EINVAL, errno); + + /* Checks zero access value. */ + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_denied, 0)); + EXPECT_EQ(ENOMSG, errno); + + /* Adds with legitimate values. */ + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind, 0)); +} + +TEST_F(mini, tcp_port_overflow) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + const struct landlock_net_port_attr port_max_bind = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = UINT16_MAX, + }; + const struct landlock_net_port_attr port_max_connect = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP, + .port = UINT16_MAX, + }; + const struct landlock_net_port_attr port_overflow1 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = UINT16_MAX + 1, + }; + const struct landlock_net_port_attr port_overflow2 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = UINT16_MAX + 2, + }; + const struct landlock_net_port_attr port_overflow3 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = UINT32_MAX + 1UL, + }; + const struct landlock_net_port_attr port_overflow4 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = UINT32_MAX + 2UL, + }; + const struct protocol_variant ipv4_tcp = { + .domain = AF_INET, + .type = SOCK_STREAM, + }; + struct service_fixture srv_denied, srv_max_allowed; + int ruleset_fd; + + ASSERT_EQ(0, set_service(&srv_denied, ipv4_tcp, 0)); + + /* Be careful to avoid port inconsistencies. */ + srv_max_allowed = srv_denied; + srv_max_allowed.port = port_max_bind.port; + srv_max_allowed.ipv4_addr.sin_port = htons(port_max_bind.port); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_max_bind, 0)); + + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_overflow1, 0)); + EXPECT_EQ(EINVAL, errno); + + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_overflow2, 0)); + EXPECT_EQ(EINVAL, errno); + + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_overflow3, 0)); + EXPECT_EQ(EINVAL, errno); + + /* Interleaves with invalid rule additions. */ + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_max_connect, 0)); + + EXPECT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &port_overflow4, 0)); + EXPECT_EQ(EINVAL, errno); + + enforce_ruleset(_metadata, ruleset_fd); + + test_bind_and_connect(_metadata, &srv_denied, true, true); + test_bind_and_connect(_metadata, &srv_max_allowed, false, false); +} + +FIXTURE(ipv4_tcp) +{ + struct service_fixture srv0, srv1; +}; + +FIXTURE_SETUP(ipv4_tcp) +{ + const struct protocol_variant ipv4_tcp = { + .domain = AF_INET, + .type = SOCK_STREAM, + }; + + disable_caps(_metadata); + + ASSERT_EQ(0, set_service(&self->srv0, ipv4_tcp, 0)); + ASSERT_EQ(0, set_service(&self->srv1, ipv4_tcp, 1)); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(ipv4_tcp) +{ +} + +TEST_F(ipv4_tcp, port_endianness) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + }; + const struct landlock_net_port_attr bind_host_endian_p0 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + /* Host port format. */ + .port = self->srv0.port, + }; + const struct landlock_net_port_attr connect_big_endian_p0 = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP, + /* Big endian port format. */ + .port = htons(self->srv0.port), + }; + const struct landlock_net_port_attr bind_connect_host_endian_p1 = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP, + /* Host port format. */ + .port = self->srv1.port, + }; + const unsigned int one = 1; + const char little_endian = *(const char *)&one; + int ruleset_fd; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_host_endian_p0, 0)); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &connect_big_endian_p0, 0)); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect_host_endian_p1, 0)); + enforce_ruleset(_metadata, ruleset_fd); + + /* No restriction for big endinan CPU. */ + test_bind_and_connect(_metadata, &self->srv0, false, little_endian); + + /* No restriction for any CPU. */ + test_bind_and_connect(_metadata, &self->srv1, false, false); +} + +TEST_F(ipv4_tcp, with_fs) +{ + const struct landlock_ruleset_attr ruleset_attr_fs_net = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP, + }; + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, + .parent_fd = -1, + }; + struct landlock_net_port_attr tcp_bind = { + .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP, + .port = self->srv0.port, + }; + int ruleset_fd, bind_fd, dir_fd; + + /* Creates ruleset both for filesystem and network access. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr_fs_net, + sizeof(ruleset_attr_fs_net), 0); + ASSERT_LE(0, ruleset_fd); + + /* Adds a filesystem rule. */ + path_beneath.parent_fd = open("/dev", 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)); + EXPECT_EQ(0, close(path_beneath.parent_fd)); + + /* Adds a network rule. */ + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &tcp_bind, 0)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + /* Tests file access. */ + dir_fd = open("/dev", O_RDONLY); + EXPECT_LE(0, dir_fd); + EXPECT_EQ(0, close(dir_fd)); + + dir_fd = open("/", O_RDONLY); + EXPECT_EQ(-1, dir_fd); + EXPECT_EQ(EACCES, errno); + + /* Tests port binding. */ + bind_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_LE(0, bind_fd); + EXPECT_EQ(0, bind_variant(bind_fd, &self->srv0)); + EXPECT_EQ(0, close(bind_fd)); + + bind_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_LE(0, bind_fd); + EXPECT_EQ(-EACCES, bind_variant(bind_fd, &self->srv1)); +} + +FIXTURE(port_specific) +{ + struct service_fixture srv0; + struct service_fixture cli1; +}; + +FIXTURE_VARIANT(port_specific) +{ + const enum sandbox_type sandbox; + const struct protocol_variant prot; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv4) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv4) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv4) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) { + /* clang-format on */ + .sandbox = NO_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv6) { + /* clang-format on */ + .sandbox = TCP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv6) { + /* clang-format on */ + .sandbox = UDP_SANDBOX, + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + +FIXTURE_SETUP(port_specific) +{ + disable_caps(_metadata); + + ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + ASSERT_EQ(0, set_service(&self->cli1, variant->prot, 1)); + + setup_loopback(_metadata); +}; + +FIXTURE_TEARDOWN(port_specific) +{ +} + +TEST_F(port_specific, bind_connect_zero) +{ + int bind_fd, connect_fd, ret; + uint16_t port; + + /* Adds a rule layer with bind and connect actions. */ + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + }; + const struct landlock_net_port_attr bind_connect_zero = { + .allowed_access = access_rights, + .port = 0, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + /* Checks zero port value on bind and connect actions. */ + EXPECT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect_zero, 0)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + + connect_fd = socket_variant(&self->srv0); + ASSERT_LE(0, connect_fd); + + /* Sets address port to 0 for both protocol families. */ + set_port(&self->srv0, 0); + /* + * Binds on port 0, which selects a random port within + * ip_local_port_range. + */ + ret = bind_variant(bind_fd, &self->srv0); + EXPECT_EQ(0, ret); + + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + + /* Connects on port 0. */ + ret = connect_variant(connect_fd, &self->srv0); + if (variant->prot.type == SOCK_STREAM) { + EXPECT_EQ(-ECONNREFUSED, ret); + } else { + EXPECT_EQ(0, ret); + } + + /* Sets binded port for both protocol families. */ + port = get_binded_port(bind_fd, &variant->prot); + EXPECT_NE(0, port); + set_port(&self->srv0, port); + /* Connects on the binded port. */ + ret = connect_variant(connect_fd, &self->srv0); + if (is_restricted(&variant->prot, variant->sandbox)) { + /* Denied by Landlock. */ + EXPECT_EQ(-EACCES, ret); + } else { + EXPECT_EQ(0, ret); + } + + EXPECT_EQ(0, close(connect_fd)); + EXPECT_EQ(0, close(bind_fd)); +} + +TEST_F(port_specific, bind_connect_1023) +{ + int bind_fd, connect_fd, ret; + + /* Adds a rule layer with bind and connect actions. */ + if (variant->sandbox == TCP_SANDBOX || + variant->sandbox == UDP_SANDBOX) { + const __u64 bind_right = (variant->sandbox == TCP_SANDBOX ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 access_rights = + (variant->sandbox == TCP_SANDBOX ? + (LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP) : + (LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + }; + /* A rule with port value less than 1024. */ + const struct landlock_net_port_attr bind_connect_low_range = { + .allowed_access = access_rights, + .port = 1023, + }; + /* A rule with 1024 port. */ + const struct landlock_net_port_attr bind_connect = { + .allowed_access = access_rights, + .port = 1024, + }; + /* A rule with cli1's port, to use as source port. */ + const struct landlock_net_port_attr srcport = { + .allowed_access = bind_right, + .port = self->cli1.port, + }; + int ruleset_fd; + + ruleset_fd = landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect_low_range, 0)); + ASSERT_EQ(0, + landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &bind_connect, 0)); + if (variant->sandbox == UDP_SANDBOX) { + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, + LANDLOCK_RULE_NET_PORT, + &srcport, 0)); + } + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + } + + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + + /* Sets address port to 1023 for both protocol families. */ + set_port(&self->srv0, 1023); + /* Binds on port 1023. */ + ret = bind_variant(bind_fd, &self->srv0); + /* Denied by the system. */ + EXPECT_EQ(-EACCES, ret); + + /* Binds on port 1023. */ + set_cap(_metadata, CAP_NET_BIND_SERVICE); + ret = bind_variant(bind_fd, &self->srv0); + clear_cap(_metadata, CAP_NET_BIND_SERVICE); + EXPECT_EQ(0, ret); + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + + connect_fd = socket_variant(&self->srv0); + ASSERT_LE(0, connect_fd); + if (variant->prot.type == SOCK_DGRAM) { + /* + * We are about to connect(), but bind() is restricted, so for + * UDP sockets we need to use cli1's port as source port (the + * only one we are allowed to use). + */ + EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1)); + } + /* Connects on the binded port 1023. */ + ret = connect_variant(connect_fd, &self->srv0); + EXPECT_EQ(0, ret); + + EXPECT_EQ(0, close(connect_fd)); + EXPECT_EQ(0, close(bind_fd)); + + bind_fd = socket_variant(&self->srv0); + ASSERT_LE(0, bind_fd); + + connect_fd = socket_variant(&self->srv0); + ASSERT_LE(0, connect_fd); + + /* Sets address port to 1024 for both protocol families. */ + set_port(&self->srv0, 1024); + /* Binds on port 1024. */ + ret = bind_variant(bind_fd, &self->srv0); + EXPECT_EQ(0, ret); + if (variant->prot.type == SOCK_STREAM) + EXPECT_EQ(0, listen(bind_fd, backlog)); + if (variant->prot.type == SOCK_DGRAM) + EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1)); + + /* Connects on the binded port 1024. */ + ret = connect_variant(connect_fd, &self->srv0); + EXPECT_EQ(0, ret); + + EXPECT_EQ(0, close(connect_fd)); + EXPECT_EQ(0, close(bind_fd)); +} + +/** + * matches_auditlog - Check audit log for a network access denial + * + * @audit_fd: Audit file descriptor. + * @blockers: A regex-escaped blocker string, e.g., "net\.bind_tcp". + * @dir_addr: Either "saddr" or "daddr", ignored if addr is NULL. + * @addr: A regex-escaped IP address string, or NULL. + * @dir_port: Either "src" or "dest", ignored if addr is NULL. + * @port: A port number, ignored if addr is NULL. + */ +static int matches_auditlog(const int audit_fd, const char *const blockers, + const char *const dir_addr, const char *const addr, + const char *const dir_port, const __u16 port) +{ + static const char log_with_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX + " blockers=%s %s=%s %s=%u$"; + static const char log_without_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX + " blockers=%s"; + /* + * Max strlen(blockers): 16 + * Max strlen(dir_addr): 5 + * Max strlen(addr): 12 + * Max strlen(dir_port): 4 + * Max strlen(%u port): 5 + */ + char log_match[sizeof(log_with_addrport_tmpl) + 42]; + int log_match_len; + + if (addr == NULL) + log_match_len = snprintf(log_match, sizeof(log_match), + log_without_addrport_tmpl, blockers); + else + log_match_len = snprintf(log_match, sizeof(log_match), + log_with_addrport_tmpl, blockers, + dir_addr, addr, dir_port, 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 service_fixture srv1; + /* srv2 has a rule with no access but quiet bit set. */ + struct service_fixture srv2; + struct service_fixture unspec_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_tcp) { + /* clang-format on */ + .addr = "127\\.0\\.0\\.1", + .prot = { + .domain = AF_INET, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv4_udp) { + /* clang-format on */ + .addr = "127\\.0\\.0\\.1", + .prot = { + .domain = AF_INET, + .type = SOCK_DGRAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv6_tcp) { + /* clang-format on */ + .addr = "::1", + .prot = { + .domain = AF_INET6, + .type = SOCK_STREAM, + }, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(audit, ipv6_udp) { + /* clang-format on */ + .addr = "::1", + .prot = { + .domain = AF_INET6, + .type = SOCK_DGRAM, + }, +}; + +FIXTURE_SETUP(audit) +{ + struct protocol_variant prot_unspec = variant->prot; + + prot_unspec.domain = AF_UNSPEC; + + ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0)); + ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1)); + ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2)); + ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 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 char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.bind_tcp" : + "net\\.bind_udp"); + const __u64 access_rights = + (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP | + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP | + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + .quiet_access_net = access_rights, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, + }; + 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); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + 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_auditlog(self->audit_fd, audit_evt, "saddr", + variant->addr, "src", self->srv0.port)); + + 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)); + + /* Bind to srv2 (with quiet rule): no new audit logs. */ + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv2)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + +TEST_F(audit, connect) +{ + const char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const __u64 bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const __u64 access_rights = bind_right | conn_right; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + .quiet_access_net = access_rights, + }; + const struct landlock_net_port_attr rule_connect_p1 = { + .allowed_access = conn_right, + .port = self->srv1.port, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, + }; + 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); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule_connect_p1, 0)); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + 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_auditlog(self->audit_fd, audit_evt, "daddr", + variant->addr, "dest", self->srv0.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + if (variant->prot.type == SOCK_DGRAM) { + /* Check that autobind generates a denied bind event. */ + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1)); + + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", + NULL, NULL, NULL, 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + } + + EXPECT_EQ(0, close(sock_fd)); + + /* Connect to srv2 (with quiet rule): no new audit logs. */ + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(0, close(sock_fd)); +} + +/* Quieting bind access has no effect on connect. */ +TEST_F(audit, connect_quiet_bind) +{ + const char *audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const int bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const int conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const int access_rights = bind_right | conn_right; + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = access_rights, + .quiet_access_net = bind_right, + }; + const struct landlock_ruleset_attr ruleset_attr_2 = { + .handled_access_net = access_rights, + .quiet_access_net = conn_right, + }; + const struct landlock_net_port_attr quiet_rule = { + .allowed_access = 0, + .port = self->srv2.port, + }; + 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); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr", + variant->addr, "dest", self->srv2.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(sock_fd)); + + /* New layer that also denies connect but has the correct quiet bit. */ + ruleset_fd = landlock_create_ruleset(&ruleset_attr_2, + sizeof(ruleset_attr_2), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &quiet_rule, LANDLOCK_ADD_RULE_QUIET)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv2); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv2)); + + /* Quieted - no logs expected. */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + EXPECT_EQ(0, close(sock_fd)); +} + +static int matches_log_connect_bound(int audit_fd, const char *const blockers, + const char *const addr, __u16 lport, + __u16 dport) +{ + static const char log_template[] = REGEX_LANDLOCK_PREFIX + " blockers=%s laddr=%s lport=%u daddr=%s dest=%u$"; + /* Slack for the blockers, two addresses and two port numbers. */ + char log_match[sizeof(log_template) + 60]; + int log_match_len; + + log_match_len = snprintf(log_match, sizeof(log_match), log_template, + blockers, addr, lport, addr, dport); + if (log_match_len > sizeof(log_match)) + return -E2BIG; + + return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match, + NULL); +} + +/* + * After a bind() to an allowed port, a denied connect must report laddr/lport + * from the bound socket (made available through audit_net.sk) in addition to + * the connect sockaddr's daddr/dest. + */ +TEST_F(audit, connect_bound) +{ + const __u64 bind_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_BIND_TCP : + LANDLOCK_ACCESS_NET_BIND_UDP); + const __u64 conn_right = (variant->prot.type == SOCK_STREAM ? + LANDLOCK_ACCESS_NET_CONNECT_TCP : + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP); + const char *const audit_evt = (variant->prot.type == SOCK_STREAM ? + "net\\.connect_tcp" : + "net\\.connect_send_udp"); + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = bind_right | conn_right, + }; + const struct landlock_net_port_attr rule_bind = { + .allowed_access = bind_right, + .port = self->srv0.port, + }; + struct service_fixture srv_remote; + struct audit_records records; + int ruleset_fd, sock_fd; + + /* Uses a second port as the denied connect target. */ + ASSERT_EQ(0, set_service(&srv_remote, variant->prot, 1)); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule_bind, 0)); + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + sock_fd = socket_variant(&self->srv0); + ASSERT_LE(0, sock_fd); + EXPECT_EQ(0, bind_variant(sock_fd, &self->srv0)); + EXPECT_EQ(-EACCES, connect_variant(sock_fd, &srv_remote)); + EXPECT_EQ(0, matches_log_connect_bound(self->audit_fd, audit_evt, + variant->addr, self->srv0.port, + srv_remote.port)); + + 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, sendmsg) +{ + const struct landlock_ruleset_attr ruleset_attr = { + .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP | + LANDLOCK_ACCESS_NET_BIND_UDP, + }; + const struct landlock_net_port_attr rule = { + .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, + .port = self->srv1.port, + }; + struct audit_records records; + int ruleset_fd; + int sock_fd; + + /* Sendmsg on stream sockets is never denied. */ + if (variant->prot.type != SOCK_DGRAM) + return; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_LE(0, ruleset_fd); + ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, + &rule, 0)); + 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, sendto_variant(sock_fd, &self->srv0, "A", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp", + "daddr", variant->addr, "dest", + self->srv0.port)); + + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(1, records.domain); + + /* Check that autobind generates a denied bind event. */ + EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv1, "A", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", NULL, + NULL, NULL, 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, records.domain); + + EXPECT_EQ(-EACCES, + sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0)); + EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_udp", + "daddr", NULL, "dest", 0)); + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + EXPECT_EQ(0, 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 new file mode 100644 index 000000000..4f64c9058 --- /dev/null +++ b/tools/testing/selftests/landlock/ptrace_test.c @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Ptrace + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/landlock.h> +#include <signal.h> +#include <sys/prctl.h> +#include <sys/ptrace.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "audit.h" +#include "common.h" + +/* Copied from security/yama/yama_lsm.c */ +#define YAMA_SCOPE_DISABLED 0 +#define YAMA_SCOPE_RELATIONAL 1 + +static void create_domain(struct __test_metadata *const _metadata) +{ + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK, + }; + + 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)); + } + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + EXPECT_EQ(0, close(ruleset_fd)); +} + +static int test_ptrace_read(const pid_t pid) +{ + static const char path_template[] = "/proc/%d/environ"; + char procenv_path[sizeof(path_template) + 10]; + int procenv_path_size, fd; + + procenv_path_size = snprintf(procenv_path, sizeof(procenv_path), + path_template, pid); + if (procenv_path_size >= sizeof(procenv_path)) + return E2BIG; + + fd = open(procenv_path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return errno; + /* + * Mixing error codes from close(2) and open(2) should not lead to any + * (access type) confusion for this test. + */ + if (close(fd) != 0) + return errno; + return 0; +} + +static int get_yama_ptrace_scope(void) +{ + int ret; + char buf[2] = {}; + const int fd = open("/proc/sys/kernel/yama/ptrace_scope", O_RDONLY); + + if (fd < 0) + return 0; + + if (read(fd, buf, 1) < 0) { + close(fd); + return -1; + } + + ret = atoi(buf); + close(fd); + return ret; +} + +/* clang-format off */ +FIXTURE(scoped_domains) {}; +/* clang-format on */ + +/* + * Test multiple tracing combinations between a parent process P1 and a child + * process P2. + * + * Yama's scoped ptrace is presumed disabled. If enabled, this optional + * restriction is enforced in addition to any Landlock check, which means that + * all P2 requests to trace P1 would be denied. + */ +#include "scoped_base_variants.h" + +FIXTURE_SETUP(scoped_domains) +{ +} + +FIXTURE_TEARDOWN(scoped_domains) +{ +} + +/* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */ +TEST_F(scoped_domains, trace) +{ + pid_t child, parent; + int status, err_proc_read; + int pipe_child[2], pipe_parent[2]; + int yama_ptrace_scope; + char buf_parent; + long ret; + bool can_read_child, can_trace_child, can_read_parent, can_trace_parent; + + 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); + + /* + * can_read_child is true if a parent process can read its child + * process, which is only the case when the parent process is not + * isolated from the child with a dedicated Landlock domain. + */ + can_read_child = !variant->domain_parent; + + /* + * can_trace_child is true if a parent process can trace its child + * process. This depends on two conditions: + * - The parent process is not isolated from the child with a dedicated + * Landlock domain. + * - Yama allows tracing children (up to YAMA_SCOPE_RELATIONAL). + */ + can_trace_child = can_read_child && + yama_ptrace_scope <= YAMA_SCOPE_RELATIONAL; + + /* + * can_read_parent is true if a child process can read its parent + * process, which is only the case when the child process is not + * isolated from the parent with a dedicated Landlock domain. + */ + can_read_parent = !variant->domain_child; + + /* + * can_trace_parent is true if a child process can trace its parent + * process. This depends on two conditions: + * - The child process is not isolated from the parent with a dedicated + * Landlock domain. + * - Yama is disabled (YAMA_SCOPE_DISABLED). + */ + can_trace_parent = can_read_parent && + yama_ptrace_scope <= YAMA_SCOPE_DISABLED; + + /* + * Removes all effective and permitted capabilities to not interfere + * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS. + */ + drop_caps(_metadata); + + parent = getpid(); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + if (variant->domain_both) { + create_domain(_metadata); + if (!__test_passed(_metadata)) + /* Aborts before forking. */ + return; + } + + 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])); + if (variant->domain_child) + create_domain(_metadata); + + /* Waits for the parent to be in a domain, if any. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + + /* Tests PTRACE_MODE_READ on the parent. */ + err_proc_read = test_ptrace_read(parent); + if (can_read_parent) { + EXPECT_EQ(0, err_proc_read); + } else { + EXPECT_EQ(EACCES, err_proc_read); + } + + /* Tests PTRACE_ATTACH on the parent. */ + ret = ptrace(PTRACE_ATTACH, parent, NULL, 0); + if (can_trace_parent) { + EXPECT_EQ(0, ret); + } else { + EXPECT_EQ(-1, ret); + EXPECT_EQ(EPERM, errno); + } + if (ret == 0) { + ASSERT_EQ(parent, waitpid(parent, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0)); + } + + /* Tests child PTRACE_TRACEME. */ + ret = ptrace(PTRACE_TRACEME); + if (can_trace_child) { + EXPECT_EQ(0, ret); + } else { + EXPECT_EQ(-1, ret); + EXPECT_EQ(EPERM, errno); + } + + /* + * Signals that the PTRACE_ATTACH test is done and the + * PTRACE_TRACEME test is ongoing. + */ + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + if (can_trace_child) { + ASSERT_EQ(0, raise(SIGSTOP)); + } + + /* 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])); + if (variant->domain_parent) + create_domain(_metadata); + + /* Signals that the parent is in a domain, if any. */ + 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)); + + /* Tests child PTRACE_TRACEME. */ + if (can_trace_child) { + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); + } else { + /* The child should not be traced by the parent. */ + EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0)); + EXPECT_EQ(ESRCH, errno); + } + + /* Tests PTRACE_MODE_READ on the child. */ + err_proc_read = test_ptrace_read(child); + if (can_read_child) { + EXPECT_EQ(0, err_proc_read); + } else { + EXPECT_EQ(EACCES, err_proc_read); + } + + /* Tests PTRACE_ATTACH on the child. */ + ret = ptrace(PTRACE_ATTACH, child, NULL, 0); + if (can_trace_child) { + EXPECT_EQ(0, ret); + } else { + EXPECT_EQ(-1, ret); + EXPECT_EQ(EPERM, errno); + } + + if (ret == 0) { + ASSERT_EQ(child, waitpid(child, &status, 0)); + ASSERT_EQ(1, WIFSTOPPED(status)); + ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); + } + + /* 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; +} + +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/sandbox-and-launch.c b/tools/testing/selftests/landlock/sandbox-and-launch.c new file mode 100644 index 000000000..3e32e1a51 --- /dev/null +++ b/tools/testing/selftests/landlock/sandbox-and-launch.c @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Sandbox itself and execute another program (in a different mount point). + * + * Used by layout1.umount_sandboxer from fs_test.c + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/prctl.h> +#include <unistd.h> + +#include "wrappers.h" + +int main(int argc, char *argv[]) +{ + struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + int pipe_child, pipe_parent, ruleset_fd; + char buf; + + /* + * The first argument must be the file descriptor number of a pipe. + * The second argument must be the program to execute. + */ + if (argc != 4) { + fprintf(stderr, "Wrong number of arguments (not three)\n"); + return 1; + } + + pipe_child = atoi(argv[2]); + pipe_parent = atoi(argv[3]); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + perror("Failed to create ruleset"); + return 1; + } + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + perror("Failed to call prctl()"); + return 1; + } + + if (landlock_restrict_self(ruleset_fd, 0)) { + perror("Failed to restrict self"); + return 1; + } + + if (close(ruleset_fd)) { + perror("Failed to close ruleset"); + return 1; + } + + /* Signals that we are sandboxed. */ + errno = 0; + if (write(pipe_child, ".", 1) != 1) { + perror("Failed to write to the second argument"); + return 1; + } + + /* Waits for the parent to try to umount. */ + if (read(pipe_parent, &buf, 1) != 1) { + perror("Failed to write to the third argument"); + return 1; + } + + /* Shifts arguments. */ + argv[0] = argv[1]; + argv[1] = argv[2]; + argv[2] = argv[3]; + argv[3] = NULL; + execve(argv[0], argv, NULL); + perror("Failed to execute the provided binary"); + return 1; +} diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c new file mode 100644 index 000000000..40fc82fbf --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c @@ -0,0 +1,1208 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Abstract UNIX socket + * + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/landlock.h> +#include <sched.h> +#include <signal.h> +#include <stddef.h> +#include <sys/prctl.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "audit.h" +#include "common.h" +#include "scoped_common.h" + +/* Number of pending connections queue to be hold. */ +const short backlog = 10; + +static void create_fs_domain(struct __test_metadata *const _metadata) +{ + int ruleset_fd; + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, + }; + + 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)); + } + EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + EXPECT_EQ(0, close(ruleset_fd)); +} + +FIXTURE(scoped_domains) +{ + struct service_fixture stream_address, dgram_address; +}; + +#include "scoped_base_variants.h" + +FIXTURE_SETUP(scoped_domains) +{ + drop_caps(_metadata); + + memset(&self->stream_address, 0, sizeof(self->stream_address)); + memset(&self->dgram_address, 0, sizeof(self->dgram_address)); + set_unix_address(&self->stream_address, 0); + set_unix_address(&self->dgram_address, 1); +} + +FIXTURE_TEARDOWN(scoped_domains) +{ +} + +/* + * Test unix_stream_connect() and unix_may_send() for a child connecting to its + * parent, when they have scoped domain or no domain. + */ +TEST_F(scoped_domains, connect_to_parent) +{ + pid_t child; + bool can_connect_to_parent; + int status; + int pipe_parent[2]; + int stream_server, dgram_server; + + /* + * can_connect_to_parent is true if a child process can connect to its + * parent process. This depends on the child process not being isolated + * from the parent with a dedicated Landlock domain. + */ + can_connect_to_parent = !variant->domain_child; + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + if (variant->domain_both) { + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + if (!__test_passed(_metadata)) + return; + } + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int err; + int stream_client, dgram_client; + char buf_child; + + EXPECT_EQ(0, close(pipe_parent[1])); + if (variant->domain_child) + create_scoped_domain( + _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + stream_client = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_client); + dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_client); + + /* Waits for the server. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + + err = connect(stream_client, &self->stream_address.unix_addr, + self->stream_address.unix_addr_len); + if (can_connect_to_parent) { + EXPECT_EQ(0, err); + } else { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } + EXPECT_EQ(0, close(stream_client)); + + err = connect(dgram_client, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len); + if (can_connect_to_parent) { + EXPECT_EQ(0, err); + } else { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } + EXPECT_EQ(0, close(dgram_client)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + if (variant->domain_parent) + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + stream_server = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_server); + dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server); + ASSERT_EQ(0, bind(stream_server, &self->stream_address.unix_addr, + self->stream_address.unix_addr_len)); + ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len)); + ASSERT_EQ(0, listen(stream_server, backlog)); + + /* Signals to child that the parent is listening. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(stream_server)); + EXPECT_EQ(0, close(dgram_server)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +/* + * Test unix_stream_connect() and unix_may_send() for a parent connecting to + * its child, when they have scoped domain or no domain. + */ +TEST_F(scoped_domains, connect_to_child) +{ + pid_t child; + bool can_connect_to_child; + int err_stream, err_dgram, errno_stream, errno_dgram, status; + int pipe_child[2], pipe_parent[2]; + char buf; + int stream_client, dgram_client; + + /* + * can_connect_to_child is true if a parent process can connect to its + * child process. The parent process is not isolated from the child + * with a dedicated Landlock domain. + */ + can_connect_to_child = !variant->domain_parent; + + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + if (variant->domain_both) { + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + if (!__test_passed(_metadata)) + return; + } + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int stream_server, dgram_server; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + if (variant->domain_child) + create_scoped_domain( + _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* Waits for the parent to be in a domain, if any. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + + stream_server = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_server); + dgram_server = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server); + ASSERT_EQ(0, + bind(stream_server, &self->stream_address.unix_addr, + self->stream_address.unix_addr_len)); + ASSERT_EQ(0, bind(dgram_server, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len)); + ASSERT_EQ(0, listen(stream_server, backlog)); + + /* 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(stream_server)); + 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])); + + if (variant->domain_parent) + 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)); + + stream_client = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_client); + 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_stream = connect(stream_client, &self->stream_address.unix_addr, + self->stream_address.unix_addr_len); + errno_stream = errno; + err_dgram = connect(dgram_client, &self->dgram_address.unix_addr, + self->dgram_address.unix_addr_len); + errno_dgram = errno; + if (can_connect_to_child) { + EXPECT_EQ(0, err_stream); + EXPECT_EQ(0, err_dgram); + } else { + EXPECT_EQ(-1, err_stream); + EXPECT_EQ(-1, err_dgram); + EXPECT_EQ(EPERM, errno_stream); + EXPECT_EQ(EPERM, errno_dgram); + } + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(stream_client)); + 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_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)); +} + +FIXTURE_VARIANT(scoped_audit) +{ + const __u64 scoped; + const __u64 quiet_scoped; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, no_quiet) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = 0, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, + .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_abstract_socket_2) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | + LANDLOCK_SCOPE_SIGNAL, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_audit, quiet_unrelated) +{ + /* clang-format on */ + .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL, + .quiet_scoped = LANDLOCK_SCOPE_SIGNAL, +}; + +/* 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; + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = variant->scoped, + .quiet_scoped = variant->quiet_scoped, + }; + bool should_audit = + !(variant->quiet_scoped & LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* 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])); + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_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)); + + /* 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); + + if (should_audit) { + 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)); + } + + /* No other logs */ + EXPECT_EQ(0, audit_count_records(self->audit_fd, &records)); + EXPECT_EQ(0, records.access); + + 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, + child_stream_address, child_dgram_address; +}; + +#include "scoped_multiple_domain_variants.h" + +FIXTURE_SETUP(scoped_vs_unscoped) +{ + drop_caps(_metadata); + + memset(&self->parent_stream_address, 0, + sizeof(self->parent_stream_address)); + set_unix_address(&self->parent_stream_address, 0); + memset(&self->parent_dgram_address, 0, + sizeof(self->parent_dgram_address)); + set_unix_address(&self->parent_dgram_address, 1); + memset(&self->child_stream_address, 0, + sizeof(self->child_stream_address)); + set_unix_address(&self->child_stream_address, 2); + memset(&self->child_dgram_address, 0, + sizeof(self->child_dgram_address)); + set_unix_address(&self->child_dgram_address, 3); +} + +FIXTURE_TEARDOWN(scoped_vs_unscoped) +{ +} + +/* + * Test unix_stream_connect and unix_may_send for parent, child and + * grand child processes when they can have scoped or non-scoped domains. + */ +TEST_F(scoped_vs_unscoped, unix_scoping) +{ + pid_t child; + int status; + bool can_connect_to_parent, can_connect_to_child; + int pipe_parent[2]; + int stream_server_parent, dgram_server_parent; + + can_connect_to_child = (variant->domain_grand_child != SCOPE_SANDBOX); + can_connect_to_parent = (can_connect_to_child && + (variant->domain_children != SCOPE_SANDBOX)); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + if (variant->domain_all == OTHER_SANDBOX) + create_fs_domain(_metadata); + else if (variant->domain_all == SCOPE_SANDBOX) + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int stream_server_child, dgram_server_child; + int pipe_child[2]; + pid_t grand_child; + + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + + if (variant->domain_children == OTHER_SANDBOX) + create_fs_domain(_metadata); + else if (variant->domain_children == SCOPE_SANDBOX) + create_scoped_domain( + _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + grand_child = fork(); + ASSERT_LE(0, grand_child); + if (grand_child == 0) { + char buf; + int stream_err, dgram_err, stream_errno, dgram_errno; + int stream_client, dgram_client; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[1])); + + if (variant->domain_grand_child == OTHER_SANDBOX) + create_fs_domain(_metadata); + else if (variant->domain_grand_child == SCOPE_SANDBOX) + create_scoped_domain( + _metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + stream_client = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_client); + dgram_client = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_client); + + ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); + stream_err = connect( + stream_client, + &self->child_stream_address.unix_addr, + self->child_stream_address.unix_addr_len); + stream_errno = errno; + dgram_err = connect( + dgram_client, + &self->child_dgram_address.unix_addr, + self->child_dgram_address.unix_addr_len); + dgram_errno = errno; + if (can_connect_to_child) { + EXPECT_EQ(0, stream_err); + EXPECT_EQ(0, dgram_err); + } else { + EXPECT_EQ(-1, stream_err); + EXPECT_EQ(-1, dgram_err); + EXPECT_EQ(EPERM, stream_errno); + EXPECT_EQ(EPERM, dgram_errno); + } + + EXPECT_EQ(0, close(stream_client)); + stream_client = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_client); + /* Datagram sockets can "reconnect". */ + + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + stream_err = connect( + stream_client, + &self->parent_stream_address.unix_addr, + self->parent_stream_address.unix_addr_len); + stream_errno = errno; + dgram_err = connect( + dgram_client, + &self->parent_dgram_address.unix_addr, + self->parent_dgram_address.unix_addr_len); + dgram_errno = errno; + if (can_connect_to_parent) { + EXPECT_EQ(0, stream_err); + EXPECT_EQ(0, dgram_err); + } else { + EXPECT_EQ(-1, stream_err); + EXPECT_EQ(-1, dgram_err); + EXPECT_EQ(EPERM, stream_errno); + EXPECT_EQ(EPERM, dgram_errno); + } + EXPECT_EQ(0, close(stream_client)); + EXPECT_EQ(0, close(dgram_client)); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_child[0])); + if (variant->domain_child == OTHER_SANDBOX) + create_fs_domain(_metadata); + else if (variant->domain_child == SCOPE_SANDBOX) + create_scoped_domain( + _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + stream_server_child = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_server_child); + dgram_server_child = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server_child); + + ASSERT_EQ(0, bind(stream_server_child, + &self->child_stream_address.unix_addr, + self->child_stream_address.unix_addr_len)); + ASSERT_EQ(0, bind(dgram_server_child, + &self->child_dgram_address.unix_addr, + self->child_dgram_address.unix_addr_len)); + ASSERT_EQ(0, listen(stream_server_child, backlog)); + + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + ASSERT_EQ(grand_child, waitpid(grand_child, &status, 0)); + EXPECT_EQ(0, close(stream_server_child)); + EXPECT_EQ(0, close(dgram_server_child)); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + + if (variant->domain_parent == OTHER_SANDBOX) + create_fs_domain(_metadata); + else if (variant->domain_parent == SCOPE_SANDBOX) + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + stream_server_parent = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_server_parent); + dgram_server_parent = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_server_parent); + ASSERT_EQ(0, bind(stream_server_parent, + &self->parent_stream_address.unix_addr, + self->parent_stream_address.unix_addr_len)); + ASSERT_EQ(0, bind(dgram_server_parent, + &self->parent_dgram_address.unix_addr, + self->parent_dgram_address.unix_addr_len)); + + ASSERT_EQ(0, listen(stream_server_parent, backlog)); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(stream_server_parent)); + EXPECT_EQ(0, close(dgram_server_parent)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +FIXTURE(outside_socket) +{ + struct service_fixture address, transit_address; +}; + +FIXTURE_VARIANT(outside_socket) +{ + const bool child_socket; + const int type; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, allow_dgram_child) { + /* clang-format on */ + .child_socket = true, + .type = SOCK_DGRAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, deny_dgram_server) { + /* clang-format on */ + .child_socket = false, + .type = SOCK_DGRAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, allow_stream_child) { + /* clang-format on */ + .child_socket = true, + .type = SOCK_STREAM, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(outside_socket, deny_stream_server) { + /* clang-format on */ + .child_socket = false, + .type = SOCK_STREAM, +}; + +FIXTURE_SETUP(outside_socket) +{ + drop_caps(_metadata); + + memset(&self->transit_address, 0, sizeof(self->transit_address)); + set_unix_address(&self->transit_address, 0); + memset(&self->address, 0, sizeof(self->address)); + set_unix_address(&self->address, 1); +} + +FIXTURE_TEARDOWN(outside_socket) +{ +} + +/* + * Test unix_stream_connect and unix_may_send for parent and child processes + * when connecting socket has different domain than the process using it. + */ +TEST_F(outside_socket, socket_with_different_domain) +{ + pid_t child; + int err, status; + int pipe_child[2], pipe_parent[2]; + char buf_parent; + int server_socket; + + 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 client_socket; + char buf_child; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + + /* Client always has a domain. */ + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + if (variant->child_socket) { + int data_socket, passed_socket, stream_server; + + passed_socket = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, passed_socket); + stream_server = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_server); + ASSERT_EQ(0, bind(stream_server, + &self->transit_address.unix_addr, + self->transit_address.unix_addr_len)); + ASSERT_EQ(0, listen(stream_server, backlog)); + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + data_socket = accept(stream_server, NULL, NULL); + ASSERT_LE(0, data_socket); + ASSERT_EQ(0, send_fd(data_socket, passed_socket)); + EXPECT_EQ(0, close(passed_socket)); + EXPECT_EQ(0, close(stream_server)); + } + + client_socket = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, client_socket); + + /* Waits for parent signal for connection. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + err = connect(client_socket, &self->address.unix_addr, + self->address.unix_addr_len); + if (variant->child_socket) { + EXPECT_EQ(0, err); + } else { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } + EXPECT_EQ(0, close(client_socket)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_child[1])); + EXPECT_EQ(0, close(pipe_parent[0])); + + if (variant->child_socket) { + int client_child = socket(AF_UNIX, SOCK_STREAM, 0); + + ASSERT_LE(0, client_child); + ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + ASSERT_EQ(0, connect(client_child, + &self->transit_address.unix_addr, + self->transit_address.unix_addr_len)); + server_socket = recv_fd(client_child); + EXPECT_EQ(0, close(client_child)); + } else { + server_socket = socket(AF_UNIX, variant->type, 0); + } + ASSERT_LE(0, server_socket); + + /* Server always has a domain. */ + create_scoped_domain(_metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + ASSERT_EQ(0, bind(server_socket, &self->address.unix_addr, + self->address.unix_addr_len)); + if (variant->type == SOCK_STREAM) + ASSERT_EQ(0, listen(server_socket, backlog)); + + /* Signals to child that the parent is listening. */ + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(server_socket)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +static const char stream_path[] = TMP_DIR "/stream.sock"; +static const char dgram_path[] = TMP_DIR "/dgram.sock"; + +/* clang-format off */ +FIXTURE(various_address_sockets) {}; +/* clang-format on */ + +FIXTURE_VARIANT(various_address_sockets) +{ + const int domain; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_scoped_domain) { + /* clang-format on */ + .domain = SCOPE_SANDBOX, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_other_domain) { + /* clang-format on */ + .domain = OTHER_SANDBOX, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(various_address_sockets, pathname_socket_no_domain) { + /* clang-format on */ + .domain = NO_SANDBOX, +}; + +FIXTURE_SETUP(various_address_sockets) +{ + drop_caps(_metadata); + + umask(0077); + ASSERT_EQ(0, mkdir(TMP_DIR, 0700)); +} + +FIXTURE_TEARDOWN(various_address_sockets) +{ + EXPECT_EQ(0, unlink(stream_path)); + EXPECT_EQ(0, unlink(dgram_path)); + EXPECT_EQ(0, rmdir(TMP_DIR)); +} + +TEST_F(various_address_sockets, scoped_pathname_sockets) +{ + pid_t child; + int status; + char buf_child, buf_parent; + int pipe_parent[2]; + int unnamed_sockets[2]; + int stream_pathname_socket, dgram_pathname_socket, + stream_abstract_socket, dgram_abstract_socket, data_socket; + struct service_fixture stream_abstract_addr, dgram_abstract_addr; + struct sockaddr_un stream_pathname_addr = { + .sun_family = AF_UNIX, + }; + struct sockaddr_un dgram_pathname_addr = { + .sun_family = AF_UNIX, + }; + + /* Pathname address. */ + snprintf(stream_pathname_addr.sun_path, + sizeof(stream_pathname_addr.sun_path), "%s", stream_path); + snprintf(dgram_pathname_addr.sun_path, + sizeof(dgram_pathname_addr.sun_path), "%s", dgram_path); + + /* Abstract address. */ + memset(&stream_abstract_addr, 0, sizeof(stream_abstract_addr)); + set_unix_address(&stream_abstract_addr, 0); + memset(&dgram_abstract_addr, 0, sizeof(dgram_abstract_addr)); + set_unix_address(&dgram_abstract_addr, 1); + + /* Unnamed address for datagram socket. */ + ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM, 0, unnamed_sockets)); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int err; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(unnamed_sockets[1])); + + if (variant->domain == SCOPE_SANDBOX) + create_scoped_domain( + _metadata, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + else if (variant->domain == OTHER_SANDBOX) + create_fs_domain(_metadata); + + /* Waits for parent to listen. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + EXPECT_EQ(0, close(pipe_parent[0])); + + /* Checks that we can send data through a datagram socket. */ + ASSERT_EQ(1, write(unnamed_sockets[0], "a", 1)); + EXPECT_EQ(0, close(unnamed_sockets[0])); + + /* Connects with pathname sockets. */ + stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_pathname_socket); + ASSERT_EQ(0, + connect(stream_pathname_socket, &stream_pathname_addr, + sizeof(stream_pathname_addr))); + ASSERT_EQ(1, write(stream_pathname_socket, "b", 1)); + EXPECT_EQ(0, close(stream_pathname_socket)); + + /* Sends without connection. */ + dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_pathname_socket); + err = sendto(dgram_pathname_socket, "c", 1, 0, + &dgram_pathname_addr, sizeof(dgram_pathname_addr)); + EXPECT_EQ(1, err); + + /* Sends with connection. */ + ASSERT_EQ(0, + connect(dgram_pathname_socket, &dgram_pathname_addr, + sizeof(dgram_pathname_addr))); + ASSERT_EQ(1, write(dgram_pathname_socket, "d", 1)); + EXPECT_EQ(0, close(dgram_pathname_socket)); + + /* Connects with abstract sockets. */ + stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_abstract_socket); + err = connect(stream_abstract_socket, + &stream_abstract_addr.unix_addr, + stream_abstract_addr.unix_addr_len); + if (variant->domain == SCOPE_SANDBOX) { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } else { + EXPECT_EQ(0, err); + ASSERT_EQ(1, write(stream_abstract_socket, "e", 1)); + } + EXPECT_EQ(0, close(stream_abstract_socket)); + + /* Sends without connection. */ + dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_abstract_socket); + err = sendto(dgram_abstract_socket, "f", 1, 0, + &dgram_abstract_addr.unix_addr, + dgram_abstract_addr.unix_addr_len); + if (variant->domain == SCOPE_SANDBOX) { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } else { + EXPECT_EQ(1, err); + } + + /* Sends with connection. */ + err = connect(dgram_abstract_socket, + &dgram_abstract_addr.unix_addr, + dgram_abstract_addr.unix_addr_len); + if (variant->domain == SCOPE_SANDBOX) { + EXPECT_EQ(-1, err); + EXPECT_EQ(EPERM, errno); + } else { + EXPECT_EQ(0, err); + ASSERT_EQ(1, write(dgram_abstract_socket, "g", 1)); + } + EXPECT_EQ(0, close(dgram_abstract_socket)); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + EXPECT_EQ(0, close(unnamed_sockets[0])); + + /* Sets up pathname servers. */ + stream_pathname_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_pathname_socket); + ASSERT_EQ(0, bind(stream_pathname_socket, &stream_pathname_addr, + sizeof(stream_pathname_addr))); + ASSERT_EQ(0, listen(stream_pathname_socket, backlog)); + + dgram_pathname_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_pathname_socket); + ASSERT_EQ(0, bind(dgram_pathname_socket, &dgram_pathname_addr, + sizeof(dgram_pathname_addr))); + + /* Sets up abstract servers. */ + stream_abstract_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, stream_abstract_socket); + ASSERT_EQ(0, + bind(stream_abstract_socket, &stream_abstract_addr.unix_addr, + stream_abstract_addr.unix_addr_len)); + + dgram_abstract_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, dgram_abstract_socket); + ASSERT_EQ(0, bind(dgram_abstract_socket, &dgram_abstract_addr.unix_addr, + dgram_abstract_addr.unix_addr_len)); + ASSERT_EQ(0, listen(stream_abstract_socket, backlog)); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(pipe_parent[1])); + + /* Reads from unnamed socket. */ + ASSERT_EQ(1, read(unnamed_sockets[1], &buf_parent, sizeof(buf_parent))); + ASSERT_EQ('a', buf_parent); + EXPECT_LE(0, close(unnamed_sockets[1])); + + /* Reads from pathname sockets. */ + data_socket = accept(stream_pathname_socket, NULL, NULL); + ASSERT_LE(0, data_socket); + ASSERT_EQ(1, read(data_socket, &buf_parent, sizeof(buf_parent))); + ASSERT_EQ('b', buf_parent); + EXPECT_EQ(0, close(data_socket)); + EXPECT_EQ(0, close(stream_pathname_socket)); + + ASSERT_EQ(1, + read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent))); + ASSERT_EQ('c', buf_parent); + ASSERT_EQ(1, + read(dgram_pathname_socket, &buf_parent, sizeof(buf_parent))); + ASSERT_EQ('d', buf_parent); + EXPECT_EQ(0, close(dgram_pathname_socket)); + + if (variant->domain != SCOPE_SANDBOX) { + /* Reads from abstract sockets if allowed to send. */ + data_socket = accept(stream_abstract_socket, NULL, NULL); + ASSERT_LE(0, data_socket); + ASSERT_EQ(1, + read(data_socket, &buf_parent, sizeof(buf_parent))); + ASSERT_EQ('e', buf_parent); + EXPECT_EQ(0, close(data_socket)); + + ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent, + sizeof(buf_parent))); + ASSERT_EQ('f', buf_parent); + ASSERT_EQ(1, read(dgram_abstract_socket, &buf_parent, + sizeof(buf_parent))); + ASSERT_EQ('g', buf_parent); + } + + /* Waits for all abstract socket tests. */ + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(stream_abstract_socket)); + EXPECT_EQ(0, close(dgram_abstract_socket)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +TEST(datagram_sockets) +{ + struct service_fixture connected_addr, non_connected_addr; + int server_conn_socket, server_unconn_socket; + int pipe_parent[2], pipe_child[2]; + int status; + char buf; + pid_t child; + + drop_caps(_metadata); + memset(&connected_addr, 0, sizeof(connected_addr)); + set_unix_address(&connected_addr, 0); + memset(&non_connected_addr, 0, sizeof(non_connected_addr)); + set_unix_address(&non_connected_addr, 1); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int client_conn_socket, client_unconn_socket; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + + client_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + client_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, client_conn_socket); + ASSERT_LE(0, client_unconn_socket); + + /* Waits for parent to listen. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf, 1)); + ASSERT_EQ(0, + connect(client_conn_socket, &connected_addr.unix_addr, + connected_addr.unix_addr_len)); + + /* + * Both connected and non-connected sockets can send data when + * the domain is not scoped. + */ + ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0)); + ASSERT_EQ(1, sendto(client_unconn_socket, ".", 1, 0, + &non_connected_addr.unix_addr, + non_connected_addr.unix_addr_len)); + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + /* Scopes the domain. */ + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* + * Connected socket sends data to the receiver, but the + * non-connected socket must fail to send data. + */ + ASSERT_EQ(1, send(client_conn_socket, ".", 1, 0)); + ASSERT_EQ(-1, sendto(client_unconn_socket, ".", 1, 0, + &non_connected_addr.unix_addr, + non_connected_addr.unix_addr_len)); + ASSERT_EQ(EPERM, errno); + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + + EXPECT_EQ(0, close(client_conn_socket)); + EXPECT_EQ(0, close(client_unconn_socket)); + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + EXPECT_EQ(0, close(pipe_child[1])); + + server_conn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + server_unconn_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, server_conn_socket); + ASSERT_LE(0, server_unconn_socket); + + ASSERT_EQ(0, bind(server_conn_socket, &connected_addr.unix_addr, + connected_addr.unix_addr_len)); + ASSERT_EQ(0, bind(server_unconn_socket, &non_connected_addr.unix_addr, + non_connected_addr.unix_addr_len)); + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* Waits for child to test. */ + ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); + ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0)); + ASSERT_EQ(1, recv(server_unconn_socket, &buf, 1, 0)); + + /* + * Connected datagram socket will receive data, but + * non-connected datagram socket does not receive data. + */ + ASSERT_EQ(1, read(pipe_child[0], &buf, 1)); + ASSERT_EQ(1, recv(server_conn_socket, &buf, 1, 0)); + + /* Waits for all tests to finish. */ + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(server_conn_socket)); + EXPECT_EQ(0, close(server_unconn_socket)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +TEST(self_connect) +{ + struct service_fixture connected_addr, non_connected_addr; + int connected_socket, non_connected_socket, status; + pid_t child; + + drop_caps(_metadata); + memset(&connected_addr, 0, sizeof(connected_addr)); + set_unix_address(&connected_addr, 0); + memset(&non_connected_addr, 0, sizeof(non_connected_addr)); + set_unix_address(&non_connected_addr, 1); + + connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + non_connected_socket = socket(AF_UNIX, SOCK_DGRAM, 0); + ASSERT_LE(0, connected_socket); + ASSERT_LE(0, non_connected_socket); + + ASSERT_EQ(0, bind(connected_socket, &connected_addr.unix_addr, + connected_addr.unix_addr_len)); + ASSERT_EQ(0, bind(non_connected_socket, &non_connected_addr.unix_addr, + non_connected_addr.unix_addr_len)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + /* Child's domain is scoped. */ + create_scoped_domain(_metadata, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + + /* + * The child inherits the sockets, and cannot connect or + * send data to them. + */ + ASSERT_EQ(-1, + connect(connected_socket, &connected_addr.unix_addr, + connected_addr.unix_addr_len)); + ASSERT_EQ(EPERM, errno); + + ASSERT_EQ(-1, sendto(connected_socket, ".", 1, 0, + &connected_addr.unix_addr, + connected_addr.unix_addr_len)); + ASSERT_EQ(EPERM, errno); + + ASSERT_EQ(-1, sendto(non_connected_socket, ".", 1, 0, + &non_connected_addr.unix_addr, + non_connected_addr.unix_addr_len)); + ASSERT_EQ(EPERM, errno); + + EXPECT_EQ(0, close(connected_socket)); + EXPECT_EQ(0, close(non_connected_socket)); + _exit(_metadata->exit_code); + return; + } + + /* Waits for all tests to finish. */ + ASSERT_EQ(child, waitpid(child, &status, 0)); + EXPECT_EQ(0, close(connected_socket)); + EXPECT_EQ(0, close(non_connected_socket)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/scoped_base_variants.h b/tools/testing/selftests/landlock/scoped_base_variants.h new file mode 100644 index 000000000..7116728eb --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_base_variants.h @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock scoped_domains test variant definition. + * + * This file defines a fixture variant "scoped_domains" that has all + * permutations of parent/child process being in separate or shared + * Landlock domain, or not being in a Landlock domain at all. + * + * Scoped access tests can include this file to avoid repeating these + * combinations. + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +/* clang-format on */ +FIXTURE_VARIANT(scoped_domains) +{ + bool domain_both; + bool domain_parent; + bool domain_child; +}; + +/* + * No domain + * + * P1-. P1 -> P2 : allow + * \ P2 -> P1 : allow + * 'P2 + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, without_domain) { + /* clang-format on */ + .domain_both = false, + .domain_parent = false, + .domain_child = false, +}; + +/* + * Child domain + * + * P1--. P1 -> P2 : allow + * \ P2 -> P1 : deny + * .'-----. + * | P2 | + * '------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, child_domain) { + /* clang-format on */ + .domain_both = false, + .domain_parent = false, + .domain_child = true, +}; + +/* + * Parent domain + * .------. + * | P1 --. P1 -> P2 : deny + * '------' \ P2 -> P1 : allow + * ' + * P2 + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, parent_domain) { + /* clang-format on */ + .domain_both = false, + .domain_parent = true, + .domain_child = false, +}; + +/* + * Parent + child domain (siblings) + * .------. + * | P1 ---. P1 -> P2 : deny + * '------' \ P2 -> P1 : deny + * .---'--. + * | P2 | + * '------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, sibling_domain) { + /* clang-format on */ + .domain_both = false, + .domain_parent = true, + .domain_child = true, +}; + +/* + * Same domain (inherited) + * .-------------. + * | P1----. | P1 -> P2 : allow + * | \ | P2 -> P1 : allow + * | ' | + * | P2 | + * '-------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, inherited_domain) { + /* clang-format on */ + .domain_both = true, + .domain_parent = false, + .domain_child = false, +}; + +/* + * Inherited + child domain + * .-----------------. + * | P1----. | P1 -> P2 : allow + * | \ | P2 -> P1 : deny + * | .-'----. | + * | | P2 | | + * | '------' | + * '-----------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, nested_domain) { + /* clang-format on */ + .domain_both = true, + .domain_parent = false, + .domain_child = true, +}; + +/* + * Inherited + parent domain + * .-----------------. + * |.------. | P1 -> P2 : deny + * || P1 ----. | P2 -> P1 : allow + * |'------' \ | + * | ' | + * | P2 | + * '-----------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, nested_and_parent_domain) { + /* clang-format on */ + .domain_both = true, + .domain_parent = true, + .domain_child = false, +}; + +/* + * Inherited + parent and child domain (siblings) + * .-----------------. + * | .------. | P1 -> P2 : deny + * | | P1 . | P2 -> P1 : deny + * | '------'\ | + * | \ | + * | .--'---. | + * | | P2 | | + * | '------' | + * '-----------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_domains, forked_domains) { + /* clang-format on */ + .domain_both = true, + .domain_parent = true, + .domain_child = true, +}; diff --git a/tools/testing/selftests/landlock/scoped_common.h b/tools/testing/selftests/landlock/scoped_common.h new file mode 100644 index 000000000..a9a912d30 --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_common.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock scope test helpers + * + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +#define _GNU_SOURCE + +#include <sys/types.h> + +static void create_scoped_domain(struct __test_metadata *const _metadata, + const __u16 scope) +{ + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = scope, + }; + + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + ASSERT_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)); +} diff --git a/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h new file mode 100644 index 000000000..bcd9a8380 --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_multiple_domain_variants.h @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Landlock variants for three processes with various domains. + * + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +enum sandbox_type { + NO_SANDBOX, + SCOPE_SANDBOX, + /* Any other type of sandboxing domain */ + OTHER_SANDBOX, +}; + +/* clang-format on */ +FIXTURE_VARIANT(scoped_vs_unscoped) +{ + const int domain_all; + const int domain_parent; + const int domain_children; + const int domain_child; + const int domain_grand_child; +}; + +/* + * .-----------------. + * | ####### | P3 -> P2 : allow + * | P1----# P2 # | P3 -> P1 : deny + * | # | # | + * | # P3 # | + * | ####### | + * '-----------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_scoped) { + .domain_all = OTHER_SANDBOX, + .domain_parent = NO_SANDBOX, + .domain_children = SCOPE_SANDBOX, + .domain_child = NO_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * ################### + * # ####### # P3 -> P2 : allow + * # P1----# P2 # # P3 -> P1 : deny + * # # | # # + * # # P3 # # + * # ####### # + * ################### + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, all_scoped) { + .domain_all = SCOPE_SANDBOX, + .domain_parent = NO_SANDBOX, + .domain_children = SCOPE_SANDBOX, + .domain_child = NO_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * .-----------------. + * | .-----. | P3 -> P2 : allow + * | P1----| P2 | | P3 -> P1 : allow + * | | | | + * | | P3 | | + * | '-----' | + * '-----------------' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_other_domain) { + .domain_all = OTHER_SANDBOX, + .domain_parent = NO_SANDBOX, + .domain_children = OTHER_SANDBOX, + .domain_child = NO_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * .----. ###### P3 -> P2 : allow + * | P1 |----# P2 # P3 -> P1 : allow + * '----' ###### + * | + * P3 + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_one_domain) { + .domain_all = NO_SANDBOX, + .domain_parent = OTHER_SANDBOX, + .domain_children = NO_SANDBOX, + .domain_child = SCOPE_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * ###### .-----. P3 -> P2 : allow + * # P1 #----| P2 | P3 -> P1 : allow + * ###### '-----' + * | + * P3 + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_grand_parent_scoped) { + .domain_all = NO_SANDBOX, + .domain_parent = SCOPE_SANDBOX, + .domain_children = NO_SANDBOX, + .domain_child = OTHER_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * ###### ###### P3 -> P2 : allow + * # P1 #----# P2 # P3 -> P1 : allow + * ###### ###### + * | + * .----. + * | P3 | + * '----' + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, allow_with_parents_domain) { + .domain_all = NO_SANDBOX, + .domain_parent = SCOPE_SANDBOX, + .domain_children = NO_SANDBOX, + .domain_child = SCOPE_SANDBOX, + .domain_grand_child = NO_SANDBOX, + /* clang-format on */ +}; + +/* + * ###### P3 -> P2 : deny + * # P1 #----P2 P3 -> P1 : deny + * ###### | + * | + * ###### + * # P3 # + * ###### + */ +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoped_vs_unscoped, deny_with_self_and_grandparent_domain) { + .domain_all = NO_SANDBOX, + .domain_parent = SCOPE_SANDBOX, + .domain_children = NO_SANDBOX, + .domain_child = NO_SANDBOX, + .domain_grand_child = SCOPE_SANDBOX, + /* clang-format on */ +}; diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c new file mode 100644 index 000000000..f24f2c28f --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_signal_test.c @@ -0,0 +1,744 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Signal Scoping + * + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <linux/landlock.h> +#include <pthread.h> +#include <signal.h> +#include <sys/prctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "common.h" +#include "scoped_common.h" + +/* This variable is used for handling several signals. */ +static volatile sig_atomic_t is_signaled; + +/* clang-format off */ +FIXTURE(scoping_signals) {}; +/* clang-format on */ + +FIXTURE_VARIANT(scoping_signals) +{ + int sig; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoping_signals, sigtrap) { + /* clang-format on */ + .sig = SIGTRAP, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoping_signals, sigurg) { + /* clang-format on */ + .sig = SIGURG, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoping_signals, sighup) { + /* clang-format on */ + .sig = SIGHUP, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(scoping_signals, sigtstp) { + /* clang-format on */ + .sig = SIGTSTP, +}; + +FIXTURE_SETUP(scoping_signals) +{ + drop_caps(_metadata); + + is_signaled = 0; +} + +FIXTURE_TEARDOWN(scoping_signals) +{ +} + +static void scope_signal_handler(int sig, siginfo_t *info, void *ucontext) +{ + if (sig == SIGTRAP || sig == SIGURG || sig == SIGHUP || sig == SIGTSTP) + is_signaled = 1; +} + +/* + * In this test, a child process sends a signal to parent before and + * after getting scoped. + */ +TEST_F(scoping_signals, send_sig_to_parent) +{ + int pipe_parent[2]; + int status; + pid_t child; + pid_t parent = getpid(); + struct sigaction action = { + .sa_sigaction = scope_signal_handler, + .sa_flags = SA_SIGINFO, + + }; + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + ASSERT_LE(0, sigaction(variant->sig, &action, NULL)); + + /* The process should not have already been signaled. */ + EXPECT_EQ(0, is_signaled); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char buf_child; + int err; + + EXPECT_EQ(0, close(pipe_parent[1])); + + /* + * The child process can send signal to parent when + * domain is not scoped. + */ + err = kill(parent, variant->sig); + ASSERT_EQ(0, err); + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + EXPECT_EQ(0, close(pipe_parent[0])); + + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + /* + * The child process cannot send signal to the parent + * anymore. + */ + err = kill(parent, variant->sig); + ASSERT_EQ(-1, err); + ASSERT_EQ(EPERM, errno); + + /* + * No matter of the domain, a process should be able to + * send a signal to itself. + */ + ASSERT_EQ(0, is_signaled); + ASSERT_EQ(0, raise(variant->sig)); + ASSERT_EQ(1, is_signaled); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + + /* Waits for a first signal to be received, without race condition. */ + while (!is_signaled && !usleep(1)) + ; + ASSERT_EQ(1, is_signaled); + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(pipe_parent[1])); + is_signaled = 0; + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; + + EXPECT_EQ(0, is_signaled); +} + +/* clang-format off */ +FIXTURE(scoped_domains) {}; +/* clang-format on */ + +#include "scoped_base_variants.h" + +FIXTURE_SETUP(scoped_domains) +{ + drop_caps(_metadata); +} + +FIXTURE_TEARDOWN(scoped_domains) +{ +} + +/* + * This test ensures that a scoped process cannot send signal out of + * scoped domain. + */ +TEST_F(scoped_domains, check_access_signal) +{ + pid_t child; + pid_t parent = getpid(); + int status; + bool can_signal_child, can_signal_parent; + int pipe_parent[2], pipe_child[2]; + char buf_parent; + int err; + + can_signal_parent = !variant->domain_child; + can_signal_child = !variant->domain_parent; + + if (variant->domain_both) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + char buf_child; + + EXPECT_EQ(0, close(pipe_child[0])); + EXPECT_EQ(0, close(pipe_parent[1])); + + if (variant->domain_child) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + EXPECT_EQ(0, close(pipe_child[1])); + + /* Waits for the parent to send signals. */ + ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); + EXPECT_EQ(0, close(pipe_parent[0])); + + err = kill(parent, 0); + if (can_signal_parent) { + ASSERT_EQ(0, err); + } else { + ASSERT_EQ(-1, err); + ASSERT_EQ(EPERM, errno); + } + /* + * No matter of the domain, a process should be able to + * send a signal to itself. + */ + ASSERT_EQ(0, raise(0)); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + EXPECT_EQ(0, close(pipe_child[1])); + + if (variant->domain_parent) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); + EXPECT_EQ(0, close(pipe_child[0])); + + err = kill(child, 0); + if (can_signal_child) { + ASSERT_EQ(0, err); + } else { + ASSERT_EQ(-1, err); + ASSERT_EQ(EPERM, errno); + } + ASSERT_EQ(0, raise(0)); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(pipe_parent[1])); + ASSERT_EQ(child, waitpid(child, &status, 0)); + + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +enum thread_return { + THREAD_INVALID = 0, + THREAD_SUCCESS = 1, + THREAD_ERROR = 2, + THREAD_TEST_FAILED = 3, +}; + +static void *thread_sync(void *arg) +{ + const int pipe_read = *(int *)arg; + char buf; + + if (read(pipe_read, &buf, 1) != 1) + return (void *)THREAD_ERROR; + + return (void *)THREAD_SUCCESS; +} + +TEST(signal_scoping_thread_before) +{ + 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_sync, + &thread_pipe[0])); + + /* Enforces restriction after creating the thread. */ + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + 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); + + EXPECT_EQ(0, close(thread_pipe[0])); + 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; + +static void handle_sigurg(int sig) +{ + if (sig == SIGURG) + signal_received = 1; + else + signal_received = -1; +} + +static int setup_signal_handler(int signal) +{ + struct sigaction sa = { + .sa_handler = handle_sigurg, + }; + + if (sigemptyset(&sa.sa_mask)) + return -1; + + sa.sa_flags = SA_SIGINFO | SA_RESTART; + return sigaction(SIGURG, &sa, NULL); +} + +/* clang-format off */ +FIXTURE(fown) {}; +/* clang-format on */ + +enum fown_sandbox { + SANDBOX_NONE, + SANDBOX_BEFORE_FORK, + SANDBOX_BEFORE_SETOWN, + SANDBOX_AFTER_SETOWN, +}; + +FIXTURE_VARIANT(fown) +{ + const enum fown_sandbox sandbox_setown; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(fown, no_sandbox) { + /* clang-format on */ + .sandbox_setown = SANDBOX_NONE, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(fown, sandbox_before_fork) { + /* clang-format on */ + .sandbox_setown = SANDBOX_BEFORE_FORK, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(fown, sandbox_before_setown) { + /* clang-format on */ + .sandbox_setown = SANDBOX_BEFORE_SETOWN, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(fown, sandbox_after_setown) { + /* clang-format on */ + .sandbox_setown = SANDBOX_AFTER_SETOWN, +}; + +FIXTURE_SETUP(fown) +{ + drop_caps(_metadata); +} + +FIXTURE_TEARDOWN(fown) +{ +} + +/* + * Sending an out of bound message will trigger the SIGURG signal + * through file_send_sigiotask. + */ +TEST_F(fown, sigurg_socket) +{ + int server_socket, recv_socket; + struct service_fixture server_address; + char buffer_parent; + int status; + int pipe_parent[2], pipe_child[2]; + pid_t child; + + memset(&server_address, 0, sizeof(server_address)); + set_unix_address(&server_address, 0); + + ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); + + if (variant->sandbox_setown == SANDBOX_BEFORE_FORK) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + int client_socket; + char buffer_child; + + EXPECT_EQ(0, close(pipe_parent[1])); + EXPECT_EQ(0, close(pipe_child[0])); + + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + client_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, client_socket); + + /* Waits for the parent to listen. */ + ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); + ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr, + server_address.unix_addr_len)); + + /* + * Waits for the parent to accept the connection, sandbox + * itself, and call fcntl(2). + */ + ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); + /* May signal itself. */ + ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB)); + EXPECT_EQ(0, close(client_socket)); + ASSERT_EQ(1, write(pipe_child[1], ".", 1)); + EXPECT_EQ(0, close(pipe_child[1])); + + /* Waits for the message to be received. */ + ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); + EXPECT_EQ(0, close(pipe_parent[0])); + + if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) { + ASSERT_EQ(0, signal_received); + } else { + /* + * A signal is only received if fcntl(F_SETOWN) was + * called before any sandboxing or if the signal + * receiver is in the same domain. + */ + ASSERT_EQ(1, signal_received); + } + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(pipe_parent[0])); + EXPECT_EQ(0, close(pipe_child[1])); + + server_socket = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_LE(0, server_socket); + ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr, + server_address.unix_addr_len)); + ASSERT_EQ(0, listen(server_socket, backlog)); + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + recv_socket = accept(server_socket, NULL, NULL); + ASSERT_LE(0, recv_socket); + + if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + /* + * Sets the child to receive SIGURG for MSG_OOB. This uncommon use is + * a valid attack scenario which also simplifies this test. + */ + ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child)); + + if (variant->sandbox_setown == SANDBOX_AFTER_SETOWN) + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + + /* Waits for the child to send MSG_OOB. */ + ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1)); + EXPECT_EQ(0, close(pipe_child[0])); + ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB)); + EXPECT_EQ(0, close(recv_socket)); + EXPECT_EQ(0, close(server_socket)); + ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); + EXPECT_EQ(0, close(pipe_parent[1])); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +/* + * Checks that LANDLOCK_SCOPE_SIGNAL is enforced on the asynchronous SIGIO + * delivery path (fcntl(F_SETOWN)) when the file owner is a process group. + * + * A sandboxed process sitting at the head of its process group's PID hlist (the + * default position right after fork()) used to escape the fcntl(F_SETOWN, + * -pgrp) domain recording: pid_task(pgrp, PIDTYPE_PGID) resolved to the process + * itself, so the same-thread-group exemption skipped recording its Landlock + * domain. At SIGIO time that domain was then unset and the signal fanned out + * to every group member, including non-sandboxed processes outside the domain. + */ +TEST(sigio_to_pgid_members) +{ + int trigger[2], sync_child[2]; + char buf; + pid_t child; + int status, i; + + drop_caps(_metadata); + + /* + * Isolates the test in its own process group so the SIGIO fan-out stays + * bounded to this parent and the child forked below. + */ + ASSERT_EQ(0, setpgid(0, 0)); + + /* The non-sandboxed parent is the protected (out-of-domain) target. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); + ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC)); + + child = fork(); + ASSERT_LE(0, child); + if (child == 0) { + /* + * The child inherits the parent's new process group and, just + * attached with hlist_add_head_rcu(), is now the head of the + * pgid hlist: this is the case that used to skip the recording. + */ + EXPECT_EQ(0, close(sync_child[0])); + + /* In-domain positive control: the child must be signaled. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); + + /* Owns the SIGIO source for the whole process group. */ + ASSERT_EQ(0, fcntl(trigger[0], F_SETSIG, SIGURG)); + ASSERT_EQ(0, fcntl(trigger[0], F_SETOWN, -getpgrp())); + ASSERT_EQ(0, fcntl(trigger[0], F_SETFL, O_ASYNC)); + + /* Fans SIGURG out to every member of the process group. */ + ASSERT_EQ(1, write(trigger[1], ".", 1)); + + /* + * The sandboxed child is in its own domain and must always be + * signaled: this proves the SIGIO actually fired. + */ + for (i = 0; i < 1000 && !signal_received; i++) + usleep(1000); + EXPECT_EQ(1, signal_received); + + ASSERT_EQ(1, write(sync_child[1], ".", 1)); + EXPECT_EQ(0, close(sync_child[1])); + + _exit(_metadata->exit_code); + return; + } + EXPECT_EQ(0, close(sync_child[1])); + EXPECT_EQ(0, close(trigger[0])); + EXPECT_EQ(0, close(trigger[1])); + + /* Waits for the child to generate the SIGIO. */ + ASSERT_EQ(1, read(sync_child[0], &buf, 1)); + EXPECT_EQ(0, close(sync_child[0])); + + /* Lets a delivered-but-pending signal run our handler, if any. */ + for (i = 0; i < 100 && !signal_received; i++) + usleep(1000); + + /* + * SCOPE_SIGNAL must block the fan-out to this non-sandboxed parent, + * which is outside the child's Landlock domain. Before the fix the + * parent was signaled here. + */ + EXPECT_EQ(0, signal_received); + + ASSERT_EQ(child, waitpid(child, &status, 0)); + if (WIFSIGNALED(status) || !WIFEXITED(status) || + WEXITSTATUS(status) != EXIT_SUCCESS) + _metadata->exit_code = KSFT_FAIL; +} + +static void *thread_setown_scoped(void *arg) +{ + const int fd = *(int *)arg; + int ruleset_fd; + const struct landlock_ruleset_attr ruleset_attr = { + .scoped = LANDLOCK_SCOPE_SIGNAL, + }; + + /* Sandboxes only this non-leader thread (no thread syncing). */ + ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) + return (void *)THREAD_ERROR; + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || + landlock_restrict_self(ruleset_fd, 0)) { + close(ruleset_fd); + return (void *)THREAD_ERROR; + } + close(ruleset_fd); + + /* Makes this process group own the SIGIO source. */ + if (fcntl(fd, F_SETSIG, SIGURG) || fcntl(fd, F_SETOWN, -getpgrp()) || + fcntl(fd, F_SETFL, O_ASYNC)) + return (void *)THREAD_ERROR; + + return (void *)THREAD_SUCCESS; +} + +/* + * Checks that the SIGIO fan-out is still delivered to the file owner's own + * process when fcntl(F_SETOWN, -pgrp) was issued from a sandboxed non-leader + * thread. + * + * The Landlock domain is recorded for a process-group owner (so out-of-domain + * members stay blocked, see sigio_to_pgid_members), but the kernel signals a + * process group through its members' thread-group leaders. Here the leader is + * not sandboxed and thus has a different domain than the registering thread, so + * the registration-time check cannot tell that it belongs to the owner's own + * process. hook_file_send_sigiotask() must recognize it through the recorded + * thread group and allow the delivery, matching the same-process guarantee of + * commit 18eb75f3af40. Without that exemption the leader is wrongly denied and + * never signaled. + */ +TEST(sigio_to_pgid_self) +{ + int trigger[2]; + pthread_t thread; + enum thread_return ret = THREAD_INVALID; + int i; + + drop_caps(_metadata); + + /* Bounds the SIGIO fan-out to this process. */ + ASSERT_EQ(0, setpgid(0, 0)); + + /* The non-sandboxed thread-group leader is the SIGIO target. */ + ASSERT_EQ(0, setup_signal_handler(SIGURG)); + signal_received = 0; + + ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); + + /* + * Registers the process-group fowner from a sibling thread that + * sandboxes only itself, so its domain differs from the leader's. + */ + ASSERT_EQ(0, pthread_create(&thread, NULL, thread_setown_scoped, + &trigger[0])); + ASSERT_EQ(0, pthread_join(thread, (void **)&ret)); + ASSERT_EQ(THREAD_SUCCESS, ret); + + /* Fans SIGURG out to the process group. */ + ASSERT_EQ(1, write(trigger[1], ".", 1)); + + for (i = 0; i < 1000 && !signal_received; i++) + usleep(1000); + + /* + * Same-process delivery must always be allowed, even though the owner + * was registered from a sandboxed sibling thread. + */ + EXPECT_EQ(1, signal_received); + + EXPECT_EQ(0, close(trigger[0])); + EXPECT_EQ(0, close(trigger[1])); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/scoped_test.c b/tools/testing/selftests/landlock/scoped_test.c new file mode 100644 index 000000000..b90f76ed0 --- /dev/null +++ b/tools/testing/selftests/landlock/scoped_test.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Common scope restriction + * + * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <linux/landlock.h> +#include <sys/prctl.h> + +#include "common.h" + +#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL + +TEST(ruleset_with_unknown_scope) +{ + __u64 scoped_mask; + + for (scoped_mask = 1ULL << 63; scoped_mask != ACCESS_LAST; + scoped_mask >>= 1) { + struct landlock_ruleset_attr ruleset_attr = { + .scoped = scoped_mask, + }; + + ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, + sizeof(ruleset_attr), 0)); + ASSERT_EQ(EINVAL, errno); + } +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/landlock/true.c b/tools/testing/selftests/landlock/true.c new file mode 100644 index 000000000..3f9ccbf52 --- /dev/null +++ b/tools/testing/selftests/landlock/true.c @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0 +int main(void) +{ + return 0; +} diff --git a/tools/testing/selftests/landlock/tsync_test.c b/tools/testing/selftests/landlock/tsync_test.c new file mode 100644 index 000000000..9cf1491bb --- /dev/null +++ b/tools/testing/selftests/landlock/tsync_test.c @@ -0,0 +1,327 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Landlock tests - Enforcing the same restrictions across multiple threads + * + * Copyright © 2025 Günther Noack <gnoack3000@gmail.com> + */ + +#define _GNU_SOURCE +#include <linux/landlock.h> +#include <pthread.h> +#include <signal.h> +#include <sys/prctl.h> + +#include "common.h" + +/* create_ruleset - Create a simple ruleset FD common to all tests */ +static int create_ruleset(struct __test_metadata *const _metadata) +{ + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = (LANDLOCK_ACCESS_FS_WRITE_FILE | + LANDLOCK_ACCESS_FS_TRUNCATE), + }; + const int ruleset_fd = + landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); + + ASSERT_LE(0, ruleset_fd) + { + TH_LOG("landlock_create_ruleset: %s", strerror(errno)); + } + return ruleset_fd; +} + +TEST(single_threaded_success) +{ + const int ruleset_fd = create_ruleset(_metadata); + + disable_caps(_metadata); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC)); + + EXPECT_EQ(0, close(ruleset_fd)); +} + +static void store_no_new_privs(void *data) +{ + bool *nnp = data; + + if (!nnp) + return; + *nnp = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0); +} + +static void *idle(void *data) +{ + pthread_cleanup_push(store_no_new_privs, data); + + while (true) + sleep(1); + + pthread_cleanup_pop(1); +} + +TEST(multi_threaded_success) +{ + pthread_t t1, t2; + bool no_new_privs1, no_new_privs2; + const int ruleset_fd = create_ruleset(_metadata); + + disable_caps(_metadata); + + ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1)); + ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2)); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC)); + + ASSERT_EQ(0, pthread_cancel(t1)); + ASSERT_EQ(0, pthread_cancel(t2)); + ASSERT_EQ(0, pthread_join(t1, NULL)); + ASSERT_EQ(0, pthread_join(t2, NULL)); + + /* The no_new_privs flag was implicitly enabled on all threads. */ + EXPECT_TRUE(no_new_privs1); + EXPECT_TRUE(no_new_privs2); + + EXPECT_EQ(0, close(ruleset_fd)); +} + +TEST(multi_threaded_success_despite_diverging_domains) +{ + pthread_t t1, t2; + const int ruleset_fd = create_ruleset(_metadata); + + disable_caps(_metadata); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + ASSERT_EQ(0, pthread_create(&t1, NULL, idle, NULL)); + ASSERT_EQ(0, pthread_create(&t2, NULL, idle, NULL)); + + /* + * The main thread enforces a ruleset, + * thereby bringing the threads' Landlock domains out of sync. + */ + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); + + /* Still, TSYNC succeeds, bringing the threads in sync again. */ + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC)); + + ASSERT_EQ(0, pthread_cancel(t1)); + ASSERT_EQ(0, pthread_cancel(t2)); + ASSERT_EQ(0, pthread_join(t1, NULL)); + ASSERT_EQ(0, pthread_join(t2, NULL)); + EXPECT_EQ(0, close(ruleset_fd)); +} + +struct thread_restrict_data { + pthread_t t; + int ruleset_fd; + int result; +}; + +static void *thread_restrict(void *data) +{ + struct thread_restrict_data *d = data; + + d->result = landlock_restrict_self(d->ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC); + return NULL; +} + +TEST(competing_enablement) +{ + const int ruleset_fd = create_ruleset(_metadata); + struct thread_restrict_data d[] = { + { .ruleset_fd = ruleset_fd }, + { .ruleset_fd = ruleset_fd }, + }; + + disable_caps(_metadata); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + ASSERT_EQ(0, pthread_create(&d[0].t, NULL, thread_restrict, &d[0])); + ASSERT_EQ(0, pthread_create(&d[1].t, NULL, thread_restrict, &d[1])); + + /* Wait for threads to finish. */ + ASSERT_EQ(0, pthread_join(d[0].t, NULL)); + ASSERT_EQ(0, pthread_join(d[1].t, NULL)); + + /* Expect that both succeeded. */ + EXPECT_EQ(0, d[0].result); + EXPECT_EQ(0, d[1].result); + + EXPECT_EQ(0, close(ruleset_fd)); +} + +static void signal_nop_handler(int sig) +{ +} + +struct signaler_data { + pthread_t target; + volatile bool stop; +}; + +static void *signaler_thread(void *data) +{ + struct signaler_data *sd = data; + + while (!sd->stop) + pthread_kill(sd->target, SIGUSR1); + + return NULL; +} + +/* + * Number of idle sibling threads. This must be large enough that even on + * machines with many cores, the sibling threads cannot all complete their + * credential preparation in a single parallel wave, otherwise the signaler + * thread has no window to interrupt wait_for_completion_interruptible(). + * 200 threads on a 64-core machine yields ~3 serialized waves, giving the + * tight signal loop enough time to land an interruption. + */ +#define NUM_IDLE_THREADS 200 + +/* + * Exercises the tsync interruption and cancellation paths in tsync.c. + * + * When a signal interrupts the calling thread while it waits for sibling + * threads to finish their credential preparation + * (wait_for_completion_interruptible in landlock_restrict_sibling_threads), + * the kernel sets ERESTARTNOINTR, cancels queued task works that have not + * started yet (cancel_tsync_works), then waits for the remaining works to + * finish. On the error return, syscalls.c aborts the prepared credentials. + * The kernel automatically restarts the syscall, so userspace sees success. + */ +TEST(tsync_interrupt) +{ + size_t i; + pthread_t threads[NUM_IDLE_THREADS]; + pthread_t signaler; + struct signaler_data sd; + struct sigaction sa = {}; + const int ruleset_fd = create_ruleset(_metadata); + + disable_caps(_metadata); + + /* Install a no-op SIGUSR1 handler so the signal does not kill us. */ + sa.sa_handler = signal_nop_handler; + sigemptyset(&sa.sa_mask); + ASSERT_EQ(0, sigaction(SIGUSR1, &sa, NULL)); + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + for (i = 0; i < NUM_IDLE_THREADS; i++) + ASSERT_EQ(0, pthread_create(&threads[i], NULL, idle, NULL)); + + /* + * Start a signaler thread that continuously sends SIGUSR1 to the + * calling thread. This maximizes the chance of interrupting + * wait_for_completion_interruptible() in the kernel's tsync path. + */ + sd.target = pthread_self(); + sd.stop = false; + ASSERT_EQ(0, pthread_create(&signaler, NULL, signaler_thread, &sd)); + + /* + * The syscall may be interrupted and transparently restarted by the + * kernel (ERESTARTNOINTR). From userspace, it should always succeed. + */ + EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, + LANDLOCK_RESTRICT_SELF_TSYNC)); + + sd.stop = true; + ASSERT_EQ(0, pthread_join(signaler, NULL)); + + for (i = 0; i < NUM_IDLE_THREADS; i++) { + ASSERT_EQ(0, pthread_cancel(threads[i])); + ASSERT_EQ(0, pthread_join(threads[i], NULL)); + } + + EXPECT_EQ(0, close(ruleset_fd)); +} + +/* clang-format off */ +FIXTURE(tsync_without_ruleset) {}; +/* clang-format on */ + +FIXTURE_VARIANT(tsync_without_ruleset) +{ + const __u32 flags; + const int expected_errno; +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) { + /* clang-format on */ + .flags = LANDLOCK_RESTRICT_SELF_TSYNC, + .expected_errno = EBADF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) { + /* clang-format on */ + .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_TSYNC, + .expected_errno = EBADF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) { + /* clang-format on */ + .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON | + LANDLOCK_RESTRICT_SELF_TSYNC, + .expected_errno = EBADF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) { + /* clang-format on */ + .flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF | + LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON | + LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_TSYNC, + .expected_errno = EBADF, +}; + +/* clang-format off */ +FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) { + /* clang-format on */ + .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF | + LANDLOCK_RESTRICT_SELF_TSYNC, + .expected_errno = 0, +}; + +FIXTURE_SETUP(tsync_without_ruleset) +{ + disable_caps(_metadata); +} + +FIXTURE_TEARDOWN(tsync_without_ruleset) +{ +} + +TEST_F(tsync_without_ruleset, check) +{ + int ret; + + ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); + + ret = landlock_restrict_self(-1, variant->flags); + if (variant->expected_errno) { + EXPECT_EQ(-1, ret); + EXPECT_EQ(variant->expected_errno, errno); + } else { + EXPECT_EQ(0, ret); + } +} + +TEST_HARNESS_MAIN 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 000000000..87dbc9164 --- /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; +} diff --git a/tools/testing/selftests/landlock/wait-pipe.c b/tools/testing/selftests/landlock/wait-pipe.c new file mode 100644 index 000000000..0dbcd260a --- /dev/null +++ b/tools/testing/selftests/landlock/wait-pipe.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Write in a pipe and wait. + * + * Used by layout1.umount_sandboxer from fs_test.c + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +int main(int argc, char *argv[]) +{ + int pipe_child, pipe_parent; + char buf; + + /* 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]); + + /* 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; +} diff --git a/tools/testing/selftests/landlock/wrappers.h b/tools/testing/selftests/landlock/wrappers.h new file mode 100644 index 000000000..65548323e --- /dev/null +++ b/tools/testing/selftests/landlock/wrappers.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Syscall wrappers + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation + */ + +#define _GNU_SOURCE +#include <linux/landlock.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <unistd.h> + +#ifndef landlock_create_ruleset +static inline int +landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, + const size_t size, const __u32 flags) +{ + return syscall(__NR_landlock_create_ruleset, attr, size, flags); +} +#endif + +#ifndef landlock_add_rule +static inline int landlock_add_rule(const int ruleset_fd, + const enum landlock_rule_type rule_type, + const void *const rule_attr, + const __u32 flags) +{ + return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, + flags); +} +#endif + +#ifndef landlock_restrict_self +static inline int landlock_restrict_self(const int ruleset_fd, + const __u32 flags) +{ + return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); +} +#endif + +static inline pid_t sys_gettid(void) +{ + return syscall(__NR_gettid); +} |
