819 lines
23 KiB
C
819 lines
23 KiB
C
/*
|
|
* 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/fs.h>
|
|
#include <linux/fs_struct.h>
|
|
#include <linux/hashtable.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mount.h>
|
|
#include <linux/path.h>
|
|
#include <linux/root_dev.h>
|
|
#include <linux/sched.h> /* current and other task related stuff */
|
|
#include <linux/security.h>
|
|
|
|
#include "inode_mark.h"
|
|
#include "process_management.h"
|
|
#include "utils.h"
|
|
|
|
#define NUM_BITS 8 // 128 buckets in hash table
|
|
|
|
static DEFINE_HASHTABLE(process_setuid_policy_hashtable, NUM_BITS);
|
|
|
|
/*
|
|
* Bool signifying whether to disable fixups for process management related
|
|
* routines in the kernel (setuid, setgid, kill). Default value is false. Can
|
|
* be overridden by 'disable_process_management_policies' flag. Static vars get
|
|
* initialized to 0/false since in BSS.
|
|
**/
|
|
static bool disable_process_management_policies;
|
|
|
|
/* Disable process management policies if flag passed */
|
|
static int set_disable_process_management_policies(char *str)
|
|
{
|
|
disable_process_management_policies = true;
|
|
return 1;
|
|
}
|
|
__setup("disable_process_management_policies=",
|
|
set_disable_process_management_policies);
|
|
|
|
/*
|
|
* Hash table entry to store process management policy signifying that 'parent'
|
|
* user can use 'child' user for process management (for now that just means
|
|
* 'parent' can set*uid() to 'child'). Will be adding exceptions for set*gid()
|
|
* and kill() in the future.
|
|
*/
|
|
struct entry {
|
|
struct hlist_node next;
|
|
struct hlist_node dlist; /* for deletion cleanup */
|
|
uint64_t parent_kuid;
|
|
uint64_t child_kuid;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
static int chromiumos_security_sb_mount(const char *dev_name, struct path *path,
|
|
const char *type, unsigned long flags,
|
|
void *data)
|
|
{
|
|
int error = current->total_link_count ? -ELOOP : 0;
|
|
|
|
if (error) {
|
|
char *cmdline;
|
|
|
|
cmdline = printable_cmdline(current);
|
|
pr_notice("Mount path with symlinks prohibited - "
|
|
"pid=%d cmdline=%s\n",
|
|
task_pid_nr(current), cmdline);
|
|
kfree(cmdline);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
static void report_load_module(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);
|
|
kfree(alloced);
|
|
alloced = pathname;
|
|
}
|
|
}
|
|
}
|
|
|
|
cmdline = printable_cmdline(current);
|
|
|
|
pr_notice("init_module %s module=%s pid=%d cmdline=%s\n",
|
|
operation, pathname, task_pid_nr(current), cmdline);
|
|
|
|
kfree(cmdline);
|
|
kfree(alloced);
|
|
}
|
|
|
|
static int module_locking = 1;
|
|
static struct dentry *locked_root;
|
|
static DEFINE_SPINLOCK(locked_root_spinlock);
|
|
static DEFINE_SPINLOCK(process_setuid_policy_hashtable_spinlock);
|
|
static DEFINE_SPINLOCK(sb_nosymfollow_hashtable_spinlock);
|
|
|
|
#ifdef CONFIG_SYSCTL
|
|
static int zero;
|
|
static int one = 1;
|
|
|
|
static struct ctl_path chromiumos_sysctl_path[] = {
|
|
{ .procname = "kernel", },
|
|
{ .procname = "chromiumos", },
|
|
{ }
|
|
};
|
|
|
|
static struct ctl_table chromiumos_sysctl_table[] = {
|
|
{
|
|
.procname = "module_locking",
|
|
.data = &module_locking,
|
|
.maxlen = sizeof(int),
|
|
.mode = 0644,
|
|
.proc_handler = proc_dointvec_minmax,
|
|
.extra1 = &zero,
|
|
.extra2 = &one,
|
|
},
|
|
{ }
|
|
};
|
|
|
|
/* Check if the root device is read-only (e.g. dm-verity is enabled).
|
|
* This must be called after early kernel init, since then the rootdev
|
|
* is available.
|
|
*/
|
|
static bool rootdev_readonly(void)
|
|
{
|
|
bool rc;
|
|
struct block_device *bdev;
|
|
const fmode_t mode = FMODE_WRITE;
|
|
|
|
bdev = blkdev_get_by_dev(ROOT_DEV, mode, NULL);
|
|
if (IS_ERR(bdev)) {
|
|
/* In this weird case, assume it is read-only. */
|
|
pr_info("dev(%u,%u): FMODE_WRITE disallowed?!\n",
|
|
MAJOR(ROOT_DEV), MINOR(ROOT_DEV));
|
|
return true;
|
|
}
|
|
|
|
rc = bdev_read_only(bdev);
|
|
blkdev_put(bdev, mode);
|
|
|
|
pr_info("dev(%u,%u): %s\n", MAJOR(ROOT_DEV), MINOR(ROOT_DEV),
|
|
rc ? "read-only" : "writable");
|
|
|
|
return rc;
|
|
}
|
|
|
|
static void check_locking_enforcement(void)
|
|
{
|
|
/* If module locking is not being enforced, allow sysctl to change
|
|
* modes for testing.
|
|
*/
|
|
if (!rootdev_readonly()) {
|
|
if (!register_sysctl_paths(chromiumos_sysctl_path,
|
|
chromiumos_sysctl_table))
|
|
pr_notice("sysctl registration failed!\n");
|
|
else
|
|
pr_info("module locking can be disabled.\n");
|
|
} else
|
|
pr_info("module locking engaged.\n");
|
|
}
|
|
#else
|
|
static void check_locking_enforcement(void) { }
|
|
#endif
|
|
|
|
/* 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;
|
|
}
|
|
|
|
static int chromiumos_security_load_module(struct file *file)
|
|
{
|
|
struct dentry *module_root;
|
|
|
|
if (!file) {
|
|
if (!module_locking) {
|
|
report_load_module(NULL, "old-api-locking-ignored");
|
|
return 0;
|
|
}
|
|
|
|
report_load_module(NULL, "old-api-denied");
|
|
return -EPERM;
|
|
}
|
|
|
|
module_root = file->f_path.mnt->mnt_root;
|
|
|
|
/* First loaded module defines the root for all others. */
|
|
spin_lock(&locked_root_spinlock);
|
|
if (!locked_root) {
|
|
locked_root = dget(module_root);
|
|
/*
|
|
* Unlock now since it's only locked_root we care about.
|
|
* In the worst case, we will (correctly) report locking
|
|
* failures before we have announced that locking is
|
|
* enabled. This would be purely cosmetic.
|
|
*/
|
|
spin_unlock(&locked_root_spinlock);
|
|
report_load_module(&file->f_path, "locked");
|
|
check_locking_enforcement();
|
|
} else {
|
|
spin_unlock(&locked_root_spinlock);
|
|
}
|
|
|
|
if (module_root != locked_root) {
|
|
if (unlikely(!module_locking)) {
|
|
report_load_module(&file->f_path, "locking-ignored");
|
|
return 0;
|
|
}
|
|
|
|
report_load_module(&file->f_path, "denied");
|
|
return -EPERM;
|
|
}
|
|
|
|
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 nameidata *nd)
|
|
{
|
|
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,
|
|
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;
|
|
}
|
|
|
|
int chromiumos_security_file_open(
|
|
struct file *file,
|
|
const struct cred *cred)
|
|
{
|
|
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,
|
|
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;
|
|
}
|
|
|
|
bool chromiumos_check_setuid_policy_hashtable_key(kuid_t parent)
|
|
{
|
|
struct entry *entry;
|
|
|
|
rcu_read_lock();
|
|
hash_for_each_possible_rcu(process_setuid_policy_hashtable,
|
|
entry, next, __kuid_val(parent)) {
|
|
if (entry->parent_kuid == __kuid_val(parent)) {
|
|
rcu_read_unlock();
|
|
return true;
|
|
}
|
|
}
|
|
rcu_read_unlock();
|
|
|
|
/*
|
|
* Using RCU, its possible that a policy gets added in between the time
|
|
* we check above and when we return false here. This is fine, since
|
|
* policy updates only happen during system startup, well before
|
|
* sandboxed system services start running and the policies need to be
|
|
* queried.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
bool chromiumos_check_setuid_policy_hashtable_key_value(kuid_t parent,
|
|
kuid_t child)
|
|
{
|
|
struct entry *entry;
|
|
|
|
rcu_read_lock();
|
|
hash_for_each_possible_rcu(process_setuid_policy_hashtable,
|
|
entry, next, __kuid_val(parent)) {
|
|
if (entry->parent_kuid == __kuid_val(parent) &&
|
|
entry->child_kuid == __kuid_val(child)) {
|
|
rcu_read_unlock();
|
|
return true;
|
|
}
|
|
}
|
|
rcu_read_unlock();
|
|
|
|
/*
|
|
* Using RCU, its possible that a policy gets added in between the time
|
|
* we check above and when we return false here. This is fine, since
|
|
* policy updates only happen during system startup, well before
|
|
* sandboxed system services start running and the policies need to be
|
|
* queried.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
bool setuid_syscall(int num)
|
|
{
|
|
#ifdef CONFIG_X86_64
|
|
if (!(num == __NR_setreuid ||
|
|
num == __NR_setuid ||
|
|
num == __NR_setresuid ||
|
|
num == __NR_setfsuid))
|
|
return false;
|
|
#elif defined CONFIG_ARM64
|
|
if (!(num == __NR_compat_setuid ||
|
|
num == __NR_compat_setreuid ||
|
|
num == __NR_compat_setfsuid ||
|
|
num == __NR_compat_setresuid ||
|
|
num == __NR_compat_setreuid32 ||
|
|
num == __NR_compat_setresuid32 ||
|
|
num == __NR_compat_setuid32 ||
|
|
num == __NR_compat_setfsuid32))
|
|
return false;
|
|
#else /* CONFIG_ARM */
|
|
if (!(num == __NR_setreuid32 ||
|
|
num == __NR_setuid32 ||
|
|
num == __NR_setresuid32 ||
|
|
num == __NR_setfsuid32))
|
|
return false;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
int chromiumos_security_capable(const struct cred *cred,
|
|
struct user_namespace *ns,
|
|
int cap)
|
|
{
|
|
/* The current->mm check will fail if this is a kernel thread. */
|
|
if (!disable_process_management_policies &&
|
|
cap == CAP_SETUID &&
|
|
current->mm &&
|
|
chromiumos_check_setuid_policy_hashtable_key(cred->uid)) {
|
|
// syscall_get_nr can theoretically return 0 or -1, but that
|
|
// would signify that the syscall is being aborted due to a
|
|
// signal, so we don't need to check for this case here.
|
|
if (!(setuid_syscall(syscall_get_nr(current,
|
|
current_pt_regs())))) {
|
|
// Deny if we're not in a set*uid() syscall to avoid
|
|
// giving powers gated by CAP_SETUID that are related
|
|
// to functionality other than calling set*uid() (e.g.
|
|
// allowing user to set up userns uid mappings).
|
|
WARN(1,
|
|
"Operation requires CAP_SETUID, which is not available to UID %u for operations besides approved set*uid transitions\n",
|
|
__kuid_val(cred->uid));
|
|
return -1;
|
|
}
|
|
}
|
|
return 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;
|
|
}
|
|
|
|
/*
|
|
* Emit a warning when no entry found in whitelist. These will show up in
|
|
* kernel warning reports collected by the crash reporter, so we have some
|
|
* insight regarding failures that need addressing.
|
|
*/
|
|
void chromiumos_setuid_policy_warning(kuid_t parent, kuid_t child)
|
|
{
|
|
WARN(1,
|
|
"UID %u is restricted to using certain whitelisted UIDs for process management, and %u is not in the whitelist.\n",
|
|
__kuid_val(parent),
|
|
__kuid_val(child));
|
|
}
|
|
|
|
bool chromiumos_uid_transition_allowed(kuid_t parent, kuid_t child)
|
|
{
|
|
if (chromiumos_check_setuid_policy_hashtable_key_value(parent, child))
|
|
return true;
|
|
chromiumos_setuid_policy_warning(parent, child);
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Check whether there is either an exception for user under old cred struct to
|
|
* use user under new cred struct, or the UID transition is allowed (by Linux
|
|
* set*uid rules) even without CAP_SETUID.
|
|
*/
|
|
int chromiumos_security_task_fix_setuid(struct cred *new,
|
|
const struct cred *old, int flags)
|
|
{
|
|
/*
|
|
* Do nothing if feature is turned off by kernel compile flag or there
|
|
* are no setuid restrictions for this UID.
|
|
*/
|
|
if (disable_process_management_policies ||
|
|
!chromiumos_check_setuid_policy_hashtable_key(old->uid))
|
|
return 0;
|
|
|
|
switch (flags) {
|
|
case LSM_SETID_RE:
|
|
/*
|
|
* Users for which setuid restrictions exist can only set the
|
|
* real UID to the real UID or the effective UID, unless an
|
|
* explicit whitelist policy allows the transition.
|
|
*/
|
|
if (!uid_eq(old->uid, new->uid) &&
|
|
!uid_eq(old->euid, new->uid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->uid,
|
|
new->uid))
|
|
return -EPERM;
|
|
}
|
|
/*
|
|
* Users for which setuid restrictions exist can only set the
|
|
* effective UID to the real UID, the effective UID, or the
|
|
* saved set-UID, unless an explicit whitelist policy allows
|
|
* the transition.
|
|
*/
|
|
if (!uid_eq(old->uid, new->euid) &&
|
|
!uid_eq(old->euid, new->euid) &&
|
|
!uid_eq(old->suid, new->euid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->euid,
|
|
new->euid))
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
case LSM_SETID_ID:
|
|
/*
|
|
* Users for which setuid restrictions exist cannot change the
|
|
* real UID or saved set-UID unless an explicit whitelist
|
|
* policy allows the transition.
|
|
*/
|
|
if (!uid_eq(old->uid, new->uid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->uid,
|
|
new->uid))
|
|
return -EPERM;
|
|
}
|
|
if (!uid_eq(old->suid, new->suid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->suid,
|
|
new->suid))
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
case LSM_SETID_RES:
|
|
/*
|
|
* Users for which setuid restrictions exist cannot change the
|
|
* real UID, effective UID, or saved set-UID to anything but
|
|
* one of: the current real UID, the current effective UID or
|
|
* the current saved set-user-ID unless an explicit whitelist
|
|
* policy allows the transition.
|
|
*/
|
|
if (!uid_eq(new->uid, old->uid) &&
|
|
!uid_eq(new->uid, old->euid) &&
|
|
!uid_eq(new->uid, old->suid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->uid,
|
|
new->uid))
|
|
return -EPERM;
|
|
}
|
|
if (!uid_eq(new->euid, old->uid) &&
|
|
!uid_eq(new->euid, old->euid) &&
|
|
!uid_eq(new->euid, old->suid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->euid,
|
|
new->euid))
|
|
return -EPERM;
|
|
}
|
|
if (!uid_eq(new->suid, old->uid) &&
|
|
!uid_eq(new->suid, old->euid) &&
|
|
!uid_eq(new->suid, old->suid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->suid,
|
|
new->suid))
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
case LSM_SETID_FS:
|
|
/*
|
|
* Users for which setuid restrictions exist cannot change the
|
|
* filesystem UID to anything but one of: the current real UID,
|
|
* the current effective UID or the current saved set-UID
|
|
* unless an explicit whitelist policy allows the transition.
|
|
*/
|
|
if (!uid_eq(new->fsuid, old->uid) &&
|
|
!uid_eq(new->fsuid, old->euid) &&
|
|
!uid_eq(new->fsuid, old->suid) &&
|
|
!uid_eq(new->fsuid, old->fsuid)) {
|
|
if (!chromiumos_uid_transition_allowed(old->fsuid,
|
|
new->fsuid))
|
|
return -EPERM;
|
|
}
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Add process management policy to hash table */
|
|
int chromiumos_add_process_management_entry(kuid_t parent, kuid_t child)
|
|
{
|
|
struct entry *new;
|
|
|
|
/* Return if entry already exists */
|
|
if (chromiumos_check_setuid_policy_hashtable_key_value(parent,
|
|
child))
|
|
return 0;
|
|
|
|
new = kzalloc(sizeof(struct entry), GFP_KERNEL);
|
|
if (!new)
|
|
return -ENOMEM;
|
|
new->parent_kuid = __kuid_val(parent);
|
|
new->child_kuid = __kuid_val(child);
|
|
spin_lock(&process_setuid_policy_hashtable_spinlock);
|
|
hash_add_rcu(process_setuid_policy_hashtable,
|
|
&new->next,
|
|
__kuid_val(parent));
|
|
spin_unlock(&process_setuid_policy_hashtable_spinlock);
|
|
return 0;
|
|
}
|
|
|
|
void chromiumos_flush_process_management_entries(void)
|
|
{
|
|
struct 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.
|
|
*/
|
|
hash_for_each_safe(process_setuid_policy_hashtable, bkt_loop_cursor,
|
|
hlist_node, entry, next) {
|
|
spin_lock(&process_setuid_policy_hashtable_spinlock);
|
|
hash_del_rcu(&entry->next);
|
|
spin_unlock(&process_setuid_policy_hashtable_spinlock);
|
|
hlist_add_head(&entry->dlist, &free_list);
|
|
}
|
|
synchronize_rcu();
|
|
hlist_for_each_entry_safe(entry, hlist_node, &free_list, dlist)
|
|
kfree(entry);
|
|
}
|
|
|
|
static struct security_operations chromiumos_security_ops = {
|
|
.name = "chromiumos",
|
|
.sb_mount = chromiumos_security_sb_mount,
|
|
.kernel_module_from_file = chromiumos_security_load_module,
|
|
.inode_follow_link = chromiumos_security_inode_follow_link,
|
|
.file_open = chromiumos_security_file_open,
|
|
.sb_umount = chromiumos_security_sb_umount,
|
|
};
|
|
|
|
/* 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 int __init chromiumos_security_init(void)
|
|
{
|
|
int error;
|
|
|
|
error = register_security(&chromiumos_security_ops);
|
|
|
|
if (error)
|
|
panic("Could not register Chromium OS security module");
|
|
|
|
return error;
|
|
}
|
|
security_initcall(chromiumos_security_init);
|
|
|
|
/* Should not be mutable after boot, so not listed in sysfs (perm == 0). */
|
|
module_param(module_locking, int, 0);
|
|
MODULE_PARM_DESC(module_locking, "Module loading restrictions (default: true)");
|