463 lines · 11.8 KB · C
RawDownload
// SPDX-License-Identifier: GPL-2.0/* * Test execveat(2) with AT_EXECVE_CHECK, and prctl(2) with * SECBIT_EXEC_RESTRICT_FILE, SECBIT_EXEC_DENY_INTERACTIVE, and their locked * counterparts. * * Copyright © 2018-2020 ANSSI * Copyright © 2024 Microsoft Corporation * * Author: Mickaël Salaün <mic@digikod.net> */ #include <asm-generic/unistd.h>#include <errno.h>#include <fcntl.h>#include <linux/prctl.h>#include <linux/securebits.h>#include <stdio.h>#include <stdlib.h>#include <sys/capability.h>#include <sys/mount.h>#include <sys/prctl.h>#include <sys/socket.h>#include <sys/stat.h>#include <sys/syscall.h>#include <sys/sysmacros.h>#include <unistd.h> /* Defines AT_EXECVE_CHECK without type conflicts. */#define _ASM_GENERIC_FCNTL_H#include <linux/fcntl.h> #include "../kselftest_harness.h" 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);} static void drop_privileges(struct __test_metadata *const _metadata){	const unsigned int noroot = SECBIT_NOROOT | SECBIT_NOROOT_LOCKED;	cap_t cap_p; 	if ((cap_get_secbits() & noroot) != noroot)		EXPECT_EQ(0, cap_set_secbits(noroot)); 	cap_p = cap_get_proc();	EXPECT_NE(NULL, cap_p);	EXPECT_NE(-1, cap_clear(cap_p)); 	/*	 * Drops everything, especially CAP_SETPCAP, CAP_DAC_OVERRIDE, and	 * CAP_DAC_READ_SEARCH.	 */	EXPECT_NE(-1, cap_set_proc(cap_p));	EXPECT_NE(-1, cap_free(cap_p));} static int test_secbits_set(const unsigned int secbits){	int err; 	err = prctl(PR_SET_SECUREBITS, secbits);	if (err)		return errno;	return 0;} FIXTURE(access){	int memfd, pipefd;	int pipe_fds[2], socket_fds[2];}; FIXTURE_VARIANT(access){	const bool mount_exec;	const bool file_exec;}; /* clang-format off */FIXTURE_VARIANT_ADD(access, mount_exec_file_exec) {	/* clang-format on */	.mount_exec = true,	.file_exec = true,}; /* clang-format off */FIXTURE_VARIANT_ADD(access, mount_exec_file_noexec) {	/* clang-format on */	.mount_exec = true,	.file_exec = false,}; /* clang-format off */FIXTURE_VARIANT_ADD(access, mount_noexec_file_exec) {	/* clang-format on */	.mount_exec = false,	.file_exec = true,}; /* clang-format off */FIXTURE_VARIANT_ADD(access, mount_noexec_file_noexec) {	/* clang-format on */	.mount_exec = false,	.file_exec = false,}; static const char binary_path[] = "./false";static const char workdir_path[] = "./test-mount";static const char reg_file_path[] = "./test-mount/regular_file";static const char dir_path[] = "./test-mount/directory";static const char block_dev_path[] = "./test-mount/block_device";static const char char_dev_path[] = "./test-mount/character_device";static const char fifo_path[] = "./test-mount/fifo"; FIXTURE_SETUP(access){	int procfd_path_size;	static const char path_template[] = "/proc/self/fd/%d";	char procfd_path[sizeof(path_template) + 10]; 	/* Makes sure we are not already restricted nor locked. */	EXPECT_EQ(0, test_secbits_set(0)); 	/*	 * Cleans previous workspace if any error previously happened (don't	 * check errors).	 */	umount(workdir_path);	rmdir(workdir_path); 	/* Creates a clean mount point. */	ASSERT_EQ(0, mkdir(workdir_path, 00700));	ASSERT_EQ(0, mount("test", workdir_path, "tmpfs",			   MS_MGC_VAL | (variant->mount_exec ? 0 : MS_NOEXEC),			   "mode=0700,size=9m")); 	/* Creates a regular file. */	ASSERT_EQ(0, mknod(reg_file_path,			   S_IFREG | (variant->file_exec ? 0700 : 0600), 0));	/* Creates a directory. */	ASSERT_EQ(0, mkdir(dir_path, variant->file_exec ? 0700 : 0600));	/* Creates a character device: /dev/null. */	ASSERT_EQ(0, mknod(char_dev_path, S_IFCHR | 0400, makedev(1, 3)));	/* Creates a block device: /dev/loop0 */	ASSERT_EQ(0, mknod(block_dev_path, S_IFBLK | 0400, makedev(7, 0)));	/* Creates a fifo. */	ASSERT_EQ(0, mknod(fifo_path, S_IFIFO | 0600, 0)); 	/* Creates a regular file without user mount point. */	self->memfd = memfd_create("test-exec-probe", MFD_CLOEXEC);	ASSERT_LE(0, self->memfd);	/* Sets mode, which must be ignored by the exec check. */	ASSERT_EQ(0, fchmod(self->memfd, variant->file_exec ? 0700 : 0600)); 	/* Creates a pipefs file descriptor. */	ASSERT_EQ(0, pipe(self->pipe_fds));	procfd_path_size = snprintf(procfd_path, sizeof(procfd_path),				    path_template, self->pipe_fds[0]);	ASSERT_LT(procfd_path_size, sizeof(procfd_path));	self->pipefd = open(procfd_path, O_RDWR | O_CLOEXEC);	ASSERT_LE(0, self->pipefd);	ASSERT_EQ(0, fchmod(self->pipefd, variant->file_exec ? 0700 : 0600)); 	/* Creates a socket file descriptor. */	ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0,				self->socket_fds));} FIXTURE_TEARDOWN_PARENT(access){	/* There is no need to unlink the test files. */	EXPECT_EQ(0, umount(workdir_path));	EXPECT_EQ(0, rmdir(workdir_path));} static void fill_exec_fd(struct __test_metadata *_metadata, const int fd_out){	char buf[1024];	size_t len;	int fd_in; 	fd_in = open(binary_path, O_CLOEXEC | O_RDONLY);	ASSERT_LE(0, fd_in);	/* Cannot use copy_file_range(2) because of EXDEV. */	len = read(fd_in, buf, sizeof(buf));	EXPECT_LE(0, len);	while (len > 0) {		EXPECT_EQ(len, write(fd_out, buf, len))		{			TH_LOG("Failed to write: %s (%d)", strerror(errno),			       errno);		}		len = read(fd_in, buf, sizeof(buf));		EXPECT_LE(0, len);	}	EXPECT_EQ(0, close(fd_in));} static void fill_exec_path(struct __test_metadata *_metadata,			   const char *const path){	int fd_out; 	fd_out = open(path, O_CLOEXEC | O_WRONLY);	ASSERT_LE(0, fd_out)	{		TH_LOG("Failed to open %s: %s", path, strerror(errno));	}	fill_exec_fd(_metadata, fd_out);	EXPECT_EQ(0, close(fd_out));} static void test_exec_fd(struct __test_metadata *_metadata, const int fd,			 const int err_code){	char *const argv[] = { "", NULL };	int access_ret, access_errno; 	/*	 * If we really execute fd, filled with the "false" binary, the current	 * thread will exits with an error, which will be interpreted by the	 * test framework as an error.  With AT_EXECVE_CHECK, we only check a	 * potential successful execution.	 */	access_ret = sys_execveat(fd, "", argv, NULL,				  AT_EMPTY_PATH | AT_EXECVE_CHECK);	access_errno = errno;	if (err_code) {		EXPECT_EQ(-1, access_ret);		EXPECT_EQ(err_code, access_errno)		{			TH_LOG("Wrong error for execveat(2): %s (%d)",			       strerror(access_errno), errno);		}	} else {		EXPECT_EQ(0, access_ret)		{			TH_LOG("Access denied: %s", strerror(access_errno));		}	}} static void test_exec_path(struct __test_metadata *_metadata,			   const char *const path, const int err_code){	int flags = O_CLOEXEC;	int fd; 	/* Do not block on pipes. */	if (path == fifo_path)		flags |= O_NONBLOCK; 	fd = open(path, flags | O_RDONLY);	ASSERT_LE(0, fd)	{		TH_LOG("Failed to open %s: %s", path, strerror(errno));	}	test_exec_fd(_metadata, fd, err_code);	EXPECT_EQ(0, close(fd));} /* Tests that we don't get ENOEXEC. */TEST_F(access, regular_file_empty){	const int exec = variant->mount_exec && variant->file_exec; 	test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 	drop_privileges(_metadata);	test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);} TEST_F(access, regular_file_elf){	const int exec = variant->mount_exec && variant->file_exec; 	fill_exec_path(_metadata, reg_file_path); 	test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES); 	drop_privileges(_metadata);	test_exec_path(_metadata, reg_file_path, exec ? 0 : EACCES);} /* Tests that we don't get ENOEXEC. */TEST_F(access, memfd_empty){	const int exec = variant->file_exec; 	test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 	drop_privileges(_metadata);	test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);} TEST_F(access, memfd_elf){	const int exec = variant->file_exec; 	fill_exec_fd(_metadata, self->memfd); 	test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES); 	drop_privileges(_metadata);	test_exec_fd(_metadata, self->memfd, exec ? 0 : EACCES);} TEST_F(access, non_regular_files){	test_exec_path(_metadata, dir_path, EACCES);	test_exec_path(_metadata, block_dev_path, EACCES);	test_exec_path(_metadata, char_dev_path, EACCES);	test_exec_path(_metadata, fifo_path, EACCES);	test_exec_fd(_metadata, self->socket_fds[0], EACCES);	test_exec_fd(_metadata, self->pipefd, EACCES);} /* clang-format off */FIXTURE(secbits) {};/* clang-format on */ FIXTURE_VARIANT(secbits){	const bool is_privileged;	const int error;}; /* clang-format off */FIXTURE_VARIANT_ADD(secbits, priv) {	/* clang-format on */	.is_privileged = true,	.error = 0,}; /* clang-format off */FIXTURE_VARIANT_ADD(secbits, unpriv) {	/* clang-format on */	.is_privileged = false,	.error = EPERM,}; FIXTURE_SETUP(secbits){	/* Makes sure no exec bits are set. */	EXPECT_EQ(0, test_secbits_set(0));	EXPECT_EQ(0, prctl(PR_GET_SECUREBITS)); 	if (!variant->is_privileged)		drop_privileges(_metadata);} FIXTURE_TEARDOWN(secbits){} TEST_F(secbits, legacy){	EXPECT_EQ(variant->error, test_secbits_set(0));} #define CHILD(...)                     \	do {                           \		pid_t child = vfork(); \		EXPECT_LE(0, child);   \		if (child == 0) {      \			__VA_ARGS__;   \			_exit(0);      \		}                      \	} while (0) TEST_F(secbits, exec){	unsigned int secbits = prctl(PR_GET_SECUREBITS); 	secbits |= SECBIT_EXEC_RESTRICT_FILE;	EXPECT_EQ(0, test_secbits_set(secbits));	EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));	CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS))); 	secbits |= SECBIT_EXEC_DENY_INTERACTIVE;	EXPECT_EQ(0, test_secbits_set(secbits));	EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));	CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS))); 	secbits &= ~(SECBIT_EXEC_RESTRICT_FILE | SECBIT_EXEC_DENY_INTERACTIVE);	EXPECT_EQ(0, test_secbits_set(secbits));	EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS));	CHILD(EXPECT_EQ(secbits, prctl(PR_GET_SECUREBITS)));} TEST_F(secbits, check_locked_set){	unsigned int secbits = prctl(PR_GET_SECUREBITS); 	secbits |= SECBIT_EXEC_RESTRICT_FILE;	EXPECT_EQ(0, test_secbits_set(secbits));	secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED;	EXPECT_EQ(0, test_secbits_set(secbits)); 	/* Checks lock set but unchanged. */	EXPECT_EQ(variant->error, test_secbits_set(secbits));	CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 	secbits &= ~SECBIT_EXEC_RESTRICT_FILE;	EXPECT_EQ(EPERM, test_secbits_set(0));	CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));} TEST_F(secbits, check_locked_unset){	unsigned int secbits = prctl(PR_GET_SECUREBITS); 	secbits |= SECBIT_EXEC_RESTRICT_FILE_LOCKED;	EXPECT_EQ(0, test_secbits_set(secbits)); 	/* Checks lock unset but unchanged. */	EXPECT_EQ(variant->error, test_secbits_set(secbits));	CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 	secbits &= ~SECBIT_EXEC_RESTRICT_FILE;	EXPECT_EQ(EPERM, test_secbits_set(0));	CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));} TEST_F(secbits, restrict_locked_set){	unsigned int secbits = prctl(PR_GET_SECUREBITS); 	secbits |= SECBIT_EXEC_DENY_INTERACTIVE;	EXPECT_EQ(0, test_secbits_set(secbits));	secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED;	EXPECT_EQ(0, test_secbits_set(secbits)); 	/* Checks lock set but unchanged. */	EXPECT_EQ(variant->error, test_secbits_set(secbits));	CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 	secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE;	EXPECT_EQ(EPERM, test_secbits_set(0));	CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));} TEST_F(secbits, restrict_locked_unset){	unsigned int secbits = prctl(PR_GET_SECUREBITS); 	secbits |= SECBIT_EXEC_DENY_INTERACTIVE_LOCKED;	EXPECT_EQ(0, test_secbits_set(secbits)); 	/* Checks lock unset but unchanged. */	EXPECT_EQ(variant->error, test_secbits_set(secbits));	CHILD(EXPECT_EQ(variant->error, test_secbits_set(secbits))); 	secbits &= ~SECBIT_EXEC_DENY_INTERACTIVE;	EXPECT_EQ(EPERM, test_secbits_set(0));	CHILD(EXPECT_EQ(EPERM, test_secbits_set(0)));} TEST_HARNESS_MAIN