| // 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" | |
| #include "trace.h" | |
| #define TRACE_TASK "fs_test" | |
| #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 | |
| 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_whiteout_denied) | |
| { | |
| /* The affected file is a FIFO. */ | |
| ASSERT_EQ(0, unlink(file1_s3d3)); | |
| ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); | |
| /* Deny MAKE_REG, but allow MAKE_FIFO. */ | |
| enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL); | |
| /* | |
| * Try to rename a file with RENAME_WHITEOUT. | |
| * file1_s3d3 is in dir_s3d2 (tmpfs), so it supports RENAME_WHITEOUT. | |
| * Denied, because whiteout creation is guarded with MAKE_REG. | |
| */ | |
| EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, | |
| TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); | |
| EXPECT_EQ(EACCES, errno); | |
| } | |
| static bool is_whiteout(const char *const path) | |
| { | |
| struct stat st; | |
| if (stat(path, &st) == -1) | |
| return false; | |
| return S_ISCHR(st.st_mode) && st.st_rdev == makedev(0, 0); | |
| } | |
| static bool is_fifo(const char *const path) | |
| { | |
| struct stat st; | |
| return stat(path, &st) == 0 && S_ISFIFO(st.st_mode); | |
| } | |
| static bool is_missing(const char *const path) | |
| { | |
| struct stat st; | |
| return stat(path, &st) == -1 && errno == ENOENT; | |
| } | |
| TEST_F_FORK(layout1, rename_whiteout_allowed) | |
| { | |
| const struct rule rules[] = { | |
| { | |
| .path = dir_s3d3, | |
| .access = LANDLOCK_ACCESS_FS_MAKE_REG, | |
| }, | |
| {}, | |
| }; | |
| /* The affected file is a FIFO. */ | |
| ASSERT_EQ(0, unlink(file1_s3d3)); | |
| ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); | |
| /* Allow MAKE_REG below dir_s3d3. */ | |
| enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, rules); | |
| /* | |
| * Rename a file with RENAME_WHITEOUT within the same directory. | |
| * Allowed, because MAKE_REG is granted for the whiteout object which | |
| * gets created in the source location. | |
| */ | |
| EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, | |
| TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); | |
| /* A whiteout object took the place of the moved FIFO. */ | |
| EXPECT_TRUE(is_whiteout(file1_s3d3)); | |
| EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d3/f2")); | |
| } | |
| TEST_F_FORK(layout1, rename_whiteout_reparenting) | |
| { | |
| const struct rule rules[] = { | |
| { | |
| .path = dir_s3d2, | |
| .access = LANDLOCK_ACCESS_FS_REFER, | |
| }, | |
| { | |
| .path = dir_s3d3, | |
| .access = LANDLOCK_ACCESS_FS_MAKE_REG, | |
| }, | |
| {}, | |
| }; | |
| /* The moved files are FIFOs. */ | |
| ASSERT_EQ(0, unlink(file1_s3d3)); | |
| ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); | |
| ASSERT_EQ(0, unlink(file1_s3d4)); | |
| ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0)); | |
| /* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */ | |
| enforce_fs(_metadata, | |
| LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, | |
| rules); | |
| /* | |
| * The whiteout object is created in the source directory: Moving the | |
| * FIFO out of dir_s3d4 is denied because MAKE_REG is not granted | |
| * there, even though it is granted in the destination directory | |
| * dir_s3d3. | |
| */ | |
| EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD, | |
| TMP_DIR "/s3d1/s3d2/s3d3/f2", RENAME_WHITEOUT)); | |
| EXPECT_EQ(EACCES, errno); | |
| /* | |
| * Moving the FIFO out of dir_s3d3 is allowed, because MAKE_REG is | |
| * granted there for the created whiteout object. | |
| */ | |
| EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, | |
| TMP_DIR "/s3d1/s3d2/s3d4/f2", RENAME_WHITEOUT)); | |
| /* A whiteout object took the place of the moved FIFO. */ | |
| EXPECT_TRUE(is_whiteout(file1_s3d3)); | |
| EXPECT_TRUE(is_fifo(TMP_DIR "/s3d1/s3d2/s3d4/f2")); | |
| } | |
| TEST_F_FORK(layout1, rename_whiteout_exchange) | |
| { | |
| const char *const whiteout_s3d3 = TMP_DIR "/s3d1/s3d2/s3d3/f2"; | |
| const struct rule rules[] = { | |
| { | |
| .path = dir_s3d2, | |
| .access = LANDLOCK_ACCESS_FS_REFER, | |
| }, | |
| { | |
| .path = dir_s3d3, | |
| .access = LANDLOCK_ACCESS_FS_MAKE_REG, | |
| }, | |
| {}, | |
| }; | |
| /* The exchanged files are FIFOs and an existing whiteout object. */ | |
| ASSERT_EQ(0, unlink(file1_s3d3)); | |
| ASSERT_EQ(0, mknod(file1_s3d3, S_IFIFO | 0600, 0)); | |
| ASSERT_EQ(0, mknod(whiteout_s3d3, S_IFCHR | 0600, makedev(0, 0))); | |
| ASSERT_EQ(0, unlink(file1_s3d4)); | |
| ASSERT_EQ(0, mknod(file1_s3d4, S_IFIFO | 0600, 0)); | |
| /* Allow REFER below dir_s3d2, but MAKE_REG only below dir_s3d3. */ | |
| enforce_fs(_metadata, | |
| LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, | |
| rules); | |
| /* | |
| * With RENAME_EXCHANGE, the whiteout object moves into the source | |
| * directory of the rename: Exchanging the FIFO in dir_s3d4 with the | |
| * whiteout object is denied because MAKE_REG is not granted in | |
| * dir_s3d4, even though it is granted in the whiteout object's own | |
| * directory dir_s3d3. | |
| */ | |
| EXPECT_EQ(-1, renameat2(AT_FDCWD, file1_s3d4, AT_FDCWD, whiteout_s3d3, | |
| RENAME_EXCHANGE)); | |
| EXPECT_EQ(EACCES, errno); | |
| /* | |
| * Exchanging the FIFO in dir_s3d3 with the whiteout object is | |
| * allowed, because MAKE_REG is granted in the directory into which | |
| * the whiteout object moves. | |
| */ | |
| EXPECT_EQ(0, renameat2(AT_FDCWD, file1_s3d3, AT_FDCWD, whiteout_s3d3, | |
| RENAME_EXCHANGE)); | |
| /* The FIFO and the whiteout object swapped places. */ | |
| EXPECT_TRUE(is_whiteout(file1_s3d3)); | |
| EXPECT_TRUE(is_fifo(whiteout_s3d3)); | |
| } | |
| 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 = sys_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_whiteout) | |
| { | |
| /* | |
| * Creates a whiteout object (creation guarded by MAKE_REG). | |
| * | |
| * Contrary to the other character devices, this does not require | |
| * CAP_MKNOD, cf. vfs_mknod(). | |
| */ | |
| test_make_file(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, S_IFCHR, | |
| makedev(0, 0)); | |
| } | |
| 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)); | |
| } | |
| /* Arbitrary command with nonzero bits in both 16-bit halves. */ | |
| static const unsigned int unknown_ioctl_cmd = 0xc00ffeee; | |
| /* | |
| * 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, unknown_ioctl_cmd)); | |
| 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"; | |
| /* lower_pl1 is a FIFO and is deliberately not in the lists below. */ | |
| static const char lower_pl1[] = LOWER_DATA "/pl1"; | |
| 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"; | |
| /* merge_pl1 is a FIFO and is deliberately not in the lists below. */ | |
| static const char merge_pl1[] = MERGE_DATA "/pl1"; | |
| 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 | |
| * │ └── pl1 [FIFO] | |
| * ├── merge | |
| * │ └── data | |
| * │ ├── dl1 | |
| * │ │ └── fl2 | |
| * │ ├── do1 | |
| * │ │ ├── fl3 | |
| * │ │ ├── fo2 | |
| * │ │ └── fu3 | |
| * │ ├── du1 | |
| * │ │ └── fu2 | |
| * │ ├── fl1 | |
| * │ ├── fo1 | |
| * │ ├── fu1 | |
| * │ └── pl1 [FIFO] | |
| * └── 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); | |
| ASSERT_EQ(0, mknod(lower_pl1, S_IFIFO | 0600, 0)); | |
| 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)); | |
| EXPECT_EQ(0, remove_path(lower_pl1)); | |
| /* 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 allows 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)); | |
| } | |
| } | |
| TEST_F_FORK(layout2_overlay, rename_in_overlay_without_make_reg) | |
| { | |
| const char *const merge_pl1_renamed = MERGE_DATA "/pl1_renamed"; | |
| if (self->skip_test) | |
| SKIP(return, "overlayfs is not supported (test)"); | |
| /* | |
| * merge_pl1 is a FIFO which only exists in the lower layer. Before | |
| * the rename, the upper layer has no entry under this name. | |
| */ | |
| ASSERT_TRUE(is_fifo(merge_pl1)); | |
| ASSERT_TRUE(is_missing(UPPER_DATA "/pl1")); | |
| /* MAKE_REG is restricted, but MAKE_FIFO is not. */ | |
| enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, NULL); | |
| /* | |
| * Rename the FIFO through OverlayFS. merge_pl1 originates from the | |
| * lower layer, so this triggers a copy-up and creates the whiteout in | |
| * the upper layer to hide the lower layer FIFO file. Even though | |
| * MAKE_REG is restricted, the rename on the OverlayFS works. | |
| */ | |
| EXPECT_EQ(0, rename(merge_pl1, merge_pl1_renamed)); | |
| /* Check that the rename worked. */ | |
| EXPECT_TRUE(is_fifo(merge_pl1_renamed)); | |
| EXPECT_TRUE(is_missing(merge_pl1)); | |
| /* | |
| * Check that the whiteout object was created on the underlying "upper" | |
| * filesystem during the rename. This is OK because the whiteout object | |
| * was created by OverlayFS, not by the calling task. | |
| */ | |
| EXPECT_TRUE(is_whiteout(UPPER_DATA "/pl1")); | |
| } | |
| 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, makedev(7, 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_whiteout) | |
| { | |
| struct audit_records records; | |
| EXPECT_EQ(0, unlink(file1_s1d3)); | |
| enforce_fs(_metadata, ACCESS_ALL, NULL); | |
| /* Whiteout creation is denied and logged as fs.make_reg. */ | |
| EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, makedev(0, 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_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); | |
| } | |
| /* Checks that audit records preserve every ioctl command bit. */ | |
| 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, unknown_ioctl_cmd)); | |
| EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd, | |
| "fs\\.ioctl_dev", "/dev/null", | |
| " ioctlcmd=0xc00ffeee")); | |
| 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); | |
| } | |
| /* clang-format off */ | |
| FIXTURE(trace_layout1) { | |
| /* clang-format on */ | |
| int tracefs_ok; | |
| }; | |
| FIXTURE_SETUP(trace_layout1) | |
| { | |
| struct stat st; | |
| /* | |
| * Check tracefs availability before creating the layout, following the | |
| * layout3_fs pattern: skip before any layout creation to avoid leaving | |
| * stale TMP_DIR on skip. | |
| */ | |
| if (stat(TRACEFS_LANDLOCK_DIR, &st)) { | |
| self->tracefs_ok = 0; | |
| SKIP(return, "tracefs not available"); | |
| } | |
| self->tracefs_ok = 1; | |
| /* Isolate tracefs state (PID filter, event enables). */ | |
| set_cap(_metadata, CAP_SYS_ADMIN); | |
| ASSERT_EQ(0, unshare(CLONE_NEWNS)); | |
| ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); | |
| clear_cap(_metadata, CAP_SYS_ADMIN); | |
| prepare_layout(_metadata); | |
| create_layout1(_metadata); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_fixture_setup()); | |
| ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true)); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| ASSERT_EQ(0, tracefs_set_pid_filter(getpid())); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| } | |
| FIXTURE_TEARDOWN_PARENT(trace_layout1) | |
| { | |
| if (!self->tracefs_ok) | |
| return; | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false); | |
| tracefs_clear_pid_filter(); | |
| tracefs_fixture_teardown(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| remove_layout1(_metadata); | |
| cleanup_layout(_metadata); | |
| } | |
| /* | |
| * Verifies that check_rule_inode events include correct field values: domain, | |
| * dev, ino, access_request, and grants. All values are verified against stat() | |
| * of the rule path on a deterministic tmpfs layout. | |
| */ | |
| TEST_F(trace_layout1, check_rule_fs_fields) | |
| { | |
| struct stat dir_stat; | |
| char expected_dev[32]; | |
| char expected_ino[32]; | |
| char *buf; | |
| char field[64]; | |
| if (!self->tracefs_ok) | |
| SKIP(return, "tracefs not available"); | |
| ASSERT_EQ(0, stat(dir_s1d1, &dir_stat)); | |
| snprintf(expected_dev, sizeof(expected_dev), "%u:%u", | |
| major(dir_stat.st_dev), minor(dir_stat.st_dev)); | |
| snprintf(expected_ino, sizeof(expected_ino), "%lu", dir_stat.st_ino); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| sandbox_child_fs_access(_metadata, dir_s1d1, | |
| LANDLOCK_ACCESS_FS_READ_DIR, | |
| LANDLOCK_ACCESS_FS_READ_DIR, dir_s1d1); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| buf = tracefs_read_trace(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_NE(NULL, buf); | |
| EXPECT_EQ(1, | |
| tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK))) | |
| { | |
| TH_LOG("Expected 1 check_rule_inode event\n%s", buf); | |
| } | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "dev", field, sizeof(field))); | |
| EXPECT_STREQ(expected_dev, field) | |
| { | |
| TH_LOG("Expected dev=%s, got %s", expected_dev, field); | |
| } | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "ino", field, sizeof(field))); | |
| EXPECT_STREQ(expected_ino, field) | |
| { | |
| TH_LOG("Expected ino=%s, got %s", expected_ino, field); | |
| } | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "access_request", field, | |
| sizeof(field))); | |
| EXPECT_STREQ("read_dir", field) | |
| { | |
| TH_LOG("Expected access_request=read_dir, got %s", field); | |
| } | |
| /* | |
| * The domain handles only READ_DIR, so the rule carries the | |
| * unhandled-rights padding; intersecting with the request leaves just | |
| * the requested read_dir (no padding, no hex). | |
| */ | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "grants", field, sizeof(field))); | |
| EXPECT_STREQ("{read_dir}", field) | |
| { | |
| TH_LOG("Expected grants={read_dir}, got %s", field); | |
| } | |
| free(buf); | |
| } | |
| /* | |
| * Verifies check_rule_inode behavior with multiple rules. With rules at s1d1 | |
| * and s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the | |
| * pathwalk short-circuits after the first rule fully unmasks the single layer. | |
| */ | |
| TEST_F(trace_layout1, check_rule_fs_multiple_rules) | |
| { | |
| pid_t pid; | |
| int status; | |
| char *buf; | |
| int count; | |
| if (!self->tracefs_ok) | |
| SKIP(return, "tracefs not available"); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| pid = fork(); | |
| ASSERT_LE(0, pid); | |
| if (pid == 0) { | |
| struct landlock_ruleset_attr attr = { | |
| .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR, | |
| }; | |
| struct landlock_path_beneath_attr path_beneath = { | |
| .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR, | |
| }; | |
| int ruleset_fd, fd; | |
| ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); | |
| if (ruleset_fd < 0) | |
| _exit(1); | |
| path_beneath.parent_fd = | |
| open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd < 0) | |
| _exit(1); | |
| if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0)) | |
| _exit(1); | |
| close(path_beneath.parent_fd); | |
| path_beneath.parent_fd = | |
| open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd < 0) | |
| _exit(1); | |
| if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0)) | |
| _exit(1); | |
| close(path_beneath.parent_fd); | |
| prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); | |
| if (landlock_restrict_self(ruleset_fd, 0)) | |
| _exit(1); | |
| close(ruleset_fd); | |
| fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC); | |
| if (fd >= 0) | |
| close(fd); | |
| _exit(0); | |
| } | |
| ASSERT_EQ(pid, waitpid(pid, &status, 0)); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| EXPECT_EQ(0, WEXITSTATUS(status)); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| buf = tracefs_read_trace(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_NE(NULL, buf); | |
| /* | |
| * Only one check_rule_inode event: the rule on dir_s1d2 fully unmasks | |
| * the single layer, so the pathwalk short-circuits before reaching the | |
| * dir_s1d1 rule. | |
| */ | |
| count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); | |
| EXPECT_EQ(1, count) | |
| { | |
| TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count, | |
| buf); | |
| } | |
| free(buf); | |
| } | |
| /* | |
| * Verifies the grants array is intersected with the request: a handled, | |
| * granted, but unrequested right (execute) is filtered out, leaving only the | |
| * requested read_dir. | |
| */ | |
| TEST_F(trace_layout1, check_rule_fs_request_subset) | |
| { | |
| char *buf; | |
| char field[64]; | |
| if (!self->tracefs_ok) | |
| SKIP(return, "tracefs not available"); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| /* | |
| * Handle and grant READ_DIR|EXECUTE; the open only requests read_dir. | |
| */ | |
| sandbox_child_fs_access( | |
| _metadata, dir_s1d1, | |
| LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE, | |
| LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE, | |
| dir_s1d1); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| buf = tracefs_read_trace(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_NE(NULL, buf); | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "access_request", field, | |
| sizeof(field))); | |
| EXPECT_STREQ("read_dir", field); | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "grants", field, sizeof(field))); | |
| EXPECT_STREQ("{read_dir}", field); | |
| free(buf); | |
| } | |
| /* | |
| * Verifies that the optional TRUNCATE access right, which hook_file_open() | |
| * speculatively evaluates on every open, appears in the access_request= and | |
| * grants= fields. Opening file1_s1d1 read-only needs only read_file, but the | |
| * open hook also evaluates truncate; the domain handles and the rule grants | |
| * both, so the event reports access_request=read_file|truncate and | |
| * grants={read_file|truncate}, and the open is allowed. | |
| */ | |
| TEST_F(trace_layout1, check_rule_fs_optional_access) | |
| { | |
| pid_t pid; | |
| int status; | |
| char *buf; | |
| char field[64]; | |
| int count; | |
| if (!self->tracefs_ok) | |
| SKIP(return, "tracefs not available"); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| pid = fork(); | |
| ASSERT_LE(0, pid); | |
| if (pid == 0) { | |
| struct landlock_ruleset_attr attr = { | |
| .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE | | |
| LANDLOCK_ACCESS_FS_TRUNCATE, | |
| }; | |
| struct landlock_path_beneath_attr path_beneath = { | |
| .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE | | |
| LANDLOCK_ACCESS_FS_TRUNCATE, | |
| }; | |
| int ruleset_fd, fd; | |
| ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); | |
| if (ruleset_fd < 0) | |
| _exit(1); | |
| path_beneath.parent_fd = | |
| open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd < 0) | |
| _exit(1); | |
| if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0)) | |
| _exit(1); | |
| close(path_beneath.parent_fd); | |
| prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); | |
| if (landlock_restrict_self(ruleset_fd, 0)) | |
| _exit(1); | |
| close(ruleset_fd); | |
| /* Read-only open needs only read_file; truncate is optional. */ | |
| fd = open(file1_s1d1, O_RDONLY | O_CLOEXEC); | |
| if (fd < 0) | |
| _exit(1); | |
| close(fd); | |
| _exit(0); | |
| } | |
| ASSERT_EQ(pid, waitpid(pid, &status, 0)); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| /* The open is allowed: the required read_file is granted. */ | |
| EXPECT_EQ(0, WEXITSTATUS(status)); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| buf = tracefs_read_trace(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_NE(NULL, buf); | |
| /* The rule at dir_s1d1 matches when opening file1_s1d1. */ | |
| count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); | |
| EXPECT_EQ(1, count) | |
| { | |
| TH_LOG("Expected 1 check_rule_inode event, got %d\n%s", count, | |
| buf); | |
| } | |
| /* The open hook adds the optional truncate to the request. */ | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "access_request", field, | |
| sizeof(field))); | |
| EXPECT_STREQ("read_file|truncate", field); | |
| /* The rule grants both, so truncate appears in the grants array. */ | |
| ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK), | |
| "grants", field, sizeof(field))); | |
| EXPECT_STREQ("{read_file|truncate}", field); | |
| free(buf); | |
| } | |
| /* | |
| * Verifies that check_rule_inode fires for a rule matching the inode even when | |
| * it grants none of the requested rights, so the grants set is empty. Landlock | |
| * cannot know a rule ignores the request before reading it, so the event is | |
| * still emitted (grants={}), which lets a tracer see that the rule matched. | |
| * The domain handles READ_DIR|EXECUTE, dir_s1d2 grants only EXECUTE and its | |
| * parent dir_s1d1 grants only READ_DIR. Reading dir_s1d2 (requesting read_dir) | |
| * first matches the dir_s1d2 rule, which grants nothing requested (grants={}); | |
| * walking up to dir_s1d1 then grants read_dir (grants={read_dir}) and allows | |
| * the access. | |
| */ | |
| TEST_F(trace_layout1, check_rule_fs_empty_grant) | |
| { | |
| pid_t pid; | |
| int status; | |
| char *buf; | |
| int count; | |
| if (!self->tracefs_ok) | |
| SKIP(return, "tracefs not available"); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_EQ(0, tracefs_clear()); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| pid = fork(); | |
| ASSERT_LE(0, pid); | |
| if (pid == 0) { | |
| struct landlock_ruleset_attr attr = { | |
| .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR | | |
| LANDLOCK_ACCESS_FS_EXECUTE, | |
| }; | |
| struct landlock_path_beneath_attr path_beneath = {}; | |
| int ruleset_fd, fd; | |
| ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0); | |
| if (ruleset_fd < 0) | |
| _exit(1); | |
| /* Parent dir_s1d1 grants only READ_DIR. */ | |
| path_beneath.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR; | |
| path_beneath.parent_fd = | |
| open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd < 0) | |
| _exit(1); | |
| if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0)) | |
| _exit(1); | |
| close(path_beneath.parent_fd); | |
| /* Child dir_s1d2 grants only EXECUTE. */ | |
| path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE; | |
| path_beneath.parent_fd = | |
| open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC); | |
| if (path_beneath.parent_fd < 0) | |
| _exit(1); | |
| if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
| &path_beneath, 0)) | |
| _exit(1); | |
| close(path_beneath.parent_fd); | |
| prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); | |
| if (landlock_restrict_self(ruleset_fd, 0)) | |
| _exit(1); | |
| close(ruleset_fd); | |
| fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC); | |
| if (fd < 0) | |
| _exit(1); | |
| close(fd); | |
| _exit(0); | |
| } | |
| ASSERT_EQ(pid, waitpid(pid, &status, 0)); | |
| ASSERT_TRUE(WIFEXITED(status)); | |
| EXPECT_EQ(0, WEXITSTATUS(status)); | |
| set_cap(_metadata, CAP_DAC_OVERRIDE); | |
| buf = tracefs_read_trace(); | |
| clear_cap(_metadata, CAP_DAC_OVERRIDE); | |
| ASSERT_NE(NULL, buf); | |
| /* | |
| * dir_s1d2 (grants nothing requested) then dir_s1d1 (grants read_dir). | |
| */ | |
| count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)); | |
| EXPECT_EQ(2, count) | |
| { | |
| TH_LOG("Expected 2 check_rule_inode events, got %d\n%s", count, | |
| buf); | |
| } | |
| /* The dir_s1d2 rule matches the inode but grants none of read_dir. */ | |
| EXPECT_EQ( | |
| 1, | |
| tracefs_count_matches( | |
| buf, | |
| TRACE_PREFIX( | |
| TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ " | |
| "access_request=read_dir " | |
| "dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ " | |
| "grants={}$")) | |
| { | |
| TH_LOG("Expected a grants={} event\n%s", buf); | |
| } | |
| /* Walking up to dir_s1d1 grants the requested read_dir. */ | |
| EXPECT_EQ( | |
| 1, | |
| tracefs_count_matches( | |
| buf, | |
| TRACE_PREFIX( | |
| TRACE_TASK) "landlock_check_rule_inode: domain=[0-9a-f]\\+ " | |
| "access_request=read_dir " | |
| "dev=[0-9]\\+:[0-9]\\+ ino=[0-9]\\+ " | |
| "grants={read_dir}$")) | |
| { | |
| TH_LOG("Expected a grants={read_dir} event\n%s", buf); | |
| } | |
| free(buf); | |
| } | |
| TEST_HARNESS_MAIN |