mirror of
https://github.com/hardkernel/linux.git
synced 2026-03-24 19:40:21 +09:00
Currently we over-estimate the region of stack which must be erased. To determine the region to be erased, we scan downwards for a contiguous block of poison values (or the low bound of the stack). There are a few minor problems with this today: * When we find a block of poison values, we include this block within the region to erase. As this is included within the region to erase, this causes us to redundantly overwrite 'STACKLEAK_SEARCH_DEPTH' (128) bytes with poison. * As the loop condition checks 'poison_count <= depth', it will run an additional iteration after finding the contiguous block of poison, decrementing 'erase_low' once more than necessary. As this is included within the region to erase, this causes us to redundantly overwrite an additional unsigned long with poison. * As we always decrement 'erase_low' after checking an element on the stack, we always include the element below this within the region to erase. As this is included within the region to erase, this causes us to redundantly overwrite an additional unsigned long with poison. Note that this is not a functional problem. As the loop condition checks 'erase_low > task_stack_low', we'll never clobber the STACK_END_MAGIC. As we always decrement 'erase_low' after this, we'll never fail to erase the element immediately above the STACK_END_MAGIC. In total, this can cause us to erase `128 + 2 * sizeof(unsigned long)` bytes more than necessary, which is unfortunate. This patch reworks the logic to find the address immediately above the poisoned region, by finding the lowest non-poisoned address. This is factored into a stackleak_find_top_of_poison() helper both for clarity and so that this can be shared with the LKDTM test in subsequent patches. Signed-off-by: Mark Rutland <mark.rutland@arm.com> Cc: Alexander Popov <alex.popov@linux.com> Cc: Andrew Morton <akpm@linux-foundation.org> Cc: Andy Lutomirski <luto@kernel.org> Cc: Kees Cook <keescook@chromium.org> Signed-off-by: Kees Cook <keescook@chromium.org> Link: https://lore.kernel.org/r/20220427173128.2603085-8-mark.rutland@arm.com
140 lines
3.8 KiB
C
140 lines
3.8 KiB
C
// 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>
|
|
*
|
|
* STACKLEAK reduces the information which kernel stack leak bugs can
|
|
* reveal and blocks some uninitialized stack variable attacks.
|
|
*/
|
|
|
|
#include <linux/stackleak.h>
|
|
#include <linux/kprobes.h>
|
|
|
|
#ifdef CONFIG_STACKLEAK_RUNTIME_DISABLE
|
|
#include <linux/jump_label.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(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;
|
|
|
|
table->data = &state;
|
|
table->maxlen = sizeof(int);
|
|
ret = proc_dointvec_minmax(table, 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",
|
|
state ? "enabled" : "disabled");
|
|
return ret;
|
|
}
|
|
static 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_STACKLEAK_RUNTIME_DISABLE */
|
|
|
|
static __always_inline void __stackleak_erase(void)
|
|
{
|
|
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_STACKLEAK_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_thread_stack())
|
|
erase_high = current_stack_pointer;
|
|
else
|
|
erase_high = task_stack_high;
|
|
|
|
while (erase_low < erase_high) {
|
|
*(unsigned long *)erase_low = STACKLEAK_POISON;
|
|
erase_low += sizeof(unsigned long);
|
|
}
|
|
|
|
/* Reset the 'lowest_stack' value for the next syscall */
|
|
current->lowest_stack = task_stack_high;
|
|
}
|
|
|
|
asmlinkage void noinstr stackleak_erase(void)
|
|
{
|
|
if (skip_erasing())
|
|
return;
|
|
|
|
__stackleak_erase();
|
|
}
|
|
|
|
void __used __no_caller_saved_registers noinstr stackleak_track_stack(void)
|
|
{
|
|
unsigned long sp = current_stack_pointer;
|
|
|
|
/*
|
|
* Having CONFIG_STACKLEAK_TRACK_MIN_SIZE larger than
|
|
* STACKLEAK_SEARCH_DEPTH makes the poison search in
|
|
* stackleak_erase() unreliable. Let's prevent that.
|
|
*/
|
|
BUILD_BUG_ON(CONFIG_STACKLEAK_TRACK_MIN_SIZE > STACKLEAK_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(stackleak_track_stack);
|