[PATCH 07/10] vmcore_tasks: Add generic user-space stack unwinder

Pnina Feder <[email protected]>
Newsgroups org.infradead.lists.kexec
Message-ID <[email protected]>
Add architecture-independent user-space frame unwinder that uses
arch-provided instruction callbacks to walk the call stack.

The unwinder tries recovery in order:
  1. Signal frame: if arch reports PC looks like a signal trampoline,
     walks rt_sigframe -> ucontext -> sigcontext using vmcoreinfo
     offsets to recover saved PC, SP, and RA.
  2. Prologue scan: scans backward from PC looking for stack frame
     setup (SP adjustment) and RA save instructions. Uses a buffered
     read strategy with page-boundary handling for efficiency.

Architecture-specific behavior is provided via arch.unwind_ops:
  - is_sp_move_ins(): detect stack pointer adjustment
  - is_ra_save_ins(): detect return address save to stack
  - is_end_of_backtrace(): detect end-of-trace markers (optional)
  - post_unwind_fixup(): arch-specific post-processing (optional)
  - likely_signal_pc(): hint whether PC is in a signal trampoline
  - signal_layout: describes signal frame structure offsets

This separation allows adding new architectures by implementing
only the instruction decode callbacks, without duplicating the
unwinding logic.

Signed-off-by: Pnina Feder <[email protected]>
---
 vmcore_tasks/unwind.c | 267 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 267 insertions(+)
 create mode 100644 vmcore_tasks/unwind.c

