Re: [PATCH v3 1/1] arm64: Implement stack trace termination record

From: Mark Rutland
Date: Tue May 04 2021 - 11:13:49 EST


Hi Madhavan,

Sorry for the long radiosilence on this. I finally had some time to take
a look, and this is pretty good!

We had to make some changes in the arm64 unwinder last week, as Leo Yan
reported an existing regression. I've rebased your patch atop that with
some additional fixups -- I've noted those below, with a copy of the
altered patch at the end of the mail. If you're happy with the result,
I'll ask Will and Catalin to queue that come -rc1.

I've also pushed that to my arm64/stacktrace-termination branch:

https://git.kernel.org/pub/scm/linux/kernel/git/mark/linux.git/log/?h=arm64/stacktrace-termination
git://git.kernel.org/pub/scm/linux/kernel/git/mark/linux.git arm64/stacktrace-termination

Mark (Brown), I've kept your Reviewed-by, since I don't think any of
those changes were against the spirit of that review. If you want that
dropped, please shout!

The conflicting commit (in the arm64 for-next/core branch) is:

8533d5bfad41e74b ("arm64: stacktrace: restore terminal records")

If you want to check Leo's test case, be aware you'll also need commit:

5fd65aeb22b72682i ("tracing: Fix stack trace event size")

... which made it into v5.12-rc6 (while the arm64 for-next/core branch
is based on v5.12-rc3).

On Tue, Apr 20, 2021 at 01:44:47PM -0500, madvenka@xxxxxxxxxxxxxxxxxxx wrote:
> From: "Madhavan T. Venkataraman" <madvenka@xxxxxxxxxxxxxxxxxxx>

