// 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 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Defines AT_EXECVE_CHECK without type conflicts. */ #define _ASM_GENERIC_FCNTL_H #include #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