diff --git a/vmcore_tasks/unwind.c b/vmcore_tasks/unwind.c
new file mode 100644
index 00000000..99d23322
--- /dev/null
+++ b/vmcore_tasks/unwind.c
@@ -0,0 +1,267 @@
+#include <stdio.h>
+#include <string.h>
+#include "memory.h"
+#include "vmcore_tasks_defs.h"
+
+/* Return codes for unwind_one_frame */
+#define UNWIND_OK        0
+#define UNWIND_LEAF     -1   /* No prologue found — possible leaf function */
+#define UNWIND_FAIL     -2   /* Hard failure (bad memory, etc.) */
+
+static bool sigframe_candidate_ok(uint64_t old_pc, uint64_t old_sp, uint64_t old_ra,
+				  uint64_t new_pc, uint64_t new_sp, uint64_t new_ra)
+{
+	if (!new_pc || !new_sp)
+		return false;
+	if (new_pc == old_pc && new_ra == old_ra)
+		return false;
+	/* Signal return should restore caller SP above current sigframe SP. */
+	if (new_sp <= old_sp)
+		return false;
+	/* Reject absurd jumps in SP (false positives). */
+	if ((new_sp - old_sp) > (1ULL << 20))
+		return false;
+	return true;
+}
+
+static int unwind_signal_frame(struct stackframe *frame)
+{
+	const struct signal_unwind_layout *layout = arch.unwind_ops.signal_layout;
+	uint64_t old_pc, old_sp, old_ra;
+	uint64_t uc_addr, mctx_addr, regs_addr;
+	uint64_t new_pc = 0, new_sp = 0, new_ra = 0;
+
+	if (!frame || !layout)
+		return -1;
+
+	/* Required vmcoreinfo keys to locate rt_sigframe -> ucontext -> sigcontext regs. */
+	if (!layout->rt_sigframe_uc_key ||
+	    !layout->ucontext_mcontext_key ||
+	    !layout->sigcontext_regs_key)
+		return -1;
+
+	if (!OFFSET_EXISTS(layout->rt_sigframe_uc_key) ||
+	    !OFFSET_EXISTS(layout->ucontext_mcontext_key) ||
+	    !OFFSET_EXISTS(layout->sigcontext_regs_key))
+		return -1;
+
+	/* Walk the layout chain from current SP to saved register block. */
+	uc_addr   = frame->sp + OFFSET(layout->rt_sigframe_uc_key);
+	mctx_addr = uc_addr   + OFFSET(layout->ucontext_mcontext_key);
+	regs_addr = mctx_addr + OFFSET(layout->sigcontext_regs_key);
+
+	/* Always recover RA and SP from arch provided indices */
+	if (readmem(regs_addr + ((uint64_t)layout->ra_reg_index * layout->reg_width),
+	    &new_ra, layout->reg_width, "sigcontext ra", UVADDR) < 0)
+		return -1;
+	if (readmem(regs_addr + ((uint64_t)layout->sp_reg_index * layout->reg_width),
+	    &new_sp, layout->reg_width, "sigcontext sp", UVADDR) < 0)
+		return -1;
+
+	old_pc = frame->pc;
+	old_sp = frame->sp;
+	old_ra = frame->ra;
+
+	/* PC can be either a dedicated field or a slot in the regs array. */
+	if (layout->sigcontext_pc_key && OFFSET_EXISTS(layout->sigcontext_pc_key)) {
+		uint64_t pc_addr = mctx_addr + OFFSET(layout->sigcontext_pc_key);
+		if (readmem(pc_addr, &new_pc, layout->reg_width,
+		    "sigcontext pc", UVADDR) < 0)
+			return -1;
+	} else {
+		if (readmem(regs_addr + ((uint64_t)layout->pc_reg_index * layout->reg_width),
+				 &new_pc, layout->reg_width, "sigcontext pc", UVADDR) < 0)
+			return -1;
+		/* Try alternate PC slot for kernels/ABIs with different ordering. */
+		if (!sigframe_candidate_ok(old_pc, old_sp, old_ra, new_pc, new_sp, new_ra) &&
+		    layout->pc_reg_alt_index >= 0) {
+			if (readmem(regs_addr + ((uint64_t)layout->pc_reg_alt_index * layout->reg_width),
+					 &new_pc, layout->reg_width, "sigcontext pc(alt)", UVADDR) < 0)
+				return -1;
+		}
+	}
+
+	/* Final sanity gate before committing recovered frame. */
+	if (!sigframe_candidate_ok(old_pc, old_sp, old_ra, new_pc, new_sp, new_ra))
+		return -1;
+
+	pr_verbose("sigframe-candidate: old[pc=0x%016llx sp=0x%016llx ra=0x%016llx] new[pc=0x%016llx sp=0x%016llx ra=0x%016llx] chain[uc=0x%016llx mctx=0x%016llx regs=0x%016llx] src=%s idx[sp=%d ra=%d pc=%d alt=%d] w=%u\n",
+		   (unsigned long long)old_pc, (unsigned long long)old_sp, (unsigned long long)old_ra,
+		   (unsigned long long)new_pc, (unsigned long long)new_sp, (unsigned long long)new_ra,
+		   (unsigned long long)uc_addr, (unsigned long long)mctx_addr, (unsigned long long)regs_addr,
+		   (layout->sigcontext_pc_key && OFFSET_EXISTS(layout->sigcontext_pc_key)) ? "sigcontext_pc_key" : "regs[]",
+		   layout->sp_reg_index, layout->ra_reg_index, layout->pc_reg_index, layout->pc_reg_alt_index,
+		   layout->reg_width);
+
+	frame->pc = (unsigned long)new_pc;
+	frame->sp = (unsigned long)new_sp;
+	frame->ra = (unsigned long)new_ra;
+	return 0;
+}
+
+/*
+ * Try to recover the previous frame by scanning backward for prologue
+ * instructions (SP adjustment + RA save).
+ *
+ * Returns: UNWIND_OK on success, UNWIND_LEAF if no prologue found,
+ *          UNWIND_FAIL on hard error.
+ */
+static int unwind_prologue_scan(struct stackframe *frame, unsigned int max_check)
+{
+	off_t ra_offset = 0;
+	size_t stack_size = 0;
+	unsigned long scan_addr, scan_end;
+	uint32_t insn;
+	int sp_adjust = 0;
+	unsigned long ra_slot = 0;
+
+	uint8_t code_buf[CODE_BUF_SIZE];
+	unsigned long buf_base = 0;
+	ssize_t buf_len = 0;
+
+	scan_addr = frame->pc - arch.unwind_ops.insn_size;
+
+	if (max_check >= frame->pc)
+		scan_end = 0;
+	else
+		scan_end = frame->pc - max_check;
+
+	while (scan_addr >= scan_end) {
+		/* Refill code buffer when needed */
+		if (buf_len == 0 || scan_addr < buf_base) {
+			if (scan_addr + arch.unwind_ops.insn_size < CODE_BUF_SIZE)
+				buf_base = 0;
+			else
+				buf_base = scan_addr + arch.unwind_ops.insn_size - CODE_BUF_SIZE;
+
+			if (buf_base < scan_end)
+				buf_base = scan_end & ~((unsigned long)arch.unwind_ops.insn_size - 1);
+
+			size_t to_read = scan_addr + arch.unwind_ops.insn_size - buf_base;
+			buf_len = readmem(buf_base, code_buf, to_read,
+					  "read code chunk", UVADDR);
+			if (buf_len <= 0) {
+				unsigned long next_page = (buf_base & SYS_PAGE_MASK) + SYS_PAGE_SIZE;
+				pr_trace("Failed to read at 0x%lx, advancing scan_end to 0x%lx\n",
+					 buf_base, next_page);
+				scan_end = next_page;
+				buf_len = 0;
+				continue;
+			}
+		}
+
+		size_t offset = scan_addr - buf_base;
+		if (offset + arch.unwind_ops.insn_size > (size_t)buf_len) {
+			pr_verbose("Offset out of buffer range\n");
+			return UNWIND_FAIL;
+		}
+		memcpy(&insn, code_buf + offset, arch.unwind_ops.insn_size);
+
+		if (arch.unwind_ops.is_sp_move_ins(insn, &sp_adjust)) {
+			if (sp_adjust < 0) {
+				stack_size = (size_t)(-sp_adjust);
+				pr_trace("sp_move: pc=0x%lx insn=0x%08x adjust=%d\n",
+					 scan_addr, insn, sp_adjust);
+				break;
+			}
+		} else if (arch.unwind_ops.is_ra_save_ins(insn, &ra_slot)) {
+			pr_trace("ra_save: pc=0x%lx insn=0x%08x slot=%lu\n",
+				 scan_addr, insn, ra_slot);
+			ra_offset = (off_t)ra_slot;
+		}
+
+		if (scan_addr < arch.unwind_ops.insn_size)
+			break;
+		scan_addr -= arch.unwind_ops.insn_size;
+	}
+
+	if (!stack_size)
+		return UNWIND_LEAF;
+
+	if (ra_offset) {
+		unsigned long ra_addr = frame->sp + ra_offset;
+		unsigned long new_ra;
+		if (readmem(ra_addr, &new_ra, arch.unwind_ops.ra_width,
+			    "read RA from stack", UVADDR) != (ssize_t)arch.unwind_ops.ra_width) {
+			pr_trace("RA slot unreadable at 0x%lx; treating as end of backtrace\n",
+				 ra_addr);
+			return UNWIND_FAIL;
+		}
+		frame->ra = new_ra;
+		frame->pc = new_ra;
+	} else if (frame->ra != 0) {
+		/* No RA slot found — use existing RA as fallback (e.g. from registers) */
+		frame->pc = frame->ra;
+	} else {
+		fprintf(stderr, "No RA found and frame->ra is 0\n");
+		return UNWIND_FAIL;
+	}
+
+	frame->sp += stack_size;
+
+	pr_trace("unwound: pc=0x%lx ra=0x%lx sp=0x%lx\n",
+		 frame->pc, frame->ra, frame->sp);
+	return UNWIND_OK;
+}
+
+/*
+ * Try signal-frame recovery if the arch hints that PC looks like a
+ * signal trampoline.
+ */
+static int try_signal_frame_unwind(struct stackframe *frame)
+{
+	if (!arch.unwind_ops.signal_layout)
+		return -1;
+
+	bool likely = true;
+	if (arch.unwind_ops.likely_signal_pc)
+		likely = arch.unwind_ops.likely_signal_pc(frame->pc);
+
+	if (!likely)
+		return -1;
+
+	struct stackframe candidate = *frame;
+	if (unwind_signal_frame(&candidate) != 0)
+		return -1;
+
+	pr_verbose("signal-frame: pc 0x%lx->0x%lx sp 0x%lx->0x%lx ra 0x%lx->0x%lx\n",
+		 frame->pc, candidate.pc, frame->sp, candidate.sp,
+		 frame->ra, candidate.ra);
+	*frame = candidate;
+	return 0;
+}
+
+/*
+ * Public entry point: unwind one user-space frame.
+ *
+ * Tries, in order:
+ *   1. Signal-frame recovery
+ *   2. Prologue-scan unwinding
+ *
+ * Returns UNWIND_OK, UNWIND_LEAF, or UNWIND_FAIL.
+ */
+int unwind_user_frame(struct stackframe *frame, unsigned int max_check)
+{
+	int ret;
+
+	if (!frame->pc || !frame->sp) {
+		fprintf(stderr, "error: pc or sp is zero: pc=0x%lx sp=0x%lx\n",
+			frame->pc, frame->sp);
+		return UNWIND_FAIL;
+	}
+
+	if (try_signal_frame_unwind(frame) == 0)
+		return UNWIND_OK;
+
+	ret = unwind_prologue_scan(frame, max_check);
+	if (ret != UNWIND_OK)
+		return ret;
+
+	/* Optional arch-specific fixup after unwinding. */
+	if (arch.unwind_ops.post_unwind_fixup) {
+		if (arch.unwind_ops.post_unwind_fixup(frame, max_check) != 0)
+			return UNWIND_FAIL;
+	}
+
+	return UNWIND_OK;
+}
\ No newline at end of file
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.