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
 |