Return-Path: Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1753482AbbGXAxj (ORCPT ); Thu, 23 Jul 2015 20:53:39 -0400 Received: from smtp.outflux.net ([198.145.64.163]:46422 "EHLO smtp.outflux.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1752532AbbGXAxi (ORCPT ); Thu, 23 Jul 2015 20:53:38 -0400 Date: Thu, 23 Jul 2015 17:53:30 -0700 From: Kees Cook To: linux-security-module@vger.kernel.org Cc: James Morris , Casey Schaufler , linux-kernel@vger.kernel.org Subject: [PATCH] LSM: LoadPin for module and firmware loading restrictions Message-ID: <20150724005330.GA15227@www.outflux.net> MIME-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Disposition: inline X-HELO: www.outflux.net Sender: linux-kernel-owner@vger.kernel.org List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Length: 12144 Lines: 418 This LSM enforces that kernel-loaded modules and firmware must all come from the same filesystem, with the expectation that such a filesystem is backed by a read-only device such as dm-verity or CDROM. This allows systems that have a verified and/or unchangeable filesystem to enforce module and firmware loading restrictions without needing to sign the files individually. Signed-off-by: Kees Cook --- MAINTAINERS | 6 + include/linux/lsm_hooks.h | 5 + security/Kconfig | 1 + security/Makefile | 2 + security/loadpin/Kconfig | 9 ++ security/loadpin/Makefile | 1 + security/loadpin/loadpin.c | 279 +++++++++++++++++++++++++++++++++++++++++++++ security/security.c | 2 + 8 files changed, 305 insertions(+) create mode 100644 security/loadpin/Kconfig create mode 100644 security/loadpin/Makefile create mode 100644 security/loadpin/loadpin.c diff --git a/MAINTAINERS b/MAINTAINERS index 2d3d55c8f5be..671e760cbe85 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9101,6 +9101,12 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/jj/apparmor-dev.git S: Supported F: security/apparmor/ +LOADPIN SECURITY MODULE +M: Kees Cook +T: git git://git.kernel.org/pub/scm/linux/kernel/git/kees/linux.git lsm/loadpin +S: Supported +F: security/loadpin/ + SENSABLE PHANTOM M: Jiri Slaby S: Maintained diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h index 9429f054c323..d8ceb8099bc0 100644 --- a/include/linux/lsm_hooks.h +++ b/include/linux/lsm_hooks.h @@ -1884,5 +1884,10 @@ extern void __init capability_add_hooks(void); #ifdef CONFIG_SECURITY_YAMA_STACKED void __init yama_add_hooks(void); #endif +#ifdef CONFIG_SECURITY_LOADPIN +void __init loadpin_add_hooks(void); +#else +static inline void loadpin_add_hooks(void) { }; +#endif #endif /* ! __LINUX_LSM_HOOKS_H */ diff --git a/security/Kconfig b/security/Kconfig index bf4ec46474b6..f09b58ef43af 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -122,6 +122,7 @@ source security/selinux/Kconfig source security/smack/Kconfig source security/tomoyo/Kconfig source security/apparmor/Kconfig +source security/loadpin/Kconfig source security/yama/Kconfig source security/integrity/Kconfig diff --git a/security/Makefile b/security/Makefile index c9bfbc84ff50..f2d71cdb8e19 100644 --- a/security/Makefile +++ b/security/Makefile @@ -8,6 +8,7 @@ subdir-$(CONFIG_SECURITY_SMACK) += smack subdir-$(CONFIG_SECURITY_TOMOYO) += tomoyo subdir-$(CONFIG_SECURITY_APPARMOR) += apparmor subdir-$(CONFIG_SECURITY_YAMA) += yama +subdir-$(CONFIG_SECURITY_LOADPIN) += loadpin # always enable default capabilities obj-y += commoncap.o @@ -22,6 +23,7 @@ obj-$(CONFIG_AUDIT) += lsm_audit.o obj-$(CONFIG_SECURITY_TOMOYO) += tomoyo/ obj-$(CONFIG_SECURITY_APPARMOR) += apparmor/ obj-$(CONFIG_SECURITY_YAMA) += yama/ +obj-$(CONFIG_SECURITY_LOADPIN) += loadpin/ obj-$(CONFIG_CGROUP_DEVICE) += device_cgroup.o # Object integrity file lists diff --git a/security/loadpin/Kconfig b/security/loadpin/Kconfig new file mode 100644 index 000000000000..8efb8458a9a2 --- /dev/null +++ b/security/loadpin/Kconfig @@ -0,0 +1,9 @@ +config SECURITY_LOADPIN + bool "Pin loading of kernel modules and firmware to one filesystem" + depends on SECURITY && BLOCK + help + Kernel module and firmware loading will be pinned to the first + filesystem used for loading. Any files that come from other + filesystems will be rejected. This is best used on systems + without an initrd that have a root filesystem backed by a + read-only device such as dm-verity or a CDROM. diff --git a/security/loadpin/Makefile b/security/loadpin/Makefile new file mode 100644 index 000000000000..c2d77f83037b --- /dev/null +++ b/security/loadpin/Makefile @@ -0,0 +1 @@ +obj-$(CONFIG_SECURITY_LOADPIN) += loadpin.o diff --git a/security/loadpin/loadpin.c b/security/loadpin/loadpin.c new file mode 100644 index 000000000000..60efa69c9dfb --- /dev/null +++ b/security/loadpin/loadpin.c @@ -0,0 +1,279 @@ +/* + * Module and Firmware Pinning Security Module + * + * Copyright 2011-2015 Google Inc. + * + * Author: Kees Cook + * + * This software is licensed under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation, and + * may be copied, distributed, and modified under those terms. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#define pr_fmt(fmt) "LoadPin: " fmt + +#include +#include +#include +#include +#include /* get_cmdline() */ +#include +#include +#include /* current */ +#include + +/* + * Return an allocated string that has been escaped of special characters + * and double quotes, making it safe to log in quotes. + */ +static char *kstrdup_quotable(char *src) +{ + size_t slen, dlen; + char *dst; + const int flags = ESCAPE_HEX; + const char esc[] = "\f\n\r\t\v\a\e\\\""; + + if (!src) + return NULL; + slen = strlen(src); + + dlen = string_escape_mem(src, slen, NULL, 0, flags, esc); + dst = kmalloc(dlen + 1, GFP_KERNEL); + if (!dst) + return NULL; + + BUG_ON(string_escape_mem(src, slen, dst, dlen, flags, esc) != dlen); + dst[dlen] = '\0'; + + return dst; +} + +/* + * Returns allocated NULL-terminated string containing process + * command line, with inter-argument NULLs replaced with spaces, + * and other special characters escaped. + */ +static char *kstrdup_quotable_cmdline(struct task_struct *task) +{ + char *buffer, *quoted; + int i, res; + + buffer = kmalloc(PAGE_SIZE, GFP_TEMPORARY); + if (!buffer) + return NULL; + + res = get_cmdline(task, buffer, PAGE_SIZE - 1); + buffer[res] = '\0'; + + /* Collapse trailing NULLs. */ + for (; res > 0; res--) + if (buffer[res-1] != '\0') + break; + + /* Replace inter-argument NULLs. */ + for (i = 0; i < res; i++) + if (buffer[i] == '\0') + buffer[i] = ' '; + + /* Make sure result is printable. */ + quoted = kstrdup_quotable(buffer); + kfree(buffer); + return quoted; +} + +static void report_load(const char *origin, struct file *file, char *operation) +{ + char *alloced = NULL, *cmdline; + char *pathname; /* Pointer to either static string or "alloced". */ + + if (!file) + pathname = ""; + else { + /* We will allow 11 spaces for ' (deleted)' to be appended */ + alloced = pathname = kmalloc(PATH_MAX + 11, GFP_TEMPORARY); + if (!pathname) + pathname = ""; + else { + pathname = file_path(file, pathname, PATH_MAX + 11); + if (IS_ERR(pathname)) + pathname = ""; + else { + pathname = kstrdup_quotable(pathname); + kfree(alloced); + alloced = pathname; + } + } + } + + cmdline = kstrdup_quotable_cmdline(current); + + pr_notice("%s %s obj=%s%s%s pid=%d cmdline=%s%s%s\n", + origin, operation, + pathname ? "\"" : "", pathname, pathname ? "\"" : "", + task_pid_nr(current), + cmdline ? "\"" : "", cmdline, cmdline ? "\"" : ""); + + kfree(cmdline); + kfree(alloced); +} + +static int load_pinning = 1; +static struct vfsmount *pinned_root; +static DEFINE_SPINLOCK(pinned_root_spinlock); + +#ifdef CONFIG_SYSCTL +static int zero; +static int one = 1; + +static struct ctl_path loadpin_sysctl_path[] = { + { .procname = "kernel", }, + { } +}; + +static struct ctl_table loadpin_sysctl_table[] = { + { + .procname = "load_pinning", + .data = &load_pinning, + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = proc_dointvec_minmax, + .extra1 = &zero, + .extra2 = &one, + }, + { } +}; + +/* + * This must be called after early kernel init, since then the rootdev + * is available. + */ +static void check_pinning_enforcement(struct vfsmount *mnt) +{ + bool ro = false; + + /* + * If load pinning is not enforced via a read-only block + * device, allow sysctl to change modes for testing. + */ + if (mnt->mnt_sb->s_bdev) { + ro = bdev_read_only(mnt->mnt_sb->s_bdev); + pr_info("dev(%u,%u): %s\n", + MAJOR(mnt->mnt_sb->s_bdev->bd_dev), + MINOR(mnt->mnt_sb->s_bdev->bd_dev), + ro ? "read-only" : "writable"); + } else + pr_info("vfsmount lacks block device, treating as: writable\n"); + + if (!ro) { + if (!register_sysctl_paths(loadpin_sysctl_path, + loadpin_sysctl_table)) + pr_notice("sysctl registration failed!\n"); + else + pr_info("load pinning can be disabled.\n"); + } else + pr_info("load pinning engaged.\n"); +} +#else +static void check_pinning_enforcement(struct vfsmount *mnt) +{ + pr_info("load pinning engaged.\n"); +} +#endif + +int loadpin_sb_umount(struct vfsmount *mnt, int flags) +{ + /* + * When unmounting the filesystem we were using for load + * pinning, we must release our reservation, but make sure + * no other modules or firmware can be loaded. + */ + if (!IS_ERR_OR_NULL(pinned_root) && mnt == pinned_root) { + mntput(pinned_root); + pinned_root = ERR_PTR(-EIO); + pr_info("umount pinned fs: refusing further loads\n"); + } + + return 0; +} + +static int check_pinning(const char *origin, struct file *file) +{ + struct vfsmount *load_root; + + /* This handles the older init_module API that has a NULL file. */ + if (!file) { + if (!load_pinning) { + report_load(origin, NULL, "old-api-pinning-ignored"); + return 0; + } + + report_load(origin, NULL, "old-api-denied"); + return -EPERM; + } + + load_root = file->f_path.mnt; + + /* First loaded module/firmware defines the root for all others. */ + spin_lock(&pinned_root_spinlock); + /* + * pinned_root is only NULL at startup. Otherwise, it is either + * a valid reference, or an ERR_PTR. + */ + if (!pinned_root) { + pinned_root = mntget(load_root); + /* + * Unlock now since it's only pinned_root we care about. + * In the worst case, we will (correctly) report pinning + * failures before we have announced that pinning is + * enabled. This would be purely cosmetic. + */ + spin_unlock(&pinned_root_spinlock); + check_pinning_enforcement(pinned_root); + report_load(origin, file, "pinned"); + } else { + spin_unlock(&pinned_root_spinlock); + } + + if (IS_ERR_OR_NULL(pinned_root) || load_root != pinned_root) { + if (unlikely(!load_pinning)) { + report_load(origin, file, "pinning-ignored"); + return 0; + } + + report_load(origin, file, "denied"); + return -EPERM; + } + + return 0; +} + +int loadpin_load_module(struct file *file) +{ + return check_pinning("init_module", file); +} + +int loadpin_load_firmware(struct file *file, char *buf, size_t size) +{ + return check_pinning("request_firmware", file); +} + +static struct security_hook_list loadpin_hooks[] = { + LSM_HOOK_INIT(sb_umount, loadpin_sb_umount), + LSM_HOOK_INIT(kernel_module_from_file, loadpin_load_module), + LSM_HOOK_INIT(kernel_fw_from_file, loadpin_load_firmware), +}; + +void __init loadpin_add_hooks(void) +{ + pr_info("preparing to pin"); + security_add_hooks(loadpin_hooks, ARRAY_SIZE(loadpin_hooks)); +} + +/* Should not be mutable after boot, so not listed in sysfs (perm == 0). */ +module_param(load_pinning, int, 0); +MODULE_PARM_DESC(load_pinning, "Pin module/firmware loading (default: true)"); diff --git a/security/security.c b/security/security.c index 595fffab48b0..45557e200ac5 100644 --- a/security/security.c +++ b/security/security.c @@ -65,6 +65,8 @@ int __init security_init(void) */ yama_add_hooks(); #endif + loadpin_add_hooks(); + /* * Load the chosen module if there is one. * This will also find yama if it is stacking -- 1.9.1 -- Kees Cook Chrome OS Security -- To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to majordomo@vger.kernel.org More majordomo info at http://vger.kernel.org/majordomo-info.html Please read the FAQ at http://www.tux.org/lkml/