> diff --git a/arch/arm64/kernel/entry.S b/arch/arm64/kernel/entry.S
> index 6acfc5e6b5e0..e677b9a2b8f8 100644
> --- a/arch/arm64/kernel/entry.S
> +++ b/arch/arm64/kernel/entry.S
> @@ -263,16 +263,18 @@ alternative_else_nop_endif
> stp lr, x21, [sp, #S_LR]
>
> /*
> - * For exceptions from EL0, terminate the callchain here.
> + * For exceptions from EL0, terminate the callchain here at
> + * task_pt_regs(current)->stackframe.
> + *
> * For exceptions from EL1, create a synthetic frame record so the
> * interrupted code shows up in the backtrace.
> */
> .if \el == 0
> - mov x29, xzr
> + stp xzr, xzr, [sp, #S_STACKFRAME]
> .else
> stp x29, x22, [sp, #S_STACKFRAME]
> - add x29, sp, #S_STACKFRAME
> .endif
> + add x29, sp, #S_STACKFRAME

In 8533d5bfad41e74b we made the same logic change, so now we only need
to update the comment.

> #ifdef CONFIG_ARM64_SW_TTBR0_PAN
> alternative_if_not ARM64_HAS_PAN
> diff --git a/arch/arm64/kernel/head.S b/arch/arm64/kernel/head.S
> index 840bda1869e9..743c019a42c7 100644
> --- a/arch/arm64/kernel/head.S
> +++ b/arch/arm64/kernel/head.S
> @@ -393,6 +393,23 @@ SYM_FUNC_START_LOCAL(__create_page_tables)
> ret x28
> SYM_FUNC_END(__create_page_tables)
>
> + /*
> + * The boot task becomes the idle task for the primary CPU. The
> + * CPU startup task on each secondary CPU becomes the idle task
> + * for the secondary CPU.
> + *
> + * The idle task does not require pt_regs. But create a dummy
> + * pt_regs so that task_pt_regs(idle_task)->stackframe can be
> + * set up to be the final frame on the idle task stack just like
> + * all the other kernel tasks. This helps the unwinder to
> + * terminate the stack trace at a well-known stack offset.
> + */

I'd prefer to simplify this to:

/*
* Create a final frame record at task_pt_regs(current)->stackframe, so
* that the unwinder can identify the final frame record of any task by
* its location in the task stack. We reserve the entire pt_regs space
* for consistency with user tasks and other kernel threads.
*/

... saying `current` rather than `idle_task` makes it clear that this is
altering the current task's state, and so long as we note that we do
this when creating each task, we don't need to call out the idle tasks
specifically.

> + .macro setup_final_frame
> + sub sp, sp, #PT_REGS_SIZE
> + stp xzr, xzr, [sp, #S_STACKFRAME]
> + add x29, sp, #S_STACKFRAME
> + .endm

It's probably worth noting in the commit message that a stacktrace will
now include __primary_switched and __secondary_switched. I think that's
fine, but we should be explict that this is expected as it is a small
behavioural change.

> +
> /*
> * The following fragment of code is executed with the MMU enabled.
> *
> @@ -447,9 +464,9 @@ SYM_FUNC_START_LOCAL(__primary_switched)
> #endif
> bl switch_to_vhe // Prefer VHE if possible
> add sp, sp, #16
> - mov x29, #0
> - mov x30, #0
> - b start_kernel
> + setup_final_frame
> + bl start_kernel
> + nop

I'd prefr to use ASM_BUG() here. That'll match what we do in entry.S,
and also catch any unexpected return.

> SYM_FUNC_END(__primary_switched)
>
> .pushsection ".rodata", "a"
> @@ -606,14 +623,14 @@ SYM_FUNC_START_LOCAL(__secondary_switched)
> cbz x2, __secondary_too_slow
> msr sp_el0, x2
> scs_load x2, x3
> - mov x29, #0
> - mov x30, #0
> + setup_final_frame
>
> #ifdef CONFIG_ARM64_PTR_AUTH
> ptrauth_keys_init_cpu x2, x3, x4, x5
> #endif
>
> - b secondary_start_kernel
> + bl secondary_start_kernel
> + nop

Likewise, I'd prefer ASM_BUG() here.

> SYM_FUNC_END(__secondary_switched)
>
> SYM_FUNC_START_LOCAL(__secondary_too_slow)
> diff --git a/arch/arm64/kernel/process.c b/arch/arm64/kernel/process.c
> index 6e60aa3b5ea9..999711c55274 100644
> --- a/arch/arm64/kernel/process.c
> +++ b/arch/arm64/kernel/process.c
> @@ -439,6 +439,11 @@ int copy_thread(unsigned long clone_flags, unsigned long stack_start,
> }
> p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
> p->thread.cpu_context.sp = (unsigned long)childregs;
> + /*
> + * For the benefit of the unwinder, set up childregs->stackframe
> + * as the final frame for the new task.
> + */
> + p->thread.cpu_context.fp = (unsigned long)childregs->stackframe;
>
> ptrace_hw_copy_thread(p);
>
> diff --git a/arch/arm64/kernel/stacktrace.c b/arch/arm64/kernel/stacktrace.c
> index d55bdfb7789c..774d9af2f0b6 100644
> --- a/arch/arm64/kernel/stacktrace.c
> +++ b/arch/arm64/kernel/stacktrace.c
> @@ -44,16 +44,16 @@ int notrace unwind_frame(struct task_struct *tsk, struct stackframe *frame)
> unsigned long fp = frame->fp;
> struct stack_info info;
>
> - /* Terminal record; nothing to unwind */
> - if (!fp)
> + if (!tsk)
> + tsk = current;
> +
> + /* Final frame; nothing to unwind */
> + if (fp == (unsigned long)task_pt_regs(tsk)->stackframe)
> return -ENOENT;
>
> if (fp & 0xf)
> return -EINVAL;
>
> - if (!tsk)
> - tsk = current;
> -
> if (!on_accessible_stack(tsk, fp, &info))
> return -EINVAL;

This looks good. Commit 8533d5bfad41e74b made us check the unwound
frame's FP, but when we identify the frame by location rather than
contents we'll need to check the FP prior to unwinding as you have here.

---->8----