| /* | 
 |  * Yama Linux Security Module | 
 |  * | 
 |  * Author: Kees Cook <keescook@chromium.org> | 
 |  * | 
 |  * Copyright (C) 2010 Canonical, Ltd. | 
 |  * Copyright (C) 2011 The Chromium OS Authors. | 
 |  * | 
 |  * This program is free software; you can redistribute it and/or modify | 
 |  * it under the terms of the GNU General Public License version 2, as | 
 |  * published by the Free Software Foundation. | 
 |  * | 
 |  */ | 
 |  | 
 | #include <linux/lsm_hooks.h> | 
 | #include <linux/sysctl.h> | 
 | #include <linux/ptrace.h> | 
 | #include <linux/prctl.h> | 
 | #include <linux/ratelimit.h> | 
 | #include <linux/workqueue.h> | 
 | #include <linux/string_helpers.h> | 
 | #include <linux/task_work.h> | 
 | #include <linux/sched.h> | 
 | #include <linux/spinlock.h> | 
 |  | 
 | #define YAMA_SCOPE_DISABLED	0 | 
 | #define YAMA_SCOPE_RELATIONAL	1 | 
 | #define YAMA_SCOPE_CAPABILITY	2 | 
 | #define YAMA_SCOPE_NO_ATTACH	3 | 
 |  | 
 | static int ptrace_scope = YAMA_SCOPE_RELATIONAL; | 
 |  | 
 | /* describe a ptrace relationship for potential exception */ | 
 | struct ptrace_relation { | 
 | 	struct task_struct *tracer; | 
 | 	struct task_struct *tracee; | 
 | 	bool invalid; | 
 | 	struct list_head node; | 
 | 	struct rcu_head rcu; | 
 | }; | 
 |  | 
 | static LIST_HEAD(ptracer_relations); | 
 | static DEFINE_SPINLOCK(ptracer_relations_lock); | 
 |  | 
 | static void yama_relation_cleanup(struct work_struct *work); | 
 | static DECLARE_WORK(yama_relation_work, yama_relation_cleanup); | 
 |  | 
 | struct access_report_info { | 
 | 	struct callback_head work; | 
 | 	const char *access; | 
 | 	struct task_struct *target; | 
 | 	struct task_struct *agent; | 
 | }; | 
 |  | 
 | static void __report_access(struct callback_head *work) | 
 | { | 
 | 	struct access_report_info *info = | 
 | 		container_of(work, struct access_report_info, work); | 
 | 	char *target_cmd, *agent_cmd; | 
 |  | 
 | 	target_cmd = kstrdup_quotable_cmdline(info->target, GFP_KERNEL); | 
 | 	agent_cmd = kstrdup_quotable_cmdline(info->agent, GFP_KERNEL); | 
 |  | 
 | 	pr_notice_ratelimited( | 
 | 		"ptrace %s of \"%s\"[%d] was attempted by \"%s\"[%d]\n", | 
 | 		info->access, target_cmd, info->target->pid, agent_cmd, | 
 | 		info->agent->pid); | 
 |  | 
 | 	kfree(agent_cmd); | 
 | 	kfree(target_cmd); | 
 |  | 
 | 	put_task_struct(info->agent); | 
 | 	put_task_struct(info->target); | 
 | 	kfree(info); | 
 | } | 
 |  | 
 | /* defers execution because cmdline access can sleep */ | 
 | static void report_access(const char *access, struct task_struct *target, | 
 | 				struct task_struct *agent) | 
 | { | 
 | 	struct access_report_info *info; | 
 | 	char agent_comm[sizeof(agent->comm)]; | 
 |  | 
 | 	assert_spin_locked(&target->alloc_lock); /* for target->comm */ | 
 |  | 
 | 	if (current->flags & PF_KTHREAD) { | 
 | 		/* I don't think kthreads call task_work_run() before exiting. | 
 | 		 * Imagine angry ranting about procfs here. | 
 | 		 */ | 
 | 		pr_notice_ratelimited( | 
 | 		    "ptrace %s of \"%s\"[%d] was attempted by \"%s\"[%d]\n", | 
 | 		    access, target->comm, target->pid, | 
 | 		    get_task_comm(agent_comm, agent), agent->pid); | 
 | 		return; | 
 | 	} | 
 |  | 
 | 	info = kmalloc(sizeof(*info), GFP_ATOMIC); | 
 | 	if (!info) | 
 | 		return; | 
 | 	init_task_work(&info->work, __report_access); | 
 | 	get_task_struct(target); | 
 | 	get_task_struct(agent); | 
 | 	info->access = access; | 
 | 	info->target = target; | 
 | 	info->agent = agent; | 
 | 	if (task_work_add(current, &info->work, true) == 0) | 
 | 		return; /* success */ | 
 |  | 
 | 	WARN(1, "report_access called from exiting task"); | 
 | 	put_task_struct(target); | 
 | 	put_task_struct(agent); | 
 | 	kfree(info); | 
 | } | 
 |  | 
 | /** | 
 |  * yama_relation_cleanup - remove invalid entries from the relation list | 
 |  * | 
 |  */ | 
 | static void yama_relation_cleanup(struct work_struct *work) | 
 | { | 
 | 	struct ptrace_relation *relation; | 
 |  | 
 | 	spin_lock(&ptracer_relations_lock); | 
 | 	rcu_read_lock(); | 
 | 	list_for_each_entry_rcu(relation, &ptracer_relations, node) { | 
 | 		if (relation->invalid) { | 
 | 			list_del_rcu(&relation->node); | 
 | 			kfree_rcu(relation, rcu); | 
 | 		} | 
 | 	} | 
 | 	rcu_read_unlock(); | 
 | 	spin_unlock(&ptracer_relations_lock); | 
 | } | 
 |  | 
 | /** | 
 |  * yama_ptracer_add - add/replace an exception for this tracer/tracee pair | 
 |  * @tracer: the task_struct of the process doing the ptrace | 
 |  * @tracee: the task_struct of the process to be ptraced | 
 |  * | 
 |  * Each tracee can have, at most, one tracer registered. Each time this | 
 |  * is called, the prior registered tracer will be replaced for the tracee. | 
 |  * | 
 |  * Returns 0 if relationship was added, -ve on error. | 
 |  */ | 
 | static int yama_ptracer_add(struct task_struct *tracer, | 
 | 			    struct task_struct *tracee) | 
 | { | 
 | 	struct ptrace_relation *relation, *added; | 
 |  | 
 | 	added = kmalloc(sizeof(*added), GFP_KERNEL); | 
 | 	if (!added) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	added->tracee = tracee; | 
 | 	added->tracer = tracer; | 
 | 	added->invalid = false; | 
 |  | 
 | 	spin_lock(&ptracer_relations_lock); | 
 | 	rcu_read_lock(); | 
 | 	list_for_each_entry_rcu(relation, &ptracer_relations, node) { | 
 | 		if (relation->invalid) | 
 | 			continue; | 
 | 		if (relation->tracee == tracee) { | 
 | 			list_replace_rcu(&relation->node, &added->node); | 
 | 			kfree_rcu(relation, rcu); | 
 | 			goto out; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	list_add_rcu(&added->node, &ptracer_relations); | 
 |  | 
 | out: | 
 | 	rcu_read_unlock(); | 
 | 	spin_unlock(&ptracer_relations_lock); | 
 | 	return 0; | 
 | } | 
 |  | 
 | /** | 
 |  * yama_ptracer_del - remove exceptions related to the given tasks | 
 |  * @tracer: remove any relation where tracer task matches | 
 |  * @tracee: remove any relation where tracee task matches | 
 |  */ | 
 | static void yama_ptracer_del(struct task_struct *tracer, | 
 | 			     struct task_struct *tracee) | 
 | { | 
 | 	struct ptrace_relation *relation; | 
 | 	bool marked = false; | 
 |  | 
 | 	rcu_read_lock(); | 
 | 	list_for_each_entry_rcu(relation, &ptracer_relations, node) { | 
 | 		if (relation->invalid) | 
 | 			continue; | 
 | 		if (relation->tracee == tracee || | 
 | 		    (tracer && relation->tracer == tracer)) { | 
 | 			relation->invalid = true; | 
 | 			marked = true; | 
 | 		} | 
 | 	} | 
 | 	rcu_read_unlock(); | 
 |  | 
 | 	if (marked) | 
 | 		schedule_work(&yama_relation_work); | 
 | } | 
 |  | 
 | /** | 
 |  * yama_task_free - check for task_pid to remove from exception list | 
 |  * @task: task being removed | 
 |  */ | 
 | void yama_task_free(struct task_struct *task) | 
 | { | 
 | 	yama_ptracer_del(task, task); | 
 | } | 
 |  | 
 | /** | 
 |  * yama_task_prctl - check for Yama-specific prctl operations | 
 |  * @option: operation | 
 |  * @arg2: argument | 
 |  * @arg3: argument | 
 |  * @arg4: argument | 
 |  * @arg5: argument | 
 |  * | 
 |  * Return 0 on success, -ve on error.  -ENOSYS is returned when Yama | 
 |  * does not handle the given option. | 
 |  */ | 
 | int yama_task_prctl(int option, unsigned long arg2, unsigned long arg3, | 
 | 			   unsigned long arg4, unsigned long arg5) | 
 | { | 
 | 	int rc = -ENOSYS; | 
 | 	struct task_struct *myself = current; | 
 |  | 
 | 	switch (option) { | 
 | 	case PR_SET_PTRACER: | 
 | 		/* Since a thread can call prctl(), find the group leader | 
 | 		 * before calling _add() or _del() on it, since we want | 
 | 		 * process-level granularity of control. The tracer group | 
 | 		 * leader checking is handled later when walking the ancestry | 
 | 		 * at the time of PTRACE_ATTACH check. | 
 | 		 */ | 
 | 		rcu_read_lock(); | 
 | 		if (!thread_group_leader(myself)) | 
 | 			myself = rcu_dereference(myself->group_leader); | 
 | 		get_task_struct(myself); | 
 | 		rcu_read_unlock(); | 
 |  | 
 | 		if (arg2 == 0) { | 
 | 			yama_ptracer_del(NULL, myself); | 
 | 			rc = 0; | 
 | 		} else if (arg2 == PR_SET_PTRACER_ANY || (int)arg2 == -1) { | 
 | 			rc = yama_ptracer_add(NULL, myself); | 
 | 		} else { | 
 | 			struct task_struct *tracer; | 
 |  | 
 | 			tracer = find_get_task_by_vpid(arg2); | 
 | 			if (!tracer) { | 
 | 				rc = -EINVAL; | 
 | 			} else { | 
 | 				rc = yama_ptracer_add(tracer, myself); | 
 | 				put_task_struct(tracer); | 
 | 			} | 
 | 		} | 
 |  | 
 | 		put_task_struct(myself); | 
 | 		break; | 
 | 	} | 
 |  | 
 | 	return rc; | 
 | } | 
 |  | 
 | /** | 
 |  * task_is_descendant - walk up a process family tree looking for a match | 
 |  * @parent: the process to compare against while walking up from child | 
 |  * @child: the process to start from while looking upwards for parent | 
 |  * | 
 |  * Returns 1 if child is a descendant of parent, 0 if not. | 
 |  */ | 
 | static int task_is_descendant(struct task_struct *parent, | 
 | 			      struct task_struct *child) | 
 | { | 
 | 	int rc = 0; | 
 | 	struct task_struct *walker = child; | 
 |  | 
 | 	if (!parent || !child) | 
 | 		return 0; | 
 |  | 
 | 	rcu_read_lock(); | 
 | 	if (!thread_group_leader(parent)) | 
 | 		parent = rcu_dereference(parent->group_leader); | 
 | 	while (walker->pid > 0) { | 
 | 		if (!thread_group_leader(walker)) | 
 | 			walker = rcu_dereference(walker->group_leader); | 
 | 		if (walker == parent) { | 
 | 			rc = 1; | 
 | 			break; | 
 | 		} | 
 | 		walker = rcu_dereference(walker->real_parent); | 
 | 	} | 
 | 	rcu_read_unlock(); | 
 |  | 
 | 	return rc; | 
 | } | 
 |  | 
 | /** | 
 |  * ptracer_exception_found - tracer registered as exception for this tracee | 
 |  * @tracer: the task_struct of the process attempting ptrace | 
 |  * @tracee: the task_struct of the process to be ptraced | 
 |  * | 
 |  * Returns 1 if tracer has a ptracer exception ancestor for tracee. | 
 |  */ | 
 | static int ptracer_exception_found(struct task_struct *tracer, | 
 | 				   struct task_struct *tracee) | 
 | { | 
 | 	int rc = 0; | 
 | 	struct ptrace_relation *relation; | 
 | 	struct task_struct *parent = NULL; | 
 | 	bool found = false; | 
 |  | 
 | 	rcu_read_lock(); | 
 |  | 
 | 	/* | 
 | 	 * If there's already an active tracing relationship, then make an | 
 | 	 * exception for the sake of other accesses, like process_vm_rw(). | 
 | 	 */ | 
 | 	parent = ptrace_parent(tracee); | 
 | 	if (parent != NULL && same_thread_group(parent, tracer)) { | 
 | 		rc = 1; | 
 | 		goto unlock; | 
 | 	} | 
 |  | 
 | 	/* Look for a PR_SET_PTRACER relationship. */ | 
 | 	if (!thread_group_leader(tracee)) | 
 | 		tracee = rcu_dereference(tracee->group_leader); | 
 | 	list_for_each_entry_rcu(relation, &ptracer_relations, node) { | 
 | 		if (relation->invalid) | 
 | 			continue; | 
 | 		if (relation->tracee == tracee) { | 
 | 			parent = relation->tracer; | 
 | 			found = true; | 
 | 			break; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	if (found && (parent == NULL || task_is_descendant(parent, tracer))) | 
 | 		rc = 1; | 
 |  | 
 | unlock: | 
 | 	rcu_read_unlock(); | 
 |  | 
 | 	return rc; | 
 | } | 
 |  | 
 | /** | 
 |  * yama_ptrace_access_check - validate PTRACE_ATTACH calls | 
 |  * @child: task that current task is attempting to ptrace | 
 |  * @mode: ptrace attach mode | 
 |  * | 
 |  * Returns 0 if following the ptrace is allowed, -ve on error. | 
 |  */ | 
 | static int yama_ptrace_access_check(struct task_struct *child, | 
 | 				    unsigned int mode) | 
 | { | 
 | 	int rc = 0; | 
 |  | 
 | 	/* require ptrace target be a child of ptracer on attach */ | 
 | 	if (mode & PTRACE_MODE_ATTACH) { | 
 | 		switch (ptrace_scope) { | 
 | 		case YAMA_SCOPE_DISABLED: | 
 | 			/* No additional restrictions. */ | 
 | 			break; | 
 | 		case YAMA_SCOPE_RELATIONAL: | 
 | 			rcu_read_lock(); | 
 | 			if (!task_is_descendant(current, child) && | 
 | 			    !ptracer_exception_found(current, child) && | 
 | 			    !ns_capable(__task_cred(child)->user_ns, CAP_SYS_PTRACE)) | 
 | 				rc = -EPERM; | 
 | 			rcu_read_unlock(); | 
 | 			break; | 
 | 		case YAMA_SCOPE_CAPABILITY: | 
 | 			rcu_read_lock(); | 
 | 			if (!ns_capable(__task_cred(child)->user_ns, CAP_SYS_PTRACE)) | 
 | 				rc = -EPERM; | 
 | 			rcu_read_unlock(); | 
 | 			break; | 
 | 		case YAMA_SCOPE_NO_ATTACH: | 
 | 		default: | 
 | 			rc = -EPERM; | 
 | 			break; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	if (rc && (mode & PTRACE_MODE_NOAUDIT) == 0) | 
 | 		report_access("attach", child, current); | 
 |  | 
 | 	return rc; | 
 | } | 
 |  | 
 | /** | 
 |  * yama_ptrace_traceme - validate PTRACE_TRACEME calls | 
 |  * @parent: task that will become the ptracer of the current task | 
 |  * | 
 |  * Returns 0 if following the ptrace is allowed, -ve on error. | 
 |  */ | 
 | int yama_ptrace_traceme(struct task_struct *parent) | 
 | { | 
 | 	int rc = 0; | 
 |  | 
 | 	/* Only disallow PTRACE_TRACEME on more aggressive settings. */ | 
 | 	switch (ptrace_scope) { | 
 | 	case YAMA_SCOPE_CAPABILITY: | 
 | 		if (!has_ns_capability(parent, current_user_ns(), CAP_SYS_PTRACE)) | 
 | 			rc = -EPERM; | 
 | 		break; | 
 | 	case YAMA_SCOPE_NO_ATTACH: | 
 | 		rc = -EPERM; | 
 | 		break; | 
 | 	} | 
 |  | 
 | 	if (rc) { | 
 | 		task_lock(current); | 
 | 		report_access("traceme", current, parent); | 
 | 		task_unlock(current); | 
 | 	} | 
 |  | 
 | 	return rc; | 
 | } | 
 |  | 
 | static struct security_hook_list yama_hooks[] __lsm_ro_after_init = { | 
 | 	LSM_HOOK_INIT(ptrace_access_check, yama_ptrace_access_check), | 
 | 	LSM_HOOK_INIT(ptrace_traceme, yama_ptrace_traceme), | 
 | 	LSM_HOOK_INIT(task_prctl, yama_task_prctl), | 
 | 	LSM_HOOK_INIT(task_free, yama_task_free), | 
 | }; | 
 |  | 
 | #ifdef CONFIG_SYSCTL | 
 | static int yama_dointvec_minmax(struct ctl_table *table, int write, | 
 | 				void __user *buffer, size_t *lenp, loff_t *ppos) | 
 | { | 
 | 	struct ctl_table table_copy; | 
 |  | 
 | 	if (write && !capable(CAP_SYS_PTRACE)) | 
 | 		return -EPERM; | 
 |  | 
 | 	/* Lock the max value if it ever gets set. */ | 
 | 	table_copy = *table; | 
 | 	if (*(int *)table_copy.data == *(int *)table_copy.extra2) | 
 | 		table_copy.extra1 = table_copy.extra2; | 
 |  | 
 | 	return proc_dointvec_minmax(&table_copy, write, buffer, lenp, ppos); | 
 | } | 
 |  | 
 | static int zero; | 
 | static int max_scope = YAMA_SCOPE_NO_ATTACH; | 
 |  | 
 | struct ctl_path yama_sysctl_path[] = { | 
 | 	{ .procname = "kernel", }, | 
 | 	{ .procname = "yama", }, | 
 | 	{ } | 
 | }; | 
 |  | 
 | static struct ctl_table yama_sysctl_table[] = { | 
 | 	{ | 
 | 		.procname       = "ptrace_scope", | 
 | 		.data           = &ptrace_scope, | 
 | 		.maxlen         = sizeof(int), | 
 | 		.mode           = 0644, | 
 | 		.proc_handler   = yama_dointvec_minmax, | 
 | 		.extra1         = &zero, | 
 | 		.extra2         = &max_scope, | 
 | 	}, | 
 | 	{ } | 
 | }; | 
 | static void __init yama_init_sysctl(void) | 
 | { | 
 | 	if (!register_sysctl_paths(yama_sysctl_path, yama_sysctl_table)) | 
 | 		panic("Yama: sysctl registration failed.\n"); | 
 | } | 
 | #else | 
 | static inline void yama_init_sysctl(void) { } | 
 | #endif /* CONFIG_SYSCTL */ | 
 |  | 
 | void __init yama_add_hooks(void) | 
 | { | 
 | 	pr_info("Yama: becoming mindful.\n"); | 
 | 	security_add_hooks(yama_hooks, ARRAY_SIZE(yama_hooks), "yama"); | 
 | 	yama_init_sysctl(); | 
 | } |