| // 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 |