Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Mickaël Salaün 1185 85.37% 2 50.00%
Jeff Xu 197 14.19% 1 25.00%
Jakub Kiciński 6 0.43% 1 25.00%
Total 1388 4


// 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 (!__test_passed(_metadata))
			/* Aborts before forking. */
			return;
	}

	child = fork();
	ASSERT_LE(0, child);
	if (child == 0) {
		char buf_child;

		ASSERT_EQ(0, close(pipe_parent[1]));
		ASSERT_EQ(0, close(pipe_child[0]));
		if (variant->domain_child)
			create_domain(_metadata);

		/* Waits for the parent to be in a domain, if any. */
		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));

		/* Tests PTRACE_MODE_READ on the parent. */
		err_proc_read = test_ptrace_read(parent);
		if (can_read_parent) {
			EXPECT_EQ(0, err_proc_read);
		} else {
			EXPECT_EQ(EACCES, err_proc_read);
		}

		/* Tests PTRACE_ATTACH on the parent. */
		ret = ptrace(PTRACE_ATTACH, parent, NULL, 0);
		if (can_trace_parent) {
			EXPECT_EQ(0, ret);
		} else {
			EXPECT_EQ(-1, ret);
			EXPECT_EQ(EPERM, errno);
		}
		if (ret == 0) {
			ASSERT_EQ(parent, waitpid(parent, &status, 0));
			ASSERT_EQ(1, WIFSTOPPED(status));
			ASSERT_EQ(0, ptrace(PTRACE_DETACH, parent, NULL, 0));
		}

		/* Tests child PTRACE_TRACEME. */
		ret = ptrace(PTRACE_TRACEME);
		if (can_trace_child) {
			EXPECT_EQ(0, ret);
		} else {
			EXPECT_EQ(-1, ret);
			EXPECT_EQ(EPERM, errno);
		}

		/*
		 * Signals that the PTRACE_ATTACH test is done and the
		 * PTRACE_TRACEME test is ongoing.
		 */
		ASSERT_EQ(1, write(pipe_child[1], ".", 1));

		if (can_trace_child) {
			ASSERT_EQ(0, raise(SIGSTOP));
		}

		/* Waits for the parent PTRACE_ATTACH test. */
		ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1));
		_exit(_metadata->exit_code);
		return;
	}

	ASSERT_EQ(0, close(pipe_child[1]));
	ASSERT_EQ(0, close(pipe_parent[0]));
	if (variant->domain_parent)
		create_domain(_metadata);

	/* Signals that the parent is in a domain, if any. */
	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));

	/*
	 * Waits for the child to test PTRACE_ATTACH on the parent and start
	 * testing PTRACE_TRACEME.
	 */
	ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1));

	/* Tests child PTRACE_TRACEME. */
	if (can_trace_child) {
		ASSERT_EQ(child, waitpid(child, &status, 0));
		ASSERT_EQ(1, WIFSTOPPED(status));
		ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
	} else {
		/* The child should not be traced by the parent. */
		EXPECT_EQ(-1, ptrace(PTRACE_DETACH, child, NULL, 0));
		EXPECT_EQ(ESRCH, errno);
	}

	/* Tests PTRACE_MODE_READ on the child. */
	err_proc_read = test_ptrace_read(child);
	if (can_read_child) {
		EXPECT_EQ(0, err_proc_read);
	} else {
		EXPECT_EQ(EACCES, err_proc_read);
	}

	/* Tests PTRACE_ATTACH on the child. */
	ret = ptrace(PTRACE_ATTACH, child, NULL, 0);
	if (can_trace_child) {
		EXPECT_EQ(0, ret);
	} else {
		EXPECT_EQ(-1, ret);
		EXPECT_EQ(EPERM, errno);
	}

	if (ret == 0) {
		ASSERT_EQ(child, waitpid(child, &status, 0));
		ASSERT_EQ(1, WIFSTOPPED(status));
		ASSERT_EQ(0, ptrace(PTRACE_DETACH, child, NULL, 0));
	}

	/* Signals that the parent PTRACE_ATTACH test is done. */
	ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
	ASSERT_EQ(child, waitpid(child, &status, 0));

	if (WIFSIGNALED(status) || !WIFEXITED(status) ||
	    WEXITSTATUS(status) != EXIT_SUCCESS)
		_metadata->exit_code = KSFT_FAIL;
}

TEST_HARNESS_MAIN