ARM: enable dumping stacks for CONFIG_SMP
v2 cover ARM: enable dumping stacks for CONFIG_SMP
This topic has come up before, see
http://comments.gmane.org/gmane.linux.ports.arm.kernel/102458
for the previous discussion.
SMP is now the norm for new ARM systems, and the limitation that CONFIG_STACKTRACE doesn't work for tasks besides 'current' causes problems. The particular case I'm dealing with is automated debugging information collected from /proc/<pid>/stack when userspace detects a process is not responding.
Dumping stacktraces is currently disabled due to the worry that the task may be running on another CPU and that the unwinder may be unstable when presented with a stack that is being modified. I have attempted to harden the frame pointer based unwinder and the unwind table based unwinder against invalid stacks. I separated the two into individual patches, as I expect the patch to the table unwinder to be more controversial than the frame pointer unwinder.
Even without the hardening, unwinding a stack for a running process is not completely untested. When CONFIG_ARM_UNWIND is enabled, sysrq-t calls unwind_backtrace for all tasks including running ones. In addition, any callers to unwind_frame with preemption enabled, including proc_pid_stack, could see a modified stack even on a UP system (pointed out by Rabin Vincent the last time this topic came up).
v2 of the patchset adds additional checks to avoid starting the stacktrace in an unmapped region or looping forever when unwinding a bad stack.
v2:
add depth parameter to unwind_frame verify that sp changes for non-leaf frames verify that initial sp value is in mapped lowmem verify that stack offsets are in the range
[sizeof(struct thread_info), THREAD_START_SP)
arch/arm/include/asm/stacktrace.h | 6 ++- arch/arm/kernel/process.c | 2 +- arch/arm/kernel/stacktrace.c | 122 +++++++++++++++++++++++++++++++------ arch/arm/kernel/time.c | 3 +- arch/arm/kernel/unwind.c | 46 ++++++++++---- 5 files changed, 145 insertions(+), 34 deletions(-)
PATCH v2 1/3 ARM: stacktrace: harden FP stacktraces against invalid stacks
Dumping stacktraces is currently disabled in ARM SMP for all tasks except the current task due to the worry that the task may be running on another CPU and that the unwinder may be unstable when presented with a stack that is being modified.
Unwinding with CONFIG_FRAME_POINTER is fairly simple compared to when CONFIG_ARM_UNWIND is set. The next frame's FP and SP registers are read from the stack and can be validated against the current values to ensure that they do not leave the stack and make progress towards the upper end of the stack. This guarantees that accesses do not fault and that execution is bounded.
Add additional validations to the version of unwind_frame used when CONFIG_FRAME_POINTER=y:
Verify that the stack is in a mapped region of kernel memory. Fixes crashes seen in unwind_frame on real systems, although stack corruption caused by memory instability is likely the cause of the invalid sp and fp values.
Fix address comparison to catch fp >= 0xfffffffc correctly. Fixes crash reported by Todd Poynor.
Ensure the stack pointer moves to a higher address between each frame to make sure repeated calls to unwind_frame can't loop forever.
Includes ideas from Dave Martin.
---
v2:
verify that initial sp value is in mapped lowmem verify that stack offsets are in the range
[sizeof(struct thread_info), THREAD_START_SP)
export stack validation functions for use by unwind
arch/arm/include/asm/stacktrace.h | 4 ++ arch/arm/kernel/stacktrace.c | 105 ++++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 6 deletions(-)
arch/arm/include/asm/stacktrace.h +4 -0
@@ -12,4 +12,8 @@ struct stackframe {extern void walk_stackframe (struct stackframe *frame,int (*fn)(struct stackframe *,void *),void *data); +bool sp_addr_valid (unsigned long sp); +bool addr_in_stack (unsigned long orig_sp,unsigned long vsp); +bool sp_in_stack (unsigned long orig_sp,unsigned long vsp); +#endif /* __ASM_STACKTRACE_H */
arch/arm/kernel/stacktrace.c +99 -6
129 lines not on this page · raw message
Signature · 1 line
1.7.7.3
PATCH v2 1/3 ARM: stacktrace: harden FP stacktraces against invalid stacks
Quoted 22 lines · Colin Cross
arch/arm/kernel/stacktrace.c +22
-0+bool sp_addr_valid (unsigned long sp) +{ +unsigned long high; +unsigned long offset; +unsigned int pfn; +unsigned int start_pfn; +unsigned int end_pfn; + +if (!IS_ALIGNED (sp,4 )) +return false; + + offset = sp & (THREAD_SIZE -1 ); + +if (offset >THREAD_START_SP ) +return false; + +if (offset <sizeof (struct thread_info )) +return false; + + high =STACK_MAX (sp); + +if (!virt_addr_valid (sp) || !virt_addr_valid (high))
arch/arm/kernel/stacktrace.c line 53
Quoted 10 lines
arch/arm/kernel/stacktrace.c +10
-0+return false; + + start_pfn =page_to_pfn (virt_to_page (sp)); + end_pfn =page_to_pfn (virt_to_page (high)); +for (pfn = start_pfn; pfn <= end_pfn; pfn++) +if (!pfn_valid (pfn)) +return false; + +return true; +}
I get crashes on bootup with CONFIG_SPARSEMEM enabled if a stacktrace needs to be saved before the sections are setup:
<1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] Internal error: Oops: 5 [#1] PREEMPT SMP ARM <4>[ 0.000000] Modules linked in: <4>[ 0.000000] CPU: 0 Not tainted (3.4.0-ga472ec0-00007-g6479b9e-dirty #10) <4>[ 0.000000] PC is at sp_addr_valid+0xb0/0x1bc <4>[ 0.000000] LR is at unwind_frame+0x4c/0x5b0 ... <1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] BUG: spinlock lockup on CPU#0, swapper/0 <0>[ 0.000000] lock: die_lock+0x0/0x10, .magic: dead4ead, .owner: swapper/0, .owner_cpu: 0 <1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] BUG: spinlock lockup on CPU#0, swapper/0 <0>[ 0.000000] lock: die_lock+0x0/0x10, .magic: dead4ead, .owner:
swapper/0, .owner_cpu: 0
[repeat several more times]
In this case, the stacktrace is being saved via a call to kmemleak_free in free_bootmem. The sections have not yet been initialized so there is a crash in virt_to_page when accessing the section data.
I don't see an easy workaround for this right now unless we want to restrict sp_addr_valid until later in bootup.
Laura
Signature · 2 lines
Qualcomm Innovation Center, Inc. is a member of Code Aurora Forum, hosted by The Linux Foundation
PATCH v2 1/3 ARM: stacktrace: harden FP stacktraces against invalid stacks
Quoted 66 lines · Laura Abbott
Quoted 22 lines · Colin Cross
arch/arm/kernel/stacktrace.c +22
-0+bool sp_addr_valid (unsigned long sp) +{ +unsigned long high; +unsigned long offset; +unsigned int pfn; +unsigned int start_pfn; +unsigned int end_pfn; + +if (!IS_ALIGNED (sp,4 )) +return false; + + offset = sp & (THREAD_SIZE -1 ); + +if (offset >THREAD_START_SP ) +return false; + +if (offset <sizeof (struct thread_info )) +return false; + + high =STACK_MAX (sp); + +if (!virt_addr_valid (sp) || !virt_addr_valid (high))arch/arm/kernel/stacktrace.c line 53
Quoted 10 lines
arch/arm/kernel/stacktrace.c +10
-0+return false; + + start_pfn =page_to_pfn (virt_to_page (sp)); + end_pfn =page_to_pfn (virt_to_page (high)); +for (pfn = start_pfn; pfn <= end_pfn; pfn++) +if (!pfn_valid (pfn)) +return false; + +return true; +}I get crashes on bootup with
CONFIG_SPARSEMEMenabled if a stacktrace needs to be saved before the sections are setup:<1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] Internal error: Oops: 5 [#1] PREEMPT SMP ARM <4>[ 0.000000] Modules linked in: <4>[ 0.000000] CPU: 0 Not tainted (3.4.0-ga472ec0-00007-g6479b9e-dirty #10) <4>[ 0.000000] PC is at sp_addr_valid+0xb0/0x1bc <4>[ 0.000000] LR is at unwind_frame+0x4c/0x5b0 ... <1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] BUG: spinlock lockup on CPU#0, swapper/0 <0>[ 0.000000] lock: die_lock+0x0/0x10, .magic: dead4ead, .owner: swapper/0, .owner_cpu: 0 <1>[ 0.000000] Unable to handle kernel NULL pointer dereference at virtual address 00000000 <1>[ 0.000000] pgd = c0004000 <1>[ 0.000000] [00000000] *pgd=00000000 <0>[ 0.000000] BUG: spinlock lockup on CPU#0, swapper/0 <0>[ 0.000000] lock: die_lock+0x0/0x10, .magic: dead4ead, .owner:swapper/0, .owner_cpu: 0
[repeat several more times]
Quoted 6 lines
In this case, the stacktrace is being saved via a call to
kmemleak_freeinfree_bootmem. The sections have not yet been initialized so there is a crash invirt_to_pagewhen accessing the section data.I don't see an easy workaround for this right now unless we want to restrict
sp_addr_validuntil later in bootup.
Thanks for testing, I was able to reproduce the problem. It is easily fixed by replacing page_to_pfn(virt_to_page(x)) with __phys_to_pfn(__virt_to_phys(x)) to avoid touching the page structs at all.
PATCH v2 2/3 ARM: unwind: harden unwinding against invalid stacks
In reply to Colin CrossUnwinding with CONFIG_ARM_UNWIND is much more complicated than unwinding with CONFIG_FRAME_POINTER, but there are only a few points that require validation in order to avoid faults or infinite loops. Avoiding faults is easy by adding checks to verify that the stack bounds are in lowmem, and that all accesses relative to the stack pointer remain inside the stack.
When CONFIG_FRAME_POINTER is not set it is possible for a leaf frame to have the same SP as its caller, but otherwise the SP of the caller should always be higher than the SP of the callee. Add a depth parameter to unwind_frame to allow it to distinguish leaf frames from non-leaf frames, and return an error if a caller has the same sp as a non-leaf frame callee.
---
v2:
add depth parameter to unwind_frame verify that sp changes for non-leaf frames verify that initial sp value is in mapped lowmem verify that stack offsets are in the range
[sizeof(struct thread_info), THREAD_START_SP)
arch/arm/include/asm/stacktrace.h | 2 +- arch/arm/kernel/process.c | 2 +- arch/arm/kernel/stacktrace.c | 6 +++- arch/arm/kernel/time.c | 3 +- arch/arm/kernel/unwind.c | 46 +++++++++++++++++++++++++++--------- 5 files changed, 42 insertions(+), 17 deletions(-)
arch/arm/include/asm/stacktrace.h +1 -1
@@ -8,7 +8,7 @@ struct stackframe {unsigned long pc; }; -extern int unwind_frame (struct stackframe *frame); +extern int unwind_frame (struct stackframe *frame,int depth);extern void walk_stackframe (struct stackframe *frame,int (*fn)(struct stackframe *,void *),void *data);
arch/arm/kernel/process.c +1 -1
@@ -633,7 +633,7 @@ unsigned long get_wchan(struct task_struct *p) frame.lr =0 ;/* recovered from the stack */ frame.pc =thread_saved_pc (p); do { - int ret = unwind_frame(&frame); + int ret = unwind_frame(&frame, count); if (ret < 0) return 0; if (!in_sched_functions(frame.pc))
arch/arm/kernel/stacktrace.c +4 -2
@@ -101,7 +101,7 @@ bool sp_in_stack(unsigned long orig_sp, unsigned long sp)* Note that with framepointer enabled, even the leaf functions have the same * prologue and epilogue, therefore we can ignore the LR value in this case. */ -int notrace unwind_frame(struct stackframe *frame) +int notrace unwind_frame(struct stackframe *frame, int depth) {unsigned long fp = frame->fp;unsigned long sp = frame->sp;
@@ -139,12 +139,14 @@ int notrace unwind_frame(struct stackframe *frame) void notrace walk_stackframe(struct stackframe *frame,int (*fn)(struct stackframe *,void *),void *data) { +int depth =0 ; +while (1 ) {int ret;if (fn (frame, data))break ; - ret =unwind_frame (frame); + ret =unwind_frame (frame, depth++);if (ret <0 )break ; }
arch/arm/kernel/time.c +2 -1
@@ -51,6 +51,7 @@unsigned long profile_pc (struct pt_regs *regs) {struct stackframe frame; +int depth =0 ;if (!in_lock_functions (regs->ARM_pc))return regs->ARM_pc;
@@ -60,7 +61,7 @@ unsigned long profile_pc(struct pt_regs *regs) frame.lr = regs->ARM_lr; frame.pc = regs->ARM_pc;do { -int ret =unwind_frame (&frame); +int ret =unwind_frame (&frame, depth++);if (ret <0 )return 0 ; }while (in_lock_functions (frame.pc));
arch/arm/kernel/unwind.c +34 -12
123 lines not on this page · raw message
Signature · 1 line
1.7.7.3
PATCH v2 3/3 ARM: stacktrace: enable save_stack_trace_tsk for CONFIG_SMP
In reply to Colin CrossNow that unwind_frame has been hardened against invalid stacks for CONFIG_ARM_UNWIND or CONFIG_FRAME_POINTER, enable dumping the stack on non-current threads for CONFIG_SMP.
--- arch/arm/kernel/stacktrace.c | 11 ----------- 1 files changed, 0 insertions(+), 11 deletions(-)
arch/arm/kernel/stacktrace.c +0 -11
@@ -187,22 +187,11 @@ void save_stack_trace_tsk(struct task_struct *tsk, struct stack_trace *trace) data.skip = trace->skip;if (tsk != current) { -#ifdef CONFIG_SMP -/* -* What guarantees do we have here that 'tsk' is not -* running on another CPU? For now, ignore it as we -* can't guarantee we won't explode. -*/ -if (trace->nr_entries < trace->max_entries) - trace->entries[trace->nr_entries++] =ULONG_MAX ; -return ; -#else data.no_sched_functions =1 ; frame.fp =thread_saved_fp (tsk); frame.sp =thread_saved_sp (tsk); frame.lr =0 ;/* recovered from the stack */ frame.pc =thread_saved_pc (tsk); -#endif }else { registerunsigned long current_sp asm ("sp" );
Signature · 1 line
1.7.7.3