blob: c2128de849c560c55f2a3c82cb815d7b15ba9d9f [file] [log] [blame]
/*
* Linux Security Module for Chromium OS
*
* Copyright 2011 Google Inc. All Rights Reserved
*
* Authors:
* Stephan Uphoff <ups@google.com>
* Kees Cook <keescook@chromium.org>
*
* 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) "Chromium OS LSM: " fmt
#include <asm/syscall.h>
#include <linux/cred.h>
#include <linux/fs.h>
#include <linux/fs_struct.h>
#include <linux/hashtable.h>
#include <linux/lsm_hooks.h>
#include <linux/module.h>
#include <linux/mount.h>
#include <linux/namei.h> /* for nameidata_get_total_link_count */
#include <linux/path.h>
#include <linux/ptrace.h>
#include <linux/sched/task_stack.h>
#include <linux/sched.h> /* current and other task related stuff */
#include <linux/security.h>
#include "inode_mark.h"
#include "utils.h"
#define NUM_BITS 8 // 128 buckets in hash table
static DEFINE_HASHTABLE(sb_nosymfollow_hashtable, NUM_BITS);
struct sb_entry {
struct hlist_node next;
struct hlist_node dlist; /* for deletion cleanup */
uintptr_t sb;
};
#if defined(CONFIG_SECURITY_CHROMIUMOS_NO_UNPRIVILEGED_UNSAFE_MOUNTS) || \
defined(CONFIG_SECURITY_CHROMIUMOS_NO_SYMLINK_MOUNT)
static void report(const char *origin, const struct path *path, char *operation)
{
char *alloced = NULL, *cmdline;
char *pathname; /* Pointer to either static string or "alloced". */
if (!path)
pathname = "<unknown>";
else {
/* We will allow 11 spaces for ' (deleted)' to be appended */
alloced = pathname = kmalloc(PATH_MAX+11, GFP_KERNEL);
if (!pathname)
pathname = "<no_memory>";
else {
pathname = d_path(path, pathname, PATH_MAX+11);
if (IS_ERR(pathname))
pathname = "<too_long>";
else {
pathname = printable(pathname, PATH_MAX+11);
kfree(alloced);
alloced = pathname;
}
}
}
cmdline = printable_cmdline(current);
pr_notice("%s %s obj=%s pid=%d cmdline=%s\n", origin,
operation, pathname, task_pid_nr(current), cmdline);
kfree(cmdline);
kfree(alloced);
}
#endif
static int chromiumos_security_sb_mount(const char *dev_name,
const struct path *path,
const char *type, unsigned long flags,
void *data)
{
#ifdef CONFIG_SECURITY_CHROMIUMOS_NO_SYMLINK_MOUNT
if (nameidata_get_total_link_count()) {
report("sb_mount", path, "Mount path with symlinks prohibited");
pr_notice("sb_mount dev=%s type=%s flags=%#lx\n",
dev_name, type, flags);
return -ELOOP;
}
#endif
#ifdef CONFIG_SECURITY_CHROMIUMOS_NO_UNPRIVILEGED_UNSAFE_MOUNTS
if ((!(flags & (MS_BIND | MS_MOVE | MS_SHARED | MS_PRIVATE | MS_SLAVE |
MS_UNBINDABLE)) ||
((flags & MS_REMOUNT) && (flags & MS_BIND))) &&
!capable(CAP_SYS_ADMIN)) {
int required_mnt_flags = MNT_NOEXEC | MNT_NOSUID | MNT_NODEV;
if (flags & MS_REMOUNT) {
/*
* If this is a remount, we only require that the
* requested flags are a superset of the original mount
* flags.
*/
required_mnt_flags &= path->mnt->mnt_flags;
}
/*
* The three flags we are interested in disallowing in
* unprivileged user namespaces (MS_NOEXEC, MS_NOSUID, MS_NODEV)
* cannot be modified when doing a bind-mount. The kernel
* attempts to dispatch calls to do_mount() within
* fs/namespace.c in the following order:
*
* * If the MS_REMOUNT flag is present, it calls do_remount().
* When MS_BIND is also present, it only allows to modify the
* per-mount flags, which are copied into
* |required_mnt_flags|. Otherwise it bails in the absence of
* the CAP_SYS_ADMIN in the init ns.
* * If the MS_BIND flag is present, the only other flag checked
* is MS_REC.
* * If any of the mount propagation flags are present
* (MS_SHARED, MS_PRIVATE, MS_SLAVE, MS_UNBINDABLE),
* flags_to_propagation_type() filters out any additional
* flags.
* * If MS_MOVE flag is present, all other flags are ignored.
*/
if ((required_mnt_flags & MNT_NOEXEC) && !(flags & MS_NOEXEC)) {
report("sb_mount", path,
"Mounting a filesystem with 'exec' flag requires CAP_SYS_ADMIN in init ns");
pr_notice("sb_mount dev=%s type=%s flags=%#lx\n",
dev_name, type, flags);
return -EPERM;
}
if ((required_mnt_flags & MNT_NOSUID) && !(flags & MS_NOSUID)) {
report("sb_mount", path,
"Mounting a filesystem with 'suid' flag requires CAP_SYS_ADMIN in init ns");
pr_notice("sb_mount dev=%s type=%s flags=%#lx\n",
dev_name, type, flags);
return -EPERM;
}
if ((required_mnt_flags & MNT_NODEV) && !(flags & MS_NODEV) &&
strcmp(type, "devpts")) {
report("sb_mount", path,
"Mounting a filesystem with 'dev' flag requires CAP_SYS_ADMIN in init ns");
pr_notice("sb_mount dev=%s type=%s flags=%#lx\n",
dev_name, type, flags);
return -EPERM;
}
}
#endif
return 0;
}
static DEFINE_SPINLOCK(sb_nosymfollow_hashtable_spinlock);
/* Check for entry in hash table. */
static bool chromiumos_check_sb_nosymfollow_hashtable(struct super_block *sb)
{
struct sb_entry *entry;
uintptr_t sb_pointer = (uintptr_t)sb;
bool found = false;
rcu_read_lock();
hash_for_each_possible_rcu(sb_nosymfollow_hashtable,
entry, next, sb_pointer) {
if (entry->sb == sb_pointer) {
found = true;
break;
}
}
rcu_read_unlock();
/*
* Its possible that a policy gets added in between the time we check
* above and when we return false here. Such a race condition should
* not affect this check however, since it would only be relevant if
* userspace tried to traverse a symlink on a filesystem before that
* filesystem was done being mounted (or potentially while it was being
* remounted with new mount flags).
*/
return found;
}
/* Add entry to hash table. */
static int chromiumos_add_sb_nosymfollow_hashtable(struct super_block *sb)
{
struct sb_entry *new;
uintptr_t sb_pointer = (uintptr_t)sb;
/* Return if entry already exists */
if (chromiumos_check_sb_nosymfollow_hashtable(sb))
return 0;
new = kzalloc(sizeof(struct sb_entry), GFP_KERNEL);
if (!new)
return -ENOMEM;
new->sb = sb_pointer;
spin_lock(&sb_nosymfollow_hashtable_spinlock);
hash_add_rcu(sb_nosymfollow_hashtable, &new->next, sb_pointer);
spin_unlock(&sb_nosymfollow_hashtable_spinlock);
return 0;
}
/* Flush all entries from hash table. */
void chromiumos_flush_sb_nosymfollow_hashtable(void)
{
struct sb_entry *entry;
struct hlist_node *hlist_node;
unsigned int bkt_loop_cursor;
HLIST_HEAD(free_list);
/*
* Could probably use hash_for_each_rcu here instead, but this should
* be fine as well.
*/
spin_lock(&sb_nosymfollow_hashtable_spinlock);
hash_for_each_safe(sb_nosymfollow_hashtable, bkt_loop_cursor,
hlist_node, entry, next) {
hash_del_rcu(&entry->next);
hlist_add_head(&entry->dlist, &free_list);
}
spin_unlock(&sb_nosymfollow_hashtable_spinlock);
synchronize_rcu();
hlist_for_each_entry_safe(entry, hlist_node, &free_list, dlist)
kfree(entry);
}
/* Remove entry from hash table. */
static void chromiumos_remove_sb_nosymfollow_hashtable(struct super_block *sb)
{
struct sb_entry *entry;
struct hlist_node *hlist_node;
uintptr_t sb_pointer = (uintptr_t)sb;
bool free_entry = false;
/*
* Could probably use hash_for_each_rcu here instead, but this should
* be fine as well.
*/
spin_lock(&sb_nosymfollow_hashtable_spinlock);
hash_for_each_possible_safe(sb_nosymfollow_hashtable, entry,
hlist_node, next, sb_pointer) {
if (entry->sb == sb_pointer) {
hash_del_rcu(&entry->next);
free_entry = true;
break;
}
}
spin_unlock(&sb_nosymfollow_hashtable_spinlock);
if (free_entry) {
synchronize_rcu();
kfree(entry);
}
}
int chromiumos_security_sb_umount(struct vfsmount *mnt, int flags)
{
/* If mnt->mnt_sb is in nosymfollow hashtable, remove it. */
chromiumos_remove_sb_nosymfollow_hashtable(mnt->mnt_sb);
return 0;
}
/*
* NOTE: The WARN() calls will emit a warning in cases of blocked symlink
* traversal attempts. These will show up in kernel warning reports
* collected by the crash reporter, so we have some insight on spurious
* failures that need addressing.
*/
static int chromiumos_security_inode_follow_link(struct dentry *dentry,
struct inode *inode, bool rcu)
{
static char accessed_path[PATH_MAX];
enum chromiumos_inode_security_policy policy;
/* Deny if symlinks have been disabled on this superblock. */
if (chromiumos_check_sb_nosymfollow_hashtable(dentry->d_sb)) {
WARN(1,
"Blocked symlink traversal for path %x:%x:%s (symlinks were disabled on this FS through the 'nosymfollow' mount option)\n",
MAJOR(dentry->d_sb->s_dev),
MINOR(dentry->d_sb->s_dev),
dentry_path(dentry, accessed_path, PATH_MAX));
return -EACCES;
}
policy = chromiumos_get_inode_security_policy(
dentry, inode,
CHROMIUMOS_SYMLINK_TRAVERSAL);
WARN(policy == CHROMIUMOS_INODE_POLICY_BLOCK,
"Blocked symlink traversal for path %x:%x:%s (see https://goo.gl/8xICW6 for context and rationale)\n",
MAJOR(dentry->d_sb->s_dev), MINOR(dentry->d_sb->s_dev),
dentry_path(dentry, accessed_path, PATH_MAX));
return policy == CHROMIUMOS_INODE_POLICY_BLOCK ? -EACCES : 0;
}
static int chromiumos_security_file_open(struct file *file)
{
static char accessed_path[PATH_MAX];
enum chromiumos_inode_security_policy policy;
struct dentry *dentry = file->f_path.dentry;
/* Returns 0 if file is not a FIFO */
if (!S_ISFIFO(file->f_inode->i_mode))
return 0;
policy = chromiumos_get_inode_security_policy(
dentry, dentry->d_inode,
CHROMIUMOS_FIFO_ACCESS);
/*
* Emit a warning in cases of blocked fifo access attempts. These will
* show up in kernel warning reports collected by the crash reporter,
* so we have some insight on spurious failures that need addressing.
*/
WARN(policy == CHROMIUMOS_INODE_POLICY_BLOCK,
"Blocked fifo access for path %x:%x:%s\n (see https://goo.gl/8xICW6 for context and rationale)\n",
MAJOR(dentry->d_sb->s_dev), MINOR(dentry->d_sb->s_dev),
dentry_path(dentry, accessed_path, PATH_MAX));
return policy == CHROMIUMOS_INODE_POLICY_BLOCK ? -EACCES : 0;
}
/*
* This hook inspects the string pointed to by the first parameter, looking for
* the "nosymfollow" mount option. The second parameter points to an empty
* page-sized buffer that is used for holding LSM-specific mount options that
* are grabbed (after this function executes, in security_sb_copy_data) from
* the mount string in the first parameter. Since the chromiumos LSM is stacked
* ahead of SELinux for ChromeOS, the page-sized buffer is empty when this
* function is called. If the "nosymfollow" mount option is encountered in this
* function, we write "nosymflw" to the empty page-sized buffer which lets us
* transmit information which will be visible in chromiumos_sb_kern_mount
* signifying that symlinks should be disabled for the sb. We store this token
* at a spot in the buffer that is at a greater offset than the bytes needed to
* record the rest of the LSM-specific mount options (e.g. those for SELinux).
* The "nosymfollow" option will be stripped from the mount string if it is
* encountered.
*/
int chromiumos_sb_copy_data(char *orig, char *copy)
{
char *orig_copy;
char *orig_copy_cur;
char *option;
size_t offset = 0;
bool found = false;
if (!orig || *orig == 0)
return 0;
orig_copy = alloc_secdata();
if (!orig_copy)
return -ENOMEM;
strncpy(orig_copy, orig, PAGE_SIZE);
memset(orig, 0, strlen(orig));
orig_copy_cur = orig_copy;
while (orig_copy_cur) {
option = strsep(&orig_copy_cur, ",");
if (strcmp(option, "nosymfollow") == 0) {
if (found) /* Found multiple times. */
return -EINVAL;
found = true;
} else {
if (offset > 0) {
orig[offset] = ',';
offset++;
}
strcpy(orig + offset, option);
offset += strlen(option);
}
}
if (found)
strcpy(copy + offset + 1, "nosymflw");
free_secdata(orig_copy);
return 0;
}
/* Unfortunately the kernel doesn't implement memmem function. */
static void *search_buffer(void *haystack, size_t haystacklen,
const void *needle, size_t needlelen)
{
if (!needlelen)
return (void *)haystack;
while (haystacklen >= needlelen) {
haystacklen--;
if (!memcmp(haystack, needle, needlelen))
return (void *)haystack;
haystack++;
}
return NULL;
}
int chromiumos_sb_kern_mount(struct super_block *sb, int flags, void *data)
{
int ret;
char search_str[10] = "\0nosymflw";
if (!data)
return 0;
if (search_buffer(data, PAGE_SIZE, search_str, 10)) {
ret = chromiumos_add_sb_nosymfollow_hashtable(sb);
if (ret)
return ret;
}
return 0;
}
static struct security_hook_list chromiumos_security_hooks[] = {
LSM_HOOK_INIT(sb_mount, chromiumos_security_sb_mount),
LSM_HOOK_INIT(inode_follow_link, chromiumos_security_inode_follow_link),
LSM_HOOK_INIT(file_open, chromiumos_security_file_open),
LSM_HOOK_INIT(sb_copy_data, chromiumos_sb_copy_data),
LSM_HOOK_INIT(sb_kern_mount, chromiumos_sb_kern_mount),
LSM_HOOK_INIT(sb_umount, chromiumos_security_sb_umount)
};
static int __init chromiumos_security_init(void)
{
security_add_hooks(chromiumos_security_hooks,
ARRAY_SIZE(chromiumos_security_hooks), "chromiumos");
pr_info("enabled");
return 0;
}
security_initcall(chromiumos_security_init);