Skip to content

Commit

Permalink
kfence: use pt_regs to generate stack trace on faults
Browse files Browse the repository at this point in the history
Instead of removing the fault handling portion of the stack trace based on
the fault handler's name, just use struct pt_regs directly.

Change kfence_handle_page_fault() to take a struct pt_regs, and plumb it
through to kfence_report_error() for out-of-bounds, use-after-free, or
invalid access errors, where pt_regs is used to generate the stack trace.

If the kernel is a DEBUG_KERNEL, also show registers for more information.

Link: https://lkml.kernel.org/r/[email protected]
Signed-off-by: Marco Elver <[email protected]>
Suggested-by: Mark Rutland <[email protected]>
Acked-by: Mark Rutland <[email protected]>
Cc: Alexander Potapenko <[email protected]>
Cc: Dmitry Vyukov <[email protected]>
Cc: Jann Horn <[email protected]>
Signed-off-by: Andrew Morton <[email protected]>
Signed-off-by: Linus Torvalds <[email protected]>
  • Loading branch information
melver authored and torvalds committed Feb 26, 2021
1 parent 840b239 commit d438fab
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 46 deletions.
2 changes: 0 additions & 2 deletions arch/arm64/include/asm/kfence.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

#include <asm/cacheflush.h>

#define KFENCE_SKIP_ARCH_FAULT_HANDLER "el1_sync"

static inline bool arch_kfence_init_pool(void) { return true; }

