From patchwork Tue Apr 28 17:51:28 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= X-Patchwork-Id: 11515329 Return-Path: Received: from mail.kernel.org (pdx-korg-mail-1.web.codeaurora.org [172.30.200.123]) by pdx-korg-patchwork-2.web.codeaurora.org (Postfix) with ESMTP id B21F4112C for ; Tue, 28 Apr 2020 17:52:35 +0000 (UTC) Received: from mother.openwall.net (mother.openwall.net [195.42.179.200]) by mail.kernel.org (Postfix) with SMTP id C744620757 for ; Tue, 28 Apr 2020 17:52:34 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mail.kernel.org C744620757 Authentication-Results: mail.kernel.org; dmarc=none (p=none dis=none) header.from=digikod.net Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=kernel-hardening-return-18665-patchwork-kernel-hardening=patchwork.kernel.org@lists.openwall.com Received: (qmail 17885 invoked by uid 550); 28 Apr 2020 17:52:02 -0000 Mailing-List: contact kernel-hardening-help@lists.openwall.com; run by ezmlm Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-ID: Delivered-To: mailing list kernel-hardening@lists.openwall.com Received: (qmail 17776 invoked from network); 28 Apr 2020 17:52:01 -0000 From: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= To: linux-kernel@vger.kernel.org Cc: =?utf-8?q?Micka=C3=ABl_Sala=C3=BCn?= , Aleksa Sarai , Alexei Starovoitov , Al Viro , Andy Lutomirski , Christian Heimes , Daniel Borkmann , Deven Bowers , Eric Chiang , Florian Weimer , James Morris , Jan Kara , Jann Horn , Jonathan Corbet , Kees Cook , Matthew Garrett , Matthew Wilcox , Michael Kerrisk , =?utf-8?q?Micka=C3=ABl_Sala=C3=BC?= =?utf-8?q?n?= , Mimi Zohar , =?utf-8?q?Philippe_Tr=C3=A9buchet?= , Scott Shell , Sean Christopherson , Shuah Khan , Steve Dower , Steve Grubb , Thibaut Sautereau , Vincent Strubel , kernel-hardening@lists.openwall.com, linux-api@vger.kernel.org, linux-security-module@vger.kernel.org, linux-fsdevel@vger.kernel.org Subject: [PATCH v3 4/5] selftest/openat2: Add tests for RESOLVE_MAYEXEC enforcing Date: Tue, 28 Apr 2020 19:51:28 +0200 Message-Id: <20200428175129.634352-5-mic@digikod.net> X-Mailer: git-send-email 2.26.2 In-Reply-To: <20200428175129.634352-1-mic@digikod.net> References: <20200428175129.634352-1-mic@digikod.net> MIME-Version: 1.0 X-Antivirus: Dr.Web (R) for Unix mail servers drweb plugin ver.6.0.2.8 X-Antivirus-Code: 0x100000 Test propagation of noexec mount points or file executability through files open with or without RESOLVE_MAYEXEC. Signed-off-by: Mickaël Salaün Reviewed-by: Thibaut Sautereau Cc: Aleksa Sarai Cc: Al Viro Cc: Kees Cook Cc: Shuah Khan --- 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 | 3 + .../testing/selftests/openat2/omayexec_test.c | 315 ++++++++++++++++++ 5 files changed, 324 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 #include #include 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..2a46d5446110 100644 --- a/tools/testing/selftests/openat2/helpers.h +++ b/tools/testing/selftests/openat2/helpers.h @@ -9,6 +9,7 @@ #define _GNU_SOURCE #include +#include #include #include #include "../kselftest.h" @@ -60,6 +61,8 @@ bool needs_openat2(const struct open_how *how); #define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".." be scoped inside the dirfd (similar to chroot(2)). */ +#define RESOLVE_MAYEXEC 0x20 /* Command execution from file is + intended, checks exec permissions. */ #endif /* RESOLVE_IN_ROOT */ #define E_func(func, ...) \ diff --git a/tools/testing/selftests/openat2/omayexec_test.c b/tools/testing/selftests/openat2/omayexec_test.c new file mode 100644 index 000000000000..5298cbd5b7e9 --- /dev/null +++ b/tools/testing/selftests/openat2/omayexec_test.c @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test openat2(2) with RESOLVE_MAYEXEC + * + * Copyright © 2018-2020 ANSSI + * + * Author: Mickaël Salaün + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "helpers.h" +#include "../kselftest_harness.h" + +#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 MAYEXEC. */ + fd = sys_openat2(AT_FDCWD, path, &how); + ASSERT_LE(0, fd); + EXPECT_EQ(0, close(fd)); + + /* Opens with MAYEXEC. */ + how.resolve = RESOLVE_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