The Linux Kernel Mailing List
 help / color / mirror / Atom feed
* [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling
@ 2026-08-18 14:49 Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 01/25] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
                   ` (25 more replies)
  0 siblings, 26 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

This series adds support for parsing DWARF Call Frame Information (CFI)
from the .eh_frame_hdr and .eh_frame sections of user space ELF files.

The code is based on the deferred unwind user work originally done for
SFrame by Josh, Steven, and myself:

  v4 : https://lore.kernel.org/all/cover.1737511963.git.jpoimboe@kernel.org/
  v10: https://lore.kernel.org/all/20250827201548.448472904@kernel.org/
  v16: https://lore.kernel.org/all/20260521142546.3908498-1-jremus@linux.ibm.com/

The goal is to make user space stack traces available in-kernel without
requiring frame pointers and without copying large parts of the user
stack for later processing.

Today, reliable user stack traces from the kernel generally requires
frame pointers.  Otherwise, profilers such as perf have to copy large
amounts of user space stack into the kernel ring buffer and process it
later.  Frame pointers are simple and robust, but enabling them for
all executables and libraries has a performance cost.

Another issue is that the frame layout can vary between compilers and
architectures, and on architectures such as s390 there is no defined
frame layout which allows reliable frame-pointer based stack tracing.
The only way to perform user space profiling on there architectures is
to copy the user space into the kernel buffer.


The .eh_frame section is already emitted by most toolchains on most
architectures unless explicitly disabled.  It contains DWARF CFI
describing how to recover the caller state at any point in a function.
The .eh_frame_hdr section provides a binary search table for looking
up the Frame Description Entry (FDE) for a given instruction pointer
(IP).

Because the .eh_frame_hdr and .eh_frame sections live in the ELF file,
they need to be faulted in when used.  This means that walking the user
space stack requires being in a faultable context.  As profilers like
perf request a stack trace in interrupt or NMI context, the walking
cannot be done when requested.  This series reuses the deferred unwind
user framework, that performed the actual user stack trace is later in
a faultable context, before the task returns to user space.

This series implements .eh_frame[_hdr] support for the deferred unwind
user code and enables it for x86-64 and s390.

It intentionally not implement a complete DWARF unwinder.  It evaluates
only the subset of DWARF CFI needed for stack tracing:

  - Call Frame Address (CFA):  Using rule from DWARF CFI.

  - Stack pointer (SP):  Using an implicit rule based on the CFA
    definition (SP = CFA for most architectures).

  - Frame pointer (FP):  Using rule from DWARF CFI.

  - Return address (RA):  Using rule from DWARF CFI.

Unsupported CFI instructions, unsupported expressions, invalid data, or
user memory faults stop the stack tracing safely and results in a partial
stack trace.


This series applies on top of v7.2 tag:

  git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git  v7.2

The to be stack-traced user space executables and libraries need to
contain .eh_frame_hdr and .eh_frame sections as well as a GNU_EH_FRAME
PHDR.

Namhyung Kim's related perf tools deferred callchain support can be used
for testing, for example:

  perf record --call-graph fp,defer ...
  perf report
  perf script


Why .eh_frame?

This series is not meant to replace or undermine the SFrame work.
SFrame remains the simpler and more purpose-built format for user stack
tracing.  The motivation for .eh_frame support is pragmatic:  .eh_frame
is already widely deployed today.

- Availability and maturity:  .eh_frame is already present in most ELF
  binaries for C++ exception handling.  It has been used in production
  for decades for exception handling and debugger stack unwinding.

- Toolchain support:  .eh_frame is supported across all major compilers
  and architectures today, whereas .sframe adoption is still emerging.

- Size:  .sframe would be added in addition to existing .eh_frame[_hdr]
  rather than replacing it, increasing the ELF file size. [1]


Addressing historical DWARF concerns:

Using DWARF for kernel unwinding has a bad history.  Previous attempts
were complex, fragile, slow, and hard to maintain.  Hand-written
assembly and the complexity of the DWARF state machine were among the
reasons the simpler ORC kernel unwind format was developed. [2,3,4]

However, this implementation for user space stack tracing differs from
those problematic kernel unwinding attempts:

- It stack traces user space, not kernel.

- It runs in a deferred, faultable context, not in NMI, interrupt, or
  oops context.

- It may return partial stack traces.  Bad CFI, unsupported operations,
  invalid user memory, or faults are allowed to terminate the unwind.

- It implements only the CFI subset needed for stack tracing, not a
  general DWARF unwinder.

- It does not include a general-purpose DWARF expression VM.  Expression
  size is bounded.  Only a small number of pattern-matched expressions
  is supported (e.g. DRAP and PLT expressions on x86).  Unsupported
  expressions cause stack tracing to fail safely.

- All user memory access uses [unsafe_]get_user() with proper bounds
  checking and fault handling.

- Corruption detection with automatic section removal on invalid
  .eh_frame prevents further stack tracing attempts.


Limitations and future work:

- CIE version 1 support only and no DWARF64 support, as I have not run
  into either during my testing.

- Signal frames are not handled yet.  An architecture hook could support
  unwinding through FDEs whose CIE augmentation contains 'S' (signal
  frame), similar to Glibc's SFrame backtrace() support.  See also my
  "[RFC PATCH v1 0/5] s390: Signal frame user space unwinding". [5]

- x86-32, x86-x32, and 32-bit compat mode support not implemented yet.

- CIE caching would be useful.  Reading an FDE requires reading its
  referenced CIE first to obtain the FDE encoding.  Most .eh_frame
  sections have only a very small number of CIEs, often one default
  CIE shared by most FDEs and possibly one signal frame CIE.  Caching
  the last CIE per section, together with the initial CFA, FP, and RA
  rules, would avoid repeated CIE parsing and initial CFI instruction
  processing.


[1]: https://lore.kernel.org/all/CAN30aBFVDxeoXApn_g_Hw0Ayhi4V=m7CcX8UDO6ZDTi6xA-3Pg@mail.gmail.com/
[2]: https://lwn.net/Articles/727553/
[3]: https://lkml.org/lkml/2012/2/10/356
[4]: https://lkml.org/lkml/2017/5/20/165
[5]: https://lore.kernel.org/all/20260127153331.2902504-1-jremus@linux.ibm.com/


Patches 1-6 add base functionality to unwind user to support .eh_frame-
based (or .sframe-based) unwinding.  Patches originate from my latest
.sframe patch series.

Patches 7-10 add the basic infrastructure for reading .eh_frame_hdr and
.eh_frame sections and storing them in a per-mm maple tree.

Patches 11-14 wire up the eh_frame infrastructure to the unwind user
framework and add error handling and debugging support.

Patch 15 duplicates registered .eh_frame_hdr section data on clone/fork.

Patch 16 adds an experimental linear .eh_frame search fallback, for the
rare case, that .eh_frame_hdr does not contain a binary search table.

Patch 17 improves .eh_frame DWARF CFI instruction processing.

Patch 18 enables architectures to implement selected DWARF expressions
in CFI instructions.

Patches 19-22 enable .eh_frame unwinding on x86-64 with minimal DWARF
expression support for DRAP and PLT expressions.

Patches 23-24 enable .eh_frame unwinding on s390.

Patch 25 adds a prctl() interface for (un)registering .eh_frame_hdr
sections for shared libraries.  I will send a related test-patch for
Glibc separately.


Regards,
Jens


Jens Remus (24):
  unwind_user: Add generic and arch-specific headers to MAINTAINERS
  unwind_user: Stop when reaching an outermost frame
  unwind_user: Enable archs that pass RA in a register
  unwind_user: Flexible FP/RA recovery rules
  unwind_user: Flexible CFA recovery rules
  unwind_user: Enable archs that define CFA = SP_callsite + offset
  unwind_user/eh_frame: Add support for reading .eh_frame_hdr section
  unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple
    tree
  unwind_user/eh_frame: Add support for reading .eh_frame section
  unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables
  unwind_user/eh_frame: Wire up unwind_user to eh_frame
  unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected
    corruption
  unwind_user/eh_frame: Show file name in debug output
  unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
  unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section
    data on clone/fork
  unwind_user/eh_frame: Add linear .eh_frame search fallback
  unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size
  unwind_user/eh_frame: Add support for DWARF expressions
  unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
  unwind_user/eh_frame/x86: Handle PLT expressions
  unwind_user/eh_frame/x86: Handle DRAP expressions
  s390/ptrace: Provide frame_pointer()
  unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390
  unwind_user/eh_frame: Add prctl() interface for (un)registering
    .eh_frame_hdr sections

Josh Poimboeuf (1):
  x86/uaccess: Add unsafe_copy_from_user() implementation

 MAINTAINERS                                  |    5 +
 arch/Kconfig                                 |   35 +
 arch/s390/Kconfig                            |    1 +
 arch/s390/include/asm/ptrace.h               |    6 +
 arch/s390/include/asm/unwind_user.h          |   71 +
 arch/s390/include/asm/unwind_user_eh_frame.h |   24 +
 arch/x86/Kconfig                             |    1 +
 arch/x86/include/asm/mmu.h                   |    2 +-
 arch/x86/include/asm/uaccess.h               |   39 +-
 arch/x86/include/asm/unwind_user.h           |   77 +-
 arch/x86/include/asm/unwind_user_eh_frame.h  |  153 ++
 fs/binfmt_elf.c                              |   49 +-
 include/asm-generic/Kbuild                   |    1 +
 include/asm-generic/unwind_user_eh_frame.h   |   84 +
 include/linux/eh_frame.h                     |  104 +
 include/linux/mm_types.h                     |    3 +
 include/linux/unwind_user.h                  |   20 +
 include/linux/unwind_user_eh_frame_types.h   |   41 +
 include/linux/unwind_user_types.h            |   51 +-
 include/uapi/linux/eh_frame.h                |   14 +
 include/uapi/linux/prctl.h                   |    4 +
 kernel/fork.c                                |   10 +
 kernel/sys.c                                 |   11 +
 kernel/unwind/Makefile                       |    3 +-
 kernel/unwind/eh_frame.c                     | 1771 ++++++++++++++++++
 kernel/unwind/eh_frame.h                     |   83 +
 kernel/unwind/eh_frame_debug.h               |   71 +
 kernel/unwind/user.c                         |  142 +-
 mm/init-mm.c                                 |    2 +
 mm/mmap.c                                    |    5 +
 30 files changed, 2842 insertions(+), 41 deletions(-)
 create mode 100644 arch/s390/include/asm/unwind_user.h
 create mode 100644 arch/s390/include/asm/unwind_user_eh_frame.h
 create mode 100644 arch/x86/include/asm/unwind_user_eh_frame.h
 create mode 100644 include/asm-generic/unwind_user_eh_frame.h
 create mode 100644 include/linux/eh_frame.h
 create mode 100644 include/linux/unwind_user_eh_frame_types.h
 create mode 100644 include/uapi/linux/eh_frame.h
 create mode 100644 kernel/unwind/eh_frame.c
 create mode 100644 kernel/unwind/eh_frame.h
 create mode 100644 kernel/unwind/eh_frame_debug.h


base-commit: 8d3ae59288f1e7d58d76558a6ee96d533bc5019f
-- 
2.53.0


^ permalink raw reply	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 01/25] unwind_user: Add generic and arch-specific headers to MAINTAINERS
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame Jens Remus
                   ` (24 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James, Dylan Hatch

Commit 71753c6ed2bf ("unwind_user: Add user space unwinding API with
frame pointer support") introduced include/asm-generic/unwind_user.h
without adding it to MAINTAINERS, as well as any future arch-specific
versions such as the one added by commit 49cf34c0815f
("unwind_user/x86: Enable frame pointer unwinding on x86") which
introduced arch/x86/include/asm/unwind_user.h.

Suggested-by: Dylan Hatch <dylanbhatch@google.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-2-jremus@linux.ibm.com/

 MAINTAINERS | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 8014b9f8253e..e5738a250d63 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28201,6 +28201,8 @@ USERSPACE STACK UNWINDING
 M:	Josh Poimboeuf <jpoimboe@kernel.org>
 M:	Steven Rostedt <rostedt@goodmis.org>
 S:	Maintained
+F:	arch/*/include/asm/unwind_user.h
+F:	include/asm-generic/unwind_user.h
 F:	include/linux/unwind*.h
 F:	kernel/unwind/
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 01/25] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 03/25] unwind_user: Enable archs that pass RA in a register Jens Remus
                   ` (23 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Add an indication for an outermost frame to the unwind user frame
structure and stop unwinding when reaching an outermost frame.

This will be used by unwind user sframe, as SFrame may represent an
undefined return address as indication for an outermost frame.

Reviewed-by: Indu Bhagat <ibhagatgnu@gmail.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-9-jremus@linux.ibm.com/

 arch/x86/include/asm/unwind_user.h | 6 ++++--
 include/linux/unwind_user_types.h  | 1 +
 kernel/unwind/user.c               | 6 ++++++
 3 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
index 6e469044e4de..2dfb5ef11e36 100644
--- a/arch/x86/include/asm/unwind_user.h
+++ b/arch/x86/include/asm/unwind_user.h
@@ -23,13 +23,15 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 	.cfa_off	=  2*(ws),			\
 	.ra_off		= -1*(ws),			\
 	.fp_off		= -2*(ws),			\
-	.use_fp		= true,
+	.use_fp		= true,				\
+	.outermost	= false,
 
 #define ARCH_INIT_USER_FP_ENTRY_FRAME(ws)		\
 	.cfa_off	=  1*(ws),			\
 	.ra_off		= -1*(ws),			\
 	.fp_off		= 0,				\
-	.use_fp		= false,
+	.use_fp		= false,			\
+	.outermost	= false,
 
 static inline bool unwind_user_at_function_start(struct pt_regs *regs)
 {
diff --git a/include/linux/unwind_user_types.h b/include/linux/unwind_user_types.h
index 412729a269bc..4fc3dd9b4e29 100644
--- a/include/linux/unwind_user_types.h
+++ b/include/linux/unwind_user_types.h
@@ -30,6 +30,7 @@ struct unwind_user_frame {
 	s32 ra_off;
 	s32 fp_off;
 	bool use_fp;
+	bool outermost;
 };
 
 struct unwind_user_state {
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index 90ab3c1a205e..28091ece830c 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -31,6 +31,12 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 {
 	unsigned long cfa, fp, ra;
 
+	/* Stop unwinding when reaching an outermost frame. */
+	if (frame->outermost) {
+		state->done = true;
+		return 0;
+	}
+
 	/* Get the Canonical Frame Address (CFA) */
 	if (frame->use_fp) {
 		if (state->fp < state->sp)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 03/25] unwind_user: Enable archs that pass RA in a register
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 01/25] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
                   ` (22 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Not all architectures/ABIs pass the return address (RA) on the stack on
function entry, like x86-64 does due to its CALL instruction pushing
the RA onto the stack.  Architectures/ABIs, such as s390, also do not
require the RA to be saved on the stack in the function prologue.  In
particular, the RA may never be saved to the stack at all, such as in
leaf functions.  Unwinding must therefore not assume the presence of a
RA saved on stack for the topmost frame.

Treat a RA offset from CFA of zero as indication that the RA is not
saved (on the stack).  For the topmost frame treat it as indication that
the RA is in the link/RA register, such as on arm64 and s390, and obtain
it from there.  For non-topmost frames treat it as error, as the RA must
be saved.

Additionally allow the SP to be unchanged in the topmost frame, for
architectures where SP at function entry == SP at call site, such as
arm64 and s390.

Note that treating a RA offset from CFA of zero as indication that
the RA is not saved on the stack additionally allows for architectures,
such as s390, where the frame pointer (FP) may be saved without the RA
being saved as well.  Provided that such architectures represent this
in SFrame by encoding the "missing" RA offset using a padding RA offset
with a value of zero.

Reviewed-by: Indu Bhagat <ibhagatgnu@gmail.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-14-jremus@linux.ibm.com/

 include/linux/unwind_user.h | 10 ++++++++++
 kernel/unwind/user.c        | 20 ++++++++++++++++----
 2 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/include/linux/unwind_user.h b/include/linux/unwind_user.h
index 64618618febd..941cef652435 100644
--- a/include/linux/unwind_user.h
+++ b/include/linux/unwind_user.h
@@ -23,6 +23,16 @@ static inline bool unwind_user_at_function_start(struct pt_regs *regs)
 #define unwind_user_at_function_start unwind_user_at_function_start
 #endif
 
+#ifndef unwind_user_get_ra_reg
+static inline int unwind_user_get_ra_reg(unsigned long *val)
+{
+	pr_debug("%s (%d): %s() not implemented\n",
+		 current->comm, current->pid, __func__);
+	return -EINVAL;
+}
+#define unwind_user_get_ra_reg unwind_user_get_ra_reg
+#endif
+
 int unwind_user(struct unwind_stacktrace *trace, unsigned int max_entries);
 
 #endif /* _LINUX_UNWIND_USER_H */
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index 28091ece830c..f8a97d4167f0 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -2,6 +2,9 @@
 /*
 * Generic interfaces for unwinding user space
 */
+
+#define pr_fmt(fmt)	"unwind_user: " fmt
+
 #include <linux/kernel.h>
 #include <linux/sched.h>
 #include <linux/sched/task_stack.h>
@@ -47,8 +50,12 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 	}
 	cfa += frame->cfa_off;
 
-	/* Make sure that stack is not going in wrong direction */
-	if (cfa <= state->sp)
+	/*
+	 * Make sure that stack is not going in wrong direction.  Allow SP
+	 * to be unchanged for the topmost frame, by subtracting topmost,
+	 * which is either 0 or 1.
+	 */
+	if (cfa <= state->sp - state->topmost)
 		return -EINVAL;
 
 	/* Make sure that the address is word aligned */
@@ -56,8 +63,13 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 		return -EINVAL;
 
 	/* Get the Return Address (RA) */
-	if (get_user_word(&ra, cfa, frame->ra_off, state->ws))
-		return -EINVAL;
+	if (frame->ra_off) {
+		if (get_user_word(&ra, cfa, frame->ra_off, state->ws))
+			return -EINVAL;
+	} else {
+		if (!state->topmost || unwind_user_get_ra_reg(&ra))
+			return -EINVAL;
+	}
 
 	/* Get the Frame Pointer (FP) */
 	if (frame->fp_off && get_user_word(&fp, cfa, frame->fp_off, state->ws))
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (2 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 03/25] unwind_user: Enable archs that pass RA in a register Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
                   ` (21 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

To enable support for SFrame V3 flexible FDEs with a subsequent patch,
add support for the following flexible frame pointer (FP) and return
address (RA) recovery rules:

  FP/RA = *(CFA + offset)
  FP/RA = register + offset
  FP/RA = *(register + offset)

Note that FP/RA recovery rules that use arbitrary register contents are
only valid when in the topmost frame, as their contents are otherwise
unknown.

This also enables unwinding of user space for architectures, such as
s390, that may save the frame pointer (FP) and/or return address (RA) in
other registers, for instance when in a leaf function.

Reviewed-by: Indu Bhagat <ibhagatgnu@gmail.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-15-jremus@linux.ibm.com/

 arch/x86/include/asm/unwind_user.h | 21 +++++++--
 include/linux/unwind_user.h        | 10 +++++
 include/linux/unwind_user_types.h  | 23 +++++++++-
 kernel/unwind/user.c               | 70 +++++++++++++++++++++++++++---
 4 files changed, 111 insertions(+), 13 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
index 2dfb5ef11e36..9c3417be4283 100644
--- a/arch/x86/include/asm/unwind_user.h
+++ b/arch/x86/include/asm/unwind_user.h
@@ -21,15 +21,26 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 
 #define ARCH_INIT_USER_FP_FRAME(ws)			\
 	.cfa_off	=  2*(ws),			\
-	.ra_off		= -1*(ws),			\
-	.fp_off		= -2*(ws),			\
+	.ra		= {				\
+		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
+		.offset		= -1*(ws),		\
+			},				\
+	.fp		= {				\
+		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
+		.offset		= -2*(ws),		\
+			},				\
 	.use_fp		= true,				\
 	.outermost	= false,
 
 #define ARCH_INIT_USER_FP_ENTRY_FRAME(ws)		\
 	.cfa_off	=  1*(ws),			\
-	.ra_off		= -1*(ws),			\
-	.fp_off		= 0,				\
+	.ra		= {				\
+		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
+		.offset		= -1*(ws),		\
+			},				\
+	.fp		= {				\
+		.rule		= UNWIND_USER_RULE_RETAIN,\
+			},				\
 	.use_fp		= false,			\
 	.outermost	= false,
 
@@ -41,4 +52,6 @@ static inline bool unwind_user_at_function_start(struct pt_regs *regs)
 
 #endif /* CONFIG_HAVE_UNWIND_USER_FP */
 
+#include <asm-generic/unwind_user.h>
+
 #endif /* _ASM_X86_UNWIND_USER_H */
diff --git a/include/linux/unwind_user.h b/include/linux/unwind_user.h
index 941cef652435..b2b1c2572fed 100644
--- a/include/linux/unwind_user.h
+++ b/include/linux/unwind_user.h
@@ -33,6 +33,16 @@ static inline int unwind_user_get_ra_reg(unsigned long *val)
 #define unwind_user_get_ra_reg unwind_user_get_ra_reg
 #endif
 
+#ifndef unwind_user_get_reg
+static inline int unwind_user_get_reg(unsigned long *val, unsigned int regnum)
+{
+	pr_debug("%s (%d): %s(%u) not implemented\n",
+		 current->comm, current->pid, __func__, regnum);
+	return -EINVAL;
+}
+#define unwind_user_get_reg unwind_user_get_reg
+#endif
+
 int unwind_user(struct unwind_stacktrace *trace, unsigned int max_entries);
 
 #endif /* _LINUX_UNWIND_USER_H */
diff --git a/include/linux/unwind_user_types.h b/include/linux/unwind_user_types.h
index 4fc3dd9b4e29..1b5db76e886a 100644
--- a/include/linux/unwind_user_types.h
+++ b/include/linux/unwind_user_types.h
@@ -25,10 +25,29 @@ struct unwind_stacktrace {
 	unsigned long	*entries;
 };
 
+#define UNWIND_USER_RULE_DEREF			BIT(31)
+
+enum unwind_user_rule {
+	UNWIND_USER_RULE_RETAIN,		/* entity = entity */
+	UNWIND_USER_RULE_CFA_OFFSET,		/* entity = CFA + offset */
+	UNWIND_USER_RULE_REG_OFFSET,		/* entity = register + offset */
+	/* DEREF variants */
+	UNWIND_USER_RULE_CFA_OFFSET_DEREF =	/* entity = *(CFA + offset) */
+		UNWIND_USER_RULE_CFA_OFFSET | UNWIND_USER_RULE_DEREF,
+	UNWIND_USER_RULE_REG_OFFSET_DEREF =	/* entity = *(register + offset) */
+		UNWIND_USER_RULE_REG_OFFSET | UNWIND_USER_RULE_DEREF,
+};
+
+struct unwind_user_rule_data {
+	enum unwind_user_rule rule;
+	s32 offset;
+	unsigned int regnum;
+};
+
 struct unwind_user_frame {
 	s32 cfa_off;
-	s32 ra_off;
-	s32 fp_off;
+	struct unwind_user_rule_data ra;
+	struct unwind_user_rule_data fp;
 	bool use_fp;
 	bool outermost;
 };
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index f8a97d4167f0..f5d68e86d72b 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -11,6 +11,17 @@
 #include <linux/unwind_user.h>
 #include <linux/uaccess.h>
 
+#ifdef CONFIG_DYNAMIC_DEBUG
+
+#define dbg_once(fmt, ...)							\
+	pr_debug_once("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+
+#else /* !CONFIG_DYNAMIC_DEBUG */
+
+#define dbg_once(args...)		no_printk(args)
+
+#endif /* !CONFIG_DYNAMIC_DEBUG */
+
 #define for_each_user_frame(state) \
 	for (unwind_user_start(state); !(state)->done; unwind_user_next(state))
 
@@ -63,22 +74,67 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 		return -EINVAL;
 
 	/* Get the Return Address (RA) */
-	if (frame->ra_off) {
-		if (get_user_word(&ra, cfa, frame->ra_off, state->ws))
-			return -EINVAL;
-	} else {
+	switch (frame->ra.rule) {
+	case UNWIND_USER_RULE_RETAIN:
 		if (!state->topmost || unwind_user_get_ra_reg(&ra))
 			return -EINVAL;
+		break;
+	case UNWIND_USER_RULE_CFA_OFFSET:
+		/*
+		 * RA = CFA + offset does not make sense.
+		 * A return address cannot legitimately be a stack address.
+		 */
+		dbg_once("UNWIND_USER_RULE_CFA_OFFSET invalid for RA\n");
+		return -EINVAL;
+	case UNWIND_USER_RULE_CFA_OFFSET_DEREF:
+		ra = cfa + frame->ra.offset;
+		break;
+	case UNWIND_USER_RULE_REG_OFFSET:
+	case UNWIND_USER_RULE_REG_OFFSET_DEREF:
+		if (!state->topmost || unwind_user_get_reg(&ra, frame->ra.regnum))
+			return -EINVAL;
+		ra += frame->ra.offset;
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		return -EINVAL;
 	}
+	if (frame->ra.rule & UNWIND_USER_RULE_DEREF &&
+	    get_user_word(&ra, ra, 0, state->ws))
+		return -EINVAL;
 
 	/* Get the Frame Pointer (FP) */
-	if (frame->fp_off && get_user_word(&fp, cfa, frame->fp_off, state->ws))
+	switch (frame->fp.rule) {
+	case UNWIND_USER_RULE_RETAIN:
+		fp = state->fp;
+		break;
+	case UNWIND_USER_RULE_CFA_OFFSET:
+		/*
+		 * FP = CFA + offset is currently not used for FP
+		 * (e.g. SFrame cannot represent this rule).
+		 */
+		dbg_once("UNWIND_USER_RULE_CFA_OFFSET unsupported for FP\n");
+		return -EINVAL;
+	case UNWIND_USER_RULE_CFA_OFFSET_DEREF:
+		fp = cfa + frame->fp.offset;
+		break;
+	case UNWIND_USER_RULE_REG_OFFSET:
+	case UNWIND_USER_RULE_REG_OFFSET_DEREF:
+		if (!state->topmost || unwind_user_get_reg(&fp, frame->fp.regnum))
+			return -EINVAL;
+		fp += frame->fp.offset;
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		return -EINVAL;
+	}
+	if (frame->fp.rule & UNWIND_USER_RULE_DEREF &&
+	    get_user_word(&fp, fp, 0, state->ws))
 		return -EINVAL;
 
 	state->ip = ra;
 	state->sp = cfa;
-	if (frame->fp_off)
-		state->fp = fp;
+	state->fp = fp;
 	state->topmost = false;
 	return 0;
 }
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 05/25] unwind_user: Flexible CFA recovery rules
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (3 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 06/25] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
                   ` (20 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

To enable support for SFrame V3 flexible FDEs with a subsequent patch,
add support for the following flexible Canonical Frame Address (CFA)
recovery rules:

  CFA = SP + offset
  CFA = *(SP + offset)
  CFA = FP + offset
  CFA = *(FP + offset)
  CFA = register + offset
  CFA = *(register + offset)

Note that CFA recovery rules that use arbitrary register contents are
only valid when in the topmost frame, as their contents are otherwise
unknown.

Reviewed-by: Indu Bhagat <ibhagatgnu@gmail.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-16-jremus@linux.ibm.com/

 arch/x86/include/asm/unwind_user.h | 12 ++++++++----
 include/linux/unwind_user_types.h  | 22 ++++++++++++++++++++--
 kernel/unwind/user.c               | 24 ++++++++++++++++++++----
 3 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
index 9c3417be4283..f38f7c5ff1de 100644
--- a/arch/x86/include/asm/unwind_user.h
+++ b/arch/x86/include/asm/unwind_user.h
@@ -20,7 +20,10 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 #ifdef CONFIG_HAVE_UNWIND_USER_FP
 
 #define ARCH_INIT_USER_FP_FRAME(ws)			\
-	.cfa_off	=  2*(ws),			\
+	.cfa		= {				\
+		.rule		= UNWIND_USER_CFA_RULE_FP_OFFSET,\
+		.offset		=  2*(ws),		\
+			},				\
 	.ra		= {				\
 		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
 		.offset		= -1*(ws),		\
@@ -29,11 +32,13 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
 		.offset		= -2*(ws),		\
 			},				\
-	.use_fp		= true,				\
 	.outermost	= false,
 
 #define ARCH_INIT_USER_FP_ENTRY_FRAME(ws)		\
-	.cfa_off	=  1*(ws),			\
+	.cfa		= {				\
+		.rule		= UNWIND_USER_CFA_RULE_SP_OFFSET,\
+		.offset		=  1*(ws),		\
+			},				\
 	.ra		= {				\
 		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
 		.offset		= -1*(ws),		\
@@ -41,7 +46,6 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 	.fp		= {				\
 		.rule		= UNWIND_USER_RULE_RETAIN,\
 			},				\
-	.use_fp		= false,			\
 	.outermost	= false,
 
 static inline bool unwind_user_at_function_start(struct pt_regs *regs)
diff --git a/include/linux/unwind_user_types.h b/include/linux/unwind_user_types.h
index 1b5db76e886a..670ac860ae76 100644
--- a/include/linux/unwind_user_types.h
+++ b/include/linux/unwind_user_types.h
@@ -27,6 +27,25 @@ struct unwind_stacktrace {
 
 #define UNWIND_USER_RULE_DEREF			BIT(31)
 
+enum unwind_user_cfa_rule {
+	UNWIND_USER_CFA_RULE_SP_OFFSET,		/* CFA = SP + offset */
+	UNWIND_USER_CFA_RULE_FP_OFFSET,		/* CFA = FP + offset */
+	UNWIND_USER_CFA_RULE_REG_OFFSET,	/* CFA = register + offset */
+	/* DEREF variants */
+	UNWIND_USER_CFA_RULE_SP_OFFSET_DEREF =	/* CFA = *(SP + offset) */
+		UNWIND_USER_CFA_RULE_SP_OFFSET | UNWIND_USER_RULE_DEREF,
+	UNWIND_USER_CFA_RULE_FP_OFFSET_DEREF =	/* CFA = *(FP + offset) */
+		UNWIND_USER_CFA_RULE_FP_OFFSET | UNWIND_USER_RULE_DEREF,
+	UNWIND_USER_CFA_RULE_REG_OFFSET_DEREF =	/* CFA = *(register + offset) */
+		UNWIND_USER_CFA_RULE_REG_OFFSET | UNWIND_USER_RULE_DEREF,
+};
+
+struct unwind_user_cfa_rule_data {
+	enum unwind_user_cfa_rule rule;
+	s32 offset;
+	unsigned int regnum;
+};
+
 enum unwind_user_rule {
 	UNWIND_USER_RULE_RETAIN,		/* entity = entity */
 	UNWIND_USER_RULE_CFA_OFFSET,		/* entity = CFA + offset */
@@ -45,10 +64,9 @@ struct unwind_user_rule_data {
 };
 
 struct unwind_user_frame {
-	s32 cfa_off;
+	struct unwind_user_cfa_rule_data cfa;
 	struct unwind_user_rule_data ra;
 	struct unwind_user_rule_data fp;
-	bool use_fp;
 	bool outermost;
 };
 
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index f5d68e86d72b..9df5040df9b3 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -52,14 +52,30 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 	}
 
 	/* Get the Canonical Frame Address (CFA) */
-	if (frame->use_fp) {
+	switch (frame->cfa.rule) {
+	case UNWIND_USER_CFA_RULE_SP_OFFSET:
+	case UNWIND_USER_CFA_RULE_SP_OFFSET_DEREF:
+		cfa = state->sp;
+		break;
+	case UNWIND_USER_CFA_RULE_FP_OFFSET:
+	case UNWIND_USER_CFA_RULE_FP_OFFSET_DEREF:
 		if (state->fp < state->sp)
 			return -EINVAL;
 		cfa = state->fp;
-	} else {
-		cfa = state->sp;
+		break;
+	case UNWIND_USER_CFA_RULE_REG_OFFSET:
+	case UNWIND_USER_CFA_RULE_REG_OFFSET_DEREF:
+		if (!state->topmost || unwind_user_get_reg(&cfa, frame->cfa.regnum))
+			return -EINVAL;
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		return -EINVAL;
 	}
-	cfa += frame->cfa_off;
+	cfa += frame->cfa.offset;
+	if (frame->cfa.rule & UNWIND_USER_RULE_DEREF &&
+	    get_user_word(&cfa, cfa, 0, state->ws))
+		return -EINVAL;
 
 	/*
 	 * Make sure that stack is not going in wrong direction.  Allow SP
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 06/25] unwind_user: Enable archs that define CFA = SP_callsite + offset
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (4 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
                   ` (19 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Most architectures define their CFA as the value of the stack pointer
(SP) at the call site in the previous frame, as suggested by the DWARF
standard.  Therefore the SP at call site can be unwound using an
implicitly assumed value offset from CFA rule with an offset of zero:

  .cfi_val_offset <SP>, 0

As a result the SP at call site computes as follows:

  SP = CFA

Enable unwinding of user space for architectures, such as s390, which
define their CFA as the value of the SP at the call site in the previous
frame with an offset.  Do so by enabling architectures to override the
default SP value offset from CFA of zero with an architecture-specific
one:

  .cfi_val_offset <SP>, offset

So that the SP at call site computes as follows:

  SP = CFA + offset

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v4 00/12] s390: SFrame user space unwinding"
    series and adjusted to .eh_frame:
    https://lore.kernel.org/all/20260127151926.2805123-6-jremus@linux.ibm.com/

 arch/x86/include/asm/unwind_user.h |  2 ++
 include/linux/unwind_user_types.h  |  1 +
 kernel/unwind/user.c               | 11 ++++++-----
 3 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
index f38f7c5ff1de..c96645c824d1 100644
--- a/arch/x86/include/asm/unwind_user.h
+++ b/arch/x86/include/asm/unwind_user.h
@@ -32,6 +32,7 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 		.rule		= UNWIND_USER_RULE_CFA_OFFSET_DEREF,\
 		.offset		= -2*(ws),		\
 			},				\
+	.sp_off		= 0,				\
 	.outermost	= false,
 
 #define ARCH_INIT_USER_FP_ENTRY_FRAME(ws)		\
@@ -46,6 +47,7 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 	.fp		= {				\
 		.rule		= UNWIND_USER_RULE_RETAIN,\
 			},				\
+	.sp_off		= 0,				\
 	.outermost	= false,
 
 static inline bool unwind_user_at_function_start(struct pt_regs *regs)
diff --git a/include/linux/unwind_user_types.h b/include/linux/unwind_user_types.h
index 670ac860ae76..4da058096259 100644
--- a/include/linux/unwind_user_types.h
+++ b/include/linux/unwind_user_types.h
@@ -67,6 +67,7 @@ struct unwind_user_frame {
 	struct unwind_user_cfa_rule_data cfa;
 	struct unwind_user_rule_data ra;
 	struct unwind_user_rule_data fp;
+	s32 sp_off;
 	bool outermost;
 };
 
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index 9df5040df9b3..830c620fe453 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -43,7 +43,7 @@ get_user_word(unsigned long *word, unsigned long base, int off, unsigned int ws)
 static int unwind_user_next_common(struct unwind_user_state *state,
 				   const struct unwind_user_frame *frame)
 {
-	unsigned long cfa, fp, ra;
+	unsigned long cfa, sp, fp, ra;
 
 	/* Stop unwinding when reaching an outermost frame. */
 	if (frame->outermost) {
@@ -77,16 +77,17 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 	    get_user_word(&cfa, cfa, 0, state->ws))
 		return -EINVAL;
 
+	/* Get the Stack Pointer (SP) */
+	sp = cfa + frame->sp_off;
 	/*
 	 * Make sure that stack is not going in wrong direction.  Allow SP
 	 * to be unchanged for the topmost frame, by subtracting topmost,
 	 * which is either 0 or 1.
 	 */
-	if (cfa <= state->sp - state->topmost)
+	if (sp <= state->sp - state->topmost)
 		return -EINVAL;
-
 	/* Make sure that the address is word aligned */
-	if (cfa & (state->ws - 1))
+	if (sp & (state->ws - 1))
 		return -EINVAL;
 
 	/* Get the Return Address (RA) */
@@ -149,7 +150,7 @@ static int unwind_user_next_common(struct unwind_user_state *state,
 		return -EINVAL;
 
 	state->ip = ra;
-	state->sp = cfa;
+	state->sp = sp;
 	state->fp = fp;
 	state->topmost = false;
 	return 0;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (5 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 06/25] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
                   ` (18 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

In preparation for unwinding user space stacks with .eh_frame, add
basic unwind user eh_frame infrastructure and support for reading the
.eh_frame_hdr section.

The .eh_frame_hdr section provides a binary search table for efficient
lookup of Frame Description Entries (FDEs) in the .eh_frame section
based on instruction pointer (IP).

eh_frame_add_section() reads the .eh_frame_hdr section and
unconditionally returns an error, so it is not very useful yet.  A
subsequent patch will improve that.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 MAINTAINERS              |   1 +
 arch/Kconfig             |   3 +
 include/linux/eh_frame.h |  45 +++++
 kernel/unwind/Makefile   |   3 +-
 kernel/unwind/eh_frame.c | 363 +++++++++++++++++++++++++++++++++++++++
 kernel/unwind/eh_frame.h |  30 ++++
 6 files changed, 444 insertions(+), 1 deletion(-)
 create mode 100644 include/linux/eh_frame.h
 create mode 100644 kernel/unwind/eh_frame.c
 create mode 100644 kernel/unwind/eh_frame.h

diff --git a/MAINTAINERS b/MAINTAINERS
index e5738a250d63..f19f5bb87f00 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28203,6 +28203,7 @@ M:	Steven Rostedt <rostedt@goodmis.org>
 S:	Maintained
 F:	arch/*/include/asm/unwind_user.h
 F:	include/asm-generic/unwind_user.h
+F:	include/linux/eh_frame.h
 F:	include/linux/unwind*.h
 F:	kernel/unwind/
 
diff --git a/arch/Kconfig b/arch/Kconfig
index fa7507ac8e13..60542d5e5731 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -486,6 +486,9 @@ config HAVE_HARDLOCKUP_DETECTOR_ARCH
 config UNWIND_USER
 	bool
 
+config HAVE_UNWIND_USER_EH_FRAME
+	bool
+
 config HAVE_UNWIND_USER_FP
 	bool
 	select UNWIND_USER
diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
new file mode 100644
index 000000000000..aaac2dd663d5
--- /dev/null
+++ b/include/linux/eh_frame.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_EH_FRAME_H
+#define _LINUX_EH_FRAME_H
+
+#ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
+
+struct eh_frame_section {
+	unsigned long	eh_frame_hdr_start;
+	unsigned long	eh_frame_hdr_end;
+	unsigned long	text_start;
+	unsigned long	text_end;
+
+	/* .eh_frame_hdr information */
+	unsigned long	eh_frame_start;
+	unsigned long	eh_frame_vma_end;
+	unsigned long	binary_search_table_start;
+	unsigned long	binary_search_table_end;
+	unsigned long	fde_count;
+	u8		binary_search_table_enc;
+};
+
+extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
+				unsigned long eh_frame_hdr_end,
+				unsigned long text_start,
+				unsigned long text_end);
+extern int eh_frame_remove_section(unsigned long eh_frame_hdr_start);
+
+#else /* !CONFIG_HAVE_UNWIND_USER_EH_FRAME */
+
+static inline int eh_frame_add_section(unsigned long eh_frame_hdr_start,
+				       unsigned long eh_frame_hdr_end,
+				       unsigned long text_start,
+				       unsigned long text_end)
+{
+	return -ENOSYS;
+}
+
+static inline int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
+{
+	return -ENOSYS;
+}
+
+#endif /* CONFIG_HAVE_UNWIND_USER_EH_FRAME */
+
+#endif /* _LINUX_EH_FRAME_H */
diff --git a/kernel/unwind/Makefile b/kernel/unwind/Makefile
index eae37bea54fd..2d97e2625d8a 100644
--- a/kernel/unwind/Makefile
+++ b/kernel/unwind/Makefile
@@ -1 +1,2 @@
- obj-$(CONFIG_UNWIND_USER)	+= user.o deferred.o
+ obj-$(CONFIG_UNWIND_USER)			+= user.o deferred.o
+ obj-$(CONFIG_HAVE_UNWIND_USER_EH_FRAME)	+= eh_frame.o
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
new file mode 100644
index 000000000000..8d2b638145bd
--- /dev/null
+++ b/kernel/unwind/eh_frame.c
@@ -0,0 +1,363 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Userspace eh_frame access functions
+ */
+
+#define pr_fmt(fmt)	"eh_frame: " fmt
+
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <linux/srcu.h>
+#include <linux/uaccess.h>
+#include <linux/mm.h>
+#include <linux/string_helpers.h>
+#include <linux/eh_frame.h>
+#include <linux/unwind_user_types.h>
+
+#include "eh_frame.h"
+
+#define dbg(fmt, ...)							\
+	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+
+#define UNSAFE_GET_USER_INC(to, from, end, label)			\
+({									\
+	typeof(to) __to;						\
+	if (from + sizeof(__to) > end)					\
+		return -EINVAL;						\
+	unsafe_get_user(__to, (typeof(to) __user *)from, label);	\
+	from += sizeof(__to);						\
+	to = __to;							\
+})
+
+static __always_inline int read_uleb128(unsigned long *addr, unsigned long end,
+					unsigned long *value)
+{
+	unsigned long cur = *addr;
+	unsigned long result = 0;
+	int shift = 0;
+	u8 byte;
+
+	do {
+		if (shift >= BITS_PER_LONG)
+			return -EINVAL;
+
+		UNSAFE_GET_USER_INC(byte, cur, end, Efault);
+		result |= (unsigned long)(byte & 0x7f) << shift;
+		shift += 7;
+	} while (byte & 0x80);
+
+	*value = result;
+	*addr = cur;
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int read_sleb128(unsigned long *addr, unsigned long end,
+					long *value)
+{
+	unsigned long cur = *addr;
+	long result = 0;
+	int shift = 0;
+	u8 byte;
+
+	do {
+		if (shift >= BITS_PER_LONG)
+			return -EINVAL;
+
+		UNSAFE_GET_USER_INC(byte, cur, end, Efault);
+		result |= (long)(byte & 0x7f) << shift;
+		shift += 7;
+	} while (byte & 0x80);
+
+	/* Sign extend if necessary */
+	if (shift < BITS_PER_LONG && (byte & 0x40))
+		result |= -(1L << shift);
+
+	*value = result;
+	*addr = cur;
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int encoded_pointer_size(u8 encoding)
+{
+	u8 format = DW_EH_PE_format(encoding);
+
+	switch (format) {
+	case DW_EH_PE_absptr:
+		return sizeof(unsigned long);
+	case DW_EH_PE_udata2:
+	case DW_EH_PE_sdata2:
+		return 2;
+	case DW_EH_PE_udata4:
+	case DW_EH_PE_sdata4:
+		return 4;
+	case DW_EH_PE_udata8:
+	case DW_EH_PE_sdata8:
+		return 8;
+	case DW_EH_PE_uleb128:
+	case DW_EH_PE_sleb128:
+		/* Variable length */
+		return 0;
+	default:
+		return 0;
+	}
+}
+
+static __always_inline int read_encoded_pointer(struct eh_frame_section *sec,
+						unsigned long *addr,
+						unsigned long end,
+						u8 encoding,
+						unsigned long *value)
+{
+	unsigned long cur = *addr;
+	u8 format = DW_EH_PE_format(encoding);
+	u8 application = DW_EH_PE_application(encoding);
+	unsigned long result = 0;
+	int ret;
+
+	if (encoding == DW_EH_PE_omit)
+		return -EINVAL;
+
+	/* Determine base address based on application */
+	switch (application) {
+	case 0:
+		/* Absolute */
+		break;
+	case DW_EH_PE_pcrel:
+		result = *addr;
+		break;
+	case DW_EH_PE_datarel:
+		result = sec->eh_frame_hdr_start;
+		break;
+	case DW_EH_PE_textrel:
+		result = sec->text_start;
+		break;
+	case DW_EH_PE_funcrel:
+	case DW_EH_PE_aligned:
+		return -EOPNOTSUPP;
+	default:
+		return -EINVAL;
+	}
+
+	/* Read value based on format */
+	switch (format) {
+	case DW_EH_PE_absptr:
+		UNSAFE_GET_USER_INC(result, cur, end, Efault);
+		break;
+	case DW_EH_PE_uleb128: {
+		unsigned long tmp;
+		ret = read_uleb128(&cur, end, &tmp);
+		if (ret)
+			return ret;
+		result += tmp;
+		break;
+	}
+	case DW_EH_PE_udata2: {
+		u16 tmp16;
+		UNSAFE_GET_USER_INC(tmp16, cur, end, Efault);
+		result += tmp16;
+		break;
+	}
+	case DW_EH_PE_udata4: {
+		u32 tmp32;
+		UNSAFE_GET_USER_INC(tmp32, cur, end, Efault);
+		result += tmp32;
+		break;
+	}
+	case DW_EH_PE_udata8: {
+		u64 tmp64;
+		UNSAFE_GET_USER_INC(tmp64, cur, end, Efault);
+		result += tmp64;
+		break;
+	}
+	case DW_EH_PE_sleb128: {
+		long stmp;
+		ret = read_sleb128(&cur, end, &stmp);
+		if (ret)
+			return ret;
+		result += stmp;
+		break;
+	}
+	case DW_EH_PE_sdata2: {
+		s16 stmp16;
+		UNSAFE_GET_USER_INC(stmp16, cur, end, Efault);
+		result += stmp16;
+		break;
+	}
+	case DW_EH_PE_sdata4: {
+		s32 stmp32;
+		UNSAFE_GET_USER_INC(stmp32, cur, end, Efault);
+		result += stmp32;
+		break;
+	}
+	case DW_EH_PE_sdata8: {
+		s64 stmp64;
+		UNSAFE_GET_USER_INC(stmp64, cur, end, Efault);
+		result += stmp64;
+		break;
+	}
+	default:
+		return -EINVAL;
+	}
+
+	/* Indirect (dereference) - should not occur */
+	if (encoding & DW_EH_PE_indirect)
+		return -EOPNOTSUPP;
+
+	*value = result;
+	*addr = cur;
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static void free_section(struct eh_frame_section *sec)
+{
+	kfree(sec);
+}
+
+static int eh_frame_read_header(struct eh_frame_section *sec)
+{
+	struct mm_struct *mm = current->mm;
+	void __user *eh_frame_hdr = (void __user *)sec->eh_frame_hdr_start;
+	unsigned long cur = sec->eh_frame_hdr_start, end = sec->eh_frame_hdr_end;
+	unsigned long eh_frame_start, eh_frame_vma_end, table_start, table_end;
+	u8 version, eh_frame_ptr_enc, fde_count_enc, table_enc;
+	unsigned long fde_count;
+	int entry_size;
+	int ret;
+
+	/*
+	 * Unaligned access to .eh_frame[_hdr] fields using
+	 * unsafe_get_user() via UNSAFE_GET_USER_INC()
+	 */
+	BUILD_BUG_ON(!IS_ENABLED(CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS));
+
+	scoped_user_read_access_size(eh_frame_hdr, end - sec->eh_frame_hdr_start,
+				     Efault) {
+		/* Read version */
+		UNSAFE_GET_USER_INC(version, cur, end, Efault);
+		if (version != 1)
+			return -EINVAL;
+
+		/* Read encoding information */
+		UNSAFE_GET_USER_INC(eh_frame_ptr_enc, cur, end, Efault);
+		UNSAFE_GET_USER_INC(fde_count_enc, cur, end, Efault);
+		UNSAFE_GET_USER_INC(table_enc, cur, end, Efault);
+
+		/* .eh_frame_hdr without binary search table is not supported */
+		if (fde_count_enc == DW_EH_PE_omit || table_enc == DW_EH_PE_omit)
+			return -EINVAL;
+
+		/* Read pointer to .eh_frame */
+		ret = read_encoded_pointer(sec, &cur, end,
+					   eh_frame_ptr_enc, &eh_frame_start);
+		if (ret)
+			return ret;
+
+		/* Read FDE count */
+		ret = read_encoded_pointer(sec, &cur, end,
+					   fde_count_enc, &fde_count);
+		if (ret)
+			return ret;
+
+		/* Determine binary search table start and end */
+		table_start = cur;
+		entry_size = 2 * encoded_pointer_size(table_enc);
+		if (!entry_size)
+			return -EINVAL;
+		table_end = table_start + fde_count * entry_size;
+		if (table_end > sec->eh_frame_hdr_end)
+			return -EINVAL;
+	}
+
+end:
+	scoped_guard(mmap_read_lock, mm) {
+		struct vm_area_struct *eh_frame_vma;
+
+		eh_frame_vma = vma_lookup(mm, eh_frame_start);
+		if (!eh_frame_vma) {
+			dbg("bad eh_frame address (0x%lx)\n", eh_frame_start);
+			return -EINVAL;
+		}
+		eh_frame_vma_end = eh_frame_vma->vm_end;
+	}
+
+	sec->eh_frame_start		= eh_frame_start;
+	sec->eh_frame_vma_end		= eh_frame_vma_end;
+	sec->binary_search_table_start	= table_start;
+	sec->binary_search_table_end	= table_end;
+	sec->binary_search_table_enc	= table_enc;
+	sec->fde_count			= fde_count;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+int eh_frame_add_section(unsigned long eh_frame_hdr_start,
+			 unsigned long eh_frame_hdr_end,
+			 unsigned long text_start,
+			 unsigned long text_end)
+{
+	struct mm_struct *mm = current->mm;
+	struct eh_frame_section *sec;
+	int ret;
+
+	if (eh_frame_hdr_start >= eh_frame_hdr_end || text_start >= text_end) {
+		dbg("invalid eh_frame/text address\n");
+		return -EINVAL;
+	}
+
+	scoped_guard(mmap_read_lock, mm) {
+		struct vm_area_struct *eh_frame_hdr_vma, *text_vma;
+
+		eh_frame_hdr_vma = vma_lookup(mm, eh_frame_hdr_start);
+		if (!eh_frame_hdr_vma || eh_frame_hdr_end > eh_frame_hdr_vma->vm_end) {
+			dbg("bad eh_frame_hdr address (0x%lx - 0x%lx)\n",
+			    eh_frame_hdr_start, eh_frame_hdr_end);
+			return -EINVAL;
+		}
+
+		text_vma = vma_lookup(mm, text_start);
+		if (!text_vma ||
+		    !(text_vma->vm_flags & VM_EXEC) ||
+		    text_end > text_vma->vm_end) {
+			dbg("bad text address (0x%lx - 0x%lx)\n",
+			    text_start, text_end);
+			return -EINVAL;
+		}
+	}
+
+	sec = kzalloc(sizeof(*sec), GFP_KERNEL_ACCOUNT);
+	if (!sec)
+		return -ENOMEM;
+
+	sec->eh_frame_hdr_start	= eh_frame_hdr_start;
+	sec->eh_frame_hdr_end	= eh_frame_hdr_end;
+	sec->text_start		= text_start;
+	sec->text_end		= text_end;
+
+	ret = eh_frame_read_header(sec);
+	if (ret)
+		goto err_free;
+
+	/* TODO nowhere to store it yet - just free it and return an error */
+	ret = -ENOSYS;
+
+err_free:
+	free_section(sec);
+	return ret;
+}
+
+int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
+{
+	return -ENOSYS;
+}
diff --git a/kernel/unwind/eh_frame.h b/kernel/unwind/eh_frame.h
new file mode 100644
index 000000000000..77eda5376dfb
--- /dev/null
+++ b/kernel/unwind/eh_frame.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef _EH_FRAME_H
+#define _EH_FRAME_H
+
+/* DWARF exception header pointer encodings */
+#define DW_EH_PE_omit			0xff
+/* Formats */
+#define DW_EH_PE_absptr			0x00
+#define DW_EH_PE_uleb128		0x01
+#define DW_EH_PE_udata2			0x02
+#define DW_EH_PE_udata4			0x03
+#define DW_EH_PE_udata8			0x04
+#define DW_EH_PE_sleb128		0x09
+#define DW_EH_PE_sdata2			0x0a
+#define DW_EH_PE_sdata4			0x0b
+#define DW_EH_PE_sdata8			0x0c
+/* Applications */
+#define DW_EH_PE_pcrel			0x10
+#define DW_EH_PE_textrel		0x20
+#define DW_EH_PE_datarel		0x30
+#define DW_EH_PE_funcrel		0x40
+#define DW_EH_PE_aligned		0x50
+/* Flags */
+#define DW_EH_PE_indirect		0x80
+
+/* Helpers for DWARF exception header pointer encodings */
+#define DW_EH_PE_format(encoding)	((encoding) & 0x0f)
+#define DW_EH_PE_application(encoding)	((encoding) & 0x70)
+
+#endif /* _EH_FRAME_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (6 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
                   ` (17 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Associate .eh_frame_hdr sections with their mm by adding them to a
per-mm maple tree which is indexed by the corresponding text address
range.  A single .eh_frame_hdr section can be associated with multiple
text ranges.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/x86/include/asm/mmu.h |  2 +-
 include/linux/eh_frame.h   | 20 +++++++++++
 include/linux/mm_types.h   |  3 ++
 kernel/fork.c              | 10 ++++++
 kernel/unwind/eh_frame.c   | 68 ++++++++++++++++++++++++++++++++++++--
 mm/init-mm.c               |  2 ++
 6 files changed, 101 insertions(+), 4 deletions(-)

diff --git a/arch/x86/include/asm/mmu.h b/arch/x86/include/asm/mmu.h
index 0fe9c569d171..227a32899a59 100644
--- a/arch/x86/include/asm/mmu.h
+++ b/arch/x86/include/asm/mmu.h
@@ -87,7 +87,7 @@ typedef struct {
 	.context = {							\
 		.ctx_id = 1,						\
 		.lock = __MUTEX_INITIALIZER(mm.context.lock),		\
-	}
+	},
 
 void leave_mm(void);
 #define leave_mm leave_mm
diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index aaac2dd663d5..95df4911fd23 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -2,9 +2,14 @@
 #ifndef _LINUX_EH_FRAME_H
 #define _LINUX_EH_FRAME_H
 
+#include <linux/mm_types.h>
+#include <linux/srcu.h>
+
 #ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
 
 struct eh_frame_section {
+	struct rcu_head	rcu;
+
 	unsigned long	eh_frame_hdr_start;
 	unsigned long	eh_frame_hdr_end;
 	unsigned long	text_start;
@@ -19,14 +24,27 @@ struct eh_frame_section {
 	u8		binary_search_table_enc;
 };
 
+#define INIT_MM_EH_FRAME .eh_frame_mt = MTREE_INIT(eh_frame_mt, 0),
+extern void eh_frame_free_mm(struct mm_struct *mm);
+
 extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 				unsigned long eh_frame_hdr_end,
 				unsigned long text_start,
 				unsigned long text_end);
 extern int eh_frame_remove_section(unsigned long eh_frame_hdr_start);
 
+static inline bool current_has_eh_frame(void)
+{
+	struct mm_struct *mm = current->mm;
+
+	return mm && !mtree_empty(&mm->eh_frame_mt);
+}
+
 #else /* !CONFIG_HAVE_UNWIND_USER_EH_FRAME */
 
+#define INIT_MM_EH_FRAME
+static inline void eh_frame_free_mm(struct mm_struct *mm) {}
+
 static inline int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 				       unsigned long eh_frame_hdr_end,
 				       unsigned long text_start,
@@ -40,6 +58,8 @@ static inline int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
 	return -ENOSYS;
 }
 
+static inline bool current_has_eh_frame(void) { return false; }
+
 #endif /* CONFIG_HAVE_UNWIND_USER_EH_FRAME */
 
 #endif /* _LINUX_EH_FRAME_H */
diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index b18c2b2e7d2c..ac51d6212e86 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -1404,6 +1404,9 @@ struct mm_struct {
 #ifdef CONFIG_MM_ID
 		mm_id_t mm_id;
 #endif /* CONFIG_MM_ID */
+#ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
+		struct maple_tree eh_frame_mt;
+#endif
 	} __randomize_layout;
 
 	/*
diff --git a/kernel/fork.c b/kernel/fork.c
index f0e2e131a9a5..c0d71818e197 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -111,6 +111,7 @@
 #include <linux/tick.h>
 #include <linux/unwind_deferred.h>
 #include <linux/pgalloc.h>
+#include <linux/eh_frame.h>
 #include <linux/uaccess.h>
 
 #include <asm/mmu_context.h>
@@ -738,6 +739,7 @@ void __mmdrop(struct mm_struct *mm)
 	mm_pasid_drop(mm);
 	mm_destroy_cid(mm);
 	percpu_counter_destroy_many(mm->rss_stat, NR_MM_COUNTERS);
+	eh_frame_free_mm(mm);
 
 	free_mm(mm);
 }
@@ -1082,6 +1084,13 @@ static void mmap_init_lock(struct mm_struct *mm)
 #endif
 }
 
+static void mm_init_eh_frame(struct mm_struct *mm)
+{
+#ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
+	mt_init(&mm->eh_frame_mt);
+#endif
+}
+
 static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
 {
 	mt_init_flags(&mm->mm_mt, MM_MT_FLAGS);
@@ -1109,6 +1118,7 @@ static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
 	mm->pmd_huge_pte = NULL;
 #endif
 	mm_init_uprobes_state(mm);
+	mm_init_eh_frame(mm);
 	hugetlb_count_init(mm);
 	futex_mm_init(mm);
 
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 8d2b638145bd..7bdf7589466a 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -19,6 +19,8 @@
 #define dbg(fmt, ...)							\
 	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
 
+DEFINE_STATIC_SRCU(eh_frame_srcu);
+
 #define UNSAFE_GET_USER_INC(to, from, end, label)			\
 ({									\
 	typeof(to) __to;						\
@@ -307,6 +309,7 @@ int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 			 unsigned long text_start,
 			 unsigned long text_end)
 {
+	struct maple_tree *eh_frame_mt = &current->mm->eh_frame_mt;
 	struct mm_struct *mm = current->mm;
 	struct eh_frame_section *sec;
 	int ret;
@@ -349,15 +352,74 @@ int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 	if (ret)
 		goto err_free;
 
-	/* TODO nowhere to store it yet - just free it and return an error */
-	ret = -ENOSYS;
+	ret = mtree_insert_range(eh_frame_mt, sec->text_start, sec->text_end - 1,
+				 sec, GFP_KERNEL_ACCOUNT);
+	if (ret) {
+		dbg("mtree_insert_range failed: text=%lx-%lx\n",
+		    sec->text_start, sec->text_end);
+		goto err_free;
+	}
+
+	return 0;
 
 err_free:
 	free_section(sec);
 	return ret;
 }
 
+static void eh_frame_free_srcu(struct rcu_head *rcu)
+{
+	struct eh_frame_section *sec = container_of(rcu, struct eh_frame_section, rcu);
+
+	free_section(sec);
+}
+
+static int __eh_frame_remove_section(struct mm_struct *mm,
+				     struct eh_frame_section *sec)
+{
+	if (!mtree_erase(&mm->eh_frame_mt, sec->text_start)) {
+		dbg("mtree_erase failed: text=%lx\n", sec->text_start);
+		return -EINVAL;
+	}
+
+	call_srcu(&eh_frame_srcu, &sec->rcu, eh_frame_free_srcu);
+
+	return 0;
+}
+
 int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
 {
-	return -ENOSYS;
+	struct mm_struct *mm = current->mm;
+	struct eh_frame_section *sec;
+	unsigned long index = 0;
+	bool found = false;
+	int ret = 0;
+
+	guard(srcu)(&eh_frame_srcu);
+
+	mt_for_each(&mm->eh_frame_mt, sec, index, ULONG_MAX) {
+		if (sec->eh_frame_hdr_start == eh_frame_hdr_start) {
+			found = true;
+			ret |= __eh_frame_remove_section(mm, sec);
+		}
+	}
+
+	if (!found || ret)
+		return -EINVAL;
+
+	return 0;
+}
+
+void eh_frame_free_mm(struct mm_struct *mm)
+{
+	struct eh_frame_section *sec;
+	unsigned long index = 0;
+
+	if (!mm)
+		return;
+
+	mt_for_each(&mm->eh_frame_mt, sec, index, ULONG_MAX)
+		free_section(sec);
+
+	mtree_destroy(&mm->eh_frame_mt);
 }
diff --git a/mm/init-mm.c b/mm/init-mm.c
index 3e792aad7626..25c26341fc4f 100644
--- a/mm/init-mm.c
+++ b/mm/init-mm.c
@@ -11,6 +11,7 @@
 #include <linux/atomic.h>
 #include <linux/user_namespace.h>
 #include <linux/iommu.h>
+#include <linux/eh_frame.h>
 #include <asm/mmu.h>
 
 #ifndef INIT_MM_CONTEXT
@@ -48,6 +49,7 @@ struct mm_struct init_mm = {
 #endif
 	.flexible_array	= MM_STRUCT_FLEXIBLE_ARRAY_INIT,
 	INIT_MM_CONTEXT(init_mm)
+	INIT_MM_EH_FRAME
 };
 
 void setup_initial_init_mm(void *start_code, void *end_code,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (7 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables Jens Remus
                   ` (16 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

In preparation for using .eh_frame to unwind user space stacks, add an
eh_frame_find() interface for finding the .eh_frame[_hdr] information
associated with a given text address.

The implementation parses DWARF Call Frame Information (CFI) from the
.eh_frame section, including:
- Common Information Entries (CIEs) that define default unwinding rules,
- Frame Description Entries (FDEs) that describe unwinding for specific
  function address ranges, and
- DWARF CFI instructions that specify how to restore registers.

For performance, use user_read_access_begin() and the corresponding
unsafe_*() accessors.  Note that use of pr_debug() in uaccess-enabled
regions would break noinstr validation, so there aren't any debug
messages yet.  That will be added in a subsequent commit.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 MAINTAINERS                                |   2 +
 include/asm-generic/Kbuild                 |   1 +
 include/asm-generic/unwind_user_eh_frame.h |  43 +
 include/linux/eh_frame.h                   |   7 +
 kernel/unwind/eh_frame.c                   | 943 ++++++++++++++++++++-
 kernel/unwind/eh_frame.h                   |  38 +
 kernel/unwind/eh_frame_debug.h             |  19 +
 7 files changed, 1048 insertions(+), 5 deletions(-)
 create mode 100644 include/asm-generic/unwind_user_eh_frame.h
 create mode 100644 kernel/unwind/eh_frame_debug.h

diff --git a/MAINTAINERS b/MAINTAINERS
index f19f5bb87f00..d5f46f26996f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -28202,7 +28202,9 @@ M:	Josh Poimboeuf <jpoimboe@kernel.org>
 M:	Steven Rostedt <rostedt@goodmis.org>
 S:	Maintained
 F:	arch/*/include/asm/unwind_user.h
+F:	arch/*/include/asm/unwind_user_eh_frame.h
 F:	include/asm-generic/unwind_user.h
+F:	include/asm-generic/unwind_user_eh_frame.h
 F:	include/linux/eh_frame.h
 F:	include/linux/unwind*.h
 F:	kernel/unwind/
diff --git a/include/asm-generic/Kbuild b/include/asm-generic/Kbuild
index 15df9dcb42a5..bd031eb6adc4 100644
--- a/include/asm-generic/Kbuild
+++ b/include/asm-generic/Kbuild
@@ -62,6 +62,7 @@ mandatory-y += topology.h
 mandatory-y += trace_clock.h
 mandatory-y += uaccess.h
 mandatory-y += unwind_user.h
+mandatory-y += unwind_user_eh_frame.h
 mandatory-y += vermagic.h
 mandatory-y += vga.h
 mandatory-y += video.h
diff --git a/include/asm-generic/unwind_user_eh_frame.h b/include/asm-generic/unwind_user_eh_frame.h
new file mode 100644
index 000000000000..e6d207597206
--- /dev/null
+++ b/include/asm-generic/unwind_user_eh_frame.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_GENERIC_UNWIND_USER_EH_FRAME_H
+#define _ASM_GENERIC_UNWIND_USER_EH_FRAME_H
+
+#ifndef EH_FRAME_MAX_CIE_LENGTH
+#define EH_FRAME_MAX_CIE_LENGTH 128
+#endif
+
+#ifndef EH_FRAME_MAX_AUGSTR_LENGTH
+#define EH_FRAME_MAX_AUGSTR_LENGTH 16
+#endif
+
+#ifndef EH_FRAME_MAX_STATE_STACK
+#define EH_FRAME_MAX_STATE_STACK 8
+#endif
+
+#ifndef EH_FRAME_MAX_CODE_ALIGN
+#define EH_FRAME_MAX_CODE_ALIGN 8
+#endif
+
+#ifndef EH_FRAME_MIN_DATA_ALIGN
+#define EH_FRAME_MIN_DATA_ALIGN -8
+#endif
+
+#ifndef EH_FRAME_MAX_DATA_ALIGN
+#define EH_FRAME_MAX_DATA_ALIGN -1
+#endif
+
+#ifndef EH_FRAME_SP_VAL_OFFSET
+/* Most archs/ABIs define CFA as SP at call site, so that SP = CFA + 0 */
+#define EH_FRAME_SP_VAL_OFFSET 0
+#endif
+
+#ifndef eh_frame_reject_sp_rule
+static inline bool eh_frame_reject_sp_rule(void)
+{
+	return true;
+}
+#define eh_frame_reject_sp_rule eh_frame_reject_sp_rule
+#endif
+
+#endif /* _ASM_GENERIC_UNWIND_USER_EH_FRAME_H */
+
diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index 95df4911fd23..e33041e4f9c0 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -4,6 +4,7 @@
 
 #include <linux/mm_types.h>
 #include <linux/srcu.h>
+#include <linux/unwind_user_types.h>
 
 #ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
 
@@ -32,6 +33,7 @@ extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 				unsigned long text_start,
 				unsigned long text_end);
 extern int eh_frame_remove_section(unsigned long eh_frame_hdr_start);
+extern int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame);
 
 static inline bool current_has_eh_frame(void)
 {
@@ -58,6 +60,11 @@ static inline int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
 	return -ENOSYS;
 }
 
+static inline int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
+{
+	return -ENOSYS;
+}
+
 static inline bool current_has_eh_frame(void) { return false; }
 
 #endif /* CONFIG_HAVE_UNWIND_USER_EH_FRAME */
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 7bdf7589466a..5ef8fef06a80 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -13,14 +13,83 @@
 #include <linux/string_helpers.h>
 #include <linux/eh_frame.h>
 #include <linux/unwind_user_types.h>
+#include <asm/unwind_user_eh_frame.h>
 
 #include "eh_frame.h"
-
-#define dbg(fmt, ...)							\
-	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+#include "eh_frame_debug.h"
+
+/* Register state for CFI interpreter */
+enum eh_frame_cfa_rule {
+	CFA_UNDEFINED,		/* unrecoverable */
+	CFA_REG_OFFSET,		/* CFA = reg + offset */
+};
+
+enum eh_frame_reg_rule {
+	REG_UNDEFINED_IMPLICIT,	/* reg = reg */
+	REG_UNDEFINED_EXPLICIT,	/* unrecoverable; RA: outermost frame */
+	REG_SAME_VALUE,		/* reg = reg; TODO: reset to CIE initial CFI */
+	REG_OFFSET,		/* reg = *(CFA + offset) */
+	REG_VAL_OFFSET,		/* reg = CFA + offset */
+	REG_REGISTER,		/* reg = other_reg */
+};
+
+enum eh_frame_reg_index {
+	FP_IDX,			/* frame pointer (FP) */
+	RA_IDX,			/* return address (RA) */
+	NR_REGS
+};
+
+struct eh_frame_reg_state {
+	/* CFA recovery rule */
+	enum eh_frame_cfa_rule cfa_rule;
+	unsigned long cfa_regnum;
+	long cfa_offset;
+
+	/* FP and RA recovery rules (SP uses implicit recovery) */
+	enum eh_frame_reg_rule reg_rule[NR_REGS];
+	unsigned long reg_regnum[NR_REGS];
+	long reg_offset[NR_REGS];
+};
+
+struct eh_frame_cfi_context {
+	struct eh_frame_reg_state state;
+	struct eh_frame_reg_state stack[EH_FRAME_MAX_STATE_STACK];
+	int stack_depth;
+};
+
+struct eh_frame_cie {
+	unsigned long cfi_insn_start;
+	unsigned long cfi_insn_end;
+	int data_align;
+	unsigned int code_align;
+	u8 fde_addr_enc;		/* from CIE 'R' augmentation */
+	bool aug_data_present;		/* from CIE 'z' augmentation */
+	bool signal_frame;		/* from CIE 'S' augmentation */
+};
+
+struct eh_frame_fde {
+	unsigned long func_addr;
+	unsigned long func_size;
+	unsigned long cfi_insn_start;
+	unsigned long cfi_insn_end;
+
+	struct eh_frame_cie cie;	/* referenced CIE*/
+};
 
 DEFINE_STATIC_SRCU(eh_frame_srcu);
 
+#define GET_USER_INC(to, from, end)					\
+({									\
+	typeof(to) __to;						\
+	int ret;							\
+	if (from + sizeof(__to) > end)					\
+		return -EINVAL;						\
+	ret = get_user(__to, (typeof(to) __user *)from);		\
+	from += sizeof(__to);						\
+	to = __to;							\
+	ret;								\
+})
+
 #define UNSAFE_GET_USER_INC(to, from, end, label)			\
 ({									\
 	typeof(to) __to;						\
@@ -111,6 +180,7 @@ static __always_inline int encoded_pointer_size(u8 encoding)
 }
 
 static __always_inline int read_encoded_pointer(struct eh_frame_section *sec,
+						struct eh_frame_fde *fde,
 						unsigned long *addr,
 						unsigned long end,
 						u8 encoding,
@@ -140,6 +210,10 @@ static __always_inline int read_encoded_pointer(struct eh_frame_section *sec,
 		result = sec->text_start;
 		break;
 	case DW_EH_PE_funcrel:
+		if (!fde)
+			return -EINVAL;
+		result = fde->func_addr;
+		break;
 	case DW_EH_PE_aligned:
 		return -EOPNOTSUPP;
 	default:
@@ -219,6 +293,865 @@ static __always_inline int read_encoded_pointer(struct eh_frame_section *sec,
 	return -EFAULT;
 }
 
+static __always_inline int __read_cie(struct eh_frame_section *sec,
+				      unsigned long cie_addr,
+				      struct eh_frame_cie *cie)
+{
+	void __user *cie_ptr = (void __user *)cie_addr;
+	unsigned long cur = cie_addr, end;
+	u32 length, cie_id;
+	u8 version;
+	char aug_str[EH_FRAME_MAX_AUGSTR_LENGTH];
+	int aug_idx;
+	unsigned long code_align;
+	long data_align;
+	u8 ra_reg;
+	bool aug_data_present = false;
+	unsigned long aug_data_len, aug_data_end = cie_addr;
+	u8 fde_addr_enc = DW_EH_PE_absptr;
+	bool signal_frame = false;
+	int ret;
+
+	/* Read CIE length */
+	ret = GET_USER_INC(length, cur, sec->eh_frame_vma_end);
+	if (ret)
+		return ret;
+	if (!length || length == EH_FRAME_DWARF64_LENGTH || length > EH_FRAME_MAX_CIE_LENGTH)
+		return -EINVAL;
+	end = cie_addr + 4 + length;
+	if (end > sec->eh_frame_vma_end)
+		return -EFAULT;
+
+	scoped_user_read_access_size(cie_ptr, 4 + length, Efault) {
+		/* Read CIE_ID (must be 0 for CIE; FDE otherwise) */
+		UNSAFE_GET_USER_INC(cie_id, cur, end, Efault);
+		if (cie_id != EH_FRAME_CIE_ID)
+			return -EINVAL;
+
+		/* Read version */
+		UNSAFE_GET_USER_INC(version, cur, end, Efault);
+		if (version != 1)
+			return -EOPNOTSUPP;
+
+		/* Read augmentation string */
+		for (aug_idx = 0; aug_idx < sizeof(aug_str); aug_idx++) {
+			UNSAFE_GET_USER_INC(aug_str[aug_idx], cur, end, Efault);
+			if (aug_str[aug_idx] == '\0')
+				break;
+		}
+		if (aug_idx >= sizeof(aug_str))
+			return -EINVAL;
+
+		/* Read code alignment factor */
+		ret = read_uleb128(&cur, end, &code_align);
+		if (ret)
+			return ret;
+		if (!code_align || code_align > EH_FRAME_MAX_CODE_ALIGN)
+			return -EINVAL;
+
+		/* Read data alignment factor */
+		ret = read_sleb128(&cur, end, &data_align);
+		if (ret)
+			return ret;
+		if (!data_align || (data_align < EH_FRAME_MIN_DATA_ALIGN ||
+				    data_align > EH_FRAME_MAX_DATA_ALIGN))
+			return -EINVAL;
+
+		/* Read return address register number */
+		UNSAFE_GET_USER_INC(ra_reg, cur, end, Efault);
+		if (ra_reg != EH_FRAME_REG_RA)
+			return -EOPNOTSUPP;
+
+		/* Parse augmentation string and read augmentation data if present */
+		for (aug_idx = 0; aug_str[aug_idx]; aug_idx++) {
+			switch (aug_str[aug_idx]) {
+			case 'z':
+				/* Augmentation data present - must be first character */
+				if (aug_idx != 0)
+					return -EINVAL;
+				aug_data_present = true;
+				ret = read_uleb128(&cur, end, &aug_data_len);
+				if (ret)
+					return ret;
+				aug_data_end = cur + aug_data_len;
+				if (aug_data_end > end)
+					return -EINVAL;
+				break;
+			case 'L': {
+				/* LSDA encoding - skip */
+				u8 lsda_enc;
+				if (!aug_data_present)
+					return -EINVAL;
+				UNSAFE_GET_USER_INC(lsda_enc, cur, aug_data_end, Efault);
+				break;
+			}
+			case 'P': {
+				/* Personality encoding and routine - skip */
+				u8 personality_enc;
+				unsigned long personality_rtn;
+				if (!aug_data_present)
+					return -EINVAL;
+				UNSAFE_GET_USER_INC(personality_enc, cur, aug_data_end, Efault);
+				/*
+				 * Clear indirect flag to avoid user read from
+				 * arbitrary address; still skip field.
+				 */
+				personality_enc &= ~DW_EH_PE_indirect;
+				ret = read_encoded_pointer(sec, NULL, &cur, aug_data_end,
+							   personality_enc, &personality_rtn);
+				if (ret)
+					return ret;
+				break;
+			}
+			case 'R':
+				/* FDE encoding */
+				if (!aug_data_present)
+					return -EINVAL;
+				UNSAFE_GET_USER_INC(fde_addr_enc, cur, aug_data_end, Efault);
+				break;
+			case 'S':
+				/* Signal frame */
+				signal_frame = true;
+				break;
+			default:
+				/* Unknown augmentation */
+				return -EOPNOTSUPP;
+			}
+		}
+	}
+
+	cie->code_align		= code_align;
+	cie->data_align		= data_align;
+	cie->fde_addr_enc	= fde_addr_enc;
+	cie->aug_data_present	= aug_data_present;
+	cie->signal_frame	= signal_frame;
+	cie->cfi_insn_start	= cur;
+	cie->cfi_insn_end	= end;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __read_fde(struct eh_frame_section *sec,
+				      unsigned long fde_addr,
+				      struct eh_frame_fde *fde)
+{
+	void __user *fde_ptr = (void __user *)fde_addr;
+	unsigned long cur = fde_addr, end;
+	u32 length, cie_offset;
+	unsigned long cie_addr, func_addr, func_size;
+	int ret;
+
+	/* Read FDE length */
+	ret = GET_USER_INC(length, cur, sec->eh_frame_vma_end);
+	if (ret)
+		return ret;
+	if (!length || length == EH_FRAME_DWARF64_LENGTH)
+		return -EINVAL;
+	end = fde_addr + 4 + length;
+	if (end > sec->eh_frame_vma_end)
+		return -EFAULT;
+
+	scoped_user_read_access_size(fde_ptr, 4 + length, Efault) {
+		/* Read CIE pointer (offset from current position) */
+		UNSAFE_GET_USER_INC(cie_offset, cur, end, Efault);
+		cie_addr = cur - 4 - cie_offset;
+		if (cie_addr + EH_FRAME_CIE_MIN_LENGTH > fde_addr)
+			return -EINVAL;
+		if (cie_addr < sec->eh_frame_start)
+			return -EINVAL;
+	}
+
+	/* Read the CIE to populate alignment factors, RA register, and FDE encoding */
+	ret = __read_cie(sec, cie_addr, &fde->cie);
+	if (ret)
+		return ret;
+
+	scoped_user_read_access_size(fde_ptr, 4 + length, Efault) {
+		/* Read PC begin (function start address) */
+		ret = read_encoded_pointer(sec, fde, &cur, end, fde->cie.fde_addr_enc, &func_addr);
+		if (ret)
+			return ret;
+		if (func_addr < sec->text_start || func_addr >= sec->text_end)
+			return -EINVAL;
+
+		/* Read PC range (function size) using PE format only */
+		u8 range_enc = DW_EH_PE_format(fde->cie.fde_addr_enc);
+		ret = read_encoded_pointer(sec, fde, &cur, end, range_enc, &func_size);
+		if (ret)
+			return ret;
+		if (func_addr + func_size > sec->text_end)
+			return -EINVAL;
+
+		/* Skip augmentation data if present */
+		if (fde->cie.aug_data_present) {
+			unsigned long aug_data_len;
+
+			ret = read_uleb128(&cur, end, &aug_data_len);
+			if (ret)
+				return ret;
+			if (cur + aug_data_len > end)
+				return -EINVAL;
+			cur += aug_data_len;
+		}
+	}
+
+	fde->func_addr		= func_addr;
+	fde->func_size		= func_size;
+	fde->cfi_insn_start	= cur;
+	fde->cfi_insn_end	= end;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+
+static __always_inline int __find_fde(struct eh_frame_section *sec,
+				      unsigned long ip,
+				      struct eh_frame_fde *fde)
+{
+	void __user *table_start_ptr;
+	unsigned long table_size;
+	u8 table_enc;
+	int entry_size;
+	unsigned long low, high;
+	unsigned long found_cur = 0, found_func_addr;
+	unsigned long fde_addr;
+	int ret;
+
+	if (!sec->fde_count)
+		return -ENOENT;
+
+	table_enc = sec->binary_search_table_enc;
+	entry_size = 2 * encoded_pointer_size(table_enc);
+	if (!entry_size)
+		return -EINVAL;
+
+	table_start_ptr = (void __user *)sec->binary_search_table_start;
+	table_size = sec->binary_search_table_end - sec->binary_search_table_start;
+	scoped_user_read_access_size(table_start_ptr, table_size, Efault) {
+		/*
+		 * Binary search in .eh_frame_hdr table using half-open
+		 * interval [low, high) to avoid underflow of high if
+		 * target IP is lower than first entry.
+		 */
+		low = 0;
+		high = sec->fde_count;
+		while (low < high) {
+			unsigned long mid, cur, func_addr;
+
+			mid = low + ((high - low) / 2);
+			cur = sec->binary_search_table_start + mid * entry_size;
+
+			/* Read function start address from table */
+			ret = read_encoded_pointer(sec, NULL, &cur, sec->binary_search_table_end,
+						   table_enc, &func_addr);
+			if (ret)
+				return ret;
+
+			if (ip >= func_addr) {
+				found_cur = cur;
+				found_func_addr = func_addr;
+				low = mid + 1;
+			} else {
+				high = mid;
+			}
+		}
+
+		if (!found_cur)
+			return -ENOENT;
+
+		/* Read FDE address from table */
+		ret = read_encoded_pointer(sec, NULL, &found_cur, sec->binary_search_table_end,
+					   table_enc, &fde_addr);
+		if (ret)
+			return ret;
+		if (fde_addr < sec->eh_frame_start)
+			return -EINVAL;
+	}
+
+	ret = __read_fde(sec, fde_addr, fde);
+	if (ret)
+		return ret;
+	if (found_func_addr != fde->func_addr)
+		return -EINVAL;
+
+	/* Make sure it is not a gap */
+	if (ip < fde->func_addr || ip >= fde->func_addr + fde->func_size)
+		return -ENOENT;
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+/* Helper to convert DWARF register number to index (FP=0, RA=1) */
+static inline int reg_to_index(unsigned int reg)
+{
+	if (reg == EH_FRAME_REG_FP)
+		return 0;
+	if (reg == EH_FRAME_REG_RA)
+		return 1;
+	return -1;
+}
+
+static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
+					 struct eh_frame_fde *fde,
+					 unsigned long *cur_ptr,
+					 unsigned long end,
+					 unsigned long *ip_ptr,
+					 unsigned long target_ip,
+					 struct eh_frame_cfi_context *ctx)
+{
+	unsigned long cur = *cur_ptr;
+	unsigned long ip = *ip_ptr;
+	u8 opcode;
+	int ret;
+
+	UNSAFE_GET_USER_INC(opcode, cur, end, Efault);
+
+	switch (DW_CFA_opcode(opcode)) {
+	case DW_CFA_advance_loc: {
+		unsigned long offset = DW_CFA_operand(opcode) * fde->cie.code_align;
+
+		ip += offset;
+		break;
+	}
+
+	case DW_CFA_offset: {
+		u8 reg = DW_CFA_operand(opcode);
+		long offset;
+		int idx;
+
+		ret = read_uleb128(&cur, end, &offset);
+		if (ret)
+			return ret;
+		offset *= fde->cie.data_align;
+
+		if (reg == EH_FRAME_REG_SP && eh_frame_reject_sp_rule())
+			return -EOPNOTSUPP;
+
+		idx = reg_to_index(reg);
+		if (idx >= 0) {
+			ctx->state.reg_rule[idx] = REG_OFFSET;
+			ctx->state.reg_offset[idx] = offset;
+		}
+		break;
+	}
+
+	case DW_CFA_restore: {
+		u8 reg = DW_CFA_operand(opcode);
+		int idx;
+
+		idx = reg_to_index(reg);
+		if (idx >= 0)
+			ctx->state.reg_rule[idx] = REG_UNDEFINED_IMPLICIT;
+		break;
+	}
+
+	case 0: /* Extended opcodes */
+		switch (opcode) {
+		case DW_CFA_nop:
+			break;
+
+		case DW_CFA_advance_loc1: {
+			unsigned long offset;
+			u8 delta;
+
+			UNSAFE_GET_USER_INC(delta, cur, end, Efault);
+			offset = delta * fde->cie.code_align;
+			ip += offset;
+			break;
+		}
+
+		case DW_CFA_advance_loc2: {
+			unsigned long offset;
+			u16 delta;
+
+			UNSAFE_GET_USER_INC(delta, cur, end, Efault);
+			offset = delta * fde->cie.code_align;
+			ip += offset;
+			break;
+		}
+
+		case DW_CFA_advance_loc4: {
+			unsigned long offset;
+			u32 delta;
+
+			UNSAFE_GET_USER_INC(delta, cur, end, Efault);
+			offset = delta * fde->cie.code_align;
+			ip += offset;
+			break;
+		}
+
+		case DW_CFA_def_cfa: {
+			unsigned long reg, offset;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_uleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+
+			if (offset > LONG_MAX)
+				return -EOPNOTSUPP;
+
+			ctx->state.cfa_rule = CFA_REG_OFFSET;
+			ctx->state.cfa_regnum = reg;
+			ctx->state.cfa_offset = offset;
+			break;
+		}
+
+		case DW_CFA_def_cfa_sf: {
+			unsigned long reg;
+			long offset;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_sleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+			offset *= fde->cie.data_align;
+
+			ctx->state.cfa_rule = CFA_REG_OFFSET;
+			ctx->state.cfa_regnum = reg;
+			ctx->state.cfa_offset = offset;
+			break;
+		}
+
+		case DW_CFA_def_cfa_register: {
+			unsigned long reg;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+
+			ctx->state.cfa_rule = CFA_REG_OFFSET;
+			ctx->state.cfa_regnum = reg;
+			break;
+		}
+
+		case DW_CFA_def_cfa_offset: {
+			unsigned long offset;
+
+			ret = read_uleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+
+			if (offset > LONG_MAX)
+				return -EOPNOTSUPP;
+
+			ctx->state.cfa_offset = offset;
+			break;
+		}
+
+		case DW_CFA_def_cfa_offset_sf: {
+			long offset;
+
+			ret = read_sleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+			offset *= fde->cie.data_align;
+
+			ctx->state.cfa_offset = offset;
+			break;
+		}
+
+		case DW_CFA_undefined: {
+			unsigned long reg;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+
+			if (reg == EH_FRAME_REG_SP)
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0)
+				ctx->state.reg_rule[idx] = REG_UNDEFINED_EXPLICIT;
+			break;
+		}
+
+		case DW_CFA_same_value: {
+			unsigned long reg;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+
+			if (reg == EH_FRAME_REG_SP && eh_frame_reject_sp_rule())
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0)
+				ctx->state.reg_rule[idx] = REG_SAME_VALUE;
+			break;
+		}
+
+		case DW_CFA_offset_extended: {
+			unsigned long reg, _offset;
+			long offset;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_uleb128(&cur, end, &_offset);
+			if (ret)
+				return ret;
+			offset = _offset * fde->cie.data_align;
+
+			if (reg == EH_FRAME_REG_SP && eh_frame_reject_sp_rule())
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0) {
+				ctx->state.reg_rule[idx] = REG_OFFSET;
+				ctx->state.reg_offset[idx] = offset;
+			}
+			break;
+		}
+
+		case DW_CFA_offset_extended_sf: {
+			unsigned long reg;
+			long offset;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_sleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+			offset *= fde->cie.data_align;
+
+			if (reg == EH_FRAME_REG_SP && eh_frame_reject_sp_rule())
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0) {
+				ctx->state.reg_rule[idx] = REG_OFFSET;
+				ctx->state.reg_offset[idx] = offset;
+			}
+			break;
+		}
+
+		case DW_CFA_val_offset: {
+			unsigned long reg, _offset;
+			long offset;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_uleb128(&cur, end, &_offset);
+			if (ret)
+				return ret;
+			offset = _offset * fde->cie.data_align;
+
+			if (reg == EH_FRAME_REG_SP && offset != EH_FRAME_SP_VAL_OFFSET)
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0) {
+				ctx->state.reg_rule[idx] = REG_VAL_OFFSET;
+				ctx->state.reg_offset[idx] = offset;
+			}
+			break;
+		}
+
+		case DW_CFA_val_offset_sf: {
+			unsigned long reg;
+			long offset;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_sleb128(&cur, end, &offset);
+			if (ret)
+				return ret;
+			offset *= fde->cie.data_align;
+
+			if (reg == EH_FRAME_REG_SP && offset != EH_FRAME_SP_VAL_OFFSET)
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg);
+			if (idx >= 0) {
+				ctx->state.reg_rule[idx] = REG_VAL_OFFSET;
+				ctx->state.reg_offset[idx] = offset;
+			}
+			break;
+		}
+
+		case DW_CFA_register: {
+			unsigned long reg1, reg2;
+			int idx;
+
+			ret = read_uleb128(&cur, end, &reg1);
+			if (ret)
+				return ret;
+			ret = read_uleb128(&cur, end, &reg2);
+			if (ret)
+				return ret;
+
+			if (reg1 == EH_FRAME_REG_SP && eh_frame_reject_sp_rule())
+				return -EOPNOTSUPP;
+
+			idx = reg_to_index(reg1);
+			if (idx >= 0) {
+				ctx->state.reg_rule[idx] = REG_REGISTER;
+				ctx->state.reg_regnum[idx] = reg2;
+			}
+			break;
+		}
+
+		case DW_CFA_expression:
+		case DW_CFA_val_expression: {
+			unsigned long reg, expr_len;
+
+			ret = read_uleb128(&cur, end, &reg);
+			if (ret)
+				return ret;
+			ret = read_uleb128(&cur, end, &expr_len);
+			if (ret)
+				return ret;
+
+			if (cur + expr_len > end)
+				return -EINVAL;
+
+			if (reg == EH_FRAME_REG_SP || reg == EH_FRAME_REG_FP || reg == EH_FRAME_REG_RA)
+				return -EOPNOTSUPP;
+
+			cur += expr_len;
+			break;
+		}
+
+		case DW_CFA_remember_state:
+			if (ctx->stack_depth >= EH_FRAME_MAX_STATE_STACK)
+				return -EINVAL;
+			ctx->stack[ctx->stack_depth++] = ctx->state;
+			break;
+
+		case DW_CFA_restore_state:
+			if (ctx->stack_depth <= 0)
+				return -EINVAL;
+			ctx->state = ctx->stack[--ctx->stack_depth];
+			break;
+
+		default:
+			return -EOPNOTSUPP;
+		}
+		break;
+
+	default:
+		return -EOPNOTSUPP;
+	}
+
+	*cur_ptr = cur;
+	*ip_ptr = ip;
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __do_cfi_program(struct eh_frame_section *sec,
+					    struct eh_frame_fde *fde,
+					    unsigned long target_ip,
+					    struct eh_frame_cfi_context *ctx)
+{
+	void __user *cfi_ptr;
+	unsigned long cfi_size;
+	unsigned long ip = fde->func_addr;
+	unsigned long cur;
+	int ret;
+
+	/* Initialize state */
+	ctx->state.cfa_rule = CFA_UNDEFINED;
+	ctx->state.reg_rule[FP_IDX] = REG_UNDEFINED_IMPLICIT;
+	ctx->state.reg_rule[RA_IDX] = REG_UNDEFINED_IMPLICIT;
+	ctx->stack_depth = 0;
+
+	/* Process CIE initial CFI instructions (if any) */
+	cfi_ptr = (void __user *)fde->cie.cfi_insn_start;
+	cfi_size = fde->cie.cfi_insn_end - fde->cie.cfi_insn_start;
+	scoped_user_read_access_size(cfi_ptr, cfi_size, Efault) {
+		cur = fde->cie.cfi_insn_start;
+		while (cur < fde->cie.cfi_insn_end) {
+			ret = __do_cfi_insn(sec, fde, &cur, fde->cie.cfi_insn_end, &ip, target_ip, ctx);
+			if (ret)
+				return ret;
+		}
+	}
+
+	/* Do not allow remember/restore between CIE and FDE */
+	ctx->stack_depth = 0;
+
+	/* Process FDE CFI instructions up to target IP */
+	cfi_ptr = (void __user *)fde->cfi_insn_start;
+	cfi_size = fde->cfi_insn_end - fde->cfi_insn_start;
+	scoped_user_read_access_size(cfi_ptr, cfi_size, Efault) {
+		cur = fde->cfi_insn_start;
+		while (cur < fde->cfi_insn_end && ip <= target_ip) {
+			ret = __do_cfi_insn(sec, fde, &cur, fde->cfi_insn_end, &ip, target_ip, ctx);
+			if (ret)
+				return ret;
+		}
+	}
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+static __always_inline int __find_frame_row(struct eh_frame_section *sec,
+					    struct eh_frame_fde *fde,
+					    unsigned long ip,
+					    struct unwind_user_frame *frame)
+{
+	struct eh_frame_cfi_context ctx;
+	int ret;
+
+	/* TODO: Signal frame - not supported yet */
+	if (fde->cie.signal_frame)
+		return -EOPNOTSUPP;
+
+	ret = __do_cfi_program(sec, fde, ip, &ctx);
+	if (ret)
+		return ret;
+
+	/* Convert CFA rule */
+	if (ctx.state.cfa_rule != CFA_REG_OFFSET)
+		return -EINVAL;
+
+	if (ctx.state.cfa_regnum == EH_FRAME_REG_SP)
+		frame->cfa.rule = UNWIND_USER_CFA_RULE_SP_OFFSET;
+	else if (ctx.state.cfa_regnum == EH_FRAME_REG_FP)
+		frame->cfa.rule = UNWIND_USER_CFA_RULE_FP_OFFSET;
+	else {
+		if (ctx.state.cfa_regnum > UINT_MAX)
+			return -EINVAL;
+		frame->cfa.rule = UNWIND_USER_CFA_RULE_REG_OFFSET;
+		frame->cfa.regnum = ctx.state.cfa_regnum;
+	}
+
+	if (ctx.state.cfa_offset < INT_MIN ||
+	    ctx.state.cfa_offset > INT_MAX)
+		return -EOPNOTSUPP;
+	frame->cfa.offset = ctx.state.cfa_offset;
+
+	/* Convert RA rule */
+	frame->outermost = false;
+	switch (ctx.state.reg_rule[RA_IDX]) {
+	case REG_UNDEFINED_IMPLICIT:
+		frame->ra.rule = UNWIND_USER_RULE_RETAIN;
+		break;
+	case REG_UNDEFINED_EXPLICIT:
+		frame->outermost = true;
+		break;
+	case REG_SAME_VALUE:
+		frame->ra.rule = UNWIND_USER_RULE_RETAIN;
+		break;
+	case REG_OFFSET:
+		if (ctx.state.reg_offset[RA_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[RA_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->ra.rule = UNWIND_USER_RULE_CFA_OFFSET_DEREF;
+		frame->ra.offset = ctx.state.reg_offset[RA_IDX];
+		break;
+	case REG_VAL_OFFSET:
+		if (ctx.state.reg_offset[RA_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[RA_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->ra.rule = UNWIND_USER_RULE_CFA_OFFSET;
+		frame->ra.offset = ctx.state.reg_offset[RA_IDX];
+		break;
+	case REG_REGISTER:
+		if (ctx.state.reg_regnum[RA_IDX] > UINT_MAX)
+			return -EINVAL;
+		frame->ra.rule = UNWIND_USER_RULE_REG_OFFSET;
+		frame->ra.regnum = ctx.state.reg_regnum[RA_IDX];
+		frame->ra.offset = 0;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	/* Convert FP rule */
+	switch (ctx.state.reg_rule[FP_IDX]) {
+	case REG_UNDEFINED_IMPLICIT:
+	case REG_UNDEFINED_EXPLICIT:
+		frame->fp.rule = UNWIND_USER_RULE_RETAIN;
+		break;
+	case REG_SAME_VALUE:
+		frame->fp.rule = UNWIND_USER_RULE_RETAIN;
+		break;
+	case REG_OFFSET:
+		if (ctx.state.reg_offset[FP_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[FP_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->fp.rule = UNWIND_USER_RULE_CFA_OFFSET_DEREF;
+		frame->fp.offset = ctx.state.reg_offset[FP_IDX];
+		break;
+	case REG_VAL_OFFSET:
+		if (ctx.state.reg_offset[FP_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[FP_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->fp.rule = UNWIND_USER_RULE_CFA_OFFSET;
+		frame->fp.offset = ctx.state.reg_offset[FP_IDX];
+		break;
+	case REG_REGISTER:
+		if (ctx.state.reg_regnum[FP_IDX] > UINT_MAX)
+			return -EINVAL;
+		frame->fp.rule = UNWIND_USER_RULE_REG_OFFSET;
+		frame->fp.regnum = ctx.state.reg_regnum[FP_IDX];
+		frame->fp.offset = 0;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	/* SP offset from CFA used in implicit CFA rule */
+	frame->sp_off = EH_FRAME_SP_VAL_OFFSET;
+
+	return 0;
+}
+
+int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
+{
+	struct mm_struct *mm = current->mm;
+	struct eh_frame_section *sec;
+	struct eh_frame_fde fde;
+	int ret;
+
+	if (!mm)
+		return -EINVAL;
+
+	guard(srcu)(&eh_frame_srcu);
+
+	sec = mtree_load(&mm->eh_frame_mt, ip);
+	if (!sec)
+		return -ENOENT;
+
+	ret = __find_fde(sec, ip, &fde);
+	if (!ret)
+		ret = __find_frame_row(sec, &fde, ip, frame);
+
+	return ret;
+}
+
 static void free_section(struct eh_frame_section *sec)
 {
 	kfree(sec);
@@ -258,13 +1191,13 @@ static int eh_frame_read_header(struct eh_frame_section *sec)
 			return -EINVAL;
 
 		/* Read pointer to .eh_frame */
-		ret = read_encoded_pointer(sec, &cur, end,
+		ret = read_encoded_pointer(sec, NULL, &cur, end,
 					   eh_frame_ptr_enc, &eh_frame_start);
 		if (ret)
 			return ret;
 
 		/* Read FDE count */
-		ret = read_encoded_pointer(sec, &cur, end,
+		ret = read_encoded_pointer(sec, NULL, &cur, end,
 					   fde_count_enc, &fde_count);
 		if (ret)
 			return ret;
diff --git a/kernel/unwind/eh_frame.h b/kernel/unwind/eh_frame.h
index 77eda5376dfb..9a0c71102742 100644
--- a/kernel/unwind/eh_frame.h
+++ b/kernel/unwind/eh_frame.h
@@ -2,6 +2,38 @@
 #ifndef _EH_FRAME_H
 #define _EH_FRAME_H
 
+/* DWARF CFI opcodes */
+#define DW_CFA_advance_loc		0x40
+#define DW_CFA_offset			0x80
+#define DW_CFA_restore			0xc0
+#define DW_CFA_nop			0x00
+#define DW_CFA_set_loc			0x01
+#define DW_CFA_advance_loc1		0x02
+#define DW_CFA_advance_loc2		0x03
+#define DW_CFA_advance_loc4		0x04
+#define DW_CFA_offset_extended		0x05
+#define DW_CFA_restore_extended		0x06
+#define DW_CFA_undefined		0x07
+#define DW_CFA_same_value		0x08
+#define DW_CFA_register			0x09
+#define DW_CFA_remember_state		0x0a
+#define DW_CFA_restore_state		0x0b
+#define DW_CFA_def_cfa			0x0c
+#define DW_CFA_def_cfa_register		0x0d
+#define DW_CFA_def_cfa_offset		0x0e
+#define DW_CFA_def_cfa_expression	0x0f
+#define DW_CFA_expression		0x10
+#define DW_CFA_offset_extended_sf	0x11
+#define DW_CFA_def_cfa_sf		0x12
+#define DW_CFA_def_cfa_offset_sf	0x13
+#define DW_CFA_val_offset		0x14
+#define DW_CFA_val_offset_sf		0x15
+#define DW_CFA_val_expression		0x16
+
+/* Helpers for CFI opcodes */
+#define DW_CFA_opcode(insn)		((insn) & 0xc0)
+#define DW_CFA_operand(insn)		((insn) & 0x3f)
+
 /* DWARF exception header pointer encodings */
 #define DW_EH_PE_omit			0xff
 /* Formats */
@@ -27,4 +59,10 @@
 #define DW_EH_PE_format(encoding)	((encoding) & 0x0f)
 #define DW_EH_PE_application(encoding)	((encoding) & 0x70)
 
+/* CIE/FDE constants */
+#define EH_FRAME_CIE_ID			0
+#define EH_FRAME_DWARF64_LENGTH		0xffffffff
+#define EH_FRAME_CIE_MIN_LENGTH		13
+#define EH_FRAME_FDE_MIN_LENGTH		10
+
 #endif /* _EH_FRAME_H */
diff --git a/kernel/unwind/eh_frame_debug.h b/kernel/unwind/eh_frame_debug.h
new file mode 100644
index 000000000000..5a3e4e7f065a
--- /dev/null
+++ b/kernel/unwind/eh_frame_debug.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _EH_FRAME_DEBUG_H
+#define _EH_FRAME_DEBUG_H
+
+#include <linux/eh_frame.h>
+#include "eh_frame.h"
+
+#ifdef CONFIG_DYNAMIC_DEBUG
+
+#define dbg(fmt, ...)							\
+	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
+
+#else /* !CONFIG_DYNAMIC_DEBUG */
+
+#define dbg(args...)			no_printk(args)
+
+#endif /* !CONFIG_DYNAMIC_DEBUG */
+
+#endif /* _EH_FRAME_DEBUG_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (8 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
                   ` (15 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

When loading an ELF executable, automatically detect .eh_frame_hdr
sections and associate them with the mm_struct.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 fs/binfmt_elf.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 47 insertions(+), 2 deletions(-)

diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 16a56b6b3f6c..8c1cdd8318c5 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -48,6 +48,7 @@
 #include <linux/uaccess.h>
 #include <uapi/linux/rseq.h>
 #include <linux/rseq.h>
+#include <linux/eh_frame.h>
 #include <asm/param.h>
 #include <asm/page.h>
 
@@ -637,6 +638,23 @@ static inline int make_prot(u32 p_flags, struct arch_elf_state *arch_state,
 	return arch_elf_adjust_prot(prot, arch_state, has_interp, is_interp);
 }
 
+static void elf_add_eh_frame(const struct elf_phdr *text,
+			     const struct elf_phdr *eh_frame,
+			     unsigned long base_addr)
+{
+	unsigned long eh_frame_start, eh_frame_end,
+		      text_start, text_end;
+
+	eh_frame_start = base_addr + eh_frame->p_vaddr;
+	eh_frame_end   = eh_frame_start + eh_frame->p_memsz;
+
+	text_start   = base_addr + text->p_vaddr;
+	text_end     = text_start + text->p_memsz;
+
+	/* Ignore return value, eh_frame[_hdr] section isn't critical */
+	eh_frame_add_section(eh_frame_start, eh_frame_end, text_start, text_end);
+}
+
 /* This is much more generalized than the library routine read function,
    so we keep this separate.  Technically the library read function
    is only provided so that we can read a.out libraries that have
@@ -647,7 +665,7 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 		unsigned long no_base, struct elf_phdr *interp_elf_phdata,
 		struct arch_elf_state *arch_state)
 {
-	struct elf_phdr *eppnt;
+	struct elf_phdr *eppnt, *eh_frame_phdr = NULL;
 	unsigned long load_addr = 0;
 	int load_addr_set = 0;
 	unsigned long error = ~0UL;
@@ -673,7 +691,8 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 
 	eppnt = interp_elf_phdata;
 	for (i = 0; i < interp_elf_ex->e_phnum; i++, eppnt++) {
-		if (eppnt->p_type == PT_LOAD) {
+		switch (eppnt->p_type) {
+		case PT_LOAD: {
 			int elf_type = MAP_PRIVATE;
 			int elf_prot = make_prot(eppnt->p_flags, arch_state,
 						 true, true);
@@ -712,6 +731,19 @@ static unsigned long load_elf_interp(struct elfhdr *interp_elf_ex,
 				error = -ENOMEM;
 				goto out;
 			}
+			break;
+		}
+		case PT_GNU_EH_FRAME:
+			eh_frame_phdr = eppnt;
+			break;
+		}
+	}
+
+	if (eh_frame_phdr) {
+		eppnt = interp_elf_phdata;
+		for (i = 0; i < interp_elf_ex->e_phnum; i++, eppnt++) {
+			if (eppnt->p_flags & PF_X && eppnt->p_type == PT_LOAD)
+				elf_add_eh_frame(eppnt, eh_frame_phdr, load_addr);
 		}
 	}
 
@@ -837,6 +869,7 @@ static int load_elf_binary(struct linux_binprm *bprm)
 	unsigned long error;
 	struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
 	struct elf_phdr *elf_property_phdata = NULL;
+	struct elf_phdr *eh_frame_phdr = NULL;
 	unsigned long elf_brk;
 	bool brk_moved = false;
 	int retval, i;
@@ -945,6 +978,10 @@ static int load_elf_binary(struct linux_binprm *bprm)
 				executable_stack = EXSTACK_DISABLE_X;
 			break;
 
+		case PT_GNU_EH_FRAME:
+			eh_frame_phdr = elf_ppnt;
+			break;
+
 		case PT_LOPROC ... PT_HIPROC:
 			retval = arch_elf_pt_proc(elf_ex, elf_ppnt,
 						  bprm->file, false,
@@ -1242,6 +1279,14 @@ static int load_elf_binary(struct linux_binprm *bprm)
 			elf_brk = k;
 	}
 
+	if (eh_frame_phdr) {
+		for (i = 0, elf_ppnt = elf_phdata;
+		     i < elf_ex->e_phnum; i++, elf_ppnt++) {
+			if (elf_ppnt->p_flags & PF_X && elf_ppnt->p_type == PT_LOAD)
+				elf_add_eh_frame(elf_ppnt, eh_frame_phdr, load_bias);
+		}
+	}
+
 	e_entry = elf_ex->e_entry + load_bias;
 	phdr_addr += load_bias;
 	elf_brk += load_bias;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (9 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
                   ` (14 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Now that the eh_frame infrastructure is fully in place, make it work by
hooking it up to the unwind_user interface.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/Kconfig                      |  1 +
 include/linux/unwind_user_types.h |  4 +++-
 kernel/unwind/user.c              | 23 +++++++++++++++++++++++
 3 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/arch/Kconfig b/arch/Kconfig
index 60542d5e5731..ea969811f798 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -488,6 +488,7 @@ config UNWIND_USER
 
 config HAVE_UNWIND_USER_EH_FRAME
 	bool
+	select UNWIND_USER
 
 config HAVE_UNWIND_USER_FP
 	bool
diff --git a/include/linux/unwind_user_types.h b/include/linux/unwind_user_types.h
index 4da058096259..88fc2fee8534 100644
--- a/include/linux/unwind_user_types.h
+++ b/include/linux/unwind_user_types.h
@@ -9,7 +9,8 @@
  * available.
  */
 enum unwind_user_type_bits {
-	UNWIND_USER_TYPE_FP_BIT =		0,
+	UNWIND_USER_TYPE_EH_FRAME_BIT =		0,
+	UNWIND_USER_TYPE_FP_BIT =		1,
 
 	NR_UNWIND_USER_TYPE_BITS,
 };
@@ -17,6 +18,7 @@ enum unwind_user_type_bits {
 enum unwind_user_type {
 	/* Type "none" for the start of stack walk iteration. */
 	UNWIND_USER_TYPE_NONE =			0,
+	UNWIND_USER_TYPE_EH_FRAME =		BIT(UNWIND_USER_TYPE_EH_FRAME_BIT),
 	UNWIND_USER_TYPE_FP =			BIT(UNWIND_USER_TYPE_FP_BIT),
 };
 
diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index 830c620fe453..85fc82252af1 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -10,6 +10,7 @@
 #include <linux/sched/task_stack.h>
 #include <linux/unwind_user.h>
 #include <linux/uaccess.h>
+#include <linux/eh_frame.h>
 
 #ifdef CONFIG_DYNAMIC_DEBUG
 
@@ -173,6 +174,16 @@ static int unwind_user_next_fp(struct unwind_user_state *state)
 	return unwind_user_next_common(state, &fp_frame);
 }
 
+static int unwind_user_next_eh_frame(struct unwind_user_state *state)
+{
+	struct unwind_user_frame frame;
+
+	/* eh_frame expects the frame to be local storage */
+	if (eh_frame_find(state->ip, &frame))
+		return -ENOENT;
+	return unwind_user_next_common(state, &frame);
+}
+
 static int unwind_user_next(struct unwind_user_state *state)
 {
 	unsigned long iter_mask = state->available_types;
@@ -186,6 +197,16 @@ static int unwind_user_next(struct unwind_user_state *state)
 
 		state->current_type = type;
 		switch (type) {
+		case UNWIND_USER_TYPE_EH_FRAME:
+			switch (unwind_user_next_eh_frame(state)) {
+			case 0:
+				return 0;
+			case -ENOENT:
+				continue;	/* Try next method. */
+			default:
+				state->done = true;
+			}
+			break;
 		case UNWIND_USER_TYPE_FP:
 			if (!unwind_user_next_fp(state))
 				return 0;
@@ -214,6 +235,8 @@ static int unwind_user_start(struct unwind_user_state *state)
 		return -EINVAL;
 	}
 
+	if (current_has_eh_frame())
+		state->available_types |= UNWIND_USER_TYPE_EH_FRAME;
 	if (IS_ENABLED(CONFIG_HAVE_UNWIND_USER_FP))
 		state->available_types |= UNWIND_USER_TYPE_FP;
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (10 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 13/25] unwind_user/eh_frame: Show file name in debug output Jens Remus
                   ` (13 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

To avoid continued attempted use of a bad .eh_frame[_hdr] sections, remove
them on demand when the first sign of corruption is detected.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 kernel/unwind/eh_frame.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 5ef8fef06a80..7657291324c0 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1149,6 +1149,15 @@ int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
 	if (!ret)
 		ret = __find_frame_row(sec, &fde, ip, frame);
 
+	/*
+	 * Unregister .eh_frame[_hdr] in case of an error,
+	 * e.g. EINVAL (corrupted) or EFAULT (inaccessible).
+	 * Keep if ENOENT (not found) or EOPNOTSUPP (unsupported CFI).
+	 */
+	if (ret && (ret != -ENOENT && ret != -EOPNOTSUPP))
+		if (eh_frame_remove_section(sec->eh_frame_hdr_start))
+			dbg("eh_frame_remove_section() failed\n");
+
 	return ret;
 }
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 13/25] unwind_user/eh_frame: Show file name in debug output
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (11 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
                   ` (12 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

When debugging eh_frame issues, the error messages aren't all that helpful
without knowing what file a corresponding .eh_frame[_hdr] section belongs
to.  Prefix debug output strings with the file name.

Based on Josh Poimboeuf's, Steven Rostedt's, and my unwind user sframe
implementation.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 include/linux/eh_frame.h       |  4 ++++
 kernel/unwind/eh_frame.c       | 13 ++++++++----
 kernel/unwind/eh_frame_debug.h | 37 ++++++++++++++++++++++++++++++++++
 3 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index e33041e4f9c0..b2f98cd6166f 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -11,6 +11,10 @@
 struct eh_frame_section {
 	struct rcu_head	rcu;
 
+#ifdef CONFIG_DYNAMIC_DEBUG
+	const char	*filename;
+#endif
+
 	unsigned long	eh_frame_hdr_start;
 	unsigned long	eh_frame_hdr_end;
 	unsigned long	text_start;
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 7657291324c0..46ffb535ca53 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1154,15 +1154,18 @@ int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
 	 * e.g. EINVAL (corrupted) or EFAULT (inaccessible).
 	 * Keep if ENOENT (not found) or EOPNOTSUPP (unsupported CFI).
 	 */
-	if (ret && (ret != -ENOENT && ret != -EOPNOTSUPP))
+	if (ret && (ret != -ENOENT && ret != -EOPNOTSUPP)) {
+		dbg_sec("removing bad .eh_frame[_hdr] section\n");
 		if (eh_frame_remove_section(sec->eh_frame_hdr_start))
 			dbg("eh_frame_remove_section() failed\n");
+	}
 
 	return ret;
 }
 
 static void free_section(struct eh_frame_section *sec)
 {
+	dbg_free(sec);
 	kfree(sec);
 }
 
@@ -1290,6 +1293,8 @@ int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 	sec->text_start		= text_start;
 	sec->text_end		= text_end;
 
+	dbg_init(sec);
+
 	ret = eh_frame_read_header(sec);
 	if (ret)
 		goto err_free;
@@ -1297,8 +1302,8 @@ int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 	ret = mtree_insert_range(eh_frame_mt, sec->text_start, sec->text_end - 1,
 				 sec, GFP_KERNEL_ACCOUNT);
 	if (ret) {
-		dbg("mtree_insert_range failed: text=%lx-%lx\n",
-		    sec->text_start, sec->text_end);
+		dbg_sec("mtree_insert_range failed: text=%lx-%lx\n",
+			sec->text_start, sec->text_end);
 		goto err_free;
 	}
 
@@ -1320,7 +1325,7 @@ static int __eh_frame_remove_section(struct mm_struct *mm,
 				     struct eh_frame_section *sec)
 {
 	if (!mtree_erase(&mm->eh_frame_mt, sec->text_start)) {
-		dbg("mtree_erase failed: text=%lx\n", sec->text_start);
+		dbg_sec("mtree_erase failed: text=%lx\n", sec->text_start);
 		return -EINVAL;
 	}
 
diff --git a/kernel/unwind/eh_frame_debug.h b/kernel/unwind/eh_frame_debug.h
index 5a3e4e7f065a..40a80861d4d4 100644
--- a/kernel/unwind/eh_frame_debug.h
+++ b/kernel/unwind/eh_frame_debug.h
@@ -3,6 +3,7 @@
 #define _EH_FRAME_DEBUG_H
 
 #include <linux/eh_frame.h>
+#include <linux/mm.h>
 #include "eh_frame.h"
 
 #ifdef CONFIG_DYNAMIC_DEBUG
@@ -10,9 +11,45 @@
 #define dbg(fmt, ...)							\
 	pr_debug("%s (%d): " fmt, current->comm, current->pid, ##__VA_ARGS__)
 
+#define dbg_sec(fmt, ...)						\
+	dbg("%s: " fmt, sec->filename, ##__VA_ARGS__)
+
+static inline void dbg_init(struct eh_frame_section *sec)
+{
+	struct mm_struct *mm = current->mm;
+	struct vm_area_struct *vma;
+	const char *name;
+
+	guard(mmap_read_lock)(mm);
+	vma = vma_lookup(mm, sec->eh_frame_hdr_start);
+	if (!vma)
+		sec->filename = kstrdup("(vma gone???)", GFP_KERNEL_ACCOUNT);
+	else if (vma_is_anonymous(vma))
+		 sec->filename = kstrdup("(anonymous)", GFP_KERNEL_ACCOUNT);
+	else if (vma->vm_file)
+		sec->filename = kstrdup_quotable_file(vma->vm_file, GFP_KERNEL_ACCOUNT);
+	else if (vma->vm_ops && vma->vm_ops->name && (name = vma->vm_ops->name(vma)))
+		sec->filename = kstrdup(name, GFP_KERNEL_ACCOUNT);
+	else if (arch_vma_name(vma))
+		sec->filename = kstrdup(arch_vma_name(vma), GFP_KERNEL_ACCOUNT);
+	else if (!vma->vm_mm)
+		sec->filename = kstrdup("(vdso)", GFP_KERNEL_ACCOUNT);
+	else
+		sec->filename = kstrdup("(vma unknown???)", GFP_KERNEL_ACCOUNT);
+}
+
+static inline void dbg_free(struct eh_frame_section *sec)
+{
+	kfree(sec->filename);
+}
+
 #else /* !CONFIG_DYNAMIC_DEBUG */
 
 #define dbg(args...)			no_printk(args)
+#define dbg_sec(args...)		no_printk(args)
+
+static inline void dbg_init(struct eh_frame_section *sec) {}
+static inline void dbg_free(struct eh_frame_section *sec) {}
 
 #endif /* !CONFIG_DYNAMIC_DEBUG */
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (12 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 13/25] unwind_user/eh_frame: Show file name in debug output Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
                   ` (11 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Add a debug feature to validate all .eh_frame[_hdr] sections when first
loading the file rather than on demand.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    FIXME: dbg*() with UACCESS enabled.

 arch/Kconfig                   | 22 ++++++++
 kernel/unwind/eh_frame.c       | 93 ++++++++++++++++++++++++++++++++++
 kernel/unwind/eh_frame_debug.h |  4 ++
 3 files changed, 119 insertions(+)

diff --git a/arch/Kconfig b/arch/Kconfig
index ea969811f798..30d9e876f28a 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -490,6 +490,28 @@ config HAVE_UNWIND_USER_EH_FRAME
 	bool
 	select UNWIND_USER
 
+config EH_FRAME_VALIDATION
+	bool "Enable .eh_frame[_hdr] section debugging"
+	depends on HAVE_UNWIND_USER_EH_FRAME
+	depends on DYNAMIC_DEBUG
+	help
+	  When adding an .eh_frame_hdr section for a test, validate the
+	  entire section and its referenced entrire .eh_frame section
+	  immediately rather than on demand.
+
+	  This is a debug feature which is helpful for rooting out
+	  .eh_frame[_hdr] section issues.  If the .eh_frame[_hdr]
+	  section is corrupt, it will fail to load immediately, with
+	  more information provided in dynamic printks.
+
+	  This has a significant page cache footprint due to its reading
+	  of the entire .eh_frame[_hdr] sections for every loaded executable
+	  and shared library.  Also, it's done for all processes, even those
+	  which don't get stack traced by the kernel.  Not recommended for
+	  general use.
+
+	  If unsure, say N.
+
 config HAVE_UNWIND_USER_FP
 	bool
 	select UNWIND_USER
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 46ffb535ca53..c9161229196c 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1163,6 +1163,95 @@ int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
 	return ret;
 }
 
+#ifdef CONFIG_EH_FRAME_VALIDATION
+
+static int eh_frame_validate_section(struct eh_frame_section *sec)
+{
+	void __user *table_start_ptr;
+	unsigned long table_size;
+	u8 table_enc;
+	int entry_size;
+	unsigned long prev_func_addr;
+	unsigned int i;
+
+	if (!sec->has_binary_search_table)
+		return 0;
+
+	if (!sec->fde_count) {
+		dbg_sec(".eh_frame_hdr: invalid FDE count\n");
+		return -EINVAL;
+	}
+
+	table_enc = sec->binary_search_table_enc;
+	entry_size = 2 * encoded_pointer_size(table_enc);
+	if (!entry_size) {
+		dbg_sec(".eh_frame_hdr: invalid binary search table entry size\n");
+		return -EINVAL;
+	}
+	table_start_ptr = (void __user *)sec->binary_search_table_start;
+	table_size = sec->binary_search_table_end - sec->binary_search_table_start;
+
+	for (i = 0; i < sec->fde_count; i++) {
+		struct eh_frame_fde fde;
+		unsigned long cur;
+		unsigned long func_addr, fde_addr;
+		int ret;
+
+		cur = sec->binary_search_table_start + i * entry_size;
+
+		scoped_user_read_access_size(table_start_ptr, table_size, Efault) {
+			/* Read function start address from table */
+			ret = read_encoded_pointer(sec, NULL, &cur,
+						   sec->binary_search_table_end,
+						   table_enc, &func_addr);
+			if (ret) {
+				dbg_sec_ehfh(cur, "table[%u]: failed to read function start address\n", i);
+				return ret;
+			}
+			if (i && func_addr <= prev_func_addr) {
+				dbg_sec(".eh_frame_hdr: table[%u]: not sorted\n", i);
+				return -EINVAL;
+			}
+			prev_func_addr = func_addr;
+
+			/* Read FDE address from table */
+			ret = read_encoded_pointer(sec, NULL, &cur,
+						   sec->binary_search_table_end,
+						   table_enc, &fde_addr);
+			if (ret) {
+				dbg_sec_ehfh(cur, "table[%u]: failed to read FDE pointer\n", i);
+				return ret;
+			}
+			if (fde_addr < sec->eh_frame_start) {
+				dbg_sec(".eh_frame_hdr: table[%u]: invalid FDE address\n", i);
+				return -EINVAL;
+			}
+		}
+
+		ret = __read_fde(sec, fde_addr, &fde);
+		if (ret) {
+			dbg_sec(".eh_frame_hdr: table[%u]: failed to read FDE at .eh_frame+%#lx\n",
+				i, fde_addr - sec->eh_frame_start);
+			return ret;
+		}
+		if (func_addr != fde.func_addr) {
+			dbg_sec(".eh_frame_hdr: table[%u]: function start address mismatch\n", i);
+			return -EINVAL;
+		}
+	}
+
+	return 0;
+
+Efault:
+	return -EFAULT;
+}
+
+#else /* !CONFIG_EH_FRAME_VALIDATION */
+
+static int eh_frame_validate_section(struct eh_frame_section *sec) { return 0; }
+
+#endif /* !CONFIG_EH_FRAME_VALIDATION */
+
 static void free_section(struct eh_frame_section *sec)
 {
 	dbg_free(sec);
@@ -1299,6 +1388,10 @@ int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 	if (ret)
 		goto err_free;
 
+	ret = eh_frame_validate_section(sec);
+	if (ret)
+		goto err_free;
+
 	ret = mtree_insert_range(eh_frame_mt, sec->text_start, sec->text_end - 1,
 				 sec, GFP_KERNEL_ACCOUNT);
 	if (ret) {
diff --git a/kernel/unwind/eh_frame_debug.h b/kernel/unwind/eh_frame_debug.h
index 40a80861d4d4..bcb2d03ab9ab 100644
--- a/kernel/unwind/eh_frame_debug.h
+++ b/kernel/unwind/eh_frame_debug.h
@@ -14,6 +14,9 @@
 #define dbg_sec(fmt, ...)						\
 	dbg("%s: " fmt, sec->filename, ##__VA_ARGS__)
 
+#define dbg_sec_ehfh(addr, fmt, ...)					\
+	dbg_sec(".eh_frame_hdr+%#lx: " fmt, ((addr) - sec->eh_frame_hdr_start), ##__VA_ARGS__)
+
 static inline void dbg_init(struct eh_frame_section *sec)
 {
 	struct mm_struct *mm = current->mm;
@@ -47,6 +50,7 @@ static inline void dbg_free(struct eh_frame_section *sec)
 
 #define dbg(args...)			no_printk(args)
 #define dbg_sec(args...)		no_printk(args)
+#define dbg_sec_ehfh(args...)		no_printk(args)
 
 static inline void dbg_init(struct eh_frame_section *sec) {}
 static inline void dbg_free(struct eh_frame_section *sec) {}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (13 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback Jens Remus
                   ` (10 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

When duplicating a process' virtual memory mappings also duplicate all
of its registered .eh_frame_hdr sections stored in the per-mm maple tree
to enable stacktracing using eh_frame of the child process.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 include/linux/eh_frame.h       |  7 +++++
 kernel/unwind/eh_frame.c       | 48 ++++++++++++++++++++++++++++++++++
 kernel/unwind/eh_frame_debug.h |  7 +++++
 mm/mmap.c                      |  5 ++++
 4 files changed, 67 insertions(+)

diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index b2f98cd6166f..65f87c2714d8 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -30,6 +30,7 @@ struct eh_frame_section {
 };
 
 #define INIT_MM_EH_FRAME .eh_frame_mt = MTREE_INIT(eh_frame_mt, 0),
+extern int eh_frame_dup_mm(struct mm_struct *mm, struct mm_struct *oldmm);
 extern void eh_frame_free_mm(struct mm_struct *mm);
 
 extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
@@ -49,6 +50,12 @@ static inline bool current_has_eh_frame(void)
 #else /* !CONFIG_HAVE_UNWIND_USER_EH_FRAME */
 
 #define INIT_MM_EH_FRAME
+
+static inline int eh_frame_dup_mm(struct mm_struct *mm, struct mm_struct *oldmm)
+{
+	return 0;
+}
+
 static inline void eh_frame_free_mm(struct mm_struct *mm) {}
 
 static inline int eh_frame_add_section(unsigned long eh_frame_hdr_start,
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index c9161229196c..7f572d1711d3 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1450,6 +1450,54 @@ int eh_frame_remove_section(unsigned long eh_frame_hdr_start)
 	return 0;
 }
 
+static void __eh_frame_dup_section(struct eh_frame_section *sec,
+				   struct eh_frame_section *oldsec)
+{
+	sec->eh_frame_hdr_start	= oldsec->eh_frame_hdr_start;
+	sec->eh_frame_hdr_end	= oldsec->eh_frame_hdr_end;
+	sec->text_start		= oldsec->text_start;
+	sec->text_end		= oldsec->text_end;
+
+	sec->eh_frame_start		= oldsec->eh_frame_start;
+	sec->eh_frame_vma_end		= oldsec->eh_frame_vma_end;
+	sec->binary_search_table_start	= oldsec->binary_search_table_start;
+	sec->binary_search_table_end	= oldsec->binary_search_table_end;
+	sec->fde_count			= oldsec->fde_count;
+	sec->binary_search_table_enc	= oldsec->binary_search_table_enc;
+
+	dbg_dup(sec, oldsec);
+}
+
+int eh_frame_dup_mm(struct mm_struct *mm, struct mm_struct *oldmm)
+{
+	struct eh_frame_section *sec, *oldsec;
+	unsigned long index = 0;
+	int ret;
+
+	guard(srcu)(&eh_frame_srcu);
+
+	mt_for_each(&oldmm->eh_frame_mt, oldsec, index, ULONG_MAX) {
+		sec = kzalloc(sizeof(*sec), GFP_KERNEL_ACCOUNT);
+		if (!sec)
+			return -ENOMEM;
+
+		__eh_frame_dup_section(sec, oldsec);
+
+		ret = mtree_insert_range(&mm->eh_frame_mt,
+					 sec->text_start,
+					 sec->text_end - 1,
+					 sec, GFP_KERNEL_ACCOUNT);
+		if (ret)
+			goto err_free;
+	}
+
+	return 0;
+
+err_free:
+	free_section(sec);
+	return ret;
+}
+
 void eh_frame_free_mm(struct mm_struct *mm)
 {
 	struct eh_frame_section *sec;
diff --git a/kernel/unwind/eh_frame_debug.h b/kernel/unwind/eh_frame_debug.h
index bcb2d03ab9ab..e72e011ba539 100644
--- a/kernel/unwind/eh_frame_debug.h
+++ b/kernel/unwind/eh_frame_debug.h
@@ -41,6 +41,12 @@ static inline void dbg_init(struct eh_frame_section *sec)
 		sec->filename = kstrdup("(vma unknown???)", GFP_KERNEL_ACCOUNT);
 }
 
+static inline void dbg_dup(struct eh_frame_section *sec, struct eh_frame_section *oldsec)
+{
+	if (oldsec->filename)
+		sec->filename = kstrdup(oldsec->filename, GFP_KERNEL_ACCOUNT);
+}
+
 static inline void dbg_free(struct eh_frame_section *sec)
 {
 	kfree(sec->filename);
@@ -53,6 +59,7 @@ static inline void dbg_free(struct eh_frame_section *sec)
 #define dbg_sec_ehfh(args...)		no_printk(args)
 
 static inline void dbg_init(struct eh_frame_section *sec) {}
+static inline void dbg_dup(struct eh_frame_section *sec, struct eh_frame_section *oldsec) {}
 static inline void dbg_free(struct eh_frame_section *sec) {}
 
 #endif /* !CONFIG_DYNAMIC_DEBUG */
diff --git a/mm/mmap.c b/mm/mmap.c
index 2311ae7c2ff4..7715a799810e 100644
--- a/mm/mmap.c
+++ b/mm/mmap.c
@@ -48,6 +48,7 @@
 #include <linux/sched/mm.h>
 #include <linux/ksm.h>
 #include <linux/memfd.h>
+#include <linux/eh_frame.h>
 
 #include <linux/uaccess.h>
 #include <asm/cacheflush.h>
@@ -1844,6 +1845,9 @@ __latent_entropy int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
 			goto loop_out;
 		}
 	}
+	retval = eh_frame_dup_mm(mm, oldmm);
+	if (retval)
+		goto loop_out;
 	/* a new mm has just been created */
 	retval = arch_dup_mmap(oldmm, mm);
 loop_out:
@@ -1893,6 +1897,7 @@ __latent_entropy int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
 			vm_unacct_memory(charge);
 		}
 		__mt_destroy(&mm->mm_mt);
+		eh_frame_free_mm(mm);
 		/*
 		 * The mm_struct is going to exit, but the locks will be dropped
 		 * first.  Set the mm_struct as unstable is advisable as it is
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (14 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size Jens Remus
                   ` (9 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Fallback to a linear .eh_frame search when .eh_frame_hdr does not
contain a binary search table.  Add validation of the referenced
.eh_frame section as well.

While testing the .eh_frame validation, it was observed that many
ELF binaries contain .eh_frame sections without a zero terminator
("ZERO terminator" in readelf -wf output).

For linear search, this is problematic because .eh_frame_hdr only
provides a pointer to the start of the .eh_frame section and does
not describe its extent.  In the absence of a zero terminator,
__find_fde_lsearch() may walk beyond the end of the section when
there is no FDE for the IP.  This was discovered, as it causes the
added validation logic in eh_frame_validate_eh_frame() to read past
the section boundary.

Therefore linear .eh_frame search is guarded by config option
EH_FRAME_LINEAR_SEARCH.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    This patch highlights a potential issue in the linear .eh_frame search
    path:  FDE iteration may read beyond the bounds of the section if it
    lacks a zero terminator.
    
    That said, .eh_frame_hdr sections without a binary search table do not
    appear to exist in practice, so I currently favor dropping this patch
    in a follow-up revision.
    
    It is not clear under what circumstances .eh_frame is generated without
    a zero terminator.  There have been several GNU linker commits related
    to the .eh_frame zero terminator over the years, including:
    - f60e73e9fc09 ("Drop unwanted zero terminators")
    - 4de1599bcf04 ("ld -r abort in _bfd_elf_write_section_eh_frame")
    - 2e0ce1c84d32 ("Align eh_frame FDEs according to their encoding")
    - af471f828cc7 ("PR22048, Incorrect .eh_frame section in libc.so")
    - 9866ffe25a0f ("Remove .eh_frame zero terminators")
    
    Perhaps the zero terminator is expected to originate from crtend.o,
    though this remains to be verified.
    
    IIUC, GCC's libgcc unwinder appears exhibit similar out-of-bounds
    behavior in its linear .eh_frame search path, if the zero terminator
    is absent.

 arch/Kconfig                   |   9 ++
 include/linux/eh_frame.h       |   1 +
 kernel/unwind/eh_frame.c       | 183 +++++++++++++++++++++++++++++++--
 kernel/unwind/eh_frame_debug.h |   4 +
 4 files changed, 188 insertions(+), 9 deletions(-)

diff --git a/arch/Kconfig b/arch/Kconfig
index 30d9e876f28a..191baf01e948 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -490,6 +490,15 @@ config HAVE_UNWIND_USER_EH_FRAME
 	bool
 	select UNWIND_USER
 
+config EH_FRAME_LINEAR_SEARCH
+	bool "Enable .eh_frame linear search fallback"
+	depends on HAVE_UNWIND_USER_EH_FRAME
+	help
+	  When a .eh_frame_hdr section has no binary search table, fallback
+	  to linear search of the .eh_frame section for a FDE for an IP.
+
+	  If unsure, say N.
+
 config EH_FRAME_VALIDATION
 	bool "Enable .eh_frame[_hdr] section debugging"
 	depends on HAVE_UNWIND_USER_EH_FRAME
diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index 65f87c2714d8..de68f21e1050 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -27,6 +27,7 @@ struct eh_frame_section {
 	unsigned long	binary_search_table_end;
 	unsigned long	fde_count;
 	u8		binary_search_table_enc;
+	bool		has_binary_search_table;
 };
 
 #define INIT_MM_EH_FRAME .eh_frame_mt = MTREE_INIT(eh_frame_mt, 0),
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 7f572d1711d3..ac288cec8021 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -509,10 +509,9 @@ static __always_inline int __read_fde(struct eh_frame_section *sec,
 	return -EFAULT;
 }
 
-
-static __always_inline int __find_fde(struct eh_frame_section *sec,
-				      unsigned long ip,
-				      struct eh_frame_fde *fde)
+static __always_inline int __find_fde_bsearch(struct eh_frame_section *sec,
+					      unsigned long ip,
+					      struct eh_frame_fde *fde)
 {
 	void __user *table_start_ptr;
 	unsigned long table_size;
@@ -590,6 +589,82 @@ static __always_inline int __find_fde(struct eh_frame_section *sec,
 	return -EFAULT;
 }
 
+#ifdef CONFIG_EH_FRAME_LINEAR_SEARCH
+
+static __always_inline int __find_fde_lsearch(struct eh_frame_section *sec,
+					      unsigned long ip,
+					      struct eh_frame_fde *fde)
+{
+	unsigned long start = sec->eh_frame_start;
+	unsigned long vma_end = sec->eh_frame_vma_end;
+	unsigned long cur;
+	int ret;
+
+	/* Linear search through .eh_frame */
+	cur = start;
+	while (cur >= start && cur < vma_end) {
+		unsigned long entry_start = cur, entry_end;
+		u32 length, cie_id;
+		struct eh_frame_fde _fde;
+
+		/* Read CIE/FDE length */
+		ret = GET_USER_INC(length, cur, vma_end);
+		if (ret)
+			return ret;
+		if (!length)
+			break;			/* End marker */
+		if (length == EH_FRAME_DWARF64_LENGTH)
+			return -EINVAL;		/* DWARF64, remove .eh_frame */
+		entry_end = entry_start + 4 + length;
+		if (entry_end > vma_end)
+			return -EFAULT;
+
+		/* Read CIE ID / FDE CIE pointer */
+		ret = GET_USER_INC(cie_id, cur, entry_end);
+		if (ret)
+			return ret;
+		if (cie_id == EH_FRAME_CIE_ID) {
+			/* This is a CIE, skip it */
+			cur = entry_end;
+			continue;
+		}
+
+		/* This is an FDE, check if it covers the IP */
+		ret = __read_fde(sec, entry_start, &_fde);
+		if (ret)
+			return ret;
+		if (ip >= _fde.func_addr && ip < _fde.func_addr + _fde.func_size) {
+			*fde = _fde;
+			return 0;
+		}
+
+		cur = entry_end;
+	}
+
+	return -ENOENT;
+}
+
+#else /* !CONFIG_EH_FRAME_LINEAR_SEARCH */
+
+static __always_inline int __find_fde_lsearch(struct eh_frame_section *sec,
+					      unsigned long ip,
+					      struct eh_frame_fde *fde)
+{
+	return 0;
+}
+
+#endif /* !CONFIG_EH_FRAME_LINEAR_SEARCH */
+
+static __always_inline int __find_fde(struct eh_frame_section *sec,
+				      unsigned long ip,
+				      struct eh_frame_fde *fde)
+{
+	if (sec->has_binary_search_table)
+		return __find_fde_bsearch(sec, ip, fde);
+	else
+		return __find_fde_lsearch(sec, ip, fde);
+}
+
 /* Helper to convert DWARF register number to index (FP=0, RA=1) */
 static inline int reg_to_index(unsigned int reg)
 {
@@ -1165,7 +1240,7 @@ int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame)
 
 #ifdef CONFIG_EH_FRAME_VALIDATION
 
-static int eh_frame_validate_section(struct eh_frame_section *sec)
+static int eh_frame_validate_eh_frame_hdr(struct eh_frame_section *sec)
 {
 	void __user *table_start_ptr;
 	unsigned long table_size;
@@ -1246,6 +1321,90 @@ static int eh_frame_validate_section(struct eh_frame_section *sec)
 	return -EFAULT;
 }
 
+static int eh_frame_validate_eh_frame(struct eh_frame_section *sec)
+{
+	unsigned long start = sec->eh_frame_start;
+	unsigned long vma_end = sec->eh_frame_vma_end;
+	unsigned long cur;
+	int ret;
+
+	cur = start;
+	while (cur >= start && cur < vma_end) {
+		struct eh_frame_cie cie;
+		struct eh_frame_fde fde;
+		unsigned long entry_start = cur, entry_end;
+		u32 length, cie_id;
+
+		/* Read CIE/FDE length */
+		ret = GET_USER_INC(length, cur, vma_end);
+		if (ret) {
+			dbg_sec_ehf(cur, "failed to read CIE/FDE length\n");
+			return ret;
+		}
+		if (!length)
+			break;			/* End marker */
+		else if (length == EH_FRAME_DWARF64_LENGTH) {
+			dbg_sec_ehf(cur, "invalid CIE/FDE length (DWARF64)\n");
+			return -EINVAL;
+		}
+		entry_end = entry_start + 4 + length;
+
+		/* Read CIE ID / FDE CIE pointer */
+		ret = GET_USER_INC(cie_id, cur, entry_end);
+		if (ret) {
+			dbg_sec_ehf(cur, "failed to read CIE ID / FDE CIE pointer\n");
+			return ret;
+		}
+
+		if (cie_id == EH_FRAME_CIE_ID) {
+			/* This is a CIE */
+			ret = __read_cie(sec, entry_start, &cie);
+			if (ret) {
+				dbg_sec_ehf(entry_start, "failed to read CIE\n");
+				return ret;
+			}
+
+		} else {
+			/* This is a FDE */
+			ret = __read_fde(sec, entry_start, &fde);
+			if (ret) {
+				dbg_sec_ehf(entry_start, "failed to read FDE\n");
+				return ret;
+			}
+		}
+
+		cur = entry_end;
+	}
+
+	return 0;
+}
+
+static int eh_frame_validate_section(struct eh_frame_section *sec)
+{
+	int ret;
+
+	/*
+	 * Validate .eh_frame_hdr binary search table
+	 * (incl. all referenced FDE and CIE in .eh_frame).
+	 */
+	ret = eh_frame_validate_eh_frame_hdr(sec);
+	if (ret)
+		return ret;
+
+	/*
+	 * Validate .eh_frame CIE and FDE.  Skip if linear search
+	 * is disabled, as many .eh_frame sections lack a zero
+	 * terminator and the section end if unknown.
+	 */
+	if (IS_ENABLED(CONFIG_EH_FRAME_LINEAR_SEARCH)) {
+		ret = eh_frame_validate_eh_frame(sec);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
 #else /* !CONFIG_EH_FRAME_VALIDATION */
 
 static int eh_frame_validate_section(struct eh_frame_section *sec) { return 0; }
@@ -1266,6 +1425,7 @@ static int eh_frame_read_header(struct eh_frame_section *sec)
 	unsigned long eh_frame_start, eh_frame_vma_end, table_start, table_end;
 	u8 version, eh_frame_ptr_enc, fde_count_enc, table_enc;
 	unsigned long fde_count;
+	bool has_table = false;
 	int entry_size;
 	int ret;
 
@@ -1287,16 +1447,17 @@ static int eh_frame_read_header(struct eh_frame_section *sec)
 		UNSAFE_GET_USER_INC(fde_count_enc, cur, end, Efault);
 		UNSAFE_GET_USER_INC(table_enc, cur, end, Efault);
 
-		/* .eh_frame_hdr without binary search table is not supported */
-		if (fde_count_enc == DW_EH_PE_omit || table_enc == DW_EH_PE_omit)
-			return -EINVAL;
-
 		/* Read pointer to .eh_frame */
 		ret = read_encoded_pointer(sec, NULL, &cur, end,
 					   eh_frame_ptr_enc, &eh_frame_start);
 		if (ret)
 			return ret;
 
+		/* Handle binary search table if provided */
+		if (fde_count_enc == DW_EH_PE_omit || table_enc == DW_EH_PE_omit)
+			goto end;
+		has_table = true;
+
 		/* Read FDE count */
 		ret = read_encoded_pointer(sec, NULL, &cur, end,
 					   fde_count_enc, &fde_count);
@@ -1327,6 +1488,9 @@ static int eh_frame_read_header(struct eh_frame_section *sec)
 
 	sec->eh_frame_start		= eh_frame_start;
 	sec->eh_frame_vma_end		= eh_frame_vma_end;
+	sec->has_binary_search_table	= has_table;
+	if (!has_table)
+		return 0;
 	sec->binary_search_table_start	= table_start;
 	sec->binary_search_table_end	= table_end;
 	sec->binary_search_table_enc	= table_enc;
@@ -1464,6 +1628,7 @@ static void __eh_frame_dup_section(struct eh_frame_section *sec,
 	sec->binary_search_table_end	= oldsec->binary_search_table_end;
 	sec->fde_count			= oldsec->fde_count;
 	sec->binary_search_table_enc	= oldsec->binary_search_table_enc;
+	sec->has_binary_search_table	= oldsec->has_binary_search_table;
 
 	dbg_dup(sec, oldsec);
 }
diff --git a/kernel/unwind/eh_frame_debug.h b/kernel/unwind/eh_frame_debug.h
index e72e011ba539..e03fc8bfed86 100644
--- a/kernel/unwind/eh_frame_debug.h
+++ b/kernel/unwind/eh_frame_debug.h
@@ -17,6 +17,9 @@
 #define dbg_sec_ehfh(addr, fmt, ...)					\
 	dbg_sec(".eh_frame_hdr+%#lx: " fmt, ((addr) - sec->eh_frame_hdr_start), ##__VA_ARGS__)
 
+#define dbg_sec_ehf(addr, fmt, ...)					\
+	dbg_sec(".eh_frame+%#lx: " fmt, ((addr) - sec->eh_frame_start), ##__VA_ARGS__)
+
 static inline void dbg_init(struct eh_frame_section *sec)
 {
 	struct mm_struct *mm = current->mm;
@@ -57,6 +60,7 @@ static inline void dbg_free(struct eh_frame_section *sec)
 #define dbg(args...)			no_printk(args)
 #define dbg_sec(args...)		no_printk(args)
 #define dbg_sec_ehfh(args...)		no_printk(args)
+#define dbg_sec_ehf(args...)		no_printk(args)
 
 static inline void dbg_init(struct eh_frame_section *sec) {}
 static inline void dbg_dup(struct eh_frame_section *sec, struct eh_frame_section *oldsec) {}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (15 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
                   ` (8 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

The DW_CFA_GNU_args_size DWARF CFI instruction is used by GCC to track
the size of arguments pushed on the stack for exception handling
purposes.  It is not needed for stack tracing.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    This patch could be squashed into "unwind_user/eh_frame: Add support for
    reading .eh_frame section".  Leaving separate for review.

 kernel/unwind/eh_frame.c | 11 +++++++++++
 kernel/unwind/eh_frame.h |  1 +
 2 files changed, 12 insertions(+)

diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index ac288cec8021..64176242b7d8 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1012,6 +1012,17 @@ static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
 			break;
 		}
 
+		case DW_CFA_GNU_args_size: {
+			unsigned long args_size;
+
+			ret = read_uleb128(&cur, end, &args_size);
+			if (ret)
+				return ret;
+
+			/* Ignore DW_CFA_GNU_args_size */
+			break;
+		}
+
 		case DW_CFA_remember_state:
 			if (ctx->stack_depth >= EH_FRAME_MAX_STATE_STACK)
 				return -EINVAL;
diff --git a/kernel/unwind/eh_frame.h b/kernel/unwind/eh_frame.h
index 9a0c71102742..a43560486283 100644
--- a/kernel/unwind/eh_frame.h
+++ b/kernel/unwind/eh_frame.h
@@ -29,6 +29,7 @@
 #define DW_CFA_val_offset		0x14
 #define DW_CFA_val_offset_sf		0x15
 #define DW_CFA_val_expression		0x16
+#define DW_CFA_GNU_args_size		0x2e
 
 /* Helpers for CFI opcodes */
 #define DW_CFA_opcode(insn)		((insn) & 0xc0)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (16 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
                   ` (7 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Enable architectures to handle DWARF expressions in DWARF CFI
instructions DW_CFA_def_cfa_expression, DW_CFA_expression, and
DW_CFA_val_expression.  Limit the maximum expression length to a
reasonable size, while enabling architectures to override the
limit.

Architectures are supposed to only handle specific known expressions
or expression patterns, not to implement a stack-based expression
evaluation machinery.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 include/asm-generic/unwind_user_eh_frame.h | 41 +++++++++++++
 include/linux/unwind_user_eh_frame_types.h | 37 ++++++++++++
 kernel/unwind/eh_frame.c                   | 70 +++++++++++-----------
 3 files changed, 113 insertions(+), 35 deletions(-)
 create mode 100644 include/linux/unwind_user_eh_frame_types.h

diff --git a/include/asm-generic/unwind_user_eh_frame.h b/include/asm-generic/unwind_user_eh_frame.h
index e6d207597206..da9bc52a645a 100644
--- a/include/asm-generic/unwind_user_eh_frame.h
+++ b/include/asm-generic/unwind_user_eh_frame.h
@@ -2,6 +2,8 @@
 #ifndef _ASM_GENERIC_UNWIND_USER_EH_FRAME_H
 #define _ASM_GENERIC_UNWIND_USER_EH_FRAME_H
 
+#include <linux/unwind_user_eh_frame_types.h>
+
 #ifndef EH_FRAME_MAX_CIE_LENGTH
 #define EH_FRAME_MAX_CIE_LENGTH 128
 #endif
@@ -10,6 +12,10 @@
 #define EH_FRAME_MAX_AUGSTR_LENGTH 16
 #endif
 
+#ifndef EH_FRAME_MAX_EXPRESSION_LENGTH
+#define EH_FRAME_MAX_EXPRESSION_LENGTH 32
+#endif
+
 #ifndef EH_FRAME_MAX_STATE_STACK
 #define EH_FRAME_MAX_STATE_STACK 8
 #endif
@@ -39,5 +45,40 @@ static inline bool eh_frame_reject_sp_rule(void)
 #define eh_frame_reject_sp_rule eh_frame_reject_sp_rule
 #endif
 
+#ifndef eh_frame_do_def_cfa_expression
+static inline int eh_frame_do_def_cfa_expression(const char *expr,
+						 int size,
+						 unsigned long ip,
+						 struct eh_frame_reg_state *reg_state)
+{
+	return -EOPNOTSUPP;
+}
+#define eh_frame_do_def_cfa_expression eh_frame_do_def_cfa_expression
+#endif
+
+#ifndef eh_frame_do_expression
+static inline int eh_frame_do_expression(unsigned int reg,
+					 const char *expr,
+					 int size,
+					 unsigned long ip,
+					 struct eh_frame_reg_state *reg_state)
+{
+	return -EOPNOTSUPP;
+}
+#define eh_frame_do_expression eh_frame_do_expression
+#endif
+
+#ifndef eh_frame_do_val_expression
+static inline int eh_frame_do_val_expression(unsigned int reg,
+					     const char *expr,
+					     int size,
+					     unsigned long ip,
+					     struct eh_frame_reg_state *reg_state)
+{
+	return -EOPNOTSUPP;
+}
+#define eh_frame_do_val_expression eh_frame_do_val_expression
+#endif
+
 #endif /* _ASM_GENERIC_UNWIND_USER_EH_FRAME_H */
 
diff --git a/include/linux/unwind_user_eh_frame_types.h b/include/linux/unwind_user_eh_frame_types.h
new file mode 100644
index 000000000000..e9f9d1abb76f
--- /dev/null
+++ b/include/linux/unwind_user_eh_frame_types.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_UNWIND_USER_EH_FRAME_TYPES_H
+#define _LINUX_UNWIND_USER_EH_FRAME_TYPES_H
+
+enum eh_frame_cfa_rule {
+	CFA_UNDEFINED,		/* unrecoverable */
+	CFA_REG_OFFSET,		/* CFA = reg + offset */
+};
+
+enum eh_frame_reg_rule {
+	REG_UNDEFINED_IMPLICIT,	/* reg = reg */
+	REG_UNDEFINED_EXPLICIT,	/* unrecoverable; RA: outermost frame */
+	REG_SAME_VALUE,		/* reg = reg; TODO: reset to CIE initial CFI */
+	REG_OFFSET,		/* reg = *(CFA + offset) */
+	REG_VAL_OFFSET,		/* reg = CFA + offset */
+	REG_REGISTER,		/* reg = other_reg */
+};
+
+enum eh_frame_reg_index {
+	FP_IDX,			/* frame pointer (FP) */
+	RA_IDX,			/* return address (RA) */
+	NR_REGS
+};
+
+struct eh_frame_reg_state {
+	/* CFA recovery rule */
+	enum eh_frame_cfa_rule cfa_rule;
+	unsigned long cfa_regnum;
+	long cfa_offset;
+
+	/* FP and RA recovery rules (SP uses implicit recovery) */
+	enum eh_frame_reg_rule reg_rule[NR_REGS];
+	unsigned long reg_regnum[NR_REGS];
+	long reg_offset[NR_REGS];
+};
+
+#endif /* _LINUX_UNWIND_USER_EH_FRAME_TYPES_H */
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 64176242b7d8..f7f1234b0437 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -12,45 +12,14 @@
 #include <linux/mm.h>
 #include <linux/string_helpers.h>
 #include <linux/eh_frame.h>
+#include <linux/types.h>
 #include <linux/unwind_user_types.h>
+#include <linux/unwind_user_eh_frame_types.h>
 #include <asm/unwind_user_eh_frame.h>
 
 #include "eh_frame.h"
 #include "eh_frame_debug.h"
 
-/* Register state for CFI interpreter */
-enum eh_frame_cfa_rule {
-	CFA_UNDEFINED,		/* unrecoverable */
-	CFA_REG_OFFSET,		/* CFA = reg + offset */
-};
-
-enum eh_frame_reg_rule {
-	REG_UNDEFINED_IMPLICIT,	/* reg = reg */
-	REG_UNDEFINED_EXPLICIT,	/* unrecoverable; RA: outermost frame */
-	REG_SAME_VALUE,		/* reg = reg; TODO: reset to CIE initial CFI */
-	REG_OFFSET,		/* reg = *(CFA + offset) */
-	REG_VAL_OFFSET,		/* reg = CFA + offset */
-	REG_REGISTER,		/* reg = other_reg */
-};
-
-enum eh_frame_reg_index {
-	FP_IDX,			/* frame pointer (FP) */
-	RA_IDX,			/* return address (RA) */
-	NR_REGS
-};
-
-struct eh_frame_reg_state {
-	/* CFA recovery rule */
-	enum eh_frame_cfa_rule cfa_rule;
-	unsigned long cfa_regnum;
-	long cfa_offset;
-
-	/* FP and RA recovery rules (SP uses implicit recovery) */
-	enum eh_frame_reg_rule reg_rule[NR_REGS];
-	unsigned long reg_regnum[NR_REGS];
-	long reg_offset[NR_REGS];
-};
-
 struct eh_frame_cfi_context {
 	struct eh_frame_reg_state state;
 	struct eh_frame_reg_state stack[EH_FRAME_MAX_STATE_STACK];
@@ -839,6 +808,27 @@ static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
 			break;
 		}
 
+		case DW_CFA_def_cfa_expression: {
+			unsigned long expr_len;
+			char expr[EH_FRAME_MAX_EXPRESSION_LENGTH];
+
+			ret = read_uleb128(&cur, end, &expr_len);
+			if (ret)
+				return ret;
+
+			if (cur + expr_len > end)
+				return -EINVAL;
+
+			if (expr_len > sizeof(expr))
+				return -EOPNOTSUPP;
+			unsafe_copy_from_user(&expr, (void __user *)cur, expr_len, Efault);
+			ret = eh_frame_do_def_cfa_expression(expr, expr_len, target_ip, &ctx->state);
+			if (ret)
+				return ret;
+			cur += expr_len;
+			break;
+		}
+
 		case DW_CFA_undefined: {
 			unsigned long reg;
 			int idx;
@@ -1005,9 +995,19 @@ static __always_inline int __do_cfi_insn(struct eh_frame_section *sec,
 			if (cur + expr_len > end)
 				return -EINVAL;
 
-			if (reg == EH_FRAME_REG_SP || reg == EH_FRAME_REG_FP || reg == EH_FRAME_REG_RA)
-				return -EOPNOTSUPP;
+			if (reg == EH_FRAME_REG_SP || reg == EH_FRAME_REG_FP || reg == EH_FRAME_REG_RA) {
+				char expr[EH_FRAME_MAX_EXPRESSION_LENGTH];
 
+				if (expr_len > sizeof(expr))
+					return -EOPNOTSUPP;
+				unsafe_copy_from_user(&expr, (void __user *)cur, expr_len, Efault);
+				if (opcode == DW_CFA_expression)
+					ret = eh_frame_do_expression(reg, expr, expr_len, target_ip, &ctx->state);
+				else
+					ret = eh_frame_do_val_expression(reg, expr, expr_len, target_ip, &ctx->state);
+				if (ret)
+					return ret;
+			}
 			cur += expr_len;
 			break;
 		}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (17 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 20/25] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
                   ` (6 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James, Steven Rostedt (Google)

From: Josh Poimboeuf <jpoimboe@kernel.org>

Add an x86 implementation of unsafe_copy_from_user() similar to the
existing unsafe_copy_to_user().

Signed-off-by: Josh Poimboeuf <jpoimboe@kernel.org>
Signed-off-by: Steven Rostedt (Google) <rostedt@goodmis.org>
Reviewed-by: Indu Bhagat <ibhagatgnu@gmail.com>
Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v16 00/20] unwind_deferred: Implement sframe
    handling" series:
    https://lore.kernel.org/all/20260521142546.3908498-5-jremus@linux.ibm.com/

 arch/x86/include/asm/uaccess.h | 39 +++++++++++++++++++++++++---------
 1 file changed, 29 insertions(+), 10 deletions(-)

diff --git a/arch/x86/include/asm/uaccess.h b/arch/x86/include/asm/uaccess.h
index 3a0dd3c2b233..235886106f31 100644
--- a/arch/x86/include/asm/uaccess.h
+++ b/arch/x86/include/asm/uaccess.h
@@ -598,7 +598,7 @@ _label:									\
  * We want the unsafe accessors to always be inlined and use
  * the error labels - thus the macro games.
  */
-#define unsafe_copy_loop(dst, src, len, type, label)				\
+#define unsafe_copy_to_user_loop(dst, src, len, type, label)			\
 	while (len >= sizeof(type)) {						\
 		unsafe_put_user(*(type *)(src),(type __user *)(dst),label);	\
 		dst += sizeof(type);						\
@@ -606,15 +606,34 @@ _label:									\
 		len -= sizeof(type);						\
 	}
 
-#define unsafe_copy_to_user(_dst,_src,_len,label)			\
-do {									\
-	char __user *__ucu_dst = (_dst);				\
-	const char *__ucu_src = (_src);					\
-	size_t __ucu_len = (_len);					\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u64, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u32, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u16, label);	\
-	unsafe_copy_loop(__ucu_dst, __ucu_src, __ucu_len, u8, label);	\
+#define unsafe_copy_to_user(_dst, _src, _len, label)				\
+do {										\
+	void __user *__dst = (_dst);						\
+	const void *__src = (_src);						\
+	size_t __len = (_len);							\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u64, label);		\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u32, label);		\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u16, label);		\
+	unsafe_copy_to_user_loop(__dst, __src, __len, u8,  label);		\
+} while (0)
+
+#define unsafe_copy_from_user_loop(dst, src, len, type, label)			\
+	while (len >= sizeof(type)) {						\
+		unsafe_get_user(*(type *)(dst), (type __user *)(src), label);	\
+		dst += sizeof(type);						\
+		src += sizeof(type);						\
+		len -= sizeof(type);						\
+	}
+
+#define unsafe_copy_from_user(_dst, _src, _len, label)				\
+do {										\
+	void *__dst = (_dst);							\
+	const void __user *__src = (_src);					\
+	size_t __len = (_len);							\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u64, label);		\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u32, label);		\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u16, label);		\
+	unsafe_copy_from_user_loop(__dst, __src, __len, u8,  label);		\
 } while (0)
 
 #ifdef CONFIG_CC_HAS_ASM_GOTO_OUTPUT
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 20/25] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (18 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
                   ` (5 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

The x86-64 .eh_frame implementation works well with GCC and Clang
generated code.  Enable it.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/x86/Kconfig                            |  1 +
 arch/x86/include/asm/unwind_user.h          | 40 +++++++++++++++++++++
 arch/x86/include/asm/unwind_user_eh_frame.h | 22 ++++++++++++
 3 files changed, 63 insertions(+)
 create mode 100644 arch/x86/include/asm/unwind_user_eh_frame.h

diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index bdad90f210e4..fe7919915084 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -300,6 +300,7 @@ config X86
 	select HAVE_SYSCALL_TRACEPOINTS
 	select HAVE_UACCESS_VALIDATION		if HAVE_OBJTOOL
 	select HAVE_UNSTABLE_SCHED_CLOCK
+	select HAVE_UNWIND_USER_EH_FRAME	if X86_64
 	select HAVE_UNWIND_USER_FP		if X86_64
 	select HAVE_USER_RETURN_NOTIFIER
 	select HAVE_GENERIC_VDSO
diff --git a/arch/x86/include/asm/unwind_user.h b/arch/x86/include/asm/unwind_user.h
index c96645c824d1..df1966bcb0f6 100644
--- a/arch/x86/include/asm/unwind_user.h
+++ b/arch/x86/include/asm/unwind_user.h
@@ -4,6 +4,7 @@
 
 #ifdef CONFIG_UNWIND_USER
 
+#include <linux/sched/task_stack.h>
 #include <asm/ptrace.h>
 #include <asm/uprobes.h>
 
@@ -15,6 +16,45 @@ static inline int unwind_user_word_size(struct pt_regs *regs)
 	return user_64bit_mode(regs) ? 8 : 4;
 }
 
+#ifdef CONFIG_X86_64
+
+static inline int unwind_user_get_reg(unsigned long *val, unsigned int regnum)
+{
+	struct pt_regs *regs = task_pt_regs(current);
+
+	/* TODO: eh_frame is currently limited to x86-64 */
+	if (!user_64bit_mode(regs))
+		return -EINVAL;
+
+	switch (regnum) {
+	/* DWARF register numbers 0..15 */
+	case  0: *val = regs->ax; break;
+	case  1: *val = regs->dx; break;
+	case  2: *val = regs->cx; break;
+	case  3: *val = regs->bx; break;
+	case  4: *val = regs->si; break;
+	case  5: *val = regs->di; break;
+	case  6: *val = regs->bp; break;
+	case  7: *val = regs->sp; break;
+	case  8: *val = regs->r8; break;
+	case  9: *val = regs->r9; break;
+	case 10: *val = regs->r10; break;
+	case 11: *val = regs->r11; break;
+	case 12: *val = regs->r12; break;
+	case 13: *val = regs->r13; break;
+	case 14: *val = regs->r14; break;
+	case 15: *val = regs->r15; break;
+	default:
+		pr_debug("%s (%d): %s(%u): unsupported register number\n",
+			 current->comm, current->pid, __func__, regnum);
+		return -EINVAL;
+	}
+	return 0;
+}
+#define unwind_user_get_reg unwind_user_get_reg
+
+#endif /* CONFIG_X86_64 */
+
 #endif /* CONFIG_UNWIND_USER */
 
 #ifdef CONFIG_HAVE_UNWIND_USER_FP
diff --git a/arch/x86/include/asm/unwind_user_eh_frame.h b/arch/x86/include/asm/unwind_user_eh_frame.h
new file mode 100644
index 000000000000..fdccbda2fe4b
--- /dev/null
+++ b/arch/x86/include/asm/unwind_user_eh_frame.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_X86_UNWIND_USER_EH_FRAME_H
+#define _ASM_X86_UNWIND_USER_EH_FRAME_H
+
+#ifdef CONFIG_X86_64
+
+#define EH_FRAME_REG_SP	7	/* designated stack pointer register */
+#define EH_FRAME_REG_FP	6	/* designated frame pointer register */
+#define EH_FRAME_REG_RA	16	/* (pseudo) return address register */
+
+/* Instructions must be 1-byte aligned */
+#define EH_FRAME_MAX_CODE_ALIGN 1
+
+/* Stack grows towards lower addresses and SP must be 8-byte aligned */
+#define EH_FRAME_MIN_DATA_ALIGN -8
+#define EH_FRAME_MAX_DATA_ALIGN -1
+
+#endif
+
+#include <asm-generic/unwind_user_eh_frame.h>
+
+#endif /* _ASM_X86_UNWIND_USER_EH_FRAME_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (19 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 20/25] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
                   ` (4 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

Add x86-specific support for handling the CFA expression found in GNU
linker generated .eh_frame for PLT.

This enables unwinding through GNU linker generated PLT entries on i386
and x86-64, unless linker option --no-ld-generated-unwind-info is used.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/x86/include/asm/unwind_user_eh_frame.h | 68 ++++++++++++++++++++-
 kernel/unwind/eh_frame.c                    |  3 +-
 kernel/unwind/eh_frame.h                    | 14 +++++
 3 files changed, 83 insertions(+), 2 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user_eh_frame.h b/arch/x86/include/asm/unwind_user_eh_frame.h
index fdccbda2fe4b..8268eb1a0ff5 100644
--- a/arch/x86/include/asm/unwind_user_eh_frame.h
+++ b/arch/x86/include/asm/unwind_user_eh_frame.h
@@ -2,6 +2,8 @@
 #ifndef _ASM_X86_UNWIND_USER_EH_FRAME_H
 #define _ASM_X86_UNWIND_USER_EH_FRAME_H
 
+#include <linux/unwind_user_eh_frame_types.h>
+
 #ifdef CONFIG_X86_64
 
 #define EH_FRAME_REG_SP	7	/* designated stack pointer register */
@@ -15,7 +17,71 @@
 #define EH_FRAME_MIN_DATA_ALIGN -8
 #define EH_FRAME_MAX_DATA_ALIGN -1
 
-#endif
+#endif /* CONFIG_X86_64 */
+
+static inline int memcmp_masked(const void *s1, const void *s2,
+				const void *mask, size_t n)
+{
+	const unsigned char *p1 = s1, *p2 = s2, *m = mask;
+	int res = 0;
+
+	while (n--)
+		if ((res = (*p1++ ^ *p2++) & *m++))
+			break;
+
+	return res;
+}
+
+static inline int eh_frame_do_def_cfa_expression(const char *expr,
+						 int size,
+						 unsigned long ip,
+						 struct eh_frame_reg_state *reg_state)
+{
+	/*
+	 * PLT CFA expression:
+	 *
+	 * DW_OP_breg<SP> + <SP_offset>	// 4 (ESP) + 4 or 7 (RSP) + 8
+	 * DW_OP_breg<IP> + 0		// 8 (EIP) or 16 (RIP)
+	 * DW_OP_lit15
+	 * DW_OP_and
+	 * DW_OP_lit<N>
+	 * DW_OP_ge
+	 * DW_OP_lit<shift>		// 2 or 3
+	 * DW_OP_shl
+	 * DW_OP_plus
+	 *
+	 * CFA = (SP + offset) + (((IP & 0xf) >= N) << shift)
+	 */
+	static const char plt_expr[] = {0x00,0x00,0x00,0x00,0x3f,0x1a,0x30,0x2a,0x30,0x24,0x22};
+	static const char plt_mask[] = {0x00,0xf0,0x00,0xff,0xff,0xff,0xf0,0xff,0xf0,0xff,0xff};
+
+	if (size == sizeof(plt_expr) &&
+	    !memcmp_masked(expr, plt_expr, plt_mask, sizeof(plt_expr))) {
+		unsigned char sp_op = expr[0];
+		unsigned char sp_offset = expr[1] & 0x0f;
+		unsigned char ip_op = expr[2];
+		unsigned char n = DW_OP_lit_value(expr[6]);
+		unsigned char shift = DW_OP_lit_value(expr[8]);
+		unsigned char sp_reg, ip_reg;
+
+		if (!DW_OP_is_breg(sp_op) || !DW_OP_is_breg(ip_op))
+			return -EOPNOTSUPP;
+
+		sp_reg = DW_OP_breg_register(sp_op);
+		ip_reg = DW_OP_breg_register(ip_op);
+		if (sp_reg != EH_FRAME_REG_SP || ip_reg != EH_FRAME_REG_RA)
+			return -EOPNOTSUPP;
+
+		/* CFA = (SP + SP_offset) + (((IP & 0xf) >= N) << shift) */
+		reg_state->cfa_rule = CFA_REG_OFFSET;
+		reg_state->cfa_regnum = EH_FRAME_REG_SP;
+		reg_state->cfa_offset = sp_offset + (((ip & 15) >= n) << shift);
+		return 0;
+	}
+
+	return -EOPNOTSUPP;
+}
+#define eh_frame_do_def_cfa_expression eh_frame_do_def_cfa_expression
 
 #include <asm-generic/unwind_user_eh_frame.h>
 
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index f7f1234b0437..19e2bc96ddbc 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -15,11 +15,12 @@
 #include <linux/types.h>
 #include <linux/unwind_user_types.h>
 #include <linux/unwind_user_eh_frame_types.h>
-#include <asm/unwind_user_eh_frame.h>
 
 #include "eh_frame.h"
 #include "eh_frame_debug.h"
 
+#include <asm/unwind_user_eh_frame.h>
+
 struct eh_frame_cfi_context {
 	struct eh_frame_reg_state state;
 	struct eh_frame_reg_state stack[EH_FRAME_MAX_STATE_STACK];
diff --git a/kernel/unwind/eh_frame.h b/kernel/unwind/eh_frame.h
index a43560486283..2f87d1b32bbc 100644
--- a/kernel/unwind/eh_frame.h
+++ b/kernel/unwind/eh_frame.h
@@ -60,6 +60,20 @@
 #define DW_EH_PE_format(encoding)	((encoding) & 0x0f)
 #define DW_EH_PE_application(encoding)	((encoding) & 0x70)
 
+/* DWARF expression operations */
+#define DW_OP_lit0	0x30
+/* ... */
+#define DW_OP_lit31	0x4f
+#define DW_OP_breg0	0x70
+/* ... */
+#define DW_OP_breg31	0x8f
+
+/* Helpers for DWARF expression operations */
+#define DW_OP_is_lit(op)		((op) >= DW_OP_lit0 && (op) <= DW_OP_lit31)
+#define DW_OP_is_breg(op)		((op) >= DW_OP_breg0 && (op) <= DW_OP_breg31)
+#define DW_OP_lit_value(op)		((op) - DW_OP_lit0)
+#define DW_OP_breg_register(op)		((op) - DW_OP_breg0)
+
 /* CIE/FDE constants */
 #define EH_FRAME_CIE_ID			0
 #define EH_FRAME_DWARF64_LENGTH		0xffffffff
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (20 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
                   ` (3 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

GCC uses DRAP (Dynamic Realign Argument Pointer) when the stack needs
to be dynamically realigned (e.g. for aligned local variables).  This
uses DWARF expressions to describe how to unwind through such frames.

Add x86-specific handling for the CFA and FP expressions patterns:

1. CFA expression (DW_OP_breg<FP> + offset, DW_OP_deref):
   - Semantics: CFA = *(FP + offset)
   - Restores the CFA from the saved SP at FP-4 (i386) or FP-8 (x86-64).

2. FP expression (DW_OP_breg<FP> +0):
   - Semantics: FP = *(FP + 0)
   - Restores FP from DRAP location (FP+0).

The implementation uses mask-based pattern matching similar to the
existing x86 PLT expression support.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/x86/include/asm/unwind_user_eh_frame.h | 65 +++++++++++++++++++++
 include/linux/unwind_user_eh_frame_types.h  |  4 ++
 kernel/unwind/eh_frame.c                    | 57 ++++++++++++++----
 3 files changed, 115 insertions(+), 11 deletions(-)

diff --git a/arch/x86/include/asm/unwind_user_eh_frame.h b/arch/x86/include/asm/unwind_user_eh_frame.h
index 8268eb1a0ff5..670740cdbd8c 100644
--- a/arch/x86/include/asm/unwind_user_eh_frame.h
+++ b/arch/x86/include/asm/unwind_user_eh_frame.h
@@ -79,10 +79,75 @@ static inline int eh_frame_do_def_cfa_expression(const char *expr,
 		return 0;
 	}
 
+	/*
+	 * DRAP (Dynamic Realignment Pointer) CFA expression:
+	 *
+	 * DW_OP_breg<FP> + <FP_offset>	// 5 (EBP) - 4 or 6 (RBP) - 8
+	 * DW_OP_deref
+	 *
+	 * CFA = *(FP + FP_offset)
+	 */
+	static const char drap_expr[] = {0x70, 0x00, 0x06};
+	static const char drap_mask[] = {0xf0, 0x80, 0xff};
+
+	if (size == sizeof(drap_expr) &&
+	    !memcmp_masked(expr, drap_expr, drap_mask, sizeof(drap_expr))) {
+		unsigned char fp_reg = DW_OP_breg_register(expr[0]);
+		unsigned char fp_offset_byte = expr[1];
+		long fp_offset;
+
+		if (fp_reg != EH_FRAME_REG_FP)
+			return -EOPNOTSUPP;
+
+		fp_offset = (long)(fp_offset_byte);
+		if (fp_offset_byte & 0x40)
+			fp_offset |= -(1L << 7);	/* Sign extend */
+
+		/* CFA = *(FP + offset) */
+		reg_state->cfa_rule = CFA_REG_OFFSET_DEREF;
+		reg_state->cfa_regnum = EH_FRAME_REG_FP;
+		reg_state->cfa_offset = fp_offset;
+		return 0;
+	}
+
 	return -EOPNOTSUPP;
 }
 #define eh_frame_do_def_cfa_expression eh_frame_do_def_cfa_expression
 
+static inline int eh_frame_do_expression(unsigned int reg,
+					 const char *expr,
+					 int size,
+					 unsigned long ip,
+					 struct eh_frame_reg_state *reg_state)
+{
+	/*
+	 * DRAP (Dynamic Realignment Pointer) FP expression:
+	 *
+	 * DW_OP_breg<FP> +0	// 5 (EBP) or 6 (RBP)
+	 *
+	 * FP = *(FP + 0)
+	 */
+	static const char drap_fp_expr[] = {0x70, 0x00};
+	static const char drap_fp_mask[] = {0xf0, 0xff};
+
+	if (reg == EH_FRAME_REG_FP && size == sizeof(drap_fp_expr) &&
+	    !memcmp_masked(expr, drap_fp_expr, drap_fp_mask, sizeof(drap_fp_expr))) {
+		unsigned char fp_reg = DW_OP_breg_register(expr[0]);
+
+		if (fp_reg != EH_FRAME_REG_FP)
+			return -EOPNOTSUPP;
+
+		/* FP = *(FP + 0) */
+		reg_state->reg_rule[FP_IDX] = REG_REGISTER_OFFSET_DEREF;
+		reg_state->reg_regnum[FP_IDX] = EH_FRAME_REG_FP;
+		reg_state->reg_offset[FP_IDX] = 0;
+		return 0;
+	}
+
+	return -EOPNOTSUPP;
+}
+#define eh_frame_do_expression eh_frame_do_expression
+
 #include <asm-generic/unwind_user_eh_frame.h>
 
 #endif /* _ASM_X86_UNWIND_USER_EH_FRAME_H */
diff --git a/include/linux/unwind_user_eh_frame_types.h b/include/linux/unwind_user_eh_frame_types.h
index e9f9d1abb76f..9547e963bb5f 100644
--- a/include/linux/unwind_user_eh_frame_types.h
+++ b/include/linux/unwind_user_eh_frame_types.h
@@ -5,6 +5,8 @@
 enum eh_frame_cfa_rule {
 	CFA_UNDEFINED,		/* unrecoverable */
 	CFA_REG_OFFSET,		/* CFA = reg + offset */
+	/* CFA expressions rules */
+	CFA_REG_OFFSET_DEREF,	/* CFA = *(reg + offset) */
 };
 
 enum eh_frame_reg_rule {
@@ -14,6 +16,8 @@ enum eh_frame_reg_rule {
 	REG_OFFSET,		/* reg = *(CFA + offset) */
 	REG_VAL_OFFSET,		/* reg = CFA + offset */
 	REG_REGISTER,		/* reg = other_reg */
+	/* expressions rules */
+	REG_REGISTER_OFFSET_DEREF,	/* reg = *(other_reg + offset) */
 };
 
 enum eh_frame_reg_index {
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 19e2bc96ddbc..49e8a3e8d794 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -1120,18 +1120,33 @@ static __always_inline int __find_frame_row(struct eh_frame_section *sec,
 		return ret;
 
 	/* Convert CFA rule */
-	if (ctx.state.cfa_rule != CFA_REG_OFFSET)
+	switch (ctx.state.cfa_rule) {
+	case CFA_REG_OFFSET:
+		if (ctx.state.cfa_regnum == EH_FRAME_REG_SP)
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_SP_OFFSET;
+		else if (ctx.state.cfa_regnum == EH_FRAME_REG_FP)
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_FP_OFFSET;
+		else {
+			if (ctx.state.cfa_regnum > UINT_MAX)
+				return -EINVAL;
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_REG_OFFSET;
+			frame->cfa.regnum = ctx.state.cfa_regnum;
+		}
+		break;
+	case CFA_REG_OFFSET_DEREF:
+		if (ctx.state.cfa_regnum == EH_FRAME_REG_SP)
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_SP_OFFSET_DEREF;
+		else if (ctx.state.cfa_regnum == EH_FRAME_REG_FP)
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_FP_OFFSET_DEREF;
+		else {
+			if (ctx.state.cfa_regnum > UINT_MAX)
+				return -EINVAL;
+			frame->cfa.rule = UNWIND_USER_CFA_RULE_REG_OFFSET_DEREF;
+			frame->cfa.regnum = ctx.state.cfa_regnum;
+		}
+		break;
+	default:
 		return -EINVAL;
-
-	if (ctx.state.cfa_regnum == EH_FRAME_REG_SP)
-		frame->cfa.rule = UNWIND_USER_CFA_RULE_SP_OFFSET;
-	else if (ctx.state.cfa_regnum == EH_FRAME_REG_FP)
-		frame->cfa.rule = UNWIND_USER_CFA_RULE_FP_OFFSET;
-	else {
-		if (ctx.state.cfa_regnum > UINT_MAX)
-			return -EINVAL;
-		frame->cfa.rule = UNWIND_USER_CFA_RULE_REG_OFFSET;
-		frame->cfa.regnum = ctx.state.cfa_regnum;
 	}
 
 	if (ctx.state.cfa_offset < INT_MIN ||
@@ -1172,6 +1187,16 @@ static __always_inline int __find_frame_row(struct eh_frame_section *sec,
 		frame->ra.regnum = ctx.state.reg_regnum[RA_IDX];
 		frame->ra.offset = 0;
 		break;
+	case REG_REGISTER_OFFSET_DEREF:
+		if (ctx.state.reg_regnum[RA_IDX] > UINT_MAX)
+			return -EINVAL;
+		if (ctx.state.reg_offset[RA_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[RA_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->ra.rule = UNWIND_USER_RULE_REG_OFFSET_DEREF;
+		frame->ra.regnum = ctx.state.reg_regnum[RA_IDX];
+		frame->ra.offset = ctx.state.reg_offset[RA_IDX];
+		break;
 	default:
 		return -EINVAL;
 	}
@@ -1206,6 +1231,16 @@ static __always_inline int __find_frame_row(struct eh_frame_section *sec,
 		frame->fp.regnum = ctx.state.reg_regnum[FP_IDX];
 		frame->fp.offset = 0;
 		break;
+	case REG_REGISTER_OFFSET_DEREF:
+		if (ctx.state.reg_regnum[FP_IDX] > UINT_MAX)
+			return -EINVAL;
+		if (ctx.state.reg_offset[FP_IDX] < INT_MIN ||
+		    ctx.state.reg_offset[FP_IDX] > INT_MAX)
+			return -EOPNOTSUPP;
+		frame->fp.rule = UNWIND_USER_RULE_REG_OFFSET_DEREF;
+		frame->fp.regnum = ctx.state.reg_regnum[FP_IDX];
+		frame->fp.offset = ctx.state.reg_offset[FP_IDX];
+		break;
 	default:
 		return -EINVAL;
 	}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer()
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (21 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 24/25] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390 Jens Remus
                   ` (2 subsequent siblings)
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

On s390 64-bit the s390x ELF ABI [1] designates register 11 as the
"preferred" frame pointer (FP) register in user space.

[1]: s390x ELF ABI, https://github.com/IBM/s390x-abi/releases

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---

Notes (jremus):
    Cherry-picked from "[PATCH v4 00/12] s390: SFrame user space unwinding"
    series and adjusted to .eh_frame:
    https://lore.kernel.org/all/20260127151926.2805123-9-jremus@linux.ibm.com/

 arch/s390/include/asm/ptrace.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/arch/s390/include/asm/ptrace.h b/arch/s390/include/asm/ptrace.h
index 495e310c3d6d..85765979a595 100644
--- a/arch/s390/include/asm/ptrace.h
+++ b/arch/s390/include/asm/ptrace.h
@@ -255,6 +255,12 @@ static __always_inline unsigned long user_stack_pointer(const struct pt_regs *re
 	return regs->gprs[15];
 }
 
+static __always_inline unsigned long frame_pointer(const struct pt_regs *regs)
+{
+	/* Return ABI-designated "preferred" frame-pointer register value. */
+	return regs->gprs[11];
+}
+
 static __always_inline unsigned long regs_get_register(const struct pt_regs *regs,
 						       unsigned int offset)
 {
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 24/25] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (22 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 14:49 ` [RFC PATCH v1 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
  2026-08-18 17:21 ` [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Steven Rostedt
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

The unwind user eh_frame implementation works well with GCC and Clang
generated code on s390.  Enable it.

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 arch/s390/Kconfig                            |  1 +
 arch/s390/include/asm/unwind_user.h          | 71 ++++++++++++++++++++
 arch/s390/include/asm/unwind_user_eh_frame.h | 24 +++++++
 3 files changed, 96 insertions(+)
 create mode 100644 arch/s390/include/asm/unwind_user.h
 create mode 100644 arch/s390/include/asm/unwind_user_eh_frame.h

diff --git a/arch/s390/Kconfig b/arch/s390/Kconfig
index 84404e6778d5..fa229ac92d5a 100644
--- a/arch/s390/Kconfig
+++ b/arch/s390/Kconfig
@@ -251,6 +251,7 @@ config S390
 	select HAVE_SOFTIRQ_ON_OWN_STACK
 	select HAVE_STACKPROTECTOR if CC_HAS_STACKPROTECTOR_GLOBAL
 	select HAVE_SYSCALL_TRACEPOINTS
+	select HAVE_UNWIND_USER_EH_FRAME
 	select HAVE_VIRT_CPU_ACCOUNTING
 	select HAVE_VIRT_CPU_ACCOUNTING_IDLE
 	select HOTPLUG_SMT
diff --git a/arch/s390/include/asm/unwind_user.h b/arch/s390/include/asm/unwind_user.h
new file mode 100644
index 000000000000..41ff0b477626
--- /dev/null
+++ b/arch/s390/include/asm/unwind_user.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_S390_UNWIND_USER_H
+#define _ASM_S390_UNWIND_USER_H
+
+#include <linux/sched/task_stack.h>
+#include <linux/types.h>
+#include <asm/fpu.h>
+
+#ifdef CONFIG_UNWIND_USER
+
+static inline int unwind_user_word_size(struct pt_regs *regs)
+{
+	return 8;
+}
+
+static inline int unwind_user_get_ra_reg(unsigned long *val)
+{
+	struct pt_regs *regs = task_pt_regs(current);
+	*val = regs->gprs[14];
+	return 0;
+}
+#define unwind_user_get_ra_reg unwind_user_get_ra_reg
+
+static inline unsigned long __s390_dwarf_fpr_to_fpr(unsigned int regnum)
+{
+	unsigned int fpr;
+
+	/*
+	 * Convert from s390 DWARF floating-point register number (16..31)
+	 * to floating-point register number (0..15): left rotate the least
+	 * significant three bits and then return the least significant four
+	 * bits.
+	 */
+	fpr  = (regnum & 3) << 1;
+	fpr |= (regnum & 4) >> 2;
+	fpr |= (regnum & 8);
+	return fpr;
+}
+
+static inline unsigned long __s390_get_dwarf_fpr(unsigned int regnum)
+{
+	struct fpu *fpu = &current->thread.ufpu;
+
+	save_user_fpu_regs();
+	return fpu->vxrs[__s390_dwarf_fpr_to_fpr(regnum)].high;
+}
+
+static inline int unwind_user_get_reg(unsigned long *val, unsigned int regnum)
+{
+	if (regnum <= 15) {
+		/* DWARF register numbers 0..15 */
+		struct pt_regs *regs = task_pt_regs(current);
+		*val = regs->gprs[regnum];
+		return 0;
+	} else if (regnum <= 31) {
+		/* DWARF register numbers 16..31 */
+		*val = __s390_get_dwarf_fpr(regnum);
+		return 0;
+	}
+
+	pr_debug("%s (%d): %s(%u): unsupported register number\n",
+		 current->comm, current->pid, __func__, regnum);
+	return -EINVAL;
+}
+#define unwind_user_get_reg unwind_user_get_reg
+
+#endif /* CONFIG_UNWIND_USER */
+
+#include <asm-generic/unwind_user.h>
+
+#endif /* _ASM_S390_UNWIND_USER_H */
diff --git a/arch/s390/include/asm/unwind_user_eh_frame.h b/arch/s390/include/asm/unwind_user_eh_frame.h
new file mode 100644
index 000000000000..842d9f7c3d80
--- /dev/null
+++ b/arch/s390/include/asm/unwind_user_eh_frame.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _ASM_S390_UNWIND_USER_EH_FRAME_H
+#define _ASM_S390_UNWIND_USER_EH_FRAME_H
+
+#define EH_FRAME_REG_SP	15	/* designated stack pointer register */
+#define EH_FRAME_REG_FP	11	/* "preferred" frame pointer register */
+#define EH_FRAME_REG_RA	14	/* designaged return address register */
+
+/* Instructions must be 2-byte aligned */
+#define EH_FRAME_MAX_CODE_ALIGN 2
+
+/* CFA is defined as SP at call site + 160, so that SP = CFA - 160 */
+#define EH_FRAME_SP_VAL_OFFSET -160
+
+/* SP may be saved on the stack or in a register */
+static inline bool eh_frame_reject_sp_rule(void)
+{
+	return false;
+}
+#define eh_frame_reject_sp_rule eh_frame_reject_sp_rule
+
+#include <asm-generic/unwind_user_eh_frame.h>
+
+#endif /* _ASM_S390_UNWIND_USER_EH_FRAME_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* [RFC PATCH v1 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (23 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 24/25] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390 Jens Remus
@ 2026-08-18 14:49 ` Jens Remus
  2026-08-18 17:21 ` [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Steven Rostedt
  25 siblings, 0 replies; 27+ messages in thread
From: Jens Remus @ 2026-08-18 14:49 UTC (permalink / raw)
  To: linux-kernel, linux-trace-kernel, linux-s390, x86, Steven Rostedt,
	Josh Poimboeuf, Peter Zijlstra, Mathieu Desnoyers
  Cc: Jens Remus, Heiko Carstens, Vasily Gorbik, Alexander Gordeev,
	Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner, Ingo Molnar,
	Borislav Petkov, Dave Hansen, H. Peter Anvin, Namhyung Kim,
	Andrii Nakryiko, Kees Cook, Sam James

The kernel does not have direct visibility to the ELF contents of
shared libraries.  Add prctl() interfaces to register and unregister
.eh_frame_hdr sections that allow dynamic linkers to tell the kernel
where to find these.

Both prctl's take a pointer to a new structure:

  struct eh_frame_setup {
	__u64	eh_frame_hdr_start;
	__u64	eh_frame_hdr_size;
	__u64	text_start;
	__u64	text_size;
  };

and a size of the passed in structure.  If the prctl's need to be
extended, then the structure could be changes and the sizre of that
structure will tell the kernel what it is the new version.  If the
kernel does not recognize the structure, it returns -EINVAL.

  eh_frame_hdr_start - Virtual address of the .eh_frame_hdr section
  eh_frame_hdr_size  - Length of the .eh_frame_hdr section
  text_start         - Virtual address of the related text section
  text_size          - Length of the related text section

The unregister only needs the eh_frame_hdr_start and requires all of
the remaining fields to be zero.

Based on Steven's patch "[PATCH v2] unwind: Add sframe_(un)register()
system calls". [1]

[1]: https://lore.kernel.org/all/20260528151023.00f5ec4e@gandalf.local.home/

Signed-off-by: Jens Remus <jremus@linux.ibm.com>
---
 include/linux/eh_frame.h      | 20 +++++++++++++++
 include/uapi/linux/eh_frame.h | 14 +++++++++++
 include/uapi/linux/prctl.h    |  4 +++
 kernel/sys.c                  | 11 +++++++++
 kernel/unwind/eh_frame.c      | 46 +++++++++++++++++++++++++++++++++++
 5 files changed, 95 insertions(+)
 create mode 100644 include/uapi/linux/eh_frame.h

diff --git a/include/linux/eh_frame.h b/include/linux/eh_frame.h
index de68f21e1050..540beab5bf61 100644
--- a/include/linux/eh_frame.h
+++ b/include/linux/eh_frame.h
@@ -6,6 +6,8 @@
 #include <linux/srcu.h>
 #include <linux/unwind_user_types.h>
 
+struct eh_frame_setup;
+
 #ifdef CONFIG_HAVE_UNWIND_USER_EH_FRAME
 
 struct eh_frame_section {
@@ -39,6 +41,12 @@ extern int eh_frame_add_section(unsigned long eh_frame_hdr_start,
 				unsigned long text_start,
 				unsigned long text_end);
 extern int eh_frame_remove_section(unsigned long eh_frame_hdr_start);
+
+extern int eh_frame_register(struct eh_frame_setup __user *user_data,
+			     __kernel_size_t size);
+extern int eh_frame_unregister(struct eh_frame_setup __user *user_data,
+			       __kernel_size_t size);
+
 extern int eh_frame_find(unsigned long ip, struct unwind_user_frame *frame);
 
 static inline bool current_has_eh_frame(void)
@@ -77,6 +85,18 @@ static inline int eh_frame_find(unsigned long ip, struct unwind_user_frame *fram
 	return -ENOSYS;
 }
 
+static inline int eh_frame_register(struct eh_frame_setup __user *user_data,
+				    __kernel_size_t size)
+{
+	return -EINVAL;
+}
+
+static inline int eh_frame_unregister(struct eh_frame_setup __user *user_data,
+				      __kernel_size_t size)
+{
+	return -EINVAL;
+}
+
 static inline bool current_has_eh_frame(void) { return false; }
 
 #endif /* CONFIG_HAVE_UNWIND_USER_EH_FRAME */
diff --git a/include/uapi/linux/eh_frame.h b/include/uapi/linux/eh_frame.h
new file mode 100644
index 000000000000..8cca3792f323
--- /dev/null
+++ b/include/uapi/linux/eh_frame.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_LINUX_EH_FRAME_H
+#define _UAPI_LINUX_EH_FRAME_H
+
+#include <linux/types.h>
+
+struct eh_frame_setup {
+	__u64	eh_frame_hdr_start;
+	__u64	eh_frame_hdr_size;
+	__u64	text_start;
+	__u64	text_size;
+};
+
+#endif /* _UAPI_LINUX_EH_FRAME_H */
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index b6ec6f693719..801a209bab3f 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -416,4 +416,8 @@ struct prctl_mm_map {
 # define PR_CFI_DISABLE		_BITUL(1)
 # define PR_CFI_LOCK		_BITUL(2)
 
+/* EH_FRAME management */
+#define PR_REGISTER_EH_FRAME		82
+#define PR_UNREGISTER_EH_FRAME		83
+
 #endif /* _LINUX_PRCTL_H */
diff --git a/kernel/sys.c b/kernel/sys.c
index df69bd71de03..ee67badf267c 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -65,6 +65,7 @@
 #include <linux/rcupdate.h>
 #include <linux/uidgid.h>
 #include <linux/cred.h>
+#include <linux/eh_frame.h>
 
 #include <linux/nospec.h>
 
@@ -2907,6 +2908,16 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		if (arg3 & PR_CFI_LOCK && !(arg3 & PR_CFI_DISABLE))
 			error = arch_prctl_lock_branch_landing_pad_state(me);
 		break;
+	case PR_REGISTER_EH_FRAME:
+		if (arg4 || arg5)
+			return -EINVAL;
+		error = eh_frame_register((void __user *)arg2, arg3);
+		break;
+	case PR_UNREGISTER_EH_FRAME:
+		if (arg4 || arg5)
+			return -EINVAL;
+		error = eh_frame_unregister((void __user *)arg2, arg3);
+		break;
 	default:
 		trace_task_prctl_unknown(option, arg2, arg3, arg4, arg5);
 		error = -EINVAL;
diff --git a/kernel/unwind/eh_frame.c b/kernel/unwind/eh_frame.c
index 49e8a3e8d794..12279f02381d 100644
--- a/kernel/unwind/eh_frame.c
+++ b/kernel/unwind/eh_frame.c
@@ -15,6 +15,7 @@
 #include <linux/types.h>
 #include <linux/unwind_user_types.h>
 #include <linux/unwind_user_eh_frame_types.h>
+#include <uapi/linux/eh_frame.h>
 
 #include "eh_frame.h"
 #include "eh_frame_debug.h"
@@ -1723,3 +1724,48 @@ void eh_frame_free_mm(struct mm_struct *mm)
 
 	mtree_destroy(&mm->eh_frame_mt);
 }
+
+int eh_frame_register(struct eh_frame_setup __user *user_data, __kernel_size_t size)
+{
+	struct eh_frame_setup data;
+	unsigned long eh_frame_hdr_end, text_end;
+
+	if (!user_data && !size)
+		return -EINVAL;
+
+	if (size != sizeof(data))
+		return -EINVAL;
+
+	if (copy_from_user(&data, user_data, sizeof(data)))
+		return -EFAULT;
+
+	if (check_add_overflow(data.eh_frame_hdr_start, data.eh_frame_hdr_size,
+			       &eh_frame_hdr_end))
+		return -EINVAL;
+
+	if (check_add_overflow(data.text_start, data.text_size, &text_end))
+		return -EINVAL;
+
+	return eh_frame_add_section(data.eh_frame_hdr_start, eh_frame_hdr_end,
+				    data.text_start, text_end);
+}
+
+int eh_frame_unregister(struct eh_frame_setup __user *user_data, __kernel_size_t size)
+{
+	struct eh_frame_setup data;
+
+	if (!user_data && !size)
+		return -EINVAL;
+
+	if (size != sizeof(data))
+		return -EINVAL;
+
+	if (copy_from_user(&data, user_data, sizeof(data)))
+		return -EFAULT;
+
+	/* Unregister only uses eh_frame_hdr_start */
+	if (data.eh_frame_hdr_size || data.text_start || data.text_size)
+		return -EINVAL;
+
+	return eh_frame_remove_section(data.eh_frame_hdr_start);
+}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 27+ messages in thread

* Re: [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling
  2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
                   ` (24 preceding siblings ...)
  2026-08-18 14:49 ` [RFC PATCH v1 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
@ 2026-08-18 17:21 ` Steven Rostedt
  25 siblings, 0 replies; 27+ messages in thread
From: Steven Rostedt @ 2026-08-18 17:21 UTC (permalink / raw)
  To: Jens Remus
  Cc: linux-kernel, linux-trace-kernel, linux-s390, x86, Josh Poimboeuf,
	Peter Zijlstra, Mathieu Desnoyers, Heiko Carstens, Vasily Gorbik,
	Alexander Gordeev, Ilya Leoshkevich, Indu Bhagat, Thomas Gleixner,
	Ingo Molnar, Borislav Petkov, Dave Hansen, H. Peter Anvin,
	Namhyung Kim, Andrii Nakryiko, Kees Cook, Sam James

On Tue, 18 Aug 2026 16:49:29 +0200
Jens Remus <jremus@linux.ibm.com> wrote:

> This series adds support for parsing DWARF Call Frame Information (CFI)
> from the .eh_frame_hdr and .eh_frame sections of user space ELF files.

Very nice! I don't have time this week to look at it, but I just
applied the series and tried it out with:

  perf record --call-graph fp,defer trace-cmd report ~/trace.dat

And did a: perf -D script

And found this:

264990534603 0x19370 [0x78]: PERF_RECORD_CALLCHAIN_DEFERRED(IP, 0x2): 1165/1165: 0x105700000005
... FP chain: nr:10
.....  0: 00007f5feafd362a
.....  1: 00007f5feb249781
.....  2: 00007f5feb2498de
.....  3: 000055935526fbdc
.....  4: 00005593552649e6
.....  5: 000055935523c2d0
.....  6: 000055935523d24c
.....  7: 0000559355227bfe
.....  8: 00007f5feaef9f75
.....  9: 00007ffdaf879d38
 ... thread: trace-cmd:1165
 ...... dso: /proc/kcore
trace-cmd    1165   264.990473:     395990 cpu/cycles/P: 
        ffffffff8477d8d3 check_preemption_disabled+0x13 ([kernel.kallsyms])
        ffffffff819aa495 rcu_is_watching+0x15 ([kernel.kallsyms])
        ffffffff816dbcff unwind_next_frame+0x45f ([kernel.kallsyms])
        ffffffff81642e91 arch_stack_walk+0xa1 ([kernel.kallsyms])
        ffffffff81a2e633 stack_trace_save+0x93 ([kernel.kallsyms])
        ffffffff82251210 kasan_save_stack+0x30 ([kernel.kallsyms])
        ffffffff822541b0 kasan_record_aux_stack+0xb0 ([kernel.kallsyms])
        ffffffff819c1e2a __call_rcu_common+0xca ([kernel.kallsyms])
        ffffffff821be05f kmem_cache_free+0x2ef ([kernel.kallsyms])
        ffffffff8233e4a7 vfs_fstatat+0x57 ([kernel.kallsyms])
        ffffffff8233e573 __do_sys_newfstatat+0x83 ([kernel.kallsyms])
        ffffffff84772a8e do_syscall_64+0x7e ([kernel.kallsyms])
        ffffffff8100012f entry_SYSCALL_64_after_hwframe+0x76 ([kernel.kallsyms])
            7f5feafd362a __GI___fstatat64+0xa (/usr/lib/x86_64-linux-gnu/libc.so.6)
            7f5feb249781 tep_load_plugins_hook+0xd1 (/usr/local/lib64/libtraceevent.so.1.9.0)
            7f5feb2498de tep_load_plugins+0x35 (/usr/local/lib64/libtraceevent.so.1.9.0)
            55935526fbdc tcmd_load_plugins+0x85 (/usr/local/bin/trace-cmd)
            5593552649e6 tracecmd_alloc_fd+0x2da (/usr/local/bin/trace-cmd)
            55935523c2d0 read_trace_header+0x62 (/usr/local/bin/trace-cmd)
            55935523d24c trace_report+0x869 (/usr/local/bin/trace-cmd)
            559355227bfe main+0x90 (/usr/local/bin/trace-cmd)
            7f5feaef9f75 __libc_start_call_main+0x75 (/usr/lib/x86_64-linux-gnu/libc.so.6)
            7ffdaf879d38 [unknown] ([unknown])

I injected trace_printk() into the code to make sure it was using the
eh_frame unwinding:

diff --git a/kernel/unwind/user.c b/kernel/unwind/user.c
index 85fc82252af1..585f022bcabe 100644
--- a/kernel/unwind/user.c
+++ b/kernel/unwind/user.c
@@ -200,6 +200,7 @@ static int unwind_user_next(struct unwind_user_state *state)
 		case UNWIND_USER_TYPE_EH_FRAME:
 			switch (unwind_user_next_eh_frame(state)) {
 			case 0:
+				trace_printk("USE EH_FRAME\n");
 				return 0;
 			case -ENOENT:
 				continue;	/* Try next method. */
@@ -208,8 +209,9 @@ static int unwind_user_next(struct unwind_user_state *state)
 			}
 			break;
 		case UNWIND_USER_TYPE_FP:
-			if (!unwind_user_next_fp(state))
-				return 0;
+			if (!unwind_user_next_fp(state)) {
+				trace_printk("USE FRAME POINTER\n");
+				return 0; }
 			continue;
 		default:
 			WARN_ONCE(1, "Undefined unwind bit %d", bit);

And have this:

# trace-cmd show
[..]
       trace-cmd-1136    [001] .....   239.482498: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482501: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482504: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482507: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482821: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482825: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482828: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482831: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.482834: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483004: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483007: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483010: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483013: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483016: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483230: unwind_user: USE FRAME POINTER
       trace-cmd-1136    [001] .....   239.483234: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483238: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483240: unwind_user: USE EH_FRAME
       trace-cmd-1136    [001] .....   239.483243: unwind_user: USE EH_FRAME
[..]

Note, the compile had one warning (with all patches applied):

  vmlinux.o: warning: objtool: eh_frame_find+0x270c: call to eh_frame_do_expression.isra.0() with UACCESS enabled

I'll try to get time when I get back from my travels next week to look
at each of the patches.

I also do not think this makes sframe obsolete. I believe there's holes
with eh_frame and some may not like the complexity of it. But this
gives us an honest look at what options are available to us.

Thanks for doing this!

-- Steve

^ permalink raw reply related	[flat|nested] 27+ messages in thread

end of thread, other threads:[~2026-08-18 17:21 UTC | newest]

Thread overview: 27+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 14:49 [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 01/25] unwind_user: Add generic and arch-specific headers to MAINTAINERS Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 02/25] unwind_user: Stop when reaching an outermost frame Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 03/25] unwind_user: Enable archs that pass RA in a register Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 04/25] unwind_user: Flexible FP/RA recovery rules Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 05/25] unwind_user: Flexible CFA " Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 06/25] unwind_user: Enable archs that define CFA = SP_callsite + offset Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 07/25] unwind_user/eh_frame: Add support for reading .eh_frame_hdr section Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 08/25] unwind_user/eh_frame: Store .eh_frame_hdr section data in per-mm maple tree Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 09/25] unwind_user/eh_frame: Add support for reading .eh_frame section Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 10/25] unwind_user/eh_frame: Detect .eh_frame_hdr sections in executables Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 11/25] unwind_user/eh_frame: Wire up unwind_user to eh_frame Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 12/25] unwind_user/eh_frame: Remove .eh_frame[_hdr] section on detected corruption Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 13/25] unwind_user/eh_frame: Show file name in debug output Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 14/25] unwind_user/eh_frame: Add .eh_frame[_hdr] validation option Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 15/25] unwind_user/eh_frame: Duplicate registered .eh_frame[_hdr] section data on clone/fork Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 16/25] unwind_user/eh_frame: Add linear .eh_frame search fallback Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 17/25] unwind_user/eh_frame: Ignore DW_CFA_GNU_args_size Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 18/25] unwind_user/eh_frame: Add support for DWARF expressions Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 19/25] x86/uaccess: Add unsafe_copy_from_user() implementation Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 20/25] unwind_user/eh_frame/x86: Enable eh_frame unwinding on x86 Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 21/25] unwind_user/eh_frame/x86: Handle PLT expressions Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 22/25] unwind_user/eh_frame/x86: Handle DRAP expressions Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 23/25] s390/ptrace: Provide frame_pointer() Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 24/25] unwind_user/eh_frame/s390: Enable eh_frame unwinding on s390 Jens Remus
2026-08-18 14:49 ` [RFC PATCH v1 25/25] unwind_user/eh_frame: Add prctl() interface for (un)registering .eh_frame_hdr sections Jens Remus
2026-08-18 17:21 ` [RFC PATCH v1 00/25] unwind_user: Implement .eh_frame handling Steven Rostedt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox