439 lines
10 KiB
C
439 lines
10 KiB
C
// 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
|