Test propagation of noexec mount points or file executability through files open with or without O_MAYEXEC, thanks to the fs.open_mayexec_enforce sysctl. Signed-off-by: Mickaël Salaün <mic@xxxxxxxxxxx> Reviewed-by: Thibaut Sautereau <thibaut.sautereau@xxxxxxxxxxx> Cc: Aleksa Sarai <cyphar@xxxxxxxxxx> Cc: Al Viro <viro@xxxxxxxxxxxxxxxxxx> Cc: Kees Cook <keescook@xxxxxxxxxxxx> Cc: Shuah Khan <shuah@xxxxxxxxxx> --- Changes since v3: * Replace RESOLVE_MAYEXEC with O_MAYEXEC. * Add tests to check that O_MAYEXEC is ignored by open(2) and openat(2). Changes since v2: * Move tests from exec/ to openat2/ . * Replace O_MAYEXEC with RESOLVE_MAYEXEC from openat2(2). * Cleanup tests. Changes since v1: * Move tests from yama/ to exec/ . * Fix _GNU_SOURCE in kselftest_harness.h . * Add a new test sysctl_access_write to check if CAP_MAC_ADMIN is taken into account. * Test directory execution which is always forbidden since commit 73601ea5b7b1 ("fs/open.c: allow opening only regular files during execve()"), and also check that even the root user can not bypass file execution checks. * Make sure delete_workspace() always as enough right to succeed. * Cosmetic cleanup. --- tools/testing/selftests/kselftest_harness.h | 3 + tools/testing/selftests/openat2/Makefile | 3 +- tools/testing/selftests/openat2/config | 1 + tools/testing/selftests/openat2/helpers.h | 1 + .../testing/selftests/openat2/omayexec_test.c | 330 ++++++++++++++++++ 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/openat2/config create mode 100644 tools/testing/selftests/openat2/omayexec_test.c diff --git a/tools/testing/selftests/kselftest_harness.h b/tools/testing/selftests/kselftest_harness.h index 2bb8c81fc0b4..f6e056ba4a13 100644 --- a/tools/testing/selftests/kselftest_harness.h +++ b/tools/testing/selftests/kselftest_harness.h @@ -50,7 +50,10 @@ #ifndef __KSELFTEST_HARNESS_H #define __KSELFTEST_HARNESS_H +#ifndef _GNU_SOURCE #define _GNU_SOURCE +#endif + #include <asm/types.h> #include <errno.h> #include <stdbool.h> diff --git a/tools/testing/selftests/openat2/Makefile b/tools/testing/selftests/openat2/Makefile index 4b93b1417b86..cb98bdb4d5b1 100644 --- a/tools/testing/selftests/openat2/Makefile +++ b/tools/testing/selftests/openat2/Makefile @@ -1,7 +1,8 @@ # SPDX-License-Identifier: GPL-2.0-or-later CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined -TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test +LDLIBS += -lcap +TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test omayexec_test include ../lib.mk diff --git a/tools/testing/selftests/openat2/config b/tools/testing/selftests/openat2/config new file mode 100644 index 000000000000..dd53c266bf52 --- /dev/null +++ b/tools/testing/selftests/openat2/config @@ -0,0 +1 @@ +CONFIG_SYSCTL=y diff --git a/tools/testing/selftests/openat2/helpers.h b/tools/testing/selftests/openat2/helpers.h index a6ea27344db2..1dcd3e1e2f38 100644 --- a/tools/testing/selftests/openat2/helpers.h +++ b/tools/testing/selftests/openat2/helpers.h @@ -9,6 +9,7 @@ #define _GNU_SOURCE #include <stdint.h> +#include <stdbool.h> #include <errno.h> #include <linux/types.h> #include "../kselftest.h" diff --git a/tools/testing/selftests/openat2/omayexec_test.c b/tools/testing/selftests/openat2/omayexec_test.c new file mode 100644 index 000000000000..7052c852daf8 --- /dev/null +++ b/tools/testing/selftests/openat2/omayexec_test.c @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test O_MAYEXEC + * + * Copyright © 2018-2020 ANSSI + * + * Author: Mickaël Salaün <mic@xxxxxxxxxxx> + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/capability.h> +#include <sys/mount.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "helpers.h" +#include "../kselftest_harness.h" + +#ifndef O_MAYEXEC +#define O_MAYEXEC 040000000 +#endif + +#define SYSCTL_MAYEXEC "/proc/sys/fs/open_mayexec_enforce" + +#define BIN_DIR "./test-mount" +#define BIN_PATH BIN_DIR "/file" +#define DIR_PATH BIN_DIR "/directory" + +#define ALLOWED 1 +#define DENIED 0 + +static void ignore_dac(struct __test_metadata *_metadata, int override) +{ + cap_t caps; + const cap_value_t cap_val[2] = { + CAP_DAC_OVERRIDE, + CAP_DAC_READ_SEARCH, + }; + + caps = cap_get_proc(); + ASSERT_NE(NULL, caps); + ASSERT_EQ(0, cap_set_flag(caps, CAP_EFFECTIVE, 2, cap_val, + override ? CAP_SET : CAP_CLEAR)); + ASSERT_EQ(0, cap_set_proc(caps)); + EXPECT_EQ(0, cap_free(caps)); +} + +static void ignore_mac(struct __test_metadata *_metadata, int override) +{ + cap_t caps; + const cap_value_t cap_val[1] = { + CAP_MAC_ADMIN, + }; + + caps = cap_get_proc(); + ASSERT_NE(NULL, caps); + ASSERT_EQ(0, cap_set_flag(caps, CAP_EFFECTIVE, 1, cap_val, + override ? CAP_SET : CAP_CLEAR)); + ASSERT_EQ(0, cap_set_proc(caps)); + EXPECT_EQ(0, cap_free(caps)); +} + +static void test_omx(struct __test_metadata *_metadata, + const char *const path, const int exec_allowed) +{ + struct open_how how = { + .flags = O_RDONLY | O_CLOEXEC, + }; + int fd; + + /* Opens without O_MAYEXEC. */ + fd = sys_openat2(AT_FDCWD, path, &how); + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + + how.flags |= O_MAYEXEC; + + /* Checks that O_MAYEXEC is ignored with open(2). */ + fd = open(path, how.flags); + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + + /* Checks that O_MAYEXEC is ignored with openat(2). */ + fd = openat(AT_FDCWD, path, how.flags); + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + + /* Opens with O_MAYEXEC. */ + fd = sys_openat2(AT_FDCWD, path, &how); + if (exec_allowed) { + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + } else { + ASSERT_EQ(-EACCES, fd); + } +} + +static void test_omx_dir_file(struct __test_metadata *_metadata, + const char *const dir_path, const char *const file_path, + const int exec_allowed) +{ + /* + * Directory execution is always denied since commit 73601ea5b7b1 + * ("fs/open.c: allow opening only regular files during execve()"). + */ + test_omx(_metadata, dir_path, DENIED); + test_omx(_metadata, file_path, exec_allowed); +} + +static void test_dir_file(struct __test_metadata *_metadata, + const char *const dir_path, const char *const file_path, + const int exec_allowed) +{ + /* Tests as root. */ + ignore_dac(_metadata, 1); + test_omx_dir_file(_metadata, dir_path, file_path, exec_allowed); + + /* Tests without bypass. */ + ignore_dac(_metadata, 0); + test_omx_dir_file(_metadata, dir_path, file_path, exec_allowed); +} + +static void sysctl_write(struct __test_metadata *_metadata, + const char *path, const char *value) +{ + int fd; + size_t len_value; + ssize_t len_wrote; + + fd = open(path, O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + len_value = strlen(value); + len_wrote = write(fd, value, len_value); + ASSERT_EQ(len_wrote, len_value); + EXPECT_EQ(0, close(fd)); +} + +static void create_workspace(struct __test_metadata *_metadata, + int mount_exec, int file_exec) +{ + int fd; + + /* + * Cleans previous workspace if any error previously happened (don't + * check errors). + */ + umount(BIN_DIR); + rmdir(BIN_DIR); + + /* Creates a clean mount point. */ + ASSERT_EQ(0, mkdir(BIN_DIR, 00700)); + ASSERT_EQ(0, mount("test", BIN_DIR, "tmpfs", + MS_MGC_VAL | (mount_exec ? 0 : MS_NOEXEC), + "mode=0700,size=4k")); + + /* Creates a test file. */ + fd = open(BIN_PATH, O_CREAT | O_RDONLY | O_CLOEXEC, + file_exec ? 00500 : 00400); + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + + /* Creates a test directory. */ + ASSERT_EQ(0, mkdir(DIR_PATH, file_exec ? 00500 : 00400)); +} + +static void delete_workspace(struct __test_metadata *_metadata) +{ + ignore_mac(_metadata, 1); + sysctl_write(_metadata, SYSCTL_MAYEXEC, "0"); + + /* There is no need to unlink BIN_PATH nor DIR_PATH. */ + ASSERT_EQ(0, umount(BIN_DIR)); + ASSERT_EQ(0, rmdir(BIN_DIR)); +} + +FIXTURE_DATA(mount_exec_file_exec) { }; + +FIXTURE_SETUP(mount_exec_file_exec) +{ + create_workspace(_metadata, 1, 1); +} + +FIXTURE_TEARDOWN(mount_exec_file_exec) +{ + delete_workspace(_metadata); +} + +TEST_F(mount_exec_file_exec, mount) +{ + /* Enforces mount exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "1"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, ALLOWED); +} + +TEST_F(mount_exec_file_exec, file) +{ + /* Enforces file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "2"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, ALLOWED); +} + +TEST_F(mount_exec_file_exec, mount_file) +{ + /* Enforces mount and file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "3"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, ALLOWED); +} + +FIXTURE_DATA(mount_exec_file_noexec) { }; + +FIXTURE_SETUP(mount_exec_file_noexec) +{ + create_workspace(_metadata, 1, 0); +} + +FIXTURE_TEARDOWN(mount_exec_file_noexec) +{ + delete_workspace(_metadata); +} + +TEST_F(mount_exec_file_noexec, mount) +{ + /* Enforces mount exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "1"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, ALLOWED); +} + +TEST_F(mount_exec_file_noexec, file) +{ + /* Enforces file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "2"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +TEST_F(mount_exec_file_noexec, mount_file) +{ + /* Enforces mount and file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "3"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +FIXTURE_DATA(mount_noexec_file_exec) { }; + +FIXTURE_SETUP(mount_noexec_file_exec) +{ + create_workspace(_metadata, 0, 1); +} + +FIXTURE_TEARDOWN(mount_noexec_file_exec) +{ + delete_workspace(_metadata); +} + +TEST_F(mount_noexec_file_exec, mount) +{ + /* Enforces mount exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "1"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +TEST_F(mount_noexec_file_exec, file) +{ + /* Enforces file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "2"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, ALLOWED); +} + +TEST_F(mount_noexec_file_exec, mount_file) +{ + /* Enforces mount and file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "3"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +FIXTURE_DATA(mount_noexec_file_noexec) { }; + +FIXTURE_SETUP(mount_noexec_file_noexec) +{ + create_workspace(_metadata, 0, 0); +} + +FIXTURE_TEARDOWN(mount_noexec_file_noexec) +{ + delete_workspace(_metadata); +} + +TEST_F(mount_noexec_file_noexec, mount) +{ + /* Enforces mount exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "1"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +TEST_F(mount_noexec_file_noexec, file) +{ + /* Enforces file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "2"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +TEST_F(mount_noexec_file_noexec, mount_file) +{ + /* Enforces mount and file exec check. */ + sysctl_write(_metadata, SYSCTL_MAYEXEC, "3"); + test_dir_file(_metadata, DIR_PATH, BIN_PATH, DENIED); +} + +TEST(sysctl_access_write) +{ + int fd; + ssize_t len_wrote; + + ignore_mac(_metadata, 1); + sysctl_write(_metadata, SYSCTL_MAYEXEC, "0"); + + ignore_mac(_metadata, 0); + fd = open(SYSCTL_MAYEXEC, O_WRONLY | O_CLOEXEC); + ASSERT_LE(0, fd); + len_wrote = write(fd, "0", 1); + ASSERT_EQ(len_wrote, -1); + EXPECT_EQ(0, close(fd)); + + ignore_mac(_metadata, 1); +} + +TEST_HARNESS_MAIN -- 2.26.2