diff options
Diffstat (limited to 'kernel/kstack_erase.c')
| -rw-r--r-- | kernel/kstack_erase.c | 177 | 
1 files changed, 177 insertions, 0 deletions
| diff --git a/kernel/kstack_erase.c b/kernel/kstack_erase.c new file mode 100644 index 000000000000..e49bb88b4f0a --- /dev/null +++ b/kernel/kstack_erase.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This code fills the used part of the kernel stack with a poison value + * before returning to userspace. It's part of the STACKLEAK feature + * ported from grsecurity/PaX. + * + * Author: Alexander Popov <alex.popov@linux.com> + * + * KSTACK_ERASE reduces the information which kernel stack leak bugs can + * reveal and blocks some uninitialized stack variable attacks. + */ + +#include <linux/kstack_erase.h> +#include <linux/kprobes.h> + +#ifdef CONFIG_KSTACK_ERASE_RUNTIME_DISABLE +#include <linux/jump_label.h> +#include <linux/string_choices.h> +#include <linux/sysctl.h> +#include <linux/init.h> + +static DEFINE_STATIC_KEY_FALSE(stack_erasing_bypass); + +#ifdef CONFIG_SYSCTL +static int stack_erasing_sysctl(const struct ctl_table *table, int write, +			void __user *buffer, size_t *lenp, loff_t *ppos) +{ +	int ret = 0; +	int state = !static_branch_unlikely(&stack_erasing_bypass); +	int prev_state = state; +	struct ctl_table table_copy = *table; + +	table_copy.data = &state; +	ret = proc_dointvec_minmax(&table_copy, write, buffer, lenp, ppos); +	state = !!state; +	if (ret || !write || state == prev_state) +		return ret; + +	if (state) +		static_branch_disable(&stack_erasing_bypass); +	else +		static_branch_enable(&stack_erasing_bypass); + +	pr_warn("stackleak: kernel stack erasing is %s\n", +					str_enabled_disabled(state)); +	return ret; +} +static const struct ctl_table stackleak_sysctls[] = { +	{ +		.procname	= "stack_erasing", +		.data		= NULL, +		.maxlen		= sizeof(int), +		.mode		= 0600, +		.proc_handler	= stack_erasing_sysctl, +		.extra1		= SYSCTL_ZERO, +		.extra2		= SYSCTL_ONE, +	}, +}; + +static int __init stackleak_sysctls_init(void) +{ +	register_sysctl_init("kernel", stackleak_sysctls); +	return 0; +} +late_initcall(stackleak_sysctls_init); +#endif /* CONFIG_SYSCTL */ + +#define skip_erasing()	static_branch_unlikely(&stack_erasing_bypass) +#else +#define skip_erasing()	false +#endif /* CONFIG_KSTACK_ERASE_RUNTIME_DISABLE */ + +#ifndef __stackleak_poison +static __always_inline void __stackleak_poison(unsigned long erase_low, +					       unsigned long erase_high, +					       unsigned long poison) +{ +	while (erase_low < erase_high) { +		*(unsigned long *)erase_low = poison; +		erase_low += sizeof(unsigned long); +	} +} +#endif + +static __always_inline void __stackleak_erase(bool on_task_stack) +{ +	const unsigned long task_stack_low = stackleak_task_low_bound(current); +	const unsigned long task_stack_high = stackleak_task_high_bound(current); +	unsigned long erase_low, erase_high; + +	erase_low = stackleak_find_top_of_poison(task_stack_low, +						 current->lowest_stack); + +#ifdef CONFIG_KSTACK_ERASE_METRICS +	current->prev_lowest_stack = erase_low; +#endif + +	/* +	 * Write poison to the task's stack between 'erase_low' and +	 * 'erase_high'. +	 * +	 * If we're running on a different stack (e.g. an entry trampoline +	 * stack) we can erase everything below the pt_regs at the top of the +	 * task stack. +	 * +	 * If we're running on the task stack itself, we must not clobber any +	 * stack used by this function and its caller. We assume that this +	 * function has a fixed-size stack frame, and the current stack pointer +	 * doesn't change while we write poison. +	 */ +	if (on_task_stack) +		erase_high = current_stack_pointer; +	else +		erase_high = task_stack_high; + +	__stackleak_poison(erase_low, erase_high, KSTACK_ERASE_POISON); + +	/* Reset the 'lowest_stack' value for the next syscall */ +	current->lowest_stack = task_stack_high; +} + +/* + * Erase and poison the portion of the task stack used since the last erase. + * Can be called from the task stack or an entry stack when the task stack is + * no longer in use. + */ +asmlinkage void noinstr stackleak_erase(void) +{ +	if (skip_erasing()) +		return; + +	__stackleak_erase(on_thread_stack()); +} + +/* + * Erase and poison the portion of the task stack used since the last erase. + * Can only be called from the task stack. + */ +asmlinkage void noinstr stackleak_erase_on_task_stack(void) +{ +	if (skip_erasing()) +		return; + +	__stackleak_erase(true); +} + +/* + * Erase and poison the portion of the task stack used since the last erase. + * Can only be called from a stack other than the task stack. + */ +asmlinkage void noinstr stackleak_erase_off_task_stack(void) +{ +	if (skip_erasing()) +		return; + +	__stackleak_erase(false); +} + +void __used __no_caller_saved_registers noinstr __sanitizer_cov_stack_depth(void) +{ +	unsigned long sp = current_stack_pointer; + +	/* +	 * Having CONFIG_KSTACK_ERASE_TRACK_MIN_SIZE larger than +	 * KSTACK_ERASE_SEARCH_DEPTH makes the poison search in +	 * stackleak_erase() unreliable. Let's prevent that. +	 */ +	BUILD_BUG_ON(CONFIG_KSTACK_ERASE_TRACK_MIN_SIZE > KSTACK_ERASE_SEARCH_DEPTH); + +	/* 'lowest_stack' should be aligned on the register width boundary */ +	sp = ALIGN(sp, sizeof(unsigned long)); +	if (sp < current->lowest_stack && +	    sp >= stackleak_task_low_bound(current)) { +		current->lowest_stack = sp; +	} +} +EXPORT_SYMBOL(__sanitizer_cov_stack_depth); | 
