| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Landlock tests - Ptrace |
| * |
| * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> |
| * Copyright © 2019-2020 ANSSI |
| */ |
| |
| #define _GNU_SOURCE |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <linux/landlock.h> |
| #include <signal.h> |
| #include <sys/prctl.h> |
| #include <sys/ptrace.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include "common.h" |
| |
| /* Copied from security/yama/yama_lsm.c */ |
| #define YAMA_SCOPE_DISABLED 0 |
| #define YAMA_SCOPE_RELATIONAL 1 |
| #define YAMA_SCOPE_CAPABILITY 2 |
| #define YAMA_SCOPE_NO_ATTACH 3 |
| |
| static void create_domain(struct __test_metadata *const _metadata) |
| { |
| int ruleset_fd; |
| struct landlock_ruleset_attr ruleset_attr = { |
| .handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_BLOCK, |
| }; |
| |
| ruleset_fd = |
| landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); |
| EXPECT_LE(0, ruleset_fd) |
| { |
| TH_LOG("Failed to create a ruleset: %s", strerror(errno)); |
| } |
| EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); |
| EXPECT_EQ(0, close(ruleset_fd)); |
| } |
| |
| static int test_ptrace_read(const pid_t pid) |
| { |
| static const char path_template[] = "/proc/%d/environ"; |
| char procenv_path[sizeof(path_template) + 10]; |
| int procenv_path_size, fd; |
| |
| procenv_path_size = snprintf(procenv_path, sizeof(procenv_path), |
| path_template, pid); |
| if (procenv_path_size >= sizeof(procenv_path)) |
| return E2BIG; |
| |
| fd = open(procenv_path, O_RDONLY | O_CLOEXEC); |
| if (fd < 0) |
| return errno; |
| /* |
| * Mixing error codes from close(2) and open(2) should not lead to any |
| * (access type) confusion for this test. |
| */ |
| if (close(fd) != 0) |
| return errno; |
| return 0; |
| } |
| |
| static int get_yama_ptrace_scope(void) |
| { |
| int ret; |
| char buf[2] = {}; |
| const int fd = open("/proc/sys/kernel/yama/ptrace_scope", O_RDONLY); |
| |
| if (fd < 0) |
| return 0; |
| |
| if (read(fd, buf, 1) < 0) { |
| close(fd); |
| return -1; |
| } |
| |
| ret = atoi(buf); |
| close(fd); |
| return ret; |
| } |
| |
| /* clang-format off */ |
| FIXTURE(hierarchy) {}; |
| /* clang-format on */ |
| |
| FIXTURE_VARIANT(hierarchy) |
| { |
| const bool domain_both; |
| const bool domain_parent; |
| const bool domain_child; |
| }; |
| |
| /* |
| * Test multiple tracing combinations between a parent process P1 and a child |
| * process P2. |
| * |
| * Yama's scoped ptrace is presumed disabled. If enabled, this optional |
| * restriction is enforced in addition to any Landlock check, which means that |
| * all P2 requests to trace P1 would be denied. |
| */ |
| |
| /* |
| * No domain |
| * |
| * P1-. P1 -> P2 : allow |
| * \ P2 -> P1 : allow |
| * 'P2 |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, allow_without_domain) { |
| /* clang-format on */ |
| .domain_both = false, |
| .domain_parent = false, |
| .domain_child = false, |
| }; |
| |
| /* |
| * Child domain |
| * |
| * P1--. P1 -> P2 : allow |
| * \ P2 -> P1 : deny |
| * .'-----. |
| * | P2 | |
| * '------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, allow_with_one_domain) { |
| /* clang-format on */ |
| .domain_both = false, |
| .domain_parent = false, |
| .domain_child = true, |
| }; |
| |
| /* |
| * Parent domain |
| * .------. |
| * | P1 --. P1 -> P2 : deny |
| * '------' \ P2 -> P1 : allow |
| * ' |
| * P2 |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, deny_with_parent_domain) { |
| /* clang-format on */ |
| .domain_both = false, |
| .domain_parent = true, |
| .domain_child = false, |
| }; |
| |
| /* |
| * Parent + child domain (siblings) |
| * .------. |
| * | P1 ---. P1 -> P2 : deny |
| * '------' \ P2 -> P1 : deny |
| * .---'--. |
| * | P2 | |
| * '------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, deny_with_sibling_domain) { |
| /* clang-format on */ |
| .domain_both = false, |
| .domain_parent = true, |
| .domain_child = true, |
| }; |
| |
| /* |
| * Same domain (inherited) |
| * .-------------. |
| * | P1----. | P1 -> P2 : allow |
| * | \ | P2 -> P1 : allow |
| * | ' | |
| * | P2 | |
| * '-------------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, allow_sibling_domain) { |
| /* clang-format on */ |
| .domain_both = true, |
| .domain_parent = false, |
| .domain_child = false, |
| }; |
| |
| /* |
| * Inherited + child domain |
| * .-----------------. |
| * | P1----. | P1 -> P2 : allow |
| * | \ | P2 -> P1 : deny |
| * | .-'----. | |
| * | | P2 | | |
| * | '------' | |
| * '-----------------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, allow_with_nested_domain) { |
| /* clang-format on */ |
| .domain_both = true, |
| .domain_parent = false, |
| .domain_child = true, |
| }; |
| |
| /* |
| * Inherited + parent domain |
| * .-----------------. |
| * |.------. | P1 -> P2 : deny |
| * || P1 ----. | P2 -> P1 : allow |
| * |'------' \ | |
| * | ' | |
| * | P2 | |
| * '-----------------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, deny_with_nested_and_parent_domain) { |
| /* clang-format on */ |
| .domain_both = true, |
| .domain_parent = true, |
| .domain_child = false, |
| }; |
| |
| /* |
| * Inherited + parent and child domain (siblings) |
| * .-----------------. |
| * | .------. | P1 -> P2 : deny |
| * | | P1 . | P2 -> P1 : deny |
| * | '------'\ | |
| * | \ | |
| * | .--'---. | |
| * | | P2 | | |
| * | '------' | |
| * '-----------------' |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(hierarchy, deny_with_forked_domain) { |
| /* clang-format on */ |
| .domain_both = true, |
| .domain_parent = true, |
| .domain_child = true, |
| }; |
| |
| FIXTURE_SETUP(hierarchy) |
| { |
| } |
| |
| FIXTURE_TEARDOWN(hierarchy) |
| { |
| } |
| |
| /* Test PTRACE_TRACEME and PTRACE_ATTACH for parent and child. */ |
| TEST_F(hierarchy, trace) |
| { |
| pid_t child, parent; |
| int status, err_proc_read; |
| int pipe_child[2], pipe_parent[2]; |
| int yama_ptrace_scope; |
| char buf_parent; |
| long ret; |
| bool can_read_child, can_trace_child, can_read_parent, can_trace_parent; |
| |
| yama_ptrace_scope = get_yama_ptrace_scope(); |
| ASSERT_LE(0, yama_ptrace_scope); |
| |
| if (yama_ptrace_scope > YAMA_SCOPE_DISABLED) |
| TH_LOG("Incomplete tests due to Yama restrictions (scope %d)", |
| yama_ptrace_scope); |
| |
| /* |
| * can_read_child is true if a parent process can read its child |
| * process, which is only the case when the parent process is not |
| * isolated from the child with a dedicated Landlock domain. |
| */ |
| can_read_child = !variant->domain_parent; |
| |
| /* |
| * can_trace_child is true if a parent process can trace its child |
| * process. This depends on two conditions: |
| * - The parent process is not isolated from the child with a dedicated |
| * Landlock domain. |
| * - Yama allows tracing children (up to YAMA_SCOPE_RELATIONAL). |
| */ |
| can_trace_child = can_read_child && |
| yama_ptrace_scope <= YAMA_SCOPE_RELATIONAL; |
| |
| /* |
| * can_read_parent is true if a child process can read its parent |
| * process, which is only the case when the child process is not |
| * isolated from the parent with a dedicated Landlock domain. |
| */ |
| can_read_parent = !variant->domain_child; |
| |
| /* |
| * can_trace_parent is true if a child process can trace its parent |
| * process. This depends on two conditions: |
| * - The child process is not isolated from the parent with a dedicated |
| * Landlock domain. |
| * - Yama is disabled (YAMA_SCOPE_DISABLED). |
| */ |
| can_trace_parent = can_read_parent && |
| yama_ptrace_scope <= YAMA_SCOPE_DISABLED; |
| |
| /* |
| * Removes all effective and permitted capabilities to not interfere |
| * with cap_ptrace_access_check() in case of PTRACE_MODE_FSCREDS. |
| */ |
| drop_caps(_metadata); |
| |
| parent = getpid(); |
| ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| if (variant->domain_both) { |
| create_domain(_metadata); |
| if (!_metadata->passed) |
| /* Aborts before forking. */ |
| return; |
| } |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| char buf_child; |
| |
| ASSERT_EQ(0, close(pipe_parent[1])); |
| ASSERT_EQ(0, close(pipe_child[0])); |
| if (variant->domain_child) |
| create_domain(_metadata); |
| |
| /* Waits for the parent to be in a domain, if any. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
| |
| /* Tests PTRACE_MODE_READ on the parent. */ |
| err_proc_read = test_ptrace_read(parent); |
| if (can_read_parent) { |
| EXPECT_EQ(0, err_proc_read); |
| } else { |
| EXPECT_EQ(EACCES, err_proc_read); |
| } |
| |
| /* Tests PTRACE_ATTACH on the parent. */ |
| ret = ptrace(PTRACE_ATTACH, parent, NULL, 0); |
| if (can_trace_parent) { |
| EXPECT_EQ(0, ret); |
| } else { |
| EXPECT_EQ(-1, ret); |
| EXPECT_EQ(EPERM, errno); |
| } |
| if (ret == 0) { |
| ASSERT_EQ(parent, waitpid(parent, &status, 0)); |
| ASSERT_EQ(1, WIFSTOPPED(status)); |
| ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0)); |
| } |
| |
| /* Tests child PTRACE_TRACEME. */ |
| ret = ptrace(PTRACE_TRACEME); |
| if (can_trace_child) { |
| EXPECT_EQ(0, ret); |
| } else { |
| EXPECT_EQ(-1, ret); |
| EXPECT_EQ(EPERM, errno); |
| } |
| |
| /* |
| * Signals that the PTRACE_ATTACH test is done and the |
| * PTRACE_TRACEME test is ongoing. |
| */ |
| ASSERT_EQ(1, write(pipe_child[1], ".", 1)); |
| |
| if (can_trace_child) { |
| ASSERT_EQ(0, raise(SIGSTOP)); |
| } |
| |
| /* Waits for the parent PTRACE_ATTACH test. */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); |
| _exit(_metadata->passed ? EXIT_SUCCESS : EXIT_FAILURE); |
| return; |
| } |
| |
| ASSERT_EQ(0, close(pipe_child[1])); |
| ASSERT_EQ(0, close(pipe_parent[0])); |
| if (variant->domain_parent) |
| create_domain(_metadata); |
| |
| /* Signals that the parent is in a domain, if any. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| /* |
| * Waits for the child to test PTRACE_ATTACH on the parent and start |
| * testing PTRACE_TRACEME. |
| */ |
| ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); |
| |
| /* Tests child PTRACE_TRACEME. */ |
| if (can_trace_child) { |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| ASSERT_EQ(1, WIFSTOPPED(status)); |
| ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); |
| } else { |
| /* The child should not be traced by the parent. */ |
| EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0)); |
| EXPECT_EQ(ESRCH, errno); |
| } |
| |
| /* Tests PTRACE_MODE_READ on the child. */ |
| err_proc_read = test_ptrace_read(child); |
| if (can_read_child) { |
| EXPECT_EQ(0, err_proc_read); |
| } else { |
| EXPECT_EQ(EACCES, err_proc_read); |
| } |
| |
| /* Tests PTRACE_ATTACH on the child. */ |
| ret = ptrace(PTRACE_ATTACH, child, NULL, 0); |
| if (can_trace_child) { |
| EXPECT_EQ(0, ret); |
| } else { |
| EXPECT_EQ(-1, ret); |
| EXPECT_EQ(EPERM, errno); |
| } |
| |
| if (ret == 0) { |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| ASSERT_EQ(1, WIFSTOPPED(status)); |
| ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0)); |
| } |
| |
| /* Signals that the parent PTRACE_ATTACH test is done. */ |
| ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->passed = 0; |
| } |
| |
| TEST_HARNESS_MAIN |