c05564c4d8
Android 13
1140 lines
27 KiB
C
Executable file
1140 lines
27 KiB
C
Executable file
/*
|
|
* This code is based on IMA's code
|
|
*
|
|
* Copyright (C) 2016 Samsung Electronics, Inc.
|
|
*
|
|
* Egor Uleyskiy, <e.uleyskiy@samsung.com>
|
|
* Viacheslav Vovchenko <v.vovchenko@samsung.com>
|
|
* Yevgen Kopylov <y.kopylov@samsung.com>
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/file.h>
|
|
#include <linux/binfmts.h>
|
|
#include <linux/mount.h>
|
|
#include <linux/mman.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/xattr.h>
|
|
#include <crypto/hash_info.h>
|
|
#include <linux/ptrace.h>
|
|
#include <linux/task_integrity.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/shmem_fs.h>
|
|
|
|
#include "five.h"
|
|
#include "five_audit.h"
|
|
#include "five_hooks.h"
|
|
#include "five_state.h"
|
|
#include "five_pa.h"
|
|
#include "five_porting.h"
|
|
#include "five_cache.h"
|
|
#include "five_dmverity.h"
|
|
#include "five_dsms.h"
|
|
#include "five_tint_dev.h"
|
|
|
|
static const bool unlink_on_error; // false
|
|
|
|
static const bool check_dex2oat_binary = true;
|
|
static const bool check_memfd_file = true;
|
|
|
|
static struct file *memfd_file __ro_after_init;
|
|
|
|
static struct workqueue_struct *g_five_workqueue;
|
|
|
|
static inline void task_integrity_processing(struct task_integrity *tint);
|
|
static inline void task_integrity_done(struct task_integrity *tint);
|
|
static void process_measurement(const struct processing_event_list *params);
|
|
static inline struct processing_event_list *five_event_create(
|
|
enum five_event event, struct task_struct *task,
|
|
struct file *file, int function, gfp_t flags);
|
|
static inline void five_event_destroy(
|
|
const struct processing_event_list *file);
|
|
|
|
#ifdef CONFIG_FIVE_DEBUG
|
|
static int five_enabled = 1;
|
|
|
|
static ssize_t five_enabled_write(struct file *file, const char __user *buf,
|
|
size_t count, loff_t *pos)
|
|
{
|
|
char command;
|
|
|
|
if (get_user(command, buf))
|
|
return -EFAULT;
|
|
|
|
switch (command) {
|
|
case '0':
|
|
five_enabled = 0;
|
|
break;
|
|
case '1':
|
|
five_enabled = 1;
|
|
break;
|
|
default:
|
|
pr_err("FIVE: %s: unknown cmd: %hhx\n", __func__, command);
|
|
return -EINVAL;
|
|
}
|
|
|
|
pr_info("FIVE debug: FIVE %s\n", five_enabled ? "enabled" : "disabled");
|
|
return count;
|
|
}
|
|
|
|
static ssize_t five_enabled_read(struct file *file, char __user *user_buf,
|
|
size_t count, loff_t *pos)
|
|
{
|
|
char buf[2];
|
|
|
|
buf[0] = five_enabled ? '1' : '0';
|
|
buf[1] = '\n';
|
|
|
|
return simple_read_from_buffer(user_buf, count, pos, buf, sizeof(buf));
|
|
}
|
|
|
|
static const struct file_operations five_enabled_fops = {
|
|
.owner = THIS_MODULE,
|
|
.read = five_enabled_read,
|
|
.write = five_enabled_write
|
|
};
|
|
|
|
static int __init init_fs(void)
|
|
{
|
|
struct dentry *debug_file = NULL;
|
|
umode_t umode = (S_IRUGO | S_IWUSR | S_IWGRP);
|
|
|
|
debug_file = debugfs_create_file(
|
|
"five_enabled", umode, NULL, NULL, &five_enabled_fops);
|
|
if (IS_ERR_OR_NULL(debug_file))
|
|
goto error;
|
|
|
|
return 0;
|
|
error:
|
|
if (debug_file)
|
|
return -PTR_ERR(debug_file);
|
|
|
|
return -EEXIST;
|
|
}
|
|
|
|
static inline int is_five_enabled(void)
|
|
{
|
|
return five_enabled;
|
|
}
|
|
|
|
int five_fcntl_debug(struct file *file, void __user *argp)
|
|
{
|
|
struct inode *inode;
|
|
struct five_stat stat = {0};
|
|
struct integrity_iint_cache *iint;
|
|
|
|
if (unlikely(!file || !argp))
|
|
return -EINVAL;
|
|
|
|
inode = file_inode(file);
|
|
|
|
inode_lock(inode);
|
|
iint = integrity_inode_get(inode);
|
|
if (unlikely(!iint)) {
|
|
inode_unlock(inode);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
stat.cache_status = five_get_cache_status(iint);
|
|
stat.cache_iversion = inode_query_iversion(iint->inode);
|
|
stat.inode_iversion = inode_query_iversion(inode);
|
|
|
|
inode_unlock(inode);
|
|
|
|
if (unlikely(copy_to_user(argp, &stat, sizeof(stat))))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
#else
|
|
static int __init init_fs(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static inline int is_five_enabled(void)
|
|
{
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
static void work_handler(struct work_struct *in_data)
|
|
{
|
|
struct worker_context *context = container_of(in_data,
|
|
struct worker_context, data_work);
|
|
struct task_integrity *intg;
|
|
|
|
if (unlikely(!context))
|
|
return;
|
|
|
|
intg = context->tint;
|
|
|
|
spin_lock(&intg->list_lock);
|
|
while (!list_empty(&(intg->events.list))) {
|
|
struct processing_event_list *five_file;
|
|
|
|
five_file = list_entry(intg->events.list.next,
|
|
struct processing_event_list, list);
|
|
spin_unlock(&intg->list_lock);
|
|
switch (five_file->event) {
|
|
case FIVE_VERIFY_BUNCH_FILES: {
|
|
process_measurement(five_file);
|
|
break;
|
|
}
|
|
case FIVE_RESET_INTEGRITY: {
|
|
task_integrity_reset(intg);
|
|
five_hook_integrity_reset(five_file->task,
|
|
NULL, CAUSE_UNKNOWN);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
spin_lock(&intg->list_lock);
|
|
list_del(&five_file->list);
|
|
five_event_destroy(five_file);
|
|
}
|
|
|
|
task_integrity_done(intg);
|
|
spin_unlock(&intg->list_lock);
|
|
task_integrity_put(intg);
|
|
|
|
kfree(context);
|
|
}
|
|
|
|
static void fix_dpath(const struct path *path, char *pathbuf, char *pathname)
|
|
{
|
|
/* `d_path' appends " (deleted)" string if a file is unlinked. Below
|
|
* code removes it.
|
|
* `d_path' fills the buffer from the end of it therefore we can easily
|
|
* calculate the length of the pathname.
|
|
*/
|
|
const long pathname_size = pathbuf + PATH_MAX - pathname;
|
|
const char str_deleted[] = " (deleted)";
|
|
const int deleted_size = sizeof(str_deleted);
|
|
|
|
if (pathname_size > deleted_size && d_unlinked(path->dentry)) {
|
|
char *start_deleted = pathbuf + PATH_MAX - deleted_size;
|
|
|
|
if (!strncmp(str_deleted, start_deleted, deleted_size))
|
|
*start_deleted = '\0';
|
|
}
|
|
}
|
|
|
|
const char *five_d_path(const struct path *path, char **pathbuf, char *namebuf)
|
|
{
|
|
char *pathname = NULL;
|
|
|
|
*pathbuf = __getname();
|
|
if (*pathbuf) {
|
|
pathname = d_path(path, *pathbuf, PATH_MAX);
|
|
if (IS_ERR(pathname)) {
|
|
__putname(*pathbuf);
|
|
*pathbuf = NULL;
|
|
pathname = NULL;
|
|
}
|
|
fix_dpath(path, *pathbuf, pathname);
|
|
}
|
|
|
|
if (!pathname) {
|
|
strlcpy(namebuf, path->dentry->d_name.name, NAME_MAX);
|
|
pathname = namebuf;
|
|
}
|
|
|
|
return pathname;
|
|
}
|
|
|
|
int five_check_params(struct task_struct *task, struct file *file)
|
|
{
|
|
struct inode *inode;
|
|
|
|
if (unlikely(!file))
|
|
return 1;
|
|
|
|
inode = file_inode(file);
|
|
if (!S_ISREG(inode->i_mode))
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* cut a list into two
|
|
* @cur_list: a list with entries
|
|
* @new_list: a new list to add all removed entries. Should be an empty list
|
|
*
|
|
* Use it under spin_lock
|
|
*
|
|
* The function moves second entry and following entries to new list.
|
|
* First entry is left in cur_list.
|
|
*
|
|
* Initial state:
|
|
* [cur_list]<=>[first]<=>[second]<=>[third]<=>...<=>[last] [new_list]
|
|
* ^=================================================^ ^====^
|
|
* Result:
|
|
* [cur_list]<=>[first]
|
|
* ^==========^
|
|
* [new_list]<=>[second]<=>[third]<=>...<=>[last]
|
|
* ^=====================================^
|
|
*
|
|
* the function is similar to kernel list_cut_position, but there are few
|
|
* differences:
|
|
* - cut position is second entry
|
|
* - original list is left with only first entry
|
|
* - moving entries are from second entry to last entry
|
|
*/
|
|
static void list_cut_tail(struct list_head *cur_list,
|
|
struct list_head *new_list)
|
|
{
|
|
if ((!list_empty(cur_list)) &&
|
|
(!list_is_singular(cur_list))) {
|
|
new_list->next = cur_list->next->next;
|
|
cur_list->next->next->prev = new_list;
|
|
cur_list->next->next = cur_list;
|
|
new_list->prev = cur_list->prev;
|
|
cur_list->prev->next = new_list;
|
|
cur_list->prev = cur_list->next;
|
|
}
|
|
}
|
|
|
|
static void free_files_list(struct list_head *list)
|
|
{
|
|
struct list_head *tmp, *list_entry;
|
|
struct processing_event_list *file_entry;
|
|
|
|
list_for_each_safe(list_entry, tmp, list) {
|
|
file_entry = list_entry(list_entry,
|
|
struct processing_event_list, list);
|
|
list_del(&file_entry->list);
|
|
five_event_destroy(file_entry);
|
|
}
|
|
}
|
|
|
|
static int push_file_event_bunch(struct task_struct *task, struct file *file,
|
|
int function)
|
|
{
|
|
int rc = 0;
|
|
struct worker_context *context;
|
|
struct processing_event_list *five_file;
|
|
|
|
if (unlikely(!is_five_enabled()) || five_check_params(task, file))
|
|
return 0;
|
|
|
|
context = kmalloc(sizeof(struct worker_context), GFP_KERNEL);
|
|
if (unlikely(!context))
|
|
return -ENOMEM;
|
|
|
|
five_file = five_event_create(FIVE_VERIFY_BUNCH_FILES, task, file,
|
|
function, GFP_KERNEL);
|
|
if (unlikely(!five_file)) {
|
|
kfree(context);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
spin_lock(&TASK_INTEGRITY(task)->list_lock);
|
|
|
|
if (list_empty(&(TASK_INTEGRITY(task)->events.list))) {
|
|
task_integrity_get(TASK_INTEGRITY(task));
|
|
task_integrity_processing(TASK_INTEGRITY(task));
|
|
|
|
context->tint = TASK_INTEGRITY(task);
|
|
|
|
list_add_tail(&five_file->list, &TASK_INTEGRITY(task)->events.list);
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
INIT_WORK(&context->data_work, work_handler);
|
|
rc = queue_work(g_five_workqueue, &context->data_work) ? 0 : 1;
|
|
} else {
|
|
struct list_head dead_list;
|
|
|
|
INIT_LIST_HEAD(&dead_list);
|
|
if ((function == BPRM_CHECK) &&
|
|
(!list_is_singular(&(TASK_INTEGRITY(task)->events.list)))) {
|
|
list_cut_tail(&TASK_INTEGRITY(task)->events.list,
|
|
&dead_list);
|
|
}
|
|
list_add_tail(&five_file->list, &TASK_INTEGRITY(task)->events.list);
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
free_files_list(&dead_list);
|
|
kfree(context);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int push_reset_event(struct task_struct *task,
|
|
enum task_integrity_reset_cause cause, struct file *file)
|
|
{
|
|
struct list_head dead_list;
|
|
struct task_integrity *current_tint;
|
|
struct processing_event_list *five_reset;
|
|
|
|
if (unlikely(!is_five_enabled()))
|
|
return 0;
|
|
|
|
INIT_LIST_HEAD(&dead_list);
|
|
current_tint = TASK_INTEGRITY(task);
|
|
task_integrity_get(current_tint);
|
|
|
|
task_integrity_set_reset_reason(current_tint, cause, file);
|
|
|
|
five_reset = five_event_create(FIVE_RESET_INTEGRITY, task, NULL, 0,
|
|
GFP_KERNEL);
|
|
if (unlikely(!five_reset)) {
|
|
task_integrity_reset_both(current_tint);
|
|
five_hook_integrity_reset(task, file, cause);
|
|
task_integrity_put(current_tint);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
task_integrity_reset_both(current_tint);
|
|
five_hook_integrity_reset(task, file, cause);
|
|
spin_lock(¤t_tint->list_lock);
|
|
if (!list_empty(¤t_tint->events.list)) {
|
|
list_cut_tail(¤t_tint->events.list, &dead_list);
|
|
five_reset->event = FIVE_RESET_INTEGRITY;
|
|
list_add_tail(&five_reset->list, ¤t_tint->events.list);
|
|
spin_unlock(¤t_tint->list_lock);
|
|
} else {
|
|
spin_unlock(¤t_tint->list_lock);
|
|
five_event_destroy(five_reset);
|
|
}
|
|
|
|
task_integrity_put(current_tint);
|
|
/* remove dead_list */
|
|
free_files_list(&dead_list);
|
|
return 0;
|
|
}
|
|
|
|
void task_integrity_delayed_reset(struct task_struct *task,
|
|
enum task_integrity_reset_cause cause, struct file *file)
|
|
{
|
|
push_reset_event(task, cause, file);
|
|
}
|
|
|
|
static void five_check_last_writer(struct integrity_iint_cache *iint,
|
|
struct inode *inode, struct file *file)
|
|
{
|
|
fmode_t mode = file->f_mode;
|
|
|
|
if (!(mode & FMODE_WRITE))
|
|
return;
|
|
|
|
inode_lock(inode);
|
|
if (atomic_read(&inode->i_writecount) == 1) {
|
|
if (!inode_eq_iversion(inode, iint->version))
|
|
five_set_cache_status(iint, FIVE_FILE_UNKNOWN);
|
|
}
|
|
iint->five_signing = false;
|
|
inode_unlock(inode);
|
|
}
|
|
|
|
/**
|
|
* five_file_free - called on __fput()
|
|
* @file: pointer to file structure being freed
|
|
*
|
|
* Flag files that changed, based on i_version
|
|
*/
|
|
void five_file_free(struct file *file)
|
|
{
|
|
struct inode *inode = file_inode(file);
|
|
struct integrity_iint_cache *iint;
|
|
|
|
if (!S_ISREG(inode->i_mode))
|
|
return;
|
|
|
|
fivepa_fsignature_free(file);
|
|
|
|
iint = integrity_iint_find(inode);
|
|
if (!iint)
|
|
return;
|
|
|
|
five_check_last_writer(iint, inode, file);
|
|
}
|
|
|
|
void five_task_free(struct task_struct *task)
|
|
{
|
|
task_integrity_put(TASK_INTEGRITY(task));
|
|
}
|
|
|
|
/* Returns string representation of input function */
|
|
const char *five_get_string_fn(enum five_hooks fn)
|
|
{
|
|
switch (fn) {
|
|
case FILE_CHECK:
|
|
return "file-check";
|
|
case MMAP_CHECK:
|
|
return "mmap-check";
|
|
case BPRM_CHECK:
|
|
return "bprm-check";
|
|
case POST_SETATTR:
|
|
return "post-setattr";
|
|
}
|
|
return "unknown-function";
|
|
}
|
|
|
|
static inline bool is_dex2oat_binary(const struct file *file)
|
|
{
|
|
const char *pathname = NULL;
|
|
char *pathbuf = NULL;
|
|
const char * const dex2oat_full_path[] = {
|
|
/* R OS */
|
|
"/apex/com.android.art/bin/dex2oat",
|
|
"/apex/com.android.art/bin/dex2oat32",
|
|
"/apex/com.android.art/bin/dex2oat64",
|
|
/* Q OS */
|
|
"/apex/com.android.runtime/bin/dex2oat"
|
|
};
|
|
char filename[NAME_MAX];
|
|
bool res = false;
|
|
size_t i;
|
|
|
|
if (!file || !file->f_path.dentry)
|
|
return false;
|
|
|
|
if (strncmp(file->f_path.dentry->d_iname, "dex2oat",
|
|
sizeof("dex2oat")) &&
|
|
strncmp(file->f_path.dentry->d_iname, "dex2oat32",
|
|
sizeof("dex2oat32")) &&
|
|
strncmp(file->f_path.dentry->d_iname, "dex2oat64",
|
|
sizeof("dex2oat64")))
|
|
return false;
|
|
|
|
pathname = five_d_path(&file->f_path, &pathbuf, filename);
|
|
|
|
for (i = 0; i < ARRAY_SIZE(dex2oat_full_path); ++i) {
|
|
if (!strncmp(pathname, dex2oat_full_path[i],
|
|
strlen(dex2oat_full_path[i]) + 1)) {
|
|
res = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pathbuf)
|
|
__putname(pathbuf);
|
|
|
|
return res;
|
|
}
|
|
|
|
static inline bool match_trusted_executable(const struct five_cert *cert,
|
|
const struct integrity_iint_cache *iint,
|
|
const struct file *file)
|
|
{
|
|
const struct five_cert_header *hdr = NULL;
|
|
|
|
if (!cert)
|
|
return check_dex2oat_binary && is_dex2oat_binary(file);
|
|
|
|
if (five_get_cache_status(iint) != FIVE_FILE_RSA)
|
|
return false;
|
|
|
|
hdr = (const struct five_cert_header *)cert->body.header->value;
|
|
|
|
if (hdr->privilege == FIVE_PRIV_ALLOW_SIGN)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static inline void task_integrity_processing(struct task_integrity *tint)
|
|
{
|
|
tint->user_value = INTEGRITY_PROCESSING;
|
|
}
|
|
|
|
static inline void task_integrity_done(struct task_integrity *tint)
|
|
{
|
|
tint->user_value = task_integrity_read(tint);
|
|
}
|
|
|
|
static void process_file(struct task_struct *task,
|
|
struct file *file,
|
|
int function,
|
|
struct file_verification_result *result)
|
|
{
|
|
struct inode *inode = d_real_inode(file_dentry(file));
|
|
struct integrity_iint_cache *iint = NULL;
|
|
struct five_cert cert = { {0} };
|
|
struct five_cert *pcert = NULL;
|
|
int rc = -ENOMEM;
|
|
char *xattr_value = NULL;
|
|
int xattr_len = 0;
|
|
|
|
if (!S_ISREG(inode->i_mode)) {
|
|
rc = 0;
|
|
goto out;
|
|
}
|
|
|
|
iint = integrity_inode_get(inode);
|
|
if (!iint)
|
|
goto out;
|
|
|
|
/* Nothing to do, just return existing appraised status */
|
|
if (five_get_cache_status(iint) != FIVE_FILE_UNKNOWN) {
|
|
rc = 0;
|
|
goto out;
|
|
}
|
|
|
|
xattr_len = five_read_xattr(d_real_comp(file->f_path.dentry),
|
|
&xattr_value);
|
|
if (xattr_value && xattr_len) {
|
|
rc = five_cert_fillout(&cert, xattr_value, xattr_len);
|
|
if (rc) {
|
|
pr_err("FIVE: certificate is incorrect inode=%lu\n",
|
|
inode->i_ino);
|
|
goto out;
|
|
}
|
|
|
|
pcert = &cert;
|
|
|
|
if (file->f_flags & O_DIRECT) {
|
|
rc = -EACCES;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
rc = five_appraise_measurement(task, function, iint, file, pcert);
|
|
if (!rc && match_trusted_executable(pcert, iint, file))
|
|
iint->five_flags |= FIVE_TRUSTED_FILE;
|
|
|
|
out:
|
|
if (rc && iint)
|
|
iint->five_flags &= ~FIVE_TRUSTED_FILE;
|
|
|
|
result->file = file;
|
|
result->task = task;
|
|
result->iint = iint;
|
|
result->fn = function;
|
|
result->xattr = xattr_value;
|
|
result->xattr_len = (size_t)xattr_len;
|
|
|
|
if (!iint || five_get_cache_status(iint) == FIVE_FILE_UNKNOWN
|
|
|| five_get_cache_status(iint) == FIVE_FILE_FAIL)
|
|
result->five_result = 1;
|
|
else
|
|
result->five_result = 0;
|
|
}
|
|
|
|
static void process_measurement(const struct processing_event_list *params)
|
|
{
|
|
struct task_struct *task = params->task;
|
|
struct task_integrity *integrity = TASK_INTEGRITY(task);
|
|
struct file *file = params->file;
|
|
struct inode *inode = file_inode(file);
|
|
int function = params->function;
|
|
struct file_verification_result file_result;
|
|
|
|
if (function != BPRM_CHECK) {
|
|
if (task_integrity_read(integrity) == INTEGRITY_NONE)
|
|
return;
|
|
}
|
|
|
|
file_verification_result_init(&file_result);
|
|
inode_lock(inode);
|
|
|
|
process_file(task, file, function, &file_result);
|
|
|
|
five_hook_file_processed(task, file,
|
|
file_result.xattr, file_result.xattr_len,
|
|
file_result.five_result);
|
|
|
|
five_state_proceed(integrity, &file_result);
|
|
|
|
inode_unlock(inode);
|
|
file_verification_result_deinit(&file_result);
|
|
}
|
|
|
|
#define MFD_NAME_PREFIX "memfd:"
|
|
#define MFD_NAME_PREFIX_LEN (sizeof(MFD_NAME_PREFIX) - 1)
|
|
|
|
static bool is_memfd_file(struct file *file)
|
|
{
|
|
struct inode *inode;
|
|
struct inode *memfd_inode;
|
|
|
|
if (!file)
|
|
return false;
|
|
|
|
memfd_inode = file_inode(memfd_file);
|
|
inode = file_inode(file);
|
|
if (inode && memfd_inode && inode->i_sb == memfd_inode->i_sb)
|
|
if (file->f_path.dentry &&
|
|
!strncmp(file->f_path.dentry->d_iname, MFD_NAME_PREFIX,
|
|
MFD_NAME_PREFIX_LEN))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* five_file_mmap - measure files being mapped executable based on
|
|
* the process_measurement() policy decision.
|
|
* @file: pointer to the file to be measured (May be NULL)
|
|
* @prot: contains the protection that will be applied by the kernel.
|
|
*
|
|
* On success return 0.
|
|
*/
|
|
int five_file_mmap(struct file *file, unsigned long prot)
|
|
{
|
|
int rc = 0;
|
|
struct task_struct *task = current;
|
|
struct task_integrity *tint = TASK_INTEGRITY(task);
|
|
|
|
if (five_check_params(task, file))
|
|
return 0;
|
|
|
|
if (check_memfd_file && is_memfd_file(file))
|
|
return 0;
|
|
|
|
if (file && task_integrity_user_read(tint)) {
|
|
if (prot & PROT_EXEC) {
|
|
rc = push_file_event_bunch(task, file, MMAP_CHECK);
|
|
if (rc)
|
|
return rc;
|
|
} else {
|
|
five_hook_file_skipped(task, file);
|
|
}
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* five_bprm_check - Measure executable being launched based on
|
|
* the process_measurement() policy decision.
|
|
* @bprm: contains the linux_binprm structure
|
|
*
|
|
* Notes:
|
|
* bprm_check could be called few times for one process when few binary loaders
|
|
* are used. Example: execution of shell script.
|
|
* In this case we should process first file (e.g. shell script) as main and
|
|
* use BPRM_CHECK. The second file (interpetator ) will be processed as general
|
|
* mapping (MMAP_CHECK).
|
|
* To implement this option variable bprm->recursion_depth is used.
|
|
*
|
|
* On success return 0.
|
|
*/
|
|
int five_bprm_check(struct linux_binprm *bprm)
|
|
{
|
|
int rc = 0;
|
|
struct task_struct *task = current;
|
|
struct task_integrity *old_tint = TASK_INTEGRITY(task);
|
|
|
|
if (unlikely(task->ptrace))
|
|
return rc;
|
|
|
|
if (bprm->recursion_depth > 0) {
|
|
rc = push_file_event_bunch(task, bprm->file, MMAP_CHECK);
|
|
} else {
|
|
struct task_integrity *tint = task_integrity_alloc();
|
|
|
|
task_integrity_assign(task, tint);
|
|
if (likely(TASK_INTEGRITY(task))) {
|
|
rc = push_file_event_bunch(task,
|
|
bprm->file, BPRM_CHECK);
|
|
} else {
|
|
rc = -ENOMEM;
|
|
}
|
|
task_integrity_put(old_tint);
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
/* Does `unlink' of the `file'.
|
|
* This function breaks delegation (drops file's leases. See
|
|
* man 2 fcntl "Leases"). do_unlinkat function in fs/namei.c was used
|
|
* as an example.
|
|
*/
|
|
static int five_unlink(struct file *file)
|
|
{
|
|
int rc;
|
|
struct dentry *dentry = file->f_path.dentry;
|
|
struct inode *inode = d_backing_inode(dentry->d_parent);
|
|
struct inode *delegated_inode = NULL;
|
|
bool retry;
|
|
|
|
do {
|
|
delegated_inode = NULL;
|
|
retry = false;
|
|
inode_lock_nested(inode, I_MUTEX_PARENT);
|
|
ihold(inode);
|
|
rc = vfs_unlink(inode, dentry, &delegated_inode);
|
|
inode_unlock(inode);
|
|
iput(inode);
|
|
if (rc == -EWOULDBLOCK && delegated_inode) {
|
|
rc = break_deleg_wait(&delegated_inode);
|
|
if (!rc)
|
|
retry = true;
|
|
}
|
|
} while (retry);
|
|
|
|
five_audit_info(current, file, "five_unlink", 0, 0,
|
|
"Unlink a file", rc);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* This function handles two situations:
|
|
* 1. Device had been rebooted before five_sign finished.
|
|
* Then xattr_len will be zero and iint->five_signing will be false.
|
|
* 2. The file is being signing when another process tries to open it.
|
|
* Then xattr_len will be zero and iint->five_signing will be true.
|
|
*
|
|
* - five_fcntl_edit stores the xattr with zero length and set
|
|
* iint->five_signing to true
|
|
* - five_fcntl_sign stores correct certificates and set
|
|
* iint->five_signing to false
|
|
*
|
|
* On success returns 0
|
|
*/
|
|
int five_file_open(struct file *file)
|
|
{
|
|
ssize_t xattr_len;
|
|
struct inode *inode = file_inode(file);
|
|
|
|
if (!S_ISREG(inode->i_mode))
|
|
return 0;
|
|
|
|
xattr_len = vfs_getxattr(file->f_path.dentry, XATTR_NAME_FIVE,
|
|
NULL, 0);
|
|
if (xattr_len == 0) {
|
|
struct integrity_iint_cache *iint;
|
|
bool is_signing = false;
|
|
|
|
if (!unlink_on_error) {
|
|
five_audit_info(current, file, "five_unlink", 0, 0,
|
|
"Found a dummy-cert", 0);
|
|
return 0;
|
|
}
|
|
|
|
inode_lock(inode);
|
|
iint = integrity_iint_find(inode);
|
|
if (iint)
|
|
is_signing = iint->five_signing;
|
|
inode_unlock(inode);
|
|
|
|
if (!is_signing) {
|
|
int rc;
|
|
|
|
rc = five_unlink(file);
|
|
rc = rc ?: -ENOENT;
|
|
|
|
return rc;
|
|
}
|
|
return -EPERM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* five_file_verify - force five integrity measurements for file
|
|
* the process_measurement() policy decision. This check affects
|
|
* task integrity.
|
|
* @file: pointer to the file to be measured (May be NULL)
|
|
*
|
|
* On success return 0.
|
|
*/
|
|
int five_file_verify(struct task_struct *task, struct file *file)
|
|
{
|
|
int rc = 0;
|
|
struct task_integrity *tint = TASK_INTEGRITY(task);
|
|
|
|
if (file && task_integrity_user_read(tint))
|
|
rc = push_file_event_bunch(task, file, FILE_CHECK);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static struct notifier_block five_reboot_nb = {
|
|
.notifier_call = five_reboot_notifier,
|
|
.priority = INT_MAX,
|
|
};
|
|
|
|
int five_hash_algo __ro_after_init = HASH_ALGO_SHA1;
|
|
|
|
static int __init hash_setup(const char *str)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < HASH_ALGO__LAST; i++) {
|
|
if (strcmp(str, hash_algo_name[i]) == 0) {
|
|
five_hash_algo = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int __init init_five(void)
|
|
{
|
|
int error;
|
|
|
|
g_five_workqueue = alloc_workqueue("%s", WQ_FREEZABLE | WQ_MEM_RECLAIM,
|
|
0, "five_wq");
|
|
if (!g_five_workqueue)
|
|
return -ENOMEM;
|
|
|
|
hash_setup(CONFIG_FIVE_DEFAULT_HASH);
|
|
error = five_init();
|
|
if (error)
|
|
return error;
|
|
|
|
error = five_hook_wq_init();
|
|
if (error)
|
|
return error;
|
|
|
|
error = register_reboot_notifier(&five_reboot_nb);
|
|
if (error)
|
|
return error;
|
|
|
|
/**
|
|
* This empty file is needed in is_memfd_file() function.
|
|
* The only way to check whether the file was created using memfd_create()
|
|
* syscall is to compare its superblock address with address of another memfd
|
|
* file.
|
|
*
|
|
* Below code is copied from shmem_kernel_file_setup(). The difference between
|
|
* shmem_file_setup() isshmem_kernel_file_setup() calls __shmem_file_setup()
|
|
* with S_PRIVATE flag but shmem_file_setup() calls it without one. After that
|
|
* __shmem_file_setup() sets S_PRIVATE flag in inode->i_flags:
|
|
inode->i_flags |= i_flags;
|
|
*/
|
|
memfd_file = shmem_file_setup(
|
|
"five_memfd_check", 0, VM_NORESERVE);
|
|
if (IS_ERR(memfd_file)) {
|
|
error = PTR_ERR(memfd_file);
|
|
memfd_file = NULL;
|
|
return error;
|
|
}
|
|
file_inode(memfd_file)->i_flags |= S_PRIVATE;
|
|
|
|
error = init_fs();
|
|
if (error)
|
|
return error;
|
|
|
|
five_dsms_init("1", 0);
|
|
|
|
error = five_init_dmverity();
|
|
if (error)
|
|
return error;
|
|
|
|
error = five_tint_init_dev();
|
|
|
|
return error;
|
|
}
|
|
|
|
static int fcntl_verify(struct file *file)
|
|
{
|
|
int rc = 0;
|
|
struct task_struct *task = current;
|
|
struct task_integrity *tint = TASK_INTEGRITY(task);
|
|
|
|
if (task_integrity_user_read(tint))
|
|
rc = push_file_event_bunch(task, file, FILE_CHECK);
|
|
return rc;
|
|
}
|
|
|
|
/* Called from do_fcntl */
|
|
int five_fcntl_verify_async(struct file *file)
|
|
{
|
|
return fcntl_verify(file);
|
|
}
|
|
|
|
/* Called from do_fcntl */
|
|
int five_fcntl_verify_sync(struct file *file)
|
|
{
|
|
return -EINVAL;
|
|
}
|
|
|
|
int five_fork(struct task_struct *task, struct task_struct *child_task)
|
|
{
|
|
int rc = 0;
|
|
|
|
spin_lock(&TASK_INTEGRITY(task)->list_lock);
|
|
|
|
if (!list_empty(&TASK_INTEGRITY(task)->events.list)) {
|
|
/*copy the list*/
|
|
struct list_head *tmp;
|
|
struct processing_event_list *from_entry;
|
|
struct worker_context *context;
|
|
|
|
context = kmalloc(sizeof(struct worker_context), GFP_ATOMIC);
|
|
if (unlikely(!context)) {
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
list_for_each(tmp, &TASK_INTEGRITY(task)->events.list) {
|
|
struct processing_event_list *five_file;
|
|
|
|
from_entry = list_entry(tmp,
|
|
struct processing_event_list, list);
|
|
|
|
five_file = five_event_create(
|
|
from_entry->event,
|
|
child_task,
|
|
from_entry->file,
|
|
from_entry->function,
|
|
GFP_ATOMIC);
|
|
if (unlikely(!five_file)) {
|
|
kfree(context);
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
list_add_tail(&five_file->list,
|
|
&TASK_INTEGRITY(child_task)->events.list);
|
|
}
|
|
|
|
context->tint = TASK_INTEGRITY(child_task);
|
|
|
|
rc = task_integrity_copy(TASK_INTEGRITY(task),
|
|
TASK_INTEGRITY(child_task));
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
task_integrity_get(context->tint);
|
|
task_integrity_processing(TASK_INTEGRITY(child_task));
|
|
INIT_WORK(&context->data_work, work_handler);
|
|
rc = queue_work(g_five_workqueue, &context->data_work) ? 0 : 1;
|
|
} else {
|
|
rc = task_integrity_copy(TASK_INTEGRITY(task),
|
|
TASK_INTEGRITY(child_task));
|
|
spin_unlock(&TASK_INTEGRITY(task)->list_lock);
|
|
}
|
|
|
|
if (!rc)
|
|
five_hook_task_forked(task, child_task);
|
|
|
|
return rc;
|
|
}
|
|
|
|
int five_ptrace(struct task_struct *task, long request)
|
|
{
|
|
switch (request) {
|
|
case PTRACE_TRACEME:
|
|
case PTRACE_ATTACH:
|
|
case PTRACE_SEIZE:
|
|
case PTRACE_INTERRUPT:
|
|
case PTRACE_CONT:
|
|
case PTRACE_DETACH:
|
|
case PTRACE_PEEKTEXT:
|
|
case PTRACE_PEEKDATA:
|
|
case PTRACE_PEEKUSR:
|
|
case PTRACE_GETREGSET:
|
|
case PTRACE_GETSIGINFO:
|
|
case PTRACE_PEEKSIGINFO:
|
|
case PTRACE_GETSIGMASK:
|
|
case PTRACE_GETEVENTMSG:
|
|
#ifdef CONFIG_ARM64
|
|
case COMPAT_PTRACE_GETREGS:
|
|
case COMPAT_PTRACE_GET_THREAD_AREA:
|
|
case COMPAT_PTRACE_GETVFPREGS:
|
|
case COMPAT_PTRACE_GETHBPREGS:
|
|
#else
|
|
case PTRACE_GETREGS:
|
|
case PTRACE_GET_THREAD_AREA:
|
|
case PTRACE_GETVFPREGS:
|
|
case PTRACE_GETHBPREGS:
|
|
#endif
|
|
break;
|
|
default: {
|
|
struct task_integrity *tint = TASK_INTEGRITY(task);
|
|
|
|
if (task_integrity_user_read(tint) == INTEGRITY_NONE)
|
|
break;
|
|
|
|
task_integrity_delayed_reset(task, CAUSE_PTRACE, NULL);
|
|
five_audit_err(task, NULL, "ptrace", task_integrity_read(tint),
|
|
INTEGRITY_NONE, "reset-integrity", 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int five_process_vm_rw(struct task_struct *task, int write)
|
|
{
|
|
if (write) {
|
|
struct task_integrity *tint = TASK_INTEGRITY(task);
|
|
|
|
if (task_integrity_user_read(tint) == INTEGRITY_NONE)
|
|
goto exit;
|
|
|
|
task_integrity_delayed_reset(task, CAUSE_VMRW, NULL);
|
|
five_audit_err(task, NULL, "process_vm_rw",
|
|
task_integrity_read(tint), INTEGRITY_NONE,
|
|
"reset-integrity", 0);
|
|
}
|
|
|
|
exit:
|
|
return 0;
|
|
}
|
|
|
|
static inline struct processing_event_list *five_event_create(
|
|
enum five_event event, struct task_struct *task,
|
|
struct file *file, int function, gfp_t flags)
|
|
{
|
|
struct processing_event_list *five_file;
|
|
|
|
five_file = kzalloc(sizeof(struct processing_event_list), flags);
|
|
if (unlikely(!five_file))
|
|
return NULL;
|
|
|
|
five_file->event = event;
|
|
|
|
switch (five_file->event) {
|
|
case FIVE_VERIFY_BUNCH_FILES: {
|
|
get_task_struct(task);
|
|
get_file(file);
|
|
five_file->task = task;
|
|
five_file->file = file;
|
|
five_file->function = function;
|
|
break;
|
|
}
|
|
case FIVE_RESET_INTEGRITY: {
|
|
get_task_struct(task);
|
|
five_file->task = task;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return five_file;
|
|
}
|
|
|
|
static inline void five_event_destroy(
|
|
const struct processing_event_list *file)
|
|
{
|
|
switch (file->event) {
|
|
case FIVE_VERIFY_BUNCH_FILES: {
|
|
fput(file->file);
|
|
put_task_struct(file->task);
|
|
break;
|
|
}
|
|
case FIVE_RESET_INTEGRITY: {
|
|
put_task_struct(file->task);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
kfree(file);
|
|
}
|
|
|
|
late_initcall(init_five);
|
|
|
|
MODULE_DESCRIPTION("File-based process Integrity Verifier");
|
|
MODULE_LICENSE("GPL");
|