static inline bool kfence_protect_page(unsigned long addr, bool protect)
Expand Down
2 changes: 1 addition & 1 deletion arch/arm64/mm/fault.c
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ static void __do_kernel_fault(unsigned long addr, unsigned int esr,
} else if (addr < PAGE_SIZE) {
msg = "NULL pointer dereference";
} else {
if (kfence_handle_page_fault(addr))
if (kfence_handle_page_fault(addr, regs))
return;

msg = "paging request";
Expand Down
6 changes: 0 additions & 6 deletions arch/x86/include/asm/kfence.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@
#include <asm/set_memory.h>
#include <asm/tlbflush.h>

/*
* The page fault handler entry function, up to which the stack trace is
* truncated in reports.
*/
#define KFENCE_SKIP_ARCH_FAULT_HANDLER "asm_exc_page_fault"

/* Force 4K pages for __kfence_pool. */
static inline bool arch_kfence_init_pool(void)
{
Expand Down
2 changes: 1 addition & 1 deletion arch/x86/mm/fault.c
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ page_fault_oops(struct pt_regs *regs, unsigned long error_code,
efi_crash_gracefully_on_page_fault(address);

/* Only not-present faults should be handled by KFENCE. */
if (!(error_code & X86_PF_PROT) && kfence_handle_page_fault(address))
if (!(error_code & X86_PF_PROT) && kfence_handle_page_fault(address, regs))
return;

oops:
Expand Down
5 changes: 3 additions & 2 deletions include/linux/kfence.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ static __always_inline __must_check bool kfence_free(void *addr)
/**
* kfence_handle_page_fault() - perform page fault handling for KFENCE pages
* @addr: faulting address
* @regs: current struct pt_regs (can be NULL, but shows full stack trace)
*
* Return:
* * false - address outside KFENCE pool,
Expand All @@ -196,7 +197,7 @@ static __always_inline __must_check bool kfence_free(void *addr)
* cases KFENCE prints an error message and marks the offending page as
* present, so that the kernel can proceed.
*/
bool __must_check kfence_handle_page_fault(unsigned long addr);
bool __must_check kfence_handle_page_fault(unsigned long addr, struct pt_regs *regs);

#else /* CONFIG_KFENCE */

Expand All @@ -209,7 +210,7 @@ static inline size_t kfence_ksize(const void *addr) { return 0; }
static inline void *kfence_object_start(const void *addr) { return NULL; }
static inline void __kfence_free(void *addr) { }
static inline bool __must_check kfence_free(void *addr) { return false; }
static inline bool __must_check kfence_handle_page_fault(unsigned long addr) { return false; }
static inline bool __must_check kfence_handle_page_fault(unsigned long addr, struct pt_regs *regs) { return false; }

#endif

Expand Down
10 changes: 5 additions & 5 deletions mm/kfence/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ static inline bool check_canary_byte(u8 *addr)
return true;

atomic_long_inc(&counters[KFENCE_COUNTER_BUGS]);
kfence_report_error((unsigned long)addr, addr_to_metadata((unsigned long)addr),
kfence_report_error((unsigned long)addr, NULL, addr_to_metadata((unsigned long)addr),
KFENCE_ERROR_CORRUPTION);
return false;
}
Expand Down Expand Up @@ -351,7 +351,7 @@ static void kfence_guarded_free(void *addr, struct kfence_metadata *meta, bool z
if (meta->state != KFENCE_OBJECT_ALLOCATED || meta->addr != (unsigned long)addr) {
/* Invalid or double-free, bail out. */
atomic_long_inc(&counters[KFENCE_COUNTER_BUGS]);
kfence_report_error((unsigned long)addr, meta, KFENCE_ERROR_INVALID_FREE);
kfence_report_error((unsigned long)addr, NULL, meta, KFENCE_ERROR_INVALID_FREE);
raw_spin_unlock_irqrestore(&meta->lock, flags);
return;
}
Expand Down Expand Up @@ -766,7 +766,7 @@ void __kfence_free(void *addr)
kfence_guarded_free(addr, meta, false);
}

bool kfence_handle_page_fault(unsigned long addr)
bool kfence_handle_page_fault(unsigned long addr, struct pt_regs *regs)
{
const int page_index = (addr - (unsigned long)__kfence_pool) / PAGE_SIZE;
struct kfence_metadata *to_report = NULL;
Expand Down Expand Up @@ -829,11 +829,11 @@ bool kfence_handle_page_fault(unsigned long addr)

out:
if (to_report) {
kfence_report_error(addr, to_report, error_type);
kfence_report_error(addr, regs, to_report, error_type);
raw_spin_unlock_irqrestore(&to_report->lock, flags);
} else {
/* This may be a UAF or OOB access, but we can't be sure. */
kfence_report_error(addr, NULL, KFENCE_ERROR_INVALID);
kfence_report_error(addr, regs, NULL, KFENCE_ERROR_INVALID);
}

return kfence_unprotect(addr); /* Unprotect and let access proceed. */
Expand Down
4 changes: 2 additions & 2 deletions mm/kfence/kfence.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ enum kfence_error_type {
KFENCE_ERROR_INVALID_FREE, /* Invalid free. */
};

void kfence_report_error(unsigned long address, const struct kfence_metadata *meta,
enum kfence_error_type type);
void kfence_report_error(unsigned long address, struct pt_regs *regs,
const struct kfence_metadata *meta, enum kfence_error_type type);

void kfence_print_object(struct seq_file *seq, const struct kfence_metadata *meta);

Expand Down
63 changes: 36 additions & 27 deletions mm/kfence/report.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <linux/kernel.h>
#include <linux/lockdep.h>
#include <linux/printk.h>
#include <linux/sched/debug.h>
#include <linux/seq_file.h>
#include <linux/stacktrace.h>
#include <linux/string.h>
Expand Down Expand Up @@ -41,16 +42,19 @@ static int get_stack_skipnr(const unsigned long stack_entries[], int num_entries
{
char buf[64];
int skipnr, fallback = 0;
bool is_access_fault = false;

if (type) {
/* Depending on error type, find different stack entries. */
switch (*type) {
case KFENCE_ERROR_UAF:
case KFENCE_ERROR_OOB:
case KFENCE_ERROR_INVALID:
is_access_fault = true;
break;
/*
* kfence_handle_page_fault() may be called with pt_regs
* set to NULL; in that case we'll simply show the full
* stack trace.
*/
return 0;
case KFENCE_ERROR_CORRUPTION:
case KFENCE_ERROR_INVALID_FREE:
break;
Expand All @@ -60,26 +64,21 @@ static int get_stack_skipnr(const unsigned long stack_entries[], int num_entries
for (skipnr = 0; skipnr < num_entries; skipnr++) {
int len = scnprintf(buf, sizeof(buf), "%ps", (void *)stack_entries[skipnr]);

if (is_access_fault) {
if (!strncmp(buf, KFENCE_SKIP_ARCH_FAULT_HANDLER, len))
goto found;
} else {
if (str_has_prefix(buf, "kfence_") || str_has_prefix(buf, "__kfence_") ||
!strncmp(buf, "__slab_free", len)) {
/*
* In case of tail calls from any of the below
* to any of the above.
*/
fallback = skipnr + 1;
}

/* Also the *_bulk() variants by only checking prefixes. */
if (str_has_prefix(buf, "kfree") ||
str_has_prefix(buf, "kmem_cache_free") ||
str_has_prefix(buf, "__kmalloc") ||
str_has_prefix(buf, "kmem_cache_alloc"))
goto found;
if (str_has_prefix(buf, "kfence_") || str_has_prefix(buf, "__kfence_") ||
!strncmp(buf, "__slab_free", len)) {
/*
* In case of tail calls from any of the below
* to any of the above.
*/
fallback = skipnr + 1;
}

/* Also the *_bulk() variants by only checking prefixes. */
if (str_has_prefix(buf, "kfree") ||
str_has_prefix(buf, "kmem_cache_free") ||
str_has_prefix(buf, "__kmalloc") ||
str_has_prefix(buf, "kmem_cache_alloc"))
goto found;
}
if (fallback < num_entries)
return fallback;
Expand Down Expand Up @@ -157,13 +156,20 @@ static void print_diff_canary(unsigned long address, size_t bytes_to_show,
pr_cont(" ]");
}

void kfence_report_error(unsigned long address, const struct kfence_metadata *meta,
enum kfence_error_type type)
void kfence_report_error(unsigned long address, struct pt_regs *regs,
const struct kfence_metadata *meta, enum kfence_error_type type)
{
unsigned long stack_entries[KFENCE_STACK_DEPTH] = { 0 };
int num_stack_entries = stack_trace_save(stack_entries, KFENCE_STACK_DEPTH, 1);
int skipnr = get_stack_skipnr(stack_entries, num_stack_entries, &type);
const ptrdiff_t object_index = meta ? meta - kfence_metadata : -1;
int num_stack_entries;
int skipnr = 0;

if (regs) {
num_stack_entries = stack_trace_save_regs(regs, stack_entries, KFENCE_STACK_DEPTH, 0);
} else {
num_stack_entries = stack_trace_save(stack_entries, KFENCE_STACK_DEPTH, 1);
skipnr = get_stack_skipnr(stack_entries, num_stack_entries, &type);
}

/* Require non-NULL meta, except if KFENCE_ERROR_INVALID. */
if (WARN_ON(type != KFENCE_ERROR_INVALID && !meta))
Expand Down Expand Up @@ -227,7 +233,10 @@ void kfence_report_error(unsigned long address, const struct kfence_metadata *me

/* Print report footer. */
pr_err("\n");
dump_stack_print_info(KERN_ERR);
if (IS_ENABLED(CONFIG_DEBUG_KERNEL) && regs)
show_regs(regs);
else
dump_stack_print_info(KERN_ERR);
pr_err("==================================================================\n");

lockdep_on();
Expand Down

0 comments on commit d438fab

Please sign in to comment.